Digital signage solution for a school

This time around, I’ll be documenting a simple and cheap project I did for my workplace – a digital signage solution for the school I work at. Our plan was to get a larger LED TV, mount it in the main hall and use it to display information and reminders about upcoming events, activities and so on. Parents often have to wait for their children in this same hall, so they would also have something to check out as they wait. 

Trying to find the best way to do this, I’ve immediately discounted all offline solutions which would involve running photos or video off a USB stick on that TV because I needed a way to allow a limited staff to edit and update the information. Since the TV at my disposal had an HDMI port, a Raspberry Pi seemed like the perfect choice. However, after reviewing a large number of commercial, freemium and free digital signage solutions, none of them turned out to be a clear winner – they were either too complicated to edit or focused on video, a complete overkill for displaying a few lines of text or a couple of photos.

I figured the best way would be to present a fullscreen website in a browser. Again, I tried out a few different ideas, including a digital signage WordPress theme, but it lacked some editing options. Instead, I decided to show an HTML version of an online presentation tool like Microsoft’s PowerPoint online or Google Docs Slides. Office Online gave me loads of trouble providing public access to the plain HTML version, so Google Docs Slides was the only choice left.

After creating a presentation and using the Publish to the Web option, I received an embed code to display fullscreen using the Chromium browser on the Raspberry Pi. However, the actual fullscreen view looked like this:

As you can see, the display is not actually fullscreen because Google adds a toolbar at the bottom and there is no publishing option that would allow you to remove it. However, there is an undocumented (or semi-documented) flag, “rm=minimal” which removes the toolbar, so my final embed URL looked like this:[PRESENTATION ID]/embed?start=true&loop=true&delayms=15000&rm=minimal

Since it was clear I would use HTML/CSS to display an iFrame with the presentation, I decided to overlay a clock and some minimal weather info (current temperature) using JavaScript.

HTML/JavaScript is freely accessible on the production site I load on the RPi, available at, but just to be on the safe side, the source is included in this post. (Sorry, inline comments are partly in Croatian).

<!DOCTYPE html>
    <link href="|Days+One|Source+Sans+Pro" rel="stylesheet" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta charset="utf-8" />
html, body {
	background: white;
	padding: 0;
	margin: 0;
	width: 100%;
	height: 100%;

.container {
	width: 100%;
	height: 100%;
	overflow: hidden;
    z-index: 1;
.container iframe {
	height: 100%;
	width: 100%;

.boks {
    font-family: 'Droid Sans Mono', monospace;
	color: red;
	position: absolute;
	right: 2%;
	top: 5%;
    z-index: 2;
	width: 24%;
	height: 9%;
    font-weight: bold;
	display: flex;
    align-items: center;
    justify-content: center;
    font-size: 5vw;
	background: linear-gradient(#a6e3f8fa, #b9e9fafa);  

.kalendar {
    font-family: 'Helvetica', sans-serif;
    font-weight: bold;
	color: #1155cc;
	position: absolute;
	bottom: 1%;
	left: 6%;
	z-index: 2;
	width: 90%;
	height: 3.4vw;
	font-size: 2.9vw;
	overflow: hidden;
	background: #e5e5e5;
	overflow: hidden;
    margin: 0;
    padding: 0;
    list-style: none;
.kalendar li {
	height: 3.4vw;
	padding: 0px;
	margin: 0px 5px;

#boks2 {
	top: calc(5% + 9%);
	background: linear-gradient(#b9e9fa90, #c7edfa90);  
    font-family: 'Days One', sans-serif;
    font-weight: normal;

.clock {
ul {
	list-style: none;
	padding-left: 0px;

ul li {
	display: inline;

#point {
	position: relative;
    padding-left: 0px;
    padding-right: 0px;
#weather-temperature {
	z-index: 2;

/* Animations */
@-webkit-keyframes blink {
	0% { opacity: 1.0;}
	50% { opacity: 0;}
	100% { opacity: 1.0;};      

@-moz-keyframes blink {
	0% {opacity: 1.0;}        
	50% {opacity: 0;}
	100% {opacity: 1.0;};
  <div class="container">
    <iframe id="presentFrame"
    frameborder="0" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe>
  <div class="boks">
    <div class="clock">
        <li id="hours"></li>
        <li id="point">:</li>
        <li id="min"></li>
        <li id="point">:</li>
        <li id="sec"></li>
    <br />
  <div class="boks" id="boks2">
    <div id="weather-temperature"></div>
  <div id="kalendar-data" class="kalendar-boks"></div>
  <script src="js/jquery-1.12.4.min.js"></script> 
  <script type="text/javascript">
// Javascript za iscrtavanje sata
$(document).ready(function() {
        setInterval( function() {
                $(".clock").css("visibility", "visible");
                // Create a new Date() object and extract the seconds, minutes and hours...
                var time = new Date();
                var seconds = time.getSeconds();
                var minutes = time.getMinutes();
                var hours = time.getHours();
                $("#sec").html(( seconds < 10 ? "0" : "" ) + seconds);
                $("#min").html(( minutes < 10 ? "0" : "" ) + minutes);
                $("#hours").html(( hours < 10 ? "0" : "" ) + hours);
  <script type="text/javascript">
// Osvježavanje temperature
$(function worker(){
        $.ajaxSetup ({
cache: false,
complete: function() {
	setTimeout(worker, 10*60*1000);
        // load() functions
        var loadUrl = "temp";
        // end  
  <script type="text/javascript">
// osvježavanje prezentacije
var refreshMinutes = 30;
var timer=setInterval(function(){refreshFrame()}, refreshMinutes*60*1000);
function refreshFrame()
        // var iframe = document.getElementById('presentFrame');
        // var iframeURL = iframe.src;
        // iframe.src = iframeURL;
  <script type="text/javascript">
// Ispis kalendara
$(function worker(){
        $.ajaxSetup ({
cache: false,
complete: function() {
	setTimeout(worker, 10*60*1000);
	// load() functions
	var loadUrl = "kalendar.cache";
	// end  
<script type="text/javascript">
  // ticker kalendara
function tick(){
    $('#kalendar li:first').fadeToggle( function () { $(this).appendTo($('#kalendar')).fadeToggle(); });
setInterval(function(){ tick () }, 6000);

Temperature data is polled from a PHP script which gets it from a local database; if you don’t have one, you can use OpenWeatherMap.

Google Calendar events are polled from a different PHP script which uses the Google Docs API to import events and formats it as a unnumbered list.

setlocale(LC_ALL, 'hr_HR.UTF-8');
$apikey = 'API_KEY'; // YOUR API key here!
$sada = date(DATE_ATOM);
$limit = new DateTime();
$limit = $limit->add(new DateInterval("P1M"))->format(DATE_ATOM);
$sada = urlencode($sada);
$limit = urlencode($limit);
$brojrezultata = 100;
$adresa = "[ID_KALENDARA]$brojrezultata&orderBy=startTime&singleEvents=true&timeMin=$sada&timeMax=$limit&key=$apikey";
$response = file_get_contents($adresa);
$eventi = json_decode($response, true)['items'];
echo "<ul id=\"kalendar\" class=\"kalendar\">";
if (!empty($eventi))
foreach ($eventi as $dogadjaj)
	if (array_key_exists('dateTime', $dogadjaj['start']))
	$dan =  strftime("%A, %e.%-m", strtotime($dogadjaj['start']['dateTime']));
	$pocetak =  date("G:i", strtotime($dogadjaj['start']['dateTime']));
	$recenica_vrijeme = "$dan u $pocetak sati"; 
	else if (array_key_exists('date', $dogadjaj['start']))
	$dan =  strftime("%A, %e.%-m", strtotime($dogadjaj['start']['date']));
	$recenica_vrijeme = "$dan"; 
	$opis = $dogadjaj['summary'];
	echo "<li style=\"display: list-item\">$recenica_vrijeme: $opis</li>";
	echo "Nema najavljenih događaja";
echo "</ul>"; 

As for the Raspberry Pi itself, I used the latest version of Raspbian (desktop) and made a few changes.

  1. Using raspi-conf  I enabled autologin for “pi” user
  2. Configured the wireless network via Desktop
  3. Installed unclutter to hide the mouse 
    sudo apt install unclutter 
  4. Edited the autostart file for the “pi” user, located at  ~/.config/lxsession/LXDE-pi/autostart
    @lxpanel --profile LXDE-pi
    @pcmanfm --desktop --profile LXDE-pi
    @xscreensaver -no-splash
    @unclutter -idle 0.1
    @xset s off
    @xset -dpms
    @xset s noblank
    @chromium-browser --app= --temp-profile --no-touch-pinch --kiosk

Another good idea is to enable a Chromium flag which would automatically reload the page once the browser goes online (in case of network connectivity losses). This can be done through chromium://flags.

Of course, this whole project is in a perpetual work-in-progress state, so don’t expect all this to work for you out of the box.

Huawei E3372 LTE modding

Please note – most of the firmware links on this page are dead. Sorry about that.

Huawei E3372 LTE stick supports all the LTE/4G frequencies used by Croatian providers (VIPnet uses 1800 MHz, Hrvatski telekom uses the 1800 MHz band in cities and 800 MHz in rural areas) and costs an arm and a leg if bought network-locked from Croatian telecom (HRK 450 /60 € contract-free). Luckily, you can get it for about 30 euros on eBay, fully unlocked.

Since my plan was to find out which frequency HT is using in my area, I tried to sniff it out from the statistics page, but were ultimately unsuccesful in doing so. The firmware I had in my E3372 was customised for Latvia’s LMT and did have signal quality info, but there was no way I could select individual frequency bands, only 2G/3G/HDSPA/4G. Then I tried out a bunch of available firmware packages found on this Austrian forum, but stock versions were more limited than the one I had, while modded versions would simply not flash.

Then I tried reflashing the Huawei to act as a surf-stick, with emulated modem interfaces. Huawei’s AT command set is fairly well-documented, but I was still unable to switch off individual frequency bands so I could find out whether I’m using 800 or 1800 MHz.

Huawei E3372h is very popular in Russia, so the 4pda forum has a lot of information and solutions for it, but even if you can read Russian Cyrillic script and understand a bit of Russian (or know how to use Gooogle Translate), you still have to be logged in to download. That process requires registration and solving not one, but two CAPTCHA’s in Russian. After a lot of work, I registered an account – the CAPTCHA on the registration page wants you to add up two numbers visible in the image (XX плюс XX равно or “XX plus XX equals”), but the CAPTCHA on the login page is a hell to solve because it gives you a four-digit number, in words, written in Russian cursive (an example in regular, ‘print’ Cyrillic: девять тысяч восемьсот сорок семь or devyat’ tysyach vosem’sot sorok sem’ or nine thousand eight hundred and forty seven).

Once I solved that, I was back in the game. The forum has detailed instructions on which firmware versions can be flashed out-of-the-box, how to modify the WebUI, how to get telnet/ADB running and how to switch the modem to DEBUG mode, which exposes its control ports.

I’ve used some of the stuff I found there, simplified some things and tested all of it on a new Huawei E3372 straight out of the box, so a standard disclaimer is in order: all of this stuff worked for me, it should work for you, but please don’t come crying if you mess something up. You’ve been warned.
Now, if you do want to make your Huawei E3372h-153 more usable, do read all of the instructions before you start fiddling with it because the first couple of points are a way to manually mod the stock firmware. If you don’t care for that version, you can skip to step 6, flash an appropriate firmware, do steps 1-3 and then just simply flash a modded firmware, ignoring steps 4 and 5.

1. Activating DEBUG mode

The first step in modding the stick is to switch it to DEBUG mode. This gives you access to additional COM ports we’ll use to unlock the modem and run busybox.

The switching procedure requires you to POST some XML to the modems API, which is usually at

This is what you need to post:

<?xml version="1.0" encoding="UTF-8" ?> 
<api version="1.0">

Scripts and curl.exe necessary for the above procedure can be found here.

2. Activating debug ports and calculating the OEM key needed for console access

I’m assuming you’ve switched your USB stick into DEBUG mode by running the sw_debug_mode.cmd script.

Download Huawei Modem Terminal, unzip it and run it. Select “FC – PC UI interface” and click on “Connect”.


Activate the console by issuing the following command:

You’ll see that the console is active once Huawei Modem Terminal starts showing modem stats.

Time to calculate the OEM key. Download, uzip and run CalculatorOEM. Send the AT command:


The modem will respond with something like this:

Recieve: ^NVRDEX: 50502,0,128,60 DE 2A 11 94 B3 4C 64 78 93 C6 5B 3F 21 57 CC 8A 6C 7C 25 DC 5B E5 C4 61 7A DF B5 EC 5F B4 C2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 62 7E F5 73 BC 33 24 87 39 33 8A 55 CD 75 33 72 05 9C 45 B8 5D BA C7 CC B0 72 6F 83 B0 D2 81 02 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

The calculator has the HEX parts you need printed in red and blue, so copy/paste the required parts into CalculatorOEM as shown and click on Подобрать код. After a couple of minutes (the calculations do take a lot of time and it may appear as if the program is not responding) you’ll get your OEM key. Write it down, you’ll need it later. You can get the NCK, the network unlock code, the same way, just replace 50502 with 50503 in the AT command.

Reset the modem: AT^RESET
Once it boots up and the WebUI opens in your browser, switch it to DEBUG mode once again.

3. Starting telnet/busybox

Now use Huawei Modem Terminal to connect to the “FC – ShallB” port.


Paste your OEM code in the “Send” box and hit Enter. The modem should reply:

Recieve: Login success 
Recieve: EUAP>
Recieve: EUAP>


Run busybox/telnet by issuing the following command

busybox telnetd -l /bin/sh

Now you can fire up your favourite telnet client (PuTTy, KiTTY) and open a Telnet connection to port 23, host IP

4. Autorun of adb and telnet

Download the necessary files, copy them on a MicroSD card and put it in the Huawei. Download and unpack this archive to the root folder of your SD card: ADB Daemon

First we’ll mount the MicroSD card so we can use it:

mount -t vfat /dev/block/mmcblk0p1 /mnt/sdcard

We’ll also remount /system with read-write permissions

mount -o remount,rw /dev/block/mtdblock16 /system

Then we’ll un-tar the adb daemon to system/bin:

busybox tar -xzvf /mnt/sdcard/adbd_s.tgz -C /system/bin

and finally add busybox and the adb daemon to the autorun script

echo -e "\nbusybox telnetd -l /bin/sh\n/system/bin/adbd &" >> /system/etc/

Now you can try to reboot your Huawei. Telnet and ADB should be accessible without the need to switch to DEBUG mode.

5. Replacing the webUI

Download the necessary files and copy them to your MicroSD card. You’ll need the modded full WebUI: webui17. and nand_tools if you want to backup your existing WebUI, although there are more ways of doing that.

Provided that you’ve completed steps 1-3, open a Telnet connection to the Huawei and, if needed, mount the SD card.

You can also make a backup of your existing WebUI if you feel so inclined. To restore any of these three types of backups, you’ll need telnet access.

You can either copy the files to a .tar archive:

busybox tar -cvf /mnt/sdcard/original-webui.tgz /app/webroot/*

or dump the entire partition with the WebUI using dd:

dd if=/dev/block/mtdblock15 of=/mnt/sdcard/webui.dd

or do the same with nanddump:

/mnt/sdcard/nanddump -f /mnt/sdcard/webui.img /dev/mtd/mtd15

Moving on. First we’ll remount the webroot as RW.

mount -o remount,rw /dev/block/mtdblock15 /app/webroot

Then we’ll delete the entire WebUI:

rm -r /app/webroot/WebApp
rm -r /app/webroot/upnp

Finally, we’ll unpack the modded WebUI from the SD card to webroot:

busybox tar -xzvf /mnt/sdcard/webui17. -C /app/webroot

In case of any trouble, try erasing the userdata or reverting the Huawei to factory settings. This wasn’t necessary in my case, but if you need a way of doing it, use the following scripts: erase_userdata_e3372.rar

6. Flashing a patched kernel which enables you to flash modded firmware

I couln’t find a patched kernel for the stock fimware I got with my Huawei, but you can always re-flash it to a supported version. Flash one of the following firmware versions:

You can find stock firmware on this Austrian forum, use the first one under “HI- Link Offiziell E3372h” (

If you do flash it after you’ve completed steps 1-5, you’ll have to redo steps 1-3 and then just flash a modded firwmare file/webUI.

Once you’ve flashed your Huawei to an eligible firmware version and enabled Telnet, copy the matching kernel and nand tools to the root of your SD card.
Kernels: dload_patch_kernels_HuaweiE3372h.7z
Nand tools: nand_tools

Mount the SD card:

mount -t vfat /dev/block/mmcblk0p1 /mnt/sdcard

Erase the existing kernel:

/mnt/sdcard/dload_patch/flash_erase /dev/mtd/mtd7 0 0

Flash a new kernel – use only the one appropriate for your kernel.

/mnt/sdcard/dload_patch/nandwrite /dev/mtd/mtd7 /mnt/sdcard/dload_patch/kernel_22.XXX.XX.XX.XX.bin

Finally, check if everything is okay by doing a test dump.

/mnt/sdcard/dload_patch/nanddump -f /testdump.bin /dev/mtd/mtd7

If all is well, you can reset your modem and flash a firmware/WebUI of your choosing (with or without ADB/telnet, etc). Just make sure it’s marked “M”, which means that it’s already modded to accept custom firwmare.

Adaptive Quality of Service using Zyxel VMG5313-B30 and OpenWRT

The title is not really descriptive, but I hope someone will find this useful.

This is the setup – I currently get my broadband service from Croatia’s Iskon, their 30/5 mbit package (which my line, sadly, doesn’t provide fully). In addition to this, I also have their IPTV service, so in order for me to make the best use of the broadband speed available, when I shut down my IPTV STB, I can use all of the bandwidth.

My setup also includes a TP-LINK TL-WDR3600  router, in charge of the PPPoE connection and WIFI at 2.5 and 5 GHz. The router is running  OpenWRT Barrier Breaker. One of the nifty things OpenWRT does well is Quality of Service, which distributes the bandwidth according to a set of rules (lowering priority to, for instance, P2P which tends to hog the bandwidth). However, in order for QoS to work properly, you need to set its maximum speed properly, and therein lies the problem – if I set it to the lower speed, I won’t be able to use the speed boost I get with the STB off. If I set ti to a higher speed, QoS loses its purpose and doesn’t do its job.

So what I needed was to automatically adjust the maximum download speed set in the QoS service, depending on whether the STB is on or off.

Iskon’s VDSl2 service comes with Zyxel VMG5313-B30 router which is a real powerhouse in terms of everything it offers, but most of the WebUI is hidden away from regular users. One can gain access to all of its options (including its own QoS and 3G WAN backup), but I needed a solution which worked with the standard level of access (which is basically: look, don’t touch).

The solution I came up with was this:

1) Since the VDSL2 router is running in bridge mode and connected to the WAN port on the WDR3600, I had to ensure access to its Web UI from the LAN side. I did this by setting up another fixed IP interface in the modem’s subnet (Iskon uses 192.168.5.x).

So basically, I added the following to /etc/config/interfaces:

config interface 'modem'
        option proto 'static'
        option netmask ''
        option ifname 'eth0.2'
        option ipaddr ''

(and checked the “Bring up on boot” option).

2) Now that I can access the modem from the LAN side, I need to log in via the web UI (telnet/ssh access is, of course, disabled by my broadband provider) and find a way of identifying if the IPTV STB is on.

The first step is simple, I used cURL to do a POST request to Zyxel’s login page and save the cookies.

curl -s -l -b /tmp/cookie -c /tmp/cookie -d "AuthName=Administrator&Display=Administrator&AuthPassword=Administrator"

Step two was a slightly harder. The only way I was able to identify if the STB is running was under System Monitor – Traffic Status which shows the upload/download traffic by interface (VDSL_VoIP, VDSL_IPTV, VDSL_Management). So what I needed to do was to poll the figures twice and see if they change and how much they change. I ended up with this:

curl -s -b /tmp/cookie -c /tmp/cookie | sed -nr 's/.*VDSL_IPTV\|ptm0\.3\|(.*)@1\|VDSL_Management.*/\1/p' | cut -d, -f4

If I repeat this after 10 seconds and the packet count is significant, the STB is probably on (even when the STB is off, there is some traffic, but not more than 15 packets per 10 seconds in my case)

3) The rest was easy – use the UCI to set appropriate speed limits and reload the QoS service.

uci set
uci commit qos
/etc/init.d/qos reload

And this is the whole bash script (sorry, some of the variable names are in Croatian). Name the file /etc/ and make it executable (chmod +x /etc/

logger="logger -p $0 [email protected]"
TRENUTNO=$(uci get
if [ "$TRENUTNO" == "$HIGHER" ]; then
$logger "Starting up. Current speed limit is $TRENUTNO, Receiver: $IPTVON"
if [[ -s /tmp/cookie ]]
while [ 1 ]; do
if [ "$LOGIN" == "0" ]; then
RESPONSE=$(curl -s -l -b /tmp/cookie -c /tmp/cookie -d "AuthName=Administrator&Display=Administrator&AuthPassword=Administrator" | awk 'NR==4' | sed -e "s/\(.*'\)\(.*\)\('.*\)/\\2/")
if [[ $RESPONSE == "/index.html" ]]; then

TRAFFIC=$(curl -s -b /tmp/cookie -c /tmp/cookie | sed -nr 's/.*VDSL_IPTV\|ptm0\.3\|(.*)@1\|VDSL_Management.*/\1/p' | cut -d, -f4)
if [ $TRAFFIC != "" ]; then
sleep 10
TRAFFIC2=$(curl -s -b /tmp/cookie -c /tmp/cookie | sed -nr 's/.*VDSL_IPTV\|ptm0\.3\|(.*)@1\|VDSL_Management.*/\1/p' | cut -d, -f4)
if [ "$RAZLIKA" -gt "50" ]; then
if [ "$IPTVON" == "0" ]; then
$logger "IPTV Receiver on. Lowering QOS maximum speed to $LOWER."
uci set$LOWER
uci commit qos
/etc/init.d/qos reload
if [ "$IPTVON" == "1" ]; then
$logger "IPTV Receiver is off. QOS maximum speed set to $HIGHER."
uci set$HIGHER
uci commit qos
/etc/init.d/qos reload
rm /tmp/cookie
sleep $WAIT

We’ll also need a startup script (/etc/init.d/qostuner)

#!/bin/sh /etc/rc.common
 start() {
 /etc/ &

stop() {
 killall -9

Don’t forget to set your two speeds at the beginning of the script and make it run on startup.

Temperature logging and regulation in a central heating system

Continuing my series of articles about a central heating system mentioned here, the next step was to find a way to remotely monitor temperatures throughout the system, without having to run down into the basement boiler room to see what’s going on. Since I began tinkering with Arduino at the time, I opted for Atmel’s Atmega micro-controllers, mostly because they’re very cheap, popular and you can easily find help for anything you need. I will briefly describe the whole system and let you work out the schematics for yourself, not just because I’m too lazy to draw them.

1. Temperature monitoring and logging

Since I needed more than 6 temperature sensors in the system, the right choice for the job was Dallas DS18B20 digital One-wire sensor, which makes it possible to hook up “a large number” of sensors using just one I/O pin on the Atmega. The sensors also support temperatures of up to +125 degrees Celsius, which suits my needs perfectly. OneWire sensors also have excellent Arduino support. I’ve installed 8 sensors in total, 4 at different heights on the accumulation tank, one on the hot feed, two on the cold return (one before and one after the mixing valve), and the last one was installed in the boiler itself, because it already came with a temperature sensor slot. Thermal grease and thermal-conductive glue worked wonders. For the purposes of logging, I chose remote logging via the local network, so the project also required an Ethernet module. During the first year, while the whole setup was connected using an Arduino board, I used the expensive, Wiznet 5100 based Arduino Ethernet Shield, but once I made the “final product” on a custom circuit board, I switched to the cheaper Microchip ENC28J60 based modules (at the moment, the entire module costs under USD 4.00). These modules have excellent support through the EtherCard library. I’ve written a sketch which would poll the sensors and read the temperatures from al eight sensors every 10 seconds, and log the data to a server as a HTTP GET request, which looks something like:

PHP script post.php stores the data into a MySQL database. I use Google Charts to draw temperature line charts: glinechart

2. Controlling the circulator pump

Now that I know all of the temperatures in the system at any given moment, I figured it would be neat to switch on the circulator pump automatically when the heat storage tank is sufficiently full (e.g. to one quarter) and switch it off as soon as the boiler is not able to maintain the temperature going into the heat storage tank. Although this requires just one relay, I got a 4-relay shield with opto-isolators. The opto-isolators will later prove extremely important because the relays have to switch AC motors, so once I made a printed circuit board for my circuit without them, the whole thing mysteriously started freezing up whenever one of the motors was switched on/off, and no amount of TVS’s, filters and other stuff was able to fully eliminate all the bugs.


But let’s go back to my relay shield. The circulator pump required one, I reserved two for regulation, and one was left unused. When the pump was switched on, it would start running only if the temperature at the top of the heat storage tank was above 60 degrees (heat charger maintains that temperature), and would automatically stop if the temperature were to drop below 60 (in order to maintain layering of hot water in the heat storage tank and slow down the discharge which is much more stable when the circulator is not running).

3. Regulation

This bit was much more complicated, both electrically, electronically and in terms of programming. I wanted to control the mixing valve in order to maintain constant hot feed temperature, which would also ensure constant cold return temperature. Truth be told, I had no idea at this point what I was supposed to do and how, but I was willing to experiment. I got a “dumb” actuator suitable for my mixing valve.

actuator_label actuator

As it says on the label, the actuator cannot be controlled automatically using steps and you can’t instruct it to go to a certain position – it’s powered by 230VAC and has only three leads, two phase leads controlling the direction and one neutral lead. The only thing I knew is that it takes the actuator 140 seconds to get from 0 to 90 degrees. .

I made precise measurements to see how long it took the actuator to full open or close, divided this by 10 and got ten steps I can use to control the actuator. Of course, without a way to get feedback as to whether the actuator has actually reached a position, this was extremely imprecise, but usable if you rely on limit switches in the actuator. I stored the assumed position of the actuator into the EEPROM so that my system knew where the actuator was even after a reset.

The regulation system kicked in every few minutes, comparing temperatures and opening/closing the mixing valve.

4. Circulator pump pulse drive

First of all, I must explain that my circulator pump was bought in 1986, and it was left in its original packaging until 2010 when it was installed, meaning that it doesn’t work that well. It’s also “dumb”, because it only offers three speeds which are selected using a switch, so there is no other way of regulating its speed. And I so needed a way to regulate its speed because most of the problems I had could be solved if only I could reduce the flow. The problem grew even more acute when the circulator simply stopped working at the lowest speed, so I had to switch it to run faster.

That’s when I decided to try a sort of pulse width modulation, only a really, really slow version of it – the circulator would run for 30 seconds, which is enough to supply hot water to the radiators, and then the pump would stop for 60 seconds, allowing the radiators to radiate some heat and cool down the water before sending it back to the storage tank. The line chart in this case looks like this:


The mixing valve is open to 80%, which explains the variance between the cold return feed (dark blue) and heat storage tank return (light blue). The hot feed to the radiators is some 60 degrees, and the return temperature is much lower. I have been using this system for two years and not once did I have the need to let the circulator run constantly.

5. Source code and schematics

Like I said, the “end product” on a printed circuit board with power, relays, OneWire Bus, serial LCD, Ethernet module, even a RTC chip (DS1307) proved to be much more unreliable than a prototype made using an Arduino board, so I can’t give you the schematic. Just don’t make the same mistake as I did – use opto-isolators to separate the relays from the MCU, and if you have four AC motors in the vicinity like I have (boiler ventilator, main circulator pump, heat charger circulator pump, mixing valve actuator), you can be sure that everything will go to shit if you forget them.

Arduino source code with comments in English can be found on my Github. The author makes no responsibility that the code will work or that it will be appropriate for any use. It’s quite likely that it won’t even compile, but it might give you a few pointers to help you along in whatever you’re doing.

The Trouble with Valves (Central Heating, Part 2)

In my first article I briefly wrote about how the installation of my central heating system with a wood-fired boiler, a heat storage tank and a storage tank charger went. After the installation was finished, the system was filled with water to 1.5 bar, we lit the first fire and waited to see what would happen. Everything went smoothly, once the temperature in the boiler hit 30°C a thermostat switched on the circulation pump in the storage tank loop and the heating up of the boiler continued. At 60°C the charger began adding cold water from the bottom of the storage tank to the return feed (the loading of the heat storage began). We waited for a while for the top of the heat storage to heat up to 60°C, switched on the circulation pump in the heating loop and after the initial few minutes of heating, we wound up with cold radiators.

The problem was the fact that the installation crew didn’t really know what they should do, so they installed a four-way mixing valve (4WV) between the circulation pump, the hot feed and the cold return. Such a valve is usually installed in heating systems without storage and a charger. In such installations, the 4WV serves to balance the feed and cold return temperatures, or rather enables the mixing of hot feed water with the cold return from the radiators so as not to shock the boiler with cold water.

You can see what this looks like in this rough non-technical doodle depicting a four-way mixing valve open to one third.


When the valve is half-open, we can see that the hot and cold water are mixed in both in the boiler/storage loop and the radiator loop. The valve can also be fully opened, in such cases the hot feed and the cold return from the storage are directly connected to the radiator loop, or closed, when the two loops are fully separated. These are the only two cases when there is no mixing, in all other cases the valve additionally heats up the water that returns to the heat storage.

In order to clear up why this is wrong in heat storage installations, we will discuss how they function, as they are completely different in terms of return temperature. To retain proper layering in the heat storage, the temperature difference between the hot feed and the cold return should be as high as possible. The heat storage tank looks something like this.


The boiler is connected sketch left, sketch right are the hot feed and the cold return for the radiators. Both the loading of the storage and its discharging are done in exactly the same manner, on the left we draw cold water from the bottom of the tank and return hot water to the top. On the right, we draw hot water from the top and return water cooled in radiators to the bottom return feed. When natural circulation is present in the system (i.e. the circulation pump is off and the boiler room/storage tank loop is located below the radiator loop) this works perfectly, hot water is drawn into the radiators where it is cooled down and then returned via the cold return.

The following line chart represents the loading of the storage tank in ideal conditions, the circulation pump in the radiator loop is switched off, only the pump in the heat charger is on. The temperature sensors are located in four positions on the storage tank (number 1 is at the top of the tank, number 4 is at the bottom). Hot water circulates slowly through the system, cold water returns to the tank. The spikes in the return temperature occur when additional radiators are turned on.


As we can see, the layering is perfect and while the top of the tank is at 60°C, only 30 cm lower the temperature is 40°C, the bottom is a little over 20°C.

The line chart representing the discharging of the same system using natural circulation (circulation pump off) is as follows.


Although the fire in the boiler went out and the boiler was switched off, the charger circuit continued to transfer heat to the tank (grey line), and the storage tank is discharging, cold water from the bottom pushes hot water up. The layering is preserved. (Nota bene: the line chart is for a warm day with only a few opened radiators, a 500 litre storage tank is not enough to keep my system warm for 12 hours).

In less then ideal conditions, when the circulation pump is switched on, the water circulates through the radiator loop much faster, doesn’t have time to cool down in the radiators (even at the slowest speed of the circulation pump) and the cold return is significantly warmer. Adding warm water to the bottom of the heat storage ruins the layering and after just a few minutes the water in the entire tank gets mixed up. Instead of 60°C at the top and 20°C at the bottom, we get a whole tank filled with lukewarm water at temperatures which aren’t high enough to heat up the radiators.

This is exactly what happened when I first switched on the circulation pump. In just five minutes I was left with a tank full of lukewarm water. The effect a four-way mixing valve has in such a system is either bad (loops are connected directly without mixing), worse (loops are connected in such a way that the cold return is mixed with the hot feed) or the worst (the loops are separated and the hot feed from the heat storage is connected directly to its cold return).

The four-way mixing valve was the main culprit and had to be removed. We replaced it with a three-way mixing valve which operates differently.

When the valve is fully opened, the loops are connected directly. This case is shown in the following figure, and is unacceptable for the system when the circulation pump is running.


However, when the valve is half-open (or half-closed, depending on its outlook on life), we can send most of the cold return water back into the radiator loop and draw only a little hot water from the hot feed. By doing this, we will send only a small amount of warm water back into the cold return and the layering in the heat storage will hold out much better. In this example, the three-way mixing valve is half-open.


The following line chart represents a system with a three-way mixing valve.


Early on the circulation pump is not running, the three-way mixing valve is fully opened, the hot feed and the cold return are directly connected to the radiator loop.

At 17:52 the circulation pump was switched on and we can see the return temperature spike, while the hot feed temperature is dropping. The temperature difference between the two drops from almost 35°C to barely 5°C. The bottom of the tank is heating up.

At 17:58 the three-way mixing valve is closed to about 1/3, meaning that 2/3 of the water from the cold return is mixed with 1/3 of water from the hot feed and sent back into the radiator loop. Although it is not evident in this chart, the temperature of all radiators has dropped because the pump has mixed up the layered water in them (the radiators were hot at the top and cold at the bottom) – the same thing that happens in the heat storage occurred in the radiators, they are now lukewarm from top to bottom.

At 18:22 the three-way mixing valve is opened to about a half (half of the water is sent back to the radiator loop, half is returned to the bottom of the heat storage). We see that the difference between the hot feed and the cold return is now rising as the radiators are heating up the rooms. The system is stabilizing.

The last chart shows how the loading of the storage tank continued with the circulation pump running and with a three-way mixing valve at 50%.


The difference in temperatures of the hot feed and cold return is stable at about 10°C, the storage is loading, although the chart is not as smooth as it was when the circulation pump isn’t running. It is also evident that the system is much slower to react, it takes a lot longer for the radiators to heat up (obvious from the hot feed temperatures).

To sum up, in order for the heat storage to work well, it has to be connected to the radiator loop through a three-way mxiing valve, a four-way mixing valve is completely useless in this case. These charts also indicate a need for a control system which would monitor the temperatures in the loops, switch on the circulation pump when needed and control the mixing valve in order to preserve the layering in the heat storage whilst providing a high enough hot feed temperature to heat up the radiators.

This will be discussed in future articles, the next one with deal with the first building block of the system, the sensors and methods of recording temperatures. I’ll show you how to measure the temperatures, log them on a computer, monitor them via the internet and plot colourful line charts.

To be continued…

Wood-fired Central Heating – Part One

In this series of posts, I’ll talk about issues I’ve encountered with an more modern installation of a wood-fired central heating boiler (‘more modern’ compared to the old boilers that were installed in Croatia before widespread introduction of natural gas). When I was trying to decide which fuel to choose, natural gas was quickly disqualified since, despite the fact that it enables you to just set it and forget it, I’ve missed out on its heyday in terms of pricing (i.e. I should have had it installed when it was the cheapest fuel for heating, which it isn’t anymore).

So I opted for solid fuel, since the price of wood logs remains fairly stable throughout the year, they don’t suffer from gas reductions, and in contrast to wood pellets, logs are easy to DIY if necessary. They are also carbon-neutral and a tested heating method which is used for some millions of years now 🙂 On the other hand, it’s never boring with a wood boiler, there’s always stuff to do, but this can be slightly aleviated with heat storage.

On the technical side, I’ve chosen a wood gasification boiler which, apparently, gets more heat out of wood and doesn’t require refuelling that often. I got the Buderus Logano S121, the only wood-gasification boiler available in Croatia which had the advantage of being the right size to fit into my basement and still take logs of over 50cm, which is neat as I wanted to cut the one-metre logs in half.

To make all this refuelling business less frequent, I also got a 500-litre heat storage (once again, the size was decided based on its size and my stairwell, with the intention of getting another one if it proves useful) and the ESBE LTC 141 heat charger (a cheaper version of Laddomat) with an opening temperature of 60°C.

The rest of material was also somehow acquired and after just a few months of waiting for the installation crew to arrive, the 300+ kg boiler was carried into the basement over a flight of stairs and the work began.


For the most part, the installation went as expected, but since this was the first time for this installation crew to face off against the complicated combo of boiler – charger – heat storage, regardless of their experience, there was some serious reading of manuals. And this is where the first mistakes were made, which we later noticed. However, here’s the simplifed schematic of the system: :


B is the boiler, L is the storage tank charger, AT is the accumulation tank, 3WV is the three-way mixing valve, CP is the circulation pump, and R are the radiators. This is the boiler room IRL.


Part two… 

Fully Automated Offline Backup for under 10 Euros

The Aim of this Project

  1. do nightly backups to a second hard drive
  2. the backup drive should be disconnected and powered off except when performing backups to save electricity
  3. powering up of the hard drive and the whole backup process must not require human intervention

1. This is self-explanatory.

2. While I was trying to work out how to do this, this second issue was a little harder to crack, because although most operating systems support hot swapping of SATA hard drives, this generally doesn’t work if the drive is permanently connected via the SATA connection and we just switch off the power. Making a relay switch which would disconnect both the SATA cable and the power cable would be sometwhat expensive, because of the number of relays needed (if we just switch off the power, one relay is enough).

In the end, I decided to connect the SATA hard drive to the computer through a SATA-to-USB adapter, while a switching circute would cut off the drive’s power. If the drive is permanently connected to the computer through this adapter, when we power up the drive, the system will easily detect the drive. This is the dmesg output of one such event:

kernel: [261005.130086] usb 1-8: new high speed USB device using ehci_hcd and address 13
kernel: [261005.282437] usb 1-8: configuration #1 chosen from 1 choice
kernel: [261005.285433] scsi11 : SCSI emulation for USB Mass Storage devices
kernel: [261010.281628] scsi 11:0:0:0: Direct-Access     ST325082 4AS            L    PQ: 0 ANSI: 2 CCS
kernel: [261010.285138] sd 11:0:0:0: Attached scsi generic sg1 type 0
kernel: [261010.287471] sd 11:0:0:0: [sdb] 488397168 512-byte logical blocks: (250 GB/232 GiB)
kernel: [261010.288412] sd 11:0:0:0: [sdb] Write Protect is off
kernel: [261010.296302]  sdb: sdb1
kernel: [261010.317085] sd 11:0:0:0: [sdb] Attached SCSI disk

When we power down the drive, USB will recognize an ’empty’ adapter, which will refresh once we power the drive back up, and this serves our purpose. USB-to-SATA adapters are sold on eBay for just a few dollars.

3. Solving the third item required some knowledge of electronics. Parallel ports are very suitable if you want to control relays, because the pins can be easily set to 1 or 0, and will remain in that state until you send another byte (if we were to use the serial port, we would have to continuously send 1’s to keep it powered on). Luckily, Linux allows us to easily write directly to parallel port, and we have eight data lines (bits) at our disposal.

You can find a pinout of the parallel port here, and if you need a conversion table between DEC and BIN, check this out (you’ll need this if you decide to control more than one relay through the parallel port) .

I’ve prepared a schematic of the circuit which switches off the power on a SATA power connector and is controlled via the parallel port. You can download a schematic / PCB layout here. Once finished, you should connect the circuit to any of the data pins on the parallel port connector, and we will switch all the bits to 1 or 0.

That’s our hardware part done, and we can start programming.

Parallel port control

Here, we will use a simple programme written in C, which takes a DEC value as an argument and sends the byte directly to the parallel port. Source code is available  here.
If you’re compiling it on newer distros, you’ll have to correct one of the #includes, from #include <asm/io.h> to #include <sys/io.h>
The source with these changes can be found here: lptout.c
Grab the source and compile it.

gcc lptout.c

This will give you an executable called  a.out.
We’ll rename it and change permissions.

mv a.out lpt
chmod +x lpt

If your switching circuit is connected to the computer, you can check to see if everything is working as it should by sending the following commands to switch the relay on or off (you should be in the same directory as the executable).

sudo ./lpt 255
sudo ./lpt 0

Shell script

This script will do the following:

  1. Power on the hard drive by switching all the data bits on the parallel port to “1” /home/uprava/backup/lpt 255
  2. Wait 60 seconds for the system to detect the newly connected drive.
  3. Run fsck because it needs to be done every once in a while – the system will refuse to mount an unchecked drive ($dev is the variable with the device name, for instance /dev/sdb1)
    /sbin/e2fsck -p $dev
  4. Mount the hard drive mount /mnt/backup
  5. Prepare additional backup files, a list of installed packages and dump all MySQL
    databases.dpkg --get-selections > /home/uprava/packages-installed.txt
    DBS="$(mysql -u root -h localhost -pPASSWORD -Bse 'show databases')"
    for db in $DBS
    mysqldump -u root -h localhost -pPASSWORD $db | gzip -9 > /home/uprava/mysql-$db.gz


  6. Perform the backup, I like to use rsync to make a mirror copy of the folders and delete any deleted files.
    rsync -ahv –delete –ignore-errors –exclude-from ‘exclude’ /home /mnt/backup/
  7. Check disk space usage and include it in the backup log file. There’s a nice script that does that here. In my setup, the main backup script would simply run this one.
  8. Unmount the drive
    umount /mnt/backup
  9. Power off the drive
    /home/uprava/backup/lpt 0

cron job

We must also add a cron job to run this script every day. The script must be run as ‘root’ . Open up the root’s crontab…
sudo crontab -e
Add a line similar to this one, with a full path to your backup script. I run my backup every night at 4:18 a.m.
18 4 * * * /home/uprava/backup/

The finished script

In this shell script, we’re assuming that you’ve predefined the mount parameters for your backup hard drive in /etc/fstab. The script is well commented and it shouldn’t be hard to find your way through it. The script logs or stdouts everything it does, so troubleshooting should also be easy.

# Offline Backup Script
# By: Ivan Biuklija
# v. 1.4
# Setup

mnt="/mnt/backup" # mount point for the hard drive
dev="/dev/sdb1" # device to mount
lptout="/home/uprava/pport/lpt" # path to the compiled lptout.c

# Setup ends here

$lptout 255 # Power on the hard drive
echo -e "Drive powered on \n"
sleep 60 # Wait 60 seconds for the system to detect the USB hard drive.

# FSCK, mount

if [ -e $dev ]; then
echo -e "FSCK it up! \n"
/sbin/e2fsck -p $dev
echo -e "Drive found, attempting to mount... \n"
mount $mnt
echo -e "Drive not found! Aborting."
exit 1

# Let's check if the drive is mounted

if [ -d $mnt/lost+found ]; then
echo -e "Drive successfully mounted."
echo -e "Error: Drive not mounted. Aborting."
exit 1

# dump a list of all installed packages

apt-cache pkgnames > /home/uprava/pkgnames.txt
dpkg --get-selections > /home/uprava/installed-packages.txt

# dump all MySQL databases

DBS="$(mysql -u root -h localhost -pPASSWORD -Bse 'show databases')"
for db in $DBS
mysqldump -u root -h localhost -pPASSWORD $db | gzip -9 > /home/uprava/mysql-$db.gz

# Backup any directories you want, this is just an example.
rsync -ahv --delete --ignore-errors --exclude-from '/home/uprava/backup/exclude' /home /mnt/backup/system
rsync -avh --delete --ignore-errors --exclude-from '/home/uprava/backup/exclude' /etc /mnt/backup/system
rsync -ahv --delete --ignore-errors --exclude-from '/home/uprava/backup/exclude' /root /mnt/backup/system
rsync -avh --delete --ignore-errors '/mnt/media/music' /mnt/backup/media

# Backup done. Let's check the free disk space.
echo -e "\nBackup done. Free disk space is: \n"

# Unmounting the drive

echo -e "\nUnmounting the drive... \n"
umount $mnt

# We'll wait some time for the process to finish

sleep 2

# Checking if the drive is unmounted, then powering it down

if [ -d $mnt/lost+found ]; then
echo -e "\nDrive is not unmounted! Aborting."
exit 1
echo -e "The drive is unmounted. Powering it off.\n"
$lptout 0

# Waiting for the USB to recover and detect an 'empty' adapter

sleep 30

# Checking to see if the drive is powered down

if [ -e $dev ]; then
echo -e "Error: the drive is still powered on! \n"
exit 1
echo -e "\nThe drive is powered off. Have a nice day!"
exit 0
exit 0