Nel precedente articolo abbiamo visto come poter installare un Raspberry PI in modo da far funzionare un programma Blazor. Ora vediamo le caratteristiche che dovrà avere la nostra sveglia ed iniziamo l’implementazione.
Ovviamente non saranno i classici documenti di requisiti e implementazione (SRS ed SAD) che tutti conosciamo quando si inizia un progetto, ma almeno una lista di requisiti vorrei darla anche se in questi articoli non vedremo in dettaglio tutti gli step di implementazione.
Requisiti hardware
1) scheda Raspberry PI 3 oppure 4 che faccia da server per l’applicazione Blazor
2) display touch
3) tastiera e mouse
4) alimentatore compatto
5) altoparlante
Requisiti funzionali
1) visualizzare la data e l’ora corrente
2) avere la possibilità di rimandare (snooze)
3) impostare una o più sveglie
4) scegliere i giorni della settimana in cui sarà attiva
5) essere a tutto schermo
6) dare la possibilità all’utente di scegliere la musica per la sveglia
7) attivare o disattivare la sveglia
8) cancellare una sveglia
9) visualizzare il meteo di un luogo a scelta dell’utente
Requisiti di design
1) look “dark” per non disturbare il sonno
2) orario sempre presente sullo schermo
3) meteo corrente della località principale sempre presente a schermo
4) menù per poter aggiungere una sveglia
5) gestire i suoni di sveglia personalizzati (caricare un file .mp3
personale)
6) riepilogo delle sveglie impostate
Hardware
Partiamo dal setup hardware anche se un po’ lo abbiamo scoperto nello scorso articolo.
Come base abbiamo detto che ci serve un Raspberry PI per semplicità d’uso. Potremmo utilizzare altre piattaforme ma sono sicuramente più costose e meno versatili. Sicuramente esula dallo scopo di questo articolo, per cui accetteremo un Raspberry PI come hardware di base.
Display
Altra cosa importante è il display. Io per il mio esperimento ho utilizzato un display da 4.3 pollici touch screen della Waveshare che potete trovare a questo indirizzo:
Questo tipo di display ci permette di agganciare il Raspberry PI direttamente allo schermo ottenendo un singolo oggetto molto più comodo da gestire.
Ho scelto di utilizzare un touch screen perché vorrei una user experience molto vicina a quella che è una radiosveglia tradizionale, ovvero schiacciare su un bottone per la funzione “snooze” oppure schiacciare su un altro bottone per interrompere la sveglia. Non è molto pratico per queste funzioni usare mouse e tastiera appena svegli 😉
Alimentatore
L’alimentazione è una nota dolente per Raspberry PI. Anche se a pieno carico arriva ad assorbire 1,5 / 2,0 A è necessario alimentarlo con un ottimo alimentatore in grado di erogare stabilmente 3,0A e 5V. Diversamente otterremo un errore costante a schermo di “undervoltage” abbastanza fastidioso da vedere. Si è vero, lo si può eliminare disabilitando il controllo di undervoltage, ma non è uno buona idea.
Inoltre dobbiamo considerare che con lo stesso alimentatore dovremmo far accendere anche il display che nel mio caso (4.3″) consuma circa 2A.
In conclusione il nostro alimentatore dovrà essere in grado di erogare circa 5A. Questo ci obbliga a non acquistare un semplice alimentatore come quello per un cellulare ma qualcosa di più robusto. Per completezza vi consiglio un alimentatore di questo tipo:

Questo è il link verso Amazon ma anche Aliexpress ha gli stessi alimentatori anche se con tempi di consegna decisamente più lunghi.
Infine per poter collegare sia Raspberry PI che il suo monitor all’alimentatore, avremo bisogno di un cavo doppio che dall’alimentatore arrivi ai devices.
Vi confesso che per questa fase dovrete impegnarvi un po’ a saldare e costruire i cavi per assicurare i collegamenti correttamente, ma il gioco vale la candela.
Tastiera e mouse
Per il setup e per le prime operazioni direttamente su Raspberry, è utile avere una tastiera ed un mouse. Vi consiglio di prendere un set wireless perché più comodo.
Altoparlante
Ovviamente per ogni sveglia che si rispetti, essa deve essere in grado di riprodurre un suono che avremo associato al nostro allarme. Io per il mio esperimento ho utilizzato un altoparlante con un jack da inserire direttamente nell’uscita jack del Raspberry PI.
Io ho usato questo

Prometto che con l’hardware finisco qui.
Creiamo la solution e i progetti
Iniziamo a creare una solution dal template Blazor Webassembly App Empty
per evitare di avere cose superflue che “sporcano” il nostro codice.

Alla solution daremo il nome BlazorAlarmClock
ed inizieremo con l’aggiunta di un nuget package per utilizzare una libreria di componenti gratuita. Aggiungiamo al progetto Client
la libreria MudBlazor

alcune cartelle nel frontend (progetto Client
) per gestire la comunicazione con il backend, gestire le musiche ecc…
Aggiungiamo una cartella Components
e una pagina Services
ovviamente per ospitare le rispettive classi.
Dunque iniziamo dai componenti. Aggiungeremo un componente che chiameremo AlarmComponent
in grado di gestire un allarme. In dettaglio questo componente dovrà essere in grado di:
- Gestire la partenza e l’arresto della musica
- Gestire lo snooze e lo stop dell’allarme
- Gestire il tempo di snooze
- Gestire se sarà attivo o disattivo
- Gestire i giorni della settimana in cui sarà attivo
- Gestire il caricamento e il salvataggio
L’apparenza di questo componentéderà una cosa del genere

Il gestore della musica non si vedrà ma la sua gestione la vedremo fra poco.
Gestione dell’audio
Per poter gestire l’audio in HTML5 esiste uno specifico tag <audio/>
che fa al caso nostro. Il funzionamento di questo tag esula dallo scopo di questo articolo ma almeno vedremo come usarlo. Aggiungiamo il seguente codice al componente
<audio id="myAudio" style="visibility: hidden">
<source src="@FileName" type="audio/mpeg">
</audio>
Se notate questo tag audio non sarà visibile. Questo perché a schermo si vedrebbe il player simile al seguente

ed in realtà per il nostro scopo non è l’ideale.
Il riferimento alla variabile @FileName
farà riferimento al sorgente MP3
direttamente sul browser. Il file audio di default lo metteremo in wwwroot\audio
semplicemente per semplicità d’uso ma vedremo in seguito come caricare nuovi files personali.
Riproduzione e pausa dell’audio
Ora dobbiamo avere un modo per avviare e fermare l’audio da software. Al momento l’unico modo è da JavaScript
per cui ci creiamo due piccoli scripts che faranno al caso nostro.
function PlaySound() {
document.getElementById("myAudio").loop = true;
document.getElementById("myAudio").play();
}
function StopSound() {
document.getElementById("myAudio").pause();
document.getElementById("myAudio").currentTime = 0;
}
Richiameremo i due script dal nostro codice C# ed è fatta. Allo scadere del tempo per l’allarme, eseguiremo da C# questo:
await JsRuntime.InvokeVoidAsync("PlaySound");
e se eseguiamo questo tramite un classico <button/>
funziona perfettamente ma… se cerchiamo di eseguirlo in automatico… beh… non funzionerà. Perché?
Avremo in seguito lo stesso problema quando vorremo eseguire l’applicazione a schermo intero o altri script JavaScript in automatico. I browser moderni sono dotati di politiche di sicurezza rigorose che impediscono l’esecuzione di script o codice non autorizzato o non inizializzato dall’utente. Questo significa che se il codice JavaScript viene eseguito senza alcuna interazione utente, il browser potrebbe impedirne l’esecuzione.
L’unico modo è aggiungere un <button/>
per autorizzare il full screen dell’applicazione. Questo farà in modo di autorizzare l’esecuzione di codice JavaScript per la nostra applicazione.
Creiamo il componente Blazor
Bene! Ora che abbiamo tutti gli elementi, possiamo iniziare a costruire il componente. Per brevità dell’articolo vi rimando al codice GitHub ma volevo farvi notare alcune parti importanti del processo.
Nel codice razor vediamo l’injection ad un componente SystemWatch
di cui però non abbiamo ancora parlato. Questa classe fa parte di un componente di cui abbiamo parlato in un precedente articolo atto a visualizzare la data e l’ora corrente.
Questa classe espone un evento che ad ogni secondo invia data e ora corrente. Possiamo usarlo per controllare se la nostra sveglia debba suonare o meno. Inoltre dovremmo controllare se l’allarme è stato stoppato o è in snooze oppure sta suonando ecc. Abbiamo bisogno di una semplice state machine.
Creiamo un enum
per gestire gli stati:
public enum AlarmStatus
{
/// <summary>
/// No value
/// </summary>
NONE = 0x00,
/// <summary>
/// The playing status
/// </summary>
PLAYING = 0x01,
/// <summary>
/// The snoozed status
/// </summary>
SNOOZED = 0x02,
/// <summary>
/// The stopped status
/// </summary>
STOPPED = 0x03,
/// <summary>
/// The alarm was stopped today
/// </summary>
STOPPED_TODAY = 0x04,
/// <summary>
/// The alarm status is undefined
/// </summary>
UNDEFINED = 0xFE,
/// <summary>
/// The alarm status is unknown
/// </summary>
UNKNOWN = 0xFF
}
Non vi annoio sui motivi per cui ai miei enum
aggiungo sempre tre valori NONE, UNDEFINED, UNKNOWN
ma mi concentrerei sugli altri stati.
Abbonandoci all’evento della classe SystemWatch
chiamato SystemChangedEvent
la callback fungerà fa state machine e avrà questo aspetto:
private void SystemWatch_SecondChangedEvent(object? sender, DateTime e)
{
if (CurrentAlarm is null) return;
if (!CurrentAlarm.IsActive) return;
switch (Status)
{
case AlarmStatus.NONE:
break;
case AlarmStatus.PLAYING:
break;
case AlarmStatus.SNOOZED:
if (time is null)
{
Status = AlarmStatus.STOPPED_TODAY;
break;
}
if (e.Hour == time.Value.Hours &&
e.Minute >= time.Value.Minutes &&
e.Second >= time.Value.Seconds)
{
PlaySound();
IsSnoozeVisible = true;
Status = AlarmStatus.PLAYING;
StateHasChanged();
}
break;
case AlarmStatus.STOPPED:
if (time is null)
{
break;
}
if (e.Hour == time.Value.Hours && e.Minute == time.Value.Minutes)
{
if (CurrentAlarm.AlarmDays is null || CurrentAlarm.AlarmDays.Count() == 0)
{
IsSnoozeVisible = true;
PlaySound();
}
else
{
if (CurrentAlarm.AlarmDays.Where(x => x.DayAsInt == (int)e.DayOfWeek)
.FirstOrDefault() is not null)
{
IsSnoozeVisible = true;
PlaySound();
}
}
StateHasChanged();
}
break;
case AlarmStatus.STOPPED_TODAY:
if (time is null)
{
break;
}
if (today != e.Day || (e.Hour == time.Value.Hours && e.Minute != time.Value.Minutes))
{
today = e.Day;
Status = AlarmStatus.STOPPED;
}
break;
case AlarmStatus.UNDEFINED:
case AlarmStatus.UNKNOWN:
default:
break;
}
}
Non penso ci siano cose troppo complesse da comprendere, ma uno stato l’ho aggiunto per una ragione particolare. Lo stato STOPPED_TODAY
l’ho aggiunto per un edge case particolare. Nel caso in cui la sveglia venga spenta all’interno del minuto programmato, avremmo l’effetto indesiderato di una ripartenza dell’allarme. Gestendo questo stato questo effetto indesiderato non compare più.
Aggiungiamo un audio personalizzato
Per aggiungere un audio personalizzato all’allarme, dovremo fare un upload di esso. Ci viene sicuramente in aiuto la libreria MudBlazor tramite il componente <MudFileUpload/>
e lo useremo in questo modo:
<MudFileUpload T="IBrowserFile" Accept=".mp3" FilesChanged="UploadFiles" MaximumFileCount="1">
<ButtonTemplate>
<MudButton Style="margin-top:-2px"
HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Tertiary"
StartIcon="@Icons.Material.Filled.CloudUpload"
for="@context">
Ringtone .mp3
</MudButton>
</ButtonTemplate>
</MudFileUpload>
L’aspetto sarà comunque quella di un <button/>
con un’icona ma sotto al cofano verrà richiamata la finestra di sistema per scegliere il file che ci verrà restituito in un’interfaccia IBrowserFile
da elaborare. Nel nostro caso la passeremo ad una classe di servizio nel seguente modo
public async void UploadFiles(IBrowserFile file)
{
try
{
if (file != null)
{
var ms = new MemoryStream();
await file.OpenReadStream().CopyToAsync(ms);
var fileBytes = ms.ToArray();
var fileData = new FileData()
{
FileName = file.Name,
DataBytes = fileBytes,
Path = "wwwroot/audio"
};
var response = await http.PostAsJsonAsync($"{UploadFileRingtoneEndpoint}", fileData);
if (!response.IsSuccessStatusCode)
{
OnErrorRaised?.Invoke(this, $"{response.StatusCode} - {response.ReasonPhrase}");
}
else
{
await GetRingroneList();
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
OnRingtoneUploaded?.Invoke(this, file.Name);
}
Il file, sotto forma di byte array incapsulato in una nostra classe FileData
, verrà spedito via HTTP POST al nostro backend (progetto Server) e lo salveremo riconvertendolo nel suo formato originale.
Fate attenzione al codice precedente e nella fattispecie alla linea di codice seguente
await file.OpenReadStream().CopyToAsync(ms);
Lasciata in questo modo, la lettura del file sarà limitata a 512kb di default e magari l’utente vorrebbe caricare qualcosa di un po’ più grande.
La modifica è semplice, basta aggiungere un parametro all’ OpenReadStream
ch indica la massima dimensione che il file potrà avere. Modifichiamo quindi come segue
await file.OpenReadStream(maxAllowedSize: long.MaxValue).CopyToAsync(ms);
A questo punto non dovremmo avere problemi con files anche abbastanza grandi.
Salviamo l’allarme
Per il salvataggio dell’allarme la cosa non è molto difficile. Utilizzeremo un piccolo database SQLite con due tabelle semplice che conterrà l’allarme e i giorni della settimana in cui sarà attivo. Gestendo il tutto con Entity Framework non avremo alcun problema né a leggere gli allarmi né a salvarli.
Costruiamo la mostra home page
Bene, ora che abbiamo tutti i tasselli, li componiamo in una pagina principale index.razor
tramite tre parti principali:
- L’orologio
- La lista allarmi
- Il bottone per attivare il codice JavaScript
Il codice della nostra pagina Index.razor
sarà questo:
@if (!alarmActivated)
{
<div class="overlay d-flex align-items-center">
<MudButton id="buttonId" Class="mx-auto w-75 btn-overlay" Variant="Variant.Filled" Color="Color.Tertiary"
OnClick="Full" Style="font-size: x-large">Activate the alarm</MudButton>
</div>
}
<MudGrid>
<MudItem xs="12">
<div class="d-flex justify-content-end element-container mr-12 position-absolute">
<SystemWatchComponent ClockDisplay="WatchDisplayEnum.WithBlinking" Is24H=true EnableJsTime="false" />
</div>
</MudItem>
<MudItem xs="12">
@if (alarmService.AlarmList is not null)
{
@foreach (var alarm in alarmService.AlarmList)
{
<AlarmComponent CurrentAlarm="@alarm" EditRequested="OnAlarmEditRequest" />
}
}
</MudItem>
</MudGrid>
Noterete che ogni allarme avrà la sua riga e l’orario verrà visualizzato subito sopra. Attenzione a dare uno stile molto scuro alla pagina se no potrebbe dar fastidio la notte mentre dormiamo.
Menù laterale
Per poter gestire gli allarmi, i suoni personali e altre impostazioni, abbiamo bisogno di un menù. Questo menù però non deve essere sempre visibile per cui costruiremo un drawer sulla sinistra dello schermo che conterrà i menù di navigazione.
Conclusione
Per ora diciamo che di carne al fuoco ce n’è già abbastanza. Nel prossimo articolo vedremo come aggiungere un servizio meteo alla nostra sveglia che però troverete già integrato nel codice su GitHub.