Our project is devised, designed and implemented by three students of Engineering in Computer Science at Sapienza University of Rome, as final project for IoT classroom.
Here you can find a summary slide presentation about it. And here you can find the link to our GitHub repository.
The problem space of our project is SmartCities and SocialNetworking, the idea in fact is to create a public place where you can leave in a secure way your belongings in order to go running in total freedom and also create a virtual social space where you can meet other people with your same interest in fitness. This secure public space is represented by a series of smart lockers: an user could book a locker placed in a public space. When he arrives to the place (i.e. a park) where the booked locker is, he can open (and close obviously) it using the fingerprint authentication on our application.
To achieve this result we have created an Android application with a very simple front-end, which trasmits all the data to a No-SQL database hosted by the Firebase platform. On the other hand, each locker is assigned to a Nucleo-64 STM32F401 board: in the actual implementation the nucleo board continuously pools the document on the db associated to the locker: in this way it could understand when the user wants to open or close the locker. The locker is opened by the board itself via a servo.
About the hardwareThe STM32 board is the main component of this project, hardware-wise. It provides an affordable and flexible way for users to try out new concepts and build prototypes. It is connected to a x-Nucleo-IDW01M1Wi-Fi extension board stacked on top, which allow for wireless communication. It is used right at startup, when the board ensures it is connected to a secure Wi-Fi network.
int connectWifi() {
int ret;
// get wifi interface
printf("\n\rGetting WiFi interface...\r\n");
wifi = WiFiInterface::get_default_instance();
if (!wifi) {
printf("No WiFi Interface\n\r");
return -1;
}
// connect to the wifi network
printf("\n\rConnecting to AP %s\n\r", MBED_CONF_APP_WIFI_SSID);
ret = wifi->connect(MBED_CONF_APP_WIFI_SSID, MBED_CONF_APP_WIFI_PASSWORD, NSAPI_SECURITY_WPA2);
if (ret!=0) {
printf("Wifi connection error: %d\n\r", ret);
return -1;
}
printf("IP address: %s\n\r", wifi->get_ip_address());
return 0;
}
The MBED_CONF_APP_WIFI_SSID and MBED_CONF_APP_WIFI_PASSWORD constants can be defined in the mbed_app.json file:
{
"config": {
"wifi-ssid": {
"help": "WiFi SSID",
"value": "\"PUT_YOUR_SSID\""
},
"wifi-password": {
"help": "WiFi Password",
"value": "\"PUT_YOUR_PASSWORD\""
}
},
//...
}
The last component is a servo motor, connected to D10 pin (PA_7) and operated via PWM, that will open and close the locker on demand. Based on the current status of the locker (open or closed) the servo will rotate clockwise or counterclockwise to lock/unlock the locker door.
void pressed(){
printf("Open the door\n");
myServo.period_ms(20);
myServo.pulsewidth_us(MID); //NB in microseconds
if(open_door == false){
for (int i=MIN;i<=MAX;i+=STEP){
myServo.pulsewidth_us(i);
wait_ms(TIME);
}
open_door = true;
}
else if(open_door == true){
for (int i=MAX;i>=MIN;i-=STEP){
myServo.pulsewidth_us(i);
wait_ms(TIME);
}
open_door = false;
}
myLed = !myLed;
wait(0.2);
myLed = !myLed;
}
The PWM constants are fine-tuned for the servo used for this project, with different servos results may vary.
While it's running the software periodically checks for changes in the Firebase DB (see next section). When it receives the response to the GET, it updates the the status of the locker if necessary. When the status changes the above shown function is called in order to open or close the locker.
int poolRequest(){
int ret;
//GET data
printf("\n\rTrying to fetch page...\n\r");
NetworkInterface* wifimodule = (NetworkInterface*) wifi;
HttpsRequest* request = new HttpsRequest(wifimodule, SSL_CA_PEM, HTTP_GET, "FIREBASE_URL");
HttpResponse* response;
response = request->send(NULL, 0); // execute the request
// analysis of the response
if (response!=NULL && response->get_status_code()==200) {
// getting the body
char* body_p = (char *) (response->get_body_as_string()).c_str();
int body_length = strlen(body_p);
// body too large
if (body_length >= 2048) {
printf("Body too large (>= 2048), ignoring the response\n\r");
ret = -1;
}
// compute the body
else {
strncpy(jsonSource, body_p, body_length); // copy the body to variable jsonSource
printf("Page fetched successfully - read %d characters\n\r", strlen(jsonSource));
ret = getLockerState(); // get "open" value of the locker from JSON file
if (ret!=0)
printf("Can't get locker state from JSON file, ignoring it\n\r");
else {
if (open_door != last_state) {
open_door = last_state;
printf("Move the servo!\n\r");
pressed();
}
else
printf("Locker state doesn't change\n\r");
}
}
}
else {
printf("HttpRequest failed (Error Code=%d)\n\r", request->get_error());
printf("Can't get data from DB\n\r");
ret = -1;
}
delete request;
return ret;
}
Since Firebase uses HTTPS protocol make sure you have a valid SSL certificate in PEM format (the one provided with our code is static and will eventually expire).
About FirebaseFirebase is a platform for mobile and web application development that has a lot of functionalities. such as the possibility to host your database. We use firebase because it offers a simple way to manage the most important back-end functionalities (such as the management of the users authentication and also the manipulation of data).
In order to create a database on the Firebase platform, the first thing you need to do is to create a new project on Firebase. After that you can enter the firebase console for your project:
In order to create a new authentication method (which can be supported by Firebase) you can click on Authentication (placed on the left of your console). After this, if you click on Access Method you will be redirected to the following page (where you can choose between different authentication methods):
For our project (as you can see) we have chosen Facebook and Email/Password methods. Now you need to enable both methods.
Once done that in your Android Studio project you can implement the following methods (in order to implement the authentication with Facebook).
loginButton = (LoginButton) findViewById(R.id.login_button);
loginButton.registerCallback(callbackManager,
new FacebookCallback<LoginResult>(){
@Override
public void onSuccess(LoginResult loginResult) {
handleFacebookAccessToken(loginResult.getAccessToken());
}
@Override
public void onCancel() {
info.setText("Login attempt canceled.");
}
@Override
public void onError(FacebookException error) {
info.setText("Login attempt failed.");
}
});
...
private void handleFacebookAccessToken(AccessToken token) {
AuthCredential credential = FacebookAuthProvider
.getCredential(token.getToken());
mAuth.signInWithCredential(credential)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
// Sign in success, update UI with the
// signed-in user's information
FirebaseUser user = mAuth.getCurrentUser();
handleResponse(user);
} else {
// If sign in fails,
// display a message to the user.
Toast.makeText(LoginActivity.this,
"Authentication failed.",Toast.LENGTH_SHORT).show();
handleResponse(null);
}
}
});
}
Obviously so that everything can work you need to create a new application on Facebook and enable authentication service (you can follow this guide from Facebook).
The method that you need to implement for the local signup and login are the followings:
SIGNUP
auth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(SignupActivity.this,
new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
Toast.makeText(SignupActivity.this,
"createUserWithEmail:onComplete:" + task.isSuccessful(),
Toast.LENGTH_SHORT).show();
// If sign in fails, display a message to the user.
// If sign in succeeds the auth state listener will
// be notified and logic to handle the signed in user
// can be handled in the listener.
if (!task.isSuccessful()) {
Toast.makeText(SignupActivity.this,
"Authentication failed." + task.getException(),
Toast.LENGTH_SHORT).show();
} else {
final FirebaseUser user = FirebaseAuth.getInstance()
.getCurrentUser();
UserProfileChangeRequest profileUpdates =
new UserProfileChangeRequest.Builder()
.setDisplayName(username)
.build();
user.updateProfile(profileUpdates)
.addOnCompleteListener(
new OnCompleteListener<Void>(){
@Override
public void onComplete(@NonNull Task<Void> task) {
if(task.isSuccessful()) {
addUser(user);
startActivity(new Intent(SignupActivity.this,
MainActivity.class));
finish();
}
}
});
}
}
});
LOGIN
auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(LocalLoginActivity.this,
new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (!task.isSuccessful()) {
// there was an error
if (password.length() < 6) {
inputPassword.setError("Password too short,
enter minimum 6 characters!");
} else {
Toast.makeText(LocalLoginActivity.this,
"The email/password are not correct",
Toast.LENGTH_LONG).show();
}
} else {
Intent intent = new Intent(LocalLoginActivity.this,
MainActivity.class);
startActivity(intent);
finish();
}
}
});
In order to get the current user you need only to do the following:
FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser();
// Get username
String username = user.getDisplayName();
// Get email (here only with local login)
String email = user.getEmail();
To sign out an user you can call:
FirebaseAuth.getInstance().signOut();
In Android we have two main methods to read data (one is for read data once and the other is to read data in realtime). In our application we use realtime reading for all the RecyclerView that are shown in the application (except for the one that shows you the people that have booked a locker near you). The method to read data once is the following:
private void getNearBookings(){
db.collection("bookings")
.whereEqualTo("park", parkName)
.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
@Override
public void onComplete(@NonNull Task<QuerySnapshot> task) {
for(QueryDocumentSnapshot doc : task.getResult()){
if(!doc.get("user").equals(user)){
nearYou.add(doc.toObject(Booking.class));
}
}
}
});
}
with this method you can get all the documents stored in the park
collection, where the park
attribute is equal to parkName
. If you want to get all the documents that are stored into park
, instead of .whereEqualTo(...)
you must use .get()
.
The method used for realtime updates is the following:
db.collection("bookings")
.whereEqualTo("user", user)
.addSnapshotListener(new EventListener<QuerySnapshot>() {
@Override
public void onEvent(@Nullable QuerySnapshot value,
@Nullable FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "Listen failed.", e);
return;
}
List<String> bookings = new ArrayList<>();
for (QueryDocumentSnapshot doc : value) {
bookings.add(doc.getString("parks"));
}
Log.d(TAG, "Current parks where user has booked a locker " +
"are: " + bookings);
}
});
In our project we have used a variant of this realtime reading, in order to display all the documents that match a query into a RecyclerView element (every time that in the application you see a card layout). This variant uses the Firebase Query object:
Query myBookings = db.collection("bookings").whereEqualTo("user", user);
The problem is that Cloud Firestore does not support the following types of queries:
- Queries with range filters on different fields
- Logical
OR
queries. In this case, you should create a separate query for eachOR
condition and merge the query results in your app. - Queries with a
!=
clause.
This means that you need to manipulate the results of the query client-side. The last point caused us many problems to return all the bookings of the users that are different from the one logged in.
Another problem is that, since we need to display results in realtime, the usual implementation of a RecyclerView is not sufficient in this case.
An example of implementation of the RecycleView is the following:
private void getUserBookings(){
Query query = db.collection("bookings")
.whereEqualTo("user", user);
FirestoreRecyclerOptions<Booking> response =
new FirestoreRecyclerOptions.Builder<Booking>()
.setQuery(query, Booking.class)
.build();
bookingAdapter = new FirestoreRecyclerAdapter<Booking, BookingHolder>
(response) {
@NonNull
@Override
public BookingHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
...
}
@Override
protected void onBindViewHolder(@NonNull BookingHolder
bookingHolder, int i, @NonNull final Booking booking) {
...
}
};
bookingAdapter.notifyDataSetChanged();
bookedRV.setAdapter(bookingAdapter);
}
public class BookingHolder extends RecyclerView.ViewHolder{
@BindView(R.id.parkTV)
TextView parkB;
@BindView(R.id.dateTV)
TextView dateB;
public BookingHolder(View itemView){
super(itemView);
ButterKnife.bind(this, itemView);
}
}
The method to write data is the following:
private void setLock(String lockName, String user){
Map<String, Object> lock = new HashMap<>();
lock.put("user", user);
lock.put("available", false);
lock.put("open", false);
String lockPark = park + lockName;
int lockHash = lockPark.hashCode();
db.collection("parks/"+park.hashCode()+"/lockers")
.document(Integer.toString(lockHash))
.set(lock, SetOptions.merge())
.addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "DocumentSnapshot successfully written!");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.w(TAG, "Error writing document", e);
}
});
}
From the Firebase console you can also manage your database (simply by clicking on Database in the left navigation bar. Our database is this:
For further information about Firebase Cloud Firestore database you can see this link.
The Android ApplicationWe have made a very simple Android application that works in this way:
- You can create a new account (which will be stored on Firebase), log in or use your Facebook account to access the application.
- In the home page you can see your bookings still active (if there are no bookings nothing will be shown).
- In the navigation bar (by clicking on the third icon) you will be redirected to a page where you can select a park from the ones that are stored on Firebase.
- Once selected the park where you want to go you will see a map that shows you its entrance. By clicking on SelectHour you can select the date of the running.
- Once clicked on the button you will be redirected to a page where you will be shown all the active bookings in that park. Now you can click on one of the shown bookings (in this way you will book a locker for the same date of the other user) or you can click on the Book button and select the hour and the date of the running.
- In both cases you will be redirected to a page where all the lockers are shown: the red ones are not available while the green ones are available.
- After selected the locker you will be redirected to the home page where you can see the booking just made
- To manage the locker you need to click on the correspondent booking on the home page: if you want to open/close the locker you must click on the correspondent button and, after you authenticate yourself with your fingerprint, the operation will be performed. When your running is over you simply press the LeaveLocker button to leave it and your booking will become disabled (in this case your booking will appear on the profile page); if you press the DeleteBooking you can delete the actual booking.
- On the profile page will be displayed all your disabled bookings: if you click on one of this you can see all its details
Comments