Il software

Il software

SmartOcthaedron da solo non basta...L'app dedicata per smartphone è l'unica altra cosa di cui avrà bisogno!

I vincoli

Il software da sviluppare per questo particolare dispositivo, deve sostanzialmente svolgere queste funzioni:

  • Interfacciarsi con SmatOctahedron via Bluetooth ed essere quindi in grado di ricevere delle stringhe di testo da esso;
  • Gestire in modo corretto l’avvio di una registrazione, memorizzando volta per volta i dati ricevuti da SmatOctahedron;
  • Calcolare, nel mentre o una volta terminata la registrazione, le percentuali di tempo che l’utente ha impiegato a svolgere per ognuna attività;
  • Fornire all’utente la possibilità di creare vari “profili”, corrispondenti ciascuno di essi ad un ambito diverso (es. lavorativo, sportivo ecc). Ognuno, associa le facce di SmatOctahedron ad una diversa attività, in base all’ambito in cui l’utente si trova.

La scelta del framework di sviluppo

Xamarin è stata la scelta che sin da subito è risultata la più adatta per questi scopi. In particolare, un’app Xamarin Forms, utilizza per lo più librerie .Net Framework e il linguaggio C#, già ampiamente a conoscenza dei nostri sviluppatori. Nel processo di compilazione, si può scegliere mediante una semplice combobox il device di destinazione, Apple o Android. Per i nostri scopi, ci siamo concentrati per ora solamente sull’applicativo Android. Il codice scritto mediante Xamarin viene appunto chiamato codice condiviso, perché appunto condiviso da più ambienti di esecuzione. Possono però esserci delle eccezioni nei quali Xamarin non è in grado di “tradurre” biunivocamente il codice per tutte le piattaforme di esecuzione, perlopiù quando si tratta di accedere a dispositivi Hardware oppure quando si effettuano delle particolari chiamate al sistema operativo. Questo accade per alcune profonde differenze tra il sistema operativo iOS e Android. In questi casi si ricorre all’utilizzo delle “DependencyServices”, overo dei “pezzi” di codice nativo specifici per ogni piattaforma.

Mia foto

La struttura dell'applicazione

Aprendo l’applicazione denominata “TimeKeeper”, si accederà ad una schermata di menù nel quale sarà possibile accedere alle varie sezioni dell’app. Si potrà inoltre verificare se SmatOctahedron è già connesso allo smartphone e se una registrazione è già in esecuzione. Grazie alla tecnica del backgrounding è possibile infatti far continuare a lavorare l’applicazione mentre lo schermo del telefono è bloccato o si stanno eseguendo altre applicazioni (più in dettaglio, la sezione dedicata). In particolare:

  • Scegli device: permette di connettersi a SmatOctahedron scegliendo tra i diversi device Bluetooth disponibili;
  • Nuova cattura: permette di avviare una nuova registrazione, scegliendo un nome e un profilo;
  • Cattura corrente: se è in corso una registrazione, permette di visualizzarne il riepilogo cioè la lista di tutte le attività del profilo associato e il tempo totale impiegato per ciascuna di esse;
  • Archivio catture: permette di visualizzare la lista delle catture passate, permettendo la visualizzazione delle stesse;
  • Gestione profili: permette la creazione e la gestione dei profili, e quindi l’associazione delle facce dell’ottaedro (identificate in modo assoluto da un numero) ad una data attività, specifica appunto per ogni profilo.
Mia foto
A titolo esemplificativo, è riportato il codice sorgente della pagina principale (in linguaggio C#) nel quale si possono notare le chiamate alle funzioni di sistema per la lettura da uno specifico file di specifiche funzioni di stato quali la connessione o meno ad un device o la registrazione eventualmente corrente.


	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Linq;
	using System.Text;
	using System.Threading.Tasks;
	using Xamarin.Forms;
	using System.Resources; 
	namespace TimeKeeper
	{
		// Learn more about making custom code visible in the Xamarin.Forms previewer
		// by visiting https://aka.ms/xamarinforms-previewer
		[DesignTimeVisible(false)]
		public partial class MainPage : ContentPage
		{
	
		   public string sto_registrando;
		   public string id_registrazione_corrente;
		   public string sono_connesso="false";
		   public string id_dispositivo="";
			public MainPage()
			{
				InitializeComponent();
	
			  //  DependencyService.Get().DeleteFile("Settings.txt");
				//vedi se esiste il file contenente i settaggi, se non esiste, crealo
				if (DependencyService.Get().FileExsist("Settings.txt") == false)
				{
					DependencyService.Get().WriteData("Settings.txt", "false\r\n");
					DependencyService.Get().AppendText("Settings.txt", "null\r\n");
					DependencyService.Get().AppendText("Settings.txt", "false\r\n");
					DependencyService.Get().AppendText("Settings.txt", "null");
				}
	
	
				//leggi i settaggi dall'apposito file
	
				using (var reader = DependencyService.Get().OpenRead("Settings.txt")) {
					sto_registrando = reader.ReadLine();
					id_registrazione_corrente = reader.ReadLine();
					sono_connesso = reader.ReadLine();
					id_dispositivo = reader.ReadLine();
					reader.Close();
				}
	
				if (sto_registrando == "true")
				{
					sto_registrando_label.TextColor = Color.Green;
					sto_registrando_label.Text = "Sto registrando: si";
				}
	
				MessagingCenter.Subscribe(this, "StatoConnessione",(sender,arg)=>
					{
						if (arg == "sono_connesso")
						{
							sono_connesso = "true";
						}
						else if(arg.StartsWith("id_dispositivo")){
							string[] elementi=arg.Split('#');
							id_dispositivo = elementi[1];
						}
						else if (arg.StartsWith("stop_registrazione"))
						{
							sto_registrando = "false";
						}
						else if (arg.StartsWith("resume_registrazione"))
						{
							sto_registrando = "true";
						}
					});
	
			}
	
	
			public void AggiornaContenuti(object sender, EventArgs args)
			{
				if (this.sto_registrando == "true")
				{
					sto_registrando_label.Text = "Sto registrando: SI";
					string[] elementi = id_registrazione_corrente.Split('#');
					DateTime data_inizio = Convert.ToDateTime(elementi[2]);
					id_registrazione_label.Text = "ID registrazione corrente: " + elementi[0] + " iniziata il " + data_inizio.ToString();
					sto_registrando_label.TextColor = Color.Green;
				}
				else
				{
					sto_registrando_label.Text = "Sto registrando: NO";
					sto_registrando_label.TextColor = Color.Red;
				}
	
				if (this.sono_connesso == "true")
				{
					connesso_label.Text = "Sono connesso: SI, con " + id_dispositivo;
					connesso_label.TextColor = Color.Green;
				}
				else
				{
					connesso_label.Text = "Sono connesso: NO";
					connesso_label.TextColor = Color.Red;
				}
	
			}
	
			public async void ScegliDevice (object sender, EventArgs args)
			{
				await Navigation.PushAsync(new ConnettiDevice( this ));
			
			}
	
			public async void NuovaCattura(object sender, EventArgs args)
			{
				await Navigation.PushAsync(new NuovaCattura(this));
	
			}
	
			public async void CatturaCorrente(object sender, EventArgs args)
			{
				await Navigation.PushAsync(new CatturaCorrente(this));
	
			}
	
			public async void ArchivioCatture(object sender, EventArgs args)
			{
				await Navigation.PushAsync(new ArchivioCatture(this));
	
			}
	
			public async void GestisciProfili(object sender, EventArgs args)
			{
				await Navigation.PushAsync(new GestisciProfili());
	
			}
	
		}
	}	

Connetti a device

Banalmente, vengono visualizzati i device attualmente disponibili. Selezionandone uno di essi (naturalmente deve corrispondere a SmatOctahedron), il software apre una comunicazione Bluetooth con esso, effettuando una chiamata ad un pezzo di codice nativo.


	public void Connetti(object sender2, ItemTappedEventArgs args)
	{      
		try
		{
			DependencyService.Get().Start(args.Item.ToString(), 1000, false);
			 MessagingCenter.Subscribe(this, "TrasmessaFaccia", async (sender, arg) => {
				 mandante.sono_connesso = "true";
				 mandante.id_dispositivo = args.Item.ToString();
				 if (mandante.sto_registrando == "true")
				 {
					 DependencyService.Get().AppendText("cattura#"+mandante.id_registrazione_corrente,arg+"!"+ DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss")+"\r\n");
				 }
							 
			  });

			Application.Current.MainPage.DisplayAlert("Connesso", "Connessione con il dispositivo " + args.Item.ToString() + " effettuata correttamente", "Ok");
		}
		catch (Exception ex)
		{
			 Application.Current.MainPage.DisplayAlert("Attention", ex.Message, "Ok");
		} 
	}

Si noti che la funzione "Connetti", dopo aver richiesto ad una parte di codice nativo di accedere al Bluetooth dello smartphone e quindi di connettersi al device selezionato, effettua una sottoscrizione al thread "TrasmessaFaccia". Questo sistema di messaggistica intra-app viene usato per creare una via di comunicazione tra la parte dell'applicazione scritta in codice nativo e la parte scritta mediante Xamarin Forms. Non appena si riceve un messaggio sul thread corrispondente, una routine di gestione è associata: in particolare, se ci sono dati disponibili in ingresso (cioè una certa faccia corrente, e quindi un messaggio sul thread "TrasmessaFaccia") e c’è una registrazione attiva, il software memorizzerà il tutto nel file corrispondente alla registrazione. L’ottaedro invia la faccia in cui si trova ogni circa 5 secondi. Il software memorizza volta per volta la faccia, abbinata all’istante in cui viene ricevuta (se ovviamente, una registrazione è avviata). Ciò rende possibile, a posteriori, mettendo assieme tutte queste informazioni, il calcolo di quanto una certa faccia è rimasta “stabile” e quindi il calcolo del tempo totale trascorso per una certa attività.

Nuova cattura

Mediante la scelta di uno specifico nome e di uno specifico profilo, sarà possibile avviare una nuova registrazione. Appena ciò avviene viene creato mediante la chiamata al codice nativo, il file corrispondente alla nuova registrazione:


	async void  AvviaCattura(object sender, EventArgs args)
	{
		if (lista_profili.SelectedItem==null)
		{
			await DisplayAlert("Errore", "Nessun profilo selezionato", "OK");
			return;
		}

		if (mainPage_sender.sono_connesso=="false")
		{
			await DisplayAlert("Attenzione", "Nessun device collegato. La cattura verrà comunque avviata", "OK");
		}

		string profilo_scelto="";
		string nome_cattura;


		using (StreamReader reader = DependencyService.Get().OpenRead("Profili.txt"))
		{
			string line;
			string[] elementi;
			while ((line = reader.ReadLine()) != null)
			{
				elementi = line.Split(',');
				if (elementi[1] == lista_profili.SelectedItem.ToString())
				{
					profilo_scelto = elementi[0];
					break;
				}
			}

			nome_cattura = nome.Text;

			DependencyService.Get().WriteData("cattura#"+nome_cattura+"#"+profilo_scelto+"#"+ DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss") ,"");
		  
			mainPage_sender.sto_registrando = "true";
			mainPage_sender.id_registrazione_corrente = nome_cattura + "#" + profilo_scelto + "#" + DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
		}
	}

E’ possibile notare che nel nome del file vengono memorizzati: il nome della registrazione, il profilo scelto e la data/ora dell’inizio. Vengono aggiornati anche alcuni flag e alcune variabili globali che segnalano che il software, sta registrando.

Cattura corrente

Leggendo il file corrispondente alla registrazione in atto (eventualmente presente), sarà possibile calcolare per ogni faccia (quindi attività, associandola al profilo) il tempo totale. In particolare, si è scelto di creare una classe “Cattura” racchiudendo in essa tutte le funzioni che essa può svolgere (ad esempio il calcolo di determinati tempi o parametri). E' riportata di seguito:


namespace TimeKeeper
	{
		public class Cattura
		{
	
			string id;
			double[] tempi_attivita_local;
			string id_profilo;
			public Cattura(string id)
			{
				this.id = id;
				string[] elementi = id.Split('#');
				id_profilo = elementi[1];
			}
	
			public double[] tempi_attivita()
			{
	
				double[] durate_attivita = new double[8];
				using (StreamReader sr = DependencyService.Get().OpenRead("cattura#" + this.id))
				{
	
					string str_letta;
					bool stato_pausa = false;
					bool primo_dato = true;
					string faccia_corrente = "";
	
	
					DateTime tempo_precedente = DateTime.Now;
					DateTime istante_letto;
	
					while ((str_letta = sr.ReadLine()) != null)
					{
	
						System.Diagnostics.Debug.WriteLine(str_letta);
	
						if (str_letta == "PAUSA")
						{
							primo_dato = true;
	
						}
						else
						{
							string[] elementi = str_letta.Split('!');
							string[] elementi2 = elementi[0].Split('#');
							if (primo_dato)
							{
								faccia_corrente = elementi2[1];
								istante_letto = Convert.ToDateTime(elementi[1]);
								tempo_precedente = istante_letto;
								primo_dato = false;
								int faccia = Convert.ToInt32(elementi2[1]);
								durate_attivita[faccia - 1] += 5;
							}
							else
							{
								if (elementi2[1] == faccia_corrente)
								{
									//fai una differenza di tempo e accumula il registro
									istante_letto = Convert.ToDateTime(elementi[1]);                             
									TimeSpan differenza = istante_letto.Subtract(tempo_precedente);                              
									int faccia = Convert.ToInt32(elementi2[1]);
									durate_attivita[faccia - 1] += differenza.TotalSeconds;
									tempo_precedente = istante_letto;
	
								}
								else
								{
									//è stata cambiata la faccia
									faccia_corrente = elementi2[1];
									istante_letto = Convert.ToDateTime(elementi[1]);
									tempo_precedente = istante_letto;
									int faccia = Convert.ToInt32(elementi2[1]);
									durate_attivita[faccia - 1] += 5;
								}
							}
	
	
						}
	
					}
					sr.Close();
				}
				return durate_attivita;
			}
	
			public double tempo_totale()
			{
				tempi_attivita_local = this.tempi_attivita();
				double totale = 0.0;
				foreach (double tempo in tempi_attivita_local)
				{
					totale += tempo;
				}
	
				return totale;
			}
	
			public string nome_profilo()
			{
				using (StreamReader reader = DependencyService.Get().OpenRead("Profili.txt"))
				{
					string line;
					string[] elementi;
					while ((line = reader.ReadLine()) != null)
					{
						elementi = line.Split(',');
						if (elementi[0] == id_profilo)
						{
							reader.Close();
							return elementi[1];                        
						}
					}
				}
				return null;
			}
	
			public string[] nomi_attivita()
			{
				using (StreamReader reader = DependencyService.Get().OpenRead("Profili.txt"))
				{
					string line;
					string[] elementi;
					while ((line = reader.ReadLine()) != null)
					{
						elementi = line.Split(',');
						if (elementi[0] == id_profilo)
						{
							return new string[] { elementi[2], elementi[3], elementi[4], elementi[5], elementi[6], elementi[7], elementi[8], elementi[9] };
						}
					}
				}
				return null;
			}
	
			public string nome_cattura()
			{
			   string[] elementi = id.Split('#');
			   return elementi[0];
			}
	
			public DateTime inizio()
			{
				string[] elementi = id.Split('#');
				return Convert.ToDateTime(elementi[2]);
			}
	
		}
	
	
	}

E’ possibile inoltre mettere in pausa la registrazione. Facendo ciò si segnalerà all’applicazione che non dovrà più memorizzare alcun dato nel file corrispondente alla registrazione, se non un flag speciale “PAUSA” che nel processo di lettura segnalerà il fatto che l’istante eventualmente successivo a quella riga, non dovrà essere sottratto a quello precedente (falsando quindi i dati), poiché in quel lasso di tempo, la registrazione non era in esecuzione.

Archivio catture

Viene visualizzata una lista con tutte le registrazioni precedenti. Ciò viene fatto banalmente visualizzando tutti i file presenti in una cartella e formattandone il nome in modo più “user friendly”. Ogni registrazione corrisponde infatti ad un determinato file, contenente tutte le varie facce abbinate ad un dato istante di tempo. Dati sufficienti per tutte le informazioni che si vogliono estrapolare. Cliccando su una registrazione, verrà aperta una pagina simile alla pagina corrispondente a “Cattura Corrente”, nella quale sarà visualizzata la registrazione e tutti i suoi tempi. E’ inoltre possibile riprendere una certa registrazione stoppata precedentemente.


public ArchivioCatture(MainPage mandante)
{
	InitializeComponent();

	this.mandante = mandante;
	string[] files = DependencyService.Get().OttieniFiles();
	
	catture = new Hashtable();

	foreach(string str in files)
	{
		string[] elementi = str.Split('#');
		if (!elementi[0].EndsWith("cattura")) { continue; }
		catture.Add(elementi[1] + "("+elementi[3]+")",elementi[1]+"#"+elementi[2]+"#"+elementi[3]);
	}
	listView1.ItemsSource = catture.Keys;       
}

Gestione profili

Un particolare file contiene la lista di tutti profili, compresi per ognuno l’associazione delle facce a specifiche attività uniche per ogni profilo. Nello specifico viene visualizzata una lista di tutti i profili presenti (di default e al massimo, 8). Cliccando sopra per ognuno di essi è possibile modificarne e salvarne il contenuto.


public void RefreshItems(object sender, EventArgs args)
{

	List nomi_profili = new List { };

	string text = "";
	using (StreamReader reader = DependencyService.Get().OpenRead("Profili.txt"))
	{
		string line;
		string[] elementi;
		while ((line = reader.ReadLine()) != null)
		{
			elementi = line.Split(',');
			nomi_profili.Add(elementi[1]);
		}
		reader.Close();
	}

	listView1.ItemsSource = nomi_profili;
}

Il background

L’applicazione viene messa in background non appena l’utente spegne lo schermo dello smartphone o accede al menù principale cambiando per esempio app. Mettendo in background un’applicazione, tutti i suoi processi e le sue risorse vengono di fatto “congelati”. Nel caso di SmatOctahedron si vuole ovviamente evitare questo fatto: l’utente deve essere libero di poter utilizzare comodamente qualsiasi app del proprio smartphone o di poter spegnerne lo schermo quando desidera, non interrompendo però la registrazione.

Il fatto che l’applicazione venga messa in background, viene notificato dall’esecuzione della seguente porzione di codice presente nel file “App.cs”, contenente le routine associate all’avvio dell’app (“OnStart”), alla messa in background (“OnSleep”) ecc.


protected override void OnSleep()
{
	DependencyService.Get().DeleteFile("Settings.txt");

	if (DependencyService.Get().FileExsist("Settings.txt") == false)
	{
		DependencyService.Get().WriteData("Settings.txt", pagina_principale.sto_registrando + "\r\n");
		DependencyService.Get().AppendText("Settings.txt", pagina_principale.id_registrazione_corrente + "\r\n");
		DependencyService.Get().AppendText("Settings.txt", pagina_principale.sono_connesso+"\r\n");
		DependencyService.Get().AppendText("Settings.txt", pagina_principale.id_dispositivo);
	}

	if (pagina_principale.sono_connesso == "true")
	{
		DependencyService.Get().Cancel();
	}

	if (pagina_principale.sono_connesso == "true" && pagina_principale.sto_registrando == "true")
	{
		DependencyService.Get().AppendText("cattura#" + pagina_principale.id_registrazione_corrente, "PAUSA\r\n");
		Xamarin.Forms.MessagingCenter.Send((App)Xamarin.Forms.Application.Current, "AvviaServizioBackground", null);
	}

}

In sostanza, oltre che salvare i nuovi valori nel file contenente le impostazioni, verifica se correntemente c’è una registrazione attiva. Se sì, oltre a terminarla temporaneamente e a disconnettere il device, comunica con una DependencyService (“pezzo” di codice scrito in linguaggio nativo) delegata alla gestione del background. Esso non fa altro che tornare a connettersi al device, ma utilizzando un ServizioBackground, cioè uno speciale thread di esecuzione che anche se l’app viene messa in pausa, continua a funzionare. Il canale comunicativo con SmatOctahedron rimarrà quindi attivo e le facce correnti trasmesse da esso, verranno correttamente ricevute dall’applicazione. Questa volta però, verranno memorizzate in un file diverso. Alla riapertura dell’applicazione, viene eseguita la routine “OnResume” del file “App.cs”, analogamente a quanto successo in precedenza:


protected override void OnResume()
	{
		string str2;

		using (StreamReader sr = DependencyService.Get().OpenRead("Settings.txt"))
		{
			if ((str2 = sr.ReadLine()) == "true") //se stavo registrando vado a vedere se ci sono contenuti nel file scritto in background. se ci sono valori li copio tutti nel file corrispondenti con un PAUSE finale
			{
				if (DependencyService.Get().FileExsist("ValoriLettiBackground"))
				{

					Xamarin.Forms.MessagingCenter.Send((App)Xamarin.Forms.Application.Current, "StopServizioBackground", null);

					using (StreamReader sr2 = DependencyService.Get().OpenRead("ValoriLettiBackground"))
					{

						List valori_letti = new List { };
						string str;
						while ((str = sr2.ReadLine()) != null)
						{
							valori_letti.Add(str);
						} 

						string percorso_cattura = sr.ReadLine();

						foreach(string str3 in valori_letti)
						{
								DependencyService.Get().AppendText("cattura#"+percorso_cattura,str3+"\r\n");
						}

						DependencyService.Get().AppendText("cattura#" + percorso_cattura, "PAUSA" + "\r\n");
						sr2.Close();

						DependencyService.Get().WriteData("ValoriLettiBackground", "");
					}

					sr.ReadLine();
					string nome_dispositivo = sr.ReadLine();

					DependencyService.Get().Start(nome_dispositivo, 1000, false);
					MessagingCenter.Subscribe(this, "Barcode", async (sender, arg) => {
						pagina_principale.sono_connesso = "true";
						pagina_principale.id_dispositivo = nome_dispositivo;
						if (pagina_principale.sto_registrando == "true")
						{
							DependencyService.Get().AppendText("cattura#" + pagina_principale.id_registrazione_corrente, arg + "!" + DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss") + "\r\n");
						}

					});

				}
			}

			sr.Close();
		}
	}

Banalmente: se nel file impostazioni il software legge che l’ultima volta che l’app è stata messa in background stava registrando, allora controlla se sono presenti valori nel file specifico “ValoriLettiBackground”. Se sono presenti, l’app non fa altro che copiarli nel giusto file corrispondente alla registrazione alla quale si riferiscono (presente nel file delle impostazioni), fermare il processo in background (quindi mettendo in pausa la registrazione) quindi riconnettersi al device mediante il thread “in-app” e riprendere mediante esso, la ricezione dei valori dall’ottaedro.

Più nel dettaglio. Come funziona?

La struttura

Per realizzare la struttura ci siamo serviti della tecnlogia di stampa 3D.

L'hardware

La piattaforma open source "Arduino" è il cuore pulsante dell'ottaedro, con attorno a se molti altri componenti importantissimi.

Il firmware

Un firmware "ah hoc" è stato sviluppato per coordinare tutte le funzionalità dell'ottaedro.

Il software

Avvalendosi dell'ambiente "Xamarin", è stato possibile creare in modo semplice e veloce un'app per smarthone in grado di tener traccia di tutte le attività, connettendosi con l'ottaedro.