blog:2020:0630_homectrl_project

Action disabled: register

Controlling our entrance gate from android app

Okay so, there is a “little home project” I would like to quickly setup here: we are currently using remote controls to open/close our entrance gate [just as most people do!]. But now I'm thinking I could try to push this a bit further and implement a custom mechanism to control the opening of the gate from the Internet éh éh éh [I know: not the most secured thing to do on earth, but we'll get back to that point another day].

If I can do that, then I could create a minimal android app that I could install on our family phones to open/close the gate with a simple button press! That would be great [and fun at the same time]… So where should we start ?

First thing first: I have a couple of spare raspberry pi units here, so I'll be using one of those as the “controller/server” in this project. Thus, I need to ensure I can access it remotely through ssh for quick configuration.

  • The current IP address for that device on my network is currently: 192.168.1.23
  • ⇒ let's make this a static DHCP assignment to IP 192.168.1.2 instead: Done
  • Now we check if we have an ssh server running there:
    ssh 192.168.1.2
  • And nope, lol it doesn't work. So I have to connect directly on the raspberry to install it. Done
  • Now to connect remotely from ssh we use the command:
    ssh pi@192.168.1.2
The default password for the pi user is “raspberry” in raspbian
  • Let's change the password just in case… Done

To be able to test the raspberry with a relay we need a minimal test circuit, let prepare that.

okay, so the circuit itself is pretty simple, with just one battery, one LED, one resistor and cables:

circuit_disconnected.jpg

circuit_connected.jpg

But now comes the funny part: how am I going to insert a relay in there (note: I only have a “naked” SRD-05VDC-SL-C component for the moment lol)

⇒ And I finally found some kind of datasheet for that component, it was not very clear but I could finally figure out how it works, and updated my little test circuit to include the relay, and this seems to work as expected:

relay_disabled.jpg

relay_enabled.jpg

As one can see on the picture above, I'm using the battery itself as power source for the relay… This is a 4.5V battery, but on raspberry I will only get 3.3V… let's hope this will still work!
And in fact when connected to the raspberry over ssh, we can use the command “pinout” to get a very useful mapping of the GPIO ports.
  • So I decided to connect on the pin GND and GPIO14, so here is the setup I have:

connected_to_raspberry.jpg

Good, now it's time to build a small program to try this thing :-)!

We have python 2.7.9 with pip 1.5.6 installed on the raspberry. I'm not quite sure I have the package “RPi.GPIO” installed by default, but anyway, let's just try the following minimal test program to see how this behaves first:

import RPi.GPIO as GPIO
from time import sleep
relay_pin = 
GPIO.setmode(GPIO.BCM)
GPIO.setup(relay_pin, GPIO.OUT)
GPIO.output(relay_pin, 1)
try:
    while True:
        GPIO.output(relay_pin, 0)
        sleep(5)
        GPIO.output(relay_pin, 1)
        sleep(5)
except KeyboardInterrupt:
  pass
GPIO.cleanup()

Hmmm…. that doesn't seem to work… sniff… let's try with some debug outputs… and also, I just read on this page that GPIO is only available as root. So I updated the script to:

import RPi.GPIO as GPIO
from time import sleep
relay_pin = 14
GPIO.setmode(GPIO.BCM)
GPIO.setup(relay_pin, GPIO.OUT)
GPIO.output(relay_pin, 1)
try:
    while True:
        print("Toggling pin to 0")
        GPIO.output(relay_pin, 0)
        sleep(5)
        print("Toggling pin to 1")
        GPIO.output(relay_pin, 1)
        sleep(5)
except KeyboardInterrupt:
  pass
GPIO.cleanup()

And now running as root:

sudo python main.py

Crap… :-(: So I can see the outputs, and when using a voltmeter I can see the voltage going from 0V to 3.3V and vice versa but the relay will not react at all :-(!

⇒ So I've been thinking about how to “workaround this problem” but unfortunately, I really cannot find anything that would make sense. Instead I think I should really just order a few SRD-03VDC-SL-C components from amazon and wait a couple of days befor continuing this little experiment!

[…3 days later…]

Hmmm! So…. I updated my test circuit to use a SRD-03VDC-SL-C component and… this is still not working ! :-| I mean, it's working fine when I use the GND pin with the 3V3 pin for instance. But as soon as I try to use a GPIO pin (+GND or +3V3) then it doesn't work anymore. How could that be ?!

… Oh… okay, I see now: just found this page: basically the problem is, we cannot draw any power from the GPIO pin itself (just about 16mA) (but we can draw much more from the 3V3 or 5V pins, that's why those pins will works!).

Conclusion then is: I need to place a transistor on the GPIO pin to control the load activation, as shown for instance on this page: https://raspberrypi.stackexchange.com/questions/41173/how-to-switch-on-off-a-circuit-using-gpio

Question then is of course: what kind of transistor should that be and do I have any of them available ? :-) Let's find out. I have:

  • LM350T (x2): these are “Three-Terminal Adjustable Output Positive Voltage Regulators”, not what I need.
  • [..And other stuff I didn't check because then I realized that…] Actually, maybe that's the point of buying a relay mounted on a PCB with other components ?!

⇒ And indeed, if we check the relay on PCB, there is a resistance R1, an transistor Q1, a LED, etc: this is definitely what we are after, so I tried using that and it just worked! In fact I can provide either 3V3 or 5V as input power and it works in both cases. All good!

relay_on_pcb.jpg

Now it's time to move online :-)!

I found this article which seems to be exactly what I want to do, so let's use that as a starting point.

  • First we install FLASK for python3 (actually it's already installed):
    sudo apt-get install python3-flask
  • Next we create a folder to store the project files:
    mkdir gatectrl
  • And we create the sub folders:
    cd gatectrl
    mkdir {static,templates}
  • We create a basic helloworld version app:
    vim server.py
  • Then we write the following python code:
    from flask import Flask
    app = Flask(__name__)
    @app.route('/')
    def index():
        return 'Hello world'
    if __name__ == '__main__':
        app.run(debug=True, port=80, host='0.0.0.0')
  • And we run this test app:
    sudo python3 server.py

OK we get our hello world message when navigating to the local page: http://192.168.1.2/

Now let's create a minimal HTML template templates/index.html with the following content:

<!DOCTYPE html>
   <head>
      <title>{{ title }}</title>
   </head>
   <body>
      <h1>Hello, World!</h1>
      <h2>The date and time on the server is: {{ time }}</h2>
   </body>
</html>

And we update our python script as follow:

from flask import Flask, render_template
import datetime
app = Flask(__name__)
@app.route("/")
def hello():
   now = datetime.datetime.now()
   timeString = now.strftime("%Y-%m-%d %H:%M")
   templateData = {
      'title' : 'HELLO!',
      'time': timeString
      }
   return render_template('index.html', **templateData)
if __name__ == "__main__":
   app.run(host='0.0.0.0', port=80, debug=True)

OK! Working just fine.

And finally we will now add support to perform a GPIO action with a button click. Let's update out python script as follow:

import RPi.GPIO as GPIO
from flask import Flask, render_template, request, redirect, url_for 
from time import sleep
import datetime

app = Flask(__name__)

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

#define actuators GPIOs
gatePin = 14

#initialize GPIO status variables
gateState = 0

# Define led pins as output
GPIO.setup(gatePin, GPIO.OUT)   

# turn relay OFF: 
GPIO.output(gatePin, GPIO.LOW)

@app.route("/")
def root():
  return redirect(url_for('index'))

@app.route("/index")
def index():
   now = datetime.datetime.now()
   timeString = now.strftime("%Y-%m-%d %H:%M")
   templateData = {
      'title' : 'HELLO!',
      'time': timeString
      }
   return render_template('index.html', **templateData)

@app.route("/<deviceName>/<action>")
def action(deviceName, action):
  global gateState
  if deviceName == 'entrance' and action == "trigger":
    if gateState == 0:
      print("Activating entrance.")
      GPIO.output(gatePin, GPIO.HIGH)
      gateState = 1
      sleep(0.5)
      GPIO.output(gatePin, GPIO.LOW)
      gateState = 0
    else:
      print("Entrance signal already triggered.")
  
  return redirect(url_for('index'))

if __name__ == "__main__":
   app.run(host='0.0.0.0', port=80, debug=True)

And we add the required button on our index page:

<!DOCTYPE html>
   <head>
      <title>GPIO Control</title>
      <link rel="stylesheet" href='../static/style.css'/>
   </head>
   <body>
    <h2> Commands </h2>
    <h3> 
      Entrance gate control: 
      <a href="/entrance/trigger" class="button">Activate</a>  
    </h3>
   </body>
</html>

And we also define the button class as follow:

.button {
  font: bold 15px Arial;
  text-decoration: none;
  background-color: #EEEEEE;
  color: #333333;
  border-radius: 3px;
  padding: 2px 6px 2px 6px;
  border-top: 1px solid #CCCCCC;
  border-right: 1px solid #333333;
  border-bottom: 1px solid #333333;
  border-left: 1px solid #CCCCCC;
}

⇒ Cool LOL! It's working here too: I can hear my relay clicking behing me when I click the button ;-)!

Editing 1 or 2 files through an ssh connection a couple of times is one thing, but trying to inject a lot of changes this way with tests and errors is becoming quite painful. So now, the next task that seems the most important to me is to figure out how to provide a shared folder access to my pi drive with samba. So let's get started:

  • Installing samba:
    sudo apt-get update
    sudo apt-get upgrade
    sudo apt-get install samba samba-common-bin
    mkdir /home/pi/shared
    sudo vim /etc/samba/smb.conf
    
    # Write the content:
    ### start of content ### 
    [shared]
    path = /home/pi/shared
    writeable=Yes
    create mask=0777
    directory mask=0777
    public=no
    ### end of content ### 
  • Add our pi user:
    sudo smbpasswd -a pi
  • Restart smbd:
    sudo systemctl restart smbd

⇒ And with that I can now connect to the shared folder \\192.168.1.2\shared: good! It should now be much easier to edit our python scripts using Visual Studio Code directly :-)

Using a post request

The above minimal webserver designed with Flask is working very well already. Yet, I would like to try something a little different now: currently I'm simply sending a standard “GET” request to perform our action, yet, I think it should also be possible to perform a “POST” request instead. And in that case, I could provide additional fields in the post data map.

So let's see how we could update the code to achieve this:

  • First I'm providing a dedicated implementation for the triggerGate action:
    def triggerGate():
      global gateState
      if gateState == 0:
        print("Activating entrance.")
        GPIO.output(gatePin, GPIO.HIGH)
        gateState = 1
        sleep(0.5)
        GPIO.output(gatePin, GPIO.LOW)
        gateState = 0
      else:
        print("Entrance signal already triggered.")
  • And then I implemented the Post action on the “/trigger” route:
    # Now trying to define a post request:
    # cf. https://stackoverflow.com/questions/22947905/flask-example-with-post
    @app.route("/trigger", methods = ['POST'])
    def on_trigger():
      if request.method == 'POST':
        data = request.form # a multidict containing POST data
        print("Received post data: %s" % data)
        dev = data.get('device')
        act = data.get('action')
        if dev == 'entrance' and act == "trigger":
          triggerGate()
          return jsonify(status = "OK")
        else:
          return jsonify(status = "Error", message = "Invalid device/action: dev=%s, act=%s" % (dev, act))
      else:
        print("not supported request method: %s" % request.method)
        return jsonify(status = "Error", message = "Invalid request method %s" % request.method)
See this page for reference.
  • And now I'm testing a post request with curl:
    curl -d "device=entrance&action=trigger" -X POST http://192.168.1.2/trigger

⇒ yeeeppeee! It works perfectly!

Continuing on our journey, now I want to create a minimal ionic app with a simple button to trigger the entrace gate activation action:

  • Preparing the ionic app:
    nv_call_ionic start homectrl blank
  • Then updating the ionic app to display a simple button and adding the following click handler:
      public activateEntranceGate()
      {
        console.log("Requesting entrance gate activation...");
        this.http.post<any>("http://192.168.1.2/trigger", {device: 'entrance', action: 'trigger'}).subscribe(data => {
          console.log("Received reply: "+JSON.stringify(data))
        })
      }

⇒ Unfortunately, this doesn't just work out of the box since we have a CORS issue:

Access to XMLHttpRequest at 'http://192.168.1.2/trigger' from origin 'http://localhost:8182' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

⇒ I think should install flask-cors to handle that.

  • Installing flask-cors:
    sudo pip3 install -U flask-cors
  • Now upgrading the server app to allow CORS:
    from flask_cors import CORS
    
    app = Flask(__name__)
    CORS(app)
  • OK The CORS error message is now fixed, but I get another error message coming from the handler I created:
    Received reply: {"message":"Invalid device/action: dev=None, act=None","status":"Error"}
  • So the arguments I'm providing are not recognized somehow… let's find out why!

⇒ Oh, okay, it seems that would be because flask is not parsing the data I send correctly and according to this page I should try using request.json directly. Let's have a try… OK!

  • But now I need to update my curl command accordingly:
    curl -H "Content-Type: application/json" -d '{"device": "entrance", "action":"trigger"}' -X POST http://192.168.1.2/trigger

Great! Both the curl command and ionic app are now working as expected ;-)!

  • To be able to convert our ionic app to an actual android app, I'm following this guide. First thing we need to do this is to install Android Studio: OK
  • Next we add capacitor to our ionic project:
    cd nodejs/ionic/homectrl
    nv_call_npm install @capacitor/core @capacitor/cli
  • We init capacitor on our project:
    nv_call_npx cap init "org.nervtech.homectrl" "NervHome Ctrl"
I must provide the required arguments directly on the command line for cap init because cygwin is detected as a “non-interactive shell”
  • ⇒ Hmmm… actually, the command above just doesn't work because I have a space in the app name and the arguments order is confusing. Thus I create a new shell function nv_cmd to open a cmd command prompt with the correct PATH in the current folder. Let's see how this work…
  • So I use the following to [try to] init capacitor:
    nv_cmd
    npx cap init
  • ⇒ Same error message:
    [error] Non-interactive shell detected. Run the command with --help to see a list of arguments that must be provided.
  • So I really need to open a new batch window… how can I do that ? ⇒ I updated my new script command as follow and this is now working:
    nv_cmd()
    {
      local pname="$(uname -s)"
      case "${pname}" in
      CYGWIN*)
        local PREVPATH="$PATH"
        local pdir="`nv_get_project_dir`/tools/windows/$__nv_tool_nodejs"
        export PATH="$pdir:$PATH"
        cmd /C "start cmd"
        export PATH="$PREVPATH"
        ;;
      *)
        echo "cmd not supported here!"
        ;;
      esac
    }
  • And I can finally setup capacitor:
    Microsoft Windows [Version 10.0.18362.900]
    (c) 2019 Microsoft Corporation. All rights reserved.
    
    W:\Projects\NervSeed\nodejs\ionic\homectrl>npx cap init
    ? App name NervHome Ctrl
    ? App Package ID (in Java package format, no dashes) org.nervtech.homectrl
    √ Initializing Capacitor project in W:\Projects\NervSeed\nodejs\ionic\homectrl in 14.82ms
    
    
    *   Your Capacitor project is ready to go!  *
    
    Add platforms using "npx cap add":
    
      npx cap add android
      npx cap add ios
      npx cap add electron
    
    Follow the Developer Workflow guide to get building:
    https://capacitor.ionicframework.com/docs/basics/workflow
  • Then I try to add the android target (still from the batch command prompt):
    npx cap add android
  • But this will produce an error message:
    W:\Projects\NervSeed\nodejs\ionic\homectrl>npx cap add android
    [error] Capacitor could not find the web assets directory "W:\Projects\NervSeed\nodejs\ionic\homectrl\www".
        Please create it, and make sure it has an index.html file. You can change
        the path of this directory in capacitor.config.json.
        More info: https://capacitor.ionicframework.com/docs/basics/configuring-your-app
  • ⇒ So I think I must somehow “build” the project first:
    ionic build
  • Then trying to add the android target again:
    W:\Projects\NervSeed\nodejs\ionic\homectrl>npx cap add android
    √ Installing android dependencies in 17.96s
    √ Adding native android project in: W:\Projects\NervSeed\nodejs\ionic\homectrl\android in 120.39ms
    √ Syncing Gradle in 472.60μp
    √ add in 18.09s
    √ Copying web assets from www to android\app\src\main\assets\public in 5.54s
    √ Copying native bridge in 34.14ms
    √ Copying capacitor.config.json in 12.36ms
    √ copy in 5.62s
    √ Updating Android plugins in 21.78ms
      Found 0 Capacitor plugins for android:
    √ update android in 181.62ms
    
    Now you can run npx cap open android to launch Android Studio
  • So let's open the project in Android Studio now as suggested:
    W:\Projects\NervSeed\nodejs\ionic\homectrl>npx cap open android
    [info] Opening Android project at W:\Projects\NervSeed\nodejs\ionic\homectrl\android
    [error] Android Studio not found. Make sure it's installed and configure "windowsAndroidStudioPath" in your capacitor.config.json to point to the location of studio64.exe, using JavaScript-escaped paths:
    Example:
    {
      "windowsAndroidStudioPath": "C:\\Program Files\\Android\\Android Studio\\bin\\studio64.exe"
    }
  • Hi hi hi, and of course the command above is failing, because I didn't specify anywhere where I have my android studio folder ;-) ⇒ Fixing that in the capacitor.config.json file, adding the line:
    "windowsAndroidStudioPath": "W:\\Apps\\android-studio\\bin\\studio64.exe"
  • Now trying again:
    W:\Projects\NervSeed\nodejs\ionic\homectrl>npx cap open android
    [info] Opening Android project at W:\Projects\NervSeed\nodejs\ionic\homectrl\android
  • This time Android Studio is started. And then we must wait because the gradle package is downloaded…
  • After waiting a bit the android project is built, but then it seems I get the error:
    Module 'app': platform 'android-29' not found.
  • ⇒ So let's see if I can install that “android-29” platform somehow… Yes, simply: “Tools -> SDK Manager”, and from there we install the correct package.
  • OK I could then rebuild the app (apparently ?) but I get one failing test:
    Delegate runner 'org.robolectric.RobolectricTestRunner' for AndroidJUnit4 could not be loaded.
  • Checking the application folder, I found an apk file in android\app\build\outputs\apk\debug, but obviously, this is a debug apk file. I wonder how I could build a release version ?
  • But first, I think I really need to run that app on a virtual device anyway: too many “open questions” left for me to keep moving forward blindly. ⇒ So I created a Virtual android device based on Android 6.0: and surprise! I could eventually run my minimal app in there:

homectrl_in_android_virtual_device.jpg

And guess what? Clicking on the button is still working just fine [I must be lucky today!] :-)!

  • So, next we need to select the release build variant: to do so, we simply select “release” for the app module from the “Build -> Select build variant…” menu, and then we rebuild the project (ie. “Build -> Make Project” or ctrl+F9)
  • Unfortunately once we switch to the release build variant we then get an error message when trying to run the resulting APK on the virtual device stating that “the apk is not signed” and we should specify “a signing configuration for this variant”
  • On this topic I found this page
    • We right click on the “app” module and we select “Open Module Settings”
    • Then on the “Signing Configs” tab we need to add a new signing config.
    • Note: I needed to create a new keystore so I added a new one as Security\android\nervtech.keystore in my personal files seafile repo (using “Build -> Generate Signed Bundle / APK…”)
  • ⇒ Now I have the signed APK file: android/app/release/app-release.apk
  • Let's try to install that on my phone now… Cool also working fine!
To install on android < 7.0 we really need to keep the signature v1 version enabled, otherwise the application will not install at all.

So our entrance gate control mechanism is working great, but for the moment, the API REST point is only reachable on our local network (ie. from http://192.168.1.2/trigger): not terribly useful… or, at least, this could be improved further if I could allow external access from my nginx server. Let's see how I can do that.

⇒ I just added the following location redirection in my api.nervtech.org site config:

    location /homectrl/ {
      proxy_set_header Host $host/homectrl;
      proxy_pass http://192.168.1.2:80/;
      proxy_redirect off;
    }

Then we test with a curl command:

curl -H "Content-Type: application/json" -d '{"device": "entrance", "action":"trigger"}' -X POST https://api.nervtech.org/homectrl/trigger

And victory! [It feels almost too easy…]

I should now update my ionic app accordingly. Done

⇒ See this page as reference.

  • Rebuilding/copying app data:
    nv_cmd
    ionic capacitor copy android
  • Then I replaced the icon of the app from inside Android Studio as suggested in the link just above,
  • And I manually rebuilt the splash screen for each possible resolution… tedious, but not impossible.
  • ⇒ rebuilding and signing, and hop! Now we are good to go! ;-)

Currently I'm manually starting the homectrl server with the command sudo python3 server.py, but instead I should really be starting that process automatically, and then monitor it to ensure it is always running.

So I will use the following script to do so (note: I renamed the “server.py” script to “homectrl.py” to make it less confusing):

#!/bin/bash

nump=$(ps -ef | grep '[/]home/pi/shared/apps/gatectrl/homectrl.py' | wc -l)
# echo "Num processes: ${nump}"

if [ $nump -eq 0 ]; then
  echo "Starting homectrl process."
  sudo python3 /home/pi/shared/apps/gatectrl/homectrl.py 
  echo "homectrl process done."
else
  echo "homectrl process already running."
fi

And then I should simply edit my crontab to execute that script regularly:

crontab -e
* * * * * bash /home/pi/shared/apps/gatectrl/run.sh 2>&1 1>>/home/pi/shared/apps/gatectrl/homectrl.log

OK! All good.

Last but not least, it would be very good to display a message inside our android application stating that the activation was done correctly or not, because otherwise, we are never quite sure if the network is very slow… Let's handle that.

⇒ I thus updated the code to display a toast message depending on the success of the request:

  async presentToast(msg:string, color:string) {
    this.toast = await this.tctrl.create({
      message: msg,
      duration: 3000,
      color: color
    });

    this.toast.present();
  }

And then when actually sending a request we do something with the result we get:

    this.http.post<any>(addr, {device: 'entrance', action: 'trigger'}).subscribe(data => {
      console.log("Received reply: "+JSON.stringify(data))
      
      this.requestSent = false;

      if(data['status'] == 'OK') {
        this.presentToast("Gate activated successfully.", "success");
      }
      else {
        this.presentToast(data['message'], "warning");
      }

    })
  • Now let's update the android app again:
    nv_cmd
    ionic capacitor copy android
  • And finally we rebuild the android app from Android Studio: Done

And with those latest changes when I click the “Activate” button on my android app, I eventually receive a toast indicating that the activation was done successfully! This is very handy to know exactly when your action was indeed taken into account and to avoid clicking too many times on the button because you are not sure anything is happening lol.

⇒ Anyway, I think I should stop here for this time since the application is working fine, and we will see in the coming days if there is anything to change!

Meanwhile, happy hacking everyone!

  • blog/2020/0630_homectrl_project.txt
  • Last modified: 2020/07/10 12:11
  • by 127.0.0.1