James Batchelor https://james-batchelor.com Useful I.T & VoIP Ramblings Mon, 29 Dec 2025 18:07:40 +0000 en-US hourly 1 https://wordpress.org/?v=6.8.5 https://james-batchelor.com/wp-content/uploads/2025/05/cropped-cropped-logo-jb-202505-32x32.png James Batchelor https://james-batchelor.com 32 32 Grafana Static Displays https://james-batchelor.com/index.php/2025/12/29/grafana-static-displays/ Mon, 29 Dec 2025 18:07:40 +0000 https://james-batchelor.com/?p=1080 Continue reading "Grafana Static Displays"]]> In the last post I migrated a Grafana database to a new server and took the opportunity to upgrade the Grafana version in the process.

This created a new issue, since Grafana’s supported browser list moves with the times, my two “NOC” screens consisting each of a Raspberry Pi 3A running Raspbian Buster and Hyperpixel 4.0 screens no longer worked. Instead, the outdated Chromium browsers now displayed the “If you’re seeing this Grafana has failed to load its application files” page when visiting the Grafana web interface.

Bringing the OS on the Pi’s up to date is the logical resolution, however, there are a couple of issues with this:

  • One of the Hyperpixel 4.0 screens is an early revision model, meaning the official and legacy drivers do not work.
  • Even with working screen drivers, compatible browsers will not load on the 512MB RAM available to the Pi 3A.

Not wanting to obsolete a pair of Pi 3A’s with a now unique form factor, instead a workaround was found to continue displaying Grafana dashboards on this older hardware. The solution even gave a slight benefit over the original URL…

To achieve this, Grafana has released grafana-image-renderer, essentially a binary with a self-contained and headless Chromium instance that will load a dashboard and capture an image of the output.

Until recently, this was available as a Grafana plugin, however this plugin has since been depreciated in favour of it being a standalone application or docker container. A lot of online documentation still refers to it in its plugin form and so is now outdated.

The current iteration of grafana-image-renderer recommends using it within a docker container due to the potential resource usage it can generate. However, for this guide, only the standalone binary will be used as its only generating two images, and I find resource usage is at a minimum.

Installation

Firstly, grab the binary and store it in a easy but sensible location:

cd /opt
wget https://github.com/grafana/grafana-image-renderer/releases/download/v5.0.6/grafana-image-renderer-linux-amd64

Then make the file executable:

chmod +x grafana-image-renderer-linux-amd64

To have it run as a service, create a service file for it:

nano /etc/systemd/system/grafana-image-renderer.service

And populate the file with the following:

[Unit]
Description=Grafana Image Renderer service
After=network.target

[Service]
ExecStart=/opt/grafana-image-renderer-linux-amd64 server --
WorkingDirectory=/opt/
Restart=on-failure
User=root
Environment=GRAFANA_RENDERER_PLUGIN_STARTUP_TIMEOUT=30s 
[Install]
WantedBy=multi-user.target

Reload the service daemon, start the new service, and check it is running:

systemctl daemon-reload
systemctl start grafana-image-renderer.service
systemctl status grafana-image-renderer.service

You can check If the service is running by visiting the web page of the server on port 8081:

Authentication

A default Grafana server install requires login before being able to see any dashboards, this extends to the image renderer. As we can’t login to the web interface interactively within the service, a service account needs to be created.

In the main Grafana web interface, login and navigate to Home > Administration > Users and access > Service accounts, then click “Add service account”.

Give it a display name and set the role to viewer:

Now in the newly created service account, click “Add service token”, then on the popup dialog, click “Generate Token”.

A token with a “glsa_” prefix is displayed, make a copy of this as it will not be available to view after closing this dialog box:

This will be used as authentication when generating our dashboard images…

Generating Images

With the renderer installed, and authentication available, images can now be generated.

Images are generated by requesting a URL from the service with the dashboard within the variables, the syntax is:

http://{grafana-image-renderer}:8081/render?encoding=png&url=http://{grafana-dashboard}?{options}

Where:
Grafana-image-renderer – The IP of the machine the renderer is running on.
Grafana-dashboard – The URL of the dashboard you’d like to capture.
Options – Options to manipulate the output/how the dashboard is displayed.

However, the authentication header is needed to successfully access the dashboard. For this reason its easier to use a curl command to set the headers and grab the image

For example, if the renderer is running on the same machine as the main Grafana, a dashboard can be generated by executing:

curl -H "X-Auth-Token: {glsa_token}" "http://localhost:8081/render?encoding=png&url=http://localhost:3000/d/{id}/{dashboad}?kiosk&width=800&height=600" -o image.png

The options used here is kiosk; to remove the menus from the output, and height and width to match the intended displays.

Automation:

One minute updates are fine for this use case, so the easiest way automate this is via cron. Using crontab -e, add the following line:

/usr/bin/curl -H "X-Auth-Token: {glsa_token}" "http://localhost:8081/render?encoding=png&url=http://localhost:3000/d/{id}/{dashboard}?kiosk&width=800&height=600" -o /var/www/html/dash1.png

This line generates the dashboard every minute and stores it on the default web server directory, allowing it to be viewed on the network via an installed web server.

But this will only create a static image, to allow the image to update on a display, a simple webpage can be created and stored on the webserver for easier access and reference.

Create a new webpage:

nano /var/www/html/dashboard.html

And paste the following:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Grafana Dashboard</title>
  <style>
    body {
      margin: 0;
      background-color: black;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }
    img {
      max-width: 100%;
      max-height: 100%;
      object-fit: contain;
    }
  </style>
</head>
<body>
  <img id="liveImage" src="dash1.png" alt="Live Image">
  <script>
    const img = document.getElementById("liveImage");
    setInterval(() => {
      const timestamp = new Date().getTime(); // avoid browser cache
      img.src = "dash1.png?t=" + timestamp;
    }, 60000); // 60 seconds
  </script>
</body>
</html>

When visited, this webpage will update the same image (dash1.png) every 60 seconds to ensure the browser updates with the latest available image instead of caching.

Displays

Viewing this on the display is as simple as visiting the above webpage. As the html is served from the Grafana server and where the auto refresh is taken care of, as long as the display is able to load the page, all is taken care of at the Grafana server.

Summary

Since all that is required from a display is being able to load and render a static image, this allows the oldest and slowest machines to be able to display a Grafana dashboard, negating the need to keep the displays up to date with Grafana’s evolving browser support.

I wish I learnt of this sooner, as it gives more standardised control of the layout of a dashboard. The HyperPixel displays are quite high resolution for their size, and loading a dashboard natively often lead to frustrations with the layout of panels not matching that displyed on a laptop/desktop screen. Presumably due to scaling. Using the image render allows you to test and fix the layout before deploying to a screen, and with a higher panel density too.

Another benefit is that most processing is taken away from the Pi 3A’s, as it is now only responsible for displaying a simple webpage with a single image, rather than loading the full Grafana UI. The Pis have been running on this new method for over a month without any lockups, which was previously seen on a weekly basis.

]]>
Migrate Zabbix/Grafana to new server https://james-batchelor.com/index.php/2025/11/09/migrate-zabbix-grafana-to-new-server/ Sun, 09 Nov 2025 15:38:07 +0000 https://james-batchelor.com/?p=1067 Continue reading "Migrate Zabbix/Grafana to new server"]]> I’m in the process of migrating hypervisors and the time has come to move my Zabbix instance that monitors my network, and Grafana that I use for dashboard displays.

Instead of a backup and restore of the VM, it seems the right time to migrate Zabbix and Grafana from an aging RHEL 8 instance to a new VM running a fresh copy of Debian 13. At the same time upgrading the applications to their latest versions…

Zabbix

New Server: Preparation

Begin by installing a LAMP stack on the new server:

apt install mariadb-server apache2 php php-mysql php-bcmath php-mbstring php-xml php-ldap php-json php-gd php-zip curl gnupg lsb-release

Then setup the basic configuration of MariaDB:

mysql -u root

ALTER USER 'root'@'localhost' IDENTIFIED BY 'password';
DELETE FROM mysql.user WHERE User='';
DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
FLUSH PRIVILEGES;

To give it somewhere to migrate to, install a fresh, blank copy of Zabbix on the new server. The latest version can be used and is available on the Zabbix website, as the application will detect and auto upgrade your exisiting data (So long as a direct upgrade path between versions is supported).

As an example with version 7.4, first add the Zabbix repositories:

wget https://repo.zabbix.com/zabbix/7.4/release/debian/pool/main/z/zabbix-release/zabbix-release_latest_7.4+debian13_all.deb
dpkg -i zabbix-release_latest_7.4+debian13_all.deb
apt update

And install Zabbix:

apt install zabbix-server-mysql zabbix-frontend-php zabbix-apache-conf zabbix-sql-scripts zabbix-agent2

If following the install intructions from the Zabbix website, stop before starting the service. Instead, we’ll first pull and import the database from the old server…

Old Server: Export database

On the old/source server, temporarily stop Zabbix server:

systemctl stop zabbix-server

Now dump the database to a file. As this old server only ran Zabbix, we’ll dump the whole MySQL instance to preserve users and settings which will make migration easier.

If yours is more complex, dump just the Zabbix database and recreate the users manually on the new server.

Export the whole MySQL instance:

mysqldump -u root -p --all-databases > mysql.sql

When it completes, restart Zabbix on the old server, just incase you’d need another go at it later.

systemctl start zabbix-server

New Server: Import

Back on the new server, pull the exported database locally:

scp root@{old-server-ip}:/root/mysql.sql .

Also grab the configuration files of the old server, it’ll come in handy later:

scp -r root@{old-server-ip}:/etc/zabbix/* .

Import the copied database:

mysql -u root -p < mysql.sql

If importing an “–all-databases” export, the newly added users need to be commited before they can be used.

Note that after executing the below, the root credentials will be overwritten by those of the old server:

mysql -u root -p
FLUSH PRIVILEGES;

Next is the configuration file. I would recommend comparing the old and new /etc/zabbix/zabbix_server.conf files, and updating the new .conf to match the old rather than just copying the file.

nano /etc/zabbix/zabbix_server.conf

Now, its time to start Zabbix:

systemctl enable --now zabbix-server

Checking for any errors can be made by monitoring the log file:

tail -n 100 /var/log/zabbix/zabbix_server.log

Soon after starting the service you’ll notice a database upgrade completing in the file, this is Zabbix automatically upgrading your old data to the newly installed version, neat.

Visit the Zabbix web interface on the new server:

http://{new-server-ip}/zabbix/

You’ll be greeted with the setup wizard, follow the steps on screen to complete.

Don’t worry, Zabbix is running and logging now data to your existing Zabbix hosts, this wizard simply creates the /etc/zabbix/web/zabbix.conf.php file.

Once the wizard is completed, you’ll return to a familiar yet updated interface via the new server.

Zabbix Proxy

If the migration involves a major version change and you’re using proxies, the dashboard will quickly flag a problem with the proxy version being incompatible.

On the proxy, you’ll need to update the application by setting the associated version in the repositories and installing the updated version.

Using a Rocky 8 install as an example, add the updated repo:

rpm -Uvh https://repo.zabbix.com/zabbix/7.4/release/rocky/8/noarch/zabbix-release-latest-7.4.el8.noarch.rpm
dnf clean all

Then install the updated version:

Install
dnf install zabbix-proxy-mysql

Restart the service, and it will be up to date:

systemctl restart zabbix-proxy

Grafana

Migrating and updating Grafana is much the same process as Zabbix, if a little easier.

Install Grafana, on Debian 13 it is available from the main repository:

apt install grafana

Pull the required files direct from the old server to the new:

scp root@{old-server-ip}:/etc/grafana/grafana.ini .
scp root@{old-server-ip}:/var/lib/grafana/grafana.db .

Overwrite the installed files with the ones pulled from the old server, and set the correct permissions:

cp grafana.ini /etc/grafana/grafana.ini
chown root:grafana /etc/grafana/grafana.ini
cp grafana.db /var/lib/grafana/grafana.db
chown grafana:grafana /var/lib/grafana/grafana.db

Before starting, install any plugins used on the old server, for example:

grafana-cli plugins install alexanderzobnin-zabbix-app
grafana-cli plugins install grafana-clock-panel

Now Grafana can be started:

systemctl enable --now grafana-server

And visit the web interface:

http://{new-server-ip}:3000/

Where you can pick up at where you left on the old server.

]]>
10 years of website logs, and the resulting nostalgia trip https://james-batchelor.com/index.php/2025/09/28/10-years-of-website-logs-and-the-resulting-nostalgia-trip/ Sun, 28 Sep 2025 09:31:15 +0000 https://james-batchelor.com/?p=1051 Continue reading "10 years of website logs, and the resulting nostalgia trip"]]> This week I noticed a little milestone; my current iteration of website logs had clicked over to 10 years’ worth of accumulated statistics.

While I’ve been self-hosting websites since 2002, it wasn’t until 2015 that I started making a conscious effort to backup and retain web logs during server reinstalls and migrations.

To create statistics from these logs, I’ve been using Analog since around 2004. A simple parser that dates back to the early days of the internet and remains wholly unchanged for many decades.

This milestone prompted a nostalgia dive into the archives to see how far back I can go, uncovering Analog output from 2004, and an historic look on how the internet has changed over the last 20 years…

History

Since getting online in the late 90’s, I had always found the concept of networks and being able to access servers and web content from anywhere in the world absolutely fascinating.

I dabbled in html in the early years, creating my stake on the internet using Yahoo GeoCities. So, when given the opportunity to build a website for my friend’s music band, I wanted to create something bespoke; something that included social interaction, but being fully integrated to the website and importantly, without adverts.

Back then, to do this for free would involve creating a guestbook on a 3rd party site such as Lycos, among the other web tools that were rife at the time. Hosting dynamic content was relatively involved and not inexpensive, so providers such as Lycos would offer to do this for you, and support it with adverts. I’ve touched on this a while back following the end of Lycos. LINK

My solution, host the web server myself! The rational was that using my own server I could serve dynamic content and fully integrate it into the website. The catalyst was recently getting an “always on” internet connection in the form of cable internet in 2001, and that my first PC was made obsolete by the introduction of my second PC.

In 2002, using my first PC, Windows NT 4.0 Server, a dynamic DNS service, and purchasing a Netgear RT311 router, I started self-hosting.

Logging

The thrill of self-hosting came from the IIS logs, constantly reviewing them to see connections from around the world requesting files from the box in the corner of my room.

Analog, the web log parser came along as an evolution to this, allow consolidation of the logs into a single web page for easy reference and wonder. The more the logs grew, the more the statistics grew and the trends started to develop and reinforce themselves.

Analog has been around since 1995 and is largely unchanged, but still receives periodic maintenance updates from the C:Amie Edition.

10 Year Trend

Starting with the 10 year view, these are my points of interest for the modern internet, from this website’s perspective…

Traffic exploded during lockdown

Page requests and visits really took off in 2020, seeing over a five-fold increase in traffic from June 2020. Its unsure if it was a popular post, or the Google algorithm looked favourably on my site, but people sure had a lot of spare time around this period.

Self-hosting shielded bot access

I’ve recently moved away from hosting directly from home (more in a future post) and since moving presence to a commercial VPS service I’ve seen an uptick in visits. This should be good news, but a deeper look at the logs and the increased traffic is from malicious bots, targeting the common WordPress vulnerabilities. These bots must target IP ranges of established hosting providers.

Domains mean nothing anymore

Back when I started viewing Analog reports, the domain report was my way of gauging where in the world a visitor is from. Since then, the boundaries of domains have blurred drastically, and more used for vanity rather that identification. For example, have I really had 13722 visits from .vn (Vietnam)?

Cloudflare ruins logs

While hosting from home, I used Cloudflare as a proxy for a thin veil of security against exposing my IP. This had a detriment to the logs in the form of caching, where Cloudflare would serve a page rather than my server, and so not getting the log of the transaction. This caching also did register as a noticeable drop in traffic.

Bots, bots everywhere

Back in 2004, I only need worry about web crawlers scraping and indexing the site. Today, anyone can use a cheap VPS or commercial VPN service to anonymously attack and brute force a site it has become an epidemic. Now, my highest failure reports come from other hosting services.

Operating Systems

Finally, did someone really try to visit this site on a RISC OS system or even an Amiga?

20 Year Snapshot

Turning the clock back 20 years to my hosting infancy, I found an old report buried in a website backup, again, parsed by Analog.

Granted, this is a snapshot of from a much-reduced sample period, but in honesty, believe the real human visitor count is on par to what this website is now. Albeit catering to very localised content.

Hourly trends were what you expect.

This may be highly skewed by the localised content of the site at the time, but what struck me was the predictability of when users were visiting the site. With a sharp drop during the early hours and peaking in the evening. An indication that these were real visitors.

File requests were easier to understand.

A benefit of creating the website yourself. With the exception of forums, off the shelf applications such as WordPress were yet to materialise. Therefore, the file request reports were easier to decipher and to identify popular sections.

Data transfer was at a premium.

Self-hosting relied on using your home connection to serve pages to the world, and given that most home internet connections are asynchronous, you had to make the website as efficient as possible.

During this snapshot, while I had a modest 600kbps downstream bandwidth, the upstream utilised by visitors was just 128kbps.

Operating Systems

More nostalgia than a trend, but this complete list of OS visitors takes you back.

Summary

While a delightful look back, both at the web logs and the website files the discovery was bundled with. It makes you want to go back to those simpler times.

]]>
SIP Radio https://james-batchelor.com/index.php/2025/09/17/sip-radio/ Wed, 17 Sep 2025 19:00:00 +0000 https://james-batchelor.com/?p=1044 Continue reading "SIP Radio"]]> In a previous post, I hinted at the possibility of replacing a “smart” speaker with readily placed VoIP phones as a way to play radio around the house.

This would kind of make sense, phones use the RTP protocol for audio is designed for real-time communication and so, naturally sync with each other in a local network.

As a proof of concept, I wanted to create a service that allowed me to “dial-in” to a radio stream on demand…

Initial thoughts was to just to pipe a continuous radio stream to an extension. However, in addition to the waste of bandwidth, any network disruptions would essentially kill the stream without recovery. Therefore, a play on-demand service would help keep the stream fresh whilst saving bandwidth at idle.

My preferred radio for testing is Kerrang radio, I get the URL’s for radio feed via this site and downloading the playlist .pls file, then opening the file in a text editor to extract the actual stream URL.

Baresip Setup

Similar to the earlier project in piping audio from a Raspberry Pi to SIP, a minimal install of Baresip will be used to handle the SIP element and added as a system service in a mostly similar way.

To give the script some context on when to play on demand, we need a log of baresip’s output.

In the service configuration file, under [service] change the following line:

ExecStart=/usr/bin/baresip

to:

ExecStart=/bin/bash -c "/usr/bin/baresip > /path/to/sipaudio.log 2>&1"

This will now run the application and send all output to a sipaudio.log file for processing by the script.

Script

The script will read the log file for any newly established calls and add them to a counter to establish how many calls are active, while the call count is greater than zero, trigger the radio stream.

Similarly, call terminations are also registered and affect the active_calls variable.

The goal is to ensure the stream is only triggered when the first active call is dectected, and only stop the stream when the last remaining call is cleared down.

For example, if Phone A calls in, the stream is triggered and starts playing. Then, phone B also calls in and hears the established stream. If Phone A was to hangup, we’ll need to continue the stream for phone B (i.e not latching to the phone that triggered the stream), but if phone B also hangs up, the stream is stopped as there’s nothing there to listen.

Create the script file and add the following:

#!/bin/bash

# Path to Baresip log file
LOG_FILE="/path/to/sipaudio.log"
STREAM_URL="http://edge-bauerall-01-gos2.sharp-stream.com/kerrang.mp3?aw_0_1st.skey=1736072895"

# Track active calls
active_calls=0
mpv_pid=""

start_stream() {
    if [[ -z $mpv_pid ]]; then
        echo "Starting stream..."
        mpv "$STREAM_URL" &
        mpv_pid=$!
    else
        echo "Stream is already running."
    fi
}

stop_stream() {
    if [[ -n $mpv_pid ]]; then
        echo "Stopping stream..."
        kill $mpv_pid
        wait $mpv_pid 2>/dev/null
        mpv_pid=""
    else
        echo "Stream is not running."
    fi
}

monitor_calls() {
    echo "Monitoring Baresip log for call events..."
    tail -Fn0 "$LOG_FILE" | while read -r line; do
        if [[ "$line" == *"Call established"* ]]; then
            ((active_calls++))
            echo "Call incoming. Active calls: $active_calls"
            if [[ $active_calls -eq 1 ]]; then
                start_stream
            fi
        elif [[ "$line" == *"session closed"* ]]; then
            ((active_calls--))
            echo "Call ended. Active calls: $active_calls"
            if [[ $active_calls -le 0 ]]; then
                active_calls=0
                stop_stream
            fi
        fi
    done
}

# Start monitoring calls
monitor_calls

Make the file executatble with:

chmod +x /path/to/filename.sh

Service

This can be ran via the terminal/SSH, but for ease of use and reboot survival, lets create a service for the script.

Create and edit a service file:

sudo nano /etc/systemd/system/sip.radio.service

Add the following to the new service file:

[Unit]
Description=Kerrang Radio Stream
After=sound.target network.target

[Service]
ExecStart=/path/to/filename.sh
Restart=always
RestartSec=10
User=pi
WorkingDirectory=/home/pi
StandardOutput=journal
StandardError=journal
Environment=HOME=/home/pi
Environment=XDG_RUNTIME_DIR=/run/user/1000

[Install]
WantedBy=multi-user.target

When saved, reload services:

sudo systemctl daemon-reload

Start the service and enable it to start at boot:

sudo systemctl enable --now sipradio

Now a test call can be made to the baresip extension, and hopefully the radio will be though in a second or two.

Summary

Since originally starting this in March, the script and SIP endpoint has been idle for a few months, but seeing if it still works while writing this, the stream fired right up on first asking.

I would like to significantly reduce my “smart” speaker density, as they are almost exclusivley music players at this point due to the frustration in using them for anything else (even playing music is a challenge at times), but are always listening in.

To put this theory into production will require both opus capable phone hardware and decent wired/bluetooth speakers with connectivity inbetween.

I wonder if a Pi Zero W2 could come to a cheap option rescue?

]]>
Asus X205TA – Lengthen Laptop Life with Linux https://james-batchelor.com/index.php/2025/07/19/asus-x205ta-lengthen-laptop-life-with-linux/ Sat, 19 Jul 2025 14:23:26 +0000 https://james-batchelor.com/?p=1039 Continue reading "Asus X205TA – Lengthen Laptop Life with Linux"]]> It’s strange to think that I’ve had this laptop around for nearly 10 years. Putting those years in context of my technical knowledge, it seems a lifetime ago.

I owe a lot to this little, £130, underpowered (even at the time) machine. Sat in my car refining and sending off my CV for numerous jobs whilst waiting for my hateful call centre job to begin helped me break into a technical role.

The end of Windows 10 support in October 2025 could be considered a full stop on this laptop’s usefulness, however Windows claimed this as a victim long ago. The 32GB eMMC storage was entirely consumed by just the operating system and its pending updates, which relegated this system to version 18.09.

One point of the X205TA that really impressed was the battery life, with an OS estimated 10 hours at full charge it was something 2016 me had never seen before.

The thought occurred that I’d like a lounge terminal, something with a proper keyboard and screen that I could use to SSH into other machines, with a web browser for reference.

Could I breathe new life into the X205TA with the introduction of a Linux Desktop?

Specifications

All that is required is a terminal and a web browser.

This could be achieved with a simple window manager but due to the laptop’s svelte specs (Intel Atom Z3735F, 2GB RAM, 32GB eMMC storage), I don’t think a barebones setup is going to give much of a performance boost.

Debian is my preference, naturally the system will be built on this. Luckily, and something that needs consideration these days, is this is a 32-bit processor.

Secure boot, enabled by default, will need to be disabled in BIOS in order to install a Linux system.

Desktop Environments

Debian’s wiki has a page for the X205TA , but the referenced kernel versions suggest this may not be the most up to date.

Attempting a fully fledged environment in the first instance…

KDE Plasma

For this I opted for a minimal install of Debian 12, followed by a minimal install of KDE Plasma on first login:

apt install kde-plasma-desktop plasma-nm

This give the KDE desktop, with a couple of issues. Firstly and immediately obvious, despite WiFi working for the net-install ISO and on first boot, post KDE install the WiFi would not connect to my network.

This was resolved by commenting out ALL of the /etc/network/interfaces file, then, through the GUI, forcing the wireless to connect over 2.4Ghz to my SSID that is active of both 2.4 and 5Ghz.

Following this, audio was not detected, not a dealbreaker but something worth noting.

The touchpad controls (tap to click) though not enabled, was easily configured. However, its was quite sluggish, with RAM at 1.4GB at idle, and a browser (Firefox or Chromium) exacerbated this.

Next was a renowned lightweight desktop…

LXDE

Similar to KDE, install via a net-install ISO went seamlessly, but on first boot to the desktop no signs of WiFi or networking was to be seen.

This could of prompted a troubleshooting session, but the lack of touchpad customisation was to be a dealbreaker.

On to the next…

XFCE

Like LXDE, this has the same goal of being a lightweight desktop, but seems to be under more active and continuous development.

Installation was via the same net-install ISO, but on first boot, everything just worked!

With all installation, Debian advised that propriety firmware was recommended prior to installation, yet I’m not seeing any detriment to skipping this step.

RAM usage at idle sits at ~800MB, yet I feel offers the same customisation offered in KDE at 1.4GB, especially with tap to click on the trackpad.

With this level of simplicity of install, XFCE is the one for me.

Summary

Also offered with XFCE are the same comforts of a modern UI, such battery life status and estimates. It’s amazing that this cheap is still able to offer 9 hours of battery life at 10 years old, this was after a charge cycle and booting the system off-grid.

It’s over optimistic to think that this could be a serious workhorse, now or even back then. But to have a simple terminal machine to hand when lounging on the sofa as a more convenient option to a frustrating mobile experience still gives this little cheap laptop some value.

]]>
MySQL Replication https://james-batchelor.com/index.php/2025/06/29/mysql-replication/ Sun, 29 Jun 2025 15:03:01 +0000 https://james-batchelor.com/?p=1018 Continue reading "MySQL Replication"]]> Sure, its not the most original of topics, but it is one I’ve relied upon from time to time. For years, this guide from Digital Ocean was my go to choice.

It wasn’t until my most recent visit for an upcoming project that things looked, well, a little different. It was mostly the same, but subtle differences meant it was no longer compatible with how I’d been familiar with setting it up.

Therefore this quick post is to capture the old method of setting it up for posterity.

The below steps are to replicate all MySQL databases on another server.

Prepare MySQL .conf files

To allow MySQL to replicate, the servers must be arranged in a hirarchy and logging enabled so each system can keep track and update any changes made.

The .conf files can be found in the following locations:

/etc/my.cnf.d/server.cnf

Or on newer distros:

/etc/mysql/mariadb.conf.d/50-server.cnf

Source/Master Server

On the source server (server you want to copy from), add the following lines below and within the [mysqld] section:

bind-address            = {local ip address},{remote ip address}
server-id               = 1
log_bin                 = /var/log/mariadb/mysql-bin.log

binlog-format = mixed
sync_binlog=1

Replica/Slave Server

On the replica server (server that databases are copied to), add the following lines below and within the [mysqld] section:

server-id               = 2
log_bin                 = /var/log/mysql/mysql-bin.log
relay-log               = /var/log/mysql/mysql-relay-bin.log
binlog-format           = mixed
read-only               = 1

On both servers, MySQL requires a restart to pick up the config changes:

systemctl restart mariadb

Create a replication user

A replication needs to be authenticated, rather than using the root user its preferred to create a user that is only able to perform the replication.

On the source server, login to MySQL:

mysql -u root -p

Create the user, and give it replication duties with the following commands:

CREATE USER '{replica_user}'@'{replica_ip}' IDENTIFIED BY '{password}';
GRANT REPLICATION SLAVE ON *.* TO '{replica_user}'@'{replica_ip}';
FLUSH PRIVILEGES;

Copy current databases

Before the replication can begin, we’ll need a copy of the existing data captured within a known point of time. This allows the replica to know where to pick up from where the imported databases left off.

Source Server

If not already, log into mysql :

mysql -u root -p

Lock the databases from being able to be written to:

FLUSH TABLES WITH READ LOCK;

Display the current state of the logfiles while the databases are frozen in time:

SHOW MASTER STATUS;

This will produce something similar to this:

MariaDB [(none)]> SHOW MASTER STATUS;
+------------------+----------+--------------+------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000016 |      342 |              |                  |
+------------------+----------+--------------+------------------+
1 row in set (0.000 sec)

Make a note of the File and Position values, we’ll need this as a reference point for the replica. I’d recommend saving it as a file for future reference.

Now, open up new SSH session to the source server. Its important that the current MySQL session is kept open and in its locked state.

Take a mysqldump of the entire database:

mysqldump -u root -p --all-databases > mysql_replication.sql

When this completes, return to the original session to return the databases to normal:

UNLOCK TABLES;

Transfer SQL file to Replica

Move the generated mysqldump file to the replica server, I usually transfer the master status txt file too for future reference:

scp mysql_replication.sql mysql_master.txt root@{replica_ip}:/root/

Setup Replication

Import the newly transferred file to the replica’s MySQL:

mysql -u root -p < mysql_replication.sql

Then log in to MySQL:

mysql -u root -p

Time to setup the replication, using the replica user setup previously, and the logfile positions captured during the mysqldump. The SHOW MASTER STATUS; output from above is referenced in this command example:

 CHANGE MASTER TO MASTER_HOST='{source_ip}', MASTER_USER='{replica_user}', MASTER_PASSWORD='{password}', MASTER_LOG_FILE='mysql-bin.000016', MASTER_LOG_POS=342;

Reference points now in place, start the replication process:

START SLAVE;

You can check it is running with:

SHOW SLAVE STATUS\G;

In the output you’re looking for Slave_IO_Running and Slave_SQL_Running to both be Yes:

With both reporting Yes, the replication is running. All changes made to the source server is automatically pushed to the replica.

Notes

Ports

The two databases communicate over its MySQL port, default is 3306. Ensure that both servers are able to communicate on this port.

Authentication

Immediately after setting up replication, you’ll be able to log in to the replica server as normal. However, if the service/server was to be restarted you may not be able to login as normal.

As this guide replicates everything on the source MySQL, it will also replicate the users and permissions.

Following a replication start, the “old” credentials are still usable But following a service restart, the credentials of the source are the new normal on the replica, and should be used for any future login.

This will also apply to any instances of phpMyAdmin.

]]>
SIP device as Pi Audio Output https://james-batchelor.com/index.php/2025/02/16/sip-device-as-pi-audio-output/ Sun, 16 Feb 2025 16:55:45 +0000 https://james-batchelor.com/?p=992 Continue reading "SIP device as Pi Audio Output"]]> In my Development Den (the spare room) I have a Raspberry Pi 4 setup with a monitor for use as a quick reference station when working on nearby devices.

With no speakers connected this can sometimes pose an issue when trying to follw a tutorial video, and when I do need audio, a Bluetooth speaker is never around.

There is a SIP phone next to the Pi on my desk, and so I thought; that has a decent network connected speaker, why not use that?

This is what ensued…

The idea is to have a SIP endpoint running as a service on the Pi in auto-answer mode. This will allow a desk phone to dial the Pi extension and receive the Pi’s audio through the loudspeaker.

The Pi is running stock Raspbian Bullseye with the desktop environment.

SIP Endpoint

Baresip looks a good choice for this project due to its modularity.

As this will be a service running in the background, only the core module of Baresip needs to be installed:

sudo apt install --no-install-recommends baresip-core

When installed, run the program briefly to have it create its default configuration files:

baresip

This will create the default template files in a .baresip directory within the home folder. We’ll need to edit the accounts and config files to get it to a call answering state.

Starting with the accounts file:

nano .baresip/accounts

Add the following line to the bottom of the file:

<sip:{endpoint}@{sip_server}>;auth_pass={sip_secret};answermode=auto

Where:
{endpoint} – SIP extension number
{sip_server} – IP/hostname of the PBX
{sip_secret} – Extension password

Answermode flag has been added to allow calls to this extension to be answered automatically.

After the accounts file, move onto the config:

nano .baresip/config

This file can be left as default, however a few quality-of-life improvements will be made…

Uncomment the following lines:

module                  opus.so
module                  g722.so

These allow the use of the higher quality codecs commonly in use; g722 is the elder and while it offers higher quality from a phone call audio point of view, may fall short for music. Opus is the newer and can be configured for excellent quality overall, but being newer may not be an available choice on older phones.

If Opus is available, the bitrate can be increased via this line further down the config file (higher bitrate, higher audio quality):

opus_bitrate            28000 # 6000-510000

Make sure both you SIP devices and PBX are capable and configured to offer these codecs at the highest priority.

Testing

Good time for a sanity check, for this an audio file or stream playing through VLC, or any source of audio will do.

Run the application in the terminal

baresip

And make a call to its extension, you should see output in the terminal, and hear the audio through the phone.

If it’s successful, a service can be created to have this running in the background on startup…

Create Service

To allow baresip to start at boot, its best to create a service for it and restart it if it ever stops.

Create and edit a service file:

sudo nano /etc/systemd/system/sipaudio.service

Add the following to this file:

[Unit]
Description=SIP endpoint for Pi audio
After=sound.target network.target

[Service]
ExecStart=/usr/bin/baresip
Restart=always
User=pi
WorkingDirectory=/home/{username}
StandardOutput=journal
StandardError=journal
Environment=HOME=/home/{username}
Environment=XDG_RUNTIME_DIR=/run/user/1000

[Install]
WantedBy=multi-user.target

When saved, reload services:

sudo systemctl daemon-reload

Start the service and enable it to start at boot:

sudo systemctl enable --now sipaudio

With some audio playing, try another call to make sure its answering and picking up audio from the desktop environment.

Conclusion

It’s a niche solution for those who have an audio-less Pi and a SIP phone next to it, but the results are plesently convenient for those rare times when audio is needed.

My accompanying desk phone (Yealink T46S) only offers g722 has the higher codec, but still is perfectly fine for speech output and fine (not great) for audio. I’m sure using Opus at the higher bitrate will put it on par with some of the streaming services. Afterall, YouTube uses opus as audio for its videos, as noted by the “stats for nerds” section:

]]>
Pi Weather Display Project https://james-batchelor.com/index.php/2024/12/03/pi-weather-display-project/ Tue, 03 Dec 2024 17:50:00 +0000 https://james-batchelor.com/?p=975 Continue reading "Pi Weather Display Project"]]> This project is so cobbled together I’m almost proud of it, I don’t expect anyone to be able to recreate this exactly but if there are parts of it that help, here you go.

My dog prefers a walk early in the morning as the sun is dawning, so I’d like info on when dawn is along with the current weather forecast, plus very localised info on what the temperature is now and importantly, if it’s raining out.

The go to choice to present this info would be a Raspberry Pi with a small display to allow a quick glance at in the morning.

The info to display on the Pi screen can be sources from a couple of sources;

  • An API call from a weather service (weatherbit.io) for more general sunrise, sunset and current weather.
  • A SDR radio picking up a nearby weather station for more localised temperature and rainfall values.

This is how it comes together…

Concept

The plan is to keep this as simple as possible.

General weather data is to be provided from a weather service via an API call and can parse the required data for display on the screen. weatherbit.io is able to provide this for free up to 50 calls per day.

For local data from a nearby weather station, a SDR is needed to pick up the signals for decoding and parsing. In my case I already have an SDR connected to another Pi located in the attic for amateur radio, so can tap into this to pick up local stations.

To display this for quick reference, a local webpage that runs on Chromium in kiosk seems the easiest way to get the info on the screen. Rather than creating a dynamic webpage to pull the data, have a script create the html that includes a simple refresh to deal with the page changing when the data updates.

Hardware

In its most basic form, the main hardware in use:

However, the Pi 4 is a bit overkill, so I would like it to do double duty as a data store, and house it neatly in a custom case. Additional components would include:

Case

For the most compact packaging a custom 3D printed case would be created.

From previous attempts its designed to be a self-contained box, the design of this incorporated the following features:

  • Screwless / glueless design.
  • Incorporate a RJ45 socket to allow all ports to be located at the back.
  • Main io of Pi facing the rear for easy power connection.
  • Uses ribbon cable to secure / cushion HDD in place and to allow for tolerances.

Download STL files

Weatherbit API

After some trial and error, weatherbit.io seemed the best choice for easy access to weather data for the minimal of personal data, it does require signup for access to the API but is not too intrusive.

The free version for personal use allows up to 50 calls per day, allowing for a weather update every 30 minutes.

The output is quite detailed, and can be used as the only source for a weather display:

{"alerts":[],"count":1,"data":[{"app_temp":17.6,"aqi":65,"city_name":"NA","clouds":0,"country_code":"GB","datetime":"2024-08-31:12","dewpt":12.4,"dhi":110,"dni":864,"elev_angle":45.91,"ghi":723,"gust":12.3,"h_angle":0,"lat":NA,"lon":NA,"ob_time":"2024-08-31 12:48","pod":"d","precip":0,"pres":1008,"rh":72,"slp":1022,"snow":0,"solar_rad":723,"sources":["analysis","radar","satellite"],"state_code":"NA","station":"E0000","sunrise":"05:25","sunset":"19:00","temp":17.6,"timezone":"Europe/London","ts":1725108496,"uv":6,"vis":16,"weather":{"description":"Clear sky","code":800,"icon":"c01d"},"wind_cdir":"NE","wind_cdir_full":"northeast","wind_dir":55,"wind_spd":6.5}]}

Radio

I already have a separate Raspiberry Pi with an SDR attached. This uses rtl_tcp to allow connections to the SDR from software on other machines via the network. This works great to allow the SDR / antenna as high up as possible in my loft, and control it from my main PC at ground level.

Collection of data from a weather station comes from RTL 433, a fantastic tool that decodes data from a variety of devices on the 433mhz.

In order to integrate with my current setup, RTL 433 will connect to the SDR via rtl_tcp occasionally to collect latest data as only one device can use the SDR at a time. This allows my to “hijack” the SDR should I want to scan the radio.

My original scope was to use my personal weather station that only recorded temperature and humidity, but since using RTL 433 I’ve found a more advanced weather station nearby that can record rainfall and wind data, great for my project.

This station transmits its data every 60 seconds. So, to get this data I’ll run a scan via the SDR for 90 seconds every 10 minutes. This allows at least a recent transmit of the station but giving enough down time for me to step in and hijack the SDR when needed.

A SDR stream is 40Mbps, to save this going across the network every 10 minutes the script to collect data will be ran locally. The RTL 433 program can output to a json, and SNMP will be used to serve weather data to the display Pi.

Script

Bringing everything together requires some timing; SDR every ten minutes, API every half hour. Making this as easy as possible, the script is split into different sections and run at different times via cron jobs.

Collect SDR Data

This script is run on the SDR Pi and collects the radio data for 90 seconds, outputting data to a json file. This is set to run every 10 minutes:

Cron:

*/10 * * * * /home/pi/433_sensors/433_run.sh

Script:

#!/bin/bash
# Collect data for 90 seconds

timeout 90 rtl_433 -d rtl_tcp:10.0.1.242:1234 -F json:/home/pi/433_sensors/433_json.txt

The latest data is then offered to the rest of the network via SNMP…

Addition to snmpd.conf:

# Temperature - 433mhz radio
extend-sh temp_radio /etc/snmp/snmp-433mhz-temps.sh

snmp-433mhz-temps.sh

#!/bin/bash

######## Output order #########
#
# my sensor – temperature
# my sensor – humidity
# local ws – temperature
# local ws – humidity
# local ws - wind direction (degrees)
# local ws - wind average (mph)
# local ws - wind max (mph)
# local ws - rain (mm)
#
###############################

## Bresser-3CH - My temperature sensor ##

# JSON data file
JSON_FILE="/home/pi/433_sensors/433_json.txt"

# Grab latest entry for model Bresser-3CH
latest_entry=$(jq -r 'select(.model == "Bresser-3CH") | [.time, .temperature_F, .humidity] | @tsv' "$JSON_FILE" | sort -r | head -n 1)

# Check if the latest_entry is empty
if [ -z "$latest_entry" ]; then
  echo "No data found for model Bresser-3CH"
  exit 1
fi

# Extract time, temp and humidity from Bresser-3CH
latest_time=$(echo "$latest_entry" | awk -F'\t' '{print $1}')
temperature_F=$(echo "$latest_entry" | awk -F'\t' '{print $2}')
humidity=$(echo "$latest_entry" | awk -F'\t' '{print $3}')

# Convert temperature from Fahrenheit to Celsius and format to 1 decimal place
temperature_C=$(echo "scale=1; ($temperature_F - 32) * 5 / 9" | bc)

# Output results for SNMP collection
echo "$temperature_C"
echo "$humidity"

## WHx080 local weather station ##

# Grab latest entry for model Fineoffset-WHx080
latest_fineoffset_entry=$(jq -r 'select(.model == "Fineoffset-WHx080") | [.time, .temperature_C, .humidity, .wind_dir_deg, .wind_avg_km_h, .wind_max_km_h, .rain_mm] | @tsv' "$JSON_FILE" | sort -r | head -n 1)

# Check if the latest_fineoffset_entry is empty
if [ -z "$latest_fineoffset_entry" ]; then
  echo "No data found for model Fineoffset-WHx080"
  exit 1
fi

# Extract data from Fineoffset-WHx080
temperature_C_fineoffset=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $2}')
humidity_fineoffset=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $3}')
wind_dir_deg=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $4}')
wind_avg_km_h=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $5}')
wind_max_km_h=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $6}')
rain_mm=$(echo "$latest_fineoffset_entry" | awk -F'\t' '{print $7}')

# Convert wind speeds from km/h to mph
wind_avg_mph=$(echo "scale=1; $wind_avg_km_h * 0.621371" | bc)
wind_max_mph=$(echo "scale=1; $wind_max_km_h * 0.621371" | bc)

# Output results for SNMP collection
echo "$temperature_C_fineoffset"
echo "$humidity_fineoffset"
echo "$wind_dir_deg"
echo "$wind_avg_mph"
echo "$wind_max_mph"
echo "$rain_mm"

Collect SDR data on Weather Display

This will collect the SDR data presented over the network via SNMP. This also runs every 10 minutes but is offset by a couple of minutes to ensure the most up to date values are collected.

Cron:

2,12,22,32,42,52 * * * * /home/pi/scripts/weather_data/generate_radio.sh

Script:

#!/bin/bash

# Collect weather data from SNMP and output to file.

# File paths
DATA_SNMP_TEMP="/home/pi/scripts/weather_data/data_snmp_temp.txt"
DATA_SNMP_HUMID="/home/pi/scripts/weather_data/data_snmp_humid.txt"
DATA_SNMP_RAIN="/home/pi/scripts/weather_data/data_snmp_rain.txt"
DATA_SNMP_RAIN_PREV="/home/pi/scripts/weather_data/data_snmp_rain_prev.txt"
DATA_SNMP_RAIN_FLAG="/home/pi/scripts/weather_data/data_snmp_rain_flag.txt"
DATA_SNMP_RAIN_COL="/home/pi/scripts/weather_data/data_snmp_rain_col.txt"

# Fetch temperature
SNMP_COMMAND='snmpget -v 2c 10.0.1.242 -c public NET-SNMP-EXTEND-MIB::nsExtendOutLine."temp_radio".1'

# Execute command
SNMP_RESULT=$($SNMP_COMMAND 2>&1)

# Check command did not return a timeout
if echo "$SNMP_RESULT" | grep -qi "timeout"; then
    echo "SNMP temperature request timed out, skipping."
else
    # If no timeout, save the output to file
    echo "$SNMP_RESULT" | awk '{print $NF}' > "$DATA_SNMP_TEMP"
    echo "SNMP temperature data saved"
fi

# Fetch humidity
SNMP_COMMAND='snmpget -v 2c 10.0.1.242 -c public NET-SNMP-EXTEND-MIB::nsExtendOutLine."temp_radio".2'

# Execute command
SNMP_RESULT=$($SNMP_COMMAND 2>&1)
# Check command did not return a timeout
if echo "$SNMP_RESULT" | grep -qi "timeout"; then
    echo "SNMP humidity request timed out, skipping."
else
    # If no timeout, save the output to file
    echo "$SNMP_RESULT" | awk '{print $NF}' > "$DATA_SNMP_HUMID"
    echo "SNMP humidity data saved"
fi

# Prepare files for rain flag
cp "$DATA_SNMP_RAIN" "$DATA_SNMP_RAIN_PREV"

# Fetch rain
SNMP_COMMAND='snmpget -v 2c 10.0.1.242 -c public NET-SNMP-EXTEND-MIB::nsExtendOutLine."temp_radio".8'

# Execute command
SNMP_RESULT=$($SNMP_COMMAND 2>&1)

# Check command did not return a timeout
if echo "$SNMP_RESULT" | grep -qi "timeout"; then
    echo "SNMP rain request timed out, skipping."
else
    # If no timeout, save the output to file
    echo "$SNMP_RESULT" | awk '{print $NF}' > "$DATA_SNMP_RAIN"
    echo "SNMP rain data saved"
fi

# Check if rain counter increased since last result
RAIN_PREV=$(cat "$DATA_SNMP_RAIN_PREV")
RAIN_NOW=$(cat "$DATA_SNMP_RAIN")

if (( $(echo "$RAIN_NOW > $RAIN_PREV" | bc -l) )); then
    # Output for display
    RAIN_STATUS="Yes"
    # Set text colour for display
    RAIN_COLOUR="#49b2fc"
else
    RAIN_STATUS="No"
    RAIN_COLOUR="#ffffff"
fi

echo "Raining: $RAIN_STATUS"
echo "$RAIN_STATUS" > "$DATA_SNMP_RAIN_FLAG"
echo "$RAIN_COLOUR" > "$DATA_SNMP_RAIN_COL"

Collect API Data

This pulls the weather from the API and extracts the bits we need to a file. While the API allows 50 calls a day, I’d found it returning a 403 in the early afternoon when call was set to 30 minutes. Therefore, its set to call every hour.

Cron:

0 * * * * /home/pi/scripts/weather_data/generate_api.sh

Script:

#!/bin/bash
# Pulls latest data from weatherbit api

# File paths
DATA_WEATHER_API="/home/pi/scripts/weather_data/data_api.json"
DATA_API_SUNRISE="/home/pi/scripts/weather_data/data_api_sunrise.txt"
DATA_API_SUNSET="/home/pi/scripts/weather_data/data_api_sunset.txt"
DATA_API_DESC="/home/pi/scripts/weather_data/data_api_desc.txt"
DATA_API_ICON="/home/pi/scripts/weather_data/data_api_icon.txt"

# Fetch Data
WEATHER_DATA=$(curl -X GET --header 'Accept: application/json' 'https://api.weatherbit.io/v2.0/current?lat=51.605817&lon=-3.091024&include=alerts&key=00000000000000000000000000000000')

# Check fetch was successful
if [ $? -ne 0 ]; then
    echo "API Call failed" >&2
    exit 1
else
    # If successful, write this data to file
    echo "$WEATHER_DATA" > "$DATA_WEATHER_API"
    echo "Weather data saved"
fi

# Extract data we need
SUNRISE=$(jq -r '.data[0].sunrise' < "$DATA_WEATHER_API")
SUNSET=$(jq -r '.data[0].sunset' < "$DATA_WEATHER_API")
WEATHER_DESC=$(jq -r '.data[0].weather.description' < "$DATA_WEATHER_API")
WEATHER_ICON=$(jq -r '.data[0].weather.icon' < "$DATA_WEATHER_API")

# API returns UTC time, need to adjust for BST
# Function to check current date is within the BST period
is_bst() {
    # Use system time to check if we’re in BST
    [[ $(date +%Z) == "BST" ]]
}

# Function to add 1 hour to the time
adjust_for_bst() {
    local time="$1"
    date -d "$time today + 1 hour" +%H:%M
}

# Check if it's currently BST
if is_bst; then
    # Add an hour to sunrise and sunset
    SUNRISE=$(adjust_for_bst "$SUNRISE")
    SUNSET=$(adjust_for_bst "$SUNSET")
fi

# Save extracted API data to separate files
echo "$SUNRISE" > "$DATA_API_SUNRISE"
echo "$SUNSET" > "$DATA_API_SUNSET"
echo "$WEATHER_DESC" > "$DATA_API_DESC"
echo "$WEATHER_ICON" > "$DATA_API_ICON"

Generate HTML

This brings together all collected values and inserts them into a html temple, the html file is replaced each run so also has a refresh mechanism to ensure it updates on the screen.

Like the SDR generation, this is ran every 10 minutes and offset to ensure most recently data is available to display.

Cron:

3,13,23,33,43,53 * * * * /home/pi/scripts/weather_data/generate_html.sh

Script:

#!/bin/bash
# Combines data from radio and api and creates html file for display

# File paths
DATA_SNMP_TEMP="/home/pi/scripts/weather_data/data_snmp_temp.txt"
DATA_SNMP_HUMID="/home/pi/scripts/weather_data/data_snmp_humid.txt"
#DATA_SNMP_RAIN="/home/pi/scripts/weather_data/data_snmp_rain.txt"
#DATA_SNMP_RAIN_PREV="/home/pi/scripts/weather_data/data_snmp_rain_prev.txt"
DATA_SNMP_RAIN_FLAG="/home/pi/scripts/weather_data/data_snmp_rain_flag.txt"
DATA_SNMP_RAIN_COL="/home/pi/scripts/weather_data/data_snmp_rain_col.txt"
DATA_WEATHER_API="/home/pi/scripts/weather_data/data_api.json"
DATA_API_SUNRISE="/home/pi/scripts/weather_data/data_api_sunrise.txt"
DATA_API_SUNSET="/home/pi/scripts/weather_data/data_api_sunset.txt"
DATA_API_DESC="/home/pi/scripts/weather_data/data_api_desc.txt"
DATA_API_ICON="/home/pi/scripts/weather_data/data_api_icon.txt"
DATA_HTML="/var/www/html/weather.html"

# Load latest data
TEMP=$(cat "$DATA_SNMP_TEMP")
HUMID=$(cat "$DATA_SNMP_HUMID")
RAIN=$(cat "$DATA_SNMP_RAIN_FLAG")
RAIN_COL=$(cat "$DATA_SNMP_RAIN_COL")
SUNRISE=$(cat "$DATA_API_SUNRISE")
SUNSET=$(cat "$DATA_API_SUNSET")
DESC=$(cat "$DATA_API_DESC")
ICON=$(cat "$DATA_API_ICON")

# Generate HTML
echo "Generating HTML content..."
cat << EOF > "$DATA_HTML"
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="refresh" content="60"> <!-- Refresh every 60 seconds -->
    <title>Weather Display</title>
    <style>
        body { font-family: Arial, sans-serif; font-weight: bold; color: white;}
        .container { width: 100%; margin: 0 auto; text-align: center; }
    </style>
</head>
<body style="background-color:black;">
    <div class="container">
       <table style="width:100%"><thead>
         <tr>
           <th colspan="2" rowspan="2"><img src="images/icons/$ICON.png" width="120" height="120"></th>
           <th colspan="2" style="text-align: left; font-size: 36px;">$DESC</th>
         </tr>
         <tr>
           <th style="text-align: right;"><img src="images/temperature.png" width="64" height="64"></th>
           <th style="font-size: 56px;">${TEMP}C</th>
         </tr></thead>
       <tbody>
         <tr>
           <td><img src="images/sunrise.png" width="64" height="64"></td>
           <td style="font-size: 48px;">$SUNRISE</td>
           <td style="text-align: right;"><img src="images/humidity.png" width="64" height="64"></td>
       <td style="font-size: 56px;">$HUMID%</td>
         </tr>
         <tr>
           <td><img src="images/sunset.png" width="64" height="64"></td>
           <td style="font-size: 48px;">$SUNSET</td>
           <td style="text-align: right;"><img src="images/rain.png" width="64" height="64"></td>
       <td style="font-size: 56px; color:${RAIN_COL};">$RAIN</td>
         </tr>
       </tbody>
       </table>
    </div>
</body>
</html>
EOF

Screen Display

Finally, time to get the data on the screen. For ease of use the webpage should start with the system so there is no configuring or manual execution after each reboot.

A service would be best, create the file:

/etc/systemd/system/pidisplay.service

Add this to the file:

[Unit]
Description=Launches Chromium in kiosk mode for my display.

[Service]
ExecStart=/home/pi/scripts/display_launch.sh
WorkingDirectory=/home/pi/scripts/
User=pi
Group=pi
Restart=always
Environment=DISPLAY=:0
Environment=XAUTHORITY=/home/pi/.Xauthority

[Install]
WantedBy=multi-user.target

Reload services and enable / start the service:

sudo systemctl daemon-reload
sudo systemctl enable --now pidisplay.service

This launches the display_launch.sh script which in essence loads a Chromium tab in kiosk mode (fullscreen) and with the minimum amount of disruptions:

#!/bin/bash


# Wait for X to start up properly
sleep 10
# Launch Chromium in kiosk mode with the HTML page
export DISPLAY=:0
chromium-browser --kiosk --disable-restore-session-state --no-sandbox file:///home/pi/scripts/display_output.html

Conclusion

All this in place and you have yourself a weather display. As mentioned at the top it is a bit of a cobbled together setup and way overkill. But the result is a quick reference display for when its still dark out.

Update

After a few months of operation it is working well, and with a relief that the daylight savings adjustment to the sunrise / sunset times worked without issue.

One issue that cropped up after a month’s deployment saw the radio data not being updated anymore.

The cause was the rtl433 output file becoming too large, as the JQ query scans the whole file to find the latest record, the SNMP get request was timing out before JQ had a change to find the most recent entry.

This is something a cron job to split and archive the output can solve.

Cron:

Run the scricpt every Monday at 00:05.

5 0 * * 1 /home/pi/433_sensors/433_json_archive.sh

Script:

#!/bin/bash

# Script to cleanup database, leave newest 1M at original filename.
# Note: If lines are not split cleanly, it'll cause snmp to fail.

# File paths
ORIGINAL_LOG="/home/pi/433_sensors/433_json.txt"
RECENT_LOG="/home/pi/433_sensors/temp_recent.txt"
LOG_ARCHIVE_DIR="/home/pi/433_sensors/log_archive"

# Timestamp in format YYYYMMDD-HHMMSS)
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")

# Give a filename for old records (will gzip later)
OLDER_LOG="/home/pi/433_sensors/433_json_$TIMESTAMP.log"

# Split last 1000 lines to new file
tail -n 1000 $ORIGINAL_LOG > $RECENT_LOG

# Split previous lines upto 1000 to another file
head -n -1000 $ORIGINAL_LOG > $OLDER_LOG

# Put last 1000 lines back to original filename
mv $RECENT_LOG $ORIGINAL_LOG

# Compress and move old log to archive dir.
gzip $OLDER_LOG
mv "${OLDER_LOG}.gz" $LOG_ARCHIVE_DIR

Why am I keeping the old logs? Well I may one day like to expand what I can monitor via this method, namely tire pressures on my car. However I need to identify and note the frequency of thesse transmissions before I can add to monitoring. This may be a future project.

]]>
Zen Digital Voice – Connecting to SIP https://james-batchelor.com/index.php/2024/11/01/zen-digital-voice-connecting-to-sip/ Fri, 01 Nov 2024 12:15:17 +0000 https://james-batchelor.com/?p=965 Continue reading "Zen Digital Voice – Connecting to SIP"]]> If like me you still have a requirement for a landline, or at least a landline number. Zen Internet offers a “digital voice” package as an accompaniment to its broadband service.

Traditionally this service is provided via its supplied Fritzbox router, utilising its FXS port to supply telephony to analogue devices.

As the service is SIP based, it is possible to connect direct via SIP phones or Asterisk which is ideal should the Fritzbox not be suitable for your requirements. While this is permitted by Zen, it is unsupported.

Here are a couple of examples of getting connected to Zen DV without the use of the Fritzbox.

A Pre-Setup Warning

This process involves exposing a SIP server / device to the internet, this protocol is constantly targeted by hackers and fraudsters due to its reward for exposing a vulnerable system.

For example, hackers can exploit a system to generate high volumes of calls on your account for either their own benefit (calling a premium rate number they setup) or for the sheer fun of it, while fraudsters could use your “line” to make robocalls to other phone numbers.

Therefore, it is imperative to limit the service’s exposure to only the IP’s listed below, if your router is unable to open ports to specified addresses, consider upgrading or not proceeding.

Network

From a network point of view, VoIP is split into two sections; Signalling (SIP) that deals with the actions of a call, such as “start ringing” and “hangup now”. The Media (RTP) solely deals with the audio of the call and its setup is defined via the SIP signalling.

Uniquely For the DV service, Zen employs a separate server each for inbound and outbound calls:

SIP Inbound:
voip.zen.co.uk – 212.23.7.228

SIP Outbound:
voip2.zen.co.uk – 212.23.7.235

Media uses a common range:

RTP Media:

62.3.88.0/28
62.3.88.16/28

Ports used by the service are the standard:

SIP: 5060
RTP: 10000 – 20000

Setup

As we are trying to mimic the Fritzbox, which is the first point of contact from the internet, the above ports need to be passed onto your SIP device. As this could pass any internet traffic on these ports to the device it’s a good idea to limit these open ports to only the IPs above too.

This will differ depending on the router used, as an example this is how to set it up on a Draytek router:

Allowed Client List / Whitelist

In the router’s web interface, navigate to Objects Setting >> IP Object, add the IPs:

These need to be grouped together, in Objects Setting >> IP Groups, add these IPs into the group:

Open Ports

Now there is a list of allowed IPs, the ports can be opened, in NAT >> Open Ports, edit an index, setting:

WAN Interface: Port connected to Zen internet
Source IP: The group created above
Private IP: Local IP of SIP device
Ports: Open UDP ports 5060 and a range 10000-20000

Now VoIP traffic is forwarding to the device, it can be configured…

SIP

To authenticate with the service the phone number {number} (full number including area code, in national format, i.e starting 01 or 02) will be used as the username, and you’ll need the password {password}(secret). The password is available via the old Zen portal for legacy customer (https://portal.zen.co.uk/) or by contacting Zen and asking for “your VoIP password”.

Asterisk

As there are two servers in use for SIP signalling, two trunks are required on the PBX:

Trunk 1 – Inbound; This just needs to accept calls from Zen’s server so is pretty basic:

type=peer
trustrpid=yes
nat=yes
insecure=invite
host=voip.zen.co.uk
disallow=all
allow=alaw

Trunk 2 – Outbound; This contains authentication to login (register) to the service and make calls:

type=peer
secret={password}
host=voip2.zen.co.uk
fromuser={number}
disallow=all
authuser={number}
allow=alaw
nat=yes
insecure=invite
fromdomain=voip2.zen.co.uk
defaultuser={number}

The outbound trunk also requires the registration details, this allows Zen’s servers to recognise that your service is online and ready for calls:

{number}:{password}@voip2.zen.co.uk/{number}

If you’d like to monitor the connection, you’d see this in sngrep:

Note even when setup correctly, the initial registration attempt will fail as Asterisk attempts to register without authentication, upon the 401 error it will then attempt registration with authentication details and receive the 200 OK response and so registered to the service.

SIP Phone

To connect directly to SIP device, it’s mainly a case of just getting the phone to register, from there the Zen server will know it is logged in for calls and take it from there. The generic details needed are below, apply this best to the terminology used by the device:

Username / Register Name: {number}
Password / Secret: {password}

Server / Registrar / Host: voip2.zen.co.uk
Transport / Protocol: UDP
Port: 5060

For example, this is a setup of a Yealink phone:

If setup correctly, the registration status should state “Registered”.

This is for Yealink devices but will likely be applicable for other vendors, you will need to change the following settings in Features >> General Information:

Accept SIP Trust Server Only: This needs to be disabled as inbound calls come from a different server than the device is registered to.

Allow IP Call: Important to disable especially if there is no IP whitelisting on the router, this stops random or “ghost” calls generated by hackers trying to find a response from internet facing phones. This will not be an issue if you are able to restrict open WAN ports to allowed IP’s, but if not, this helps.

Conclusion

Setup, you should be able to use your Zen DV number with your VoIP / SIP devices.

For a single phone this is a simple solution, but for multiple phones around the home your either limited to only cordless phones from the likes of Yealink or Gigaset (where the base unit is the controller and SIP registration device).

But for a combination of landline and cordless, the VoIP revolution means the end of an era of running an extension cable to the next phone, you’ll need a local PBX of some sort to handle signalling.

Finale

This is also likely an end of an era for my VoIP based posts, as I have left my telecoms role for something different.

I may return sporadically to VoIP postings if I find a home use for it, or revive a post that didn’t quite get there. But for now, it’s a goodbye to SIP.

]]>
Macbook M1 Pro – 3 External Screens https://james-batchelor.com/index.php/2024/10/30/macbook-m1-pro-3-external-screens/ Wed, 30 Oct 2024 11:42:03 +0000 https://james-batchelor.com/?p=960 Continue reading "Macbook M1 Pro – 3 External Screens"]]> New job, new laptop; this time it’s a Macbook M1 Pro 2021 model.

Switching to a Mac for daily use takes some getting used to, so my hope at least was to preserve my 3x 1080p monitor setup with the laptop in clamshell mode (lid closed). I’d been accustomed to for many years so was hoping this would aid the transition to a whole new Mac ecosystem.

The specifications and online hear-say suggest that the maximum supported outputs are two monitors plus the Mac’s display. But there is a way to get 3 screens running on an M1 Macbook Pro…

For MacBook’s of this age, at least, the secret to getting more monitors is a dock with DisplayLink, a method of driving displays over USB.

This is a great solution to overcome limitations of the hardware, however it comes with some considerations:

  • As the extra displays are delivered over USB, it is limited by the USB bus speed. Luckily for me a 1080p monitor is not (these days) not that much of a bandwidth hog. But for those using 4K or higher it may hit the limits of the USB bus, or its use will impact transfer speed of other devices.
  • The extra displays are managed by software, so the experience may not be as seamless as a dedicated display output. Many times a reboot was the solution to solve the extra displays not … displaying.
  • A constant “your display is monitored” icon is in view on the taskbar, granted this is a minor caveat, but can be distracting as it seems to appear and disappear at random intervals.

Dock

Given the port density of a modern MacBook the natural choice to add three screens was to use a dock, which allows all the extra screens plus a few more connectivity options.

I settled on the Dell UD22, this is a more universal version of the Windows centric Dell dock’s I’ve previously been used to. In addition to being Mac compatible it is also capable of being able to give 96 watts of power delivery to the host machine, allowing a one cable serves all connection to the laptop for a cleaner look.

Connection

Despite the dock having 3 monitor connections (2x DP, 1 HDMI), knowing the limitations I chose to connect the 2 of the monitors via a DP and HDMI port. The third connects via the Thunderbolt port on the rear of the dock.

However it will not work yet…

Software

To enable the third (or More) screens, download DisplayLink software, and allow to run at startup.

A restart later and all 3 screens are running off the same dock.

Caveats

From experience, exploiting the hardware limits are not as seamless as hoped:

  • The laptop if left in clamshell mode 24/7 mostly works fine, but any restarts for me stops all screens from outputting video. For this the laptop needs to be unplugged, opened, closed, then plugged in again.
  • Sometimes, the DL screens will not wake. This requires a reboot and will need to refer to the above point.
  • The DisplayLink screens will not behave the same as the normal outputs. Oddly I notice the DL screens will wake before the others connected to the dock. Resulting in the “LIVE DESKTOP” being offset to the others.

Conclusion

For adding an additional 1080p monitor at least (in my case), the DisplayLink monitor for general web browsing is perfectly useable and I cannot see any degradation in quality or framerate to a point where “this is the USB monitor”, I can’t vouch for multiscreen gaming however as; a) it’s a work laptop, and b) it’s a Mac. 

]]>