Progetto Domotica con Android

Android project smart home automation
Il progetto su cui sto lavorando recentemente consiste nella costruzione di un sistema di domotica che sfrutta un touch screen Android basato su ARM Cortex A8 a 1Ghz ed una libreria modbus nativa in modo da comunicare via rs485 con degli slave Arduino.

Nel post precedente, vi ho presentato uno sketch Arduino che gestisce richieste modbus. Passiamo ora ad analizzare il codice sorgente Android necessario alla creazione di un nodo master che comunica con slave Arduino.

La cosa carina di questo design è che la logica che controlla il sistema domotico non è vincolata ad essere nel nodo master (cioè nel touch screen nel nostro caso) ma bensì può essere distribuito in tutte le board Arduino. Inoltre, dato che l’RS485 ed il protocollo modbus sono stati creati per ambienti industriali, questo progetto può essere facilmente esteso ed applicato in contesti industriali.


I prerequisiti sono i seguenti:

Per coloro che non sono pratici con la programmazione Android con Eclipse, questo tutorial è un buon punto di partenza.

Un progetto Android è formato dalla seguente struttura e file:

  • la cartella /src, contiene l’Activity principale e la classe Java che definisce le firme delle funzioni della libreria modbus che verranno utilizzate nel progetto
  • nella cartella libs/armeabi, si trova la libreria modbus
  • la cartella res/layout, contiene il layout dell’Activity principale
  • le cartelle es/drawable-hdpi, res/drawable-ldpi, res/drawable-mdpi e res/drawable-xdpi, contiene le immagini delle lampadine gialle e rosse (fornite da Doublejdesign UK)
  • la cartella res/values, contiene il file string.xml
  • nella cartella root, ci sarà il file AndroidManifest.xml che definisce il progetto Android

In fondo alla pagina troverete un archivio con tutti i sorgenti del progetto di domotica.

L’interfaccia utente è visualizzata all’inizio di questo articolo. Questa contiene delle icone di lampadine gialle/rosse che ne indicano lo stato, 4 bottoni e 2 status analogici (per i sensori di temperatura) ed uno slider per controllare l’intensità di luce.

Nel blocco di codice che segue, vi presento il layout xml che è collegato alla main activity.
Ho utilizzato un AbsoluteLayout, anche se altri layout più responsive potevano essere usati, in modo da riutilizzare l’applicazione in device con diverse risoluzioni. Ho volutamente scelto di non concentrarmi sui aspetti di design delle interfacce, ma bensì sugli aspetti più tecnici di un progetto di domotica. Potreste essere voi, lettori, che attraverso la votra fantasia create la miglior interfaccia che risponda alle vostre necessità.

<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black">

<TextView
    android:id="@+id/textView2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="10dp"
    android:layout_marginTop="10dp"
    android:layout_x="12dp"
    android:layout_y="20dp"
    android:text="Switches status (Digital output):"
    android:textColor="@color/white"
    android:textColorHighlight="@color/white" />

<TextView
    android:id="@+id/textView1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="14dp"
    android:layout_marginRight="20dp"
    android:layout_x="376dp"
    android:layout_y="10dp"
    android:text="www.biemmeitalia.net"
    android:textColorHighlight="@color/white"
    android:textColor="@color/white"/>

<TextView
    android:id="@+id/textView3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="10dp"
    android:layout_marginTop="180dp"
    android:layout_x="12dp"
    android:layout_y="210dp"
    android:text="Analog input:"
    android:textColor="@color/white" />

<ProgressBar
    android:id="@+id/progressBar1"
    style="?android:attr/progressBarStyleHorizontal"
    android:layout_width="220dp"
    android:layout_height="wrap_content"
    android:layout_marginBottom="36dp"
    android:layout_marginLeft="22dp"
    android:layout_x="30dp"
    android:layout_y="236dp"
    android:max="1023" />

<ToggleButton
    android:id="@+id/toggleButton3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_x="282dp"
    android:layout_y="54dp"
    android:text="ToggleButton" />

<ToggleButton
    android:id="@+id/toggleButton2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_x="152dp"
    android:layout_y="54dp"
    android:text="ToggleButton" />

<ToggleButton
    android:id="@+id/toggleButton4"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_x="414dp"
    android:layout_y="54dp"
    android:text="ToggleButton" />

<ToggleButton
    android:id="@+id/toggleButton1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_x="34dp"
    android:layout_y="54dp"
    android:text="ToggleButton" />

<ImageView
    android:id="@+id/imageView1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_x="25dp"
    android:layout_y="152dp"
    android:src="@drawable/red_bulb" />

<TextView
    android:id="@+id/textView4"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="10dp"
    android:layout_marginTop="180dp"
    android:layout_x="274dp"
    android:layout_y="125dp"
    android:text="Light intensity (PWM):"
    android:textColor="@color/white" />

<TextView
    android:id="@+id/textView3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="10dp"
    android:layout_marginTop="115dp"
    android:layout_x="18dp"
    android:layout_y="121dp"
    android:text="Bulb status (Digital input):"
    android:textColor="@color/white" />

<ImageView
    android:id="@+id/imageView3"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_x="147dp"
    android:layout_y="152dp"
    android:src="@drawable/red_bulb" />

<ImageView
    android:id="@+id/imageView2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_x="82dp"
    android:layout_y="152dp"
    android:src="@drawable/yellow_bulb" />

<ImageView
    android:id="@+id/imageView4"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_x="205dp"
    android:layout_y="152dp"
    android:src="@drawable/red_bulb" />

<SeekBar
    android:id="@+id/seekBar1"
    android:layout_width="220dp"
    android:layout_height="wrap_content"
    android:layout_x="296dp"
    android:layout_y="157dp"
    android:max="128" />

<TextView
    android:id="@+id/temperature"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_x="395dp"
    android:layout_y="230dp"
    android:text="10 °C"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:textColor="@color/white" />

<TextView
    android:id="@+id/textView5"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_x="300dp"
    android:layout_y="235dp"
    android:text="Temperature:"
    android:textColor="@color/white" />

</AbsoluteLayout>

I riferimenti ai colori utilizzati in tutti il progetto sono memorizzati in string.xml. Mettetelo all’interno della cartella res/values del vostro progetto.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">BiemmeIOdemo</string>
    <color name="white">#FFFFFF</color>
    <color name="black">#000000</color>
</resources>

La classe speciale ModbusLib contiene semplicemente la firma delle funzioni modbus che possono essere utilizzate nel progetto Android e che sono definite all’interno della libreria nativa.

package com.biemme.iodemo;

public class ModbusLib {
        public native static long openCom();
        public native static long ReadHoldingRegisters(int fd, int id, int address, int no_of_registers,int []holdingRegs);
        public native static long WriteMultipleRegisters(int fd, int id, int address, int no_of_registers,int []holdingRegs);
        public native static long closeCom(int fd);
        static{
                System.loadLibrary("com_biemme_iodemo_ModbusLib");
        }
}

L’attività principale che sarà eseguita all’avvio del progetto si chiama MainActivity.java, ve ne commenterò i punti chiave. Se voleve vedere direttamente tutta la classe, troverete alla fine della pagina un archivio contenente tutto il progetto, completo di sorgenti.

Come si vede dalla firma, MainActivity estende la classe Activity per poter esser una finestra Android visualizzabile. Inoltre, implementa due interfacce OnClickListener e OnSeekBarChangeListener che gestiscono gli eventi del click sui bottoni e quelli sulla seekbar.

public class MainActivity extends Activity implements OnClickListener, OnSeekBarChangeListener

Il primo metodo che verrà eseguito in apertura della finestra è OnCreate. All’interno di questo blocco viene associato il content view (cioè il layout xml) e vengono memorizzati in attributi privati della classe i riferimenti agli elementi grafici della finestra quali bottoni, progress bar, image view ecc, in modo tale che le loro proprietà possano essere modificare in un secondo momento. Le chiamate al metodo setOnClickListener serviranno a registrare i singoli oggetti nel listener che gestisce il click (ciò vuol dire che quando un bottone viene premuto, il metodo onClick della classe viene eseguito).

protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	Log.d("msg","onCreate");
	setContentView(R.layout.activity_main);
	btn = new ToggleButton[4];
	inp = new ImageView[4];
	temp = (TextView) findViewById(R.id.temperature);
	setTitle("Biemme Android-Arduino via rs485, modbus");

	btn[0] = (ToggleButton) findViewById(R.id.toggleButton1);
	btn[1] = (ToggleButton) findViewById(R.id.toggleButton2);
	btn[2] = (ToggleButton) findViewById(R.id.toggleButton3);
	btn[3] = (ToggleButton) findViewById(R.id.toggleButton4);

	for (int i=0; i < pins; i++){
		btn[i].setOnClickListener(this);
	}
	prg1 = (ProgressBar) findViewById(R.id.progressBar1);
	skb1 = (SeekBar) findViewById(R.id.seekBar1);
	skb1.setOnSeekBarChangeListener(this);

	inp[0] = (ImageView) findViewById(R.id.imageView1);
	inp[1] = (ImageView) findViewById(R.id.imageView2);
	inp[2] = (ImageView) findViewById(R.id.imageView3);
	inp[3] = (ImageView) findViewById(R.id.imageView4);
}

Il medoto onResume sarà eseguito non appena la finestra viene caricata, ma dopo onCreate oppure dopo che è stata messa in pausa. Contiene il codice che apre la porta seriale del touch screen Android e memorizza il riferimento all’id del file in una variable privata della classe. Inoltre, inizia il refresh dello status dei bottoni, mandando delle richieste modbus agli slave.

protected void onResume() {
	super.onResume();
	Log.d("msg","onResume");
	if (fid == 0){
		fid = ModbusLib.openCom();
	}
	stopUpdates();
	startRefreshValues(0,0);
}

Il refresh dello stato dei bottoni, come detto in precedenza, viene gestito dal metodo startRefreshStatuses il quale creerà un task che sarà eseguito ad intervalli regolari. Il task a sua volta crea un oggetto come istanza dell’inner class updateView e dato che quest’ultima è un oggetto Runnable, potrà essere eseguita richiamando il metodo execute().

private void startRefreshStatuses(final long delay){
	TimerTask tt=new TimerTask() {

		public void run() {
            runOnUiThread(new Runnable() {
                public void run() {
                	new updateView().execute();
                }
            });
        }
	};
	t=new Timer();
	t.scheduleAtFixedRate(tt, delay, 500);

}

La classe updateView è un oggetto AsnyncTask (perchè lo estende) e per questo motivo deve implementare due metodi doInBackground e onPostExecute. Il primo sarà eseguito alla partenza del task ed il suo compito sarà di effettuare chiamate modbus ReadHoldingRegister (Function Code 3). Una volta terminato, il controllo viene passato al metodo onPostExecute il quale si preoccuperà di eseguire delle operazioni complementari al lavoro principale e soprattutto aggiornare i componenti grafici della maschera (infatti solo in quest’ultimo metodo si possono apportare queste modifche).

private class updateView extends AsyncTask<Integer, Integer, int[]>{

	private int[] holdingRegs;
	@Override
	protected void onPostExecute(int[] result) {
		if (result!=null){

			for (int i=0; i < 4; i++){
				if (btn[i].isChecked() && result[i+6]==0){
					btn[i].setChecked(false);
				}else if (!btn[i].isChecked() && result[i+6]==1){
					btn[i].setChecked(true);
				}

			}
			//Analog Input
			prg1.setIndeterminate(false);
			prg1.setProgress(result[0]);

			double temperatureC = (double)holdingRegs[1];
			temperatureC = (((temperatureC*5.0)/1024.0)-0.5)*100;

       			DecimalFormat df = new DecimalFormat("#.00");
			temp.setText(String.valueOf(df.format(temperatureC)+" °C"));

			//Digital Input
			for (int i=0; i < 4; i++){
				if (result[i+2]==1){
					inp[i].setImageResource(R.drawable.yellow_bulb);
				}else{
					inp[i].setImageResource(R.drawable.red_bulb);
				}
			}
			//PWM
			skb1.setProgress(result[9]);

		}
	}
	@Override
	protected int[] doInBackground(Integer... params) {

		try{
			int retries = 0;
			int no_of_registers;
			int node_to_write = 1;
			long bytes_received = 0;
			int starting_address = 0;
			holdingRegs = new int[35];

			no_of_registers = 10;

			do{
				bytes_received = ModbusLib.ReadHoldingRegisters((int)fid,node_to_write,starting_address,no_of_registers, holdingRegs);

				if (bytes_received>7){
					String s="("+String.valueOf(bytes_received) + ")";
				for (int i=0; i<no_of_registers; i++){
					s+=String.valueOf(holdingRegs[i]);
					s+=",";
				}
				Log.d("modbus3F:", s);
					return holdingRegs;
				}
				retries++;
			}while(bytes_received>0 || retries<5);

	}catch(Throwable t){
		Log.d("modbusERR", t.toString());
	}
		return null;
	}
}

Scrivere nei registri modbus di uno slave collegato al bus può essere fatto eseguendo il task denominato DelayedWrites, che ha una struttura simile al metodo updateView visto in precedenza.

private void DelayedWrites(final int address, final int value, final long delay){
	TimerTask tt=new TimerTask() {

		public void run() {
            runOnUiThread(new Runnable() {
                public void run() {
                	new WriteRegisters().execute(address, value);
                }
            });
        }
	};
	tW=new Timer();
	tW.schedule(tt, delay);
}

 

private class WriteRegisters extends AsyncTask<Integer, Void, int[]>{

	private int[] holdingRegs;
	@Override
	protected void onPostExecute(int[] result) {
		if (result!=null){
			//if the writes give no error, enable the buttons
			changeButtonsState(true);
			startRefreshValues(0,900);
		}
	}

	@Override
	protected int[] doInBackground(Integer... params) {

		try{

			int no_of_registers = 1;
			int node_to_write = 1;
			long bytes_received = 0;
			int starting_address = 1;
			holdingRegs = new int[35];
			int retries = 0;

			starting_address = params[0];		//address of the register to write
			holdingRegs[0]=params[1];		//value to write

			do{
				bytes_received = ModbusLib.WriteMultipleRegisters((int)fid, node_to_write, starting_address, 1, holdingRegs);
				retries++;

				if (bytes_received > 11){
					String s="("+String.valueOf(bytes_received) + ")";
					s+="["+ String.valueOf(calls++) +"]";
				for (int i=0; i<bytes_received; i++){
					s+=String.valueOf(holdingRegs[i]);
					s+=",";
				}
				Log.d("modbus16F:", s);
				return holdingRegs;
				}
			}while (bytes_received<=11 || retries<5);
			return null;

	}catch(Throwable t){
		Log.d("modbusERR", t.toString());
	}
		return null;
	}
}

Questo progetto Android demo che troverete nella nostra pagina GitHub vi fa vedere come accedere a registri di device remoti (non solamente Arduino, ma anche PLC) usando una libreria nativa per Android.

Disclaimer: La libreria modbus inclusa nel progetto funziona solamente con device che hanno id=1 e le richieste di lettura (ReadHoldingRegisters) restituiscono solo i primi 10 registri (da 0 a 9). Per quanto riguarda le scritture (WriteMultipleRegisters), solamente il primo registro può essere modificato. Per poter provare il progetto, estraete la cartella dall’archivio, create un nuovo progetto Android con Eclipse e specificate questa cartella come sorgente da cui importare il progetto.

Se vi è piaciuto l’articolo, per favore condividetelo!