DEV Community

Cover image for Creating a smart lock with Arduino and...Angular?!
Joshua Burleson
Joshua Burleson

Posted on

Creating a smart lock with Arduino and...Angular?!

Problem

The prevalence of remote work creates unique challenges and opportunities for software engineers and developers, sometimes with the former leading to the latter. I recently moved into a home where, for the first time, I had the opportunity to have my own office space which is great because my position is permanently remote. However, my office space is adjacent to the "play room" and secured with only french doors which are held closed with small, weak magnets attached to the top of the doors. This provides an irresistible opportunity for my 3 children, especially my toddler, to blast through these doors in dramatic fashion and start smacking my keyboard or attempting to steal items from my engineering workbench. Obviously this issue needed to be remedied for my productivity and my children's safety (soldering irons and heat guns are not as fun for toddlers).

Plan

Any reasonable person would clearly identify the solution is putting a lock on the door, and most would probably probably say the simpler the better. While I agree with sentiment of keeping things simple in most scenarios this particular scenario seemed like a perfect opportunity to try out using some of my newly purchased Arduino Nanos one of which features BLE capabilities which I hadn't, up to this point, worked with.
After an initial successful prototype development cycle with a standard Nano using a matrix keypad for external access and an ultrasonic proximity sensor for automated unlocking from the "secure" side of the door (All of which I will describe in my next article), I decided to check out BLE capabilities and add access via mobile device to the vectors for entry.

The lock on the "secure" side of the door in all its prototypical glory

Lock Prototype

Novel Challenges Presented

In order to achieve this I would need:

  • To develop a working knowledge of Bluetooth/BLE communication.
  • To develop a mobile client capable of communicating with the microcontroller for access control. (I've never developed a "native" application and don't have a working knowledge of Kotlin, Objective-C, or Swift).

I decided I needed to prioritize and set a scope for my goals related to this project. Developing a working knowledge of BLE was simple and reasonable; however, approaching the issue of creating a mobile client with native capabilities (using the devices BLE functionality) was a bit more complex I could either:

  • Setup environments for Android and iOS development and learn their respective languages, platforms, and best practices.
  • Respect that the goal is creating a specific client within a reasonable timeframe and identify a framework that will provide me with the ability to utilize native device features and create a minimalistic UI using languages and frameworks I'm familiar with.

I strongly believe in continuous personal and professional growth and identified that I probably should add familiarizing myself with standard native technologies and languages to my list of 2021 todos but decided that for now I would utilize technologies I'm more familiar with.

Research

Researching BLE communication was pretty straightforward and the most popular Arduino BLE library provides a nice, succinct introduction and is a great launchpad for learning.

Identifying frameworks for developing the mobile client was a bit more convoluted with several options available, the most popular included React Native, NativeScript, Ionic, and Flutter. Flutter required learning a new language, which while enticing, I already decided against. Ionic appeared very straightforward with some very attractive UI components however is clearly directed more toward hybrid apps and had more limited native interactivity than I was looking for so the decision came down to React Native and NativeScript.

NativeScript versus React Native

Picking the best of two evils?

Neither React Native nor NativeScript is truly native: they both provide wrappers in which we can use JS or TS to interact with native functionality and both provide UI components (or wrappers to native ones) and neither is as fast at runtime as writing an application in native code. That being said they both provide engineers and developers like myself, who aren't "mobile developers" by trade a great platform to create a mobile client. (I know, I know, lots of production mobile applications are written in both of these, especially React Native. I'm not trying to start a flame war but even Facebook only uses React Native sparingly in its mobile applications and Airbnb invested the time and money to move away from it to native code so ¯\(ツ)/¯ ).

Both frameworks allow you to use JSX-like syntax (with specific UI components as opposed to standard html-based JSX) and style as a "front-end" framework (although that's a bit of a misnomer in this context). NativeScript also provides support for Angular (not to be confused with AngularJS), Vue, Svelte, "vanilla" JS, and "vanilla" TS.

I have a good amount of professional experience with all of these, hold Vue and Svelte, so ultimately had to decide which FE framework I'd prefer for this project and which development experience seemed preferable. If this were simply a web-based client that required as little as this mobile client required I almost certainly would have gone with React or even "vanilla" TS since the overhead is notably lower and the technical complexity of the application doesn't on the surface denote that the extra "securities" provided by Angular (being an opinionated, MVVM framework). The popularity of React Native also provided a lot of allure. The deciding factor however, was BLE. BLE communication's asynchronous nature and the features of the client which were dependent, and independent of it, lead me to the determination that leveraging RxJS and Services in Angular made the most sense to develop this portion concisely; and so, the decision was made: NativeScript with Angular!

Building the Prototype #1: Arduino

I'm relatively new to C++ but even with factor in mind, writing the code for the Nano was pretty straightforward, especially with the documentation provided in the aforementioned BLE library. Creating a BLE "server", more frequently referred to as a peripheral device (and the mobile device referred to as a central device, which honestly kind of seems backward in my head, but I get it) is as easy defining services, their characteristics (read, write, notify, etc) and then doing some simple initialization. After that all you really need to do is work with the data to and from your device and perform your business logic as needed. Below is a very simple example of how you could start a service that allows a client to write to your peripheral which you then compare to a four digit code. [Note: No, this is not the code I used]

#include "Arduino.h"
#include "ArduinoBLE.h"

//Super secret BatCave access code
char insecureCode[4] = {'x','x','x','x'};

//Define Client Code Service
BLEService clientCodeService("180C");
BLEStringCharacteristic clientCode("2B56", BLEWrite, 14);

//Setup Bluetooth Connection
BLE.setLocalName( "InsecureDevice" ); //Advertised connection name
BLE.setAdvertisedService( clientCodeService );
clientCodeService.addCharacteristic( clientCode );
BLE.addService( clientCodeService );
clientCode.setValue( "0000" );

//Broadcast
BLE.advertise();

void resetClientCode() {
   clientCode.setValue( "0000" );
}

bool validCodeEntry() {
   for( int i = 0; i < 4; i++ ){
     if( clientCode.value()[i] != insecureCode[i] ) {
       clearClientCode();
       return false;
     }
   }
   resetClientCode();
   return true;
}

while( nothingStoppedYourDevice ){
  if( validCodeEntry() ){
    makeThePoorDecisionToTrustThatCodeAndProvideAccess();
  }
}
Enter fullscreen mode Exit fullscreen mode

Frustrating side note: The device I used for the "server" is the Nano Sense 33 BLE (which is capable of ML and very overkill for this use) does not have an EEPROM as other Nanos do and utilizing non-volatile memory, which I needed to write and read the actual access code on the device since I didn't want it hard-coded, was a bit of a learning curve of its own.

Building the Prototype #2: Angular Mobile Client

After verifying my nano code was working correctly and able to trigger the hardware to unlock the door with this super-handy BLE app from Nordic Semiconductor it was time to build the mobile client! Now, I'm an Android fanboy but I also wanted my client to work for iOS so I did the full setup described in NativeScript's documentation and I'll be honest, it took a little longer than I had hoped, needing to download both XCode and Android Studio as well as the standard NativeScript download and setup. That being said, once everything was up and running it was very easy to test across both device platforms (a bit easier with Android, of course). From a little bit of setup allows us to use the Angular CLI with NativeScript which, as anyone who uses Angular knows, is a huge help and greatly speeds up development.

I identified I would need 3 views (pages):

  • Home (which would direct the user to either the connection manager or lock access page dependent on their connected status to the Arduino [named AirLock]).

  • Connection Manager (which would allow the user to scan for relevant devices and connect or disconnect).

  • Lock Access Panel (where the user could submit a code to attempt access. I also determined I wanted to model the appearance of this page after the aforementioned matrix keypad).

Further I identified that I needed one service and would like 2 additional services:

  • Bluetooth Service (To handle connections and communication with the peripheral "server" and notify the UI components of updates, leveraging the NativeScript BLE package).

  • Storage Service (To cache recent connections to potentially speed up future connections).

  • Vibration Service (To provide tactile feedback to button presses through all UI components without writing redundant code).

Again, this was surprisingly simple, below are two methods from the BluetoothService class representing the simplicity of connecting to a BLE peripheral and writing a value to a service.

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { Bluetooth, ConnectOptions, Service, StartScanningOptions } from '@nativescript-community/ble';
import { BLESelection, Device } from '../models/ble.models';
import { StorageService } from './storage.service';

@Injectable({
  providedIn: 'root'
})
export class BluetoothService {
   private _ble: Bluetooth = new Bluetooth();

   private _connectedDevice: Device;
   public _connectedDevice$: BehaviorSubject<Device> = new BehaviorSubject(null);
   //...several other attributes and methods
  public async connect( uuid: string ): Promise<void> {
    const connectOptions: ConnectOptions = {
      UUID: uuid,
      onConnected: ( device ) => {
        this._codeSubmissionService = device.services.find( ( service: Service ) => service.UUID === "180c" );
        this._connectedDevice = device;
        this._connectedDevice$.next( this._connectedDevice );
        this._shiftSavedConnections( device.UUID );
      },
      onDisconnected: ( device ) => {
        console.log(`Successfully disconnected from ${device.UUID}`);
      }
    }
    await this._ble.connect( connectOptions );
  }

  public async submitAccessCode( code: string ): Promise<void> {
    if( !this._connectedDevice || !this._codeSubmissionService || !this._codeSubmissionService.characteristics.length )
      return;

    await this._ble.write({
      peripheralUUID: this._connectedDevice.UUID,
      serviceUUID: this._codeSubmissionService.UUID,
      characteristicUUID: this._codeSubmissionService.characteristics[0].UUID,
      value: code
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

Components and utilizing services was extremely easy as well. Occasionally disconnecting did not get caught by Angular as it normally would on a web client, making NgZone necessary in this rare case.

import { Component, NgZone, OnInit } from '@angular/core'
import { RadSideDrawer } from 'nativescript-ui-sidedrawer'
import { Application } from '@nativescript/core'
import { BluetoothService } from '../services/bluetooth.service';
import { Peripheral } from '@nativescript-community/ble';
import { BLESelection, Device } from '../models/ble.models';
import { RouterExtensions } from '@nativescript/angular';

@Component({
  selector: 'connection-manager',
  templateUrl: './connection-manager.component.html',
})
export class ConnectionManagerComponent implements OnInit {
  public loading: boolean = false;
  public initializedLoad: boolean = false;
  public availableDevices: Array<Peripheral> = [];
  public activeConnection: Device;

  constructor( private bleService: BluetoothService, private routerExtensions: RouterExtensions, private _ngZone: NgZone ) { }

  public scan(): void {
    this.loading = true;
    this.initializedLoad = true;
    this.bleService.scan();
  }

  public onUUIDTap( uuid: string ): void {
    this.loading = true;
    this.bleService.connect(uuid);
  }

  public async disconnect(): Promise<void> {
    await this.bleService.disconnect();
    this._ngZone.run(() => {});
  }

  ngOnInit(): void {

    this.bleService.currentConnectedDevice().subscribe( (connection: Device) => { 
      this.activeConnection = connection;
      this.loading = false;
      if( connection ) this.routerExtensions.navigate(['/featured'], {
        transition: { name: 'fade' }
      });
    });

    this.bleService.availableConnections().subscribe( ( connections: BLESelection ) => {
      this.availableDevices = Object.values( connections ).filter( ( device: Peripheral ) => device.localName === "Airlock" );
      if( this.initializedLoad ) this.loading = false;
      this._ngZone.run(() => {});
    });

  }

  onDrawerButtonTap(): void {
    const sideDrawer = <RadSideDrawer>Application.getRootView()
    sideDrawer.showDrawer()
  }
 //...ngOnDestroy stuff
}
Enter fullscreen mode Exit fullscreen mode

Below are some images of the client in action. In the end this worked great but I actually ended up swapping out the BLE-enabled nano for a standard one to try out some of its AI capabilities but plan on grabbing a non-"Sense" BLE Nano to replace it.

Connection Manager

Connection Manager Scan State View
Connection Manager Devices Found State View

Side Drawer

Side Drawer View

Access Panel/Code Entry

Access Panel View

Discussion (0)