Mechatronics Exercises

Workspace Navigation

Smart Curtains

Members: Teemu Sällylä

Yleiskuvaus

Projektin ideana oli luoda laite, joka mahdollistaa perinteisten verhojen ohjaamisen älykkäästi. Tarkoituksena oli, että laite olisi mahdollisimman yksinkertainen: sen voisi kiinnittää jo olemassa oleviin verhokiskoihin ilman tarvetta liimaamiselle tai reikien poraamiselle.

Päädyin projektissa käyttämään askelmoottoria ja hammashihnaa, joita ohjataan langattomaan verkkoon yhdistetyllä mikrokontrollerilla. Askelmoottori mahdollistaa verhojen ohjaamisen helposti ilman tarvetta ylimääräisille sensoreille. Lisäksi sen avulla verhojen sijaintia ja liikkumisnopeutta on mahdollista säätää ohjelmallisesti. Verhojen reuna on kiinnitetty hammashihnaan, joka kulkee verhokiskon vierellä.

Kommunikointi mikrokontrollerin kanssa tapahtuu langattoman verkon avulla sen yksinkertaisuuden ja toimintavarmuuden takia. Langattoman verkon avulla verhoja voi myös ohjata kodin ulkopuolelta sekä laajalti eri laitteilla ilman tarvetta asentaa ylimääräistä sovellusta. Ohjelmointirajapinnan johdosta verhojen ohjaamiseen on helppoa kehittää erillisiä ohjelmia. Projektissa tein verkkosivuun pohjautuvan käyttöliittymän, minkä lisäksi verhoja voi ääniohjata Google Assistantin kautta.

Komponentit + tarvikkeet





Elektroniikka

Tavoitteena oli mahdollisimman pieni ja yksinkertainen laite, joten komponenttien määrä pyrittiin minimoimaan, ja virtapiiri pitämään mahdollisimman yksinkertaisena. Toteutuksessa päädyttiin käyttämään askelmoottoria, sillä sen avulla verhojen sijaintia voi säätää tarkasti (esimerkiksi 75% kiinni) ja verhojen sijainti tiedetään ilman ylimääräisiä paikka- tai pyörimisantureita.

Mikrokontrollerin valinnassa päädyin Adafruit Feather Huzzah:iin. Laite on pienikokoinen, erittäin toimintavarma ja helppokäyttöinen, se sisältää paljon ominaisuuksia ja lisäksi sille on saatavilla paljon lisäosia. Askelmoottoriohjain valittiin lähinnä yhteensopivuuden moottorin kanssa. Valitun ohjaimen (Pololu 2134 DRV8834) avulla on lisäksi mahdollisuus rajoittaa moottorille kulkeutuvaa virtaa, mikä mahdollistaa suurempien jännitteiden käyttämisen moottorin kanssa.

Yksinkertaisen ratkaisun johdosta myös virtapiiri on yksinkertainen. Askelmoottori, joka saa virtansa hakkurilähteestä (tällä hetkellä 3.3 V), on kiinnitetty askelmoottoriohjaimeen. Ohjaimen DIR, STEP ja SLEEP pinnit on yhdistetty mikrokontrollerin pinneihin 5, 4 ja 2. Mikrokontrolleri saa virtansa USB-johdon kautta (5 V). Lisäksi mikrokontrollerin ja askelmoottoriohjaimen maat on yhdistetty.

Tulevaisuudessa tarkoituksena olisi vielä lisätä piiriin toinen askelmoottoriohjain, jotta systeemillä voisi ohjata kaksia verhoja samalla mikrokontrollerilla. Lisäksi optimitilanteessa koko systeemi (mikrokontrolleri + moottoriohjaimet) saisi virtansa yhdeltä virtalähteeltä. Tätä ennen systeemiä tulisi kuitenkin testata, jotta tiedetään tarvittava askelmoottorin jännite (myös mahdollisen suuremman hihnapyörän kanssa). Lisäksi tulevaisuudessa saatan vaihtaa mikrokontrollerin pienemään/halvempaan, sillä nykyisessä on paljon ominaisuuksia, joita ei tarvita verhojen ohjauksessa. Näiden jälkeen komponentit on mahdollista juottaa pysyvästi kytkentälevyyn.

Projektin kytkentäkaavio

Mekaniikka

Ideana oli 3d-tulostaa askelmoottorille yksinkertainen pidike (yksi yksinkertainen ratkaisu hahmoteltu vierellä), jonka voi liu’uttaa olemassa oleviin verhokiskoihin. Verhojen liikutus tapahtuisi hammashihnan avulla. Askelmoottorin akseliin on kiinnitetty hammashihnapyörä, ja toinen vapaasti pyörivä pyörä olisi kiinnitetty kiskon toiseen päähän. Hihna kulkee lähellä verhoja, joten reunimmaisen liukunipistimen kiinnitys hihnaan on helppoa. Kytkentälevy ohjaimineen olisi kiinnitettynä askelmoottoriin siinä jo olemassa olevien ruuvien avulla.

Kirjastojen ja yliopiston tilojen sulkeuduttua minulla ei kuitenkaan ollut mahdollisuutta 3d-tulostaa kiinnikettä, joten laite ei tullut täysin käyttökuntoon, vaikka loput komponenteista ovatkin valmiina. Lisäksi saattaa olla tarpeellista hankkia suuremmat hihnapyörät, jotta verhojen liike olisi hieman nopeampaa. Moottorin ohjaamisessa käytetään ¼-microstepping -asetusta värinän ja melun vähentämiseksi, joskin sitäkin joudutaan todennäköisesti vielä säätämään.

Hahmotelma yksinkertaisesta kiinnikkeestä moottorille

Ohjelmisto

Kommunikointi mikrokontrollerin kanssa tapahtuu langattoman verkon välityksellä. Kontrolleri toimii yksinkertaisena HTTP-palvelimena, ja komennot askelmoottorille tehdään HTTP GET -pyyntöjen avulla. Tämän ansiosta komentoja voi tehdä suoraan verkkoselaimen avulla, esimerkiksi verhojen avaus tapahtuu avaamalla sivu ”http://192.168.0.140/open” mikrokontrollerin IP-osoitteen ollessa 192.168.0.140. Rajapintaa on myös helppo kutsua muista ohjelmista ohjelmointikielestä riippumatta. Lisäksi rajapinta mahdollistaa ulkopuolisten palveluiden, kuten IFTTT:n yhdistämisen projektiin.

Mikrokontrollerin ohjelmisto koostuu pääosin kahdesta komponentista: moottoria ohjaavasta luokasta ja verkkopalvelinta käsittelevästä luokasta. Server-luokka käsittelee vastaanotetut yhteydet ja lähettää komentoja stepper-luokalle haetun URL-osoitteen perusteella. Stepper-luokka vastaa signaalien lähettämisestä askelmoottoriohjaimelle. Signaalien lähettämisessä saattaa kuitenkin kestää useampia sekunteja, jolloin koko mikrokontrollerin koodin suoritus on pysähtyneenä. Jotta palvelin pysyisi reaktiivisena, otetaan moottorin askeleet jaksoissa. Moottorille asetetaan tavoiteasema, ja se ottaa jokaisessa silmukassa 40 askelta kohti tavoitetta. Täten palvelin pystyy käsittelemään yhteyksiä samanaikaisesti moottorin pyöriessä, mikä myös mahdollistaa moottorin pysäyttämisen kesken ajon.

Lisäksi tein yksinkertaisen verkkosivun, jonka avulla verhoja on helppo ohjata käyttäen mitä tahansa puhelinta tai tietokonetta, ilman tarvetta ladata ylimääräistä sovellusta. Verkkosivu tekee rajapintakutsut käyttäen Javascriptin XMLHttpRequesteja käyttäjän painaessa eri nappeja. Sivulla on mahdollista muun muassa avata ja sulkea verhot sekä säätää tarkemmin verhojen sijaintia ja nopeutta.

Verhoja on myös mahdollista ohjata äänikomennoin käyttäen Google Assistantia, joka integroitiin projektiin IFTTT:n avulla. IFTTT:n applet käyttää Google Assistant ja webhooks-palveluita (say a simple phrase -> make a web request).


Käyttöliittymä_pieni.mp4

Käyttöliittymän esittely



Verkkopohjainen käyttöliittymä verhojen ohjaamiseksi

Toiminnassa.mp4

Projektin toimintaidean esittely ilmastointiteippikiinnityksen avulla

Koodi

Koodi löytyy (tulevine päivityksineen) Githubista


 curtain_main.ino
#include <ESP8266WiFi.h>
#include "curtain_server.h"
#include "curtain_stepper.h"

cStepper curtains(5, 4, 2);
cServer server(80, &curtains);

void setup() {
  Serial.begin(115200);
  delay(100);
  server.begin();
}

void loop() {
  server.handle();
  curtains.update();
}
 curtain_stepper.h
#ifndef curtain_stepper
#define curtain_stepper

#include "Arduino.h"

class cStepper {
    public:
        cStepper(int dir_pin, int step_pin, int sleep_pin);
        void rotateDeg(float deg, float speed);
        int update();
        int setTargetPos(int target_pos);
        int setSpeed(int speed);
        int getSpeed();
        int _cur_pos;
        int _target_pos;
        int _max_pos;
        int _min_pos;
        void enable();
        void disable();
    private:
        int _dir_pin;
        int _step_pin;
        int _sleep_pin;
        int _max_spd;
        int _min_spd;
        int _speed;
        float _speed_inv;
};

#endif
 curtain_stepper.cpp
#include "Arduino.h"
#include "curtain_stepper.h"

cStepper::cStepper(int dir_pin, int step_pin, int sleep_pin) {
    pinMode(dir_pin, OUTPUT);
    pinMode(step_pin, OUTPUT);
    pinMode(sleep_pin, OUTPUT);
    digitalWrite(sleep_pin, HIGH);
    _dir_pin = dir_pin;
    _step_pin = step_pin;
    _sleep_pin = sleep_pin;
    _max_pos = 50;
    _min_pos = -10;
    _cur_pos = 0;
    _speed = 60; // rotation speed in rounds per minute
    _speed_inv = (float) 1 / _speed;
    _max_spd = 150;
    _min_spd = 10;
}
void cStepper::rotateDeg(float deg, float speed) {
    if (deg > 0) digitalWrite(_dir_pin, HIGH);
    else digitalWrite(_dir_pin, LOW);
    int steps = abs(deg)*4/0.9;
    //float usDelay = (float) 1 / speed * 80;
    float usDelay = (float) 60 / 3200 * _speed_inv * 1e6;
    for (int i = 0; i < steps; i++) {
        digitalWrite(_step_pin, HIGH);
        delayMicroseconds(usDelay);
        digitalWrite(_step_pin, LOW);
        delayMicroseconds(usDelay);
    }
}
int cStepper::update() {
    int steps_per_pos = 40; // 40 steps = 9 degrees = 1 pos
    //float delay_us = (float) 1 / _speed * 80;
    if (_target_pos > _cur_pos) digitalWrite(_dir_pin, HIGH);
    else if (_target_pos < _cur_pos) digitalWrite(_dir_pin, LOW);
    else return 1;
    float delay_us = (float) 60 / 3200 * _speed_inv * 1e6;
    for (int i = 0; i < steps_per_pos; i++) {
        digitalWrite(_step_pin, HIGH);
        delayMicroseconds(delay_us);
        digitalWrite(_step_pin, LOW);
        delayMicroseconds(delay_us);
    }
    if (_target_pos > _cur_pos) _cur_pos++;
    else _cur_pos--;
    return 0;
}
int cStepper::setTargetPos(int target_pos) {
    if (target_pos != 1000 && target_pos != -1000)
        target_pos = max(_min_pos, min(_max_pos, target_pos));
    _target_pos = target_pos;
    return _target_pos;
}
void cStepper::disable() {
    digitalWrite(_sleep_pin, LOW);
}
void cStepper::enable() {
    digitalWrite(_sleep_pin, HIGH);
}
int cStepper::setSpeed(int speed) {
    _speed = max(_min_spd, min(_max_spd, speed));
    _speed_inv = (float) 1 / _speed;
    return _speed;
}
int cStepper::getSpeed() {
    return _speed;
}
 curtain_server.h
#ifndef curtain_server
#define curtain_server


#include <ESP8266WiFi.h>
#include "Arduino.h"
#include "curtain_stepper.h"
#include "curtain_led.h"

class cServer {
    public:
        cServer(int http_port, cStepper* stpr);
        void begin();
        void handle();
    private:
        void send200(WiFiClient client, String body);
        void send404(WiFiClient client);
        WiFiServer _server;
        Led _led;
        cStepper* _stepper;
};

#endif
 curtain_server.cpp
#include <ESP8266WiFi.h>
#include"Arduino.h"#include "curtain_server.h"
#include "curtain_led.h"
cServer::cServer(int http_port, cStepper* stpr)
    : _led(0), _server(http_port) {
        _stepper = stpr;
    }
void cServer::begin() {
    const char* ssid = "";
    const char* password = "";
    Serial.print("\n\nConnecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
  
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
        _led.toggle();
    }
    Serial.println("\nWiFi connected");  
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
    _server.begin();
}
void cServer::send200(WiFiClient client, String body) {
    client.print("HTTP/1.1 200 OK\r\n");
    client.print("Access-Control-Allow-Origin: *\r\n");
    client.print("\r\n");
    client.println(body);
}
void cServer::send404(WiFiClient client) {
  client.print("HTTP/1.1 404 Not Found\r\n");
  client.print("Access-Control-Allow-Origin: *\r\n");
  client.print("\r\n");
}
void cServer::handle(){
    WiFiClient client = _server.available();
    if (client) {
        if (client.connected()) {
            Serial.println("Connected to client");
            while (!client.available());
            String line = client.readStringUntil('\r');
            Serial.println(line);
    
            while (client.available()) client.read();
            String path = line.substring(
                line.indexOf(' ') + 1,
                line.lastIndexOf(' ')
            );
            String major_path = path, minor_path = "";
            if (path.indexOf('/', 1) != -1) {
                major_path = path.substring(
                    0,
                    path.indexOf('/', 1)
                );
                minor_path = path.substring(
                    path.indexOf('/', 1)
                );
            }
            Serial.print("Major path: ");
            Serial.println(major_path);
            Serial.print("Minor path: ");
            Serial.println(minor_path);
            if (path.equals("/on")){
                _led.turnOn();
                send200(client, "LED is on!");
            } else if (path.equals("/off")){
                _led.turnOff();
                send200(client, "LED is off!");
            } else if (path.equals("/toggle")){
                _led.toggle();
                String statusText = _led.status == LOW ? "ON" : "OFF";
                String msg = "LED toggled " + statusText;
                send200(client, msg);
            } else if (major_path.equals("/rotate")) {
                float deg = minor_path.substring(1).toFloat();
                if (deg != 0) _stepper->rotateDeg(deg, 0.3);
                send200(client, "Rotated");
            } else if (major_path.equals("/setTarget")) {
                int target = minor_path.substring(1).toInt();
                target = _stepper->setTargetPos(target);
                send200(client, "Targeting " + String(target));
            } else if (major_path.equals("/setPercentage")) {
                int target = minor_path.substring(1).toInt();
                int min = _stepper->_min_pos;
                int max = _stepper->_max_pos;
                target = (max - min) * target / 100 + min;
                target = _stepper->setTargetPos(target);
                send200(client, "Targeting " + String(target));
            } else if (path.equals("/close")) {
                _stepper->_target_pos = _stepper->_max_pos;
                send200(client, "Closing the curtains");
            } else if (path.equals("/open")) {
                _stepper->_target_pos = _stepper->_min_pos;
                send200(client, "Opening the curtains");
            } else if (path.equals("/stop")) {
                int pos = _stepper->_cur_pos;
                _stepper->_target_pos = pos;
                send200(client, "Stopping the curtains at position " + String(pos));
            } else if (major_path.equals("/setMax")) {
                int max = minor_path.substring(1).toInt();
                _stepper->_max_pos = max;
                send200(client, "Setting the maximum position to " + String(max));
            } else if (major_path.equals("/setMin")) {
                int min = minor_path.substring(1).toInt();
                _stepper->_min_pos = min;
                send200(client, "Setting the minimum position to " + String(min));
            } else if (path.equals("/setThisMax")) {
                int max = _stepper->_cur_pos;
                _stepper->_max_pos = max;
                send200(client, "Setting the maximum position to " + String(max));
            } else if (path.equals("/setThisMin")) {
                int min = _stepper->_cur_pos;
                _stepper->_min_pos = min;
                send200(client, "Setting the minimum position to " + String(min));
            } else if (major_path.equals("/setSpeed")) {
                int spd = minor_path.substring(1).toInt();
                spd = _stepper->setSpeed(spd);
                send200(client, "Setting the speed to " + String(spd));
            } else if (path.equals("/getSpeed")) {
                int spd = _stepper->getSpeed();
                send200(client, String(spd));
            } else if (path.equals("/getPos")) {
                int pos = _stepper->_cur_pos;
                send200(client, String(pos));
            } else if (path.equals("/getMax")) {
                int max = _stepper->_max_pos;
                send200(client, String(max));
            } else if (path.equals("/getMin")) {
                int min = _stepper->_min_pos;
                send200(client, String(min));
            } else if (path.equals("/getAll")) {
                String resp = String(_stepper->_cur_pos) + ",";
                resp += String(_stepper->_min_pos) + ",";
                resp += String(_stepper->_max_pos) + ",";
                resp += String(_stepper->getSpeed());
                send200(client, resp);
            } else if (path.equals("/enable")) {
                _stepper->enable();
                send200(client, "Stepper enabled");
            } else if (path.equals("/disable")) {
                _stepper->disable();
                send200(client, "Stepper disabled");
            } else {
                send404(client);
            }
            Serial.println("Response sent");
        }
        client.stop();
        Serial.println("client stopped");
    }
}
 curtain_led.h
#ifndef curtain_led
#define curtain_led

#include "Arduino.h"

// Led class mainly for debugging purposes
class Led {
    public:
        Led(int pin);
        void turnOn();
        void turnOff();
        void toggle();
        int status;
    private:
        int _pin;
};


#endif
 curtain_led.cpp
#include "Arduino.h"
#include "curtain_led.h"


Led::Led(int pin){
    pinMode(pin, OUTPUT);
    digitalWrite(pin, HIGH);
    _pin = pin;
    status = HIGH;
}

void Led::turnOn(){
    digitalWrite(_pin, LOW);
    status = LOW;
}

void Led::turnOff(){
    digitalWrite(_pin, HIGH);
    status = HIGH;
}

void Led::toggle(){
    status = !status;
    digitalWrite(_pin, status);
}
 index.html
<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width initial-scale=1 user-scalable=no">
        <style>
            div{
                max-width: 250px;
                margin-left: auto;
                margin-right: auto;
            }
            p{
                text-align: center;
                margin-bottom: 5px;
                margin-top: 15px;
            }
            .half-button{
                width: 49%;
                margin: auto;
            }
            .third-button{
                width: 32%;
                margin: auto;
                margin-top: 10px;
            }
        </style>
    </head>
    <body>
        <div id=main>
            <div id=open_close>
                <p>Open/close</p>
                <button id=open_button class=half-button>Open</button>
                <button id=close_button class=half-button>Close</button>
            </div>
            <div id=pos_div>
                <p>Position</p>
                <input id=pos_slider type=range style="width: 100%;">
                <button id=set_button style="width: 100%; margin-top: 5px;">Set position</button>


                <button id=left_button class=third-button>&lt;</button>
                <button id=stop_button class=third-button>Stop</button>
                <button id=right_button class=third-button>&gt;</button>
            </div>
            <div id=spd_div>
                <p>Speed</p>
                <input id=spd_slider type=range min="30" max="150" value="60" style="width: 100%;">
                <button id=set_speed style="width: 100%; margin-top: 5px;">Set speed</button>
            </div>
        </div>
        <script>
            const server_url = "http://192.168.0.142";
            var listener_mapping = {
                "open_button":  server_url + "/open",
                "close_button": server_url + "/close",
                "left_button":  server_url + "/setTarget/-1000",
                "stop_button":  server_url + "/stop",
                "right_button": server_url + "/setTarget/1000"
            };
            function request_page(url) {
                function resp() {
                    console.log("clicked");
                    var xhr = new XMLHttpRequest();
                    xhr.onreadystatechange = function() {
                        if (xhr.readyState === 4) {
                            console.log(xhr.response);
                        }
                    }
                    xhr.open("get", url, true);
                    xhr.send();
                }
                return resp;
            }
            function setPercent() {
                var inp = document.getElementById("pos_slider");
                var url = server_url + "/setPercentage/" + inp.value;
                request_page(url)();
            }
            function setSpeed() {
                var inp = document.getElementById("spd_slider");
                var url = server_url + "/setSpeed/" + inp.value;
                request_page(url)();
            }

            for (button_id in listener_mapping) {
                var btn = document.getElementById(button_id);
                btn.addEventListener("click", request_page(listener_mapping[button_id]));
            }
            
            document.getElementById("set_button").addEventListener("click", setPercent);
            document.getElementById("set_speed").addEventListener("click", setSpeed);

            document.getElementById("pos_slider").addEventListener("input", function (){
                var inp = document.getElementById("pos_slider");
                var btn = document.getElementById("set_button");
                btn.innerText = "Set position: " + inp.value + "%";
            });

            document.getElementById("spd_slider").addEventListener("input", function() {
                var inp = document.getElementById("spd_slider");
                var btn = document.getElementById("set_speed");
                btn.innerText = "Set speed: " + inp.value + " rpm";
            })

        </script>
    </body>
</html>

Pohdintaa

Alkuperäinen tavoite oli valmiit älyverhot. Tällä hetkellä projekti on ohjelmistoltaan jo valmis, elektroniikaltaan systeemi on toimintakunnossa ja mekaniikaltaan koossa on käyttövalmiuden mahdollistamat komponentit. Valitettavasti pandemian vuoksi projekti ei kuitenkaan tullut täysin käyttövalmiiksi, sillä kiinnikkeiden tulostaminen ei ollut mahdollista. Suurin työ on kuitenkin jo tehty ja käsillä on toimiva systeemi, joka on helppo saattaa loppuun 3d-tulostuksen ollessa taas mahdollista.

Loppujen lopuksi projekti oli varsin mielenkiintoinen. Projektin aikana komponentteja selaillessa syntyi useita uusia ideoita mekatroniikkaprojekteiksi. Lisäksi nyt tuli hankittua tarvittavia työkaluja (kuten kolvi ja yleismittari), minkä johdosta tulevaisuudessa kynnys uuden projektin aloittamiselle on matalampi.

Mitä tekisi toisin?

Olisin varmaan aloittanut aikaisemmin ja tehnyt ensimmäisinä 3d-tulostukset, jotta koronan vaikutus olisi ollut pienempi. Lisäksi olisi ollut mielenkiintoista päästä enemmän säätämään mekaniikan kanssa kuin mitä tässä projektissa pääsi.

  • No labels
  File Modified
Multimedia File 20200531_144004.mp4 May 31, 2020 by Teemu Sällylä
PNG File assembly.png May 31, 2020 by Teemu Sällylä
JPEG File Käyttöliittymä_lyhyt.jpg May 31, 2020 by Teemu Sällylä
Multimedia File Käyttöliittymä_pieni.mp4 May 31, 2020 by Teemu Sällylä
JPEG File Käyttöliittymä.jpg May 31, 2020 by Teemu Sällylä
PNG File Kokonaissysteemi.png May 31, 2020 by Teemu Sällylä
PNG File mekatroniikka_electric.png May 31, 2020 by Teemu Sällylä
Multimedia File Toiminnassa.mp4 May 31, 2020 by Teemu Sällylä