Digitalna oglasna ploča

Ovoga puta donosim jedan jednostavan i jeftin projekt koji sam napravio za posao – digitalna oglasna ploča za školu u kojoj radim. Plan je bio prenamijeniti jedan veći LED TV, montirati ga u glavni hol kroz koji svi učenici prolaze i koristiti ga kao podsjetnik s korisnim, servisnim informacijama. U istom holu i roditelji najčešće čekaju termine za informacije, pa je cijela stvar i njima korisna.

Od “offline” rješenja i korištenja preglednika fotografija/videa s USB sticka smo, naravno, u startu odustali jer sam htio nešto što određen krug suradnika može lako ažurirati. Kako nam je dostupni TV opremljen HDMI-jem, odlučio sam se iskoristiti Raspberry Pi. Poduže guglanje i pregled dostupnih komercijalnih, polukomercijalnih i besplatnih rješenja za digital signage nije ponudilo jasnog favorita – rješenja su bila komplicirana za uređivanje i bila su fokusirana na video, nešto što sam smatrao nepotrebnim za ispisivanje kratkog teksta, nekoliko fotografija ili eventualne jednostavne animacije.

Na taj način odlučio sam se za kombinaciju web stranice i fullscreen browsera. Istestirao sam više kombinacija, uključujući i digital signage temu za WordPress, ali kod nje su mi nedostajale određene mogućnosti uređivanja. Kao logično rješenje nametnulo se prikazivanje HTML inačice nekog od online servisa za prezentacije, Microsoftov PowerPoint online i Presentations u Google Docsu. Pritom sam kod Microsoftovih kombinacija imao određene probleme s prijavom odnosno s javnim pristupom HTML verziji prezentacija, pa je konačni odabir pao na Google Docs Presentations.

Pripremio sam prezentaciju i opcijom objavljivanja na web dobio kod koji sam mogao otvoriti u Chromiumu na Raspberry Piju. Prikazao sam stranicu na cijelom ekranu i dobio nešto ovako:

Kao što vidite, ekran nije cijeli ispunjen jer Google inzistira na prikazivanju alatne trake na dnu ekrana.  Za uređivanje prikaza postoji poludokumentirana/nedokumentirana opcija “rm=minimal” koja uklanja i tu traku, pa URL za ugrađivanje izgleda ovako:

https://docs.google.com/presentation/d/[ID PREZENTACIJE]/embed?start=true&loop=true&delayms=15000&rm=minimal

Kad je već bilo izvjesno da će se raditi o HTML-u/CSS-u koji prikazuje iframe s prezentacijom, u cijelu kombinaciju sam dodao sat i prikaz vanjske temeprature u javascriptu.

HTML/JavaScript ovoga svega slobodno je dostupan na samoj stranici koja se koristi za prikaz na adresi https://kiosk.oskatancic.hr/, ali za svaki slučaj arhivirat ćemo ga i ovdje na blogu.

<!DOCTYPE html>
<html>
  <head>
    <link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono|Days+One|Source+Sans+Pro" rel="stylesheet" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta charset="utf-8" />
    <title>Kiosk</title>
    <style>
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;};
}
</style>
  </head>
  <body>
  <div class="container">
    <iframe id="presentFrame"
    src="https://docs.google.com/presentation/d/[ID_PREZENTACIJE]/embed?start=true&amp;loop=true&amp;delayms=15000&amp;rm=minimal"
    frameborder="0" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe>
  </div>
  <div class="boks">
    <div class="clock">
      <ul>
        <li id="hours"></li>
        <li id="point">:</li>
        <li id="min"></li>
        <li id="point">:</li>
        <li id="sec"></li>
      </ul>
    </div>
    <br />
  </div>
  <div class="boks" id="boks2">
    <div id="weather-temperature"></div>
  </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);
        },1000);
        
});
</script> 
  <script type="text/javascript">
// Osvježavanje temperature
$(function worker(){
        $.ajaxSetup ({
cache: false,
complete: function() {
	setTimeout(worker, 10*60*1000);
                }
        });
        // load() functions
        var loadUrl = "temp";
        $("#weather-temperature").html("...");
        $("#weather-temperature").load(loadUrl);
        // end  
});
</script> 
  <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;
        window.location.reload(true);
}
</script> 
  <script type="text/javascript">
// Ispis kalendara
$(function worker(){
        $.ajaxSetup ({
cache: false,
complete: function() {
	setTimeout(worker, 10*60*1000);
	}
	});
	// load() functions
	var loadUrl = "kalendar.cache";
	$("#kalendar-data").html("...");
	$("#kalendar-data").load(loadUrl);
	// end  
});
</script> 
<script type="text/javascript">
  // ticker kalendara
function tick(){
    $('#kalendar li:first').fadeToggle( function () { $(this).appendTo($('#kalendar')).fadeToggle(); });
}
setInterval(function(){ tick () }, 6000);
</script></body>
</html>

Podatak o temperaturi dolazi iz PHP skripte koja je uzima dobiva iz baze i vraća čisti neformatirani tekst koji JavaScript periodično iščitava i prikazuje iznad prezentacije.

Događaje iz Google kalendara priprema druga PHP skripta, koja preko Google Docs API-ja povlači zadani broj događaja i formatira ih u obliku liste.

<?php
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 = "https://www.googleapis.com/calendar/v3/calendars/oskatancic.hr_[ID_KALENDARA]%40group.calendar.google.com/events?maxResults=$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>";
}
}
else
{
	echo "Nema najavljenih događaja";
}
echo "</ul>"; 
?>

Što se tiče same pripreme Raspberry PI-ja, koristio sam zadnju inačicu Raspbiana (desktop varijanta) uz određene izmjene.

  1. Pomoću raspi-conf  treba uključiti automatsku prijavu u desktop korisnika “pi”
  2. Postaviti mrežu pomoću GUI ili shell alata (ako koristite bežičnu mrežu)
  3. Instalirati paket unclutter koji skriva strelicu miša
    sudo apt install unclutter 
  4. Urediti autostart korisnika koji se nalazi u ~/.config/lxsession/LXDE-pi/autostart
    @lxpanel --profile LXDE-pi
    @pcmanfm --desktop --profile LXDE-pi
    @xscreensaver -no-splash
    @point-rpi
    @unclutter -idle 0.1
    @xset s off
    @xset -dpms
    @xset s noblank
    @chromium-browser --app=https://kiosk.oskatancic.hr/ --temp-profile --no-touch-pinch --kiosk
    

Još jedna stvar koja se pokazala korisnom je uključivanje flaga u Chromiumu koji automatski ponovno učitava stranicu kada se uspostavi mrežna povezanost (ukoliko, recimo, osvježavanje prezentacije započne u trenutku kada RPI izgubi mrežnu povezivost). Jedini način za uključivanje ove opcije je iz Desktop verzije Chromea/Chromiuma preko chromium://flags.

Sve ovo je, naravno, vječito u izradi i stalno se mijenja, ali trebalo bi vam moći poslužiti kao osnova.