Michal Hořejšek blog - codehttps://blog.horejsek.com/2023-10-03T09:00:00+00:00Software Development vs. Manual Work2023-10-03T09:00:00+00:002023-10-03T09:00:00+00:00Michal Hořejšektag:blog.horejsek.com,2023-10-03:/software-development-vs-manualwork<p>Our body is a machine that focuses on preserving energy as much as possible. That’s why we love doing nothing so much. 21st century, with all the technological improvements, is generous to keep us doing exactly that, just staring at the screens. <a href="/why-do-we-exercise/">It is unsuitable for us</a>, of course …</p><p>Our body is a machine that focuses on preserving energy as much as possible. That’s why we love doing nothing so much. 21st century, with all the technological improvements, is generous to keep us doing exactly that, just staring at the screens. <a href="/why-do-we-exercise/">It is unsuitable for us</a>, of course. But tell that to anyone working in the office eight or more hours a day.</p>
<p>I think we forgot our body is not just a machine but a biological machine. We evolved a lot over time to fit the environment around us. Until recently, that was a time without the internet and technological gadgets in our pockets. We had to work hard to keep up. It was no bed of roses.</p>
<p>Probably everyone, including all the animals in this <em>cruel</em> world, would instead give up. Maybe that even happened at the very beginning; who knows? But life found a way to go on by implementing motivation. We have all kinds of hormones triggered in proper time so we feel satisfaction we need to go on.</p>
<p>You might already guess that our machine is fine-tuned for the environment where we “grow up.” Yet we live in <a href="/mismatched-environment/">another one</a> we mainly shaped only in the past century or two. For example, software engineers sit calmly in their chairs all day with many health risks (obesity, diabetes, back pain, eye issues, depression, burnout, etc.).</p>
<p>Why is that? What are we missing?</p>
<p>I think the most crucial detail is that software development is not physical enough, and thus, it is trickier to detect we are beyond our limits.</p>
<p>Consider building something big manually. If I’m fresh enough, I can imagine working very hard from sunrise to sunset for a few days. But that’s it. Then I need several relaxing days to recover. By several, I mean at least the same amount of days as the work itself, but usually more. My body cannot exceed some physical limits, and I get a clear message about that.</p>
<p>Whereas in software engineering, we are used to <a href="/software-development-is-not-healthy/">chaotic fast pace that never ends</a>. We are asked to run fast for a short time, which is OK, but it is too late before we find out we are sprinting several marathons in a row. By that time, we have built lousy software that will require a lot of love to make things right, and it has to be done by someone else because we have burned out in the meantime.</p>
<p>Consider this: you cannot move as your legs are no longer working after running an actual marathon without any preparation. There is nothing you can do but stop and wait till your body recovers from this crazy achievement. As a software engineer, you take energy drinks.</p>
<p>I believe one of the problems with software is low or complete lack of satisfaction. You cannot touch or see it, happy hormones are not released (due to lack of physical activity), and everyone is just pushing to do more.</p>
<p>For example, when working on my cottage, I didn’t have to push myself even if progress was way below what I imagined. Surely I had days when I was sad because nothing moved on due to various unexpected situations. Still, more often, I felt a pleasant feeling in my body after hard work, and I could observe how my project was slowly taking shape in front of me. I was proud of every single shape.</p>
<p>I don’t want to say this is completely missing in software development. At least I feel good about some pieces of code, too. But that feeling is there only at the moment I commit it, and then it’s gone. The backlog is already calling for the next task.</p>
<p>I think another problem is no room for meditation. Since the beginning of my career, I have liked working on very complex and tricky challenges and also on very dumb manual straightforward tasks. The first requires my entire active mental presence, and the latter is excellent for turning my brain off and repeating what I could do even if you wake me in the middle of the night.</p>
<p>Dumb tasks might be fantastic for feeling similar sensations like during meditating. Meditation is an essential part of life to stay sane. I never considered it applying at work, though. I was surprised when I saw my neighbor was actually <em>meditating</em> on the job.</p>
<p>I wanted to fix some parts of my walls quickly and move on. I asked my neighbor. He agreed to help, but he didn’t care about half-job. He worked slowly and precisely and cared about the whole thing, not just some holes. It is a dumb task for him as he has been doing it for innumerable years. At some moments, it looked like the work of the artist. Other times it looked like he was meditating. The result was great, and he left with a happy feeling of satisfactory work.</p>
<p>Wouldn’t it be great if people in software development slowed down, produced better output, and enjoyed the work and result?</p>Software Development is Not Healthy2023-07-22T09:00:00+00:002023-07-22T09:00:00+00:00Michal Hořejšektag:blog.horejsek.com,2023-07-22:/software-development-is-not-healthy<p>Software development is complicated, at least in cases where a computer programmer and product manager are not the same person, which is the usual setup. There is a CEO or someone else with vision who then asks the team to build it.</p>
<p>The main problem is planning. When you plan …</p><p>Software development is complicated, at least in cases where a computer programmer and product manager are not the same person, which is the usual setup. There is a CEO or someone else with vision who then asks the team to build it.</p>
<p>The main problem is planning. When you plan stuff, everything seems so easy. You have a clear picture in your mind, you see the shapes, and you expect it to be done in an hour, a day, or over a relaxed weekend. It is not that easy for the other person. You can take it for granted that tons of small or big details will slow everything down. This fact doesn't make product owners happy, of course.</p>
<p>There are two ways to overcome the problem. The first one is a waterfall. It made sense initially when the software development began. The idea is to plan hard to find all the details beforehand and thus have a clear and robust plan. Meaning you end up months, if not years, planning big projects, which you can throw away because the goal is outdated before even development starts.</p>
<p>That's why some agile framework is the most common way to handle software projects—often scrum or kanban. The agile movement was initiated by people writing code because project managers don't understand how development works, so only software developers can bring practical alternatives.</p>
<p>Today, product still doesn't understand it, and some developers hate it (even though they apply Agile without knowing it). But the concept is straightforward. In the beginning, you have no idea. Tons of unknowns on both sides. That's why the work is divided into short cycles. Every cycle improves the estimation of when the project can be delivered. It is not a single date but a range that gets shorter and clearer over time.</p>
<p>The product then can use this information to adjust backlog accordingly to needs. Maybe something is more important to do sooner? Or something less critical but easy to do to please the client in the meantime? But the most important is that everyone understands when the project can be delivered, and there are precisely two options to control it: either there is a hard deadline, and thus scope is variable, or there is fixed scope, and therefore missing deadline. This is essential: either deadline or scope is variable; both cannot be set.</p>
<p>Unfortunately, only a few people truly understand this. We still can often see both scope and deadline set in stone. But to succeed, the project would need extensive planning, and we are back to the waterfall.</p>
<p>And that's the whole point of how software development becomes not healthy. Project managers accept agile but still push for deadlines <em>and</em> scope. If developers agree to those terms, they need to run like crazy. Everything is always late, there is no time to do anything properly, and tech debt is increasing; due to that, everything is even more lagging, the product is furious, and developers work even harder until they cannot work anymore.</p>
<p>The product starts micromanaging developers and their work to ensure everyone focuses on the most critical parts. That breaks good practices and team spirit even more and harms the project long-term (if there is still something to damage). No one knows what should be done precisely anymore. Therefore product needs to be present for every single decision. In such a situation, it is typical that even the short sprints are broken as the product happily changes direction (focus) anytime.</p>
<p>Over time, suddenly, someone who doesn't understand development is deciding how it is done. Professional developers are ignored, and they either suffer or instead go away. Less skilled team members, on the other hand, have a hard time learning craftsmanship. Both are at high risk of burnout.</p>
<p>Production code is full of unfinished buggy solutions which should never have been released in the first place. There was no chance it could be done, but it was done anyway. Marketing covers it with likable design and words, but it hardly hides the actual situation from the users.</p>
<p>Imagine this is how aviation or health care or construction is managed. We would have one catastrophe after another. But this is how regular web development works. No wonder tons of unusable products are out there, even from big tech.</p>
<p>Why are we still chasing unrealistic goals?</p>Scrum vs Kanban → Scrum + Kanban2023-02-25T08:00:00+00:002023-02-25T08:00:00+00:00Michal Hořejšektag:blog.horejsek.com,2023-02-25:/scrum-kanban<p>Working on a project alone is easy. You are all parties in one person, and you can make a decision efficiently at every step. Having two or three people on a team already requires some syncs, yet nothing fancy is required. With a bigger group, you already need to ask …</p><p>Working on a project alone is easy. You are all parties in one person, and you can make a decision efficiently at every step. Having two or three people on a team already requires some syncs, yet nothing fancy is required. With a bigger group, you already need to ask how to keep efficiently in sync.</p>
<p>Agile has been the main direction of the past decade. It can mean a lot of things to many people. Some can call it a swear word; others might not imagine working in any other way. For someone like me, I like to have an idea and slowly see where it takes me because, from my experience, the only constant is a change.</p>
<p>The question then comes to what agile framework to choose from. One of the most famous ones is probably Scrum. Everyone is used to having stand-ups, grooming, planning, and probably the least popular retrospectives in the past. It is more common to do those meetings just because it is written in the book than for practical purposes.</p>
<p>That's why sometimes teams go to Kanban, which seems, on first look, to be less filled with organizational work. I don't see it that way, though. It is a different type of flow emphasizing some other parts.</p>
<p>Scrum and Kanban are not the only agile frameworks. When neither of those works, teams can either look for something else or make a hybrid called Scrumban. But first, we need to figure out: what doesn't work?</p>
<p>Most of the time, in my experience, a product is composed of two very different kinds of work. Implementing new features or changing them, and fixing bugs or doing unpredictable work.</p>
<p>With Scrum, it is easy to work on features and plan releases, but a super inflexible to work on hotfixes. Trying to estimate bugs doesn't work, and hopping on new bug changes the whole sprint. On the other hand, with Kanban, it is natural to work on the most critical task, one or two at a time. Everyday priorities can change, but don't try to plan anything long term.</p>
<p>The problem is you need both unless you work on an entirely new product that has not yet been released to anyone or a mature product in maintenance mode without any further development. So what can you do?</p>
<p>I don't think Scrum, Kanban, or even Scrumban will help. I believe that combination (not hybrid) is for the rescue. The slight problem is you need a bigger team for that. I think it makes sense with at least three, ideally four, people on the team.</p>
<p>The idea is to keep Scrum for work you can plan and choose who is working on maintenance with the Kanban board. The team's velocity is then not four (for the four-member team) but three (if one person for maintenance is enough). Three people then work on nicely estimated tickets, or maybe time-boxed tasks (such as spikes, designing a new feature, proof of concept, breaking down big epic into reasonable tasks, and so on), while one person is processing unplanned or hardly planned work (triaging bugs, checking bug reports, logs, other monitoring, working on fixes, and providing answers for ad-hoc questions).</p>
<p>The next sprint maintenance role rotates to someone else, or the same person can handle two or even more sprints in a row. It depends on personality and other factors (it might not make sense for someone working on a feature spanned over several sprints to interrupt the work in the middle).</p>
<p>If one person cannot handle maintenance, the team can change the split to two:two, for example. In bigger groups, it might be natural that six people can work on features while two guard the walls.</p>
<p>The hard part might be how to work with such a split. Well, if you are still working from the office and the whole team is in one spot (lucky you!), you can mount a giant board on the wall. Make a thick line in the middle and have a feature on the top and maintenance on the bottom. In the end, both workflows need (usually) the same columns, and the only difference is what is on the board and how you approach the tickets.</p>
<p>With the remote type of work, I have fine enough experience with Jira. Every project can have as many boards as you please, and every board can have its own filters. So I have two boards, one to include only bugs, the other to include everything else. The first board uses Kanban type, while the second is Scrum.</p>
<p>For me, that works very well and solves all the problems. The team always has room to work on unplanned tickets and fix serious bugs as soon as possible, while planned important releases are not affected.</p>Code Coverage - Yes or No?2022-12-14T17:00:00+00:002022-12-14T17:00:00+00:00Michal Hořejšektag:blog.horejsek.com,2022-12-14:/code-coverage-yes-or-no<p>If you don’t know what code coverage is, lucky you! I think it can stay that way, but in short: code coverage gives you a percentage of what portion of the codebase is covered with tests. In case you are not also aware of what a test is, an …</p><p>If you don’t know what code coverage is, lucky you! I think it can stay that way, but in short: code coverage gives you a percentage of what portion of the codebase is covered with tests. In case you are not also aware of what a test is, an entirely new world is ahead of you!</p>
<p>Seriously. Tests are vital for sure. I’m not sure that TDD (test-driven development) is that big deal, to be honest. I use it in some cases, but not in all. It’s one of the tools we devs can use but is not the appropriate solution for every case. And I argue that code coverage is also a friendly tool, which can be used, but not everywhere automatically. For sure, code coverage should not be tracked by management.</p>
<p>But let’s go slowly. Why do I think that? Where are the weaknesses?</p>
<p>I trust that code coverage can be well measured for unit tests, but it’s a different story when integration tests come on stage. Of course, it will depend on your system and the tool’s abilities. Sometimes it can track integration tests as well, but often it will not work that nicely. Mostly when integration tests are not even part of the repository. End-to-end testing, like using Webdriver for websites, is even more problematic. I haven’t seen any tool to cope with that.</p>
<p>We could argue that more essential tests are unit ones anyway, so it’s okay if we focus on them. I believe if there is a way to write a unit test instead of another type of test, it’s better to use it. But in some cases, the end-to-end test is just more suitable. Even though such tests are usually less stable, they might provide better results with less complexity.</p>
<p>To be clear, I don’t want to suggest that not writing tests is better. Quite the contrary, I find tests helpful. I’m trying to say that tests should work for us, not against us. There is no point in having 100% coverage in unit tests when we could cover some parts with simpler end-to-end tests. The time needed to maintain tests should also be counted in deciding what type of coverage is necessary.</p>
<p>Probably the most problematic issue is that no tool can tell me what makes sense to cover. The 80-20 rule can be applied to software: 80% of the work is done by 20% of the code. From my experience, this rule is not far from the truth. That means having 50% coverage is a pretty astonishing number already! The question is: is it the proper 50%?</p>
<p>This question is hard to answer; tools might help but usually don’t. And I have never seen anyone (who cared about percentage) honestly care what percentage is covered. Therefore I think it’s better not to track it anyhow and absolutely not force developers always to increase it. Because writing tests for not crucial parts is easier in most cases, you will end up with tons of tests that are challenging to maintain and not needed at all or could be replaced with one integration test.</p>
<p>Here is what I find helpful about code coverage tools: it gives you not only what percentage is covered but also what files or functions are not. If you are unsure what is covered and what is not in your module and don’t want to miss anything, this is a fantastic tool!</p>
<p>Also, it might be helpful if you have a huge app and look into what is covered. But be careful. Always consult your gut about the most critical part and focus on those uncovered parts. There is not much point in spending valuable time on something minor.</p>
<p>To summarize, running code coverage to track the percentage in your pipelines after every commit wastes resources. But keep the tool around for ad-hoc cases so developers can manually run it to have data for a potentially better decision on where to spend more time in a beautiful world called testing, which makes our lives less stressful.</p>
<p>Writing tests should relax your stress, not the other way around.</p>Arduino Weather Station: Preserving Data2022-10-04T18:00:00+00:002022-10-04T18:00:00+00:00Michal Hořejšektag:blog.horejsek.com,2022-10-04:/weather-statition-preserving-data<p>After <a href="/weather-station-hello-world/">hello world</a> and <a href="/weather-station-collecting-data/">using Wi-Fi to send data</a>, we should take a look at where we actually store the data. An overwhelming number of online tutorials suggest using some already existing services. But where is the fun in doing that?</p>
<h4>Database</h4>
<p>There are so many databases you could choose …</p><p>After <a href="/weather-station-hello-world/">hello world</a> and <a href="/weather-station-collecting-data/">using Wi-Fi to send data</a>, we should take a look at where we actually store the data. An overwhelming number of online tutorials suggest using some already existing services. But where is the fun in doing that?</p>
<h4>Database</h4>
<p>There are so many databases you could choose for your project. You could choose any of the classical relational ones, but that is quite clumsy for a project like this. Or maybe something more modern like NoSQL, mainly if you plan to collect an unreasonable high load of data!</p>
<p>For example, measuring temperature several times every second. It is possible, of course, but pointless. At least with cheap modules, the precision is not super great, and who needs micro changes every second? I would argue once a minute is already quite more than enough.</p>
<p>Remember: there are so many databases because there are so many needs. No database can be optimized for every use case. Therefore it is crucial to ask yourself the question, what is the primary requirement? For the weather station, it’s collecting the same data changing over time and plots them in a nice graph. The main point here is the time. And for that, time-series databases exist!</p>
<p>My favorite, I guess, is InfluxDB. It is open source, fast (even though that is not our concern here), it has tons of options to connect to it (including several pre-made libraries for many languages!), and, mostly, it has a configurable web interface!</p>
<p>Technically, you can set up the InfluxDB instance, redirect your weather station to it with a library for Arduino, and prepare friendly dashboards. That's it.</p>
<h4>Server</h4>
<p>Unless you want a bit more. For example, last time, I said it is best to move as much work from the clients to the server. That is one of the reasons why I created my own server communicating with InfluxDB instead. The library is pleasant but still a bit more complex than what I can achieve with a simple request.</p>
<p>The main reason is compatibility, though. With one weather station, it is okay, kind of. But I already have five in many locations and may have even more of them in the future. If I needed to upgrade all of them simultaneously, it would be a trip for the whole day. Like this, I can keep it simple for my stations to make the API stable, and all the software kung-fu is done on the server side only.</p>
<p>Another minor reason is that I don't trust anything by default. Because few stations are outside my home network already, I don't want to expose InfluxDB to the world. I uncover only a minimal custom API that can do almost nothing because there is nearly nothing. If there is any severe security bug in InfluxDB, it should almost not concern me. It's actually quite more important than the practical reason above, but I also run it in its container that even if someone gets to InfluxDB, there is only that. So <em>only</em> weather data would be leaked (and maybe lost until I find the latest backup).</p>
<p>What did I choose for my web server? I love Python and Haskell, but I went with Go. Even though functional programming and typing are great, it would slow me down here. With Python, I could have it done in no time, but packing it is a bit tricky. In the end, writing such a simple server in Go is also simple, and I can build one executable containing everything to execute it on the server.</p>
<div class="highlight"><pre><span></span><code><span class="kd">func</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">http</span><span class="p">.</span><span class="nx">HandleFunc</span><span class="p">(</span><span class="s">"/write"</span><span class="p">,</span> <span class="nx">errorMiddleware</span><span class="p">(</span><span class="nx">handleWrite</span><span class="p">))</span>
<span class="nx">log</span><span class="p">.</span><span class="nx">Fatal</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">ListenAndServe</span><span class="p">(</span><span class="s">":8000"</span><span class="p">,</span> <span class="kc">nil</span><span class="p">))</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nx">errorMiddleware</span><span class="p">(</span><span class="nx">handler</span> <span class="kd">func</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="kt">error</span><span class="p">)</span> <span class="kd">func</span><span class="p">(</span><span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">err</span> <span class="o">:=</span> <span class="nx">handler</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span>
<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
<span class="nx">log</span><span class="p">.</span><span class="nx">Warn</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
<span class="nx">http</span><span class="p">.</span><span class="nx">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nx">Sprint</span><span class="p">(</span><span class="nx">err</span><span class="p">),</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusBadRequest</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nx">handleWrite</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
<span class="nx">location</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">getParamString</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="s">"loc"</span><span class="p">)</span>
<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">err</span>
<span class="p">}</span>
<span class="nx">temperature</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">getParamFloat</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="s">"temp"</span><span class="p">)</span>
<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
<span class="nx">log</span><span class="p">.</span><span class="nx">Error</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">logTemperature</span><span class="p">(</span><span class="nx">location</span><span class="p">,</span> <span class="nx">temperature</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="nx">log</span><span class="p">.</span><span class="nx">WithFields</span><span class="p">(</span><span class="nx">log</span><span class="p">.</span><span class="nx">Fields</span><span class="p">{</span>
<span class="s">"location"</span><span class="p">:</span> <span class="nx">location</span><span class="p">,</span>
<span class="s">"temperature"</span><span class="p">:</span> <span class="nx">temperature</span><span class="p">,</span>
<span class="c1">// ...</span>
<span class="p">}).</span><span class="nx">Info</span><span class="p">(</span><span class="s">"Got data"</span><span class="p">)</span>
<span class="k">return</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nx">getParamFloat</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">,</span> <span class="nx">param</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">float64</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">value</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">getParamString</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">,</span> <span class="nx">param</span><span class="p">)</span>
<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">err</span>
<span class="p">}</span>
<span class="nx">number</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">strconv</span><span class="p">.</span><span class="nx">ParseFloat</span><span class="p">(</span><span class="nx">value</span><span class="p">,</span> <span class="mi">8</span><span class="p">)</span>
<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
<span class="k">return</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nx">Errorf</span><span class="p">(</span><span class="s">"%s not a number: %s"</span><span class="p">,</span> <span class="nx">param</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">number</span><span class="p">,</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nx">getParamString</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">,</span> <span class="nx">param</span> <span class="kt">string</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">values</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nx">Query</span><span class="p">()[</span><span class="nx">param</span><span class="p">]</span>
<span class="k">if</span> <span class="p">!</span><span class="nx">ok</span> <span class="p">{</span>
<span class="k">return</span> <span class="s">""</span><span class="p">,</span> <span class="nx">fmt</span><span class="p">.</span><span class="nx">Errorf</span><span class="p">(</span><span class="s">"%s not set"</span><span class="p">,</span> <span class="nx">param</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">values</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nx">logTemperature</span><span class="p">(</span><span class="nx">location</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">value</span> <span class="kt">float64</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">logData</span><span class="p">(</span><span class="nx">location</span><span class="p">,</span> <span class="s">"temperature"</span><span class="p">,</span> <span class="s">"htu"</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="kd">func</span> <span class="nx">logData</span><span class="p">(</span><span class="nx">location</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">sensor</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">key</span> <span class="kt">string</span><span class="p">,</span> <span class="nx">value</span> <span class="kt">float64</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">logger</span> <span class="o">:=</span> <span class="nx">log</span><span class="p">.</span><span class="nx">WithFields</span><span class="p">(</span><span class="nx">log</span><span class="p">.</span><span class="nx">Fields</span><span class="p">{</span>
<span class="s">"location"</span><span class="p">:</span> <span class="nx">location</span><span class="p">,</span>
<span class="s">"sensor"</span><span class="p">:</span> <span class="nx">sensor</span><span class="p">,</span>
<span class="s">"value"</span><span class="p">:</span> <span class="nx">value</span><span class="p">,</span>
<span class="p">})</span>
<span class="nx">url</span> <span class="o">:=</span> <span class="s">"https://INFLUX_HOST/api/v2/write?org=home&bucket=home&precision=s"</span>
<span class="nx">payload</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nx">Sprintf</span><span class="p">(</span><span class="s">"%s,host=%s %s=%f"</span><span class="p">,</span> <span class="nx">sensor</span><span class="p">,</span> <span class="nx">location</span><span class="p">,</span> <span class="nx">key</span><span class="p">,</span> <span class="nx">value</span><span class="p">)</span>
<span class="nx">req</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">NewRequest</span><span class="p">(</span><span class="s">"POST"</span><span class="p">,</span> <span class="nx">url</span><span class="p">,</span> <span class="nx">strings</span><span class="p">.</span><span class="nx">NewReader</span><span class="p">(</span><span class="nx">payload</span><span class="p">))</span>
<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">Error</span><span class="p">(</span><span class="s">"Failed to make a request"</span><span class="p">)</span>
<span class="k">return</span>
<span class="p">}</span>
<span class="nx">req</span><span class="p">.</span><span class="nx">Header</span><span class="p">.</span><span class="nx">Add</span><span class="p">(</span><span class="s">"Authorization"</span><span class="p">,</span> <span class="s">"TOKEN"</span><span class="p">)</span>
<span class="nx">client</span> <span class="o">:=</span> <span class="o">&</span><span class="nx">http</span><span class="p">.</span><span class="nx">Client</span><span class="p">{}</span>
<span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">client</span><span class="p">.</span><span class="nx">Do</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span>
<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
<span class="nx">logger</span><span class="p">.</span><span class="nx">Error</span><span class="p">(</span><span class="s">"Failed to log data"</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<h4>Heat index</h4>
<p>Now you have all the building blocks to make your cool weather station a reality. Except, it can be better. For example, the temperature is one thing, but the real feel depends on many more variables, notably humidity. Outdoor also on wind speed, but I care primarily about indoor and during warm temperatures. For that, the heat index is the best to calculate the <em>real</em> temperature and also show warnings when it goes above a comfortable level.</p>
<p>Computing heat index is quite complex. You can check out the beast <a href="https://en.wikipedia.org/wiki/Heat_index#Formula">formula on Wikipedia</a>. Converting it into code is not super straightforward, and the good thing is, <a href="https://www.influxdata.com/blog/hiding-complexity-with-custom-functions-calculating-heat-index/">someone already did it for InfluxDB</a>! There were mistakes, but still, this helped a lot. If you want to use also Go server for presenting data as I did, you could use my function (I have defined a few structs representing my measurements and periods, which you should not need and it should be clear with what values to replace my variables):</p>
<div class="highlight"><pre><span></span><code><span class="kd">func</span> <span class="nx">buildQueryHeatIndex</span><span class="p">(</span><span class="nx">period</span> <span class="nx">period</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// https://www.influxdata.com/blog/hiding-complexity-with-custom-functions-calculating-heat-index/</span>
<span class="nx">query</span> <span class="o">:=</span> <span class="nx">fmt</span><span class="p">.</span><span class="nx">Sprintf</span><span class="p">(</span><span class="s">`</span>
<span class="s"> import "math"</span>
<span class="s"> temperature = from(bucket: "%s")</span>
<span class="s"> |> range(start: %s, stop: %s)</span>
<span class="s"> |> filter(fn: (r) => r._measurement == "%s" and r["_field"] == "%s")</span>
<span class="s"> |> aggregateWindow(every: %s, fn: mean)</span>
<span class="s"> |> keep(columns: ["_value", "_time", "host"])</span>
<span class="s"> humidity = from(bucket: "%s")</span>
<span class="s"> |> range(start: %s, stop: %s)</span>
<span class="s"> |> filter(fn: (r) => r._measurement == "%s" and r["_field"] == "%s")</span>
<span class="s"> |> aggregateWindow(every: %s, fn: mean)</span>
<span class="s"> |> keep(columns: ["_value", "_time", "host"])</span>
<span class="s"> first_join = join(tables: {temperature: temperature, humidity: humidity}, on: ["_time", "host"])</span>
<span class="s"> |> map(fn: (r) => ({temperature: r._value_temperature, humidity:r._value_humidity, _time: r._time, host: r.host}))</span>
<span class="s"> |> keep(columns: ["_time", "humidity", "temperature", "host"])</span>
<span class="s"> |> map(fn: (r) => ({t: ((r.temperature*(9.0/5.0))+32.0), h: r.humidity, _time: r._time, host: r.host}))</span>
<span class="s"> |> map(fn: (r) => ({</span>
<span class="s"> r with heatIndex:</span>
<span class="s"> if ((0.5 * (r.t + 61.0 + ((r.t-68.0)*1.2) + (r.h*0.094)))/2.0) < 80.0 then (0.5 * (r.t + 61.0 + ((r.t - 68.0)*1.2) + (r.h*0.094)))</span>
<span class="s"> else if ( r.h < 13.0 and r.t > 80.0) then ((-42.379 + 2.04901523*r.t + 10.14333127*r.h - .22475541*r.t*r.h - .00683783*r.t*r.h - .05481717*r.t*r.h + .00122874*r.t*r.t*r.h + .00085282*r.t*r.h*r.h - .00000199*r.t*r.t*r.h*r.h - (((13.0-r.h)/4.0)*math.sqrt(x: ((17.0-math.abs(x: (r.t-95.0))/17.0))))))</span>
<span class="s"> else if r.h > 85.0 and r.t >= 80.0 and r.t <= 87.0 then ((-42.379 + 2.04901523*r.t + 10.14333127*r.h - .22475541*r.t*r.h - .00683783*r.t*r.h - .05481717*r.t*r.h + .00122874*r.t*r.t*r.h + .00085282*r.t*r.h*r.h - .00000199*r.t*r.t*r.h*r.h) + (( r.h-85.0 )/10.0) *((87.0-r.t)/5.0))</span>
<span class="s"> else (-42.379 + 2.04901523*r.t + 10.14333127*r.h - .22475541*r.t*r.h - .00683783*r.t*r.t - .05481717*r.h*r.h + .00122874*r.t*r.t*r.h + .00085282*r.t*r.h*r.h - .00000199*r.t*r.t*r.h*r.h)</span>
<span class="s"> })</span>
<span class="s"> )</span>
<span class="s"> |> map(fn: (r) => ({_value: ((r.heatIndex-32.0)/1.8), _time: r._time, host: r.host}))</span>
<span class="s"> |> yield(name: "HeatIndex")</span>
<span class="s"> `</span><span class="p">,</span>
<span class="nx">influxBucket</span><span class="p">,</span>
<span class="nx">period</span><span class="p">.</span><span class="nx">start</span><span class="p">,</span>
<span class="nx">period</span><span class="p">.</span><span class="nx">end</span><span class="p">,</span>
<span class="nx">temperature</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
<span class="nx">temperature</span><span class="p">.</span><span class="nx">field</span><span class="p">,</span>
<span class="nx">period</span><span class="p">.</span><span class="nx">aggregation</span><span class="p">,</span>
<span class="nx">influxBucket</span><span class="p">,</span>
<span class="nx">period</span><span class="p">.</span><span class="nx">start</span><span class="p">,</span>
<span class="nx">period</span><span class="p">.</span><span class="nx">end</span><span class="p">,</span>
<span class="nx">humidity</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
<span class="nx">humidity</span><span class="p">.</span><span class="nx">field</span><span class="p">,</span>
<span class="nx">period</span><span class="p">.</span><span class="nx">aggregation</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">return</span> <span class="nx">query</span><span class="p">,</span> <span class="kc">nil</span>
<span class="p">}</span>
</code></pre></div>
<p><br /></p>
<p>Wow! The problem is that this will not be visible in the UI of InfluxDB. You can either set it up there instead or prepare a custom presentation as I did. Let's take a look into that next time.</p>
<p><img alt="" src="/images/code/arduino/weather/weather-influxdb.png"></p>Arduino Weather Station: Collecting Data2022-09-07T20:00:00+00:002022-09-07T20:00:00+00:00Michal Hořejšektag:blog.horejsek.com,2022-09-07:/weather-station-collecting-data<p><a href="/weather-station-hello-world/">Last time</a>, we put together a very simple weather station. Not very useful as data were logged only to console, and only the current values. Observing trends would not be possible like that. Of course, we could attach a display and fill Arduino memory with the statistics, but that is …</p><p><a href="/weather-station-hello-world/">Last time</a>, we put together a very simple weather station. Not very useful as data were logged only to console, and only the current values. Observing trends would not be possible like that. Of course, we could attach a display and fill Arduino memory with the statistics, but that is not very feasible. The memory is relatively small, and every restart would lose the whole history. Therefore, we need to collect data and store it somewhere else. Let's talk about collecting first.</p>
<p>It's a pain.</p>
<p>Really.</p>
<p>The new era of an interconnected home where every bulb can talk to every LED is scary. Just making simple wireless communication is hard, not yet mentioning secure or even private. I don't want to know how bad all the products available on the market are—sweat reliable cables.</p>
<p>I might be biased because I think I tried the worst possible option: Arduino with externally connected ESP8266 ESP-01 module. Arduino is cheap, and ESP-01 is super cheap. The marketing said “the cheapest IoT”! So I bought a few of those modules. I would change the marketing to “the most unreliable and complex IoT”! I don't use ESP-01 in a single build.</p>
<h4>Raspberry Pi</h4>
<p>After finding out how frustrating it can be to make a simple request with Arduino and ESP-01, I decided to grab simply Raspberry Pi. It's a whole computer, and I can do anything with my favorite Python. That may be true, but Raspberry will consume more power, is more expensive, and is just an overkill. On top of that, there is no analog pin, which I need for air quality sensor.</p>
<p>Anyway, I had it available and wanted to finally gather some data and send it to my server, so I connected it all together: that is, sensors to Arduino (which I already had in place) and Arduino board to Raspberry Pi. Then you can simply read a serial line and do whatever is needed. I logged all measurements to serial as shown in the previous post, and then I used the following Python code to read and send it to my server:</p>
<div class="highlight"><pre><span></span><code><span class="kn">import</span> <span class="nn">serial</span>
<span class="n">ser</span> <span class="o">=</span> <span class="n">serial</span><span class="o">.</span><span class="n">Serial</span><span class="p">(</span><span class="s1">'/dev/ttyUSB0'</span><span class="p">,</span> <span class="mi">9600</span><span class="p">)</span>
<span class="k">while</span> <span class="kc">True</span><span class="p">:</span>
<span class="n">line</span> <span class="o">=</span> <span class="n">ser</span><span class="o">.</span><span class="n">readline</span><span class="p">()</span>
<span class="n">processLine</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">line</span><span class="p">,</span> <span class="s1">'utf8'</span><span class="p">))</span>
</code></pre></div>
<p>It's very oversimplified code. Mine is adding a lot of extra exception handling. But it is enough to illustrate what else can be achieved. It might be helpful one day for something serious.</p>
<p>Note that you need to install <code>python3-serial</code> package and the Python library <code>pyserial</code>. Also, you might find your Arduino in a different place; just check it out by listing all ttys: <code>ls /dev/tty*</code></p>
<h4>NodeMCU</h4>
<p>In the end, I ended up with NodeMCU boards that have Wi-Fi integrated, and this nice <code>ESP8266WiFi</code> library can be used. It is not the only available alternative, though. Any alternative where you can load mentioned library is 99% of success. Then the connection to the server and the data sending is as easy as:</p>
<div class="highlight"><pre><span></span><code><span class="cp">#define WIFI_SSID ""</span>
<span class="cp">#define WIFI_PASS ""</span>
<span class="cp">#define METEO_HOST ""</span>
<span class="cp">#define METEO_PORT 80</span>
<span class="cp">#define METEO_LOCATION "test"</span>
<span class="kt">void</span> <span class="nf">connect</span><span class="p">()</span> <span class="p">{</span>
<span class="n">WiFi</span><span class="p">.</span><span class="n">mode</span><span class="p">(</span><span class="n">WIFI_STA</span><span class="p">);</span>
<span class="n">WiFi</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="n">WIFI_SSID</span><span class="p">,</span> <span class="n">WIFI_PASS</span><span class="p">);</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"Connecting"</span><span class="p">);</span>
<span class="k">while</span> <span class="p">(</span><span class="n">WiFi</span><span class="p">.</span><span class="n">status</span><span class="p">()</span> <span class="o">!=</span> <span class="n">WL_CONNECTED</span><span class="p">)</span> <span class="p">{</span>
<span class="n">delay</span><span class="p">(</span><span class="mi">500</span><span class="p">);</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"."</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">();</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"Connected, IP address: "</span><span class="p">);</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">WiFi</span><span class="p">.</span><span class="n">localIP</span><span class="p">());</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">sendData</span><span class="p">()</span> <span class="p">{</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"Connecting to "</span><span class="p">);</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">METEO_HOST</span><span class="p">);</span>
<span class="n">WiFiClient</span> <span class="n">client</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">client</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="n">METEO_HOST</span><span class="p">,</span> <span class="n">METEO_PORT</span><span class="p">))</span> <span class="p">{</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">">>> Failed"</span><span class="p">);</span>
<span class="n">connect</span><span class="p">();</span>
<span class="n">sendData</span><span class="p">();</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">client</span><span class="p">.</span><span class="n">connected</span><span class="p">())</span> <span class="p">{</span>
<span class="n">String</span> <span class="n">url</span> <span class="o">=</span> <span class="p">(</span>
<span class="s">"/?loc="</span><span class="o">+</span><span class="n">String</span><span class="p">(</span><span class="n">METEO_LOCATION</span><span class="p">)</span><span class="o">+</span>
<span class="s">"&temp="</span><span class="o">+</span><span class="n">String</span><span class="p">(</span><span class="n">temperature</span><span class="p">)</span><span class="o">+</span>
<span class="s">"&hum="</span><span class="o">+</span><span class="n">String</span><span class="p">(</span><span class="n">humidity</span><span class="p">)</span><span class="o">+</span>
<span class="s">"&pre="</span><span class="o">+</span><span class="n">String</span><span class="p">(</span><span class="n">pressure</span><span class="p">)</span><span class="o">+</span>
<span class="s">"&air="</span><span class="o">+</span><span class="n">String</span><span class="p">(</span><span class="n">airQuality</span><span class="p">)</span>
<span class="p">);</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">">>> Request to "</span><span class="p">);</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">url</span><span class="p">);</span>
<span class="n">client</span><span class="p">.</span><span class="n">print</span><span class="p">(</span>
<span class="n">String</span><span class="p">(</span><span class="s">"GET "</span><span class="p">)</span> <span class="o">+</span> <span class="n">url</span> <span class="o">+</span> <span class="s">" HTTP/1.1</span><span class="se">\r\n</span><span class="s">"</span> <span class="o">+</span>
<span class="s">"Host: "</span> <span class="o">+</span> <span class="n">METEO_HOST</span> <span class="o">+</span> <span class="s">"</span><span class="se">\r\n</span><span class="s">"</span> <span class="o">+</span>
<span class="s">"User-Agent: esp</span><span class="se">\r\n</span><span class="s">"</span> <span class="o">+</span>
<span class="s">"Connection: close</span><span class="se">\r\n\r\n</span><span class="s">"</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">timeout</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
<span class="k">while</span> <span class="p">(</span><span class="n">client</span><span class="p">.</span><span class="n">available</span><span class="p">()</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">millis</span><span class="p">()</span> <span class="o">-</span> <span class="n">timeout</span> <span class="o">></span> <span class="mi">5000</span><span class="p">)</span> <span class="p">{</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">">>> Timeouted"</span><span class="p">);</span>
<span class="n">client</span><span class="p">.</span><span class="n">stop</span><span class="p">();</span>
<span class="n">connect</span><span class="p">();</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">while</span> <span class="p">(</span><span class="n">client</span><span class="p">.</span><span class="n">available</span><span class="p">())</span> <span class="p">{</span>
<span class="kt">char</span> <span class="n">ch</span> <span class="o">=</span> <span class="n">static_cast</span><span class="o"><</span><span class="kt">char</span><span class="o">></span><span class="p">(</span><span class="n">client</span><span class="p">.</span><span class="n">read</span><span class="p">());</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">ch</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">();</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">">>> OK"</span><span class="p">);</span>
<span class="n">client</span><span class="p">.</span><span class="n">stop</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div>
<p>You might notice that there is HTTP, not HTTPS. The usage is way better than low-level AP commands, but we are still working with a microcontroller, not a microcomputer. I decided not to care about everything and use plain HTTP as all the communication is done on my network anyway, but I would reconsider if this should be available outside of home network as well.</p>
<p>Also, I recommend moving as much work as possible to the server part to keep the logic on the Arduino board as simple as possible. The client part is more limited and harder to debug.</p>
<p><br /></p>
<p>Good! We are sending data somewhere to the cloud (a fancy name for a server), which is not there yet. Let's look into this crucial detail in the next post.</p>
<p><img alt="" src="/images/code/arduino/weather/weather-nodemcu.png"></p>Arduino Weather Station: Hello World2022-08-15T08:00:00+00:002022-08-15T08:00:00+00:00Michal Hořejšektag:blog.horejsek.com,2022-08-15:/weather-station-hello-world<p>A weather station is a <em>hello world</em> build for everyone starting with Arduino. It makes perfect sense. Hello world is about making something super simple, and a weather station can be just that. It’s simply about connecting a sensor or two to the Arduino board, loading a library for …</p><p>A weather station is a <em>hello world</em> build for everyone starting with Arduino. It makes perfect sense. Hello world is about making something super simple, and a weather station can be just that. It’s simply about connecting a sensor or two to the Arduino board, loading a library for it, and writing a few simple lines of code to display measured values in the monitoring tool on the computer screen.</p>
<p>Ok, maybe that’s already too much. Actual hello world is usually a one-liner and not a very useful line. That would be to light up a LED. Let’s say the weather station is like a to-do list application which is a hello world for JavaScript frameworks.</p>
<h4>Temperature and humidity</h4>
<p>Either way, I put a few such builds together soon after I got my first Arduino, and I believe you will do the same when you get yours. My box contained HTU21D as a temperature and humidity sensor and BMP180 as a pressure sensor or barometer. Both sensors use the I2C bus; thus, I connected SDAs to pin A4, SCLs to A5, GNDs to GND, and VCCs to 3 or 5V pin (mine works with both levels; double-check what your version needs).</p>
<p>Once we have it all connected, the next step is reading the values. The good part about those common sensors is there are libraries for them. To read temperature and humidity, let’s simply use <a href="https://github.com/sparkfun/SparkFun_HTU21D_Breakout_Arduino_Library">HTU21D library</a>, which can be used like this:</p>
<div class="highlight"><pre><span></span><code><span class="cp">#include</span> <span class="cpf">"SparkFunHTU21D.h"</span><span class="cp"></span>
<span class="n">HTU21D</span> <span class="n">sensorHTU</span><span class="p">;</span>
<span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">9600</span><span class="p">);</span>
<span class="n">sensorHTU</span><span class="p">.</span><span class="n">begin</span><span class="p">();</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">loop</span><span class="p">()</span> <span class="p">{</span>
<span class="kt">float</span> <span class="n">temperature</span> <span class="o">=</span> <span class="n">sensorHTU</span><span class="p">.</span><span class="n">readTemperature</span><span class="p">();</span>
<span class="kt">float</span> <span class="n">humidity</span> <span class="o">=</span> <span class="n">sensorHTU</span><span class="p">.</span><span class="n">readHumidity</span><span class="p">();</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"Temperature: "</span><span class="p">);</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">temperature</span><span class="p">);</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"°C"</span><span class="p">);</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"Humidity: "</span><span class="p">);</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">humidity</span><span class="p">);</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"%"</span><span class="p">);</span>
<span class="n">delay</span><span class="p">(</span><span class="mi">1000</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<h4>Pressure</h4>
<p>Similarly, we can use <a href="https://github.com/adafruit/Adafruit-BMP085-Library">BMP085 library</a> to get pressure basically the same way. If you are curious about why we use the BMP085 library for the BMP180 sensor, no worries, it’s all fine. BMP180 is just a cheaper and less precise version of BMP085 with the same interface.</p>
<p>If you connect all things and run the readings, you might notice that the pressure is way off. Is that sensor really so much inaccurate? Not really. You just need to calibrate it based on your altitude. The question is whether you even need the calibration. I don’t care. Honestly, the exact value will not say to me much anyway. I even didn’t know why I should care about pressure until I read the book <em>Weather for Dummies</em>. It’s not a perfect book, and I don’t recommend reading it. But at least it explained to me how pressure works:</p>
<p>In a nutshell, the trend is crucial. When the pressure goes up, a warm wave is coming, and thus we can expect <em>nicer</em> weather. At least more sun and warmth is what we usually call nice. And conversely, if pressure is falling, it brings a bigger chance of rain and colder weather. You can think of it as a mountain (high pressure) and a valley (low pressure): water always flows down.</p>
<p>I tested it with uncalibrated sensors, and it works just as well. My readings are only way lower than the actual pressure.</p>
<h4>Air quality</h4>
<p>Displaying temperature, humidity and pressure is nice but doesn’t make much of an impression. Many products do the same for less money, so it might even be boring. I wanted something more. First idea was to open and close windows automatically, but soon I realized that is way out of scope. For now. Instead, I directed my focus to air quality.</p>
<p>I made two mistakes. First, I focused on CO<sub>2</sub> and looked for sensors sensitive to this gas. I bought quite an expensive MG-811. The problem is that this sensor needs 6V, so you cannot power it simply with an Arduino board. Also, the sensor has to be calibrated for conversion to PPM. It’s not plug & play.</p>
<p>My second mistake was basically the same but dropping the <sub>2</sub> and looking for a sensor sensitive to CO. For that, I found MQ-9. The very cheap sensor that can be plugged into a 5V pin. The problem is that CO is not what I was looking for. MQ-9 can be great for fire detection applications but useless for detecting air quality.</p>
<p>What you want for air quality is MQ-135, a sensor sensitive to a wide range of gases you don’t want to breathe in your home or office. The connection is effortless, and reading can be done via analog output for precise resistance or also via a digital output to get a signal only when the readings are higher than a set threshold.</p>
<p>Personally, I use analog values. MQ-135, like all other gas sensors, needs calibration for proper warning levels. You might be tricked by some libraries that provide a convenient way of getting ppm level, but don’t use it without calibration. All such libraries have hard-coded values, which will indeed not be aligned with your setup.</p>
<p>Because I wanted several stations, I didn’t want to bother with the calibration of every setup, pay attention which one I connect to what board and properly load the correct code. Instead, I use air quality the same way as pressure readings. In the end, I don’t care about PPM or comparison between rooms. The trend is essential. Is it high than usual for that room? Then I should open the window. It’s simple like that.</p>
<p><br /></p>
<p>Ok. We can read values, but we must collect them to see the trends. Let’s continue next time.</p>
<p><img alt="" src="/images/code/arduino/weather/weather-build.jpg"></p>Arduino Build: Restroom Clock2022-03-02T08:00:00+00:002022-03-02T08:00:00+00:00Michal Hořejšektag:blog.horejsek.com,2022-03-02:/arduino-build-restroom-clock<p>Our flat has a clock in every single room but the restroom. My wife quickly fixed that issue with an old wireless clock with a meteorological station that was not working anymore. The clock was old, and not even a fresh battery could do the trick. But when the clock …</p><p>Our flat has a clock in every single room but the restroom. My wife quickly fixed that issue with an old wireless clock with a meteorological station that was not working anymore. The clock was old, and not even a fresh battery could do the trick. But when the clock was working, we even knew how hot it was in there!</p>
<p>Anyway, it was a clear sign for an Arduino project. After the last failed attempt to make <a href="/arduino-build-night-light/">wireless dim light</a>, I decided to combine the light and clock build together and put it into the plug instead.</p>
<h3>Time sync</h3>
<p>Displaying anything is a relatively easy thing to do. The tricky part, in this case, is how to sync the time. Arduino has only time since it was booted, no info about the real-time.</p>
<p>One way to solve this issue is using a Wi-Fi module and syncing time with the internet. I bought a few ESP8266 ESP-01 modules just for fun. They are super cheap. The issue is it’s also extremely difficult to make them work with Arduino. I spent countless hours without having anything stable before switching to the NodeMCU CP2102 board, which has an integrated Wi-Fi module. It works like a charm! At least for my meteorological stations. I didn’t have more NodeMCU boards, so I took it as a chance to play with different challenges: how to do it without the internet?</p>
<p>The answer is clear. I guess we all know how many kitchen appliances with time we have around when there is a time shift to or from DST or when we experience a power outage. Arduino counts the time since it started, and I just need to store somewhere information about the time when the clock was plugged in. I can compute the time like any microwave can with those two variables.</p>
<p>The problem occurs with the <code>millis()</code> function: it overflows roughly after 49 days and 17 hours. If we don’t take that into account, then the clock needs to be corrected every two months instead of twice a year. I solved it with a custom seconds counter. The crucial is how to check delays. It is best if you always do it in this form, only that way you can avoid overflow issue: <code>(millis() - prevMillis) > delay</code>. (At least if the delay is shorter than 50 days!)</p>
<p>Here is the corresponding piece of code to keep proper time offset:</p>
<div class="highlight"><pre><span></span><code><span class="cp">#define MAX_SEC_DAY 86400</span>
<span class="kt">long</span> <span class="n">setTimeOffset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// Set by user.</span>
<span class="kt">long</span> <span class="n">dayTimeOffset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// Set automatically using millis.</span>
<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">previousMillis</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">void</span> <span class="nf">updateDayTimeOffset</span><span class="p">()</span> <span class="p">{</span>
<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">currentMillis</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
<span class="k">if</span> <span class="p">((</span><span class="n">currentMillis</span> <span class="o">-</span> <span class="n">previousMillis</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">1000</span><span class="p">)</span> <span class="p">{</span>
<span class="n">previousMillis</span> <span class="o">+=</span> <span class="mi">1000</span><span class="p">;</span>
<span class="n">dayTimeOffset</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">dayTimeOffset</span> <span class="o">=</span> <span class="n">dayTimeOffset</span> <span class="o">%</span> <span class="n">MAX_SEC_DAY</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<p>That can be then printed with the following code: </p>
<div class="highlight"><pre><span></span><code><span class="kt">void</span> <span class="nf">printTime</span><span class="p">()</span> <span class="p">{</span>
<span class="kt">long</span> <span class="n">time</span> <span class="o">=</span> <span class="p">(</span><span class="n">setTimeOffset</span> <span class="o">+</span> <span class="n">dayTimeOffset</span><span class="p">)</span> <span class="o">%</span> <span class="n">MAX_SEC_DAY</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">h</span> <span class="o">=</span> <span class="n">time</span> <span class="o">/</span> <span class="mi">3600</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">m</span> <span class="o">=</span> <span class="p">(</span><span class="n">time</span><span class="o">/</span><span class="mi">60</span><span class="p">)</span> <span class="o">%</span> <span class="mi">60</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">s</span> <span class="o">=</span> <span class="n">time</span> <span class="o">%</span> <span class="mi">60</span><span class="p">;</span>
<span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div>
<h3>Time acceleration</h3>
<p>I was surprised it really worked. After two months, no overflow issue happened, the time continued flawlessly. Except, every day, the time was more and more off, about 13 seconds. In two months, our time was approximately 13 minutes ahead. Thanks to the pandemic, we are always home, so we can adjust our brain every day (slowly boiled frog), but after two weeks long vacation, I could be afraid I’m two minutes late and need to force myself to poop faster! Not very useful.</p>
<p>I guess the crystal or ceramic resonator speed can differ for every Arduino, so I fixed it in my program with some variables to make it a bit universal. For 13 seconds a day, it means I need to slow it down by a second every 106 minutes (or 6360 seconds).</p>
<div class="highlight"><pre><span></span><code><span class="cp">#define CORRECTION_EVERY_SEC 6360</span>
<span class="cp">#define CORRECTION_SEC -1</span>
<span class="kt">long</span> <span class="n">correctionCounter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="c1">// Added to the updateDayTimeOffset:</span>
<span class="k">if</span> <span class="p">((</span><span class="n">currentMillis</span> <span class="o">-</span> <span class="n">previousMillis</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">1000</span><span class="p">)</span> <span class="p">{</span>
<span class="n">correctionCounter</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">correctionCounter</span> <span class="o">==</span> <span class="n">CORRECTION_EVERY_SEC</span><span class="p">)</span> <span class="p">{</span>
<span class="n">correctionCounter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">dayTimeOffset</span> <span class="o">+=</span> <span class="n">CORRECTION_SEC</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>
<h3>Time settings</h3>
<p>Time is ready! But with no option to manually set it. I wanted to create something better than found elsewhere because I often struggle with a manual. Clock settings are often soo unfriendly!</p>
<p>I thought having only one button had to be simple enough. Well, the whole thing is more challenging than I thought! But, even though I never tested my system on anyone else, I believe a one-button solution works excellently.</p>
<p>Here is the UX: short press does nothing by default. In other words: there is no way to change time accidentally. Long press (for one second) enters the user into settings mode, first the hour mode. Short press now increases hour by one. Once the requested hour is set, the long press switches mode to minute instead. The same repeats for minutes, and the last long-press exits the settings. The active mode is visible on display.</p>
<p>Written like that seems an easy task, but it took me some time to fine-tune the code! But I didn’t want to give up. I wanted to use the least components possible.</p>
<p>I found myself sometimes pressing the button like crazy because I could only increment the value, but still, I liked that I could fit the whole thing on a tiny board. Also, there is no confusion about what needs to be pressed, and there are only two options: short and long press, which sounds reasonably straightforward.</p>
<div class="highlight"><pre><span></span><code><span class="k">if</span> <span class="p">(</span><span class="n">lastButtonPress</span> <span class="o">></span> <span class="mi">0</span> <span class="o">&&</span> <span class="p">(</span><span class="n">millis</span><span class="p">()</span> <span class="o">-</span> <span class="n">lastButtonPress</span><span class="p">)</span> <span class="o">>=</span> <span class="n">BUTTON_LONG_PRESS_MS</span><span class="p">)</span> <span class="p">{</span>
<span class="n">settingsMode</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">settingsMode</span> <span class="o">></span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
<span class="n">settingsMode</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">lastButtonPress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">render</span><span class="p">();</span>
<span class="p">}</span>
<span class="kt">int</span> <span class="n">buttonState</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_BUTTON</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">buttonState</span> <span class="o">!=</span> <span class="n">prevButtonState</span><span class="p">)</span> <span class="p">{</span>
<span class="n">prevButtonState</span> <span class="o">=</span> <span class="n">buttonState</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">buttonState</span> <span class="o">==</span> <span class="n">HIGH</span><span class="p">)</span> <span class="p">{</span>
<span class="n">lastButtonPress</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">lastButtonPress</span> <span class="o">></span> <span class="mi">0</span> <span class="o">&&</span> <span class="p">(</span><span class="n">millis</span><span class="p">()</span> <span class="o">-</span> <span class="n">lastButtonPress</span><span class="p">)</span> <span class="o"><</span> <span class="n">BUTTON_LONG_PRESS_MS</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">settingsMode</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="n">setTimeOffset</span> <span class="o">+=</span> <span class="mi">3600</span><span class="p">;</span>
<span class="n">render</span><span class="p">();</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">settingsMode</span> <span class="o">==</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
<span class="n">setTimeOffset</span> <span class="o">+=</span> <span class="mi">60</span><span class="p">;</span>
<span class="n">render</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">setTimeOffset</span> <span class="o">></span> <span class="n">MAX_SEC_DAY</span><span class="p">)</span> <span class="p">{</span>
<span class="n">setTimeOffset</span> <span class="o">=</span> <span class="n">setTimeOffset</span> <span class="o">%</span> <span class="n">MAX_SEC_DAY</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">lastButtonPress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<h3>Displaying time</h3>
<p>Time is set! Now, how to display it? I used a 128x64 OLED display connected over I2C. I could use a 7-segment display, but you usually get it as 4 or 8 digits, yet I wanted 6 (hour + minute + seconds), and I wanted some way to display also settings mode. With OLED, it’s just nicer. And with the U8glib library, it’s super easy to do:</p>
<div class="highlight"><pre><span></span><code><span class="cp">#include</span> <span class="cpf">"U8glib.h"</span><span class="cp"></span>
<span class="n">U8GLIB_SSD1306_128X64</span> <span class="nf">display</span><span class="p">(</span><span class="n">U8G_I2C_OPT_NONE</span><span class="p">);</span>
<span class="kt">void</span> <span class="nf">render</span><span class="p">()</span> <span class="p">{</span>
<span class="n">display</span><span class="p">.</span><span class="n">firstPage</span><span class="p">();</span>
<span class="k">do</span> <span class="p">{</span>
<span class="n">printTime</span><span class="p">();</span>
<span class="p">}</span> <span class="k">while</span><span class="p">(</span> <span class="n">display</span><span class="p">.</span><span class="n">nextPage</span><span class="p">()</span> <span class="p">);</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">printTime</span><span class="p">()</span> <span class="p">{</span>
<span class="p">...</span>
<span class="n">display</span><span class="p">.</span><span class="n">setFont</span><span class="p">(</span><span class="n">u8g_font_fub25r</span><span class="p">);</span>
<span class="n">printNumber</span><span class="p">(</span><span class="n">h</span><span class="p">);</span>
<span class="n">display</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">":"</span><span class="p">);</span>
<span class="n">printNumber</span><span class="p">(</span><span class="n">m</span><span class="p">);</span>
<span class="n">display</span><span class="p">.</span><span class="n">setFont</span><span class="p">(</span><span class="n">u8g_font_fub20r</span><span class="p">);</span>
<span class="n">display</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">":"</span><span class="p">);</span>
<span class="n">printNumber</span><span class="p">(</span><span class="n">s</span><span class="p">);</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">printNumber</span><span class="p">(</span><span class="kt">int</span> <span class="n">number</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">number</span> <span class="o"><</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span>
<span class="n">display</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"0"</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">display</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">number</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>
<p>There is one thing to be aware of: the library does not render the whole screen in one go. In my case, it updates in eight cycles, but it is different for every specific display type. That means if your variables can change during rendering, you can see a partially wrong result. It happened to me with the seconds. If the render occurred on the edge of two seconds, the top of the number was the previous one, and the bottom part was the new one. Such output doesn’t seem professional! Therefore, it is better first to calculate variables and then do the render (at least it will save some time computing it all over again).</p>
<p>The other issue I encountered was that I rendered the display in a super-fast loop and fried it by mistake, even though it was rendering a blank screen. After a few days, it stayed black. Be careful how often you do the render! It’s a nasty bug that is hard to spot until it’s too late.</p>
<h3>Motion detector with light</h3>
<p>Finally, it is time to connect the motion detector and light. It’s nothing fancy; just turn on the dim light and show the time when someone enters the room. I used a white LED that is dim enough yet contains too many unwanted blue waves, which I fixed with a thin colored paper.</p>
<p>In the corridor, we have a bulb with a motion detector, and it always takes several attempts before it turns off because when the light goes off, the motion detector sees that as a change and lights it up again. Quite frustrating, to be honest. I was thinking about how to avoid this issue in my build.</p>
<p>I found out that it is pretty easy. The essential part is not having the light close to the motion detector. I tilted the sensor a bit behind the LED, and all was good. This way, light illuminates the wall producing nice dim light, and the motion detector looks for any movement in front of the door.</p>
<p>The last hack I did was to keep it compact: use 5V from digital pins to not need to solder the build or to have too many cables around. It all fits nicely on the electrical adapter I happen to have. By the way, this adapter can provide juice for Arduino for up to five seconds, enough to survive a minor power outage. A good thing to have for the build where the internal state is set manually.</p>
<p><img alt="" src="/images/code/arduino/clock/top.jpg"></p>
<p><img alt="" src="/images/code/arduino/clock/bottom.jpg"></p>
<p>The full code:</p>
<div class="highlight"><pre><span></span><code><span class="cp">#include</span> <span class="cpf">"U8glib.h"</span><span class="cp"></span>
<span class="n">U8GLIB_SSD1306_128X64</span> <span class="nf">display</span><span class="p">(</span><span class="n">U8G_I2C_OPT_NONE</span><span class="p">);</span>
<span class="cp">#define PIN_BUTTON 13</span>
<span class="cp">#define PIN_MOVE 11</span>
<span class="cp">#define PIN_LIGHT 2</span>
<span class="cp">#define AWAKE_TIME_MS 60000</span>
<span class="cp">#define BUTTON_LONG_PRESS_MS 1000</span>
<span class="cp">#define MAX_SEC_DAY 86400</span>
<span class="c1">// Arduino clock might run faster or slower than the real clocks.</span>
<span class="c1">// In my case, I have to slow it down by a sec every 106 minutes.</span>
<span class="cp">#define CORRECTION_EVERY_SEC 6360 </span><span class="c1">// 106 minutes.</span>
<span class="cp">#define CORRECTION_SEC -1</span>
<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">lastDetectedMove</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">lastButtonPress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">lastRender</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">prevButtonState</span> <span class="o">=</span> <span class="n">LOW</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">settingsMode</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// 0: nothing, 1: hour, 2: minute</span>
<span class="kt">long</span> <span class="n">setTimeOffset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// Set by user.</span>
<span class="kt">long</span> <span class="n">dayTimeOffset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// Set automatically using millis.</span>
<span class="kt">long</span> <span class="n">correctionCounter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// Set automatically using millis.</span>
<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">previousMillis</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">9600</span><span class="p">);</span>
<span class="n">pinMode</span><span class="p">(</span><span class="n">PIN_BUTTON</span><span class="p">,</span> <span class="n">INPUT</span><span class="p">);</span>
<span class="n">pinMode</span><span class="p">(</span><span class="n">PIN_MOVE</span><span class="p">,</span> <span class="n">INPUT</span><span class="p">);</span>
<span class="n">pinMode</span><span class="p">(</span><span class="n">PIN_LIGHT</span><span class="p">,</span> <span class="n">OUTPUT</span><span class="p">);</span>
<span class="n">digitalWrite</span><span class="p">(</span><span class="n">PIN_LIGHT</span><span class="p">,</span> <span class="n">HIGH</span><span class="p">);</span>
<span class="c1">// Hack: 5V on pins.</span>
<span class="n">pinMode</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="n">OUTPUT</span><span class="p">);</span>
<span class="n">pinMode</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">OUTPUT</span><span class="p">);</span>
<span class="n">digitalWrite</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="n">HIGH</span><span class="p">);</span>
<span class="n">digitalWrite</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">HIGH</span><span class="p">);</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">loop</span><span class="p">()</span> <span class="p">{</span>
<span class="n">updateDayTimeOffset</span><span class="p">();</span>
<span class="kt">bool</span> <span class="n">moveDetected</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_MOVE</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">moveDetected</span><span class="p">)</span> <span class="p">{</span>
<span class="n">lastDetectedMove</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">lastDetectedMove</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">delay</span><span class="p">(</span><span class="mi">10</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">bool</span> <span class="n">isAwake</span> <span class="o">=</span> <span class="p">(</span><span class="n">lastDetectedMove</span> <span class="o">+</span> <span class="n">AWAKE_TIME_MS</span><span class="p">)</span> <span class="o">></span> <span class="n">millis</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isAwake</span><span class="p">)</span> <span class="p">{</span>
<span class="n">lastDetectedMove</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">digitalWrite</span><span class="p">(</span><span class="n">PIN_LIGHT</span><span class="p">,</span> <span class="n">LOW</span><span class="p">);</span>
<span class="n">clearOLED</span><span class="p">();</span>
<span class="n">delay</span><span class="p">(</span><span class="mi">10</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">digitalWrite</span><span class="p">(</span><span class="n">PIN_LIGHT</span><span class="p">,</span> <span class="n">HIGH</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">lastButtonPress</span> <span class="o">></span> <span class="mi">0</span> <span class="o">&&</span> <span class="p">(</span><span class="n">millis</span><span class="p">()</span> <span class="o">-</span> <span class="n">lastButtonPress</span><span class="p">)</span> <span class="o">>=</span> <span class="n">BUTTON_LONG_PRESS_MS</span><span class="p">)</span> <span class="p">{</span>
<span class="n">settingsMode</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">settingsMode</span> <span class="o">></span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
<span class="n">settingsMode</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">lastButtonPress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">render</span><span class="p">();</span>
<span class="p">}</span>
<span class="kt">int</span> <span class="n">buttonState</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_BUTTON</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">buttonState</span> <span class="o">!=</span> <span class="n">prevButtonState</span><span class="p">)</span> <span class="p">{</span>
<span class="n">prevButtonState</span> <span class="o">=</span> <span class="n">buttonState</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">buttonState</span> <span class="o">==</span> <span class="n">HIGH</span><span class="p">)</span> <span class="p">{</span>
<span class="n">lastButtonPress</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">lastButtonPress</span> <span class="o">></span> <span class="mi">0</span> <span class="o">&&</span> <span class="p">(</span><span class="n">millis</span><span class="p">()</span> <span class="o">-</span> <span class="n">lastButtonPress</span><span class="p">)</span> <span class="o"><</span> <span class="n">BUTTON_LONG_PRESS_MS</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">settingsMode</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="n">setTimeOffset</span> <span class="o">+=</span> <span class="mi">3600</span><span class="p">;</span>
<span class="n">render</span><span class="p">();</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">settingsMode</span> <span class="o">==</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
<span class="n">setTimeOffset</span> <span class="o">+=</span> <span class="mi">60</span><span class="p">;</span>
<span class="n">render</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">setTimeOffset</span> <span class="o">></span> <span class="n">MAX_SEC_DAY</span><span class="p">)</span> <span class="p">{</span>
<span class="n">setTimeOffset</span> <span class="o">=</span> <span class="n">setTimeOffset</span> <span class="o">%</span> <span class="n">MAX_SEC_DAY</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">lastButtonPress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">lastRender</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">millis</span><span class="p">()</span><span class="o">-</span><span class="n">lastRender</span> <span class="o">>=</span> <span class="mi">1000</span><span class="p">)</span> <span class="p">{</span>
<span class="n">render</span><span class="p">();</span>
<span class="p">}</span>
<span class="n">delay</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">updateDayTimeOffset</span><span class="p">()</span> <span class="p">{</span>
<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">currentMillis</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
<span class="k">if</span> <span class="p">((</span><span class="n">currentMillis</span> <span class="o">-</span> <span class="n">previousMillis</span><span class="p">)</span> <span class="o">>=</span> <span class="mi">1000</span><span class="p">)</span> <span class="p">{</span>
<span class="n">previousMillis</span> <span class="o">+=</span> <span class="mi">1000</span><span class="p">;</span>
<span class="n">dayTimeOffset</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
<span class="n">correctionCounter</span> <span class="o">+=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">correctionCounter</span> <span class="o">==</span> <span class="n">CORRECTION_EVERY_SEC</span><span class="p">)</span> <span class="p">{</span>
<span class="n">correctionCounter</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="n">dayTimeOffset</span> <span class="o">+=</span> <span class="n">CORRECTION_SEC</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">dayTimeOffset</span> <span class="o">=</span> <span class="n">dayTimeOffset</span> <span class="o">%</span> <span class="n">MAX_SEC_DAY</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">clearOLED</span><span class="p">(){</span>
<span class="n">display</span><span class="p">.</span><span class="n">firstPage</span><span class="p">();</span>
<span class="k">do</span> <span class="p">{</span>
<span class="p">}</span> <span class="k">while</span><span class="p">(</span> <span class="n">display</span><span class="p">.</span><span class="n">nextPage</span><span class="p">()</span> <span class="p">);</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">render</span><span class="p">()</span> <span class="p">{</span>
<span class="n">lastRender</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
<span class="n">display</span><span class="p">.</span><span class="n">firstPage</span><span class="p">();</span>
<span class="k">do</span> <span class="p">{</span>
<span class="n">renderPage</span><span class="p">();</span>
<span class="p">}</span> <span class="k">while</span><span class="p">(</span> <span class="n">display</span><span class="p">.</span><span class="n">nextPage</span><span class="p">()</span> <span class="p">);</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">renderPage</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">settingsMode</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="n">display</span><span class="p">.</span><span class="n">setPrintPos</span><span class="p">(</span><span class="mi">13</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
<span class="n">display</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"."</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">settingsMode</span> <span class="o">==</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
<span class="n">display</span><span class="p">.</span><span class="n">setPrintPos</span><span class="p">(</span><span class="mi">63</span><span class="p">,</span> <span class="mi">10</span><span class="p">);</span>
<span class="n">display</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"."</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">display</span><span class="p">.</span><span class="n">setPrintPos</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">50</span><span class="p">);</span>
<span class="n">printTime</span><span class="p">();</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">printTime</span><span class="p">()</span> <span class="p">{</span>
<span class="kt">long</span> <span class="n">time</span> <span class="o">=</span> <span class="p">(</span><span class="n">setTimeOffset</span> <span class="o">+</span> <span class="n">dayTimeOffset</span><span class="p">)</span> <span class="o">%</span> <span class="n">MAX_SEC_DAY</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">h</span> <span class="o">=</span> <span class="n">time</span> <span class="o">/</span> <span class="mi">3600</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">m</span> <span class="o">=</span> <span class="p">(</span><span class="n">time</span><span class="o">/</span><span class="mi">60</span><span class="p">)</span> <span class="o">%</span> <span class="mi">60</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">s</span> <span class="o">=</span> <span class="n">time</span> <span class="o">%</span> <span class="mi">60</span><span class="p">;</span>
<span class="n">display</span><span class="p">.</span><span class="n">setFont</span><span class="p">(</span><span class="n">u8g_font_fub25r</span><span class="p">);</span>
<span class="n">printNumber</span><span class="p">(</span><span class="n">h</span><span class="p">);</span>
<span class="n">display</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">":"</span><span class="p">);</span>
<span class="n">printNumber</span><span class="p">(</span><span class="n">m</span><span class="p">);</span>
<span class="n">display</span><span class="p">.</span><span class="n">setFont</span><span class="p">(</span><span class="n">u8g_font_fub20r</span><span class="p">);</span>
<span class="n">display</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">":"</span><span class="p">);</span>
<span class="n">printNumber</span><span class="p">(</span><span class="n">s</span><span class="p">);</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">printNumber</span><span class="p">(</span><span class="kt">int</span> <span class="n">number</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">number</span> <span class="o"><</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span>
<span class="n">display</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"0"</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">display</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">number</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>Arduino Build: Night Light2022-02-02T08:00:00+00:002022-02-02T08:00:00+00:00Michal Hořejšektag:blog.horejsek.com,2022-02-02:/arduino-build-night-light<p>Daylight contains a lot of blue light, which affects how our body operates. Do you know why you have an issue waking up in the morning during winter? Because there is not enough (blue) light that would trigger the booting procedure of your body. To overcome this issue, you can …</p><p>Daylight contains a lot of blue light, which affects how our body operates. Do you know why you have an issue waking up in the morning during winter? Because there is not enough (blue) light that would trigger the booting procedure of your body. To overcome this issue, you can find really powerful lights as an alarm clock to make your winter morning like the summer sunshine.</p>
<p>However, the bigger problem is on the other side of the day, during the evenings. Traditionally, we were surrounded by less and less blue light as night was approaching, which triggered a process of setting our body to sleep mode. These days, our screens (no matter whether it's a phone, notebook, or TV) produce a lot of artificial blue light.</p>
<p>Of course, our body is not used to that. That's the reason why we end up in the bed watching the ceiling for hours. After some time, we give it up and take our phone and mess our circadian code even more. Now you understand if you wondered why night mode is such a prominent feature of any operating system or device.</p>
<p>But that is not enough. We should also adjust other lights in our homes, not only screens. It is good to use powerful light when you don't plan to sleep yet, but switching to dim lights at least one hour before sleeping is more than welcome.</p>
<p>The question is: what about light in the bathroom, for example?</p>
<p>My wife suggested we could buy some soft light to cover the situation when we need to visit the bathroom when we are already in sleep mode. (Our only light there is super strong, and it is always painful contrast to the rest of our apartment.) I looked at her and asked why we would buy it when I could build it!</p>
<p>I'm very sorry that it took me so long! But I can say, after a year, we finally have it!</p>
<p>It took me so much time because I wanted a wireless battery version: a small box under the ceiling providing dim light when we don't use the main light, and movement is detected—three simple components connected to Arduino with a simple code. Pretty easy build indeed!</p>
<p>Except, Arduino can deplete batteries in a few days. It doesn't matter if Nano or Micro is used; the difference can be just a few hours. I managed to create the first prototype in one hour, but it lasted only till the second day. I needed something better.</p>
<p>Here comes the second reason why it took so long. I wanted to play with Arduino, but the only chance to solve the battery issue most efficiently was not using Arduino! I took a break for several months from it.</p>
<p>Sure, Arduino is quite useless anyway in this case. Motion detector provides a logical one if a movement is detected, and light sensor provides an analog value of the light intensity, which can be converted into a digital value using a resistor (kind of, but good enough for this use case). Now it's just about using gates and connect it all together.</p>
<p>At least if you have gates around. I didn't, but I had transistors. It was an enjoyable exercise to create my logical gate! Much more rewarding feeling than any code can do, to be honest. I hacked together the prototype and it kind of worked. It was okay, but not super precise, voltage fluctuated, and the consumption was still unacceptably high.</p>
<p>Example of an AND gate using transistors:</p>
<p><img alt="" src="/images/code/arduino/light/and-gate.png"></p>
<p>With pre-made gates, I could construct a much more stable prototype. First, I started with a straightforward version. When you can use any logical gate, it is an elementary task. You just put AND here, OR there, and NOT in between. The problem is, an integrated circuit comes with few logical gates of the same kind on it, and every chip is draining your battery!</p>
<p>As few paragraphs above was good to know how transistors work, now it is good to know boolean algebra. I studied all this, but I was interested only in the code back then; now, I appreciate that I still remember some stuff. Any logical circuit can be represented in many various ways. You just need to transform the function to a different form producing the same output for each input.</p>
<p>I had this logical table I had to obey (in other words, use the light when there is movement without a light):</p>
<table class="table table-striped table-sm text-center">
<thead>
<th>Movement</th>
<th>Light</th>
<th>Output</th>
</thead>
<tbody>
<tr><td>0</td><td>0</td><td>0</td></tr>
<tr><td>0</td><td>1</td><td>0</td></tr>
<tr><td>1</td><td>0</td><td>1</td></tr>
<tr><td>1</td><td>1</td><td>0</td></tr>
</tbody>
</table>
<p>That can be represented as <strong>M AND NOT L</strong>, or as <strong>((L NAND L) NAND M) NAND ((L NAND L) NAND M)</strong>. It might look complicated, but actually, it is just three NAND gates. We can simplify the representation by extracting <strong>(L NAND L) NAND M</strong> into variable <em>A</em>, which gives us <strong>A NAND A</strong>. That can be put together with a chip from 7400 series containing four NAND gates like this (where one represents movement detector and zero light detector):</p>
<p><img alt="" src="/images/code/arduino/light/logical-circuit.png"></p>
<p>I was so so happy! My calculation revealed it should be running for months, at least if used a few to several times every evening. But.</p>
<p>But I didn't consider that my wife gets used to it and will use it every time during the day! In two weeks, it was out again. It was again collecting dust on my table waiting for me to find ways around it until the project was changed into a completely different build: a restroom clock. With Arduino and exciting challenges! Stay tuned.</p>Arduino Build: Kitchen Timer2021-12-29T08:00:00+00:002021-12-29T08:00:00+00:00Michal Hořejšektag:blog.horejsek.com,2021-12-29:/arduino-build-kitchen-timer<p>In the previous post about <a href="/arduino-project-page-turner/">page turner project</a>, I talked about the course, which forced me to allocate my time and really build something. Electrical builds, including Arduino or Raspberry, are relatively easy for me. It is just about connecting pieces together and writing code for it. For sure, it …</p><p>In the previous post about <a href="/arduino-project-page-turner/">page turner project</a>, I talked about the course, which forced me to allocate my time and really build something. Electrical builds, including Arduino or Raspberry, are relatively easy for me. It is just about connecting pieces together and writing code for it. For sure, it is not as easy as developing (web) applications—UX and technical options are pretty limited—yet still not that challenging as creating a fully mechanical project.</p>
<p>One of the projects as part of the course was precisely that—mechanical build. I had no idea what I could do or even what I could manage to do (I had to use what I had at home because hobby shops were not open during strict lockdown when the course was ongoing). I found some springs around, so I thought I could build a more simplistic kitchen timer because I was amazed by automatic watches.</p>
<p>I failed miserably. Which is fine because we had a mechanical kitchen timer anyway. I was curious how much miserably I failed and opened it. It is a remarkably great device! Learning the process of how all springs are connected was tremendous fun for me. And there is an endless list of videos on the internet!</p>
<p>You read it well: the result is no build and no kitchen timer anymore. I was not able to put it back together. It’s a too fine device for my skills (and tools).</p>
<p>But! No one can stop me from making an even cooler kitchen timer presenting Arduino, right? At least that I can do.</p>
<p>I used several parts for this project:</p>
<ul>
<li>7-segment display to set and show timer.</li>
<li>Potentiometer to set time.</li>
<li>Button to start and stop the timer.</li>
<li>Buzzer to play some lovely melody.</li>
</ul>
<p>You can check out the rough schema in the following picture. Note it might be a bit different in your case as it depends on what exactly you have around. For example, my display is connected over I2C instead of over serial. The schema also connects buzzer using transistor for cases when buzzer operates on different voltages than Arduino, something I didn’t have to do.</p>
<p><img alt="" src="/images/code/arduino/kitchentimer/circuit.png"></p>
<p>This schema can look like this when converted to the physical world. I used a simple matchbox as a case for this project. It is not perfect, but unique for sure.</p>
<p><img alt="" src="/images/code/arduino/kitchentimer/internals.jpg"></p>
<p>When the timer is plugged in, the first message is AHOJ. It perfectly fits the 7-segment 4-digit display and says <em>hello</em> in Czech (and other languages).</p>
<p><img alt="" src="/images/code/arduino/kitchentimer/hello.jpg"></p>
<p>The tricky part for me was to come up with an appropriate setting. My first version used a 1kΩ potentiometer, and the analog value mapped to the time range. The range was from zero to ten minutes with a jump of five seconds. That was too granular resolution range for too clumsy potentiometer range. The analog value was not so accurate and stable with just 1kΩ. Ten kilo-ohms were better but still not as good as the final solution with one hundred.</p>
<p>At least stability was good. Accuracy, not so much. The issue was still that my Arduino provides a range of zero to 1023. To cover every fifth second in ten minutes, I need over 100 steps. The UX is not so great then. A slight change on the potentiometer can be a pretty massive change of the value.</p>
<p>After testing, I found that good behavior is with 20 steps only. Now, the task was to find a way to fit a broad range into twenty steps only. First, seconds are not necessary. How often do you need second precision? Never. So I went directly to minutes. Suddenly, I could cover now twenty minutes.</p>
<p>But twenty minutes is not enough sometimes. Sometimes, I need a reminder in one hour. Our mechanical kitchen timer could do that. Mapping twenty steps to sixty minutes mean having a precision of three minutes. Not great for short times. Do you want your egg to be boiled precisely for eight minutes? Too bad…</p>
<p>Therefore, I came up with a solution: precision of one minute for the first ten minutes, and anything beyond jumps by five minutes up to one hour—a reasonable compromise of wide range and fine-tuning the short reminders.</p>
<p>Here is the essential part of the code:</p>
<div class="highlight"><pre><span></span><code> <span class="kt">int</span> <span class="n">value</span> <span class="o">=</span> <span class="n">analogRead</span><span class="p">(</span><span class="n">A0</span><span class="p">);</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">map</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">980</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">20</span><span class="p">);</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">min</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="o">+</span> <span class="n">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">value</span><span class="o">-</span><span class="mi">10</span><span class="p">)</span><span class="o">*</span><span class="mi">5</span><span class="p">;</span>
<span class="n">value</span> <span class="o">*=</span> <span class="mi">60</span><span class="p">;</span>
</code></pre></div>
<p>In the end, the setting was easy to tweak. It just needs some time to test a few options, see how it works, and find the best usage. I was more afraid of the melody. I wanted something nice, but my musical skill is not good, and I cannot simply play an mp3 file on Arduino. Moreover, with a simple buzzer!</p>
<p>The good news is, you don’t have to do everything alone. Arduino is quite popular, and almost everything you are trying to build, someone already made before you. You just need to use Google.</p>
<p>A small side note: If you feel this is cheating, well, it’s not. At least it depends on why you are even playing with Arduino. I have fun building stuff, but not when it takes ages, and I need to first learn tons of things before making a small hello world. In the end, using Arduino could also be cheating. For example, my <a href="/arduino-project-page-turner/">previous build</a> would be possible to do without it for sure.</p>
<p>So after a bit of googling, I found an excellent <a href="https://github.com/robsoncouto/arduino-songs">repository with charming melodies</a>! I liked Pink Panther the most for this build. Simple copy&paste and here is the final product:</p>
<video controls>
<source src="/images/code/arduino/kitchentimer/video.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<p>Here is the complete code except for the melody itself. Just copy the one you like from the mentioned repository and rename <code>setup</code> to <code>bzzz</code>. ;)</p>
<div class="highlight"><pre><span></span><code><span class="cp">#include</span> <span class="cpf"><TM1637Display.h></span><span class="cp"></span>
<span class="cp">#define CLK 4</span>
<span class="cp">#define DIO 5</span>
<span class="kt">long</span> <span class="n">lastPress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">long</span> <span class="n">timer</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">bool</span> <span class="n">timerRunning</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">SEG_AHOJ</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span>
<span class="n">SEG_A</span> <span class="o">|</span> <span class="n">SEG_B</span> <span class="o">|</span> <span class="n">SEG_C</span> <span class="o">|</span> <span class="n">SEG_E</span> <span class="o">|</span> <span class="n">SEG_F</span> <span class="o">|</span> <span class="n">SEG_G</span><span class="p">,</span> <span class="c1">// A</span>
<span class="n">SEG_B</span> <span class="o">|</span> <span class="n">SEG_C</span> <span class="o">|</span> <span class="n">SEG_E</span> <span class="o">|</span> <span class="n">SEG_F</span> <span class="o">|</span> <span class="n">SEG_G</span><span class="p">,</span> <span class="c1">// H</span>
<span class="n">SEG_A</span> <span class="o">|</span> <span class="n">SEG_B</span> <span class="o">|</span> <span class="n">SEG_C</span> <span class="o">|</span> <span class="n">SEG_D</span> <span class="o">|</span> <span class="n">SEG_E</span> <span class="o">|</span> <span class="n">SEG_F</span><span class="p">,</span> <span class="c1">// O</span>
<span class="n">SEG_B</span> <span class="o">|</span> <span class="n">SEG_C</span> <span class="o">|</span> <span class="n">SEG_D</span> <span class="o">|</span> <span class="n">SEG_E</span> <span class="c1">// J</span>
<span class="p">};</span>
<span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">SEG_DONE</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span>
<span class="n">SEG_B</span> <span class="o">|</span> <span class="n">SEG_C</span> <span class="o">|</span> <span class="n">SEG_D</span> <span class="o">|</span> <span class="n">SEG_E</span> <span class="o">|</span> <span class="n">SEG_G</span><span class="p">,</span> <span class="c1">// d</span>
<span class="n">SEG_A</span> <span class="o">|</span> <span class="n">SEG_B</span> <span class="o">|</span> <span class="n">SEG_C</span> <span class="o">|</span> <span class="n">SEG_D</span> <span class="o">|</span> <span class="n">SEG_E</span> <span class="o">|</span> <span class="n">SEG_F</span><span class="p">,</span> <span class="c1">// O</span>
<span class="n">SEG_C</span> <span class="o">|</span> <span class="n">SEG_E</span> <span class="o">|</span> <span class="n">SEG_G</span><span class="p">,</span> <span class="c1">// n</span>
<span class="n">SEG_A</span> <span class="o">|</span> <span class="n">SEG_D</span> <span class="o">|</span> <span class="n">SEG_E</span> <span class="o">|</span> <span class="n">SEG_F</span> <span class="o">|</span> <span class="n">SEG_G</span> <span class="c1">// E</span>
<span class="p">};</span>
<span class="n">TM1637Display</span> <span class="nf">display</span><span class="p">(</span><span class="n">CLK</span><span class="p">,</span> <span class="n">DIO</span><span class="p">);</span>
<span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
<span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">9600</span><span class="p">);</span>
<span class="n">pinMode</span><span class="p">(</span><span class="n">A0</span><span class="p">,</span> <span class="n">INPUT</span><span class="p">);</span>
<span class="n">pinMode</span><span class="p">(</span><span class="mi">11</span><span class="p">,</span> <span class="n">OUTPUT</span><span class="p">);</span>
<span class="n">display</span><span class="p">.</span><span class="n">setBrightness</span><span class="p">(</span><span class="mi">10</span><span class="p">);</span> <span class="c1">// Range: 0-15</span>
<span class="n">display</span><span class="p">.</span><span class="n">setSegments</span><span class="p">(</span><span class="n">SEG_AHOJ</span><span class="p">);</span>
<span class="n">attachInterrupt</span><span class="p">(</span><span class="n">digitalPinToInterrupt</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span> <span class="n">buttonPressed</span><span class="p">,</span> <span class="n">FALLING</span><span class="p">);</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">loop</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">timerRunning</span><span class="p">)</span> <span class="p">{</span>
<span class="n">loopSetTimer</span><span class="p">();</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">loopTimerRunning</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">loopSetTimer</span><span class="p">()</span> <span class="p">{</span>
<span class="kt">int</span> <span class="n">value</span> <span class="o">=</span> <span class="n">analogRead</span><span class="p">(</span><span class="n">A0</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">value</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">map</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">980</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">20</span><span class="p">);</span>
<span class="n">value</span> <span class="o">=</span> <span class="n">min</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="mi">10</span><span class="p">)</span> <span class="o">+</span> <span class="n">max</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">value</span><span class="o">-</span><span class="mi">10</span><span class="p">)</span><span class="o">*</span><span class="mi">5</span><span class="p">;</span>
<span class="n">value</span> <span class="o">*=</span> <span class="mi">60</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="n">timer</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">timer</span> <span class="o">=</span> <span class="n">value</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">timer</span> <span class="o">!=</span> <span class="n">value</span><span class="p">)</span> <span class="p">{</span>
<span class="n">timer</span> <span class="o">=</span> <span class="n">value</span><span class="p">;</span>
<span class="n">printTime</span><span class="p">(</span><span class="nb">false</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">delay</span><span class="p">(</span><span class="mi">50</span><span class="p">);</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">loopTimerRunning</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">timer</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">timer</span> <span class="o">-=</span> <span class="mi">1</span><span class="p">;</span>
<span class="n">printTime</span><span class="p">(</span><span class="nb">true</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="n">display</span><span class="p">.</span><span class="n">setSegments</span><span class="p">(</span><span class="n">SEG_DONE</span><span class="p">);</span>
<span class="n">bzzz</span><span class="p">();</span>
<span class="n">delay</span><span class="p">(</span><span class="mi">100</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">printTime</span><span class="p">(</span><span class="kt">bool</span> <span class="n">showDots</span><span class="p">)</span> <span class="p">{</span>
<span class="kt">uint8_t</span> <span class="n">segment</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span> <span class="p">};</span>
<span class="k">if</span> <span class="p">((</span><span class="n">timer</span><span class="o">/</span><span class="mi">60</span><span class="p">)</span><span class="o">/</span><span class="mi">10</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="n">segment</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">display</span><span class="p">.</span><span class="n">encodeDigit</span><span class="p">((</span><span class="n">timer</span><span class="o">/</span><span class="mi">60</span><span class="p">)</span><span class="o">/</span><span class="mi">10</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">segment</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">display</span><span class="p">.</span><span class="n">encodeDigit</span><span class="p">((</span><span class="n">timer</span><span class="o">/</span><span class="mi">60</span><span class="p">)</span><span class="o">%</span><span class="mi">10</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="n">dots</span><span class="p">)</span> <span class="p">{</span>
<span class="n">segment</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="n">display</span><span class="p">.</span><span class="n">encodeDigit</span><span class="p">((</span><span class="n">timer</span><span class="o">%</span><span class="mi">60</span><span class="p">)</span><span class="o">/</span><span class="mi">10</span><span class="p">);</span>
<span class="n">segment</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="n">display</span><span class="p">.</span><span class="n">encodeDigit</span><span class="p">((</span><span class="n">timer</span><span class="o">%</span><span class="mi">60</span><span class="p">)</span><span class="o">%</span><span class="mi">10</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">display</span><span class="p">.</span><span class="n">setSegments</span><span class="p">(</span><span class="n">segment</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">showDots</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">colonDelay</span><span class="p">();</span>
<span class="c1">// To show colon (:).</span>
<span class="n">segment</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">segment</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="mi">128</span><span class="p">;</span>
<span class="n">display</span><span class="p">.</span><span class="n">setSegments</span><span class="p">(</span><span class="n">segment</span><span class="p">);</span>
<span class="n">colonDelay</span><span class="p">();</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">colonDelay</span><span class="p">()</span> <span class="p">{</span>
<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o"><</span> <span class="mi">10</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">timerRunning</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">delay</span><span class="p">(</span><span class="mi">49</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">buttonPressed</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="n">lastPress</span> <span class="o">+</span> <span class="mi">500</span> <span class="o">></span> <span class="n">millis</span><span class="p">())</span> <span class="p">{</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="n">lastPress</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
<span class="n">timerRunning</span> <span class="o">=</span> <span class="o">!</span><span class="n">timerRunning</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>