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.
ssh 192.168.1.2
ssh pi@192.168.1.2
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:
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:
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:
⇒ 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!
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.
sudo apt-get install python3-flask
mkdir gatectrl
cd gatectrl mkdir {static,templates}
vim server.py
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')
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 ! 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:
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 ###
sudo smbpasswd -a pi
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
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:
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.")
# 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)
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:
nv_call_ionic start homectrl blank
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.
sudo pip3 install -U flask-cors
from flask_cors import CORS app = Flask(__name__) CORS(app)
Received reply: {"message":"Invalid device/action: dev=None, act=None","status":"Error"}
⇒ 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!
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 !
cd nodejs/ionic/homectrl nv_call_npm install @capacitor/core @capacitor/cli
nv_call_npx cap init "org.nervtech.homectrl" "NervHome Ctrl"
nv_cmd npx cap init
[error] Non-interactive shell detected. Run the command with --help to see a list of arguments that must be provided.
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 }
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
npx cap add android
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
ionic build
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
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" }
"windowsAndroidStudioPath": "W:\\Apps\\android-studio\\bin\\studio64.exe"
W:\Projects\NervSeed\nodejs\ionic\homectrl>npx cap open android [info] Opening Android project at W:\Projects\NervSeed\nodejs\ionic\homectrl\android
Module 'app': platform 'android-29' not found.
Delegate runner 'org.robolectric.RobolectricTestRunner' for AndroidJUnit4 could not be loaded.
And guess what? Clicking on the button is still working just fine [I must be lucky today!] !
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.
nv_cmd ionic capacitor copy android
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"); } })
nv_cmd ionic capacitor copy android
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!