<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>The Blog of DO3EET</title>
    <link>https://do3eet.pages.dev/en/</link>
    <description>Recent content on The Blog of DO3EET</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en</language>
    
    <lastBuildDate>Tue, 05 May 2026 14:41:25 +0200</lastBuildDate>
    
    
    <atom:link href="https://do3eet.pages.dev/en/index.xml" rel="self" type="application/rss+xml" />
    
    
    <item>
      <title>Punctuality in Conference Calls: A Plea for Mutual Respect</title>
      <link>https://do3eet.pages.dev/en/post/conferencepunctuality/</link>
      <pubDate>Tue, 05 May 2026 14:41:25 +0200</pubDate>
      <guid>https://do3eet.pages.dev/en/post/conferencepunctuality/</guid>
      <description><![CDATA[<p>To be honest, it&rsquo;s just getting on my nerves: it has become an absolute matter of course that at the beginning of every conference call, there&rsquo;s a five-minute wait to see if any more people deign to dial in.</p>
<p>I couldn&rsquo;t help but think of a quote by Horst Lichter (from the German talk show <em>Riverboat</em>) that hits the nail on the head:</p>
<blockquote>
<p>&ldquo;You have irretrievably stolen 20 minutes from my finite life that I could not spend in a self-determined way. That really stuck with me for the rest of my life, so that no matter when, no matter where, I&rsquo;m always early, because I can determine my own time, but I don&rsquo;t want to steal other people&rsquo;s.&rdquo;</p>
</blockquote>
<p>To finally end this madness in everyday working life, I&rsquo;ve come up with a few personal rules. Starting with what is often called &ldquo;Japanese punctuality&rdquo; – a philosophy we should urgently adopt:</p>
<h2 id="my-rules-for-better-conference-calls">
  
  
  My Rules for Better Conference Calls
  
</h2>
<h3 id="rule-1-1000-means-1000--not-im-starting-to-look-for-the-link-now">
  
  
  Rule 1: 10:00 means 10:00 – not &ldquo;I&rsquo;m starting to look for the link now&rdquo;
  
</h3>
<p>If a meeting is scheduled for 10:00, it means in Japan (and in my ideal world) that the actual content work starts at exactly 10:00. This requires that you don&rsquo;t start looking for the dial-in link at 10:00 and 30 seconds, only to find that the headset isn&rsquo;t connected or that Teams wants to install a critical update.</p>
<p>Punctuality means being <strong>ready</strong> when the clock strikes. It&rsquo;s a form of professionalism to do the tech check and the dial-in early enough to be present at the start. Anyone who clicks the link at 10:00 sharp is actually already late – because they are stealing the time from everyone else who has to wait for their personal &ldquo;setup time.&rdquo;</p>
<h3 id="rule-2-greet-first-then-silence-and-mute">
  
  
  Rule 2: Greet first, then silence (and mute)
  
</h3>
<p>As soon as you&rsquo;ve dialed in, a brief &ldquo;hello&rdquo; introduction is naturally good manners – provided the framework and group size allow it. But immediately after that, the first move should be to the mute button.</p>
<p>There&rsquo;s hardly anything more disruptive in a conference call than the rhythmic clattering of a mechanical keyboard, heavy breathing directly into the microphone, or the siren of an ambulance passing by the home office. Discipline in the conference also means acoustic discipline. Anyone who only opens their microphone when they really have something to contribute shows respect for the concentration of all other participants. Background noise is not just annoying; it&rsquo;s an avoidable disruptive factor that undermines the efficiency of the entire meeting.</p>
<h3 id="rule-3-video-with-care--bandwidth-is-a-precious-resource">
  
  
  Rule 3: Video with care – bandwidth is a precious resource
  
</h3>
<p>Another point that is often underestimated: the webcam. In times of &ldquo;video-first&rdquo; cultures, it may sound old-fashioned, but I plead for an economical use of the camera. It makes the most sense if primarily the person currently speaking transmits their video.</p>
<p>This has nothing to do with being camera-shy, but simply with technical respect. Every video signal consumes bandwidth – for all participants. No matter how &ldquo;thick&rdquo; your own line at home or in the office is, you never know under what conditions your colleagues are participating (perhaps via an unstable mobile hotspot on a train or in a region with poor network coverage). By limiting the flood of video, we ensure that the audio quality remains stable and the conference runs smoothly for everyone. This is also a form of appreciation: not unnecessarily straining the technical resources of the person opposite.</p>
<h3 id="rule-4-audio-quality-is-not-a-luxury-but-empathy">
  
  
  Rule 4: Audio quality is not a luxury, but empathy
  
</h3>
<p>Anyone who regularly takes part in digital meetings should invest in decent hardware. A good microphone is not just a technical gimmick, but a sign of respect and empathy towards the listeners.</p>
<p>There&rsquo;s little more exhausting than listening to a tinny, choppy, or noisy voice for 60 minutes that sounds like it&rsquo;s coming from a tin can in a tunnel. Every &ldquo;Excuse me?&rdquo;, &ldquo;Can you repeat that?&rdquo; or &ldquo;What did you say?&rdquo; not only costs time but also robs all participants of a massive amount of concentration. Anyone who ensures that they can be understood clearly and distinctly minimizes the cognitive load on their colleagues. You save them the energy they would otherwise have to spend laboriously filtering the words out of the noise. Good audio hardware is an investment in the time and nerves of all participants.</p>
<h3 id="rule-5-visual-calm--backgrounds-and-profile-pictures">
  
  
  Rule 5: Visual calm – backgrounds and profile pictures
  
</h3>
<p>Not only the acoustics but also the visuals play a role in concentration. Anyone using their webcam should ensure a calm, non-distracting background. Blurred motifs or simple, professional backgrounds are the best choice here. A cluttered shelf in the background or family members walking through the picture automatically draw attention away from what is being said.</p>
<p>The same applies to profile pictures: these should have a scope barely larger than a classic passport photo. A clear, calm portrait helps the other participants to assign the voice to a face without being distracted by too many details or restless surroundings in the small preview image. This is also an aspect of &ldquo;visual discipline&rdquo;: we reduce unnecessary input for our colleagues to the essentials.</p>
<h3 id="rule-6-the-raise-hand-function--discipline-in-large-groups">
  
  
  Rule 6: The &ldquo;Raise Hand&rdquo; function – discipline in large groups
  
</h3>
<p>Especially in larger groups or during heated discussions, the &ldquo;raise hand&rdquo; function is not a nice extra but should be an absolute requirement. Anyone who just starts talking risks not only interrupting others but often creates an acoustic chaos in which nobody is understood in the end.</p>
<p>Waiting patiently until called upon by the moderator or the speaking person is a sign of professionalism. It ensures a structured conversation culture in which every contribution gets the space it deserves – without degenerating into a digital &ldquo;whoever screams the loudest is heard.&rdquo; This also saves time in the end because fewer sentences have to be said twice or at the same time.</p>
<h3 id="rule-7-mandatory-agenda--no-goal-without-a-roadmap">
  
  
  Rule 7: Mandatory Agenda – No goal without a roadmap
  
</h3>
<p>A point concerning preparation: in my view, a clear agenda is an absolute requirement for every conference call. This starts with the title: a meaningful subject line helps all participants to mentally prepare for the topic.</p>
<p>Receiving an appointment that only contains a cryptic short title and the dial-in data is, in my opinion, simply unacceptable. All participants should know in advance which points will be discussed so that they can prepare. There is also a duty to cooperate here: anyone who has additions or changes to the agenda should report them promptly – and the creator must incorporate them.</p>
<p>Even in small 1:1 rounds, a short roadmap is helpful to avoid getting sidetracked. Without an agenda, a meeting quickly turns into a coffee klatch at the expense of working time. Only those who know the goal can take the direct route there.</p>
<h2 id="conclusion-time-is-life">
  
  
  Conclusion: Time is Life
  
</h2>
<p>At the end of the day, all these rules are not about mere etiquette or pedantic order. It&rsquo;s about <strong>respect</strong>. When we are on time, have our technology under control, and approach meetings in a structured way, we signal to our colleagues: &ldquo;I value your time. I know that you have other tasks and a life outside of this screen.&rdquo;</p>
<p>The quote from Horst Lichter may sound harsh at first, but it hits the heart of the matter. Every minute we spend silently in a waiting loop or lose through technical inadequacies is irretrievably gone. It&rsquo;s time we can&rsquo;t use for productive work and that we might miss at the end of the day to finish on time.</p>
<p>Let&rsquo;s stop viewing other people&rsquo;s time as an infinite resource that we can dispose of freely. Punctuality and digital discipline are the simplest ways to show appreciation in everyday working life. Let&rsquo;s just start tomorrow – preferably five minutes before the actual appointment.</p>
]]></description>
    </item>
    
    <item>
      <title>Weather Statistics of my Station</title>
      <link>https://do3eet.pages.dev/en/post/weather-stats/</link>
      <pubDate>Sun, 26 Apr 2026 15:16:51 +0200</pubDate>
      <guid>https://do3eet.pages.dev/en/post/weather-stats/</guid>
      <description><![CDATA[<p>In this post, I take a look at the collected data from my weather station. I have cleaned the CSV exports and transferred them into a SQLite database to create comparative statistics and charts.</p>
  
  <img src="https://do3eet-media.dreamofjapan.de/posts/weather-stats/PXL_20220915_154306128.jpg" alt="The town hall in Grimma with light cloud cover" width="250" height="445" loading="lazy">
<p>It is amazing what you can see when you look at the data over several years (2022 to present). <em>We are again a bit nerdy on the terminal, using Python and Matplotlib for the visualization.</em></p>
<p>Here are the comparative evaluations of daily maximum and minimum temperatures for each month. The colors represent the different years:</p>
<ul>
<li><strong>2022</strong>: Purple (Data from November)</li>
<li><strong>2023</strong>: Green</li>
<li><strong>2024</strong>: Cyan</li>
<li><strong>2025</strong>: Pink</li>
<li><strong>2026</strong>: Yellow/Orange (Data until April)</li>
</ul>
<h2 id="yearly-overviews-smoothed">
  
  
  Yearly Overviews (Smoothed)
  
</h2>
<p>To provide a better overview of the trends, this summary shows the progression over the entire year. The data has been smoothed by dividing each month into 5 segments (approx. 6-day intervals). This removes the daily &ldquo;spikes&rdquo; and makes the seasonal trend visible.</p>
<p>My database contains some interesting extremes: The <strong>hottest day</strong> so far was <strong>July 2nd, 2025, with a solid 36.2 °C</strong>. On the other hand, <strong>January 11th, 2026</strong>, holds the cold record with a frosty <strong>-17.7 °C</strong>.</p>
  
  <img src="https://do3eet-media.dreamofjapan.de/posts/weather-stats/panoramio-47372951.jpg" alt="The Lutherweg in winter – matching the frosty record temperatures" width="250" height="374" loading="lazy">
<p><img src="/post/weather-stats/year_overview_max.svg" alt="Yearly Overview Max"></p>
<p>Looking at the average temperatures, it is clear that 2024, with an average of approx. 11.5 °C, was slightly warmer than 2023 (11.2 °C).</p>
<p><img src="/post/weather-stats/year_overview_avg.svg" alt="Yearly Overview Avg">
<img src="/post/weather-stats/year_overview_min.svg" alt="Yearly Overview Min"></p>
<h2 id="rainfall-comparison">
  
  
  Rainfall Comparison
  
</h2>
<p>Besides temperature, rainfall is one of the most important parameters. <strong>December 2023</strong> stands out, being the wettest month in my records with a total of <strong>122.4 mm</strong> of precipitation.</p>
<p>The diagrams show the total monthly amount and the number of days it rained (at least 0.1 mm). It is quite visible that 2024 was generally a &ldquo;wet&rdquo; year compared to the others.</p>
<p><img src="/post/weather-stats/year_overview_rain_total.svg" alt="Monthly Rainfall">
<img src="/post/weather-stats/year_overview_rainy_days.svg" alt="Number of Rainy Days"></p>
<h2 id="sunshine-comparison">
  
  
  Sunshine Comparison
  
</h2>
<p>How many hours does the sun shine? For this analysis, I count every period where the light intensity at the station reached at least 20 klux as a &ldquo;sunshine hour&rdquo;. This is a good indicator for direct sunlight or at least a very bright sky.</p>
<p>The <strong>sunniest year so far was 2024</strong> with a total of <strong>1,421 sunshine hours</strong>. The diagram below shows the smoothed daily averages per month.</p>
<p><img src="/post/weather-stats/year_overview_sun.svg" alt="Yearly Overview Sunshine"></p>
<h2 id="monthly-detailed-views">
  
  
  Monthly Detailed Views
  
</h2>
<p>For those who want to know more, you can find the raw daily values in direct comparison across the years here. You can also see the gaps in the data where the station didn&rsquo;t feel like logging or the server had a hiccup.</p>
<h3 id="january">
  
  
  January
  
</h3>
<p><img src="/post/weather-stats/temp_max_01_januar.svg" alt="January Max">
<img src="/post/weather-stats/temp_avg_01_januar.svg" alt="January Avg">
<img src="/post/weather-stats/temp_min_01_januar.svg" alt="January Min">
<img src="/post/weather-stats/sun_hours_01_januar.svg" alt="Januar Sunshine Hours"></p>
<h3 id="february">
  
  
  February
  
</h3>
<p><img src="/post/weather-stats/temp_max_02_februar.svg" alt="February Max">
<img src="/post/weather-stats/temp_avg_02_februar.svg" alt="February Avg">
<img src="/post/weather-stats/temp_min_02_februar.svg" alt="February Min">
<img src="/post/weather-stats/sun_hours_02_februar.svg" alt="Februar Sunshine Hours"></p>
<h3 id="march">
  
  
  March
  
</h3>
<p><img src="/post/weather-stats/temp_max_03_m%C3%A4rz.svg" alt="March Max">
<img src="/post/weather-stats/temp_avg_03_m%C3%A4rz.svg" alt="March Avg">
<img src="/post/weather-stats/temp_min_03_m%C3%A4rz.svg" alt="March Min">
<img src="/post/weather-stats/sun_hours_03_m%C3%A4rz.svg" alt="März Sunshine Hours"></p>
<h3 id="april">
  
  
  April
  
</h3>
<p><img src="/post/weather-stats/temp_max_04_april.svg" alt="April Max">
<img src="/post/weather-stats/temp_avg_04_april.svg" alt="April Avg">
<img src="/post/weather-stats/temp_min_04_april.svg" alt="April Min">
<img src="/post/weather-stats/sun_hours_04_april.svg" alt="April Sunshine Hours"></p>
<h3 id="may">
  
  
  May
  
</h3>
<p><img src="/post/weather-stats/temp_max_05_mai.svg" alt="May Max">
<img src="/post/weather-stats/temp_avg_05_mai.svg" alt="May Avg">
<img src="/post/weather-stats/temp_min_05_mai.svg" alt="May Min">
<img src="/post/weather-stats/sun_hours_05_mai.svg" alt="Mai Sunshine Hours"></p>
<h3 id="june">
  
  
  June
  
</h3>
<p><img src="/post/weather-stats/temp_max_06_juni.svg" alt="June Max">
<img src="/post/weather-stats/temp_avg_06_juni.svg" alt="June Avg">
<img src="/post/weather-stats/temp_min_06_juni.svg" alt="June Min">
<img src="/post/weather-stats/sun_hours_06_juni.svg" alt="Juni Sunshine Hours"></p>
<h3 id="july">
  
  
  July
  
</h3>
<p><img src="/post/weather-stats/temp_max_07_juli.svg" alt="July Max">
<img src="/post/weather-stats/temp_avg_07_juli.svg" alt="July Avg">
<img src="/post/weather-stats/temp_min_07_juli.svg" alt="July Min">
<img src="/post/weather-stats/sun_hours_07_juli.svg" alt="Juli Sunshine Hours"></p>
<h3 id="august">
  
  
  August
  
</h3>
<p><img src="/post/weather-stats/temp_max_08_august.svg" alt="August Max">
<img src="/post/weather-stats/temp_avg_08_august.svg" alt="August Avg">
<img src="/post/weather-stats/temp_min_08_august.svg" alt="August Min">
<img src="/post/weather-stats/sun_hours_08_august.svg" alt="August Sunshine Hours"></p>
<h3 id="september">
  
  
  September
  
</h3>
<p><img src="/post/weather-stats/temp_max_09_september.svg" alt="September Max">
<img src="/post/weather-stats/temp_avg_09_september.svg" alt="September Avg">
<img src="/post/weather-stats/temp_min_09_september.svg" alt="September Min">
<img src="/post/weather-stats/sun_hours_09_september.svg" alt="September Sunshine Hours"></p>
<h3 id="october">
  
  
  October
  
</h3>
<p><img src="/post/weather-stats/temp_max_10_oktober.svg" alt="October Max">
<img src="/post/weather-stats/temp_avg_10_oktober.svg" alt="October Avg">
<img src="/post/weather-stats/temp_min_10_oktober.svg" alt="October Min">
<img src="/post/weather-stats/sun_hours_10_oktober.svg" alt="Oktober Sunshine Hours"></p>
<h3 id="november">
  
  
  November
  
</h3>
<p><img src="/post/weather-stats/temp_max_11_november.svg" alt="November Max">
<img src="/post/weather-stats/temp_avg_11_november.svg" alt="November Avg">
<img src="/post/weather-stats/temp_min_11_november.svg" alt="November Min">
<img src="/post/weather-stats/sun_hours_11_november.svg" alt="November Sunshine Hours"></p>
<h3 id="december">
  
  
  December
  
</h3>
<p><img src="/post/weather-stats/temp_max_12_dezember.svg" alt="December Max">
<img src="/post/weather-stats/temp_avg_12_dezember.svg" alt="December Avg">
<img src="/post/weather-stats/temp_min_12_dezember.svg" alt="December Min">
<img src="/post/weather-stats/sun_hours_12_dezember.svg" alt="Dezember Sunshine Hours"></p>]]></description>
    </item>
    
    <item>
      <title>Review: Chemnitzer Linux-Tage 2026</title>
      <link>https://do3eet.pages.dev/en/post/clt2026/</link>
      <pubDate>Tue, 31 Mar 2026 21:00:00 +0200</pubDate>
      <guid>https://do3eet.pages.dev/en/post/clt2026/</guid>
      <description><![CDATA[<p>The Chemnitzer Linux-Tage 2026 have come to a close, and it was once again a fantastic weekend of learning, sharing, and community.</p>
<p>In this post, I want to look back at my experiences and highlights of the event.</p>
<h2 id="friday-march-27th">
  
  
  Friday, March 27th
  
</h2>
<p>My journey to this year&rsquo;s Linux-Tage began on Friday afternoon. I set off from Grimma at exactly 3:00 PM. The drive took me along country roads via Colditz, Geithain, and Narsdorf before finally hitting the A72 motorway toward Chemnitz.</p>
<iframe title="Road to CLT2026" width="560" height="315" src="https://tube.tchncs.de/videos/embed/beCGFiCtbdQ9xU5v8zqWjE?loop=1&autoplay=1&muted=1&title=0&warningTitle=0&peertubeLink=0&p2p=0" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
<p>My first stop in Chemnitz was the TU&rsquo;s main lecture hall building (Hörsaalgebäude). As soon as I arrived, I picked up my event badge – the official starting signal for a long and eventful weekend as a volunteer and exhibitor.</p>
<p>After that, I checked into my hotel. As soon as I got to my room, I set up the most essential equipment first: my D-Star hotspot and my radio. This meant I was immediately back &ldquo;QRV&rdquo; in Chemnitz and ready for some exchanges over the air.</p>
<p>However, I didn&rsquo;t stay in my room for long, as the traditional pre-event at <strong>Turmbrauhaus</strong> started at 6:30 PM. As in previous years, the upper floor was packed. With great food (luckily the menu has everything from meat to vegan), it was the perfect time for those first deep conversations with other attendees who had also arrived on Friday. It&rsquo;s the ideal way to get into the right mindset for the upcoming two days.</p>
<p>A big thank you to Silke for the great post to set the mood:</p>
    
    
        
        
        
        
            
            
                
                
                
                    
                    
                
                
                    <blockquote class="toot-blockquote" cite="https://mastodon.social@freiefunken/status/116193523212185171">
                        <div class="toot-header">
                            <a class="toot-profile" href="https://mastodon.social/@freiefunken" rel="noopener">
                                <img
                                    src="https://files.mastodon.social/accounts/avatars/107/410/856/905/107/301/original/c60ef40eed345ef9.jpg"
                                    alt="Mastodon avatar for @freiefunken@mastodon.social"
                                    loading="lazy"
                                />
                            </a>
                            <span class="toot-author">
                                <a class="toot-author-name" href="https://mastodon.social/@freiefunken" rel="noopener">Silke</a>
                                <a class="toot-author-handle" href="https://mastodon.social/@freiefunken" rel="noopener">@freiefunken@mastodon.social</a>
                            </span>
                        </div>
                        
                        <div class="sr-only">
                            Mastodon Post von User Silke (@freiefunken@mastodon.social)
                        </div>
                        <div class="toot-prompt" aria-hidden="true">
                            <span class="prompt-user">mastodon</span><span class="prompt-host">@mastodon.social</span><span class="prompt-separator">:</span><span class="prompt-dir">~</span><span class="prompt-char">$</span> cat toot.txt
                        </div>
                        <p>Du kommst zu den Chemnitzer Linuxtagen und reist auch schon am Freitag an?<br />Komm doch zum Abendessen ins Turmbrauhaus!</p><p>Wir sind in der oberen Etage im vorderen, den Fenstern zugewandten Bereich (nicht in der Mälzerstube).</p><p>Zeit: Freitag, dem 27. März 2026 ab 18:30 Uhr<br />Ort: Turmbrauhaus, Neumarkt 2, 09111 Chemnitz<br /><a href="https://www.openstreetmap.org/way/1014038458" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://www.</span><span class="ellipsis">openstreetmap.org/way/10140384</span><span class="invisible">58</span></a><br />Speisekarte (alles von Fleisch bis vegan): <a href="https://turmbrauhaus.de/wp-content/uploads/2025/11/2025_11_internet_karte.pdf" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://</span><span class="ellipsis">turmbrauhaus.de/wp-content/upl</span><span class="invisible">oads/2025/11/2025_11_internet_karte.pdf</span></a></p><p>Gerne boosten und weitersagen!</p><p><a href="https://mastodon.social/tags/CLT2026" class="mention hashtag" rel="tag">#<span>CLT2026</span></a> <a href="https://mastodon.social/tags/chemnitz" class="mention hashtag" rel="tag">#<span>chemnitz</span></a> <a href="https://mastodon.social/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a> <a href="https://mastodon.social/tags/freesoftware" class="mention hashtag" rel="tag">#<span>freesoftware</span></a> <a href="https://mastodon.social/tags/turmbrauhaus" class="mention hashtag" rel="tag">#<span>turmbrauhaus</span></a></p>
                        
                        <div class="toot-footer">
                            <a href="https://mastodon.social/@freiefunken/116193523212185171" class="toot-date" rel="noopener">12:21 PM • March 8, 2026</a>&nbsp;<span class="pokey">(UTC)</span>
                        </div>
                    </blockquote>
                
                
            
        
    
<p>Another interesting find of the evening was a flyer for the <strong>3. Open Source Hardware Conference (OSHap)</strong>, taking place from September 23rd to 24th, 2026, in Halle (Saale). Under the motto “Think open. Build open.”, they are currently calling for participation (Call for Participation). Since the topic of Open Source Hardware aligns perfectly with my interests (like the Sharp PC-E500S or RISC-V), I’m seriously considering attending the event in Halle this September. More information is available at <a href="https://www.oshop-network.de">www.oshop-network.de</a>.</p>
  
  <img src="https://do3eet-media.dreamofjapan.de/posts/CLT2026/EinladungHalle.jpg/EinladungHalle.jpg" alt="Flyer for OSHap 2026 in Halle" width="600" height="600" loading="lazy">
<h2 id="saturday-march-28th">
  
  
  Saturday, March 28th
  
</h2>
<p>The first full day of the event was particularly busy but also very exciting for me. This year, I was active as a volunteer in the <strong>VOC team</strong> (Video Operation Center).</p>
<p>My main task was live-editing the streams of the talks. This involves switching between different camera angles and the speakers&rsquo; slides to ensure that online viewers can follow the presentations as smoothly as possible.</p>
<p>My first assignment of the day started at 11:00 AM in Room V6. On the schedule was a highly topical subject: <strong>&ldquo;The Cryptocalypse: Post-Quantum Cryptography and Open Source&rdquo;</strong>. This 60-minute session by Stefan Schumacher covered the massive challenges that quantum computers pose to our current encryption methods.</p>
<p>Here’s a quick summary of the key takeaways:</p>
<ul>
<li><strong>The Problem:</strong> Algorithms like <strong>Shor</strong> (breaking RSA and ECC) and <strong>Grover</strong> (halving the security of symmetric methods like AES) mean that quantum computers threaten the foundations of our digital security.</li>
<li><strong>The Solution (PQC):</strong> The NIST standardization process has already announced the first winners. These include <strong>ML-KEM (Kyber)</strong> for key encapsulation, and <strong>ML-DSA (Dilithium)</strong> and <strong>SLH-DSA (Sphincs+)</strong> for digital signatures.</li>
<li><strong>Open Source Pioneers:</strong> Projects like <strong>Open Quantum Safe (OQS)</strong> are playing a vital role. With the <code>oqsprovider</code> for OpenSSL, hybrid methods combining classic and post-quantum cryptography can already be used today.</li>
<li><strong>Recommendations:</strong> We should focus on <strong>crypto-agility</strong>, implement hybrid schemes, and create a &ldquo;crypto inventory&rdquo; to know where encryption is being used across our systems.</li>
</ul>
<p>It was technically demanding, which made it an exciting challenge for my first live edit of the day.</p>
<p>In the afternoon, at 2:00 PM in Room V5, we moved on to a practical topic for network enthusiasts: <strong>&ldquo;Containerlab – Simulating Datacenter Networks in the Lab&rdquo;</strong>. Robert Sander (Heinlein Consulting) provided a deep dive into this project, which started at Nokia five years ago and has since built a strong community.</p>
<p>What’s particularly fascinating about Containerlab is the ability to map complex data center structures using resource-efficient Linux containers. Since modern router hardware often uses the Linux kernel, many vendors provide their firmware as container images. For anything that doesn&rsquo;t run natively on Linux, traditional VMs can also be integrated. Sitting at the video switcher, it was interesting to see how quickly and efficiently entire topologies can be spun up and down without needing a rack full of hardware.</p>
<p>The conclusion of my VOC shift that day was at 3:00 PM (also in Room V5) with the talk <strong>&ldquo;bindzwirn: Linux Port Permissions via eBPF&rdquo;</strong> by Pluto.</p>
<p>In self-hosting scenarios, whether at home or on a VPS, isolating services through separate Unix accounts (e.g., using rootless Podman containers) is best practice. The issue is that Linux lacks native permissions for IP ports. Pluto presented <a href="https://codeberg.org/bindzwirn/bindzwirn">bindzwirn</a>, a modern eBPF-based drop-in replacement for the aging <code>authbind</code>. Thanks to eBPF, access to specific ports can be restricted to specific accounts, effectively preventing attacks from compromised services. A very exciting project that demonstrates how powerful eBPF is for elegantly extending kernel functionality. (And I secretly wondered: is Pluto actually a fan of Neil deGrasse Tyson? 😉)</p>
<h3 id="relaxed-evening-chemnitzer-catering-tage">
  
  
  Relaxed Evening: &ldquo;Chemnitzer Catering-Tage&rdquo;
  
</h3>
<p>Following Saturday&rsquo;s official program, there was a wonderful evening event for all volunteers, speakers, exhibitors, and sponsors. As is tradition, it was jokingly referred to as &ldquo;Chemnitzer Catering-Tage&rdquo; (Chemnitz Catering Days) – the food was excellent once again and provided the perfect setting for some relaxed networking away from the exhibition floor&rsquo;s hustle and bustle.</p>
  
  <img src="https://do3eet-media.dreamofjapan.de/posts/CLT2026/CCT.jpg/CCT.jpg" alt="Poster for the Chemnitzer Catering-Tage" width="600" height="600" loading="lazy">
<h2 id="a-special-highlight-overnight-hardware-rescue">
  
  
  A Special Highlight: Overnight Hardware Rescue
  
</h2>
<p>In between the event days, I had a very personal success story. I had brought along my old <strong>Sharp PC-E500S Pocket Computer</strong>, which unfortunately had lost its display contrast and showed ugly black bars – a typical aging issue with the capacitors.</p>
<p>As I didn&rsquo;t trust myself with the delicate repair job, I sought help at the <strong>soldering workshop</strong>. An incredibly helpful participant (whose name I unfortunately forgot in all the excitement) offered to take a look at it overnight.</p>
<p>And it worked: He replaced the two faulty 3.3μF SMD electrolytic capacitors (16V and 25V) with robust tantalum ones. The next day, I was greeted by a flawless, clear display! I&rsquo;m absolutely thrilled, as I plan to use this classic device as a <strong>Ham Logbook</strong> for amateur radio later. A huge thank you to my unknown rescuer!</p>
<h2 id="sunday-march-29th">
  
  
  Sunday, March 29th
  
</h2>
<p>On Sunday, I switched perspectives: from the VOC team&rsquo;s video switcher to the stage. At 11:00 AM in Room K2, I hosted the <strong>Keysigning Party</strong> I had organized.</p>
<p>It’s always impressive to see how many people are passionate about digital sovereignty and encryption. My goal was to strengthen the <strong>Web of Trust</strong>. The process is as analog as it is effective:</p>
<ol>
<li><strong>Hash Sum Verification:</strong> First, we ensured that every participant had the same, up-to-date key list.</li>
<li><strong>Identity Verification:</strong> In a long line, participants compared fingerprints and verified identities using official photo IDs.</li>
<li><strong>Building Trust:</strong> This face-to-face interaction is the foundation for later signing other participants&rsquo; keys at home with a clear conscience.</li>
</ol>
<p>As the organizer, my job was to lead through the process, answer GnuPG-related questions, and ensure everything ran smoothly. Despite the technical nature of the topic, the focus was on human interaction.</p>
<h3 id="ctf-challenge-with-a-handicap">
  
  
  CTF Challenge with a Handicap
  
</h3>
<p>The <strong>CTF-Challenge by secunet Security Networks AG</strong> also kept me busy on Sunday. As it is every year, it was a major highlight for me, even though the challenge was twofold this time: I had to deal with massive firmware issues with my laptop&rsquo;s WiFi chip (which unfortunately are still ongoing). Despite this technical handicap, I managed to solve several levels, even if I couldn&rsquo;t complete the entire challenge this time around.</p>
<p>Here&rsquo;s a glimpse into the technical puzzles I had to crack:</p>
<ul>
<li><strong>Getting Started &amp; Hashing:</strong> After the initial simple steps, it was all about extracting passwords from text files or calculating them using deprecated hash algorithms like MD5 from binary data.</li>
<li><strong>Archive Gymnastics:</strong> One level involved analyzing a bzip2 archive that, once decompressed, turned into a massive, mostly empty file. The solution was hidden in scattered ASCII characters buried at specific offsets.</li>
<li><strong>Socket Voodoo:</strong> Particularly exciting was accessing a Wireguard Unix socket (<code>/var/run/wireguard/wg0.sock</code>). By sending specific commands using <code>socat</code>, I was able to extract the private key needed for the next level.</li>
<li><strong>Metadata Forensics:</strong> In a higher level, I had to inspect 64 directories for their timestamps. Only by meticulously comparing <code>Modify</code> vs. <code>Change</code> times could the correct puzzle pieces for the next password be assembled.</li>
<li><strong>SSH Tricks:</strong> Finally, it came down to manipulating SSH environment variables (<code>SendEnv</code>) to bypass shell restrictions.</li>
</ul>
<p>It&rsquo;s the spirit of the competition that counts, and even with the handicap, it was a blast once again!</p>
<p>After the party, I stayed until the official end of the event. At 6:00 PM sharp, as the doors were closed, I headed home with many new impressions.</p>
<h2 id="conclusion">
  
  
  Conclusion
  
</h2>
<p>It was great to be back in Chemnitz! In addition to all the local experiences, there was also a major technical change to this website during the weekend: All graphics and images have moved from GitHub to <strong>Cloudflare R2 storage</strong>. This step was necessary as the Git repository was becoming too large due to the many images. While they have found their new home there, I&rsquo;m not yet entirely sure if everything is working exactly as I&rsquo;d like regarding caching and S3. I&rsquo;ll likely need to do some fine-tuning over the next few days.</p>
]]></description>
    </item>
    
    <item>
      <title>Offlinetags: An Overview of All 6 Privacy Signals</title>
      <link>https://do3eet.pages.dev/en/post/offlinetags-ueberblick/</link>
      <pubDate>Mon, 23 Mar 2026 20:27:00 +0100</pubDate>
      <guid>https://do3eet.pages.dev/en/post/offlinetags-ueberblick/</guid>
      <description><![CDATA[<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f92672">[</span>frank@do3eet-terminal ~<span style="color:#f92672">]</span>$ cat /etc/privacy/offlinetags.conf
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Consent Communication Protocol v2.0</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># STATUS: ALL 6 TAGS ACTIVE</span>
</span></span></code></pre></div><p>If you spend a lot of time at barcamps, hackathons, or amateur radio meetings, you know the problem: Am I allowed to post this photo? Who is that in the background? And how do I signal myself what should happen with pictures of me?</p>
<p>This is exactly where <strong>Offlinetags</strong> come in. They are a social signal for humans – and at the same time, machine-readable markers for algorithms. Since I submitted a <strong>Pull Request</strong> today to expand the set to <strong>6 symbols</strong>, here is the guide for all photographers and tag-wearers (including the new proposals).</p>
<hr>
<style>
  .ot-row {
    display: flex;
    align-items: flex-start;
    gap: 2rem;
    margin-bottom: 3rem;
  }
  .ot-tag {
    flex: 0 0 150px;
    text-align: center;
  }
  .ot-tag img {
    width: 150px;
    height: auto;
    border: 2px solid var(--terminal-border);
    border-radius: 8px;
    background: rgba(0, 0, 0, 0.2);
  }
  .ot-content {
    flex: 1;
  }
  .ot-content h3 {
    margin-top: 0 !important;
  }
  @media (max-width: 40rem) {
    .ot-row {
      flex-direction: column;
      align-items: center;
      text-align: center;
    }
    .ot-tag {
      flex: 0 0 auto;
    }
  }
</style>
<div class="ot-row">
  <div class="ot-tag">
    <img src="https://do3eet-media.dreamofjapan.de/posts/offlinetags-ueberblick/TagMe.svg" alt="Tag Me">
  </div>
  <div class="ot-content">
    <h3>1. Tag Me (Green)</h3>
    <p><i>The "all-round carefree package" for digital utilization.</i></p>
    <ul>
      <li><strong>May:</strong> Take photos, publish, tag, and apply facial recognition procedures.</li>
      <li><strong>Should ask:</strong> In very sensitive contexts, briefly check if a specific platform (e.g., Facebook vs. Mastodon) is okay.</li>
      <li><strong>Do not:</strong> Actually nothing – the wish for digital visibility is clearly formulated here.</li>
    </ul>
  </div>
</div>
<div class="ot-row">
  <div class="ot-tag">
    <img src="https://do3eet-media.dreamofjapan.de/posts/offlinetags-ueberblick/UploadMe.svg" alt="Upload Me">
  </div>
  <div class="ot-content">
    <h3>2. Upload Me (Yellow)</h3>
    <p><i>Publication yes, tracking no.</i></p>
    <ul>
      <li><strong>May:</strong> Take the photo and post it publicly on the web.</li>
      <li><strong>Should ask:</strong> If you still want to mention the person by name (credits), you should check briefly.</li>
      <li><strong>Do not:</strong> Manual tagging in social networks or enriching with metadata for facial recognition.</li>
    </ul>
  </div>
</div>
<div class="ot-row">
  <div class="ot-tag">
    <img src="https://do3eet-media.dreamofjapan.de/posts/offlinetags-ueberblick/BlurMe.svg" alt="Blur Me">
  </div>
  <div class="ot-content">
    <h3>3. Blur Me (Blue)</h3>
    <p><i>Presence yes, identity no.</i></p>
    <ul>
      <li><strong>May:</strong> Photograph the person, <strong>provided</strong> they are made unrecognizable before publication.</li>
      <li><strong>Should ask:</strong> Whether simple pixelation is enough or if the face should be completely "covered."</li>
      <li><strong>Do not:</strong> Publish the original image without editing. Facial recognition algorithms must not run over this under any circumstances.</li>
    </ul>
  </div>
</div>
<div class="ot-row">
  <div class="ot-tag">
    <img src="https://do3eet-media.dreamofjapan.de/posts/offlinetags-ueberblick/NoPhotos.svg" alt="No Photos">
  </div>
  <div class="ot-content">
    <h3>4. No Photos (Red)</h3>
    <p><i>The clear boundary.</i></p>
    <ul>
      <li><strong>May:</strong> Point the camera in another direction.</li>
      <li><strong>Should ask:</strong> Nothing. The wish for privacy is absolute.</li>
      <li><strong>Do not:</strong> Any recording of the person. Also no "back views" or "you can only see a little bit" – respect for the wish comes first.</li>
    </ul>
  </div>
</div>
<hr>
<p><i>New (Proposed via Pull Request):</i></p>
<hr>
<div class="ot-row">
  <div class="ot-tag">
    <img src="https://do3eet-media.dreamofjapan.de/posts/offlinetags-ueberblick/NameMe.svg" alt="Name Me">
  </div>
  <div class="ot-content">
    <h3>5. Name Me (Purple)</h3>
    <p><i>Visibility for credits.</i></p>
    <ul>
      <li><strong>May:</strong> Photograph and publish.</li>
      <li><strong>Should ask:</strong> Exactly how the naming should be done (real name, callsign, or nickname?).</li>
      <li><strong>Do not:</strong> Publication without clear identification of the author or person depicted. Here's the "deal": photo for fame!</li>
    </ul>
  </div>
</div>
<div class="ot-row">
  <div class="ot-tag">
    <img src="https://do3eet-media.dreamofjapan.de/posts/offlinetags-ueberblick/BlurScreen.svg" alt="Blur Screen">
  </div>
  <div class="ot-content">
    <h3>6. Blur Screen (Teal)</h3>
    <p><i>The new standard for tech events.</i></p>
    <ul>
      <li><strong>May:</strong> Photograph the foreground (people, hardware).</li>
      <li><strong>Should ask:</strong> If you are unsure whether a screen in the background shows critical data (code, passwords, emails).</li>
      <li><strong>Do not:</strong> Publish uncensored screen content in the background. Before the image goes online, every monitor in the image area must be made unrecognizable.</li>
    </ul>
  </div>
</div>
<hr>
<p>It is particularly worth noting that the entire project has been consistently published under <a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0 1.0 (Public Domain)</a>. I think it&rsquo;s excellent that these symbols and the underlying idea are truly available to everyone without condition – no legal hurdles, entirely in the spirit of the public good.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f92672">[</span>frank@do3eet-terminal ~<span style="color:#f92672">]</span>$ systemctl status privacy-awareness
</span></span><span style="display:flex;"><span>● privacy-awareness.service - Respect <span style="color:#66d9ef">for</span> Human Signals
</span></span><span style="display:flex;"><span>   Active: active <span style="color:#f92672">(</span>running<span style="color:#f92672">)</span> since Mon 2026-03-23 13:37:00 CET
</span></span></code></pre></div><p>Offlinetags are a powerful tool to bring the discussion about digital privacy from theory into practice. Which tag will you wear at the next event?</p>
<p>Get typing (or radio me)!</p>
<p>By the way, I will definitely be wearing the <strong>Name Me</strong> and <strong>Blur Screen</strong> tags myself as soon as I find a good way to produce them as high-quality pins or buttons.</p>
<p>And honestly: I actually need a seventh tag: <strong>&ldquo;Can&rsquo;t handle praise&rdquo;</strong>&hellip; ;)</p>
<hr>
<h3 id="-call-for-participation-scientific-survey">
  
  
  📝 Call for Participation: Scientific Survey
  
</h3>
<p>Fittingly, there is currently an exciting online survey as part of a collaboration between the <strong>University of Salzburg</strong> and <strong>TU Chemnitz</strong>. It deals with the communication of photo preferences at public events.</p>
<ul>
<li><strong>Goal:</strong> Understanding attitudes towards taking photos/being photographed and the communication of personal preferences.</li>
<li><strong>Time required:</strong> approx. 5 minutes.</li>
<li><strong>Anonymity:</strong> The survey is completely anonymous.</li>
<li><strong>Deadline:</strong> Participation is possible until <strong>March 28, 2026</strong>.</li>
</ul>
<p>If you would like to support the research project, you can find the link here:<br>
👉 <strong><a href="https://bildungsportal.sachsen.de/umfragen/limesurvey/index.php/232125?lang=de">To the Survey (Limesurvey)</a></strong></p>
<p>Thank you for your support!</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f92672">[</span>frank@do3eet-terminal ~<span style="color:#f92672">]</span>$ logout
</span></span></code></pre></div>]]></description>
    </item>
    
    <item>
      <title>Cisco 3750G in the Homelab: A Rugged Path to IOS 15.0</title>
      <link>https://do3eet.pages.dev/en/post/ciscoswitchsetup/</link>
      <pubDate>Sat, 21 Mar 2026 21:50:43 +0100</pubDate>
      <guid>https://do3eet.pages.dev/en/post/ciscoswitchsetup/</guid>
      <description><![CDATA[<p>Used enterprise hardware is a staple for any homelab. Recently, I came across a <strong>Cisco WS-C3750G-24TS-S1U</strong>. A solid 24-port Gigabit switch with SFP uplinks and – quite excitingly – Layer 3 capabilities. However, getting it up and running presented some typical Cisco hurdles, especially concerning firmware updates and storage space.</p>
<h2 id="initial-state-ipbase-and-missing-cryptography">
  
  
  Initial State: IPBASE and Missing Cryptography
  
</h2>
<p>After the first boot via console cable, <code>show version</code> revealed:
<code>Cisco IOS Software, C3750 Software (C3750-IPBASE-M), Version 12.2(44)SE6</code></p>
<p>The acronym <strong>IPBASE</strong> is the core problem here. It&rsquo;s Cisco&rsquo;s entry-level feature set. Worse yet, without &ldquo;K9&rdquo; in the name, all cryptographic functions are missing. This means no SSH for the console and no HTTPS for the web interface. In a modern network, this is an absolute no-go. An attempt to activate HTTPS was promptly met with an error:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e">#ip http secure-server</span>
</span></span><span style="display:flex;"><span>                        ^
</span></span><span style="display:flex;"><span>% Invalid input detected at <span style="color:#e6db74">&#39;^&#39;</span> marker.
</span></span></code></pre></div><h2 id="the-update-marathon">
  
  
  The Update Marathon
  
</h2>
<p>My goal was version <strong>15.0(2)SE10</strong>, the last available version for this hardware series. Since jumping directly from 12.2 IPBASE to 15.0 is often problematic and I needed the K9 features, I took an intermediate step.</p>
<h3 id="step-1-laying-the-foundation-with-12255-ipservicesk9">
  
  
  Step 1: Laying the Foundation with 12.2(55) IPSERVICESK9
  
</h3>
<p>First, I installed an image from the 12.2 branch with the <strong>IPSERVICESK9</strong> feature set. This set unlocks Layer 3 features (routing) and, crucially, SSH/HTTPS. Since the image was available as a <code>.bin</code> file, a simple <code>copy</code> from my local HTTP server was sufficient:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g#copy http://192.168.254.11:8080/c3750-ipservicesk9-mz.122-55.SE12.bin flash:
</span></span></code></pre></div><h3 id="step-2-the-battle-for-every-megabyte-flash-limit">
  
  
  Step 2: The Battle for Every Megabyte (Flash Limit)
  
</h3>
<p>The Cisco 3750G (non-E/X models) has a hard limit of <strong>32 MB flash memory</strong>. This is extremely tight for modern IOS images. The <code>.tar</code> file for IOS 15.0, which contains both the image and the HTTP files for the web interface, almost exceeds this limit if another image is still on the flash.</p>
<p>Before I could risk the final update, I had to delete the just-installed 12.2 image to make room:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g#dir flash:
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ... shows approx. 21 MB free, not enough for the 15.0 TAR (approx. 22 MB) ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>3750g#delete flash:/c3750-ipservicesk9-mz.122-55.SE12.bin
</span></span></code></pre></div><h3 id="step-3-jumping-to-ios-150-with-archive">
  
  
  Step 3: Jumping to IOS 15.0 with <code>archive</code>
  
</h3>
<p>With the flash cleared, I could now use the <code>archive</code> command. This is preferable to a simple <code>copy</code> because it extracts the <code>.tar</code> file, installs all support files, and correctly sets the boot variable (<code>boot system</code>):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g#archive download-sw /overwrite /reload http://192.168.254.11:8080/c3750-ipservicesk9-tar.150-2.SE10.tar
</span></span></code></pre></div><p>After an automatic reload, the switch greeted me with the new system:
<code>Cisco IOS Software, C3750 Software (C3750-IPSERVICESK9-M), Version 15.0(2)SE10</code></p>
<h2 id="cleanup-corporate-vlans-and-legacy-baggage">
  
  
  Cleanup: Corporate VLANs and Legacy Baggage
  
</h2>
<p>The switch came from a large corporate environment. A <code>show vlan brief</code> displayed a long list of VLANs (e.g., <code>VLAN_10_PROD</code>, <code>OFFICE_NETWORK</code>, etc.). Since this information is stored in the <code>vlan.dat</code> file in flash, it survives a standard <code>erase startup-config</code>.</p>
<p>To truly start from scratch, I manually deleted the VLAN database:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g#delete flash:vlan.dat
</span></span><span style="display:flex;"><span>3750g#reload
</span></span></code></pre></div><h2 id="management-configuration-for-the-homelab">
  
  
  Management Configuration for the Homelab
  
</h2>
<p>Once the switch was &ldquo;clean&rdquo; and up to date, the standard settings followed:</p>
<ol>
<li><strong>Management IP:</strong> Assigned an IP in Vlan1 for access.</li>
<li><strong>SNMP:</strong> Configured for monitoring (e.g., with LibreNMS). Important: Only allow access via an ACL!
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e">#access-list 10 permit 192.168.254.0 0.0.0.255</span>
</span></span><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e">#snmp-server community secret RW 10</span>
</span></span></code></pre></div></li>
<li><strong>EnergyWise:</strong> Since the switch draws about 94 watts at idle, I configured EnergyWise to put unused ports into a lower power level.</li>
</ol>
<h2 id="pro-tips-for-the-homelab">
  
  
  Pro-Tips for the Homelab
  
</h2>
<p>When setting up a Cisco switch for your own lab, there are three often-overlooked points that can decide between frustration and success (and security).</p>
<h3 id="1-enabling-3rd-party-sfps">
  
  
  1. Enabling 3rd-Party SFPs
  
</h3>
<p>Cisco is known for accepting only its own SFP modules. If you insert a cheap third-party module (e.g., from FS.com), the port is often shut down with an error message (&ldquo;err-disable&rdquo;). With two magic commands, this restriction can be lifted:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e">#service unsupported-transceiver</span>
</span></span><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e">#no errdisable detect cause gbic-invalid</span>
</span></span></code></pre></div><p><em>Note: Cisco will issue a warning that no support is provided for non-Cisco hardware – in a homelab, however, this is usually of secondary importance.</em></p>
<h3 id="2-closing-a-security-gap-disabling-smart-install">
  
  
  2. Closing a Security Gap: Disabling Smart Install
  
</h3>
<p>Older IOS versions often have the &ldquo;Smart Install&rdquo; feature enabled. This has been noted over the years for critical security vulnerabilities (e.g., CVE-2018-0171) through which attackers can gain full control of the switch. Since this feature is not needed in a homelab anyway, it should absolutely be disabled:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e">#no vstack</span>
</span></span></code></pre></div><h3 id="3-enabling-ipv6-support-sdm-templates">
  
  
  3. Enabling IPv6 Support (SDM Templates)
  
</h3>
<p>An often-overlooked aspect of older Catalyst models is <strong>SDM (Switch Database Management)</strong>. Since hardware memory (TCAM) is limited, you must tell the switch what to prioritize it for. By default, the 3750G often doesn&rsquo;t support IPv6.</p>
<p>To make the switch future-proof, you need to change the SDM template:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e">#sdm prefer dual-ipv4-and-ipv6 default</span>
</span></span></code></pre></div><p><strong>Important:</strong> This change only takes effect after a restart (<code>reload</code>)! After the reboot, you can verify the success with <code>show sdm prefer</code> and by enabling IPv6 on an interface:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g#show sdm prefer
</span></span><span style="display:flex;"><span> The current template is <span style="color:#e6db74">&#34;desktop IPv4 and IPv6 default&#34;</span> template.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e">#interface vlan 1</span>
</span></span><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config-if<span style="color:#f92672">)</span><span style="color:#75715e">#ipv6 enable</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>3750g#show ipv6 interface brief
</span></span><span style="display:flex;"><span>Vlan1                      <span style="color:#f92672">[</span>up/up<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>    FE80::213:7FFF:FE59:E240  &lt;-- Link-local address is present!
</span></span></code></pre></div><h3 id="4-speeding-up-the-boot-process-stack-priority">
  
  
  4. Speeding Up the Boot Process (Stack Priority)
  
</h3>
<p>Since the 3750G is a stackable switch, it waits for a certain amount of time during boot for the &ldquo;Stack Master Election&rdquo; (<code>Waiting for Stack Master Election...</code>). Since usually only one switch is running in a homelab, you can speed up this process by setting the priority to the maximum value:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e">#switch 1 priority 15</span>
</span></span><span style="display:flex;"><span>Changing the Switch Priority of Switch Number <span style="color:#ae81ff">1</span> to <span style="color:#ae81ff">15</span>
</span></span><span style="display:flex;"><span>Do you want to <span style="color:#66d9ef">continue</span>?<span style="color:#f92672">[</span>confirm<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>New Priority has been set successfully
</span></span></code></pre></div><h3 id="5-ssh-and-the-key-issue">
  
  
  5. SSH and the Key Issue
  
</h3>
<p>For SSH to work, the switch needs an RSA key. Usually, you generate this manually with <code>crypto key generate rsa</code>. In my configuration, however, a self-signed trustpoint was already present:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>crypto pki trustpoint TP-self-signed-&lt;ID&gt;
</span></span><span style="display:flex;"><span> enrollment selfsigned
</span></span><span style="display:flex;"><span> rsakeypair TP-self-signed-&lt;ID&gt;
</span></span></code></pre></div><p>If SSH does not start directly for you, even though <code>transport input ssh</code> is set, this command usually helps (after a hostname and a domain name have been set):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e">#crypto key generate rsa</span>
</span></span></code></pre></div><p>A key length of at least <strong>2048 bits</strong> is recommended here to satisfy modern SSH clients (like current OpenSSH versions).</p>
<h3 id="6-solving-ssh-connection-issues-legacy-cryptography">
  
  
  6. Solving SSH Connection Issues (Legacy Cryptography)
  
</h3>
<p>Anyone trying to connect to the switch via SSH from a modern Linux or macOS system will often be greeted with error messages like <code>no matching key exchange method found</code> or <code>no matching host key type found</code>. This is because the IOS 15.0 SSH stack uses methods that are considered outdated (insecure) today and are disabled by default by modern clients.</p>
<p>To get in anyway, you must explicitly allow the SSH client to use these old methods:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ssh -oKexAlgorithms<span style="color:#f92672">=</span>+diffie-hellman-group14-sha1 <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -oHostKeyAlgorithms<span style="color:#f92672">=</span>+ssh-rsa <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    -oCiphers<span style="color:#f92672">=</span>+aes256-cbc <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>    root@192.168.254.1
</span></span></code></pre></div><p>Alternatively, you can create an entry in your local <code>~/.ssh/config</code> for the switch so you don&rsquo;t have to type these parameters every time:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Host 192.168.254.1
</span></span><span style="display:flex;"><span>    KexAlgorithms +diffie-hellman-group14-sha1
</span></span><span style="display:flex;"><span>    HostkeyAlgorithms +ssh-rsa
</span></span><span style="display:flex;"><span>    Ciphers +aes256-cbc
</span></span></code></pre></div><h3 id="7-the-antenna-analyzer-for-network-cables-tdr">
  
  
  7. The &ldquo;Antenna Analyzer&rdquo; for Network Cables (TDR)
  
</h3>
<p>As a ham radio operator (DO3EET), I love measurement tools. Did you know that the 3750G has a built-in TDR (Time Domain Reflectometry) meter? It can send electrical pulses through the cable to measure its length or find cable breaks with centimeter precision – even while the computer is still connected!</p>
<p>Here are two examples from my test run:</p>
<p><strong>Example 1: A working 1m patch cable (Port 1)</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g#show cable-diagnostics tdr interface gigabitEthernet 1/0/1
</span></span><span style="display:flex;"><span>Interface Speed Local pair Pair length        Remote pair Pair status
</span></span><span style="display:flex;"><span>--------- ----- ---------- ------------------ ----------- --------------------
</span></span><span style="display:flex;"><span>Gi1/0/1   1000M Pair A     <span style="color:#ae81ff">0</span>    +/- <span style="color:#ae81ff">4</span>  meters Pair A      Normal              
</span></span><span style="display:flex;"><span>                Pair B     <span style="color:#ae81ff">0</span>    +/- <span style="color:#ae81ff">4</span>  meters Pair B      Normal              
</span></span><span style="display:flex;"><span>                Pair C     <span style="color:#ae81ff">0</span>    +/- <span style="color:#ae81ff">4</span>  meters Pair C      Normal              
</span></span><span style="display:flex;"><span>                Pair D     <span style="color:#ae81ff">1</span>    +/- <span style="color:#ae81ff">4</span>  meters Pair D      Normal              
</span></span></code></pre></div><p><strong>Example 2: An open, approx. 24m long installation cable (Port 3)</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g#show cable-diagnostics tdr interface gigabitEthernet 1/0/3
</span></span><span style="display:flex;"><span>Interface Speed Local pair Pair length        Remote pair Pair status
</span></span><span style="display:flex;"><span>--------- ----- ---------- ------------------ ----------- --------------------
</span></span><span style="display:flex;"><span>Gi1/0/3   auto  Pair A     <span style="color:#ae81ff">23</span>   +/- <span style="color:#ae81ff">4</span>  meters N/A         Open                
</span></span><span style="display:flex;"><span>                Pair B     <span style="color:#ae81ff">24</span>   +/- <span style="color:#ae81ff">4</span>  meters N/A         Open                
</span></span><span style="display:flex;"><span>                Pair C     <span style="color:#ae81ff">24</span>   +/- <span style="color:#ae81ff">4</span>  meters N/A         Open                
</span></span><span style="display:flex;"><span>                Pair D     <span style="color:#ae81ff">24</span>   +/- <span style="color:#ae81ff">4</span>  meters N/A         Open                
</span></span></code></pre></div><p><strong>Interpreting the results:</strong></p>
<ul>
<li><strong>Pair status &ldquo;Normal&rdquo;:</strong> Everything is fine. The cable is connected to an active device.</li>
<li><strong>Pair status &ldquo;Open&rdquo;:</strong> The cable has no termination at the end (unplugged).</li>
<li><strong>Pair length:</strong> The &ldquo;1&rdquo; in the first example indicates a very short cable (~1m). Since the hardware has a measurement tolerance of <code>+/- 4 meters</code>, values close to zero are often displayed as 0 or 1. In the second example, we see an approx. 24-meter cable that ends &ldquo;open&rdquo; somewhere in the house.</li>
<li><strong>Remote pair:</strong> In a working connection (Normal), the switch indicates whether the wire pairs arrive correctly on the opposite side. <code>Pair A -&gt; Pair A</code> means: No twists in the cable!</li>
</ul>
<p>The result confirms: My clean-lab is fully under control measurement-wise!</p>
<h2 id="nerd-overkill-the-antenna-calculator-on-the-switch-tcl">
  
  
  Nerd Overkill: The Antenna Calculator on the Switch (TCL)
  
</h2>
<p>As a ham radio operator, I’m always looking for ways to combine my hobbies. Since modern Go binaries don&rsquo;t run on classic IOS, I used an old &ldquo;backdoor&rdquo;: <strong>TCL (Tool Command Language)</strong>.</p>
<p>I ported my Python calculator for the <strong>Comet HFJ-350M antenna</strong> to TCL and uploaded it to the switch&rsquo;s flash. Now I can calculate my antenna settings directly on the switch console – perfect when I&rsquo;m working on the rack!</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g#tclsh flash:hfj350.tcl
</span></span><span style="display:flex;"><span><span style="color:#f92672">===</span> Comet HFJ-350M Calculator <span style="color:#f92672">(</span>Cisco Edition<span style="color:#f92672">)</span> <span style="color:#f92672">===</span>
</span></span><span style="display:flex;"><span>Band <span style="color:#f92672">(</span>e.g. 40m<span style="color:#f92672">)</span> or Freq <span style="color:#f92672">(</span>MHz<span style="color:#f92672">)</span>: 7.1
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>--- Configuration <span style="color:#66d9ef">for</span> 40m ---
</span></span><span style="display:flex;"><span>Coil:   Base <span style="color:#f92672">(</span>No additional coil<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>Jumper: None
</span></span><span style="display:flex;"><span>Std Len <span style="color:#f92672">(</span>7.0 MHz<span style="color:#f92672">)</span>: <span style="color:#ae81ff">960</span> mm
</span></span><span style="display:flex;"><span>Calc Len <span style="color:#f92672">(</span>7.1 MHz<span style="color:#f92672">)</span>: <span style="color:#ae81ff">920</span> mm
</span></span></code></pre></div><p>IT infrastructure meets ham radio – it doesn&rsquo;t get more &ldquo;homelab&rdquo; than this! Maybe this is even the foundation for a HAM-Net on the 10m band? 📡🌐🛰️</p>
<p>For those who want to rebuild it, here is the full TCL code:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-tcl" data-lang="tcl"><span style="display:flex;"><span><span style="color:#75715e"># Comet HFJ-350M Antenna Calculator for Cisco IOS
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"># Author: DO3EET
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">set</span> antenna_data <span style="color:#66d9ef">{</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">{</span>band <span style="color:#e6db74">&#34;160m&#34;</span> low <span style="color:#ae81ff">1.8</span> high <span style="color:#ae81ff">2.0</span> std <span style="color:#ae81ff">1.8</span> coil <span style="color:#e6db74">&#34;Base + 3.5 coil + 1.8 coil&#34;</span> jumper <span style="color:#e6db74">&#34;None&#34;</span> len <span style="color:#ae81ff">1170</span> change <span style="color:#ae81ff">7</span><span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">{</span>band <span style="color:#e6db74">&#34;80m&#34;</span>  low <span style="color:#ae81ff">3.5</span> high <span style="color:#ae81ff">3.8</span> std <span style="color:#ae81ff">3.5</span> coil <span style="color:#e6db74">&#34;Base + 3.5 coil&#34;</span> jumper <span style="color:#e6db74">&#34;None&#34;</span> len <span style="color:#ae81ff">910</span> change <span style="color:#ae81ff">20</span><span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">{</span>band <span style="color:#e6db74">&#34;40m&#34;</span>  low <span style="color:#ae81ff">7.0</span> high <span style="color:#ae81ff">7.2</span> std <span style="color:#ae81ff">7.0</span> coil <span style="color:#e6db74">&#34;Base (No additional coil)&#34;</span> jumper <span style="color:#e6db74">&#34;None&#34;</span> len <span style="color:#ae81ff">960</span> change <span style="color:#ae81ff">25</span><span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">{</span>band <span style="color:#e6db74">&#34;30m&#34;</span>  low <span style="color:#ae81ff">10.1</span> high <span style="color:#ae81ff">10.15</span> std <span style="color:#ae81ff">10.1</span> coil <span style="color:#e6db74">&#34;Base&#34;</span> jumper <span style="color:#e6db74">&#34;Terminal 1&#34;</span> len <span style="color:#ae81ff">990</span> change <span style="color:#ae81ff">40</span><span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">{</span>band <span style="color:#e6db74">&#34;20m&#34;</span>  low <span style="color:#ae81ff">14.0</span> high <span style="color:#ae81ff">14.35</span> std <span style="color:#ae81ff">14.0</span> coil <span style="color:#e6db74">&#34;Base&#34;</span> jumper <span style="color:#e6db74">&#34;Terminal 2&#34;</span> len <span style="color:#ae81ff">800</span> change <span style="color:#ae81ff">60</span><span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">{</span>band <span style="color:#e6db74">&#34;17m&#34;</span>  low <span style="color:#ae81ff">18.068</span> high <span style="color:#ae81ff">18.168</span> std <span style="color:#ae81ff">18.0</span> coil <span style="color:#e6db74">&#34;Base&#34;</span> jumper <span style="color:#e6db74">&#34;Terminal 3 (or 2)&#34;</span> len <span style="color:#ae81ff">1070</span> change <span style="color:#ae81ff">50</span><span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">{</span>band <span style="color:#e6db74">&#34;15m&#34;</span>  low <span style="color:#ae81ff">21.0</span> high <span style="color:#ae81ff">21.45</span> std <span style="color:#ae81ff">21.0</span> coil <span style="color:#e6db74">&#34;Base&#34;</span> jumper <span style="color:#e6db74">&#34;Terminal 3&#34;</span> len <span style="color:#ae81ff">750</span> change <span style="color:#ae81ff">80</span><span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">{</span>band <span style="color:#e6db74">&#34;12m&#34;</span>  low <span style="color:#ae81ff">24.89</span> high <span style="color:#ae81ff">24.99</span> std <span style="color:#ae81ff">24.9</span> coil <span style="color:#e6db74">&#34;Base&#34;</span> jumper <span style="color:#e6db74">&#34;Terminal 3&#34;</span> len <span style="color:#ae81ff">530</span> change <span style="color:#ae81ff">100</span><span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">{</span>band <span style="color:#e6db74">&#34;10m&#34;</span>  low <span style="color:#ae81ff">28.0</span> high <span style="color:#ae81ff">29.7</span> std <span style="color:#ae81ff">28.5</span> coil <span style="color:#e6db74">&#34;Base&#34;</span> jumper <span style="color:#e6db74">&#34;Terminal 4&#34;</span> len <span style="color:#ae81ff">1000</span> change <span style="color:#ae81ff">120</span><span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">{</span>band <span style="color:#e6db74">&#34;6m&#34;</span>   low <span style="color:#ae81ff">50.0</span> high <span style="color:#ae81ff">52.0</span> std <span style="color:#ae81ff">51.0</span> coil <span style="color:#e6db74">&#34;Base&#34;</span> jumper <span style="color:#e6db74">&#34;Terminal 5&#34;</span> len <span style="color:#ae81ff">950</span> change <span style="color:#ae81ff">100</span><span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>puts <span style="color:#e6db74">&#34;=== Comet HFJ-350M Calculator (Cisco Edition) ===&#34;</span>
</span></span><span style="display:flex;"><span>puts <span style="color:#e6db74">&#34;Type &#39;exit&#39; to quit.&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">while</span> <span style="color:#66d9ef">{</span>1<span style="color:#66d9ef">}</span> <span style="color:#66d9ef">{</span>
</span></span><span style="display:flex;"><span>    puts <span style="color:#f92672">-</span>nonewline <span style="color:#e6db74">&#34;Band (e.g. 40m) or Freq (MHz): &#34;</span>
</span></span><span style="display:flex;"><span>    flush stdout
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">set</span> input <span style="color:#66d9ef">[</span>string trim <span style="color:#66d9ef">[</span>gets stdin<span style="color:#66d9ef">]]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">{</span>$input <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;exit&#34;</span> <span style="color:#f92672">||</span> $input <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;q&#34;</span><span style="color:#66d9ef">}</span> break
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">{</span>$input <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;&#34;</span><span style="color:#66d9ef">}</span> continue
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">set</span> found <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">foreach</span> item $antenna_data <span style="color:#66d9ef">{</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">array</span> set d $item
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">set</span> clean_band <span style="color:#66d9ef">[</span>string map <span style="color:#66d9ef">{</span>m <span style="color:#e6db74">&#34;&#34;</span><span style="color:#66d9ef">}</span> $d<span style="color:#66d9ef">(</span>band<span style="color:#66d9ef">)]</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">set</span> is_freq <span style="color:#66d9ef">[</span>string is double <span style="color:#f92672">-</span>strict $input<span style="color:#66d9ef">]</span>
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">{</span>$input <span style="color:#f92672">==</span> $d<span style="color:#66d9ef">(</span>band<span style="color:#66d9ef">)</span> <span style="color:#f92672">||</span> $input <span style="color:#f92672">==</span> $clean_band <span style="color:#f92672">||</span> <span style="color:#66d9ef">(</span>$is_freq <span style="color:#f92672">&amp;&amp;</span> $input <span style="color:#f92672">&gt;=</span> $d<span style="color:#66d9ef">(</span>low<span style="color:#66d9ef">)</span> <span style="color:#f92672">&amp;&amp;</span> $input <span style="color:#f92672">&lt;=</span> $d<span style="color:#66d9ef">(</span>high<span style="color:#66d9ef">))}</span> <span style="color:#66d9ef">{</span>
</span></span><span style="display:flex;"><span>            puts <span style="color:#e6db74">&#34;\n--- Configuration for $d(band) ---&#34;</span>
</span></span><span style="display:flex;"><span>            puts <span style="color:#e6db74">&#34;Coil:   $d(coil)&#34;</span>
</span></span><span style="display:flex;"><span>            puts <span style="color:#e6db74">&#34;Jumper: $d(jumper)&#34;</span>
</span></span><span style="display:flex;"><span>            puts <span style="color:#e6db74">&#34;Std Len ($d(std) MHz): $d(len) mm&#34;</span>
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">{</span>$is_freq <span style="color:#f92672">&amp;&amp;</span> $input <span style="color:#f92672">!=</span> $d<span style="color:#66d9ef">(</span>std<span style="color:#66d9ef">)}</span> <span style="color:#66d9ef">{</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">set</span> diff_khz <span style="color:#66d9ef">[expr</span> <span style="color:#66d9ef">{(</span>$input - $d<span style="color:#66d9ef">(</span>std<span style="color:#66d9ef">))</span> <span style="color:#f92672">*</span> 1000.0<span style="color:#66d9ef">}]</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">set</span> calc_len <span style="color:#66d9ef">[expr</span> <span style="color:#66d9ef">{</span>round<span style="color:#66d9ef">(</span>$d<span style="color:#66d9ef">(</span>len<span style="color:#66d9ef">)</span> - <span style="color:#66d9ef">((</span>$diff_khz <span style="color:#f92672">/</span> $d<span style="color:#66d9ef">(</span>change<span style="color:#66d9ef">))</span> <span style="color:#f92672">*</span> 10.0<span style="color:#66d9ef">))}]</span>
</span></span><span style="display:flex;"><span>                puts <span style="color:#e6db74">&#34;Calc Len ($input MHz): $calc_len mm&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>            puts <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">set</span> found <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#66d9ef">{</span><span style="color:#f92672">!</span>$found<span style="color:#66d9ef">}</span> <span style="color:#66d9ef">{</span> puts <span style="color:#e6db74">&#34;No data found for &#39;$input&#39;\n&#34;</span> <span style="color:#66d9ef">}</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">}</span>
</span></span></code></pre></div><h2 id="the-final-touch-a-login-banner">
  
  
  The Final Touch: A Login Banner
  
</h2>
<p>To round off the setup and give the switch a personal touch, I configured a login banner (Message of the Day). This not only looks professional but is often legally required in corporate environments.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>3750g<span style="color:#f92672">(</span>config<span style="color:#f92672">)</span><span style="color:#75715e">#banner motd #</span>
</span></span><span style="display:flex;"><span>**************************************************************************
</span></span><span style="display:flex;"><span>*                                                                        *
</span></span><span style="display:flex;"><span>*   DO3EET Clean-Lab | Cisco 3750G                                       *
</span></span><span style="display:flex;"><span>*                                                                        *
</span></span><span style="display:flex;"><span>*   Authorized Access Only!                                              *
</span></span><span style="display:flex;"><span>*                                                                        *
</span></span><span style="display:flex;"><span>**************************************************************************
</span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span></code></pre></div><h2 id="hardware-check-is-the-device-healthy">
  
  
  Hardware Check: Is the Device Healthy?
  
</h2>
<p>After all the software work, I wanted to know about the physical health of the used switch. A look at <code>show tech-support</code> (or specific <code>show</code> commands) provided reassuring values:</p>
<ul>
<li><strong>Temperature:</strong> At <strong>34°C</strong> (<code>System Temperature State: GREEN</code>), the device stays pleasantly cool at idle.</li>
<li><strong>Fans:</strong> A brief <code>FAN is OK</code> in the log confirms that the cooling is working properly – often a critical point with used enterprise gear (think bearing damage).</li>
<li><strong>The Heart:</strong> Inside, a <strong>PowerPC 405</strong> processor is at work. Almost ancient by today&rsquo;s standards, but still perfectly adequate for Layer 3 switching in hardware.</li>
<li><strong>Self-Test:</strong> All <code>POST</code> (Power-On Self-Test) routines, including the PortASIC memory, reported a clean <code>Passed</code>.</li>
</ul>
<p>The Final running-config</p>
<p>Here is an excerpt of the current configuration (passwords and secrets have been masked). It shows all the discussed settings working together:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>Current configuration : 6798 bytes
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>version 15.0
</span></span><span style="display:flex;"><span>no service pad
</span></span><span style="display:flex;"><span>service timestamps debug datetime msec
</span></span><span style="display:flex;"><span>service timestamps log datetime msec
</span></span><span style="display:flex;"><span>no service password-encryption
</span></span><span style="display:flex;"><span>service unsupported-transceiver
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>hostname 3750g
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>enable secret 5 &lt;ENCRYPTED&gt;
</span></span><span style="display:flex;"><span>enable password &lt;PASSWORD&gt;
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>username root privilege 15 secret 5 &lt;ENCRYPTED&gt;
</span></span><span style="display:flex;"><span>vtp domain clean-lab
</span></span><span style="display:flex;"><span>vtp mode transparent
</span></span><span style="display:flex;"><span>sdm prefer dual-ipv4-and-ipv6 default
</span></span><span style="display:flex;"><span>ip domain-name do3eet-clab.internal
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>ip dhcp excluded-address 192.168.254.1 192.168.254.10
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>ip dhcp pool MGMT_POOL
</span></span><span style="display:flex;"><span> network 192.168.254.0 255.255.255.0
</span></span><span style="display:flex;"><span> default-router 192.168.254.1 
</span></span><span style="display:flex;"><span> dns-server 8.8.8.8 1.1.1.1 
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>energywise domain MyLab security shared-secret 0 &lt;PASSWORD&gt;
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>crypto pki trustpoint TP-self-signed-&lt;ID&gt;
</span></span><span style="display:flex;"><span> enrollment selfsigned
</span></span><span style="display:flex;"><span> subject-name cn=IOS-Self-Signed-Certificate-&lt;ID&gt;
</span></span><span style="display:flex;"><span> rsakeypair TP-self-signed-&lt;ID&gt;
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>crypto pki certificate chain TP-self-signed-&lt;ID&gt;
</span></span><span style="display:flex;"><span> certificate self-signed 01
</span></span><span style="display:flex;"><span>  &lt;CERTIFICATE_DATA_MASKED&gt;
</span></span><span style="display:flex;"><span>quit
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>no errdisable detect cause gbic-invalid
</span></span><span style="display:flex;"><span>spanning-tree mode pvst
</span></span><span style="display:flex;"><span>spanning-tree extend system-id
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>interface GigabitEthernet1/0/1
</span></span><span style="display:flex;"><span> description MANAGEMENT_PORT
</span></span><span style="display:flex;"><span> switchport mode access
</span></span><span style="display:flex;"><span> spanning-tree portfast
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>interface GigabitEthernet1/0/2
</span></span><span style="display:flex;"><span> description DISABLED_FOR_SECURITY
</span></span><span style="display:flex;"><span> shutdown
</span></span><span style="display:flex;"><span> energywise level 0
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>! ... Interfaces 3 to 27 configured similarly ...
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>interface GigabitEthernet1/0/28
</span></span><span style="display:flex;"><span> description DISABLED_FOR_SECURITY
</span></span><span style="display:flex;"><span> shutdown
</span></span><span style="display:flex;"><span> energywise level 0
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>interface Vlan1
</span></span><span style="display:flex;"><span> ip address 192.168.254.1 255.255.255.0
</span></span><span style="display:flex;"><span> no ip route-cache
</span></span><span style="display:flex;"><span> ipv6 enable
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>ip http server
</span></span><span style="display:flex;"><span>ip http authentication local
</span></span><span style="display:flex;"><span>ip http secure-server
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>access-list 10 permit 192.168.254.0 0.0.0.255
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>snmp-server community &lt;SECRET&gt;
</span></span><span style="display:flex;"><span>snmp-server location 3750g
</span></span><span style="display:flex;"><span>snmp-server contact DO3EET
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>no vstack
</span></span><span style="display:flex;"><span>banner motd ^C
</span></span><span style="display:flex;"><span>**************************************************************************
</span></span><span style="display:flex;"><span>*                                                                        *
</span></span><span style="display:flex;"><span>*   DO3EET Clean-Lab | Cisco 3750G                                       *
</span></span><span style="display:flex;"><span>*                                                                        *
</span></span><span style="display:flex;"><span>*   Authorized Access Only!                                              *
</span></span><span style="display:flex;"><span>*                                                                        *
</span></span><span style="display:flex;"><span>**************************************************************************
</span></span><span style="display:flex;"><span>^C
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>line con 0
</span></span><span style="display:flex;"><span>line vty 0 15
</span></span><span style="display:flex;"><span> login local
</span></span><span style="display:flex;"><span> transport input ssh
</span></span><span style="display:flex;"><span>!
</span></span><span style="display:flex;"><span>end
</span></span></code></pre></div><h2 id="conclusion">
  
  
  Conclusion
  
</h2>
<p>Despite its age, the Cisco 3750G is an excellent device for a homelab if you want to get Layer 3 features on the cheap. However, you must plan the firmware path carefully, as the limited flash memory is very unforgiving. With SSH and IOS 15, the device is now ready for modern network tasks.</p>
]]></description>
    </item>
    
    <item>
      <title>🧩 OpenHAMClock AddOns: More Than Just a Clock</title>
      <link>https://do3eet.pages.dev/en/post/openhamclock-contributions/</link>
      <pubDate>Fri, 20 Mar 2026 22:50:00 +0100</pubDate>
      <guid>https://do3eet.pages.dev/en/post/openhamclock-contributions/</guid>
      <description><![CDATA[<p>In the world of amateur radio, information is everything. Users of <strong>OpenHAMClock</strong> already appreciate its clean display of time, weather, and propagation conditions. But as an IT professional, I couldn&rsquo;t help myself: I wanted more interactivity and automation.</p>
<p>Over the past few weeks, I’ve been working intensively on a series of <strong>AddOns</strong> and documentation to transform OpenHAMClock into a true command center for the ham shack.</p>
<h2 id="my-contributions-at-a-glance">
  
  
  My Contributions at a Glance
  
</h2>
<p>All my contributions are based on the concept of <strong>Userscripts</strong> (e.g., for Tampermonkey). This allows for integrating features directly into the web interface without having to modify the clock&rsquo;s core code.</p>
<h3 id="1-aprs-auto-position">
  
  
  1. APRS Auto-Position
  
</h3>
<p>For portable operators (SOTA/POTA) or mobile stations, this AddOn is a game-changer. It tracks a specific SSID via the <code>aprs.fi</code> API and automatically updates the clock’s position.</p>
<ul>
<li><strong>Smart Updates:</strong> The position only refreshes if you’ve moved more than 50 meters.</li>
<li><strong>Seamless Integration:</strong> As soon as the location changes, the clock recalculates all paths and map views instantly.</li>
</ul>
<h3 id="2-aprs-newsfeed-inbox">
  
  
  2. APRS Newsfeed (Inbox)
  
</h3>
<p>Who wouldn&rsquo;t want to be notified about new APRS messages without constantly checking their handheld radio?</p>
<ul>
<li>Displays the 10 most recent messages directly in the UI.</li>
<li>A subtle red badge signals new incoming messages.</li>
<li>Supports English, German, and Japanese.</li>
</ul>
<h3 id="3-hfj-350m-antenna-calculator-from-terminal-to-web">
  
  
  3. HFJ-350M Antenna Calculator: From Terminal to Web
  
</h3>
<p>The <strong>Comet HFJ-350M</strong> is a fantastic multi-band portable antenna, but matching the telescopic length using manual charts can be tedious in the field.</p>
<p>The inspiration for this AddOn came from my standalone Python tool <a href="https://github.com/frankenstein91/Comet-HFJ-350M-Toy-Box">Comet-HFJ-350M-Toy-Box</a>. Originally conceived as a CLI (Command Line Interface) utility, it already allowed me to quickly calculate the correct settings for any frequency right from the terminal. Since I use OpenHAMClock as my primary shack display, the next logical step was porting this logic into a graphical AddOn.</p>
<p>My web-based calculator now handles the heavy lifting:</p>
<ul>
<li><strong>Precision:</strong> Calculates the exact length in millimeters for any frequency from 160m to 6m.</li>
<li><strong>Visualization:</strong> Displays the required coil combinations and jumper settings directly on the dashboard.</li>
<li><strong>Fine-tuning:</strong> Includes sensitivity data (kHz/cm) to help you hit the perfect SWR with minimal effort.</li>
</ul>
<h2 id="infrastructure-for-the-community">
  
  
  Infrastructure for the Community
  
</h2>
<p>Beyond functional tools, it was important to me that other developers could easily implement their own ideas. That&rsquo;s why I authored two foundational guides:</p>
<ul>
<li><strong>AddOn Development Guide:</strong> A standard for integrating community tools. I implemented an &ldquo;AddOn Drawer&rdquo; logic (🧩 icon) so that multiple AddOns can coexist cleanly without cluttering the native design.</li>
<li><strong>Self-Hosting Guide:</strong> A guide for those running OpenHAMClock (and the AddOns) on private hardware like a Raspberry Pi or in Docker. I place special emphasis on security best practices for handling API keys.</li>
</ul>
<h2 id="conclusion">
  
  
  Conclusion
  
</h2>
<p>Open Source thrives on participation. What started as a small Python experiment for the terminal has now become a permanent part of my shack dashboard. Through the AddOn interface, OpenHAMClock has become significantly more flexible. I’m excited to see what ideas the community implements next!</p>
<p>Feel free to check out the code on GitHub: <a href="https://github.com/frankenstein91/openhamclock/tree/main/AddOns">frankenstein91/openhamclock</a></p>
<p>73 de DO3EET</p>]]></description>
    </item>
    
    <item>
      <title>System Log: Redesign Successfully Deployed to Production</title>
      <link>https://do3eet.pages.dev/en/post/redesign-2026/</link>
      <pubDate>Fri, 20 Mar 2026 22:45:00 +0100</pubDate>
      <guid>https://do3eet.pages.dev/en/post/redesign-2026/</guid>
      <description><![CDATA[<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f92672">[</span>frank@do3eet-terminal ~<span style="color:#f92672">]</span>$ git checkout redesign
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>frank@do3eet-terminal ~<span style="color:#f92672">]</span>$ git merge master
</span></span><span style="display:flex;"><span><span style="color:#f92672">[</span>frank@do3eet-terminal ~<span style="color:#f92672">]</span>$ deploy --force-coolness
</span></span></code></pre></div><p>If you briefly wondered while loading the page whether you accidentally landed on a 90s mainframe via SSH: <strong>Don&rsquo;t worry, it&rsquo;s completely intentional!</strong></p>
<p>I&rsquo;ve spent the last few days giving my digital home a complete makeover. Moving away from &ldquo;standard website&rdquo; towards a design that reflects my passions: Linux, technology, and a healthy dose of science fiction.</p>
<h2 id="whats-new-in-the-terminal">
  
  
  What&rsquo;s New in the Terminal?
  
</h2>
<h3 id="1-the-matrix-has-you-and-your-browser">
  
  
  1. The Matrix Has You (and Your Browser)
  
</h3>
<p>The new design goes all-in on <strong>Matrix Green on Tokyo Night Blue</strong>. There are now subtle scanlines, a blinking cursor, and a layout that feels like a directory tree (<code>tree</code>).</p>
<p>A special technical treat: Take a look at the header of the terminal window. It displays a dynamic prompt that recognizes which browser you&rsquo;re using (e.g., <code>[Firefox@do3eet.pages.dev /var/www]#</code>). A small client-side JavaScript makes it possible – completely local and without data storage, of course.</p>
<h3 id="2-the-great-inventory-tag-cleanup">
  
  
  2. The Great Inventory (Tag Cleanup)
  
</h3>
<p>I must admit: my keywords (tags) had become a bit&hellip; messy over time. I&rsquo;ve cleaned up!</p>
<ul>
<li>Many granular tags have been consolidated (e.g., everything related to messengers, networking, or culinary topics).</li>
<li>The new <strong>&ldquo;Hobby&rdquo;</strong> category is now packed with my DIY projects – from USB-C soldering on a Christmas star to my keyboard&rsquo;s QMK firmware adventure.</li>
</ul>
<h3 id="3-digital-treasures-and-easter-eggs">
  
  
  3. Digital Treasures and Easter Eggs
  
</h3>
<p>I couldn&rsquo;t resist building in a few small surprises:</p>
<ul>
<li><strong>Achievement Unlocked:</strong> If you leave the page open for more than 5 minutes (perhaps while cozily reading a Japan report?), you&rsquo;ll receive a small homage to old CRT monitors. No, your screen isn&rsquo;t breaking; it&rsquo;s just a &ldquo;simulated burn-in&rdquo;.</li>
<li><strong>The Ark of Truth:</strong> Stargate fans should scroll all the way to the bottom&hellip; but be careful, <em>Origin</em> is the source of truth.</li>
</ul>
<h2 id="why-all-this">
  
  
  Why All This?
  
</h2>
<p>As an IT professional and ham radio operator, I spend a lot of time in terminals. So why should my blog look any different? The new look is faster, more focused, and still offers plenty of space for my travel photos from Hokkaido or Tokyo.</p>
<p>How do you like the new look? Leave me a comment (or radio me) – I&rsquo;m looking forward to your feedback!</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#f92672">[</span>frank@do3eet-terminal ~<span style="color:#f92672">]</span>$ exit
</span></span><span style="display:flex;"><span>logout
</span></span><span style="display:flex;"><span>Connection to do3eet.pages.dev closed.
</span></span></code></pre></div>]]></description>
    </item>
    
    <item>
      <title>My QMK Adventure</title>
      <link>https://do3eet.pages.dev/en/post/qmk1/</link>
      <pubDate>Sat, 19 Jul 2025 18:05:09 +0200</pubDate>
      <guid>https://do3eet.pages.dev/en/post/qmk1/</guid>
      <description><![CDATA[<p>I recently fulfilled a long-held wish and treated myself to a fully programmable mechanical keyboard: the Keychron V5 Max in the ISO-DE layout. As a Linux user and tech enthusiast, the ability to customize every key and function to my liking was the deciding factor for the purchase. My first project was quickly defined: I wanted a visual confirmation when the number pad is active. Instead of just a single indicator LED, the entire block should change color.</p>
<h2 id="preparation-the-first-steps-into-the-unknown">
  
  
  Preparation: The First Steps into the Unknown
  
</h2>
<p>As a newcomer to the QMK world, I first stuck to the official documentation. Setting it up on my Arch Linux system went surprisingly smoothly, thanks to the AUR and <code>pikaur</code>.</p>
<pre tabindex="0"><code class="language-ssh" data-lang="ssh"># 1. Clone firmware from Keychron
git clone https://github.com/Keychron/qmk_firmware.git
# 2. Install all necessary dependencies
pikaur -S python-platformdirs python-argcomplete python-colorama python-milc python-dotty-dict python-jsonschema git avr-gcc arm-none-eabi-gcc avr-libc arm-none-eabi-binutils arm-none-eabi-newlib avr-binutils dfu-programmer dfu-util avrdude python-hjson python-pygments python-pyusb python-pyserial python-pillow gcc libffi libusb-compat clang zip wget diffutils
# 3. Set up QMK environment
qmk setup --home /path/to/qmk_firmware
</code></pre><p>Up to this point, everything went according to plan. I felt ready to write the code for my lighting idea. But QMK had other plans.</p>
<h2 id="hurdle-1-the-directory-that-didnt-exist">
  
  
  Hurdle 1: The Directory That Didn&rsquo;t Exist&hellip;
  
</h2>
<p>Every guide I found followed a clear pattern: find your keyboard&rsquo;s directory, copy the default keymap, and start customizing. Full of motivation, I typed: <code>cd keyboards/keychron/v5_max/iso/</code></p>
<p>The terminal&rsquo;s response was sobering: <code>No such file or directory</code>. A quick check with ls revealed the first problem: there was no <code>v5_max</code> directory. A small setback, but no reason to worry. Or so I thought.</p>
<h2 id="hurdle-2-the-right-train-on-the-wrong-track">
  
  
  Hurdle 2: The Right Train on the Wrong Track&hellip;
  
</h2>
<p>Even though the title sounds like a railway reference, we&rsquo;re still talking about the Universal Serial Bus&hellip;</p>
<p>After trying to work with the <code>v5</code> files, it quickly became clear that something was <strong>fundamentally</strong> wrong. The keyboard wasn&rsquo;t recognized correctly, flashing failed, and at one point, it <strong>stopped responding entirely</strong> – the first &ldquo;brick.&rdquo; A scary feeling, but fortunately, almost always fixable by re-flashing in DFU mode (unplug the USB cable, hold the button under the spacebar, plug the cable back in).</p>
<p>The crucial clue came after intensive research: Keychron often maintains new or wireless models in <strong>separate Git branches</strong>. My V5 Max wasn&rsquo;t in the main branch but was hiding in the <code>wls_2025q1</code> branch. a clean restart was necessary:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>rm -rf /path/to/qmk_firmware
</span></span><span style="display:flex;"><span>git clone https://github.com/Keychron/qmk_firmware.git
</span></span><span style="display:flex;"><span>cd qmk_firmware
</span></span><span style="display:flex;"><span>git checkout wls_2025q1
</span></span><span style="display:flex;"><span>qmk setup --home /path/to/qmk_firmware
</span></span></code></pre></div><p>Finally! The <code>v5_max</code> directory was there. But the joy was <strong>short-lived</strong>. The directory structure was completely different than expected. There was no simple <code>keymaps/default</code> directory. Instead, the ISO-DE version was hidden in a subfolder called <code>iso_encoder</code>. The <em>treasure map</em> was getting more and more complex.</p>
<h2 id="the-core-problem-hunting-for-phantom-leds">
  
  
  The Core Problem: Hunting for Phantom LEDs
  
</h2>
<p>After I finally found the right files, I added the code for my lighting. I compiled, flashed – and the wrong keys lit up. A chaotic jumble from the <code>5</code>, <code>t</code>, <code>g</code>, <code>b</code> row glowed green, while my number pad remained dark.</p>
<p>This is where the real detective work began. My assumption that the LED numbers would follow a logical sequence was not entirely wrong&hellip; but also not correct enough. A look into the JSON configuration files didn&rsquo;t help either, as they only describe the key matrix, not the LED wiring.</p>
<h2 id="the-breakthrough-a-brilliant-idea">
  
  
  The Breakthrough: A Brilliant Idea
  
</h2>
<p>After countless failed attempts to guess the correct LED numbers, I had an idea that changed everything. Instead of flashing a new firmware for every single test, I built a &ldquo;diagnostic firmware.&rdquo;
The idea:</p>
<ol>
<li>A global counter variable (led_index) is set to 0.</li>
<li>Every time the Num key is pressed, this variable is incremented by 1.</li>
<li>The lighting function turns off all LEDs except for the one whose number is stored in led_index.</li>
</ol>
<p>This way, I could &ldquo;click&rdquo; my way through the entire LED matrix, key by key, and create an exact map of my keyboard. After a few initial implementation hiccups, it worked!</p>
<p>Key by key, I pressed Num and noted which LED lit up&hellip; or so I thought&hellip;</p>
<ul>
<li>ESC was 0.</li>
<li>F1 was 1.</li>
<li>Num Lock was 30.</li>
<li>Caps Lock was 51.</li>
<li>The 0 on the numpad was 96.</li>
<li>The . on the numpad was 97.</li>
</ul>
<h2 id="the-final-hurdle-logic-instead-of-counting">
  
  
  The Final Hurdle: Logic Instead of Counting
  
</h2>
<p>But even with this method, errors occurred. One miscount and the whole list was shifted. The <code>0</code> and <code>.</code> keys wouldn&rsquo;t light up, but <code>Ctrl</code> and <code>Shift</code> did instead. The counting method was too error-prone. We needed a new strategy: logic and targeted tests.</p>
<p>Instead of testing individual LEDs, we tested entire rows of the numpad at once. A decisive test was the <code>4</code>, <code>5</code>, <code>6</code>, <code>+</code> row. From an earlier test, I knew that LED <code>65</code> was under the <code>5</code> key. So I tested the logical group <code>64</code>, <code>65</code>, <code>66</code>, <code>67</code>. The test was a success! These four keys lit up as expected. This single successful test confirmed the block structure of the LED matrix and allowed me to deduce the remaining numbers with high confidence and verify them in larger groups. This saved countless flashing operations and finally led me to the correct and complete LED map.</p>
<h2 id="the-reward-for-the-effort-the-final-code">
  
  
  The Reward for the Effort: The Final Code
  
</h2>
<p>With the complete and verified list of LED numbers, the rest was a piece of cake. I extended the code with another feature: the <code>Caps Lock</code> key should light up red when activated. As a final adjustment, I mapped the <code>Print Screen</code> key, which my keyboard lacks, to the combination <code>Fn + P</code>.</p>
<p>Here is the final code, the result of our long journey:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">QMK_KEYBOARD_H</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&#34;keychron_common.h&#34;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">enum</span> layers {
</span></span><span style="display:flex;"><span>    MAC_BASE,
</span></span><span style="display:flex;"><span>    MAC_FN,
</span></span><span style="display:flex;"><span>    WIN_BASE,
</span></span><span style="display:flex;"><span>    WIN_FN,
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">uint8_t</span> numpad_leds[] <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">30</span>, <span style="color:#ae81ff">31</span>, <span style="color:#ae81ff">32</span>, <span style="color:#ae81ff">33</span>, <span style="color:#75715e">// Num, /, *, -
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#ae81ff">48</span>, <span style="color:#ae81ff">49</span>, <span style="color:#ae81ff">50</span>,     <span style="color:#75715e">// 7, 8, 9
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#ae81ff">64</span>, <span style="color:#ae81ff">65</span>, <span style="color:#ae81ff">66</span>, <span style="color:#ae81ff">67</span>, <span style="color:#75715e">// 4, 5, 6, +
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#ae81ff">82</span>, <span style="color:#ae81ff">83</span>, <span style="color:#ae81ff">84</span>, <span style="color:#ae81ff">85</span>, <span style="color:#75715e">// 1, 2, 3, Enter
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#ae81ff">96</span>, <span style="color:#ae81ff">97</span>          <span style="color:#75715e">// 0, .
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Die LED-Nummer für die Caps-Lock-Taste
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e">#define CAPS_LOCK_LED 51
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// clang-format off
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">uint16_t</span> PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span> [MAC_BASE] <span style="color:#f92672">=</span> <span style="color:#a6e22e">LAYOUT_iso_99</span>(
</span></span><span style="display:flex;"><span>        KC_ESC,             KC_BRID,  KC_BRIU,  KC_MCTRL, KC_LNPAD, RGB_VAD,  RGB_VAI,  KC_MPRV,  KC_MPLY,  KC_MNXT,  KC_MUTE,    KC_VOLD,  KC_VOLU,            KC_DEL,   KC_HOME,  KC_END,     KC_MUTE,
</span></span><span style="display:flex;"><span>        KC_GRV,   KC_1,     KC_2,     KC_3,     KC_4,     KC_5,     KC_6,     KC_7,     KC_8,     KC_9,     KC_0,     KC_MINS,    KC_EQL,   KC_BSPC,            KC_NUM,   KC_PSLS,  KC_PAST,    KC_PMNS,
</span></span><span style="display:flex;"><span>        KC_TAB,   KC_Q,     KC_W,     KC_E,     KC_R,     KC_T,     KC_Y,     KC_U,     KC_I,     KC_O,     KC_P,     KC_LBRC,    KC_RBRC,                      KC_P7,    KC_P8,    KC_P9,      KC_PPLS,
</span></span><span style="display:flex;"><span>        KC_CAPS,  KC_A,     KC_S,     KC_D,     KC_F,     KC_G,     KC_H,     KC_J,     KC_K,     KC_L,     KC_SCLN,  KC_QUOT,    KC_NUHS,  KC_ENT,             KC_P4,    KC_P5,    KC_P6,
</span></span><span style="display:flex;"><span>        KC_LSFT,  KC_NUBS,  KC_Z,     KC_X,     KC_C,     KC_V,     KC_B,     KC_N,     KC_M,     KC_COMM,  KC_DOT,   KC_SLSH,              KC_RSFT,  KC_UP,    KC_P1,    KC_P2,    KC_P3,      KC_PENT,
</span></span><span style="display:flex;"><span>        KC_LCTL,  KC_LOPTN, KC_LCMMD,                               KC_SPC,                                 KC_RCMMD, <span style="color:#a6e22e">MO</span>(MAC_FN), KC_RCTL,  KC_LEFT,  KC_DOWN,  KC_RGHT,  KC_P0,    KC_PDOT               ),
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    [MAC_FN] <span style="color:#f92672">=</span> <span style="color:#a6e22e">LAYOUT_iso_99</span>(
</span></span><span style="display:flex;"><span>        _______,            KC_F1,    KC_F2,    KC_F3,    KC_F4,    KC_F5,    KC_F6,    KC_F7,    KC_F8,    KC_F9,    KC_F10,     KC_F11,   KC_F12,             _______,  _______,  _______,    RGB_TOG,
</span></span><span style="display:flex;"><span>        _______,  BT_HST1,  BT_HST2,  BT_HST3,  P2P4G,    _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,            _______,  _______,  _______,    _______,
</span></span><span style="display:flex;"><span>        RGB_TOG,  RGB_MOD,  RGB_VAI,  RGB_HUI,  RGB_SAI,  RGB_SPI,  _______,  _______,  _______,  _______,  KC_PSCR,  _______,    _______,                      _______,  _______,  _______,    _______, <span style="color:#75715e">// &lt;-- HIER: P = Print Screen
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        _______,  RGB_RMOD, RGB_VAD,  RGB_HUD,  RGB_SAD,  RGB_SPD,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,            _______,  _______,  _______,
</span></span><span style="display:flex;"><span>        _______,  _______,  _______,  _______,  _______,  _______,  BAT_LVL,  NK_TOGG,  _______,  _______,  _______,  _______,              _______,  _______,  _______,  _______,  _______,    _______,
</span></span><span style="display:flex;"><span>        _______,  _______,  _______,                                _______,                                _______,  _______,    _______,  _______,  _______,  _______,  _______,  _______            ),
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    [WIN_BASE] <span style="color:#f92672">=</span> <span style="color:#a6e22e">LAYOUT_iso_99</span>(
</span></span><span style="display:flex;"><span>        KC_ESC,             KC_F1,    KC_F2,    KC_F3,    KC_F4,    KC_F5,    KC_F6,    KC_F7,    KC_F8,    KC_F9,    KC_F10,     KC_F11,   KC_F12,             KC_DEL,   KC_HOME,  KC_END,     KC_MUTE,
</span></span><span style="display:flex;"><span>        KC_GRV,   KC_1,     KC_2,     KC_3,     KC_4,     KC_5,     KC_6,     KC_7,     KC_8,     KC_9,     KC_0,     KC_MINS,    KC_EQL,   KC_BSPC,            KC_NUM,   KC_PSLS,  KC_PAST,    KC_PMNS,
</span></span><span style="display:flex;"><span>        KC_TAB,   KC_Q,     KC_W,     KC_E,     KC_R,     KC_T,     KC_Y,     KC_U,     KC_I,     KC_O,     KC_P,     KC_LBRC,    KC_RBRC,                      KC_P7,    KC_P8,    KC_P9,      KC_PPLS,
</span></span><span style="display:flex;"><span>        KC_CAPS,  KC_A,     KC_S,     KC_D,     KC_F,     KC_G,     KC_H,     KC_J,     KC_K,     KC_L,     KC_SCLN,  KC_QUOT,    KC_NUHS,  KC_ENT,             KC_P4,    KC_P5,    KC_P6,
</span></span><span style="display:flex;"><span>        KC_LSFT,  KC_NUBS,  KC_Z,     KC_X,     KC_C,     KC_V,     KC_B,     KC_N,     KC_M,     KC_COMM,  KC_DOT,   KC_SLSH,              KC_RSFT,  KC_UP,    KC_P1,    KC_P2,    KC_P3,      KC_PENT,
</span></span><span style="display:flex;"><span>        KC_LCTL,  KC_LWIN,  KC_LALT,                                KC_SPC,                                 KC_RALT,  <span style="color:#a6e22e">MO</span>(WIN_FN), KC_RCTL,  KC_LEFT,  KC_DOWN,  KC_RGHT,  KC_P0,    KC_PDOT            ),
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    [WIN_FN] <span style="color:#f92672">=</span> <span style="color:#a6e22e">LAYOUT_iso_99</span>(
</span></span><span style="display:flex;"><span>        _______,            KC_BRID,  KC_BRIU,  KC_TASK,  KC_FILE,  RGB_VAD,  RGB_VAI,  KC_MPRV,  KC_MPLY,  KC_MNXT,  KC_MUTE,    KC_VOLD,  KC_VOLU,            _______,  _______,  _______,    RGB_TOG,
</span></span><span style="display:flex;"><span>        _______,  BT_HST1,  BT_HST2,  BT_HST3,  P2P4G,    _______,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,            _______,  _______,  _______,    _______,
</span></span><span style="display:flex;"><span>        RGB_TOG,  RGB_MOD,  RGB_VAI,  RGB_HUI,  RGB_SAI,  RGB_SPI,  _______,  _______,  _______,  _______,  KC_PSCR,  _______,    _______,                      _______,  _______,  _______,    _______, <span style="color:#75715e">// &lt;-- HIER: P = Print Screen
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        _______,  RGB_RMOD, RGB_VAD,  RGB_HUD,  RGB_SAD,  RGB_SPD,  _______,  _______,  _______,  _______,  _______,  _______,    _______,  _______,            _______,  _______,  _______,
</span></span><span style="display:flex;"><span>        _______,  _______,  _______,  _______,  _______,  _______,  BAT_LVL,  NK_TOGG,  _______,  _______,  _______,  _______,              _______,  _______,  _______,  _______,  _______,    _______,
</span></span><span style="display:flex;"><span>        _______,  _______,  _______,                                _______,                                _______,  _______,    _______,  _______,  _______,  _______,  _______,  _______            ),
</span></span><span style="display:flex;"><span> };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#if defined(ENCODER_MAP_ENABLE)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">uint16_t</span> PROGMEM encoder_map[][NUM_ENCODERS][<span style="color:#ae81ff">2</span>] <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    [MAC_BASE] <span style="color:#f92672">=</span> {<span style="color:#a6e22e">ENCODER_CCW_CW</span>(KC_VOLD, KC_VOLU)},
</span></span><span style="display:flex;"><span>    [MAC_FN]   <span style="color:#f92672">=</span> {<span style="color:#a6e22e">ENCODER_CCW_CW</span>(RGB_VAD, RGB_VAI)},
</span></span><span style="display:flex;"><span>    [WIN_BASE] <span style="color:#f92672">=</span> {<span style="color:#a6e22e">ENCODER_CCW_CW</span>(KC_VOLD, KC_VOLU)},
</span></span><span style="display:flex;"><span>    [WIN_FN]   <span style="color:#f92672">=</span> {<span style="color:#a6e22e">ENCODER_CCW_CW</span>(RGB_VAD, RGB_VAI)},
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span><span style="color:#75715e">#endif </span><span style="color:#75715e">// ENCODER_MAP_ENABLE
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// clang-format on
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">bool</span> <span style="color:#a6e22e">process_record_user</span>(<span style="color:#66d9ef">uint16_t</span> keycode, <span style="color:#66d9ef">keyrecord_t</span> <span style="color:#f92672">*</span>record) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">process_record_keychron_common</span>(keycode, record)) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> false;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> true;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">bool</span> <span style="color:#a6e22e">rgb_matrix_indicators_user</span>(<span style="color:#66d9ef">void</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Num-Lock-Anzeige
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">host_keyboard_led_state</span>().num_lock) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">uint8_t</span> i <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; i <span style="color:#f92672">&lt;</span> <span style="color:#66d9ef">sizeof</span>(numpad_leds); i<span style="color:#f92672">++</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">rgb_matrix_set_color</span>(numpad_leds[i], <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">0</span>); <span style="color:#75715e">// Grün
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Caps-Lock-Anzeige
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">host_keyboard_led_state</span>().caps_lock) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">rgb_matrix_set_color</span>(CAPS_LOCK_LED, <span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>); <span style="color:#75715e">// Rot
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> false;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="conclusion-and-one-last-mystery">
  
  
  Conclusion and One Last Mystery
  
</h2>
<p>This journey was a rollercoaster. It showed me how powerful, but also how unforgiving, QMK can be. The documentation is a good start, but with manufacturer-specific deviations, you are quickly on your own and have to become a detective. However, the feeling of finally having a firmware that does exactly what you want is priceless.</p>
<p>One small mystery remains unsolved, though: The key combination to display the battery level (Fn + B) no longer seems to work with my custom firmware. Unfortunately, I don&rsquo;t know why. Perhaps that&rsquo;s an adventure for another time.</p>
<p>To everyone facing a similar challenge: Don&rsquo;t give up! The solution is out there, sometimes you just have to work hard to find it. And if anyone has any ideas, <strong>please</strong> let me know.</p>
]]></description>
    </item>
    
    <item>
      <title>SSTV from space (ISS)</title>
      <link>https://do3eet.pages.dev/en/post/iss_sstv/</link>
      <pubDate>Thu, 05 Jun 2025 09:30:00 +0200</pubDate>
      <guid>https://do3eet.pages.dev/en/post/iss_sstv/</guid>
      <description><![CDATA[<p>With a bit of delay, I would finally like to report on my new awards. Unlike with ARPS and the QSL card, which are about transmitting to the ISS, these are more about reception. To obtain these documents, one must provide proof of receiving SSTV images from the ISS.</p>
<p>SSTV stands for &ldquo;Slow Scan Television&rdquo; and is an operating mode in amateur radio used for transmitting still images. In the context of the International Space Station (ISS), this means that the ISS transmits images via radio at specific times, which can be received by radio amateurs on Earth. This capability is mostly used for specific events. ARISS (Amateur Radio on the International Space Station) usually announces these events in advance.</p>
  
  <img src="https://do3eet-media.dreamofjapan.de/posts/ISS_SSTV/Diploma.png" alt="SSTV awards from ISS" width="1400" height="1400" loading="lazy">
<p>And I was there live for exactly two such cosmic photo sessions – practically as a paparazzo for interstellar snapshots! So, you sit there with pricked ears (and antennas) and listen to the ether, while a multi-ton high-tech laboratory with astronauts on board rushes past you at an altitude of 400 km and&hellip; well&hellip; sends little pictures. Sounds unspectacular at first? Not at all!
Imagine this: there&rsquo;s this characteristic beeping and chirping coming from the loudspeaker – the secret language of images from space. You hope that your own receiver and software will cooperate and that your neighbor doesn&rsquo;t decide to start the lawnmower right then, or that a cosmic cow doesn&rsquo;t interfere with the transmission. Every successfully decoded pixel is a small victory!</p>
<h1 id="a-cheer-for-the-space-pioneers">
  
  
  A Cheer for the Space Pioneers
  
</h1>
<p>The first award I &ldquo;fished&rdquo; for in this way was on the occasion of the <strong>International Day of Human Space Flight</strong>. And who could be more fitting for that than the first man in space, Yuri Gagarin? His likeness, together with his Vostok-1 capsule, now adorns this award. The transmitted images showed various milestones of human spaceflight.
  
  <img src="https://do3eet-media.dreamofjapan.de/posts/ISS_SSTV/PD120_20250415_094901.png" alt="SSTV: Internationale Tag der menschlichen Raumfahrt" width="800" height="800" loading="lazy">
</p>
<h1 id="peace-message-from-orbit">
  
  
  Peace Message from Orbit
  
</h1>
<p>Shortly thereafter, or rather, on the next major occasion, the second award arrived. This time, it addressed a more serious, but all the more important topic: the <strong>80th anniversary of the end of World War II</strong>. The ISS itself, a symbol of international cooperation and friendship, adorned this special award. A strong signal sent from above, which could be captured down here with a bit of patience and technology. That&rsquo;s when you realize that amateur radio is more than just &ldquo;<em>Hello here, hello there</em>.&rdquo;
  
  <img src="https://do3eet-media.dreamofjapan.de/posts/ISS_SSTV/PD120_20250511_201722.png" alt="SSTV: 80. Jahrestag des Endes des Zweiten Weltkriegs" width="800" height="800" loading="lazy">
</p>
<p>It&rsquo;s a pretty cool feeling to receive these little works of art directly from the space station. Each time it&rsquo;s a little thrill wondering if the transmission will work and the image will come through cleanly, especially since the ISS always seems to change images right when it&rsquo;s over my location. And in the end, you&rsquo;re not just holding a piece of paper, but confirmation that you&rsquo;re part of a worldwide community of radio enthusiasts listening to LEO.</p>
<p>Now the awards are almost on the wall, reminding me that even slow pictures can travel incredibly fast when they come from the ISS. Let&rsquo;s see which cosmic event will be immortalized via SSTV next – my receiver is ready! I hope I don&rsquo;t miss the announcement.</p>
]]></description>
    </item>
    
    <item>
      <title>Chronokinesis</title>
      <link>https://do3eet.pages.dev/en/post/zeitmanipulation/</link>
      <pubDate>Tue, 08 Apr 2025 10:07:00 +0200</pubDate>
      <guid>https://do3eet.pages.dev/en/post/zeitmanipulation/</guid>
      <description><![CDATA[<p>I believe everyone contemplates time at least once in their life. This fascination doesn&rsquo;t just stem from Chronométrophilia, but also simply from a normal life shaped by time. One person might be waiting for the love of their life at the train station, another just for the workday to end. For both, the clock ticks on.<br>
My fascination with time was particularly sparked by an article on <a href="https://sumikai.com/nachrichten-aus-japan/studie-zeigt-dass-zeit-auf-dem-tokyo-skytree-tower-etwas-schneller-vergeht-271550/">sumikai.com</a>.<br>
The theory of relativity, particularly Albert Einstein&rsquo;s General Theory of Relativity, tells us that time does not pass at the same rate for everyone. It depends on the strength of the gravitational field and how fast someone is moving. The article scientifically demonstrates this using the Tokyo Skytree.</p>
<p>Time is also relevant to me on vacation. On the one hand, to avoid missing the train, but also as a fundamental component of GPS for navigating in large cities.</p>
<p>And in my professional life, NTP time is important for server administration. Here, it is a constant requirement that the servers are in sync with a central time server, ensuring that their times are consistent.</p>
<h1 id="why-is-ntp-so-important">
  
  
  Why is NTP so important?
  
</h1>
<p>Well, servers perform countless tasks: they log every step in log files, save data in databases, and communicate with each other. If the time on these systems doesn&rsquo;t match, it creates utter chaos. Timestamps in log files become inconsistent, which turns troubleshooting into a guessing game. Imagine an error occurs, and you&rsquo;re trying to figure out what happened from the logs. Conflicting timestamps make it impossible to trace the sequence of events.<br>
Accurate time is also crucial for the smooth operation of your applications. Many processes, such as database replication or file synchronization, rely on a consistent time base. Time discrepancies here can lead to data loss or malfunctions. When it comes to security, accurate time is even more critical. The analysis of security incidents, the validity of certificates, and the synchronization of security tokens—all of these depend on precise timekeeping.</p>
<h1 id="why-is-accurate-time-important-for-gps">
  
  
  Why is accurate time important for GPS?
  
</h1>
<p>We use GPS (Global Positioning System) almost daily—whether in our cars, on our smartphones, or on our smartwatches. At its core, GPS is based on a simple physical principle: <code>Distance = Speed × Time</code>. The GPS system consists of a network of satellites orbiting the Earth. Each of these satellites continuously transmits signals. These signals contain information about the satellite&rsquo;s position and, crucially, the <strong>exact</strong> time at which the signal was sent.<br>
Your GPS receiver reads these signals from multiple satellites simultaneously. It compares the transmission time encoded in the signal with the time it receives the signal. From this difference, it can calculate how long the signal was traveling. Since we are talking about the speed of light, these time differences are minuscule.</p>
<p>Precise positioning with GPS is therefore inextricably linked with extremely accurate time measurement. It is the ability to measure and process time intervals in the nanosecond range that allows us to determine our location so precisely.</p>
<h1 id="what-kinds-of-attacks-are-there">
  
  
  What Kinds of Attacks Are There?
  
</h1>
<h2 id="gps">
  
  
  GPS
  
</h2>
<p>Precisely because accurate time is so fundamental to GPS, it&rsquo;s also an attractive target for attackers. The disruption or manipulation of these time signals can have significant consequences.</p>
<p>Jamming&hellip; This is the simplest and probably most common form of interference. An attacker uses a transmitter to broadcast strong noise or disruptive signals on GPS frequencies. The receiver loses satellite contact and can no longer determine its position or the accurate time. This constitutes a Denial of Service (DoS) attack.</p>
<p>GPS Spoofing, however, is particularly nasty&hellip; This is a much more sophisticated method of attack on the GPS signal. Instead of just blocking the signals, the attacker broadcasts counterfeit GPS signals. These signals imitate real satellite signals but contain false information—specifically, an incorrect time and/or incorrect satellite positions. The receiver then calculates a wrong position and/or an incorrect time without realizing it. The system believes it is somewhere else or that it is a different time. This can cause ships and aircraft to go off course, lead to drones being misdirected into incorrect areas, or disrupt time-critical systems.</p>
<h2 id="ntp">
  
  
  NTP
  
</h2>
<p>As with any IT system, Denial of Service attacks are commonplace for NTP servers and are nothing new. Therefore, systems usually use multiple servers or even entire server pools to determine the current time. What&rsquo;s particularly insidious about NTP servers is that, if misconfigured, they can also be exploited for DDoS amplification attacks.</p>
<p>But just as with GPS, there are also attackers who intercept NTP packets and alter the timestamps within them. The client then calculates an incorrect time based on this manipulated data.</p>
<h1 id="my-attempt-to-detect-attacks">
  
  
  My Attempt to Detect Attacks&hellip;
  
</h1>
<p>My idea is to detect a potential attack on one of these signals by monitoring its drift relative to the other signal. Of course, this requires a lot of data to differentiate the normal drift fluctuations for a specific location and internet connection from an actual attack. But I&rsquo;ve already made a start, and I don&rsquo;t want to hide it.</p>
<h2 id="gui">
  
  
  GUI
  
</h2>
<p>
  
  <img src="https://do3eet-media.dreamofjapan.de/posts/Zeitmanipulation/1.jpg" alt="Die GUI" width="1024" height="1024" loading="lazy">
At the very top, the current time is displayed, sourced directly from a GPS receiver. Below that, the tool lists the times it has received from various NTP (Network Time Protocol) servers. Further down, the difference between the GPS time and the time from each individual NTP server is calculated and displayed in milliseconds (ms). A small amount of drift is normal and is caused by network latency and system processing speeds.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e">&lt;!DOCTYPE html&gt;</span>
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">html</span> <span style="color:#a6e22e">lang</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;de&#34;</span>&gt;
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">head</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">meta</span> <span style="color:#a6e22e">charset</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;UTF-8&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">meta</span> <span style="color:#a6e22e">name</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;viewport&#34;</span> <span style="color:#a6e22e">content</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;width=device-width, initial-scale=1.0&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">title</span>&gt;GPS/NTP Drift Monitor&lt;/<span style="color:#f92672">title</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">script</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://cdn.tailwindcss.com&#34;</span>&gt;&lt;/<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">link</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;preconnect&#34;</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://fonts.googleapis.com&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">link</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;preconnect&#34;</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://fonts.gstatic.com&#34;</span> <span style="color:#a6e22e">crossorigin</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">link</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap&#34;</span> <span style="color:#a6e22e">rel</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;stylesheet&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">style</span>&gt;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">body</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">font-family</span>: <span style="color:#e6db74">&#39;Inter&#39;</span>, <span style="color:#66d9ef">sans-serif</span>;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">background-color</span>: <span style="color:#ae81ff">#1a202c</span>; <span style="color:#75715e">/* Dunkler Hintergrund */</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">color</span>: <span style="color:#ae81ff">#e2e8f0</span>; <span style="color:#75715e">/* Helle Schriftfarbe */</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        .<span style="color:#a6e22e">bg-dark</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">background-color</span>: <span style="color:#ae81ff">#2d3748</span>; <span style="color:#75715e">/* Dunklere Hintergrundfarbe für Container */</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        .<span style="color:#a6e22e">text-light</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">color</span>: <span style="color:#ae81ff">#e2e8f0</span>; <span style="color:#75715e">/* Helle Schriftfarbe */</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        .<span style="color:#a6e22e">text-accent</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">color</span>: <span style="color:#ae81ff">#63b3ed</span>; <span style="color:#75715e">/* Akzentfarbe für Titel */</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        .<span style="color:#a6e22e">logo</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">max-width</span>: <span style="color:#ae81ff">100</span><span style="color:#66d9ef">px</span>; <span style="color:#75715e">/* Maximale Breite des Logos */</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">margin</span>: <span style="color:#ae81ff">0</span> <span style="color:#66d9ef">auto</span>; <span style="color:#75715e">/* Zentrieren */</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">display</span>: <span style="color:#66d9ef">block</span>;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">style</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">tailwind</span>.<span style="color:#a6e22e">config</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">theme</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">extend</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">fontFamily</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>                        <span style="color:#e6db74">&#39;inter&#39;</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#39;Inter&#39;</span>, <span style="color:#e6db74">&#39;sans-serif&#39;</span>],
</span></span><span style="display:flex;"><span>                    },
</span></span><span style="display:flex;"><span>                },
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">head</span>&gt;
</span></span><span style="display:flex;"><span>&lt;<span style="color:#f92672">body</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;p-6&#34;</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;container max-w-2xl mx-auto bg-dark shadow-md rounded-lg p-8&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">&lt;!-- Logo hinzufügen --&gt;</span>
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">img</span> <span style="color:#a6e22e">src</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;{{ url_for(&#39;static&#39;, filename=&#39;cuteGPS.webp&#39;) }}&#34;</span> <span style="color:#a6e22e">alt</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;GPS Logo&#34;</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;logo mb-4&#34;</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">h1</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text-3xl font-semibold text-accent text-center mb-6&#34;</span>&gt;GPS/NTP Drift Monitor&lt;/<span style="color:#f92672">h1</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;mb-6&#34;</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">h2</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text-xl font-semibold text-light mb-3&#34;</span>&gt;GPS Zeit:&lt;/<span style="color:#f92672">h2</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;gps-time&#34;</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text-lg text-light p-4 bg-gray-700 rounded-md&#34;</span>&gt;Lädt...&lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;mb-6&#34;</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">h2</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text-xl font-semibold text-light mb-3&#34;</span>&gt;NTP Zeiten:&lt;/<span style="color:#f92672">h2</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ntp-times&#34;</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;space-y-2&#34;</span>&gt;
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">&lt;!-- NTP Zeiten werden hier dynamisch eingefügt --&gt;</span>
</span></span><span style="display:flex;"><span>            &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;mb-6&#34;</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">h2</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text-xl font-semibold text-light mb-3&#34;</span>&gt;Drift (GPS - NTP):&lt;/<span style="color:#f92672">h2</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;drifts&#34;</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;space-y-4&#34;</span>&gt;
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">&lt;!-- Drift-Werte werden hier dynamisch eingefügt --&gt;</span>
</span></span><span style="display:flex;"><span>            &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">div</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text-center mt-8&#34;</span>&gt;
</span></span><span style="display:flex;"><span>            &lt;<span style="color:#f92672">p</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">&#34;text-gray-400 text-sm&#34;</span>&gt;Daten werden alle 5 Sekunden aktualisiert.&lt;/<span style="color:#f92672">p</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">gpsTimeElement</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">getElementById</span>(<span style="color:#e6db74">&#39;gps-time&#39;</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ntpTimesElement</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">getElementById</span>(<span style="color:#e6db74">&#39;ntp-times&#39;</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">driftsElement</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">getElementById</span>(<span style="color:#e6db74">&#39;drifts&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">updateData</span>() {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">fetch</span>(<span style="color:#e6db74">&#39;/data&#39;</span>)
</span></span><span style="display:flex;"><span>                .<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">response</span> =&gt; <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">json</span>())
</span></span><span style="display:flex;"><span>                .<span style="color:#a6e22e">then</span>(<span style="color:#a6e22e">data</span> =&gt; {
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">// GPS Zeit aktualisieren
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    <span style="color:#a6e22e">gpsTimeElement</span>.<span style="color:#a6e22e">textContent</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">gps_time</span> <span style="color:#f92672">?</span> <span style="color:#66d9ef">new</span> Date(<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">gps_time</span>).<span style="color:#a6e22e">toLocaleString</span>() <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Keine Daten&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">// NTP Zeiten aktualisieren
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    <span style="color:#a6e22e">ntpTimesElement</span>.<span style="color:#a6e22e">innerHTML</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>; <span style="color:#75715e">// Vorherige Inhalte löschen
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">ntp_times</span>) {
</span></span><span style="display:flex;"><span>                        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">server</span> <span style="color:#66d9ef">in</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">ntp_times</span>) {
</span></span><span style="display:flex;"><span>                            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ntpTime</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">ntp_times</span>[<span style="color:#a6e22e">server</span>] <span style="color:#f92672">?</span> <span style="color:#66d9ef">new</span> Date(<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">ntp_times</span>[<span style="color:#a6e22e">server</span>]).<span style="color:#a6e22e">toLocaleString</span>() <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Keine Daten&#39;</span>;
</span></span><span style="display:flex;"><span>                            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">ntpDiv</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#39;div&#39;</span>);
</span></span><span style="display:flex;"><span>                            <span style="color:#a6e22e">ntpDiv</span>.<span style="color:#a6e22e">className</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;p-2 bg-gray-700 rounded-md text-light&#39;</span>;
</span></span><span style="display:flex;"><span>                            <span style="color:#a6e22e">ntpDiv</span>.<span style="color:#a6e22e">textContent</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">server</span><span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">ntpTime</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span>                            <span style="color:#a6e22e">ntpTimesElement</span>.<span style="color:#a6e22e">appendChild</span>(<span style="color:#a6e22e">ntpDiv</span>);
</span></span><span style="display:flex;"><span>                        }
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">// Drifts aktualisieren
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    <span style="color:#a6e22e">driftsElement</span>.<span style="color:#a6e22e">innerHTML</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;</span>; <span style="color:#75715e">// Vorherige Inhalte löschen
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">drifts</span>) {
</span></span><span style="display:flex;"><span>                        <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">server</span> <span style="color:#66d9ef">in</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">drifts</span>) {
</span></span><span style="display:flex;"><span>                            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">driftValue</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">drifts</span>[<span style="color:#a6e22e">server</span>] <span style="color:#f92672">!==</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">?</span> <span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">drifts</span>[<span style="color:#a6e22e">server</span>].<span style="color:#a6e22e">toFixed</span>(<span style="color:#ae81ff">2</span>) <span style="color:#f92672">+</span> <span style="color:#e6db74">&#39; ms&#39;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;Keine Daten&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">driftDiv</span> <span style="color:#f92672">=</span> document.<span style="color:#a6e22e">createElement</span>(<span style="color:#e6db74">&#39;div&#39;</span>);
</span></span><span style="display:flex;"><span>                            <span style="color:#a6e22e">driftDiv</span>.<span style="color:#a6e22e">className</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;p-2 bg-gray-700 rounded-md text-light&#39;</span>;
</span></span><span style="display:flex;"><span>                            <span style="color:#a6e22e">driftDiv</span>.<span style="color:#a6e22e">textContent</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">server</span><span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">driftValue</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span>                            <span style="color:#a6e22e">driftsElement</span>.<span style="color:#a6e22e">appendChild</span>(<span style="color:#a6e22e">driftDiv</span>);
</span></span><span style="display:flex;"><span>                        }
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                })
</span></span><span style="display:flex;"><span>                .<span style="color:#66d9ef">catch</span>(<span style="color:#a6e22e">error</span> =&gt; {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">console</span>.<span style="color:#a6e22e">error</span>(<span style="color:#e6db74">&#39;Fehler beim Abrufen der Daten:&#39;</span>, <span style="color:#a6e22e">error</span>);
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">gpsTimeElement</span>.<span style="color:#a6e22e">textContent</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Fehler beim Abrufen der Daten&#39;</span>;
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">ntpTimesElement</span>.<span style="color:#a6e22e">innerHTML</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Fehler beim Abrufen der Daten&#39;</span>;
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">driftsElement</span>.<span style="color:#a6e22e">innerHTML</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;Fehler beim Abrufen der Daten&#39;</span>;
</span></span><span style="display:flex;"><span>                });
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">updateData</span>(); <span style="color:#75715e">// Initiales Update
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#a6e22e">setInterval</span>(<span style="color:#a6e22e">updateData</span>, <span style="color:#ae81ff">5000</span>); <span style="color:#75715e">// Aktualisierung alle 5 Sekunden
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    &lt;/<span style="color:#f92672">script</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">body</span>&gt;
</span></span><span style="display:flex;"><span>&lt;/<span style="color:#f92672">html</span>&gt;
</span></span></code></pre></div><p>This code talks to the flask backend.</p>
<h2 id="backend">
  
  
  Backend
  
</h2>
<p>My backend is written in Python. It&rsquo;s my favorite for just about everything, though I can imagine there might be better languages for this specific project. Normally, I would have used Python&rsquo;s <code>argparse</code> module to handle configuration. For this blog post, however, I decided against it for the sake of clarity and to make the code easier to follow. This is also my first script using <code>asyncio</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> asyncio
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> re
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> datetime <span style="color:#f92672">import</span> datetime, timedelta, timezone
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> logging
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> ntplib
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> serial
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> flask <span style="color:#f92672">import</span> Flask, jsonify, render_template  <span style="color:#75715e"># Importiere Flask</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> tinyflux <span style="color:#f92672">import</span> Point, TinyFlux  <span style="color:#75715e"># Importiere TinyFlux und Point</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> tinyflux.queries <span style="color:#f92672">import</span> (FieldQuery, Query, TagQuery,  <span style="color:#75715e"># Importiere Query</span>
</span></span><span style="display:flex;"><span>                              TimeQuery)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Konfiguration der seriellen Schnittstelle für die GPS-Maus</span>
</span></span><span style="display:flex;"><span>serial_port <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/dev/ttyACM0&#34;</span>
</span></span><span style="display:flex;"><span>baud_rate <span style="color:#f92672">=</span> <span style="color:#ae81ff">9600</span>
</span></span><span style="display:flex;"><span>timeout <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># NTP-Server Liste</span>
</span></span><span style="display:flex;"><span>ntp_servers <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;1.europe.pool.ntp.org&#34;</span>, <span style="color:#e6db74">&#34;0.europe.pool.ntp.org&#34;</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Globale Variablen</span>
</span></span><span style="display:flex;"><span>latest_gps_time <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>latest_ntp_times <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>latest_drifts <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>gps_serial <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># TinyFlux Datenbank initialisieren</span>
</span></span><span style="display:flex;"><span>db <span style="color:#f92672">=</span> TinyFlux(<span style="color:#e6db74">&#34;time_data.json&#34;</span>)  <span style="color:#75715e"># Verwende eine Datei zum Speichern der Daten</span>
</span></span><span style="display:flex;"><span>tag_query <span style="color:#f92672">=</span> TagQuery()
</span></span><span style="display:flex;"><span>time_query <span style="color:#f92672">=</span> TimeQuery()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Flask App initialisieren</span>
</span></span><span style="display:flex;"><span>app <span style="color:#f92672">=</span> Flask(__name__)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Funktion zur Extraktion der Zeit vom GPS</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_gps_time</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Liest Daten von der seriellen Schnittstelle, extrahiert die Zeit aus dem GPRMC-Satz
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    und konvertiert sie in ein datetime-Objekt.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">global</span> latest_gps_time, gps_serial
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> gps_serial <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            gps_serial <span style="color:#f92672">=</span> serial<span style="color:#f92672">.</span>Serial(serial_port, baud_rate, timeout<span style="color:#f92672">=</span>timeout)
</span></span><span style="display:flex;"><span>            logging<span style="color:#f92672">.</span>info(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Serielle Verbindung zu </span><span style="color:#e6db74">{</span>serial_port<span style="color:#e6db74">}</span><span style="color:#e6db74"> hergestellt.&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> serial<span style="color:#f92672">.</span>SerialException <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>            logging<span style="color:#f92672">.</span>error(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Fehler beim Öffnen der seriellen Schnittstelle </span><span style="color:#e6db74">{</span>serial_port<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        line <span style="color:#f92672">=</span> gps_serial<span style="color:#f92672">.</span>readline()<span style="color:#f92672">.</span>decode(<span style="color:#e6db74">&#34;utf-8&#34;</span>, errors<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;ignore&#34;</span>)<span style="color:#f92672">.</span>strip()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> line<span style="color:#f92672">.</span>startswith(<span style="color:#e6db74">&#34;$GPRMC&#34;</span>):
</span></span><span style="display:flex;"><span>            parts <span style="color:#f92672">=</span> line<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#34;,&#34;</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> parts[<span style="color:#ae81ff">2</span>] <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;A&#34;</span>:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>                    time_str <span style="color:#f92672">=</span> parts[<span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>                    date_str <span style="color:#f92672">=</span> parts[<span style="color:#ae81ff">9</span>]
</span></span><span style="display:flex;"><span>                    day <span style="color:#f92672">=</span> int(date_str[<span style="color:#ae81ff">0</span>:<span style="color:#ae81ff">2</span>])
</span></span><span style="display:flex;"><span>                    month <span style="color:#f92672">=</span> int(date_str[<span style="color:#ae81ff">2</span>:<span style="color:#ae81ff">4</span>])
</span></span><span style="display:flex;"><span>                    year <span style="color:#f92672">=</span> <span style="color:#ae81ff">2000</span> <span style="color:#f92672">+</span> int(date_str[<span style="color:#ae81ff">4</span>:<span style="color:#ae81ff">6</span>])
</span></span><span style="display:flex;"><span>                    hour <span style="color:#f92672">=</span> int(time_str[<span style="color:#ae81ff">0</span>:<span style="color:#ae81ff">2</span>])
</span></span><span style="display:flex;"><span>                    minute <span style="color:#f92672">=</span> int(time_str[<span style="color:#ae81ff">2</span>:<span style="color:#ae81ff">4</span>])
</span></span><span style="display:flex;"><span>                    second <span style="color:#f92672">=</span> int(time_str[<span style="color:#ae81ff">4</span>:<span style="color:#ae81ff">6</span>])
</span></span><span style="display:flex;"><span>                    microsecond <span style="color:#f92672">=</span> (
</span></span><span style="display:flex;"><span>                        int(float(time_str[<span style="color:#ae81ff">7</span>:]) <span style="color:#f92672">*</span> <span style="color:#ae81ff">1000</span>) <span style="color:#66d9ef">if</span> len(time_str) <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">6</span> <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>                    )
</span></span><span style="display:flex;"><span>                    latest_gps_time <span style="color:#f92672">=</span> datetime(
</span></span><span style="display:flex;"><span>                        year,
</span></span><span style="display:flex;"><span>                        month,
</span></span><span style="display:flex;"><span>                        day,
</span></span><span style="display:flex;"><span>                        hour,
</span></span><span style="display:flex;"><span>                        minute,
</span></span><span style="display:flex;"><span>                        second,
</span></span><span style="display:flex;"><span>                        microsecond,
</span></span><span style="display:flex;"><span>                        tzinfo<span style="color:#f92672">=</span>timezone<span style="color:#f92672">.</span>utc,
</span></span><span style="display:flex;"><span>                    )
</span></span><span style="display:flex;"><span>                    logging<span style="color:#f92672">.</span>info(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;GPS Zeit (parsed): </span><span style="color:#e6db74">{</span>latest_gps_time<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">return</span> latest_gps_time
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">ValueError</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>                    logging<span style="color:#f92672">.</span>error(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Error beim Parsen der GPS-Zeit: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">, Zeile: </span><span style="color:#e6db74">{</span>line<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>                    latest_gps_time <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                logging<span style="color:#f92672">.</span>error(<span style="color:#e6db74">&#34;GPS Zeit ungültig (V)&#34;</span>)
</span></span><span style="display:flex;"><span>                latest_gps_time <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> serial<span style="color:#f92672">.</span>SerialException <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        logging<span style="color:#f92672">.</span>error(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Fehler beim Lesen von der seriellen Schnittstelle: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        latest_gps_time <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            gps_serial<span style="color:#f92672">.</span>close()
</span></span><span style="display:flex;"><span>            gps_serial <span style="color:#f92672">=</span> serial<span style="color:#f92672">.</span>Serial(serial_port, baud_rate, timeout<span style="color:#f92672">=</span>timeout)
</span></span><span style="display:flex;"><span>            logging<span style="color:#f92672">.</span>info(<span style="color:#e6db74">&#34;Serielle Verbindung wiederhergestellt.&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> serial<span style="color:#f92672">.</span>SerialException <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>            logging<span style="color:#f92672">.</span>error(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Fehler beim Wiederherstellen der seriellen Verbindung: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">UnicodeDecodeError</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        logging<span style="color:#f92672">.</span>error(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Fehler beim Dekodieren der seriellen Daten: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">, Zeile: </span><span style="color:#e6db74">{</span>line<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        latest_gps_time <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        logging<span style="color:#f92672">.</span>error(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Ein unerwarteter Fehler ist aufgetreten: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        latest_gps_time <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Funktion zum Abrufen der NTP-Zeit</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_ntp_time</span>(server):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Ruft die Zeit von einem NTP-Server ab.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">global</span> latest_ntp_times
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        ntp_time <span style="color:#f92672">=</span> ntplib<span style="color:#f92672">.</span>NTPClient()<span style="color:#f92672">.</span>request(server, version<span style="color:#f92672">=</span><span style="color:#ae81ff">3</span>)<span style="color:#f92672">.</span>tx_time
</span></span><span style="display:flex;"><span>        ntp_time_datetime <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>fromtimestamp(ntp_time, tz<span style="color:#f92672">=</span>timezone<span style="color:#f92672">.</span>utc)
</span></span><span style="display:flex;"><span>        latest_ntp_times[server] <span style="color:#f92672">=</span> ntp_time_datetime
</span></span><span style="display:flex;"><span>        logging<span style="color:#f92672">.</span>info(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;NTP Zeit von </span><span style="color:#e6db74">{</span>server<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>ntp_time_datetime<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> latest_ntp_times[server]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
</span></span><span style="display:flex;"><span>        logging<span style="color:#f92672">.</span>error(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Fehler beim Abrufen der NTP-Zeit von </span><span style="color:#e6db74">{</span>server<span style="color:#e6db74">}</span><span style="color:#e6db74">: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        latest_ntp_times[server] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Funktion zur Berechnung der Zeitabweichung</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">calculate_drift</span>(gps_time, ntp_time):
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Berechnet die Zeitabweichung zwischen GPS- und NTP-Zeit in Millisekunden.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> gps_time <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span> <span style="color:#f92672">or</span> ntp_time <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> (ntp_time <span style="color:#f92672">-</span> gps_time)<span style="color:#f92672">.</span>total_seconds() <span style="color:#f92672">*</span> <span style="color:#ae81ff">1000</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Funktion zum Aktualisieren der Daten (wird von Flask aufgerufen)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">update_data</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Ruft GPS- und NTP-Zeit ab und speichert sie in der Datenbank.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Berücksichtigt die Zeitverzögerung bei der Drift-Berechnung.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">global</span> latest_gps_time, latest_ntp_times, latest_drifts
</span></span><span style="display:flex;"><span>    gps_time <span style="color:#f92672">=</span> get_gps_time()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> gps_time:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> server <span style="color:#f92672">in</span> ntp_servers:
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># Messen der Zeit vor dem Abrufen der NTP-Zeit</span>
</span></span><span style="display:flex;"><span>            start_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>now(timezone<span style="color:#f92672">.</span>utc)
</span></span><span style="display:flex;"><span>            ntp_time <span style="color:#f92672">=</span> get_ntp_time(server)
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># Messen der Zeit nach dem Abrufen der NTP-Zeit</span>
</span></span><span style="display:flex;"><span>            end_time <span style="color:#f92672">=</span> datetime<span style="color:#f92672">.</span>now(timezone<span style="color:#f92672">.</span>utc)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> ntp_time:
</span></span><span style="display:flex;"><span>                <span style="color:#75715e"># Berechnung der mittleren Verzögerung</span>
</span></span><span style="display:flex;"><span>                processing_delay <span style="color:#f92672">=</span> (end_time <span style="color:#f92672">-</span> start_time)<span style="color:#f92672">.</span>total_seconds() <span style="color:#f92672">/</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e"># Anpassung der NTP-Zeit</span>
</span></span><span style="display:flex;"><span>                adjusted_ntp_time <span style="color:#f92672">=</span> ntp_time <span style="color:#f92672">-</span> timedelta(seconds<span style="color:#f92672">=</span>processing_delay)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                drift <span style="color:#f92672">=</span> calculate_drift(gps_time, adjusted_ntp_time)
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> drift <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>                    latest_drifts[server] <span style="color:#f92672">=</span> drift
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e"># Speichere die Daten in der TinyFlux Datenbank</span>
</span></span><span style="display:flex;"><span>                    db<span style="color:#f92672">.</span>insert(
</span></span><span style="display:flex;"><span>                        Point(
</span></span><span style="display:flex;"><span>                            measurement<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;time_data&#34;</span>,
</span></span><span style="display:flex;"><span>                            tags<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;GPS&#34;</span>},
</span></span><span style="display:flex;"><span>                            fields<span style="color:#f92672">=</span>{
</span></span><span style="display:flex;"><span>                                <span style="color:#e6db74">&#34;time&#34;</span>: gps_time<span style="color:#f92672">.</span>timestamp()
</span></span><span style="display:flex;"><span>                            },  <span style="color:#75715e"># Als Unix-Zeitstempel speichern</span>
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    )
</span></span><span style="display:flex;"><span>                    db<span style="color:#f92672">.</span>insert(
</span></span><span style="display:flex;"><span>                        Point(
</span></span><span style="display:flex;"><span>                            measurement<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;time_data&#34;</span>,
</span></span><span style="display:flex;"><span>                            tags<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;source&#34;</span>: <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;NTP-</span><span style="color:#e6db74">{</span>server<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>},
</span></span><span style="display:flex;"><span>                            fields<span style="color:#f92672">=</span>{
</span></span><span style="display:flex;"><span>                                <span style="color:#e6db74">&#34;time&#34;</span>: adjusted_ntp_time<span style="color:#f92672">.</span>timestamp()
</span></span><span style="display:flex;"><span>                            },  <span style="color:#75715e"># Als Unix-Zeitstempel speichern</span>
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    )
</span></span><span style="display:flex;"><span>                    db<span style="color:#f92672">.</span>insert(
</span></span><span style="display:flex;"><span>                        Point(
</span></span><span style="display:flex;"><span>                            measurement<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;drift_data&#34;</span>,
</span></span><span style="display:flex;"><span>                            tags<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;server&#34;</span>: server},
</span></span><span style="display:flex;"><span>                            fields<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;drift&#34;</span>: drift},
</span></span><span style="display:flex;"><span>                            time<span style="color:#f92672">=</span>datetime<span style="color:#f92672">.</span>now(
</span></span><span style="display:flex;"><span>                                timezone<span style="color:#f92672">.</span>utc
</span></span><span style="display:flex;"><span>                            ),  <span style="color:#75715e"># Zeitstempel für den Drift</span>
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    )
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                    latest_drifts[server] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                latest_drifts[server] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> server <span style="color:#f92672">in</span> ntp_servers:
</span></span><span style="display:flex;"><span>            latest_drifts[server] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@app.route</span>(<span style="color:#e6db74">&#34;/&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">index</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Rendert die Hauptseite der Webanwendung.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> render_template(<span style="color:#e6db74">&#34;index.html&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@app.route</span>(<span style="color:#e6db74">&#34;/data&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_data</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Gibt die aktuellen GPS-Zeit, NTP-Zeiten und Abweichungen im JSON-Format zurück.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">global</span> latest_gps_time, latest_ntp_times, latest_drifts
</span></span><span style="display:flex;"><span>    update_data()
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Lese die neuesten Daten aus der TinyFlux Datenbank</span>
</span></span><span style="display:flex;"><span>    gps_data <span style="color:#f92672">=</span> db<span style="color:#f92672">.</span>search(tag_query<span style="color:#f92672">.</span>source <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;GPS&#34;</span>)
</span></span><span style="display:flex;"><span>    latest_gps_data <span style="color:#f92672">=</span> (
</span></span><span style="display:flex;"><span>        datetime<span style="color:#f92672">.</span>fromtimestamp(gps_data[<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>]<span style="color:#f92672">.</span>fields[<span style="color:#e6db74">&#34;time&#34;</span>], tz<span style="color:#f92672">=</span>timezone<span style="color:#f92672">.</span>utc)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> gps_data
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    drift_data_list <span style="color:#f92672">=</span> db<span style="color:#f92672">.</span>search(tag_query<span style="color:#f92672">.</span>server<span style="color:#f92672">.</span>exists())
</span></span><span style="display:flex;"><span>    latest_drifts_data <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> drift_data_list:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> entry <span style="color:#f92672">in</span> drift_data_list:
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#e6db74">&#34;drift&#34;</span> <span style="color:#f92672">in</span> entry<span style="color:#f92672">.</span>fields:  <span style="color:#75715e"># Überprüfen, ob &#39;drift&#39; existiert</span>
</span></span><span style="display:flex;"><span>                latest_drifts_data[entry<span style="color:#f92672">.</span>tags[<span style="color:#e6db74">&#34;server&#34;</span>]] <span style="color:#f92672">=</span> entry<span style="color:#f92672">.</span>fields[<span style="color:#e6db74">&#34;drift&#34;</span>]
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                logging<span style="color:#f92672">.</span>warning(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;Warnung: Eintrag ohne &#39;drift&#39;-Feld gefunden: </span><span style="color:#e6db74">{</span>entry<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>        latest_drifts_data <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    latest_ntp_times_data <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> server <span style="color:#f92672">in</span> ntp_servers:
</span></span><span style="display:flex;"><span>        ntp_data <span style="color:#f92672">=</span> db<span style="color:#f92672">.</span>search(tag_query<span style="color:#f92672">.</span>source <span style="color:#f92672">==</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;NTP-</span><span style="color:#e6db74">{</span>server<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>        latest_ntp_times_data[server] <span style="color:#f92672">=</span> (
</span></span><span style="display:flex;"><span>            datetime<span style="color:#f92672">.</span>fromtimestamp(ntp_data[<span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>]<span style="color:#f92672">.</span>fields[<span style="color:#e6db74">&#34;time&#34;</span>], tz<span style="color:#f92672">=</span>timezone<span style="color:#f92672">.</span>utc)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> ntp_data
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> jsonify(
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;gps_time&#34;</span>: latest_gps_data<span style="color:#f92672">.</span>isoformat() <span style="color:#66d9ef">if</span> latest_gps_data <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">None</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;ntp_times&#34;</span>: {
</span></span><span style="display:flex;"><span>                server: time<span style="color:#f92672">.</span>isoformat() <span style="color:#66d9ef">if</span> time <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">for</span> server, time <span style="color:#f92672">in</span> latest_ntp_times_data<span style="color:#f92672">.</span>items()
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;drifts&#34;</span>: latest_drifts_data,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;ntp_servers&#34;</span>: ntp_servers,
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">start_background_task</span>():
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Startet eine Hintergrundaufgabe, um die Daten regelmäßig zu aktualisieren.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    loop <span style="color:#f92672">=</span> asyncio<span style="color:#f92672">.</span>new_event_loop()
</span></span><span style="display:flex;"><span>    asyncio<span style="color:#f92672">.</span>set_event_loop(loop)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">while</span> <span style="color:#66d9ef">True</span>:
</span></span><span style="display:flex;"><span>        update_data()
</span></span><span style="display:flex;"><span>        time<span style="color:#f92672">.</span>sleep(<span style="color:#ae81ff">5</span>)
</span></span><span style="display:flex;"><span>    loop<span style="color:#f92672">.</span>close()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> threading
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span>    logging<span style="color:#f92672">.</span>basicConfig(
</span></span><span style="display:flex;"><span>    level<span style="color:#f92672">=</span>logging<span style="color:#f92672">.</span>DEBUG,  <span style="color:#75715e"># Log-Level (z. B. DEBUG, INFO, WARNING, ERROR, CRITICAL)</span>
</span></span><span style="display:flex;"><span>    format<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span><span style="color:#e6db74">%(asctime)s</span><span style="color:#e6db74"> - </span><span style="color:#e6db74">%(levelname)s</span><span style="color:#e6db74"> - </span><span style="color:#e6db74">%(message)s</span><span style="color:#e6db74">&#34;</span>,  <span style="color:#75715e"># Log-Format</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Starte den Hintergrund-Thread</span>
</span></span><span style="display:flex;"><span>    background_thread <span style="color:#f92672">=</span> threading<span style="color:#f92672">.</span>Thread(target<span style="color:#f92672">=</span>start_background_task)
</span></span><span style="display:flex;"><span>    background_thread<span style="color:#f92672">.</span>daemon <span style="color:#f92672">=</span> <span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    background_thread<span style="color:#f92672">.</span>start()
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Starte die Flask-App</span>
</span></span><span style="display:flex;"><span>    app<span style="color:#f92672">.</span>run(debug<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>, host<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;0.0.0.0&#34;</span>)
</span></span></code></pre></div><h3 id="reading-gps-data">
  
  
  Reading GPS Data
  
</h3>
<p>The <code>get_gps_time</code> function reads data from a serial port, extracts time information from a GPS data sentence in NMEA format, and converts it into a <code>datetime</code> object. It is designed to work with GPS modules that transmit data via serial communication. The function uses global variables like <code>latest_gps_time</code> and <code>gps_serial</code> to manage the most recently captured timestamp and the serial connection.</p>
<p>Initially, the function checks if the serial connection (<code>gps_serial</code>) is already initialized. If not, it attempts to establish a connection using the parameters defined in the <code>serial_port</code>, <code>baud_rate</code>, and <code>timeout</code> variables. If an error occurs during initialization, an error message is logged, and the function returns <code>None</code>.</p>
<p>The function then reads a line from the serial port, decodes it as UTF-8, and strips any unwanted whitespace. If the line starts with <code>$GPRMC</code>, it is split into its components. The third part of the data sentence (<code>parts[2]</code>) indicates whether the GPS data is valid. Only if this value is <code>A</code> is the time and date information processed further. The time (<code>parts[1]</code>) and the date (<code>parts[9]</code>) are extracted and broken down into their respective components (hour, minute, second, day, month, year). If the time string includes milliseconds, these are also taken into account. With this information, a <code>datetime</code> object using the UTC timezone is created and stored in the global <code>latest_gps_time</code> variable.</p>
<p>If an error occurs during processing, such as an invalid time format (<code>ValueError</code>), an error message is logged, and the function returns <code>None</code>. Similar error-handling mechanisms exist for problems reading from the serial port (<code>serial.SerialException</code>), decoding errors (<code>UnicodeDecodeError</code>), and other unexpected exceptions. In case of a connection error, the function attempts to re-initialize the serial connection.</p>
<p>Also very important: this function will not work if <code>gpsd</code> is running. This tool changes the output of the GPS receiver to a different format, which this script is not designed to parse.</p>
<h3 id="fetching-ntp-time">
  
  
  Fetching NTP Time
  
</h3>
<p>The <code>get_ntp_time</code> function is built to communicate with NTP servers using the appropriate Python libraries. For my tool, I didn&rsquo;t want to rely on the system time and system configuration; I hope that wasn&rsquo;t a mistake. Since ready-made libraries for NTP already exist, this section of the code is naturally much shorter than the GPS section.</p>
<h3 id="database">
  
  
  Database
  
</h3>
<p>TinyFlux is an open-source database library designed specifically for storing and querying time-series data in Python. You can think of it as a greatly simplified, file-based alternative to larger time-series databases like InfluxDB. Instead of a complex server, TinyFlux stores its data in a single file, typically in JSON format. This significantly simplifies setup and management, especially for smaller projects or local applications.</p>
<h2 id="future-ideas">
  
  
  Future Ideas&hellip;
  
</h2>
<p>I would like to network this script&hellip; By that, I mean creating a central database for the time drift. Perhaps it would also be possible in the future to integrate this tool into the honeypot from Deutsche Telekom Security GmbH. This attack monitoring solution is already openly available on <a href="https://github.com/telekom-security/tpotce">GitHub</a>.</p>
<p>Another idea for such a solution would be a distributed database, similar to the Yacy search engine or other P2P services.</p>
<p>Supporting multiple GPS receivers also sounds tempting, but I currently lack the necessary hardware for it.</p>
<p>My final future idea is to make this available for mobile phones. Old phones are often just lying around, but they offer a large number of interfaces and sensors. This usually includes a very good GPS receiver, which would be great for this project.</p>
]]></description>
    </item>
    
    <item>
      <title>On-call duty planning goes Python</title>
      <link>https://do3eet.pages.dev/en/post/rufbereitschaft1/</link>
      <pubDate>Sun, 13 Oct 2024 15:35:13 +0200</pubDate>
      <guid>https://do3eet.pages.dev/en/post/rufbereitschaft1/</guid>
      <description><![CDATA[<p>Alright, steering clear of the usual sparks and travelogues this time around! Instead, I&rsquo;m diving back into a mission I&rsquo;m passionate about: making life a little less of a grind for those of us in the IT trenches.</p>
<p>You know the drill. As the year winds down, that familiar beast slouches into view for countless team leads (or, let&rsquo;s be honest, usually their deputies): the dreaded on-call duty roster planning. What sounds like a straightforward task is, in reality, a minefield. We&rsquo;re talking about navigating the treacherous waters of works council notification deadlines on one hand, and keeping your actual team members clued in without mass confusion on the other.</p>
<p>Believe me, I&rsquo;ve been there. I&rsquo;ve witnessed firsthand the classic standoff between the official demands of the works council and what employees actually find clear and usable. I’ve personally ridden the rollercoaster from trusty old Excel to a sleek online team calendar&hellip; and right back to Excel, all because the tool&rsquo;s export format didn&rsquo;t quite get the works council&rsquo;s seal of approval, or some other similar bureaucratic joy. It’s a recurring headache, and this time, I want to explore it further.</p>
<p>Just picture the annual slog: manually constructing a new Excel calendar from scratch, then meticulously wrestling the details for around 20 users across 12 separate worksheets. Let&rsquo;s be honest, it&rsquo;s not just mind-numbingly inefficient; it’s an approach that feels beneath anyone who&rsquo;s spent time in the IT field. But here’s where a bit of ingenuity can shine: with a touch of Python, you can at least automate the entire, tedious creation of that table structure. And if you&rsquo;re willing to invest a little more effort, you could even get Python to take on the initial heavy lifting of distributing those on-call duties.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e">#!/usr/bin/env python3</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> calendar
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> argparse
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> datetime <span style="color:#f92672">import</span> date, timedelta
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> xlsxwriter
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">numToDayShort</span>(num):
</span></span><span style="display:flex;"><span>    weekdays_short <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;Mon&#34;</span>, <span style="color:#e6db74">&#34;Tue&#34;</span>, <span style="color:#e6db74">&#34;Wed&#34;</span>, <span style="color:#e6db74">&#34;Thu&#34;</span>, <span style="color:#e6db74">&#34;Fri&#34;</span>, <span style="color:#e6db74">&#34;Sat&#34;</span>, <span style="color:#e6db74">&#34;Sun&#34;</span>]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> weekdays_short[num]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">numToMonth</span>(num):
</span></span><span style="display:flex;"><span>    months <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;January&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;February&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;March&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;April&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;May&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;June&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;July&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;August&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;September&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;October&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;November&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;December&#34;</span>,
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> months[num <span style="color:#f92672">-</span> <span style="color:#ae81ff">1</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
</span></span><span style="display:flex;"><span>    workerNames <span style="color:#f92672">=</span> [<span style="color:#e6db74">&#34;Frank Tornack&#34;</span>, <span style="color:#e6db74">&#34;Meister Kollege&#34;</span>, <span style="color:#e6db74">&#34;Walter Hugo&#34;</span>, <span style="color:#e6db74">&#34;Dr. Der Da&#34;</span>]
</span></span><span style="display:flex;"><span>    workbook <span style="color:#f92672">=</span> xlsxwriter<span style="color:#f92672">.</span>Workbook(<span style="color:#e6db74">&#34;TestRB.xlsx&#34;</span>)
</span></span><span style="display:flex;"><span>    centerformat <span style="color:#f92672">=</span> workbook<span style="color:#f92672">.</span>add_format()
</span></span><span style="display:flex;"><span>    centerformat<span style="color:#f92672">.</span>set_align(<span style="color:#e6db74">&#34;center&#34;</span>)
</span></span><span style="display:flex;"><span>    centerformat<span style="color:#f92672">.</span>set_bold()
</span></span><span style="display:flex;"><span>    centerformat<span style="color:#f92672">.</span>set_font_name(<span style="color:#e6db74">&#34;Courier New&#34;</span>)
</span></span><span style="display:flex;"><span>    wecenterformat <span style="color:#f92672">=</span> workbook<span style="color:#f92672">.</span>add_format()
</span></span><span style="display:flex;"><span>    wecenterformat<span style="color:#f92672">.</span>set_align(<span style="color:#e6db74">&#34;center&#34;</span>)
</span></span><span style="display:flex;"><span>    wecenterformat<span style="color:#f92672">.</span>set_bold()
</span></span><span style="display:flex;"><span>    wecenterformat<span style="color:#f92672">.</span>set_font_color(<span style="color:#e6db74">&#34;#FF0000&#34;</span>)
</span></span><span style="display:flex;"><span>    wecenterformat<span style="color:#f92672">.</span>set_font_name(<span style="color:#e6db74">&#34;Courier New&#34;</span>)
</span></span><span style="display:flex;"><span>    boldonlyformat <span style="color:#f92672">=</span> workbook<span style="color:#f92672">.</span>add_format()
</span></span><span style="display:flex;"><span>    boldonlyformat<span style="color:#f92672">.</span>set_bold()
</span></span><span style="display:flex;"><span>    calyear <span style="color:#f92672">=</span> date<span style="color:#f92672">.</span>today()<span style="color:#f92672">.</span>year
</span></span><span style="display:flex;"><span>    calyear <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    Calobj <span style="color:#f92672">=</span> calendar<span style="color:#f92672">.</span>Calendar()
</span></span><span style="display:flex;"><span>    currentWorker <span style="color:#f92672">=</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    daysFromLast <span style="color:#f92672">=</span> {}
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> month <span style="color:#f92672">in</span> range(<span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">12</span> <span style="color:#f92672">+</span> <span style="color:#ae81ff">1</span>):
</span></span><span style="display:flex;"><span>        col <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>        namerowcounter <span style="color:#f92672">=</span> <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span>        worksheet <span style="color:#f92672">=</span> workbook<span style="color:#f92672">.</span>add_worksheet(numToMonth(month))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> workerName <span style="color:#f92672">in</span> workerNames:
</span></span><span style="display:flex;"><span>            worksheet<span style="color:#f92672">.</span>write(namerowcounter, col, workerName, boldonlyformat)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> workerName <span style="color:#f92672">in</span> daysFromLast:
</span></span><span style="display:flex;"><span>                daysFromLast[workerName] <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>            namerowcounter <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>        worksheet<span style="color:#f92672">.</span>autofit()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> day <span style="color:#f92672">in</span> Calobj<span style="color:#f92672">.</span>itermonthdays4(calyear, month):
</span></span><span style="display:flex;"><span>            col <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> month <span style="color:#f92672">==</span> day[<span style="color:#ae81ff">1</span>]:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> currentWorker <span style="color:#f92672">==</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>:
</span></span><span style="display:flex;"><span>                    currentWorker <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">elif</span> day[<span style="color:#ae81ff">3</span>] <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>:
</span></span><span style="display:flex;"><span>                    print(<span style="color:#e6db74">&#34;Monday&#34;</span>)
</span></span><span style="display:flex;"><span>                    currentWorker <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">if</span> currentWorker <span style="color:#f92672">&gt;=</span> len(workerNames):
</span></span><span style="display:flex;"><span>                        print(<span style="color:#e6db74">&#34;back to first&#34;</span>)
</span></span><span style="display:flex;"><span>                        currentWorker <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> key <span style="color:#f92672">in</span> daysFromLast:
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> key <span style="color:#f92672">==</span> workerNames[currentWorker]:
</span></span><span style="display:flex;"><span>                    daysFromLast[key] <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                    daysFromLast[key] <span style="color:#f92672">+=</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>            worksheet<span style="color:#f92672">.</span>write(<span style="color:#ae81ff">2</span> <span style="color:#f92672">+</span> currentWorker, col, <span style="color:#e6db74">&#34;RB&#34;</span>)
</span></span><span style="display:flex;"><span>            print(day)
</span></span><span style="display:flex;"><span>            print(daysFromLast)
</span></span><span style="display:flex;"><span>            worksheet<span style="color:#f92672">.</span>set_column(col, col, <span style="color:#ae81ff">4</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> day[<span style="color:#ae81ff">3</span>] <span style="color:#f92672">&lt;=</span> <span style="color:#ae81ff">4</span>:
</span></span><span style="display:flex;"><span>                worksheet<span style="color:#f92672">.</span>write(<span style="color:#ae81ff">0</span>, col, day[<span style="color:#ae81ff">2</span>], centerformat)
</span></span><span style="display:flex;"><span>                worksheet<span style="color:#f92672">.</span>write(<span style="color:#ae81ff">1</span>, col, numToDayShort(day[<span style="color:#ae81ff">3</span>]), centerformat)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>                worksheet<span style="color:#f92672">.</span>write(<span style="color:#ae81ff">0</span>, col, day[<span style="color:#ae81ff">2</span>], wecenterformat)
</span></span><span style="display:flex;"><span>                worksheet<span style="color:#f92672">.</span>write(<span style="color:#ae81ff">1</span>, col, numToDayShort(day[<span style="color:#ae81ff">3</span>]), wecenterformat)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    workbook<span style="color:#f92672">.</span>close()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;__main__&#34;</span>:
</span></span><span style="display:flex;"><span>    main()
</span></span></code></pre></div><p>So, what’s my little Python script capable of right now? Well, it&rsquo;s still a fledgling, but it already rolls up its digital sleeves to:</p>
<ul>
<li>Whip up a fully Microsoft-compatible XLSX file right from scratch.</li>
<li>It then gets to work populating it, smartly creating a dedicated tab for each month. And here’s a neat touch: it accurately includes those often-overlooked overlapping days from the previous and next months, so you always get the complete picture.</li>
<li>Plus, it neatly lays out all employee names in the first column of every sheet.</li>
</ul>
<p>Now, for a bit of real talk: in its current incarnation, the script isn&rsquo;t yet sophisticated enough to handle command-line arguments. This means that for the time being, you’ll need to take a quick peek into the source code to adjust the workerNames array directly. The output filename follows the same story – it’s just a line below in the code, ready for your customization.</p>
<p>But wait, there’s more under the hood! The script has a budding superpower: it already keeps a running tally of the days since each person last faced an on-call shift (though, for now, this vital intel is just displayed on the console). The grand vision, however, is to turbocharge this. I’m aiming to weave in dynamic logic that can intelligently juggle vacation schedules, those notoriously tricky public holidays that vary by region (yes, all you German Bundesländer, I see you!), and even birthdays.<br>
Once that piece of the puzzle is in place, it becomes an absolute must: the system has to leverage these &rsquo;time-since-last-duty&rsquo; metrics to forge genuine fairness into the schedule, ensuring no one is inadvertently skipped or unfairly burdened. And you can absolutely bet that if I crack this code, I’ll be heralding it from the digital rooftops in a brand-new article!</p>
<p>So, over to you! What other brilliant strokes of genius or essential features do you think this on-call scheduling sidekick is desperately missing? Don&rsquo;t hold back – unleash your thoughts in the comments below!</p>
]]></description>
    </item>
    
  </channel>
</rss>
