פרויקט קצר של שרת לקוח בשימוש ionic + node


לאחרונה, השתתפתי בהאקתון, הקמנו קבוצה ובנינו ב-24 שעות מערכת קטנה אבל מרשימה.
הרעיון היה, לבנות POC של מערכת שמדגימה הגנה ע"י smart card על מכשירי IOT (אם משהו לא מובן אפשר לשאול בתגובות).
לאחר שעות רבות של עבודה ואינספור באגים, הצלחנו להעמיד מערכת שמדגימה את זה בצורה ממש יפה ומרשימה. אני הייתי אחראי על פיתוח ה-client וה-server ולכן אתמקד בשני הנושאים האלו.
רק ניתן ספויילר שהמאמץ השתלם וזכינו במקום השני 😏.

Client

בצד של הקליינט כתבתי אפליקציית ionic ששולחת http post לשרת עם צמד של key ו-value.
התחלתי את הפיתוח עם template שנקרא sidemenu ע"י הפקודה:

ionic start myApp sidemenu

הפקודה הזו יוצרת פרויקט שמוכן לשימוש ויש בו שתי ספריות בתוך ספריית \src\pages\. ספריית home וספריית list.
ספריית home אחראית על המסך הראשון ועליה בעיקר עבדתי.
את הקובץ home.html שיניתי לקוד הבא:

home.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<ion-header>
  <ion-navbar>
    <button ion-button menuToggle>
      <ion-icon name="menu"></ion-icon>
    </button>
    <ion-title>Home</ion-title>
  </ion-navbar>
</ion-header>
<ion-content> <ion-list> <ion-item (click)="doorClicked()"> <ion-thumbnail item-start> <img src="assets/icon/door.jpg"> </ion-thumbnail> <h2>Door</h2> <p *ngIf="doorStatus==1">OPEN</p> <p *ngIf="doorStatus==0">CLOSE</p> </ion-item> <ion-item (click)="cameraClicked()"> <ion-thumbnail item-start> <img src="assets/icon/camera.jpg"> </ion-thumbnail> <h2>Camera</h2> <p *ngIf="cameraStatus==1">ON</p> <p *ngIf="cameraStatus==0">OFF</p> </ion-item> <ion-item (click)="safeboxClicked()"> <ion-thumbnail item-start> <img src="assets/icon/safebox.jpg"> </ion-thumbnail> <h2>Safe Box</h2> <p *ngIf="safeboxStatus==1">OPEN</p> <p *ngIf="safeboxStatus==0">CLOSE</p> </ion-item> <ion-item (click)="device4Clicked()"> <ion-thumbnail item-start> <img src="assets/icon/device4.jpg"> </ion-thumbnail> <h2>Device 4</h2> <p *ngIf="device4Status==1">ON</p> <p *ngIf="device4Status==0">OFF</p> </ion-item> </ion-list> </ion-content>

בקוד הזה יצרתי רשימה של 4 מכשירים שעליהם אני רוצה לשלוט. לכל אחד הוספתי תמונה קטנה, כותרת, ומצב (יכול להיות OPEN/CLOSE או ON/OFF). לחיצה על כל אחד מהמכשירים מפעילה פונקציה התואמת לאותו המכשיר.

להלן ה-GUI של האפליקציה:



את המימוש של הפונקציות ושל כל הלוגיקה כתבתי בקובץ home.ts.

home.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { HTTP } from '@ionic-native/http';
import { Http, Headers, RequestOptions } from '@angular/http';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/toPromise';
import { Injectable } from '@angular/core';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
@Injectable()
export class HomePage {
public doorStatus = 0;
public cameraStatus = 0;
public safeboxStatus = 0;
public device4Status = 0;
  constructor(public navCtrl: NavController, private http: Http) {        
  }

  doorClicked(){
    let status = 'a0';
    let headers = new Headers(
    {
      'Content-Type' : 'application/json'
    });
    let options = new RequestOptions({ headers: headers });
    if(this.doorStatus == 0){
      this.doorStatus = 1;
      status = 'a1';
    }
    else{
      this.doorStatus = 0;
      status = 'a0';
    }
    let data = JSON.stringify({
      t: status
    });

    return new Promise((resolve, reject) => {
      this.http.post('http://localhost:3000/', data, options)
      .toPromise()
      .then((response) =>
      {
        console.log('API Response : ', response.json());
        resolve(response.json());
      })
      .catch((error) =>
      {
        console.error('API Error : ');
      });
    });    
  }

  cameraClicked(){
    let status = 'b0';
    let headers = new Headers(
    {
      'Content-Type' : 'application/json'
    });
    let options = new RequestOptions({ headers: headers });

    if(this.cameraStatus == 0){
      this.cameraStatus = 1;
      status = 'b1';
    }
    else{
      this.cameraStatus = 0;
      status = 'b0';
    }

    let data = JSON.stringify({
      t: status
    });

    return new Promise((resolve, reject) => {
      this.http.post('http://localhost:3000/', data, options)
      .toPromise()
      .then((response) =>
      {
        console.log('API Response : ', response.json());
        resolve(response.json());
      })
      .catch((error) =>
      {
        console.error('API Error : ');
      });
    });    
  }
  
  safeboxClicked(){
    let status = 'c0';
    let headers = new Headers(
    {
      'Content-Type' : 'application/json'
    });
    let options = new RequestOptions({ headers: headers });

    if(this.safeboxStatus == 0){
      this.safeboxStatus = 1;
      status = 'c1';
    }
    else{
      this.safeboxStatus = 0;
      status = 'c0';
    }

    let data = JSON.stringify({
      t: status
    });

    return new Promise((resolve, reject) => {
      this.http.post('http://localhost:3000/', data, options)
      .toPromise()
      .then((response) =>
      {
        console.log('API Response : ', response.json());
        resolve(response.json());
      })
      .catch((error) =>
      {
        console.error('API Error : ');
      });
    });    
  }

  device4Clicked(){
    let status = 'd0';
    let headers = new Headers(
    {
      'Content-Type' : 'application/json'
    });
    let options = new RequestOptions({ headers: headers });

    if(this.device4Status == 0){
      this.device4Status = 1;
      status = 'd1';
    }
    else{
      this.device4Status = 0;
      status = 'd0';
    }

    let data = JSON.stringify({
      t: status
    });

    return new Promise((resolve, reject) => {
      this.http.post('http://localhost:3000/', data, options)
      .toPromise()
      .then((response) =>
      {
        console.log('API Response : ', response.json());
        resolve(response.json());
      })
      .catch((error) =>
      {
        console.error('API Error : ');
      });
    });    
  }
}

בתחילת הקוד יש לכל מכשיר משתנה ששומר את המצב של אותו מכשיר. למשל doorStatus קובע אם הדלת פתוחה (1) או סגורה (0).
לאחר מכן יש מימוש של כל ארבעת הפונקציות שראינו בקובץ ה-HTML. כל פונקציה עושה את אותה הלוגיקה למכשיר אחר. 
הלוגיקה מאוד פשוטה, בלחיצה על הכפתור המצב של מכשיר משתנה מ-0 ל-1 (או מ-1 ל-0). בנוסף נשלח http post שמכיל צמד של key ו-value. במקרה הזה ה-key הוא תמיד t. וה-value משתנה לפי המכשיר והמצב שלו.
עבור המכשיר הראשון השתמשתי ב-a עבור השני ב-b וכן הלאה. כדי לשלוח את מצב המכשיר השתמשתי ב-0 ו-1.
כך ש-a1 אומר שהמכשיר הראשון דולק ו-a0 אומר שהוא כבוי. b0 אומר שהמכשיר השני כבוי, וכן הלאה.
אני יודע שאפשר לקצר את הקוד וליפות אותו, אבל בהאקתון אין הרבה זמן, אז לא מבזבזים זמן על קוד יפה וקצר.
שימו לב שה-ip שבו השתמשתי הוא localhost. זה נכון כשמריצים את הלקוח והשרת על אותו מחשב. אם רוצים שהשרת יהיה במחשב אחר צריך לשנות את ה-localhost ל-ip האמיתי של השרת ולשים לב שאנחנו באותה הרשת.

קובץ נוסף שקצת נגעתי בו הוא app.module.ts. 
app.module.ts


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';

import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { ListPage } from '../pages/list/list';

import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';
import { HTTP } from '@ionic-native/http';
import {HttpModule} from '@angular/http';

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    ListPage
  ],
  imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp),
    HttpModule
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    ListPage
  ],
  providers: [
    StatusBar,
    SplashScreen,
    HTTP,
    
    {provide: ErrorHandler, useClass: IonicErrorHandler}
  ]
})
export class AppModule {}



מה שהוספתי מסומן בצהוב. אלו הם כמה הוספות קטנות לצורך תמיכה ב-http.

Server

את השרת כתבתי ב-node תוך שימוש ב-express.
כדי שהכל יעבוד כמו שצריך הייתי צריך להתקין כמה חבילות:


npm install express --save
npm install cors

ההתקנה של cors נועדה לאפשר לשרת לקבל קריאות http מדומיין אחר. ניתן לקרוא על זה בהרחבה כאן.

לקובץ קראתי listenerServer.js.
listenerServer.js


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var express = require('express');
var app = express();
var date = new Date();
var current_hour = date.getHours();
var cors = require('cors');
var fs = require('fs');
var bodyParser = require('body-parser');

app.use(cors());

app.listen(3000, function () {
  console.log('Example app listening on port 3000!');
});

app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());

app.post('/', function (req, res) {
                var t = req.param('t');
                console.log(req.body);
  
  console.log("post arrived "+ t + " from post ");
  res.send( t + " from post " + date );
  
  fs.appendFile('IotCommands.txt', 't = '+t+"\n", function (err) {
                  if (err) throw err;
                  console.log('Saved!');
                });
});

כפי שניתן לראות, השרת פשוט מאוד. הוא מאזין על פורט 3000. ברגע שהוא מקבל http post הוא מחלץ את הפרמטר t שנשלח אליו, מדפיס הודעה קצרה בקונסול, שולח response, והדבר החשוב לענייננו - שומר בקובץ שנקרא IotCommands.txt את המחרוזת של מה שהתקבל. לדוגמה:
t = a1
t = b0

כדי להריץ את השרת צריך פשוט לכתוב ב-cmd:


node listenerServer.js

בצורה הזו, ניתן להעביר לשרת כל מידע שנרצה.
במערכת שלנו רצה על השרת תוכנית שדוגמת את הקובץ כל שניה ולפי מה שכתוב בקובץ המערכת הדליקה וכיבתה נורות.

אני חושב שהקוד הזה יכול להוות בסיס לכל מיני יישומים של לקוח ושרת כשהלקוח הוא אפליקציה, והשרת נכתב בפשטות ומהירות.

מקווה שעזרתי למישהו. 
בהצלחה!