This week we are updating our Android tutorial. With the various Android updates, and more particularly of the SDK, some things were no longer up to date. Like the last time, we are going to write a small application that displays the value of a temperature sensor connected by USB.
Although this article is aimed at beginners, we assume that you already have some knowledge of Java and the Android architecture. If you have never written an Android app before, we suggest you start with the Google tutorial. Similarly, if you have never used a Yoctopuce module, it would be a good idea to start with the first articles in the series "For the beginners" , more particularly the one on the logical structure of Yoctopuce modules.
What is supported
The Yoctopuce library can be used on any phone or tablet since Android 4.0, which represents almost the entire Android market. However, to be able to access the USB port, the phone or tablet must support USB OTG mode (for USB on the Go). In this mode, the tablet powers the connected device. Although this feature is becoming more and more widespread, a large number of Android systems do not allow operation in this mode. There is no software way to determine if your system supports this mode. Sometimes it is indicated in the manufacturer's specifications, if this is not the case, you must contact the manufacturer or scour the forums to check.
Create the project
In this article, we used Android Studio version 2021.1.1. We'll start by creating a new project that contains a basic activity and layout.
We start with the basic Android Studio activity
We take this opportunity to modify the layout: We assign the ID temperature to the text field and we change its size to make it more readable.
<TextView
android:id="@+id/temperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Temperature"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintBottom_toTopOf="@id/button_first"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
...
Adding the Yoctopuce library
Before you can access Yoctopuce modules, you must add the Yoctopuce library for Android to the project. The easiest way is to add a reference to our library in the dependencies section of the build.gradle file. During compilation, Android Studio automatically downloads and includes the Yoctopuce library.
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.navigation:navigation-fragment:2.4.1'
implementation 'androidx.navigation:navigation-ui:2.4.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'com.yoctopuce.android:YoctoLib:46606'
}
It is recommended to specify the exact version number of the library to be used, this ensures that all the machines which compile the application use the same version of our library. Note that the Android Studio inspector warns you if a new version of our library is available.
Note: since the closing of JCenter you must use the mavenCentral repository. Google has updated the default Android Studio projects, but if you're starting from an old project make sure mavenCentral is present in the list of repositories:
Extract from build.gradle file:
repositories {
google()
mavenCentral()
}
...
Note that if you do not want to depend on a package manager, it is possible to manually add the files of our library to your project. All you have to do is to copy the content of the YoctoLib sub-directory of our library into your project.
Modifying AndroidManifest.xml file
For an Android application to be granted access to the USB port, you must declare this usage in the manifest. To do so, you must add the <uses-feature android:name="android.hardware.usb.host"/> line in the manifest section of the AndroidManifest.xml file.
You can also add an intent-filter named "android.hardware.usb.action.USB_DEVICE_ATTACHED" for the main activity to be automatically started when a module is connected to the USB port. This intent-filter needs a meta-data section pointing to an xml file listing the USB devices which start this application.
By default, Android creates a new instance of the activity each time a Yoctopuce module is detected. This makes managing the API initialization much more complex. To avoid this behavior, you can simply add the android:launchMode="singleInstance" attribute to the activity which is automatically started.
We must therefore modify the AndroidManifest.xml file so that it looks like this:
<manifest package="com.yoctopuce.myapplication"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.usb.host"/>
<app
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"/>
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter"/>
</activity>
</app>
</manifest>
And create a device_filter.xml file with the content below in the /src/main/res/xml directory of your project.
<resources>
<usb-device vendor-id="9440" />
</resources>
When these modifications are performed, Android automatically proposes to run the application when a Yoctopuce module is connected on the USB port of the phone.
When a Yoctopuce module is connected, Android offers to automatically start the application
Initializing the Yoctopuce API
You must implement the onStart() and onStop() methods of the fragment to start and stop the library.
In the onStart() method, you must call the YAPI.EnableUSBHost() method and pass to this function an object that inherits from the Context class.
You must then call the RegisterHub("usb") method to start detecting modules on the USB port. Finally, you must program the deferred execution of the _periodicUpdate object which we detail below.
public void onStart()
{
super.onStart();
try {
YAPI.EnableUSBHost(getContext());
YAPI.RegisterHub("usb");
} catch (YAPI_Exception e) {
Snackbar.make(binding.temperature,
"Error:" + e.getLocalizedMessage(),
Snackbar.LENGTH_INDEFINITE).show();
}
_handler.postDelayed(_periodicUpdate, 100);
}
In the onStop() method, you must suppress the scheduled executions of the _periodicUpdate object and call the YAPI.FreeAPI() method to stop the library and free the resources that were locked by the library.
public void onStop()
{
_handler.removeCallbacks(_periodicUpdate);
YAPI.FreeAPI();
super.onStop();
}
Updating the interface
To update the interface, we need to keep a reference to the text field of our layout and we also need a Handler which enables us to periodically call an object Runnable.
private Handler _handler;
@Override
publicView onCreateView(
LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
_handler = new Handler();
binding = FragmentFirstBinding.inflate(inflater, container, false);
return binding.getRoot();
}
Now that everything is in place, all that remains is to write the _periodicUpdate object. The run() method of this object must:
- Call YAPI.UpdateDeviceList every three seconds, which forces the API to enumerate the Yoctopuce modules. It's a heavy process and you must avoid doing too often.
- If no sensor is already in use, look for the first temperature sensor detected with the YTemperature.FirstTemperature() method.
- Update the text field with the current value of the temperature sensor. First, check that the module is still online with the isOnline() method, and if it is the case retrieve the current temperature with get_currentValue().
- Program the next run of this method with the postDelayed method.
Which translates into the following code:
private YTemperature_sensor;
private Runnable _periodicUpdate = new Runnable()
{
@Override
public void run()
{
try {
if (_hardwaredetect == 0) {
YAPI.UpdateDeviceList();
}
_hardwaredetect = (_hardwaredetect + 1) % 6;
if (_sensor == null) {
_sensor = YTemperature.FirstTemperature();
}
if (_sensor != null && _sensor.isOnline()) {
final String text = String.format(Locale.US, "%.2f %s",
_sensor.get_currentValue(), _sensor.get_unit());
binding.temperature.setText(text);
} else {
binding.temperature.setText("OFFLINE");
_sensor = null;
}
} catch (YAPI_Exception e) {
Snackbar.make(binding.temperature,
"Error:" + e.getLocalizedMessage(),
Snackbar.LENGTH_INDEFINITE).show();
}
_handler.postDelayed(_periodicUpdate, 500);
}
};
Once all these modifications are done, the application can be compiled and used on any phone or tablet that has USB on the Go support.
the Google demo application modified to use a Yoctopuce module
The source code of this application is available at this address: https://github.com/yoctopuce-examples/android_template_2022 .
Kotlin vs Java
In this article we have used the Java language but it is also possible to use Kotlin. The use of the Yoctopuce library is identical. The only pitfall is exception handling. The Kotlin compiler does not verify that Checked Exceptions are handled by the caller. It is therefore easier to forget to handle errors, such as the disconnection of a module when using our library. For your information, we have an article dedicated to using our library with Kotlin.
Debugging over WiFi
Most phones have only one USB port, so it is not possible to debug the application via USB when the Yoctopuce module is connected. Fortunately, there is a little-known trick: Debug the application through the WiFi network. The procedure is explained on this page.
A few remarks
We have deliberately kept this example very basic in order not to make this article too long and to allow beginners to get started easily, but for a more complete application it is preferable not to use the Yoctopuce library from the main thread. As we explained it in details in this post, it is more efficient to deport the code which uses the library in a thread that runs in the background. It is even mandatory if you have to use a Yoctopuce module which is connected to a YoctoHub. Unfortunately, it's also much more complicated, but you can take inspiration from the solution we used in this same post.
Of all the OSes that we support, Android is clearly the most difficult one to handle. The limited size of the screen, the different Android versions, the limited resources, the management of the different states of the application, and the difficulty to debug it make developing an application a long and complex task. Plan from the start a way to display and export debug logs, this will make your life easier the day you are in the middle of nowhere and the application doesn't work anymore. Trust me, I know it first hand :-)