-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
630 lines (496 loc) · 272 KB
/
atom.xml
File metadata and controls
630 lines (496 loc) · 272 KB
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Anthroraemon | 人型叮當貓</title>
<link href="/atom.xml" rel="self"/>
<link href="http://xraywu.github.io/"/>
<updated>2018-02-15T07:01:48.969Z</updated>
<id>http://xraywu.github.io/</id>
<author>
<name>人型叮當貓</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>使用 ExpressVPN 和小米路由器搭建翻墙路由</title>
<link href="http://xraywu.github.io/2018/02/14/express-vpn-with-xiaomi-router/"/>
<id>http://xraywu.github.io/2018/02/14/express-vpn-with-xiaomi-router/</id>
<published>2018-02-14T06:06:16.000Z</published>
<updated>2018-02-15T07:01:48.969Z</updated>
<content type="html"><p><a href="https://www.expressvpn.com" target="_blank" rel="external">ExpressVPN</a>(需翻墙)是一家美国 VPN 供应商,提供同时在线 3 台设备、不限流量的高质量收费 VPN 服务。其服务器遍布全球,支持多种协议(PPTP、L2TP 或 OpenVPN)和多种设备客户端(包括移动、桌面、路由器甚至 PS 游戏机等)。简单来说,是市面上最好的收费 VPN 服务之一。当然,高质量的服务收费也很感人,一次性购买一年的费用高达 100 刀!不过,考虑这两年的国内网络环境,这样稳定的梯子实在是不可奢求更多。这不,前几天我就咬咬牙入手了一个账号,果然生活质量得到了极大的提高~</p>
<p>花了这么多钱买的梯子,自然想不只是简单在设备上简单使用啦(况且来回切换网络也很麻烦)。想了想,决定在家里原有路由器之外,再搭个翻墙路由,也好让买了许久,却因为网络环境吃灰许久的 Amazon Echo 能在无墙网络里用起来。看了一圈,决定入手个小米路由器3G。本以为其自带的 VPN 设置可以轻松完成任务,却发现小米路由器固件只支持简单的 L2TP 协议(都没有IPSec!)。折腾了两天,终于把它改造好了。下面做些记录,以备不时之需(你懂的)。</p>
<h3 id="获取小米路由器的-Root-权限"><a href="#获取小米路由器的-Root-权限" class="headerlink" title="获取小米路由器的 Root 权限"></a>获取小米路由器的 Root 权限</h3><p>小米路由器默认出厂刷的是稳定版,要获取 Root 权限,首先需要去其官网<a href="http://www1.miwifi.com/miwifi_download.html" target="_blank" rel="external">下载相应型号的最新开发版 ROM</a> 并刷机。下载完 ROM 后,从浏览器访问<code>192.168.3.1</code>进入小米路由器默认的管理后台,在右上角菜单中选择系统升级,并选择刚刚下载的升级包进行安装。安装完成后,等待路由器重启完成。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202018-02-15%20at%2010.21.16%20AM.png" alt="小米下载开发板ROM"></p>
<p>安装完开发版系统后,需要使用小米官方提供的 <a href="http://d.miwifi.com/rom/ssh" target="_blank" rel="external">Root 工具</a>。需要注意的是,下载链接需要通过小米路由器提供的网络进入,并且需要登录小米账号。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202018-02-15%20at%2010.21.21%20AM.png" alt="小米下载ROOT工具"></p>
<p>下载 Root 工具后,不要修改文件名(<code>miwifi.bin</code>),并且将文件拷到 U 盘中。将 U 盘插入小米路由器,断电,然后按住 Reset 键不放再恢复电源。按住 Reset 键并开机约 10 秒后(官网说 3-5 秒,实测需要按更长一些),会进入刷机模式,放开 Reset 键,等刷机完成后重新启动。重启完成后,路由器的 SSH 登录即被激活。此时,使用如下命令 SSH 远程连接设备若成功(Root 密码在下载 Root 工具时会在小米官网上显示),证明已经获得路由器 Root 权限。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ ssh [email protected]</div></pre></td></tr></table></figure>
<h3 id="刷入-Breed-启动器以及-Padavan-固件"><a href="#刷入-Breed-启动器以及-Padavan-固件" class="headerlink" title="刷入 Breed 启动器以及 Padavan 固件"></a>刷入 Breed 启动器以及 Padavan 固件</h3><p>为了能够刷入第三方固件,我们首先需要刷入 Bootloader。目前,<a href="https://breed.hackpascal.net/" target="_blank" rel="external">Breed</a>是小米路由器最常使用的启动器。进入上面的下载地址,找到相应型号的安装文件(比如小米路由器3G 对应 <code>breed-mt7621-xiaomi-r3g.bin</code>)并下载。下载后,将安装文件拷贝到路由器上——</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ scp ./breed-mt7621-xiaomi-r3g.bin [email protected]:/tmp/</div></pre></td></tr></table></figure>
<p>之后,由于在刷入 Bootloader 并进入控制台时会处于无网络状态,我们同样可以先准备好第三方固件。目前,一个被大家广泛使用的固件是<code>Padavan</code>,俗称“老毛子”固件。Padavan 固件功能非常强大,在这里,最重要的是其 VPN 客户端支持 <strong>OpenVPN</strong>。你可以在<a href="http://www.right.com.cn/forum/thread-161324-1-1.html" target="_blank" rel="external">这里</a>下载到由中国开发者汉化并扩展的最新版固件。</p>
<p>拷贝完成后,将小米路由器与其他路由设备(如光猫等)断开,并且使用网线,将路由器的 WAN 口与 PC 连接 <strong>(重要!)</strong>。</p>
<p>然后同样 SSH 远程登录路由器,并且执行如下命令:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">$ mtd -r write /tmp/breed-mt7621-xiaomi-r3g.bin Bootloader</div></pre></td></tr></table></figure>
<p>执行后,路由器会自动重启。重启后,和刷入 Root 时一样的操作,断电,按住 Reset 不放再开机等待 10 秒。完成后,可以进入刷机模式,通过 <code>192.168.1.1</code> 进入 Breed 控制台,就可以通过 Breed 刷入 Padavan 了。选择左边菜单栏中的<code>固件更新</code>,并且在<code>固件</code>一栏中选择前面下载的 Padavan ROM 安装包。确认更新后等待更新进度条完成,路由器会自动重启,就安装完成啦。此时,你可以将路由器重新连回光猫或外网接口,然后访问 <code>192.168.123.1</code> 进入 Padavan 的管理后台。Padavin 相关的默认密码如下——</p>
<ul>
<li>后台:admin@admin:192.168.123.1</li>
<li>SSH:admin@admin:192.168.123.1</li>
<li>Wifi 密码:1234567890</li>
</ul>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202018-02-15%20at%2010.18.38%20AM.png" alt="Breed 控制台"></p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202018-02-15%20at%2010.18.47%20AM.png" alt="Breed 刷入固件"></p>
<p>好了,下一步就是在 Padavan 控制台上配置 ExpressVPN 实现翻墙路由了!</p>
<h3 id="使用-OpenVPN-连接-ExpressVPN"><a href="#使用-OpenVPN-连接-ExpressVPN" class="headerlink" title="使用 OpenVPN 连接 ExpressVPN"></a>使用 OpenVPN 连接 ExpressVPN</h3><p>首先,在这里,我们假设你已经购买了 ExpressVPN 的服务。正如上文所说,基于 L2TP 协议的 VPN 在目前的网络状况下几乎没有成功连接的可能性。因此,在开始配置之前,你需要访问首先访问 ExpressVPN,获取你的 OpenVPN 信息。在<a href="https://www.expressvpn.com/setup#manual" target="_blank" rel="external">这个页面</a>,你能够找到你的 OpenVPN 账号和密码、ExpressVPN 各个服务器的 OpenVPN 配置文件(<code>.ovpn 文件</code>)、以及访问需要的证书和公私钥。记录或下载后,进入 Padavan 后台开始配置。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202018-02-14%20at%209.09.33%20PM.png" alt="获取ExpressVPN的OpenVPN文件"></p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202018-02-14%20at%209.10.09%20PM.png" alt="获取ExpressVPN的OpenVPN密钥和证书"></p>
<p>点击 Padavan 后台左侧菜单栏的<code>VPN Client</code>一项,选择<code>Enable VPN Client</code>。由于 Padavan 的 OpenVPN 客户端只支持手工配置,你需要从你下载的服务器配置文件中提取出服务器地址——使用任何文本编辑器即可打开配置文件,在文件中你可以找到相应的服务器地址,然后对以下选项做如下修改——</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line">- VPN Client Protocol: OpenVPN</div><div class="line">- Remote VPN Server (IP or DNS host): 服务器地址</div><div class="line">- Port: 1195 (ExpressVPN 没有使用默认的 1194 端口)</div><div class="line">- Authentication type: TLS: client.crt/client.key</div><div class="line">- Authentication Algorithm: SHA-512, 512 bit</div><div class="line">- Encryption Cipher Algorithm: AES, 256 bit</div><div class="line">- Obtaining DNS from VPN Server: Replacing all existing</div><div class="line">- Route All Traffic through the VPN interface: Yes</div></pre></td></tr></table></figure>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202018-02-15%20at%2010.12.20%20AM.png" alt="OpenVPN 配置1"></p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202018-02-15%20at%2010.12.29%20AM.png" alt="OpenVPN 配置2"></p>
<p>之后,还需要配置一些额外的参数。点击界面上的 <code>OpenVPN Extended Configuration</code> 链接,展开文本框中填入以下内容——</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line">remote-cert-tls server</div><div class="line">persist-key</div><div class="line">persist-tun</div><div class="line">fragment 1300</div><div class="line">mssfix 1450</div><div class="line">keysize 256</div><div class="line">auth-user-pass /etc/storage/auth.txt</div></pre></td></tr></table></figure>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202018-02-15%20at%2010.12.37%20AM.png" alt="OpenVPN 扩展配置"></p>
<p>完成后,先点击保存按钮。页面刷新后,点击页面的第二个 tab 选项(<code>OpenVPN Certificates &amp; Keys</code>),在四个文本框中<strong>依次</strong>填入下载的公私钥文件中的 <code>ca.crt</code>、<code>client.crt</code>、<code>client.key</code>、<code>ta.key</code> 文件中的内容,点击保存。</p>
<p>最后,由于 ExpressVPN 连接时除了公私钥还同时还需要用户名密码,我们需要把用户名密码写入上面扩展配置里指定的 <code>/etc/storage/auth.txt</code> 文件中。SSH 远程登录路由器后,新建该文件,并且将用户名密码作为内容输入(各一行),保存。完成后,回到管理后台,依次点击 <code>Administration</code> -&gt; <code>Settings</code> -&gt; <code>Commit Internal Storage to Flash Memory Now</code>,以防设备重启后丢失该文件。这样一来,OpenVPN 就配置完成啦。路由器此时会自动拨号连接,如果成功的话,在服务器右侧会有绿色的 <code>Connected</code> 提示,表明 VPN 就成功连接了。需要注意的是,尽管使用了 OpenVPN 协议,ExpressVPN 的不少服务器在国内依然很难连上。多试几个吧,毕竟 ExpressVPN 的服务器好多呢。</p>
</content>
<summary type="html">
<p><a href="https://www.expressvpn.com" target="_blank" rel="external">ExpressVPN</a>(需翻墙)是一家美国 VPN 供应商,提供同时在线 3 台设备、不限流量的高质量收费 VPN 服务。其服务器遍
</summary>
<category term="动动手" scheme="http://xraywu.github.io/categories/%E5%8A%A8%E5%8A%A8%E6%89%8B/"/>
<category term="VPN" scheme="http://xraywu.github.io/tags/VPN/"/>
<category term="OpenVPN" scheme="http://xraywu.github.io/tags/OpenVPN/"/>
<category term="Padavan" scheme="http://xraywu.github.io/tags/Padavan/"/>
<category term="ExpressVPN" scheme="http://xraywu.github.io/tags/ExpressVPN/"/>
<category term="路由器" scheme="http://xraywu.github.io/tags/%E8%B7%AF%E7%94%B1%E5%99%A8/"/>
<category term="小米路由器" scheme="http://xraywu.github.io/tags/%E5%B0%8F%E7%B1%B3%E8%B7%AF%E7%94%B1%E5%99%A8/"/>
</entry>
<entry>
<title>使用开源数据 BI 工具 Superset 对 Hive 仓储数据进行可视化分析</title>
<link href="http://xraywu.github.io/2017/08/06/an-introduction-to-superset/"/>
<id>http://xraywu.github.io/2017/08/06/an-introduction-to-superset/</id>
<published>2017-08-06T13:06:16.000Z</published>
<updated>2017-08-06T13:19:25.000Z</updated>
<content type="html"><p><a href="https://github.com/apache/incubator-superset" target="_blank" rel="external">Superset</a> (此前命名为<code>Caravel</code> 或 <code>Panoramix</code>)是由 Airbnb 开源,Apache 基金会孵化中的企业级 BI 工具,提供了一个基于 Web 的 BI 数据分析和可视化环境,并可连接多种不同数据源。当前,Superset 项目还在快速迭代中。不论是其官方文档还是中文社区,可以找到的资料和文档都比较分散,因此在使用中会有不少坑。因此,在这篇文章当中,我会介绍:1)Superset 的环境搭建,特别是如何连接 Hive 数据仓储;2)Superset 使用中的一些基本概念;3)Superset 与其他企业级 BI 工具,如 Tableau 之间的比较。</p>
<h3 id="Superset-的环境搭建与-Hive-连接"><a href="#Superset-的环境搭建与-Hive-连接" class="headerlink" title="Superset 的环境搭建与 Hive 连接"></a>Superset 的环境搭建与 Hive 连接</h3><p>Superset 的后端主要基于 Python Flask 框架开发,因此要安装 Superset 首先需要 Python 的运行环境。此外,Superset 对数据源的连接主要通过 Python 的 <code>SQLAlchemy</code> 包实现——也就是说,任何数据源只要有 SQLAlchemy 的方言实现即可。因此,尽管当前 Superset 官方并不支持对 Hive/Presto 的连接,但通过自行安装 Dropbox 开源的 <a href="https://github.com/dropbox/PyHive" target="_blank" rel="external">PyHive</a> (其中包含 Hive/Presto 的 SQLAlchemy 实现),即可达成目的。</p>
<p>通过这样的配置,在填写数据源地址时,通过下面的链接形式即可连接到 Hive/Presto (这里强烈建议使用 Presto,比起 Hive 原生的 MapReduce 要快一个数量级,而且配置相当简单。Airbnb 官方人员在 Superset 的 Github 项目上也是这么推荐的)。</p>
<ul>
<li>Hive</li>
</ul>
<p><code>hive://user:password@host:port/database</code></p>
<ul>
<li>Presto</li>
</ul>
<p><code>presto://user:password@host:port/hive/database</code></p>
<hr>
<p>下面就是一个基于 Docker 的环境搭建(参考了<a href="https://hub.docker.com/r/tyyzqmf/superset/" target="_blank" rel="external">这个</a>镜像,包括构建过程中需要的文件)——</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div></pre></td><td class="code"><pre><div class="line"># PyHive + Superset 当前在 Python3 下还不稳定</div><div class="line">FROM python:2.7</div><div class="line"></div><div class="line"># Configure environment</div><div class="line">ENV LANG=C.UTF-8 \</div><div class="line"> LC_ALL=C.UTF-8 \</div><div class="line"> PATH=$PATH:/home/superset/.bin \</div><div class="line"> PYTHONPATH=/home/superset/.superset:$PYTHONPATH \</div><div class="line"> SUPERSET_VERSION=0.19.0</div><div class="line"></div><div class="line">RUN apt-get update</div><div class="line">RUN apt-get install -y curl python-dev libmysqlclient-dev build-essential libsasl2-dev</div><div class="line">RUN pip install --upgrade pip</div><div class="line">RUN pip install superset mysqlclient ldap3 psycopg2 redis pythrifthiveapi pyhive flask-oauth flask_oauthlib flask-mail sqlalchemy-redshift</div><div class="line"></div><div class="line">RUN adduser superset &amp;&amp; \</div><div class="line"> mkdir /home/superset/.superset &amp;&amp; \</div><div class="line"> touch /home/superset/.superset/superset.db &amp;&amp; \</div><div class="line"> chown -R superset:superset /home/superset</div><div class="line"></div><div class="line"># Configure Filesysten</div><div class="line">WORKDIR /home/superset</div><div class="line">COPY superset .</div><div class="line">RUN chmod -R 777 .</div><div class="line">VOLUME /home/superset/.superset</div><div class="line"></div><div class="line"># Deploy application</div><div class="line">EXPOSE 8088</div><div class="line">HEALTHCHECK CMD [&quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:8088/health&quot;]</div><div class="line">ENTRYPOINT [&quot;superset&quot;]</div><div class="line">CMD [&quot;runserver&quot;]</div><div class="line">USER superset</div></pre></td></tr></table></figure>
<p>构建完毕后,即可启动环境——</p>
<ul>
<li>启动镜像</li>
</ul>
<p><code>docker run --detach --name superset -p 8088:8088 superset</code></p>
<ul>
<li>初始化账户</li>
</ul>
<p><code>docker exec -it superset superset-init</code></p>
<p>好了,打开你的浏览器,进入 <code>http://localhost:8088</code> 使用 Superset 吧!</p>
<h3 id="Superset-的一些基本概念"><a href="#Superset-的一些基本概念" class="headerlink" title="Superset 的一些基本概念"></a>Superset 的一些基本概念</h3><ul>
<li>Database:一个数据源,比如 Hive 里的一个数据库 </li>
<li>Table:查询数据表,目前在 Superset 中任何一张可视化图都是基于 <strong>一个</strong> Table的。也就是说,如果需要对连表数据进行可视化,需要先通过创建相应的 Query,并把 Query 结果转化为一个 Superset 中的 Table 才行。</li>
<li>Slice:基于 Table 的一个查询。任何可视化都是基于一个 Slice 的查询结果而构建的。</li>
<li>Visualization:基于一个 Slice 实现的可视化图,官方包自带了几乎所有的常见图标类型。</li>
<li>Dashboard:一组 Visualization 的集合。</li>
<li>Metrics:对聚合(Group)数据进行处理的方法——<strong>要注意,创建表时默认的 metrics 只有 Count 一种,这比较让人困惑,因为此时在 Group By 数据以后,在 y 轴上只能选择计数,而无法选择其他处理方式比如 Avg。解决方法是直接进入表编辑,通过 SQL 语法为其创建其他 metrics,再进入可视化编辑器选择。</strong></li>
</ul>
<p>了解了这些概念,在界面上点点用用,估计你就能掌握 Superset 的基本用法啦。具体我这里就不展开了,有问题可以参考<a href="https://superset.incubator.apache.org" target="_blank" rel="external">官方文档</a>。</p>
<h3 id="Superset-与其他企业级-BI-工具的比较"><a href="#Superset-与其他企业级-BI-工具的比较" class="headerlink" title="Superset 与其他企业级 BI 工具的比较"></a>Superset 与其他企业级 BI 工具的比较</h3><p>在当前版本(0.19.x),我认为,Superset 与其他企业级 BI 工具(如 Tableau、QlickView等)相比,最大的不同在于,<strong>其目标用户更多的是数据分析师、数据科学家,而非一般企业中的业务人员,因为其对用户的数据分析基本技能(如 SQL 等),要求更高。</strong></p>
<p>长久以来,在应用企业 BI 时,我认为业界一直有一种迷思,也就是希望业务人员有能力去操作 BI 软件(比如创建不同图表类型、对不同数据维度进行组合、下钻等)。为了尽可能的让他们获取这种能力,当前的 BI 软件都尽可能得做的“非常傻瓜”,比如只要通过拖拉即可组合数据维度、通过点击即可设置各种图表属性。尽管如此,但在实际情况中,根据我的观察,有这样能力的业务用户绝对不超过 5%,以至于在所谓的 “BI” 部门中,敢说自己精通公司 BI 软件的,可能也不超过三成(明明有 BI 工具和大量数据源,每个月最后还是在离线拉数据、做 Excel 报表)。此时,公司的 IT 部门会怎么做呢?答案是用乙方——基于 BI 软件再二次开发一套报表系统出来,当中内嵌好预先设定好的数据报表。在这样的情况下,无论软件如何“傻瓜化”,没有基础技能的用户仍然不会去使用,而对数据分析师、科学家来说,这样的傻瓜化所牺牲的灵活度,又显得过于小儿科了。</p>
<p>Superset 则索性走出了这一矛盾。可以说,如果你不具备一定的基础开发能力,是很难去使用 Superset 的。不管是基于已有表去创建新的表作为数据源,还是创建 metrics,都需要用户自行去定义一些 SQL 语句。把数据分析的活儿彻底交还给数据分析专家去吧——业务人员安安心心去看 Dashboard 就好。当然,你可以说这是因为 Superset 还在早期,暂时没有顾上这方面的需求,但我对这一观点存疑——从界面上无处不在的 SQL 一词即可看出,这不是 Superset 想要发展的方向。 </p>
<p>当然,Superset 目前仍然是一个快速发展中的孵化项目,未来变化也难预料。相比其他 BI 软件,坑较多,Bug 也不少,这都是上生产需要考虑的现实因素。奈何人家是开源的不是?比起 Tableau 动辄上万的 License(别误会,我是 Tableau 的大粉丝!),自己去填填坑也是应该的。</p>
<p>最后,发张 Demo 图吧。不好意思,好像打了很多码的样子 :)</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202017-08-05%20at%203.22.34%20PM.png" alt="Superset Demo"></p>
</content>
<summary type="html">
<p><a href="https://github.com/apache/incubator-superset" target="_blank" rel="external">Superset</a> (此前命名为<code>Caravel</code> 或 <code>Pan
</summary>
<category term="Visualization" scheme="http://xraywu.github.io/categories/Visualization/"/>
<category term="Visualization" scheme="http://xraywu.github.io/tags/Visualization/"/>
<category term="Superset" scheme="http://xraywu.github.io/tags/Superset/"/>
<category term="BI" scheme="http://xraywu.github.io/tags/BI/"/>
<category term="可视化" scheme="http://xraywu.github.io/tags/%E5%8F%AF%E8%A7%86%E5%8C%96/"/>
<category term="Hive" scheme="http://xraywu.github.io/tags/Hive/"/>
<category term="Presto" scheme="http://xraywu.github.io/tags/Presto/"/>
</entry>
<entry>
<title>关于“人人都是产品经理”的一点迷思</title>
<link href="http://xraywu.github.io/2017/07/13/not-everyone-can-become-a-pm/"/>
<id>http://xraywu.github.io/2017/07/13/not-everyone-can-become-a-pm/</id>
<published>2017-07-13T02:02:23.000Z</published>
<updated>2017-07-13T03:12:53.000Z</updated>
<content type="html"><p>这几年,随着互联网大潮的普及,加上某本著名的《人人都是产品经理》的流行,一瞬间,以<code>产品经理</code>为 title 的人好像多了起来。更有甚者,在公司里,不管你是做啥的,言不必谈几句产品就显得非常得不酷了……</p>
<p>然而,听多了各种所谓的对产品的建议,加上最近陆陆续续面试了不少“产品经理”,不得不感慨,各位还是不要侮辱<strong>产品</strong>这两个字了。不仅不是人人都能成为产品经理,更多的时候,你对所谓的产品的建议,基本就是bs…</p>
<p>来举几个例子 - </p>
<ol>
<li>面试某 candidate 时,说:<code>“我们的产品已经内部测试迭代两年了,还没有上线。”</code> 我:<code>“(WTF?) 那你们内部迭代了这么久,每次改版、优化的依据是啥?”</code>答:<code>我们通过自己调研、使用,发现问题,不断优化,blablabla。</code>我:<code>“(WTF?) 呃……”</code></li>
<li>公司推出了一个新功能,可以根据已知基因的用户数据推测其他家庭成员的遗传情况,初版设计的时候考虑了爷爷奶奶外公外婆爸爸妈妈兄弟姐妹,算法非常之牛逼……………………………………然而,没有孩子!没有孩子!没有孩子!为了弄明白到底是出于什么我考虑不到的牛逼原因,我去请教了下我们的产品经理。需求产品经理:<code>设计的时候没有考虑到还有孩子的情况。</code>。算法产品经理:<code>什么我的算法还能算孩子?</code>。我内心 OS:<code>“我就是个搞 IT 的,但我读了你的算法确实是可以算的……”</code></li>
<li>公司组织大家学习某产品分析视频,视频内通过<code>Design Thinking</code>的方法论框架对产品进行分析,方法运用熟练,分析结果一般。视频播放完毕,大家开始欢快的对某些分析结果中的细节开始各抒己见,完全忽略了整个分析的逻辑基础(比如基于<code>用户 persona</code>出发的分析)。</li>
</ol>
<p>拜托,我觉得各位产品经理还是要学习一个。</p>
<p>那么下面来说说我觉得够格的产品经理是怎么样的——</p>
<ol>
<li>你是张小龙</li>
<li>如果你不是张小龙,统计、数据建模、原型设计、用户调研方法、人机交互、心理学总得都略懂一些吧?就算你不会开发,相应的 IT 知识总得具备一些吧?要是都不懂,老老实实照着好的例子抄不要勇于发挥总可以了吧?</li>
</ol>
<p>写这么段话,可能会被认为我<em>愤世嫉俗</em>,又或者<em>刚愎自用</em>。可是拜托,古往今来,牛逼的产品经理本来都是<strong>大独裁者</strong>啊(比如很多人言必谈之的乔帮主)。</p>
<p>又比如我,从来不敢说自己是做“产品”的,因为自觉知识储备不够。有些人,真不知何来的自信呢?没人会对 IT 的工作指手画脚,也没人会对销售的工作指指点点。归根到底,大概是做产品太“容易”了吧。</p>
</content>
<summary type="html">
<p>这几年,随着互联网大潮的普及,加上某本著名的《人人都是产品经理》的流行,一瞬间,以<code>产品经理</code>为 title 的人好像多了起来。更有甚者,在公司里,不管你是做啥的,言不必谈几句产品就显得非常得不酷了……</p>
<p>然而,听多了各种所谓的对产品的建议
</summary>
<category term="人月神话" scheme="http://xraywu.github.io/categories/%E4%BA%BA%E6%9C%88%E7%A5%9E%E8%AF%9D/"/>
<category term="产品" scheme="http://xraywu.github.io/tags/%E4%BA%A7%E5%93%81/"/>
<category term="产品经理" scheme="http://xraywu.github.io/tags/%E4%BA%A7%E5%93%81%E7%BB%8F%E7%90%86/"/>
<category term="用户体验" scheme="http://xraywu.github.io/tags/%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C/"/>
</entry>
<entry>
<title>HDFS Docker 容器跨宿主机通信方式的解决方案</title>
<link href="http://xraywu.github.io/2017/02/18/docker-multi-host-networking-1/"/>
<id>http://xraywu.github.io/2017/02/18/docker-multi-host-networking-1/</id>
<published>2017-02-18T07:39:15.000Z</published>
<updated>2017-02-18T09:21:13.000Z</updated>
<content type="html"><p>公司本来有一套搭在单节点上的 Spark-HDFS 环境用于数据的离线计算和分析。在之前搭建的时候,为了运维和管理的方便,我们把所有依赖的服务都打包成了 Docker 镜像。部署之初,作为第一次运用容器到生产环境的我们虽然踩了不少坑,但一一解决之后,这套体系在生产中却基本没出过什么问题,运维也变得方便了不少——直到上周。因为公司业务规模变大,我们需要部署一个新的 HDFS 的 Datanode 节点到一台新的宿主机上。于是问题出现了,怎么能让新宿主机上的 HDFS Datanode 连接到原宿主机上的 HDFS Namenode 呢?为了解决这个问题,我们折腾了不少天,从 Docker 原生的网络解决方案到使用官方插件再到发现了神器 <strong>Weave</strong>,终于解决了这个问题。下面我们来看看各种方案的问题在哪里,以及是如何解决的。</p>
<h3 id="容器化的-HDFS-要如何工作?"><a href="#容器化的-HDFS-要如何工作?" class="headerlink" title="容器化的 HDFS 要如何工作?"></a>容器化的 HDFS 要如何工作?</h3><p>熟悉 HDFS 的同学们肯定知道,HDFS 有 Namenode (类似 master)及 Datanode (类似 slave)之分,它们之间通过指定 IP 的方式互相进行通讯,并在这过程中使用了 SSH 协议。如果我们的 Namenode 和 Datanode 直接在两台互通的宿主机上,那么事情非常简单——互相配好对面的 IP 地址,并各自打开 <code>22</code> 端口即可。在这个过程中,Namenode 还会定时向 Datanode 告知自己的 IP 地址。</p>
<p>然而如果它们分别在两个宿主机上的容器呢?事情就复杂了——正常情况下,两个宿主机上的容器是不能互相通讯的,不像一台宿主机上的两个容器,直接用 Docker 自带的 <code>--link</code> 参数就可以互相发现了。就这样,我们开始了艰难的网络调试……</p>
<h3 id="Docker-的默认网络模式为什么不行?"><a href="#Docker-的默认网络模式为什么不行?" class="headerlink" title="Docker 的默认网络模式为什么不行?"></a>Docker 的默认网络模式为什么不行?</h3><p>在 Docker 默认的网络模式下,每个容器有自己的内部 IP,在默认情况下互相之间不可通讯。但是,像上面说的,一台宿主机上的两个容器可以直接通过 <code>--link</code> 参数并指名来互相发现。而如果需要把一个容器内的服务暴露给外网使用,则需要通过端口映射将容器中的某个端口在宿主机上的某个端口暴露出来,外部用户通过宿主机的地址进行访问。咋一看之下,这似乎就能让两个宿主机上的两个容器通信了不是——分别暴露自己的端口给宿主机,然后两边通过宿主机的地址进行连接。看上去完全可行。</p>
<p>然而一实践,发现远没有那么简单——问题主要出在 HDFS的工作方式上。刚刚说过,Namenode 会向 Datanode 汇报自己的 IP,而一个容器中的Namenode会认为自己的 IP 是容器内部的 IP 地址,而不是宿主机的 IP,尽管我们在配置中已经给 Namenode 指定了 Datanode 所在宿主机的 IP。于是乎,我们的 Namenode 就错乱了……第一个方案就此枪毙。</p>
<h3 id="那么-Docker-的-Host-网络模式呢"><a href="#那么-Docker-的-Host-网络模式呢" class="headerlink" title="那么 Docker 的 Host 网络模式呢"></a>那么 Docker 的 Host 网络模式呢</h3><p>熟悉 Docker 的同学肯定会想,哎呀,Docker 还有好几种网络模式呢。不就是容器会分配一个内部 IP 么?我们用 Docker 的 Host 网络模式好啦——在 Host 模式下,启动的容器会直接使用宿主机的网络,并沿用宿主机 IP,同时会把容器使用的端口在宿主机上暴露。这样一来,互相指定宿主机 IP 不就没有上面说的这种问题了吗?可是新的问题又出现了。</p>
<p>这次问题仍然出现在 HDFS 的工作模式上——上面又又又说过了,Namenode 和 Datanode 是要用 SSH 协议通信的。使用了 Host 模式后,容器的 SSH 服务和宿主机的 SSH 服务又产生了冲突。当 Namenode 企图 SSH 到 Datanode 时,它只能访问到宿主机的 SSH 服务,而不是 Datanode 镜像的——我们总不能把宿主机的 SSH 服务停了吧?从实际角度,当然可以把宿主机的 SSH 端口给改了,但这显然不是一个好办法。</p>
<h3 id="再来看看-Docker-的多宿主机网络扩展方案"><a href="#再来看看-Docker-的多宿主机网络扩展方案" class="headerlink" title="再来看看 Docker 的多宿主机网络扩展方案"></a>再来看看 Docker 的多宿主机网络扩展方案</h3><p>原生的 Docker 网络解决方案看来是行不通了,于是我们开始查 Docker 的官方文档,发现现在 Docker 已经提供了多宿主机的网络扩展方案(<a href="https://docs.docker.com/engine/userguide/networking/get-started-overlay/" target="_blank" rel="external">文档</a>)了,又叫做 Overlay 模式。其大致的思路在于使用一个 Key-Value Store 的中间件注册不同宿主机上的容器,并使它们互相发现和链接。很好,很简单。</p>
<p>那就动手实践咯——按照文档里的说明,我们建了一个 Consule 的中间件,并建立了一个 overlay 网络供两边的容器加入,一切看上去都非常顺利,连通那是指日可待了,直到我们做完了所有步骤,开始 ping 的那一刻……</p>
<p>不通!不知道为什么,两边的容器甚至都已经能够解析对方 host 的虚拟 IP了,仍然不能连接到对方——使用 <code>docker network inspect</code> 查看网络,可以发现两个容器都已经注册成功了,因此它们互相能够知道对方的 host 名字。可是为啥连不通?至今我仍然不知道……我相信是我们在某些配置上出了问题,但是文档里也没有更详细的说明了,网上的教程搜了搜,好像也没找到类似问题…… 有了解的同学请留言告知……</p>
<h3 id="And-Weave-Works"><a href="#And-Weave-Works" class="headerlink" title="And Weave Works!"></a>And Weave Works!</h3><p>百般无奈之下,去求助了专门搞容器的朋友。人家说,你们还折腾什么劲啊?直接用 <a href="https://www.weave.works" target="_blank" rel="external">Weave</a> 就好啦,网上文章一搜一大把,很简单的……经过之前无数的挫折,其实我已然失去了信心……将信将疑的去试了试,嘿,成了,而且是如此简单。只恨动手前功课没做完整啊……</p>
<p>那么介绍一下 <code>Weave</code> 是啥——简单来说,Weave 通过创建虚拟子网的方式,将不同的宿主机上的容器置于同一个虚拟网络内。不同宿主机上的容器可以直接通过容器名或虚拟 IP 进行连接,就好像处在同一个物理网络下一样。与此同时,Weave 仍然支持所有的 Docker 原生功能,比如用 port mapping 暴露容器端口给宿主机等等。在整个 Weave 网络部署的过程中,也不需要对宿主机或容器的网络做任何修改,非常简便。就像官方说的一样,Weave Works!</p>
<p>具体的部署反而是非常简单的,我们基本就是按照<a href="http://tonylit.me/2016/03/29/docker-weave%E7%BD%91%E7%BB%9C%E4%BA%92%E8%BF%9E/" target="_blank" rel="external">这篇攻略</a>来进行的,没有做太多的变化。简单来说,就那么几步:</p>
<ol>
<li><a href="https://raw.githubusercontent.com/zettio/weave/master/weave" target="_blank" rel="external">下载</a> Weave 的执行脚本</li>
<li>在两台宿主机上分别启动 Weave 服务(<code>weave launch</code> 命令),在这个过程中,Weave 会自动在宿主机上配置一个虚拟网卡,以及下载启动一些 Docker 容器作为服务中间件。第二台宿主机launch Weave 时指定要加入的第一台宿主机 IP(<code>weave launch ip-of-server1</code>)。</li>
<li>在宿主机上分别用<code>weave run</code>启动容器,记得给容器一个名字。其他参数可以完全沿用 Docker 的参数。</li>
<li>到容器里 ping 对方看看吧,现在两个容器已经可以互相通信了,so easy!</li>
</ol>
<p>此外,Weave 也有更多的高级功能,比如容器可以实时加入或退出 Weave 网络,或者使用宿主机网络等。总的来说,使用 Weave 极度降低了 Docker 在多宿主机网络下使用的复杂度,对容器化 HDFS 等需要分布式环境、本身网络配置就比较复杂的服务有极大的帮助。</p>
</content>
<summary type="html">
<p>公司本来有一套搭在单节点上的 Spark-HDFS 环境用于数据的离线计算和分析。在之前搭建的时候,为了运维和管理的方便,我们把所有依赖的服务都打包成了 Docker 镜像。部署之初,作为第一次运用容器到生产环境的我们虽然踩了不少坑,但一一解决之后,这套体系在生产中却基本没
</summary>
<category term="Snippet" scheme="http://xraywu.github.io/categories/Snippet/"/>
<category term="Docker" scheme="http://xraywu.github.io/tags/Docker/"/>
<category term="Weave" scheme="http://xraywu.github.io/tags/Weave/"/>
<category term="容器" scheme="http://xraywu.github.io/tags/%E5%AE%B9%E5%99%A8/"/>
</entry>
<entry>
<title>在 TEDx 宁波演讲后的一些体会</title>
<link href="http://xraywu.github.io/2016/10/23/tedx-ningbo-reflection/"/>
<id>http://xraywu.github.io/2016/10/23/tedx-ningbo-reflection/</id>
<published>2016-10-23T10:34:56.000Z</published>
<updated>2016-10-23T11:44:11.000Z</updated>
<content type="html"><p>九月下旬的时候,应老板要求为宁波的 TEDx 组织做了关于基因检测及中国人群基因的演讲。这是我第一次正儿八经的对着一大堆观众做公众演讲(学术交流型的演讲不算)。现场大概 200 多个观众,还是买票的!当时真的是亚历山大,改稿、练习了好几天。今天视频被放出来了,自己回看了一遍表现还是觉得感受良多,在这里记录一下。</p>
<p>先放视频——</p>
<div class="owl-media owl-video owl-tencent"><embed src="http://static.video.qq.com/TPout.swf?vid=y03395s5faf&auto=0" type="application/x-shockwave-flash" quality="high" allowfullscreen="true" align="middle" allowscriptaccess="always"></div>
<p><br><br>看了视频,发现自己虽然没有伪装演说家成功,但表现好歹也不算太糟糕。不过,还是有几点值得去提高的——</p>
<ol>
<li>台风问题 —— 应 TedX 主办方的要求,在演讲时我刻意增加了一些走动和肢体语言。不过,看视频发现,虽然走动不少,到一个位置后几乎没有停留就往下一个地方走了,看上去有点无头苍蝇的感觉。停下来的时候身体摇晃也有点多。</li>
<li>随机应变能力不强 —— 其实整个演讲的稿件都是先写好的,也练了很久。最后当天演讲的时候,有几个地方有点忘了,结果没有能够随机应变,导致现场有些卡壳。虽然感觉我不是能够脱口而出的类型,未来在演讲时可能还是要先写好讲稿,但也要尽量锻炼自己不照本宣科的能力。希望可以从依赖讲稿进化到只依赖提纲/PPT再进阶到即兴(希望吧。。。)</li>
<li>演讲内容 —— TEDx 这样的公众演讲和在学术、技术会议上做交流完全不同。学术交流时有非常清晰的主线和内容,但 TEDx 演讲完全是在考验一个人讲好一个好故事的能力。在逻辑自洽的技术上还是要尽可能的增加趣味性、与观众互动。</li>
</ol>
<p>看完大概就这点感想 —— 虽然我不觉得自己会变成靠演讲吃饭的 type(每次上台下来以后都会胃抽筋),但演讲能力还是很实用的 skill,未来有机会还是要多讲多练吧。</p>
<p>最后说说 TEDx —— TEDx 是 TED 官方<strong>认证</strong>,但不属于 TED 官方活动的志愿者活动。我当天参加的活动是 TEDx 宁波 2016 年的年度大会,活动规模非常大,几百位观众、十多位讲者,但完全是由志愿者无偿组织的,让人非常钦佩。在场也有很多大牛讲者,和我这种去蹭讲的完全不一样,听了他们的演讲以后可谓受益良多,也知道了自己和真正的领域大牛们有多大的差距,千万不可自满!</p>
</content>
<summary type="html">
<p>九月下旬的时候,应老板要求为宁波的 TEDx 组织做了关于基因检测及中国人群基因的演讲。这是我第一次正儿八经的对着一大堆观众做公众演讲(学术交流型的演讲不算)。现场大概 200 多个观众,还是买票的!当时真的是亚历山大,改稿、练习了好几天。今天视频被放出来了,自己回看了一遍
</summary>
<category term="Irrelevant" scheme="http://xraywu.github.io/categories/Irrelevant/"/>
<category term="公众演讲" scheme="http://xraywu.github.io/tags/%E5%85%AC%E4%BC%97%E6%BC%94%E8%AE%B2/"/>
<category term="TEDx" scheme="http://xraywu.github.io/tags/TEDx/"/>
<category term="Public Speaking" scheme="http://xraywu.github.io/tags/Public-Speaking/"/>
<category term="Gene" scheme="http://xraywu.github.io/tags/Gene/"/>
</entry>
<entry>
<title>使用AWS IoT、Lambda及ML打造智能化设备(三)</title>
<link href="http://xraywu.github.io/2016/09/24/aws-iot-lambda-3/"/>
<id>http://xraywu.github.io/2016/09/24/aws-iot-lambda-3/</id>
<published>2016-09-24T04:09:15.000Z</published>
<updated>2016-09-24T04:19:48.000Z</updated>
<content type="html"><p>在本系列的上两篇文章中,我们介绍了如何利用 AWS IoT 打造一个可以远程操作的智能照相机并且将拍摄的照片数据通过 IoT 消息队列进入 AWS Lambda 进行处理的过程。在这篇文章中,我将着重介绍当数据进入 Lambda 后是如何被处理的。</p>
<p>先简单介绍一下 AWS 的 Lambda 服务 —— Lambda 作为一种无后台服务(server-less service)的实现被 AWS 于 2014年提出以后,在近两年的时间里得到了迅猛的发展。通过 Lambda,我们可以将一个计算/服务对应的函数(function)上传到 AWS 上进行调用,而无需管理该该函数运行的环境。简单来说,用户只需要实现计算的实现,而不用再关注架构、资源等,而计价则以函数的执行次数和实际消耗资源为准。目前,Lambda 支持运行多种语言的函数,包括 Node.js, Python 或 Java 等。其中,需要注意的是,每个任务的运行时间不能超过5分钟。</p>
<p>在系列<a href="http://xraywu.github.io/2016/02/17/aws-iot-lambda-1/">第一篇</a>文章中,我们已经提到需要创建一个 Lambda 资源,现在就让我们看看到底要怎么做。从控制台进入 Lambda 后,点击 <code>Create a Lambda Function</code>(或者和本系列第一篇一样,直接从 Iot 创建界面中进入),此时会进入模板选择页面。这里 AWS 为我们提供了许多常见函数的模板,比如从 S3 读取 Object 的操作等,先点击 <code>Skip</code> 跳过。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-09-24%20at%2010.00.00%20AM.png" alt="新建 Lambda 函数"></p>
<p>跳过模板选择后,我们会进入触发器(Trigger)的选择页面。每个 Lambda 函数都需要通过事件(Event)去触发。比较常见的场景有有——1)Kinesis 流式数据依次触发 Lambda 被处理;2)S3 有新 Object 时触发处理;3)通过 API Gateway 直接变成一项可以被 Restful 调用的服务;4)Dynamodb 有新数据入库时触发;5)通过 Cloudwatch 设置定时触发;以及6)我们今天要介绍的通过 Iot 消息队列获取消息数据触发。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-09-24%20at%2011.36.17%20AM.png" alt="选择 IoT 作为触发器"></p>
<p>在选择触发器的下拉框内,选择 AWS IoT 作为事件触发源,选择在文章第一篇中已经设置好的 IoT Rule (以及如果需要对进入 Lambda 的消息进行过滤,填写 SQL Statement 筛选数据),进入下一步。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-09-24%20at%2010.07.13%20AM.png" alt="新建 Lambda 界面"></p>
<p>这里,我们进入了函数的具体配置页面。首先,我们需要填写函数的名字以及选择函数对应的语言。在 <code>Runtime</code> 下拉框中选择 <code>NodeJS</code>。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-09-24%20at%2010.09.33%20AM.png" alt="语言选择"></p>
<p>之后,我们需要将函数的实现代码上传——这里可以选择直接在下方文本框中输入代码,或者当有依赖及代码结构较为复杂时将整个项目打包上传。下面的代码中,因为我会依赖一些第三方包,所以需要将项目整体上传。这里,和其他 node.js 项目一样需要先建立一个 <code>package.json</code> 文件管理依赖:</p>
<p><code>package.json</code></p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div></pre></td><td class="code"><pre><div class="line">&#123;</div><div class="line"> <span class="attr">"name"</span>: <span class="string">"image-feature-extraction"</span>,</div><div class="line"> <span class="attr">"version"</span>: <span class="string">"1.0.0"</span>,</div><div class="line"> <span class="attr">"description"</span>: <span class="string">"Using indico.io API for image feature extraction"</span>,</div><div class="line"> <span class="attr">"main"</span>: <span class="string">"index.js"</span>,</div><div class="line"> <span class="attr">"scripts"</span>: &#123;</div><div class="line"> <span class="attr">"test"</span>: <span class="string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span></div><div class="line"> &#125;,</div><div class="line"> <span class="attr">"author"</span>: <span class="string">"Anthroraemon"</span>,</div><div class="line"> <span class="attr">"license"</span>: <span class="string">"ISC"</span>,</div><div class="line"> <span class="attr">"dependencies"</span>: &#123;</div><div class="line"> <span class="attr">"async"</span>: <span class="string">"^1.5.2"</span>,</div><div class="line"> <span class="attr">"request"</span>: <span class="string">"^2.69.0"</span></div><div class="line"> &#125;</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>然后,我们就可以开始写函数的主体了——</p>
<p><code>index.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> request = <span class="built_in">require</span>(<span class="string">'request'</span>);</div><div class="line"><span class="keyword">var</span> <span class="keyword">async</span> = <span class="built_in">require</span>(<span class="string">'async'</span>);</div><div class="line"></div><div class="line"><span class="keyword">var</span> indicoAPIKey = <span class="string">'my_indico_key'</span>;</div><div class="line"><span class="keyword">var</span> indicoEndpoint = <span class="string">'http://apiv2.indico.io/imagefeatures?key='</span> + indicoAPIKey;</div><div class="line"></div><div class="line">exports.handler = <span class="function"><span class="keyword">function</span>(<span class="params">event, context</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> imageBase64str = event.image;</div><div class="line"> <span class="keyword">var</span> deviceName = event.deviceName;</div><div class="line"> <span class="keyword">var</span> postDate = event.id;</div><div class="line"> <span class="keyword">async</span>.waterfall([</div><div class="line"> <span class="comment">// Extract image feature from indico.io</span></div><div class="line"> <span class="function"><span class="keyword">function</span>(<span class="params">callback</span>)</span>&#123;</div><div class="line"> <span class="keyword">var</span> options = &#123;</div><div class="line"> uri: indicoEndpoint,</div><div class="line"> method: <span class="string">'POST'</span>,</div><div class="line"> json: &#123;</div><div class="line"> <span class="string">'data'</span>: imageBase64str</div><div class="line"> &#125;</div><div class="line"> &#125;;</div><div class="line"> request(options, <span class="function"><span class="keyword">function</span>(<span class="params">error, response, body</span>)</span>&#123;</div><div class="line"> <span class="keyword">if</span>(error)&#123;</div><div class="line"> <span class="keyword">return</span> callback(error.message);</div><div class="line"> &#125;</div><div class="line"> callback(<span class="literal">null</span>, body.results);</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> ], <span class="function"><span class="keyword">function</span>(<span class="params">error, result</span>)</span>&#123;</div><div class="line"> <span class="keyword">if</span>(error)&#123;</div><div class="line"> <span class="built_in">console</span>.log(error);</div><div class="line"> context.fail(error);</div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> <span class="built_in">console</span>.log(result);</div><div class="line"> context.succeed(result);</div><div class="line"> &#125;</div><div class="line"> &#125;);</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>这里我们主要做了两件事情——1)从 IoT 输入的消息中获取到设备的编号以及设备拍摄的照片的 base64 字符串;2)将图片送到 <a href="https://indico.io/" target="_blank" rel="external">Indico</a> 的 API 中抽取图像的特征向量,以供未来分析及机器学习使用。Indico 提供了非常强大的各种关于文本和图像的机器学习工具,大家有兴趣可以看一下,但不是本文介绍的主题,就不多做展开了。</p>
<p>回到上面编写的 Lambda 主体代码中——每份代码中都需要一个 main function 作为执行的主体,这里,我们以 <code>handler</code> 函数作为主函数,并且用 <code>exports</code> 暴露。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">exports.handler = function(event, context) &#123;</div><div class="line"> // Function to be executed</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>要注意的是,任何 Lambda 的 main fucntion 都需要两个输入参数,<code>event</code>和<code>context</code>。<code>event</code>变量储存所有事件触发后的 Input 数据,如上面我们用到的设备号和图形数据。无论你的触发器是 IoT 或是其他,输入变量永远在<code>event</code>变量中获取。<code>context</code>则是函数执行的环境,当函数计算完成后,需要通过<code>context.succeed(result)</code>或<code>context.fail(error)</code>方法返回计算结果,以供 AWS CloudWatch 准确得到返回状态等。其余的部分则按正常的代码编写逻辑即可。</p>
<p>完成了上面两个文件,我们就可以将项目打包上传了。首先建立项目结构——</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">mkdir mylambdafunction</div><div class="line">cp package.json ./mylambdafunction</div><div class="line">cp index.js ./mylambdafunction</div><div class="line">cd mylambdafunction</div><div class="line">npm install</div></pre></td></tr></table></figure>
<p>然后,将<code>mylambdafunction</code>文件夹<strong>里面</strong>的内容打包成 zip 文件(即不要包括<code>mylambdafunction/</code>结构)。回到 AWS 的代码上传界面,在<code>Code entry type</code>中选择<code>Upload a .ZIP file</code>,并选择刚刚打包的 zip 文件。在 <code>Handler</code> 中,填写 <code>index.handler</code>,即告诉 AWS 要执行的 main function 是 <code>index.js</code> 文件中的 <code>handler</code> 函数。选择一个执行该函数的用户角色(即如果要访问 AWS 上的其他资源的话,需要执行 Lambda 函数的用户有这些资源的权限),最后则填写分配这个函数执行时可以使用的资源,以及函数的最长执行时间(如果超时,函数运行将被自动终止),函数的运行时间上限是<code>5</code>分钟。其他语言的项目打包基本也是一样的,具体可以在 AWS 的<a href="http://docs.aws.amazon.com/lambda/latest/dg/lambda-app.html" target="_blank" rel="external">文档</a>查询。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-09-24%20at%2011.19.24%20AM.png" alt="配置函数"></p>
<p>完成这些配置以后,一个 Lambda 函数就建好了。回到控制台,选择刚刚新建的函数,我们可以在 <code>Actions</code> 下拉框中配置一个测试事件,来测试代码是否已经成功部署,也可以在 <code>Configure</code> Tab 页上更新上面配置的函数资源、或者在 <code>Trigger</code> 页面里更新其他的触发器和事件。一切就绪以后,打开你的摄像头和控制台,运行看看是否有照片被拍摄、上传和分析吧!</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-09-24%20at%2011.48.28%20AM.png" alt="配置测试数据"></p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-09-24%20at%2011.47.03%20AM.png" alt="修改函数信息"></p>
<hr>
<h3 id="番外篇"><a href="#番外篇" class="headerlink" title="番外篇"></a>番外篇</h3><p>以上就是本系列的最后一篇文章了。总结一下,在这个系列里面,我们利用到了 AWS 的服务将一个普通的摄像头变成了一个智能摄像头,同时学习了包括 Lambda 在内的一些 AWS 新服务的使用。</p>
<p>在系列第二到第三篇这半年当中,可以说无后台微服务的思想和使用都得到了大力的传播,我本人对 Lambda 的认识也在逐步加深。下面就简单讲一些使用中的 tips——</p>
<ol>
<li>Lambda 在运行 Python 代码时,因为大量 Python 第三方包都依赖预编译的 C Lib,而本身 Lambda 运行环境下是没有的,因此打包时需要将这些 lib 先编译好再一起上传。这些 lib 必须在 AWS EC2 的官方镜像下编译,一些常用的预编译好的包可以在<a href="https://github.com/Miserlou/lambda-packages" target="_blank" rel="external">这个GitHub</a>代码库中找到直接使用,也可以学习里面的构造脚本自己打包一些其他 lib。</li>
<li>无后台服务的触角甚至于已经延伸到了一般的微服务-API 架构之外。比如<a href="https://github.com/Miserlou/Zappa" target="_blank" rel="external">这个项目</a>将一个完整的 Web App 部署在了AWS 上(静态资源在 S3 上,任何后端逻辑全部在 Lambda 上)。也就是说,这个网站在没有人访问的时候是不存在的!只有有人访问时,才会触发 Lambda 函数进行后端路由等。</li>
<li>一些常见使用 Lambda 的 Use Case 包括——1)大量传感器生成的数据的解析、归档和存储 (IoT);2)大量日志文件的处理和归档 (Kinesis);3)在 NoSQL 数据库上实现 类似 SQL 数据库的 Trigger 功能。</li>
<li>Lambda 函数可以进行级联,即数据进入一个 Lambda 函数进行预处理,再调用多个不同 Lambda 函数进行后处理。</li>
<li>在今年的 AWS 中国峰会上,反复被提到的概念是 Infrastructure 的进化——经历了 <code>物理机 -&gt; 虚拟机 -&gt; 云服务器 -&gt; 容器 -&gt; 函数</code>的进化过程。可以看到,在计算资源愈加丰富的情况下,我们可以越来越专注核心功能的实现,而不是运维。Hail AWS!</li>
</ol>
</content>
<summary type="html">
<p>在本系列的上两篇文章中,我们介绍了如何利用 AWS IoT 打造一个可以远程操作的智能照相机并且将拍摄的照片数据通过 IoT 消息队列进入 AWS Lambda 进行处理的过程。在这篇文章中,我将着重介绍当数据进入 Lambda 后是如何被处理的。</p>
<p>先简单介绍
</summary>
<category term="Snippet" scheme="http://xraywu.github.io/categories/Snippet/"/>
<category term="IoT" scheme="http://xraywu.github.io/tags/IoT/"/>
<category term="Machine Learning" scheme="http://xraywu.github.io/tags/Machine-Learning/"/>
<category term="AWS" scheme="http://xraywu.github.io/tags/AWS/"/>
<category term="Coding" scheme="http://xraywu.github.io/tags/Coding/"/>
</entry>
<entry>
<title>使用AWS IoT、Lambda及ML打造智能化设备(二)</title>
<link href="http://xraywu.github.io/2016/03/16/aws-iot-lambda-2/"/>
<id>http://xraywu.github.io/2016/03/16/aws-iot-lambda-2/</id>
<published>2016-03-16T06:33:15.000Z</published>
<updated>2016-07-17T09:09:55.000Z</updated>
<content type="html"><p>在<a href="http://xraywu.github.io/2016/02/17/aws-iot-lambda-1/">上一篇</a>博文中,我们介绍了AWS IoT的基本概念,并使用其SDK搭建了一个简易的USB智能摄像头,使之能够每隔一段时间就拍摄照片并上传到云端作进一步的分析。然而,仅仅使用本地SDK,我们还不能实现对其远端控制。在这篇文章中,我们将着重理解一下AWS IoT中“Device Shadow”的概念,并搭建一个远端控制摄像头开关的Web App。这里将使用MEAN Stack进行开发。Device的列表ID会储存在本地的MongoDB中,而对远端摄像头的控制则通过Node.js后端调用AWS相应的API来实现。</p>
<p>首先,我们需要在Node.js环境下初始化一个Express应用,并添加相应的依赖,这里就不多做介绍了,可以使用之前介绍过的<a href="https://github.com/jxm262/hackathon-starter-ejs" target="_blank" rel="external">框架</a>搭建。同时也需要在MongoDB中新建一个数据库供本地存储使用。然后就可以新建相应的路由和Controller了。</p>
<p>首先创建<code>controllers/device.js</code>文件。先引入一些依赖和配置文件——</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> aws4 = <span class="built_in">require</span>(<span class="string">'aws4'</span>);</div><div class="line"><span class="keyword">var</span> <span class="keyword">async</span> = <span class="built_in">require</span>(<span class="string">'async'</span>);</div><div class="line"><span class="keyword">var</span> https = <span class="built_in">require</span>(<span class="string">'https'</span>);</div><div class="line"><span class="keyword">var</span> moment = <span class="built_in">require</span>(<span class="string">'moment'</span>);</div><div class="line"></div><div class="line"><span class="keyword">var</span> secrets = <span class="built_in">require</span>(<span class="string">'../config/secrets'</span>);</div><div class="line"><span class="keyword">var</span> deviceConf = <span class="built_in">require</span>(<span class="string">'../config/device'</span>);</div><div class="line"><span class="keyword">var</span> Device = <span class="built_in">require</span>(<span class="string">'../models/Device'</span>);</div></pre></td></tr></table></figure>
<p>上面两个配置文件分别用来配置数据库、AWS的秘钥及在上一篇文章中介绍过的用于交换心跳的AWS IoT信道名——</p>
<p><code>config/secret.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div></pre></td><td class="code"><pre><div class="line"><span class="built_in">module</span>.exports = &#123;</div><div class="line"> db: process.env.MONGODB || process.env.MONGOLAB_URI || <span class="string">'mongodb://account:password@localhost:27017/qacamera-admin'</span>,</div><div class="line"> sessionSecret: process.env.SESSION_SECRET || <span class="string">'Your Session Secret goes here'</span>,</div><div class="line"> aws: &#123;</div><div class="line"> accessKeyId: <span class="string">'Your AWS Access Key'</span>,</div><div class="line"> secretAccessKey: <span class="string">'Your AWS Secret Key'</span></div><div class="line"> &#125;,</div><div class="line"> awsIoT: &#123;</div><div class="line"> hostDomain: <span class="string">'Your AWS Host Domain'</span>,</div><div class="line"> region: <span class="string">'Your AWS Region'</span></div><div class="line"> &#125;</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p><code>config/device.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line"><span class="built_in">module</span>.exports = &#123;</div><div class="line"> heartbeatInterval: <span class="number">30</span>,</div><div class="line"> heartbeatChannel: <span class="string">'Heartbeat'</span></div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>设备信息对应的数据模型——</p>
<p><code>models/Device.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> mongoose = <span class="built_in">require</span>(<span class="string">'mongoose'</span>);</div><div class="line"><span class="keyword">var</span> mongoosePaginate = <span class="built_in">require</span>(<span class="string">'mongoose-paginate'</span>); <span class="comment">// For easy pagination</span></div><div class="line"></div><div class="line"><span class="keyword">var</span> deviceSchema = <span class="keyword">new</span> mongoose.Schema(&#123;</div><div class="line"> name: &#123; type: <span class="built_in">String</span>, unique: <span class="literal">true</span> &#125;,</div><div class="line"> lastConnected: &#123; type: <span class="built_in">Date</span>, <span class="keyword">default</span>: <span class="built_in">Date</span>.now &#125;</div><div class="line">&#125;);</div><div class="line"></div><div class="line">deviceSchema.static(<span class="string">'findByName'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">name, callback</span>)</span>&#123;</div><div class="line"> <span class="keyword">return</span> <span class="keyword">this</span>.findOne(&#123; name: name &#125;, callback);</div><div class="line">&#125;);</div><div class="line"></div><div class="line">deviceSchema.plugin(mongoosePaginate);</div><div class="line"></div><div class="line"><span class="built_in">module</span>.exports = mongoose.model(<span class="string">'Device'</span>, deviceSchema);</div></pre></td></tr></table></figure>
<p>之后则在Controller中添加相应的方法。首先是从MongoDB中获取已经储存的要管理的设备列表。这和AWS IoT关系不大。唯一需要注意的是,当中会计算现在时间和最后一次收到每一个设备发来的心跳信息的时间差,并计算这些联网设备是否在线。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// Device list is retrieved from local database and status is from AWS IoT Shadow</span></div><div class="line">exports.getDevices = <span class="function"><span class="keyword">function</span>(<span class="params">req, res</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> page = req.query.page;</div><div class="line"> <span class="keyword">var</span> limit = req.query.limit;</div><div class="line"></div><div class="line"> Device.paginate(&#123;&#125;, &#123; page: page, limit: limit&#125;, <span class="function"><span class="keyword">function</span>(<span class="params">err, result</span>)</span>&#123;</div><div class="line"> <span class="keyword">if</span>(err)&#123;</div><div class="line"> res.status(<span class="number">500</span>).json(&#123;</div><div class="line"> error: err</div><div class="line"> &#125;);</div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> <span class="comment">// When the device list is retrieved, also check their connection status</span></div><div class="line"> <span class="comment">// Using the last connected time and compare it to the current time</span></div><div class="line"> <span class="keyword">for</span>(<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; result.docs.length; i++)&#123;</div><div class="line"> <span class="keyword">var</span> now = moment(<span class="keyword">new</span> <span class="built_in">Date</span>());</div><div class="line"> <span class="keyword">var</span> lastConnected = moment(result.docs[i].lastConnected);</div><div class="line"> <span class="keyword">var</span> duration = now.diff(lastConnected, <span class="string">'seconds'</span>);</div><div class="line"> result.docs[i] = result.docs[i].toObject();</div><div class="line"> <span class="keyword">if</span>(duration &gt; deviceConf.heartbeatInterval)&#123;</div><div class="line"> result.docs[i].connected = <span class="string">'0'</span>;</div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> result.docs[i].connected = <span class="string">'1'</span>;</div><div class="line"> &#125;</div><div class="line"> &#125;</div><div class="line"> res.json(&#123;</div><div class="line"> error: <span class="literal">null</span>,</div><div class="line"> result: result</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;);</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>相应的,也添加一个辅助方法来判断某一个设备是否还在线。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// Check the connection status for a specific device by comparing its last connected time to current time</span></div><div class="line">exports.getDeviceConnection = <span class="function"><span class="keyword">function</span>(<span class="params">req, res</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> deviceName = req.query.deviceName;</div><div class="line"> Device.findByName(deviceName, <span class="function"><span class="keyword">function</span> (<span class="params">err, device</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span>(err)&#123;</div><div class="line"> res.status(<span class="number">500</span>).json(&#123;</div><div class="line"> error: err.errmsg</div><div class="line"> &#125;);</div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> <span class="keyword">var</span> now = moment(<span class="keyword">new</span> <span class="built_in">Date</span>());</div><div class="line"> <span class="keyword">var</span> lastConnected = moment(device.lastConnected);</div><div class="line"> <span class="keyword">var</span> duration = now.diff(lastConnected, <span class="string">'seconds'</span>);</div><div class="line"> <span class="keyword">var</span> connected = <span class="string">'0'</span>;</div><div class="line"> <span class="keyword">if</span>(duration &lt;= deviceConf.heartbeatInterval)&#123;</div><div class="line"> connected = <span class="string">'1'</span>;</div><div class="line"> &#125;</div><div class="line"> res.json(&#123;</div><div class="line"> name: deviceName,</div><div class="line"> connected: connected</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;);</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>之后,因为我们需要通过前端添加新的需要管理的设备,也要有向数据库添加记录的方法——</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// Add a device to be monitored at Admin Interface</span></div><div class="line">exports.addDevice = <span class="function"><span class="keyword">function</span>(<span class="params">req, res</span>) </span>&#123;</div><div class="line"> <span class="comment">// Only the device already registered on IoT cloud can be added at Admin UI</span></div><div class="line"> <span class="keyword">var</span> awsSecrets = secrets.aws;</div><div class="line"> <span class="keyword">var</span> opts = &#123;</div><div class="line"> service: <span class="string">'iotdata'</span>,</div><div class="line"> host: secrets.awsIoT.hostDomain + <span class="string">'.iot.'</span> + secrets.awsIoT.region + <span class="string">'.amazonaws.com'</span>,</div><div class="line"> path: <span class="string">'/things/'</span>+ req.body.deviceName + <span class="string">'/shadow'</span> &#125;;</div><div class="line"></div><div class="line"> aws4.sign(opts, awsSecrets);</div><div class="line"> <span class="keyword">var</span> resStr = <span class="string">''</span>;</div><div class="line"> <span class="keyword">var</span> shadowReq = https.request(opts, <span class="function"><span class="keyword">function</span>(<span class="params">shadowRes</span>)</span>&#123;</div><div class="line"> shadowRes.setEncoding(<span class="string">'utf8'</span>);</div><div class="line"> shadowRes.on(<span class="string">'data'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">chunk</span>)</span>&#123;</div><div class="line"> resStr += chunk;</div><div class="line"> &#125;);</div><div class="line"> shadowRes.on(<span class="string">'end'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> <span class="keyword">if</span>(shadowRes.statusCode !== <span class="number">200</span>)&#123;</div><div class="line"> res.status(<span class="number">500</span>).json(&#123;</div><div class="line"> error: <span class="built_in">JSON</span>.parse(resStr).message</div><div class="line"> &#125;);</div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> <span class="keyword">var</span> deviceStatus = <span class="built_in">JSON</span>.parse(resStr).state.desired.power;</div><div class="line"> <span class="keyword">var</span> device = <span class="keyword">new</span> Device(&#123;</div><div class="line"> name: req.body.deviceName</div><div class="line"> &#125;);</div><div class="line"> device.save(<span class="function"><span class="keyword">function</span>(<span class="params">err</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span>(err)&#123;</div><div class="line"> res.status(<span class="number">500</span>).json(&#123;</div><div class="line"> error: err.errmsg</div><div class="line"> &#125;);</div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> <span class="keyword">var</span> deviceCopy = &#123; name: device.name, status: deviceStatus &#125;;</div><div class="line"> res.json(&#123;</div><div class="line"> error: <span class="literal">null</span>,</div><div class="line"> result: deviceCopy</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> shadowReq.on(<span class="string">'error'</span>,<span class="function"><span class="keyword">function</span>(<span class="params">e</span>)</span>&#123;</div><div class="line"> res.status(<span class="number">500</span>).json(&#123;</div><div class="line"> error: e</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> shadowReq.end();</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>需要注意的是,这里有和AWS IoT API交互的地方。我们需要通过Shadow API来判断这个设备是否真实存在,并已经在AWS IoT里有了其虚拟化的“Shadow”。如果真实存在,则存储到MongoDB中并把其元数据返回给前端渲染。否则抛出错误信息。另外,根据AWS的要求,HTTP请求时数据需要加密,这里用到了<code>aws4</code>这个包来实现。</p>
<p>类似的,我们也想在页面中不断刷新获取最新的指令。还记得上一篇文章中我们说到,一个设备的状态并不是直接被API控制的,而是通过其“影子”的状态来进行同步。这里就是通过这样的原理获取最新需要同步的状态——</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// Check the desired status from Thing Shadow</span></div><div class="line">exports.getDeviceStatus = <span class="function"><span class="keyword">function</span>(<span class="params">req, res</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> deviceName = req.query.deviceName;</div><div class="line"> <span class="keyword">async</span>.parallel(&#123;</div><div class="line"> getShadow: <span class="function"><span class="keyword">function</span>(<span class="params">callback</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> awsSecrets = secrets.aws;</div><div class="line"> <span class="keyword">var</span> opts = &#123;</div><div class="line"> service: <span class="string">'iotdata'</span>,</div><div class="line"> host: secrets.awsIoT.hostDomain + <span class="string">'.iot.'</span> + secrets.awsIoT.region + <span class="string">'.amazonaws.com'</span>,</div><div class="line"> path: <span class="string">'/things/'</span>+ deviceName + <span class="string">'/shadow'</span> &#125;;</div><div class="line"></div><div class="line"> aws4.sign(opts, awsSecrets);</div><div class="line"> <span class="keyword">var</span> resStr = <span class="string">''</span>;</div><div class="line"> <span class="keyword">var</span> req = https.request(opts, <span class="function"><span class="keyword">function</span>(<span class="params">res</span>)</span>&#123;</div><div class="line"> res.setEncoding(<span class="string">'utf8'</span>);</div><div class="line"> res.on(<span class="string">'data'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">chunk</span>)</span>&#123;</div><div class="line"> resStr += chunk;</div><div class="line"> &#125;);</div><div class="line"> res.on(<span class="string">'end'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> <span class="keyword">if</span>(res.statusCode !== <span class="number">200</span>)&#123;</div><div class="line"> callback(<span class="built_in">JSON</span>.parse(resStr).message);</div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> callback(<span class="literal">null</span>, <span class="built_in">JSON</span>.parse(resStr));</div><div class="line"> &#125;</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> req.on(<span class="string">'error'</span>,<span class="function"><span class="keyword">function</span>(<span class="params">e</span>)</span>&#123;</div><div class="line"> <span class="keyword">return</span> callback(e);</div><div class="line"> &#125;);</div><div class="line"> req.end();</div><div class="line"> &#125;</div><div class="line"> &#125;,</div><div class="line"> <span class="function"><span class="keyword">function</span>(<span class="params">err, result</span>)</span>&#123;</div><div class="line"> <span class="keyword">if</span>(err)&#123;</div><div class="line"> res.status(<span class="number">500</span>).json(&#123;</div><div class="line"> error: err</div><div class="line"> &#125;);</div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> res.json(&#123;</div><div class="line"> error: <span class="literal">null</span>,</div><div class="line"> result: result.getShadow</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;);</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>最后,则是最为关键的远程控制摄像头的开关了。同样的,也是通过POST方法到Shadow API来改变“影子”的状态,再由其同步给摄像头。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// Remotely to (soft) turn on/off a device</span></div><div class="line">exports.switchDeviceStatus = <span class="function"><span class="keyword">function</span>(<span class="params">req, res</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> deviceName = req.body.deviceName;</div><div class="line"> <span class="keyword">var</span> status = req.body.status;</div><div class="line"></div><div class="line"> <span class="keyword">async</span>.parallel(&#123;</div><div class="line"> updateShadow: <span class="function"><span class="keyword">function</span>(<span class="params">callback</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> awsSecrets = secrets.aws;</div><div class="line"> <span class="keyword">var</span> opts = &#123;</div><div class="line"> service: <span class="string">'iotdata'</span>,</div><div class="line"> host: secrets.awsIoT.hostDomain + <span class="string">'.iot.'</span> + secrets.awsIoT.region + <span class="string">'.amazonaws.com'</span>,</div><div class="line"> path: <span class="string">'/things/'</span>+ deviceName + <span class="string">'/shadow'</span> &#125;;</div><div class="line"> <span class="keyword">var</span> statDoc = &#123;state: &#123;desired: &#123;power: status&#125;&#125;&#125;;</div><div class="line"></div><div class="line"> opts.body = <span class="built_in">JSON</span>.stringify(statDoc);</div><div class="line"> aws4.sign(opts, awsSecrets);</div><div class="line"> <span class="keyword">var</span> resStr = <span class="string">''</span>;</div><div class="line"> <span class="keyword">var</span> req = https.request(opts, <span class="function"><span class="keyword">function</span>(<span class="params">res</span>)</span>&#123;</div><div class="line"> res.setEncoding(<span class="string">'utf8'</span>);</div><div class="line"> res.on(<span class="string">'data'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">chunk</span>)</span>&#123;</div><div class="line"> resStr += chunk;</div><div class="line"> &#125;);</div><div class="line"> res.on(<span class="string">'end'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> callback(<span class="literal">null</span>, <span class="built_in">JSON</span>.parse(resStr));</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> req.on(<span class="string">'error'</span>,<span class="function"><span class="keyword">function</span>(<span class="params">e</span>)</span>&#123;</div><div class="line"> <span class="keyword">return</span> callback(e);</div><div class="line"> &#125;);</div><div class="line"> req.end(opts.body);</div><div class="line"> &#125;</div><div class="line"> &#125;,</div><div class="line"> <span class="function"><span class="keyword">function</span>(<span class="params">err, result</span>)</span>&#123;</div><div class="line"> <span class="keyword">if</span>(err)&#123;</div><div class="line"> res.status(<span class="number">500</span>).json(&#123;</div><div class="line"> error: err</div><div class="line"> &#125;);</div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> res.json(&#123;</div><div class="line"> error: <span class="literal">null</span>,</div><div class="line"> result: result.getShadow</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;);</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>有了这些方法,我们就可以在前端通过Angular.js来实现对设备的操控了。先建立这些方法相应的路由——</p>
<p><code>app.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">app.get(<span class="string">'/devices'</span>, deviceController.getDevices);</div><div class="line">app.get(<span class="string">'/devices/status'</span>, deviceController.getDeviceStatus);</div><div class="line">app.get(<span class="string">'/devices/connection'</span>, deviceController.getDeviceConnection);</div><div class="line">app.post(<span class="string">'/devices/add'</span>, deviceController.addDevice);</div><div class="line">app.post(<span class="string">'/devices/switch'</span>, deviceController.switchDeviceStatus);</div></pre></td></tr></table></figure>
<p>然后建立页面,并引入Angular.js依赖。这里还用了一些其他的前端JS Lib,功能比较简单就不详细介绍了。下面是其中的一些关键实现。</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line">script(src=&apos;/js/lib/angular.min.js&apos;)</div><div class="line">script(src=&apos;/js/lib/switchery.min.js&apos;)</div><div class="line">script(src=&apos;/js/lib/sweetalert.min.js&apos;)</div><div class="line">script(src=&apos;/js/lib/ng-switchery.js&apos;)</div><div class="line">script(src=&apos;/js/lib/ui-bootstrap-tpls-1.1.0.min.js&apos;)</div><div class="line">script(src=&apos;/js/angularController.js&apos;)</div><div class="line">script(src=&apos;/js/lib/jquery.min.js&apos;)</div><div class="line">script(src=&apos;/js/lib/bootstrap.min.js&apos;)</div><div class="line">script(src=&apos;/js/main.js&apos;)</div></pre></td></tr></table></figure>
<p>设备列表——</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div></pre></td><td class="code"><pre><div class="line">.device-section(ng-controller=&apos;DeviceListCtrl&apos;)</div><div class="line"> include add-device</div><div class="line"> .row</div><div class="line"> .col-sm-10</div><div class="line"> h4</div><div class="line"> | Registered Devices</div><div class="line"> .col-sm-2</div><div class="line"> a(href=&quot;#&quot;)</div><div class="line"> img(src=&apos;/images/add-btn.png&apos;, height=&quot;32px&quot;, width=&quot;32px&quot;,data-toggle=&quot;modal&quot; data-target=&quot;#addDeviceModal&quot;)</div><div class="line"> hr</div><div class="line"> .device-list</div><div class="line"> .row.top-buffer(ng-repeat=&apos;device in devices&apos;)</div><div class="line"> .col-sm-5.device-list-item</div><div class="line"> | &#123;&#123;device.name&#125;&#125;</div><div class="line"> .col-sm-3.device-list-item</div><div class="line"> img(ng-src=&quot;&#123;&#123;device.connected == &apos;0&apos; &amp;&amp; &apos;/images/red-light.png&apos; || &apos;/images/green-light.png&apos;&#125;&#125;&quot;, height=&quot;24px&quot;, width=&quot;24px&quot;)</div><div class="line"> .col-sm-4.device-list-item</div><div class="line"> input.text-right.js-switch.js-check-change(type=&apos;checkbox&apos;, ng-model=&quot;device.status&quot;, ui-switch, ng-change=&quot;updateShadow($index)&quot;)</div><div class="line"> hr</div><div class="line"> .row</div><div class="line"> .col-sm-12</div><div class="line"> uib-pager(previous-text=&quot;« Prev&quot;, total-items=&quot;totalItems&quot;, ng-model=&quot;currentPage&quot;, ng-change=&quot;pageChanged()&quot;, items-per-page=&quot;perPage&quot;)</div></pre></td></tr></table></figure>
<p>添加设备——</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div></pre></td><td class="code"><pre><div class="line">#addDeviceModal.modal.fade(tabindex=&quot;-1&quot;, role=&quot;dialog&quot;,aria-labelledby=&quot;addDeviceModalLabel&quot;)</div><div class="line"> .modal-dialog(role=&quot;document&quot;)</div><div class="line"> .modal-content</div><div class="line"> .modal-header</div><div class="line"> button.close(type=&quot;button&quot;,data-dismiss=&quot;modal&quot;,aria-label=&quot;Close&quot;)</div><div class="line"> span(aria-hidden=&quot;true&quot;)</div><div class="line"> | &amp;times;</div><div class="line"> h4#addDeviceModalLabel.modal-title</div><div class="line"> | Add Registered Device</div><div class="line"> .modal-body</div><div class="line"> form(ng-submit=&quot;addDevice()&quot;)</div><div class="line"> input.form-control(type=&quot;text&quot;,placeholder=&quot;Device Name&quot;,aria-describedby=&quot;sizing-addon3&quot;, ng-model=&quot;newName&quot;, required)</div><div class="line"> .modal-footer</div><div class="line"> button.btn.btn-default(type=&quot;button&quot;, data-dismiss=&quot;modal&quot;)</div><div class="line"> | Close</div><div class="line"> button.btn.btn-primary(type=&quot;submit&quot;)</div><div class="line"> | Add</div></pre></td></tr></table></figure>
<p>前端Angular的Controller,这里基本实现了几件事——1)页面读取时先调用后端获取已存在的设备列表,并一一获取其在线状态并前端渲染;2)每隔一段时间轮询设备列表,查询他们新的在线状态并渲染;3)在前端开关设备,并把该状态通过Node Controller同步到AWS IoT;4)在前端添加新的设备列表。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div><div class="line">50</div><div class="line">51</div><div class="line">52</div><div class="line">53</div><div class="line">54</div><div class="line">55</div><div class="line">56</div><div class="line">57</div><div class="line">58</div><div class="line">59</div><div class="line">60</div><div class="line">61</div><div class="line">62</div><div class="line">63</div><div class="line">64</div><div class="line">65</div><div class="line">66</div><div class="line">67</div><div class="line">68</div><div class="line">69</div><div class="line">70</div><div class="line">71</div><div class="line">72</div><div class="line">73</div><div class="line">74</div><div class="line">75</div><div class="line">76</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> cameraClient = angular.module(<span class="string">'cameraClient'</span>, [<span class="string">'NgSwitchery'</span>, <span class="string">'ui.bootstrap'</span>]);</div><div class="line"></div><div class="line">cameraClient.controller(<span class="string">'DeviceListCtrl'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">$scope, $http, $interval</span>)</span>&#123;</div><div class="line"><span class="comment">// Initialize data when page first loaded and make two-way bindings</span></div><div class="line"> $scope.currentPage = <span class="number">1</span>;</div><div class="line"> $scope.perPage = <span class="number">10</span>;</div><div class="line"> $scope.devices = [];</div><div class="line"> getData();</div><div class="line"></div><div class="line"> <span class="function"><span class="keyword">function</span> <span class="title">getData</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> $http.get(<span class="string">'/devices?limit='</span> + $scope.perPage + <span class="string">'&amp;page='</span> + $scope.currentPage).</div><div class="line"> then(<span class="function"><span class="keyword">function</span>(<span class="params">data</span>)</span>&#123;</div><div class="line"> $scope.totalItems = data.data.result.total;</div><div class="line"> <span class="keyword">var</span> devices = data.data.result.docs;</div><div class="line"> angular.forEach(devices, <span class="function"><span class="keyword">function</span>(<span class="params">value, key</span>)</span>&#123;</div><div class="line"> <span class="keyword">var</span> deviceName = value.name;</div><div class="line"> $http.get(<span class="string">'/devices/status?deviceName='</span> + deviceName).then(<span class="function"><span class="keyword">function</span>(<span class="params">data</span>)</span>&#123;</div><div class="line"> value.status = data.data.result.state.desired.power == <span class="number">0</span> ? <span class="literal">false</span> : <span class="literal">true</span>;</div><div class="line"> $scope.devices.push(value);</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> &#125;;</div><div class="line"></div><div class="line"> $scope.pageChanged = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> $scope.devices = [];</div><div class="line"> getData();</div><div class="line"> &#125;;</div><div class="line"></div><div class="line"></div><div class="line"><span class="comment">// Remotely change the status of your device</span></div><div class="line"> $scope.updateShadow = <span class="function"><span class="keyword">function</span>(<span class="params">$index</span>)</span>&#123;</div><div class="line"> <span class="keyword">var</span> power = $scope.devices[$index].status == <span class="literal">false</span> ? <span class="number">0</span> : <span class="number">1</span>;</div><div class="line"> <span class="keyword">var</span> json = &#123;</div><div class="line"> deviceName: $scope.devices[$index].name,</div><div class="line"> status: power</div><div class="line"> &#125;;</div><div class="line"> $http.post(<span class="string">'/devices/switch'</span>, json)</div><div class="line"> .success(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">"statusUpdated"</span>);</div><div class="line"> &#125;)</div><div class="line"> .error(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> $scope.devices[$index].status == <span class="literal">false</span> ? <span class="literal">true</span> : <span class="literal">false</span>;</div><div class="line"> swal(<span class="string">"Failed"</span>,<span class="string">"Unable to complete the action due to connection issue. Please try again later."</span>,<span class="string">"error"</span>);</div><div class="line"> &#125;);</div><div class="line"> &#125;;</div><div class="line"></div><div class="line"><span class="comment">// Add new device to be monitored</span></div><div class="line"> $scope.addDevice = <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> <span class="keyword">if</span>(!$scope.newName)</div><div class="line"> <span class="keyword">return</span>;</div><div class="line"></div><div class="line"> <span class="keyword">var</span> json = &#123; deviceName: $scope.newName &#125;</div><div class="line"></div><div class="line"> $http.post(<span class="string">'/devices/add'</span>, json)</div><div class="line"> .success(<span class="function"><span class="keyword">function</span>(<span class="params">data</span>)</span>&#123;</div><div class="line"> $scope.devices.push(&#123; name: $scope.newName, status: data.result.status == <span class="number">0</span> ? <span class="literal">false</span> : <span class="literal">true</span>, connected: <span class="string">"0"</span> &#125;)</div><div class="line"> $(<span class="string">'#addDeviceModal'</span>).modal(<span class="string">'hide'</span>);</div><div class="line"> $scope.newName = <span class="string">''</span>;</div><div class="line"> &#125;)</div><div class="line"> .error(<span class="function"><span class="keyword">function</span>(<span class="params">data</span>)</span>&#123;</div><div class="line"> $(<span class="string">'#addDeviceModal'</span>).modal(<span class="string">'hide'</span>);</div><div class="line"> $scope.newName = <span class="string">''</span>;</div><div class="line"> swal(<span class="string">"Failed"</span>, data.error,<span class="string">"error"</span>);</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"></div><div class="line"><span class="comment">// Check the status of the devices every 10 seconds and make them up to date</span></div><div class="line"> $interval(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> angular.forEach($scope.devices, <span class="function"><span class="keyword">function</span>(<span class="params">value, key</span>)</span>&#123;</div><div class="line"> $http.get(<span class="string">"/devices/connection?deviceName="</span> + value.name).then(<span class="function"><span class="keyword">function</span>(<span class="params">data</span>)</span>&#123;</div><div class="line"> $scope.devices[key].connected = data.data.connected;</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> &#125;, <span class="number">10000</span>)</div><div class="line">&#125;);</div></pre></td></tr></table></figure>
<p>如此一来,我们就有了一个可以在前台检查设备在线状态、控制设备开关的Web App了。对更为复杂的应用场景(如不同设备随各自状态变化的联动),其原理是一样的——<code>获取设备状态</code>-&gt;<code>更新Shadow状态</code>-&gt;<code>状态同步到设备</code>。通过这两篇文章中介绍的内容,我们也实现了设备客户端及远程管理端的闭环操作。在该系列的最后一篇文章中,我将简单介绍一下AWS Lambda,从而实现更为复杂的云端处理设备数据的功能。</p>
</content>
<summary type="html">
<p>在<a href="http://xraywu.github.io/2016/02/17/aws-iot-lambda-1/">上一篇</a>博文中,我们介绍了AWS IoT的基本概念,并使用其SDK搭建了一个简易的USB智能摄像头,使之能够每隔一段时间就拍摄照片并上传到云
</summary>
<category term="Snippet" scheme="http://xraywu.github.io/categories/Snippet/"/>
<category term="IoT" scheme="http://xraywu.github.io/tags/IoT/"/>
<category term="Machine Learning" scheme="http://xraywu.github.io/tags/Machine-Learning/"/>
<category term="AWS" scheme="http://xraywu.github.io/tags/AWS/"/>
<category term="Coding" scheme="http://xraywu.github.io/tags/Coding/"/>
</entry>
<entry>
<title>使用AWS IoT、Lambda及ML打造智能化设备(一)</title>
<link href="http://xraywu.github.io/2016/02/17/aws-iot-lambda-1/"/>
<id>http://xraywu.github.io/2016/02/17/aws-iot-lambda-1/</id>
<published>2016-02-17T05:22:33.000Z</published>
<updated>2016-07-17T09:09:37.000Z</updated>
<content type="html"><p>AWS在近两年的re:Invent大会上分别公布了用于智能设备后台服务的AWS IoT以及提供无服务器代码托管的Lambda服务。这两个服务加上一些其他的AWS服务(如Machine Learning、DynamoDB及目前仍处于preview状态的QuickSight等)形成了一个完整的闭环,使得智能硬件的开发者能够很容易的就开发出基于云服务的各种硬件来。这一系列博客将会对AWS IoT和Lambda进行一些介绍,并提供一个使用它们创建一个简易智能摄像头的例子。在系列的第一篇,让我们先来看一下AWS IoT的特性(当然你需要一个AWS除中国地区的账号才可以使用这些服务)及一个简单的Demo。</p>
<p>AWS IoT本质上提供了基于MQTT通讯协议的消息队列实现加上连接其他AWS服务的“胶水工具”。具体文档可以在<a href="https://aws.amazon.com/cn/documentation/iot/" target="_blank" rel="external">这里</a>查看。下面先介绍一下其中最重要的几个概念——</p>
<ul>
<li>Thing:代表了硬件本身在AWS IoT上注册的client。用于服务器端识别客户端及通信。</li>
<li>Shadow:代表了硬件在AWS IoT上的一个虚拟实例,也可以说是一个实体硬件的“影子”。对该影子的任何操作都可以是独立于硬件本身的,也就是说无论你的实体client是否连接到了AWS IoT,都不影响你对影子的操作。</li>
<li>Message:智能硬件客户端向服务器端发送的消息,有一个Topic名及JSON格式的body。</li>
<li>Rule:收到客户端发来的消息后,服务器端决定如何进行下一步操作的规则。包括了Filter和Action。</li>
<li>Filter:对客户端消息的过滤规则,如只取JSON中的某些attributes或只处理某些topic的message等。AWS IoT提供了类SQL的查询语言。</li>
<li>Action:处理完客户端消息后,服务器端下一步要做的事情,如将该消息作为input调用其他AWS Lambda函数。</li>
<li>Policy:客户端-服务器端通信的安全规则。</li>
</ul>
<p>这其中,Shadow是AWS IoT的核心,其背后的逻辑是通过“影子”和实体的同步来进行对硬件的操控,而不是直接操作智能硬件客户端本身。举例来说,如果你有一个智能灯泡,你需要远程打开它,你并不是直接向灯泡发送指令。相反,你先将指令发送到灯泡在IoT上的“影子”,然后由影子告诉灯泡它需要打开。这样做的好处是能够保证设备无论联网与否,始终保持需要其达到的状态。如果一个设备暂时离线,在其重新上线之后会马上请求与Shadow进行同步,获取最新的指令。AWS IoT面板里有一个互动的教程帮助理解该概念(如下图,其中左下角小的灰色灯泡代表了其左侧大灯泡的Shadow)。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-02-17%20at%2014.34.00.png" alt="AWS IoT概念示意图"></p>
<hr>
<p>理解了这些基本概念,下面让我们做一个Demo,目标是将一个普通的USB摄像头变成一个能够远程操控、定时拍照并上传的智能摄像头。和任何AWS服务一样,AWS IoT也提供了相应的SDK供开发者使用,目前有node.js、Embedded C和Arduino Yun。这里我们使用Node.js的SDK。Demo在Ubuntu 14.04下测试通过。</p>
<p>首先,我们需要在AWS IoT上创建代表该摄像头的Thing。登陆AWS控制台,进入AWS IoT面板。点击左上角的<code>Create Resource</code>按钮,然后点击<code>Create a thing</code>。在<code>Name</code>文本框里输入一个名字,就叫Camera吧,然后点击<code>Create</code>完成创建。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-02-17%20at%2014.53.47.png" alt=""></p>
<p>完成以后,面板上会出现一个叫Camera的按钮,点击以后,右侧会显示相应的API Endpoint等。记下来以后会需要。然后点击左下角的<code>Create a rule</code>按钮。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-02-17%20at%2014.56.16.png" alt=""></p>
<p>给这个rule一个名称,就叫camera_rule吧。在<code>Attribute</code>文本框里输入<code>*</code>,在<code>Topic Filter</code>文本框里输入<code>#</code>。这代表我们会对客户端发送的任何JSON消息进行处理,并且会把整条JSON数据不加删减的发送到下一步操作中去。在<code>Choose an action</code>下拉框里选择<code>Insert this message into a code function and execute it (Lambda)</code>。这代表客户端发送的消息将作为一个AWS Lambda函数的输入。如果你有任何已经存在的AWS Lambda函数,先选择它,没有的话点击<code>Create a new resource</code>并随便选一个模板创建一个。关于Lambda的具体使用我们在之后的blog里再仔细介绍。完成以后点击<code>Create</code>按钮完成创建。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-02-17%20at%2015.13.06.png" alt=""></p>
<p>之后则需要创建相应的安全规则。同样的,在IoT主面板上点击Camera按钮,然后点击右下角的<code>Connect a device</code>。在打开的页面左侧,选择NodeJS,然后点击<code>Generate certificate and policy</code>。等操作完成后,分别点击右侧的三个连接,下载公钥私钥和证书。点击<code>Confirm &amp; start connecting</code>,AWS会提供一段JSON,先记下来。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202016-02-17%20at%2015.16.56.png" alt=""></p>
<p>完成上述步骤后,我们可以开始编写相应的client了。先新建一个文件夹,<code>npm init</code>创建工程,然后安装一些需要的依赖包——</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line">npm install aws-iot-device-sdk //IoT客户端</div><div class="line">npm install fs</div><div class="line">npm install path</div><div class="line">npm install chokidar //文件夹监控</div><div class="line">npm install jpeg-js //处理原始图像数据</div><div class="line">npm install v4l2camera //操作USB摄像头</div></pre></td></tr></table></figure>
<p>在工程文件夹下建立一个叫incoming的文件夹,然后新建一个<code>app.js</code>文件。引用依赖并设置一些常量——</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> awsIot = <span class="built_in">require</span>(<span class="string">'aws-iot-device-sdk'</span>);</div><div class="line"><span class="keyword">var</span> fs = <span class="built_in">require</span>(<span class="string">'fs'</span>);</div><div class="line"><span class="keyword">var</span> path = <span class="built_in">require</span>(<span class="string">'path'</span>);</div><div class="line"><span class="keyword">var</span> chokidar = <span class="built_in">require</span>(<span class="string">'chokidar'</span>);</div><div class="line"><span class="keyword">var</span> jpegjs = <span class="built_in">require</span>(<span class="string">'jpeg-js'</span>);</div><div class="line"></div><div class="line"><span class="comment">// Initiate physical camera</span></div><div class="line"><span class="keyword">var</span> v4l2camera = <span class="built_in">require</span>(<span class="string">'v4l2camera'</span>);</div><div class="line"><span class="keyword">const</span> _watchFolder = <span class="string">'incoming/'</span>;</div><div class="line"><span class="keyword">const</span> _deviceNode = <span class="string">'/dev/video2'</span>;</div><div class="line"><span class="keyword">const</span> _shootInterval = <span class="number">10000</span>;</div><div class="line"><span class="keyword">const</span> _heartbeatInterval = <span class="number">10000</span>;</div></pre></td></tr></table></figure>
<p>其中<code>_deviceNode</code>是你的USB摄像头连接Linux以后相应的挂载地址,<code>_shootInterval</code>是要求摄像头每隔多少时间拍摄一张照片,<code>_heartbeatInterval</code>是要求摄像头每多少时间向服务器端发送一条心跳信息确定在线状态。</p>
<p>然后用<code>v4lcamera</code>这个包初始化摄像头——</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> cam = <span class="keyword">new</span> v4l2camera.Camera(_deviceNode);</div><div class="line"><span class="keyword">if</span> (cam.configGet().formatName !== <span class="string">'YUYV'</span>) &#123;</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'YUYV camera required'</span>);</div><div class="line"> process.exit(<span class="number">1</span>);</div><div class="line">&#125;</div><div class="line">cam.configSet(&#123;width: <span class="number">352</span>, height: <span class="number">288</span>&#125;);</div></pre></td></tr></table></figure>
<p>连接AWS IoT,这段config代码就是上面生成的配置代码。你需要指向正确的证书及私钥保存地址。还需要自己提供一个根证书文件——</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div></pre></td><td class="code"><pre><div class="line">// Create a device (shadow) and connect to AWS IoT</div><div class="line">var deviceShadow = awsIot.thingShadow(&#123;</div><div class="line"> &apos;host&apos;: &apos;xxxxxxxx.iot.eu-west-1.amazonaws.com&apos;,</div><div class="line"> &apos;port&apos;: 8883,</div><div class="line"> &apos;clientId&apos;: &apos;camera&apos;,</div><div class="line"> &apos;caCert&apos;: &apos;root-CA.crt&apos;,</div><div class="line"> &apos;clientCert&apos;: &apos;iot-certificate.pem.crt&apos;,</div><div class="line"> &apos;privateKey&apos;: &apos;iot-private.pem.key&apos;</div><div class="line">&#125;);</div></pre></td></tr></table></figure>
<p>之后处理设备连接到AWS IoT后不同事件的相应。首先是最关键的连通时。这里一共做了四件事情——1)向AWS IoT注册该客户端;2)等待两秒钟,用于获取其影子的状态;3)要求客户端每隔一段时间向服务器发送一条Topic为<code>Heartbeat</code>的消息告知其连接状态;4)初始化<code>chokidar</code>包监视<code>incoming</code>文件夹中的新文件,每当有新文件(照片)产生时,将其转为base64序列并以<code>Photo</code>为topic作为消息发送到服务器。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// Initialize the status to be for the device and its shadow</span></div><div class="line"><span class="comment">//var cameraState = &#123;"state": &#123;"desired": &#123;"power": 0&#125;&#125;&#125;;</span></div><div class="line"><span class="keyword">var</span> isCamOn = <span class="number">0</span>;</div><div class="line"></div><div class="line">deviceShadow.on(<span class="string">'connect'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'Device connected to AWS IoT'</span>);</div><div class="line"></div><div class="line"> <span class="comment">// When the dvice is connected with its shadow on AWS IoT, register the status first</span></div><div class="line"> deviceShadow.register(<span class="string">'camera'</span>);</div><div class="line"></div><div class="line"></div><div class="line"> setTimeout(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> deviceShadow.get(<span class="string">'camera'</span>);</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'Device shadow status retrieved'</span>);</div><div class="line"> &#125;, <span class="number">2000</span>);</div><div class="line"></div><div class="line"> <span class="comment">// Start watching the folder for incoming pictures</span></div><div class="line"> <span class="keyword">var</span> watcher = chokidar.watch(_watchFolder, &#123;</div><div class="line"> ignored: <span class="regexp">/[\/\\]\./</span>,</div><div class="line"> ignoreInitial: <span class="literal">true</span>,</div><div class="line"> persistent: <span class="literal">true</span>,</div><div class="line"> awaitWriteFinish: <span class="literal">true</span></div><div class="line"> &#125;);</div><div class="line"></div><div class="line"> <span class="comment">// When new photo comes in</span></div><div class="line"> watcher.on(<span class="string">'add'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">filepath</span>)</span>&#123;</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'File: '</span> + filepath + <span class="string">' added'</span>);</div><div class="line"></div><div class="line"> <span class="keyword">var</span> imageFile = fs.readFileSync(filepath);</div><div class="line"> <span class="keyword">var</span> imageBase64 = <span class="keyword">new</span> Buffer(imageFile).toString(<span class="string">'base64'</span>);</div><div class="line"></div><div class="line"> <span class="comment">// Send the image as base64 string as an event from the device to AWS IoT</span></div><div class="line"> <span class="comment">// A rule of this device is setup on AWS IoT to trigger a AWS Lambda function to decode image and store in S3</span></div><div class="line"> deviceShadow.publish(<span class="string">'photo'</span>, <span class="built_in">JSON</span>.stringify(&#123;</div><div class="line"> <span class="comment">// send extra information here, e.g. timestamp. product line id, etc.</span></div><div class="line"> id: path.basename(filepath),</div><div class="line"> image: imageBase64</div><div class="line"> &#125;));</div><div class="line"> &#125;);</div><div class="line"></div><div class="line"> <span class="comment">// Send heartbeat for connectivity checking</span></div><div class="line"> setInterval(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> deviceShadow.publish(<span class="string">'Heartbeat'</span>, <span class="built_in">JSON</span>.stringify(&#123;</div><div class="line"> name: <span class="string">'camera'</span>,</div><div class="line"> connected: <span class="number">1</span></div><div class="line"> &#125;));</div><div class="line"> &#125;, _heartbeatInterval);</div><div class="line">&#125;);</div></pre></td></tr></table></figure>
<p>在上面的步骤2中,我们得到了摄像头的影子的状态,也就是期望的状态。现在我们需要将其和实体摄像头的状态进行同步。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">deviceShadow.on(<span class="string">'status'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">thingName, stat, clientToken, stateObject</span>)</span>&#123;</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'received '</span> + stat + <span class="string">' on '</span> + thingName + <span class="string">': '</span> + <span class="built_in">JSON</span>.stringify(stateObject));</div><div class="line"> isCamOn = stateObject.state.desired.power;</div><div class="line">&#125;);</div></pre></td></tr></table></figure>
<p>此外,当Shadow被远程操控改变状态时,我们希望如果摄像头在线,它能够实时相应,而不是在下次重新上线时才更新状态。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// When the desired status is changed on shadow from another source (e.g. control pannel), change the local device status</span></div><div class="line">deviceShadow.on(<span class="string">'delta'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">thingName, stateObject</span>)</span>&#123;</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'Delta: '</span> + stateObject.state.power);</div><div class="line"> isCamOn = stateObject.state.power;</div><div class="line">&#125;);</div></pre></td></tr></table></figure>
<p>以上我们就完成了客户端和AWS IoT服务器端的通讯及控制。下面我们只需要根据需要的状态控制是否拍照即可。因为如何实现照相机的控制不是本文的重点,下面仅提供参考代码。具体需要一些配置可以参考v4l2camera的<a href="https://github.com/bellbind/node-v4l2camera" target="_blank" rel="external">文档</a>。</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div></pre></td><td class="code"><pre><div class="line"><span class="comment">// Use camera to take picture</span></div><div class="line">setInterval(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> takePhoto(cam, isCamOn);</div><div class="line">&#125;, _shootInterval);</div><div class="line"></div><div class="line"><span class="comment">// ------------------------ Functions -----------------------------</span></div><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">takePhoto</span>(<span class="params">cam, isCamOn</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span>(isCamOn)&#123;</div><div class="line"> cam.start();</div><div class="line"> times(<span class="number">6</span>, cam.capture.bind(cam), <span class="function"><span class="keyword">function</span> (<span class="params"></span>)</span>&#123;</div><div class="line"> <span class="keyword">var</span> fileName = <span class="built_in">Date</span>.now() + <span class="string">'.jpg'</span>;</div><div class="line"> saveAsJpg(cam.toRGB(), cam.width, cam.height, _watchFolder + fileName);</div><div class="line"> <span class="built_in">console</span>.log(<span class="string">'Image '</span> + fileName + <span class="string">' took'</span>);</div><div class="line"> cam.stop();</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line">&#125;</div><div class="line"></div><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">times</span>(<span class="params">n, async, cont</span>) </span>&#123;</div><div class="line"> <span class="keyword">return</span> <span class="keyword">async</span>(<span class="function"><span class="keyword">function</span> <span class="title">rec</span>(<span class="params">r</span>) </span>&#123;<span class="keyword">return</span> --n == <span class="number">0</span> ? cont(r) : <span class="keyword">async</span>(rec);&#125;);</div><div class="line">&#125;</div><div class="line"></div><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">saveAsJpg</span>(<span class="params">rgb, width, height, filename</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> size = width * height;</div><div class="line"> <span class="keyword">var</span> rgba = &#123;data: <span class="keyword">new</span> Buffer(size * <span class="number">4</span>), width: width, height: height&#125;;</div><div class="line"> <span class="keyword">for</span> (<span class="keyword">var</span> i = <span class="number">0</span>; i &lt; size; i++) &#123;</div><div class="line"> rgba.data[i * <span class="number">4</span> + <span class="number">0</span>] = rgb[i * <span class="number">3</span> + <span class="number">0</span>];</div><div class="line"> rgba.data[i * <span class="number">4</span> + <span class="number">1</span>] = rgb[i * <span class="number">3</span> + <span class="number">1</span>];</div><div class="line"> rgba.data[i * <span class="number">4</span> + <span class="number">2</span>] = rgb[i * <span class="number">3</span> + <span class="number">2</span>];</div><div class="line"> rgba.data[i * <span class="number">4</span> + <span class="number">3</span>] = <span class="number">255</span>;</div><div class="line"> &#125;</div><div class="line"> <span class="keyword">var</span> jpeg = jpegjs.encode(rgba, <span class="number">100</span>);</div><div class="line"> fs.createWriteStream(filename).end(Buffer(jpeg.data));</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>这样一来,我们就实现了一个简单的智能照相机。运行后他能够每隔一段时间拍摄照片并上传到AWS IoT,并被传输到Lambda中进行进一步的处理或分析。关于如何利用Shadow远程开关摄像头或用Lambda函数处理照片将在之后的blog里再作介绍。</p>
</content>
<summary type="html">
<p>AWS在近两年的re:Invent大会上分别公布了用于智能设备后台服务的AWS IoT以及提供无服务器代码托管的Lambda服务。这两个服务加上一些其他的AWS服务(如Machine Learning、DynamoDB及目前仍处于preview状态的QuickSight等)
</summary>
<category term="Snippet" scheme="http://xraywu.github.io/categories/Snippet/"/>
<category term="IoT" scheme="http://xraywu.github.io/tags/IoT/"/>
<category term="Machine Learning" scheme="http://xraywu.github.io/tags/Machine-Learning/"/>
<category term="AWS" scheme="http://xraywu.github.io/tags/AWS/"/>
<category term="Coding" scheme="http://xraywu.github.io/tags/Coding/"/>
</entry>
<entry>
<title>HumanAPI介绍及Demo</title>
<link href="http://xraywu.github.io/2016/01/08/humanapi-demo/"/>
<id>http://xraywu.github.io/2016/01/08/humanapi-demo/</id>
<published>2016-01-08T06:36:57.000Z</published>
<updated>2016-03-18T06:24:59.000Z</updated>
<content type="html"><p><a href="https://www.humanapi.co/" target="_blank" rel="external">HumanAPI</a>是美国的一家初创公司,其卖点是可以把单一用户在不同健康管理的应用中的数据聚合起来并通过统一的API供第三方应用进行访问。目前支持的数据源包括了大部分的主流可穿戴设备如Fitbit、Jawbone等,以及23andMe的基因测序信息、Apple Health Kit的科研数据甚至于电子医疗病例EMR系统中的患者就医信息。下面我会对HumanAPI做一点介绍并搭建一个简单的Demo。Demo搭建在之前介绍过的<a href="https://github.com/jxm262/hackathon-starter-ejs" target="_blank" rel="external">Hackathon Starter EJS</a>框架上。</p>
<p>首先你需要去HumanAPI上申请一个<a href="https://developer.humanapi.co/signup" target="_blank" rel="external">账号</a>。注册登陆以后进入HumanAPI的<a href="https://developer.humanapi.co/" target="_blank" rel="external">Dashbord</a>。在Dashboard上点击右上方的<code>Add New Application</code>按钮创建一个client app。创建以后,从Dashboard上点击新创建的App进入配置页面。</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202015-12-09%20at%2014.47.36.png" alt="HumanAPI界面"></p>
<p>其中有四个选项 - 1)Data Inputs,用于管理你的Client想要获取的用户第三方系统数据;2)Users,用于管理连接到你的Client的用户;3)App Settings,其中有你的client id和secret和管理员账户配置等;4)Notification,用于配置提示信息。这里先进到<code>App Settings</code>里面,记下你的client id和client secret。</p>
<p>下面我们就在Hackathon Starter EJS的基础上添加一个HumanAPI的Connection。先下载源码库 - </p>
<p><code>git clone https://github.com/jxm262/hackathon-starter-ejs.git</code></p>
<p>首先要搞定的是如何让用户连接到HumanAPI并获取他们的第三方数据。这里值得注意的是,HumanAPI并没有使用传统的OAuth流程做用户的授权,而是建立了一个他们自定义的用户授权流,由你的client为每个用户创建独立的HumanAPI用户记录。这样做的好处是用户不需要额外申请一个HumanAPI的账号,而只需要有其他第三方应用的账号(如Fitbit)即可授权你的client访问,同时如果有多个client使用了HumanAPI的服务,同一个用户也可以控制每一个client可以访问的第三方应用权限。</p>
<p><img src="https://www.filepicker.io/api/file/t94kQyxTRyUxJpuiv8xg" alt="HumanAPI用户登录授权流"></p>
<p>可以看到,由你的client首先发起通信,向服务器发送你的clent id及用户在你的app中的内部id到服务器,获取一个session token。之后你则把session token连同你的client secret发回给服务器获取API的access token及public token(用途之后会提到)。具体的实现上,HumanAPI则提供了一个<a href="https://connect.humanapi.co/connect.js" target="_blank" rel="external">javascript库</a>以简化流程。</p>
<p>那么,在前端页面先放置一个image button(HumanAPI官方button素材在<a href="https://connect.humanapi.co/assets/button/blue.png" target="_blank" rel="external">这里</a>)。在读取该页面的同时,controller需要获取你的client的clientId及该登录用户在你的app中的id并传到前端。具体的controller示例代码会贴在下面。这里先假设已经取到了。</p>
<p><code>humanapi.html</code></p>
<figure class="highlight html"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">'https://connect.humanapi.co/connect.js'</span>&gt;</span><span class="undefined"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></div><div class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">src</span>=<span class="string">'scripts/humanapi.js'</span>&gt;</span><span class="undefined"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></div><div class="line"><span class="tag">&lt;<span class="name">img</span> <span class="attr">id</span>=<span class="string">'connect-health-data-btn'</span> <span class="attr">src</span>=<span class="string">'https://connect.humanapi.co/assets/button/blue.png'</span> <span class="attr">onClick</span>=<span class="string">"connectHumanAPI("</span><span class="attr">clientUserId</span>", "<span class="attr">clientId</span>", "<span class="attr">publicToken</span>")" /&gt;</span></div></pre></td></tr></table></figure>
<p>其中,如果用户是第一次绑定授权HumanAPI,publicToken为空,否则还需要取回之前绑定时获取的public token用于匹配该用户。</p>
<p><code>scripts/humanapi.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div></pre></td><td class="code"><pre><div class="line"><span class="function"><span class="keyword">function</span> <span class="title">connectHumanAPI</span>(<span class="params">userId, clientId, publicToken</span>)</span>&#123;</div><div class="line"> <span class="keyword">var</span> options = &#123;</div><div class="line"> clientUserId: <span class="built_in">encodeURIComponent</span>(userId),</div><div class="line"> clientId: clientId, <span class="comment">// grab it from app settings page</span></div><div class="line"> publicToken: publicToken, <span class="comment">// 新用户为空</span></div><div class="line"></div><div class="line"> finish: <span class="function"><span class="keyword">function</span>(<span class="params">err, sessionTokenObject</span>) </span>&#123;</div><div class="line"> <span class="comment">// 用户完成授权后需要将HumanAPI返回的session token送到你的服务器端进行保存</span></div><div class="line"> sessionTokenObject.userId = userId;</div><div class="line"> $.post(<span class="string">'/auth/humanapi/callback'</span>, sessionTokenObject, <span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"></div><div class="line"> &#125;)</div><div class="line"> .done(<span class="function"><span class="keyword">function</span>(<span class="params"></span>)</span>&#123;</div><div class="line"> location.reload();</div><div class="line"> &#125;)</div><div class="line"> .fail(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</div><div class="line"></div><div class="line"> &#125;)</div><div class="line"> .always(<span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</div><div class="line"> location.reload();</div><div class="line"> &#125;);</div><div class="line"> &#125;,</div><div class="line"> close: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</div><div class="line"> <span class="comment">// 处理用户未选择任何第三方应用连接时的额外提示</span></div><div class="line"> &#125;,</div><div class="line"> error: <span class="function"><span class="keyword">function</span>(<span class="params">err</span>) </span>&#123;</div><div class="line"> <span class="comment">// callback出错时的处理方案</span></div><div class="line"> &#125;</div><div class="line">&#125;</div><div class="line"></div><div class="line">HumanConnect.open(options);</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>以上代码中最重要的就是<code>finish</code>后的回调,需要把HumanAPI送回的token保存到你的用户记录中。下面是回调的处理代码。</p>
<p><code>app.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line">app.post(<span class="string">'/auth/humanapi/callback'</span>, apiController.postHumanAPIAuth);</div></pre></td></tr></table></figure>
<p><code>controllers/api.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div><div class="line">50</div><div class="line">51</div><div class="line">52</div><div class="line">53</div><div class="line">54</div><div class="line">55</div><div class="line">56</div><div class="line">57</div><div class="line">58</div><div class="line">59</div><div class="line">60</div><div class="line">61</div><div class="line">62</div><div class="line">63</div><div class="line">64</div><div class="line">65</div><div class="line">66</div><div class="line">67</div><div class="line">68</div><div class="line">69</div><div class="line">70</div><div class="line">71</div><div class="line">72</div><div class="line">73</div><div class="line">74</div><div class="line">75</div><div class="line">76</div></pre></td><td class="code"><pre><div class="line"><span class="comment">/**</span></div><div class="line"> * GET /api/humanapi</div><div class="line"> * HumanAPI example.</div><div class="line"> */</div><div class="line"></div><div class="line"> exports.getHumanAPI = <span class="function"><span class="keyword">function</span>(<span class="params">req, res</span>) </span>&#123;</div><div class="line"> request = <span class="built_in">require</span>(<span class="string">'request'</span>);</div><div class="line"></div><div class="line"> <span class="keyword">var</span> publicToken = <span class="string">''</span>;</div><div class="line"> <span class="keyword">var</span> accessToken = <span class="string">''</span>;</div><div class="line"></div><div class="line"> <span class="keyword">if</span> (_.find(req.user.tokens, &#123; kind: <span class="string">'humanapi'</span> &#125;)) &#123;</div><div class="line"> <span class="keyword">var</span> token = _.find(req.user.tokens, &#123; kind: <span class="string">'humanapi'</span> &#125;);</div><div class="line"> publicToken = token.publicToken;</div><div class="line"> accessToken = token.accessToken;</div><div class="line"></div><div class="line"> <span class="keyword">async</span>.parallel(&#123;</div><div class="line"> getSources: <span class="function"><span class="keyword">function</span>(<span class="params">done</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> query = &#123; access_token : accessToken &#125;;</div><div class="line"> <span class="keyword">var</span> qs = querystring.stringify(query);</div><div class="line"> request.get(&#123;url: <span class="string">'https://api.humanapi.co/v1/human/sources?'</span> + qs&#125;, <span class="function"><span class="keyword">function</span>(<span class="params">error, request, body</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (error) <span class="keyword">return</span> done(error);</div><div class="line"> done(error, body);</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;,</div><div class="line"> <span class="function"><span class="keyword">function</span>(<span class="params">err, results</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (err) <span class="keyword">return</span> done(err);</div><div class="line"> res.render(<span class="string">'api/humanapi'</span>, &#123;</div><div class="line"> title: <span class="string">'Human API'</span>,</div><div class="line"> publicToken: publicToken,</div><div class="line"> clientUserId: req.user._id,</div><div class="line"> clientId: secrets.humanapi.clientId,</div><div class="line"> accessToken : accessToken,</div><div class="line"> sources : <span class="built_in">JSON</span>.parse(results.getSources),</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> res.render(<span class="string">'api/humanapi'</span>, &#123;</div><div class="line"> title: <span class="string">'Human API'</span>,</div><div class="line"> publicToken: publicToken,</div><div class="line"> clientUserId: req.user._id,</div><div class="line"> clientId: secrets.humanapi.clientId,</div><div class="line"> accessToken : accessToken</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;;</div><div class="line"></div><div class="line">exports.postHumanAPIAuth = <span class="function"><span class="keyword">function</span>(<span class="params">req, res</span>) </span>&#123;</div><div class="line"> request = <span class="built_in">require</span>(<span class="string">'request'</span>);</div><div class="line"> <span class="keyword">var</span> sessionTokenObject = req.body;</div><div class="line"> <span class="comment">// grab client secret from app settings page and `sign` `sessionTokenObject` with it.</span></div><div class="line"> sessionTokenObject.clientSecret = secrets.humanapi.clientSecret;</div><div class="line"></div><div class="line"> <span class="keyword">async</span>.parallel(&#123;</div><div class="line"> postSessionToken: <span class="function"><span class="keyword">function</span>(<span class="params">done</span>) </span>&#123;</div><div class="line"> request.post(&#123;url: secrets.humanapi.tokenURL, json: sessionTokenObject &#125;, <span class="function"><span class="keyword">function</span>(<span class="params">error, request, body</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (request.statusCode === <span class="number">401</span>) <span class="keyword">return</span> done(<span class="keyword">new</span> <span class="built_in">Error</span>(<span class="string">'Missing or Invalid HumanAPI Key'</span>));</div><div class="line"> <span class="comment">// 需要存下access token、public token和用户在HumanAPI里的id</span></div><div class="line"> User.findById(sessionTokenObject.userId, <span class="function"><span class="keyword">function</span>(<span class="params">err, user</span>) </span>&#123;</div><div class="line"> user.tokens.push(&#123; kind: <span class="string">'humanapi'</span>, accessToken: body.accessToken, publicToken: body.publicToken, humanId: body.humanId&#125;);</div><div class="line"> user.save(<span class="function"><span class="keyword">function</span>(<span class="params">err</span>) </span>&#123;</div><div class="line"> req.flash(<span class="string">'error'</span>, &#123; msg: <span class="string">'Error'</span>&#125;);</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> done(error, body);</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;,</div><div class="line"> <span class="function"><span class="keyword">function</span>(<span class="params">err, results</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (err) <span class="keyword">return</span> done(err);</div><div class="line"> req.flash(<span class="string">'success'</span>, &#123; msg: <span class="string">'HumanAPI account has been linked.'</span>&#125;);</div><div class="line"> res.status(<span class="number">200</span>).end();</div><div class="line"> <span class="comment">//res.redirect('/api/humanapi');</span></div><div class="line"> &#125;);</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>解释一下,这里<code>getHumanAPI()</code>方法即上面提到的从controller中获取用户client id并送到前端为之后connect HumanAPI做准备。其中还做了一件额外的事情,即当中的<code>getSources()</code>方法。该方法的作用是对已经绑定过HumanAPI及第三方应用数据的用户获取他们已经绑定过的应用列表及能获取的数据类型(如activity、sleep等),传送到前端进行展示。</p>
<p><code>postHumanAPIAuth()</code>则是处理回调的方法。主要有两个作用:1)Parse HumanAPI返回的用户token,并储存到用户对象中供下次使用;2)将获取的session token连同储存在后台的client secret提交到HumanAPI,以获取真正用于访问API的access token(这样一来你的client secret永远是安全的)。</p>
<p>如此一来,就完成了一个完整的针对HumanAPI的授权流。下一步则是利用HumanAPI提供的Chart API根据用户数据创建可视化图表。HumanAPI通过<code>iframe</code>提供图表。</p>
<figure class="highlight html"><table><tr><td class="gutter"><pre><div class="line">1</div></pre></td><td class="code"><pre><div class="line"><span class="tag">&lt;<span class="name">iframe</span> <span class="attr">src</span>=<span class="string">'https://chart.humanapi.co/v1/human/activities/summaries?chart_token=demo'</span>/&gt;</span></div></pre></td></tr></table></figure>
<p>其中chart_token需要用户的access token去交换。</p>
<p><code>controllers/api.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div><div class="line">50</div><div class="line">51</div><div class="line">52</div><div class="line">53</div><div class="line">54</div><div class="line">55</div><div class="line">56</div><div class="line">57</div></pre></td><td class="code"><pre><div class="line">exports.getHumanAPIChart = <span class="function"><span class="keyword">function</span>(<span class="params">req, res</span>) </span>&#123;</div><div class="line"> request = <span class="built_in">require</span>(<span class="string">'request'</span>);</div><div class="line"></div><div class="line"> <span class="keyword">var</span> type = req.query.type;</div><div class="line"></div><div class="line"> <span class="keyword">var</span> humanId = <span class="string">''</span>;</div><div class="line"> <span class="keyword">var</span> accessToken = <span class="string">''</span>;</div><div class="line"> <span class="keyword">var</span> publicToken = <span class="string">''</span>;</div><div class="line"> <span class="keyword">var</span> clientId = secrets.humanapi.clientId;</div><div class="line"></div><div class="line"> <span class="keyword">if</span> (_.find(req.user.tokens, &#123; kind: <span class="string">'humanapi'</span> &#125;)) &#123;</div><div class="line"> <span class="keyword">var</span> token = _.find(req.user.tokens, &#123; kind: <span class="string">'humanapi'</span> &#125;);</div><div class="line"> humanId = token.humanId;</div><div class="line"> accessToken = token.accessToken;</div><div class="line"> publicToken = token.publicToken;</div><div class="line"></div><div class="line"> <span class="keyword">if</span>(!type)&#123;</div><div class="line"> res.render(<span class="string">'api/humanapi'</span>, &#123;</div><div class="line"> title: <span class="string">'Human API'</span>,</div><div class="line"> publicToken: publicToken,</div><div class="line"> clientUserId: req.user._id,</div><div class="line"> clientId: secrets.humanapi.clientId,</div><div class="line"> accessToken : accessToken</div><div class="line"> &#125;);</div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> <span class="comment">// 使用access token及用户在HumanAPI的id和client id换取chart token</span></div><div class="line"> <span class="keyword">async</span>.parallel(&#123;</div><div class="line"> getChartToken: <span class="function"><span class="keyword">function</span>(<span class="params">done</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> query = &#123; humanId : humanId, clientId : clientId, accessToken : accessToken &#125;;</div><div class="line"> <span class="keyword">var</span> qs = <span class="built_in">JSON</span>.stringify(query);</div><div class="line"> request.post(&#123;url: <span class="string">'https://chart.humanapi.co/v1/tokens/chart'</span>, form: query&#125;, <span class="function"><span class="keyword">function</span>(<span class="params">error, request, body</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (error) <span class="keyword">return</span> done(error);</div><div class="line"> done(error, body);</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;,</div><div class="line"> <span class="function"><span class="keyword">function</span>(<span class="params">err, results</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (err) <span class="keyword">return</span> done(err);</div><div class="line"> <span class="keyword">var</span> token = <span class="built_in">JSON</span>.parse(results.getChartToken);</div><div class="line"> res.render(<span class="string">'api/humanapichart'</span>, &#123;</div><div class="line"> title: <span class="string">'Human API Chart'</span>,</div><div class="line"> type: type,</div><div class="line"> chartToken : token.chartToken,</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> <span class="comment">// 如果用户尚未绑定HumanAPI授权</span></div><div class="line"> &#125;<span class="keyword">else</span>&#123;</div><div class="line"> res.render(<span class="string">'api/humanapi'</span>, &#123;</div><div class="line"> title: <span class="string">'Human API'</span>,</div><div class="line"> publicToken: publicToken,</div><div class="line"> clientUserId: req.user._id,</div><div class="line"> clientId: secrets.humanapi.clientId,</div><div class="line"> accessToken : accessToken</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>这样一来就可以获取用户的可视化数据了。具体文档在<a href="http://hub.humanapi.co/docs/chart-api" target="_blank" rel="external">这里</a>。</p>
<p>最后看一下用户登陆授权的全过程 - </p>
<p>1) 用户登陆后点击<code>Connect Health Data</code>按钮</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202015-12-09%20at%2017.06.54.png" alt="授权按钮"></p>
<p>2) 用户选择需要连接的第三方应用数据源</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202015-12-09%20at%2017.07.14.png" alt="第三方数据源"></p>
<p>3) 用户在第三方应用处进行授权</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202015-12-09%20at%2017.07.23.png" alt="第三方应用授权"></p>
<p>4) 从HumanAPI获取第三方应用数据及可视化图表</p>
<p><img src="http://7xp1ay.com1.z0.glb.clouddn.com/Screen%20Shot%202015-12-10%20at%2014.33.29.png" alt="HumanAPI图表"></p>
</content>
<summary type="html">
<p><a href="https://www.humanapi.co/" target="_blank" rel="external">HumanAPI</a>是美国的一家初创公司,其卖点是可以把单一用户在不同健康管理的应用中的数据聚合起来并通过统一的API供第三方应用进行访问
</summary>
<category term="Snippet" scheme="http://xraywu.github.io/categories/Snippet/"/>
<category term="Coding" scheme="http://xraywu.github.io/tags/Coding/"/>
<category term="Node.js" scheme="http://xraywu.github.io/tags/Node-js/"/>
</entry>
<entry>
<title>使用Passport快速搭建用户验证及第三方应用OAuth授权</title>
<link href="http://xraywu.github.io/2015/12/08/passport-oauth/"/>
<id>http://xraywu.github.io/2015/12/08/passport-oauth/</id>
<published>2015-12-08T09:02:33.000Z</published>
<updated>2016-03-18T06:24:59.000Z</updated>
<content type="html"><p><a href="http://passportjs.org" target="_blank" rel="external">Passport</a>是Node.js下管理用户验证及第三方应用授权的包。利用它可以快速开发应用里的Authentication及Authorization流程。下面是一个在MEAN Stak下使用Passport进行用户登录校验及执行OAuth2流获取用户第三方应用数据的例子(代码大部分来自<a href="https://github.com/jxm262/hackathon-starter-ejs" target="_blank" rel="external">这里</a>)。</p>
<p>首先新建一个Node.js应用,安装必要的pacakge,如express、mongoose等。然后安装Passport及第三方应用基于Passport开发的登陆策略(也包括一个本地验证的策略,目前支持Passport的应用列表可以在<a href="http://passportjs.org" target="_blank" rel="external">这里</a>找到),这里还额外装了一个GitHub调用API的包,非必须 - </p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">npm install passport</div><div class="line">npm install passport-local</div><div class="line">npm install passport-github</div><div class="line">npm install github-api</div></pre></td></tr></table></figure>
<p>然后你需要建立一个用户模型 -</p>
<p><code>models/User.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> bcrypt = <span class="built_in">require</span>(<span class="string">'bcrypt-nodejs'</span>);</div><div class="line"><span class="keyword">var</span> crypto = <span class="built_in">require</span>(<span class="string">'crypto'</span>);</div><div class="line"><span class="keyword">var</span> mongoose = <span class="built_in">require</span>(<span class="string">'mongoose'</span>);</div><div class="line"></div><div class="line"><span class="keyword">var</span> userSchema = <span class="keyword">new</span> mongoose.Schema(&#123;</div><div class="line"> email: &#123; type: <span class="built_in">String</span>, unique: <span class="literal">true</span>, lowercase: <span class="literal">true</span> &#125;,</div><div class="line"> password: <span class="built_in">String</span>,</div><div class="line"> github: <span class="built_in">String</span>,</div><div class="line"> tokens: <span class="built_in">Array</span>,</div><div class="line"></div><div class="line"> profile: &#123;</div><div class="line"> name: &#123; type: <span class="built_in">String</span>, <span class="keyword">default</span>: <span class="string">''</span> &#125;,</div><div class="line"> gender: &#123; type: <span class="built_in">String</span>, <span class="keyword">default</span>: <span class="string">''</span> &#125;,</div><div class="line"> location: &#123; type: <span class="built_in">String</span>, <span class="keyword">default</span>: <span class="string">''</span> &#125;,</div><div class="line"> website: &#123; type: <span class="built_in">String</span>, <span class="keyword">default</span>: <span class="string">''</span> &#125;,</div><div class="line"> picture: &#123; type: <span class="built_in">String</span>, <span class="keyword">default</span>: <span class="string">''</span> &#125;</div><div class="line"> &#125;,</div><div class="line"></div><div class="line"> resetPasswordToken: <span class="built_in">String</span>,</div><div class="line"> resetPasswordExpires: <span class="built_in">Date</span></div><div class="line">&#125;);</div><div class="line"></div><div class="line"><span class="comment">/**</span></div><div class="line"> * 加密用户密码</div><div class="line"> */</div><div class="line">userSchema.pre(<span class="string">'save'</span>, <span class="function"><span class="keyword">function</span>(<span class="params">next</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> user = <span class="keyword">this</span>;</div><div class="line"> <span class="keyword">if</span> (!user.isModified(<span class="string">'password'</span>)) <span class="keyword">return</span> next();</div><div class="line"> bcrypt.genSalt(<span class="number">10</span>, <span class="function"><span class="keyword">function</span>(<span class="params">err, salt</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (err) <span class="keyword">return</span> next(err);</div><div class="line"> bcrypt.hash(user.password, salt, <span class="literal">null</span>, <span class="function"><span class="keyword">function</span>(<span class="params">err, hash</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (err) <span class="keyword">return</span> next(err);</div><div class="line"> user.password = hash;</div><div class="line"> next();</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line">&#125;);</div><div class="line"></div><div class="line"><span class="comment">/**</span></div><div class="line"> * 验证用户密码</div><div class="line"> */</div><div class="line">userSchema.methods.comparePassword = <span class="function"><span class="keyword">function</span>(<span class="params">candidatePassword, cb</span>) </span>&#123;</div><div class="line"> bcrypt.compare(candidatePassword, <span class="keyword">this</span>.password, <span class="function"><span class="keyword">function</span>(<span class="params">err, isMatch</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (err) <span class="keyword">return</span> cb(err);</div><div class="line"> cb(<span class="literal">null</span>, isMatch);</div><div class="line"> &#125;);</div><div class="line">&#125;;</div><div class="line"></div><div class="line"><span class="built_in">module</span>.exports = mongoose.model(<span class="string">'User'</span>, userSchema);</div></pre></td></tr></table></figure>
<p>其中用户的github属性用来储存通过第三方账号登陆后的用户名信息(这里用Github为例子),tokens用来储存不同的第三方应用授权后的token。</p>
<p>之后需要先配置你的应用在第三方应用处注册的client id和secret -</p>
<p><code>config/secret.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line"><span class="built_in">module</span>.exports = &#123;</div><div class="line"> github: &#123;</div><div class="line"> clientID: <span class="string">'your client id'</span>,</div><div class="line"> clientSecret: <span class="string">'your client secret'</span></div><div class="line"> callbackURL: <span class="string">'/auth/github/callback'</span>,</div><div class="line"> passReqToCallback: <span class="literal">true</span></div><div class="line"> &#125;</div><div class="line">&#125;</div></pre></td></tr></table></figure>
<p>之后你需要配置Passport中针对第三方应用的登陆策略 -</p>
<p><code>config/passport.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div><div class="line">36</div><div class="line">37</div><div class="line">38</div><div class="line">39</div><div class="line">40</div><div class="line">41</div><div class="line">42</div><div class="line">43</div><div class="line">44</div><div class="line">45</div><div class="line">46</div><div class="line">47</div><div class="line">48</div><div class="line">49</div><div class="line">50</div><div class="line">51</div><div class="line">52</div><div class="line">53</div><div class="line">54</div><div class="line">55</div><div class="line">56</div><div class="line">57</div><div class="line">58</div><div class="line">59</div><div class="line">60</div><div class="line">61</div><div class="line">62</div><div class="line">63</div><div class="line">64</div><div class="line">65</div><div class="line">66</div><div class="line">67</div><div class="line">68</div><div class="line">69</div><div class="line">70</div><div class="line">71</div><div class="line">72</div><div class="line">73</div><div class="line">74</div><div class="line">75</div><div class="line">76</div><div class="line">77</div><div class="line">78</div><div class="line">79</div><div class="line">80</div><div class="line">81</div><div class="line">82</div><div class="line">83</div><div class="line">84</div><div class="line">85</div><div class="line">86</div><div class="line">87</div><div class="line">88</div><div class="line">89</div><div class="line">90</div><div class="line">91</div><div class="line">92</div><div class="line">93</div><div class="line">94</div><div class="line">95</div><div class="line">96</div><div class="line">97</div><div class="line">98</div><div class="line">99</div><div class="line">100</div><div class="line">101</div><div class="line">102</div><div class="line">103</div><div class="line">104</div><div class="line">105</div><div class="line">106</div><div class="line">107</div><div class="line">108</div><div class="line">109</div><div class="line">110</div><div class="line">111</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> passport = <span class="built_in">require</span>(<span class="string">'passport'</span>);</div><div class="line"><span class="keyword">var</span> LocalStrategy = <span class="built_in">require</span>(<span class="string">'passport-local'</span>).Strategy;</div><div class="line"><span class="keyword">var</span> GitHubStrategy = <span class="built_in">require</span>(<span class="string">'passport-github'</span>).Strategy;</div><div class="line"></div><div class="line"><span class="keyword">var</span> secrets = <span class="built_in">require</span>(<span class="string">'./secrets'</span>);</div><div class="line"><span class="keyword">var</span> User = <span class="built_in">require</span>(<span class="string">'../models/User'</span>);</div><div class="line"></div><div class="line">passport.serializeUser(<span class="function"><span class="keyword">function</span>(<span class="params">user, done</span>) </span>&#123;</div><div class="line"> done(<span class="literal">null</span>, user.id);</div><div class="line">&#125;);</div><div class="line"></div><div class="line">passport.deserializeUser(<span class="function"><span class="keyword">function</span>(<span class="params">id, done</span>) </span>&#123;</div><div class="line"> User.findById(id, <span class="function"><span class="keyword">function</span>(<span class="params">err, user</span>) </span>&#123;</div><div class="line"> done(err, user);</div><div class="line"> &#125;);</div><div class="line">&#125;);</div><div class="line"></div><div class="line"><span class="comment">/**</span></div><div class="line"> * 利用User.js中定义的comparePassword方法进行本地用户登录验证</div><div class="line"> */</div><div class="line">passport.use(<span class="keyword">new</span> LocalStrategy(&#123; usernameField: <span class="string">'email'</span> &#125;, <span class="function"><span class="keyword">function</span>(<span class="params">email, password, done</span>) </span>&#123;</div><div class="line"> email = email.toLowerCase();</div><div class="line"> User.findOne(&#123; email: email &#125;, <span class="function"><span class="keyword">function</span>(<span class="params">err, user</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (!user) <span class="keyword">return</span> done(<span class="literal">null</span>, <span class="literal">false</span>, &#123; message: <span class="string">'Email '</span> + email + <span class="string">' not found'</span>&#125;);</div><div class="line"> user.comparePassword(password, <span class="function"><span class="keyword">function</span>(<span class="params">err, isMatch</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (isMatch) &#123;</div><div class="line"> <span class="keyword">return</span> done(<span class="literal">null</span>, user);</div><div class="line"> &#125; <span class="keyword">else</span> &#123;</div><div class="line"> <span class="keyword">return</span> done(<span class="literal">null</span>, <span class="literal">false</span>, &#123; message: <span class="string">'Invalid email or password.'</span> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line">&#125;));</div><div class="line"></div><div class="line"><span class="comment">/**</span></div><div class="line"> * 利用GitHub提供的第三方账号登录功能登陆验证</div><div class="line"> * 首先判断用户是否已经本地登陆,</div><div class="line"> * 如果本地已登陆,检测该用户是否已经绑定了一个GitHub账号,没有的话再继续。</div><div class="line"> * 如果用户没有本地登陆或者未绑定GitHub账号,通过调用GitHub的OAuth授权页面进行用户授权。</div><div class="line"> * 得到授权后通过GitHub返回的用户信息更新用户记录。</div><div class="line"> * 如果发现GitHub返回的用户Email已经存在,则提示登陆后再绑定。</div><div class="line"> * accessToken用于未来调用第三方应用API</div><div class="line"> * refreshToken用于后台更新要过期的accessToken</div><div class="line"> * profile中包含该用户在第三方应用的数据。</div><div class="line"> */</div><div class="line"></div><div class="line">passport.use(<span class="keyword">new</span> GitHubStrategy(secrets.github, <span class="function"><span class="keyword">function</span>(<span class="params">req, accessToken, refreshToken, profile, done</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (req.user) &#123;</div><div class="line"> User.findOne(&#123; github: profile.id &#125;, <span class="function"><span class="keyword">function</span>(<span class="params">err, existingUser</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (existingUser) &#123;</div><div class="line"> req.flash(<span class="string">'errors'</span>, &#123; msg: <span class="string">'用户已绑定GitHub账号!'</span> &#125;);</div><div class="line"> done(err);</div><div class="line"> &#125; <span class="keyword">else</span> &#123;</div><div class="line"> User.findById(req.user.id, <span class="function"><span class="keyword">function</span>(<span class="params">err, user</span>) </span>&#123;</div><div class="line"> user.github = profile.id;</div><div class="line"> user.tokens.push(&#123; kind: <span class="string">'github'</span>, accessToken: accessToken &#125;);</div><div class="line"> user.profile.name = user.profile.name || profile.displayName;</div><div class="line"> user.profile.picture = user.profile.picture || profile._json.avatar_url;</div><div class="line"> user.profile.location = user.profile.location || profile._json.location;</div><div class="line"> user.profile.website = user.profile.website || profile._json.blog;</div><div class="line"> user.save(<span class="function"><span class="keyword">function</span>(<span class="params">err</span>) </span>&#123;</div><div class="line"> req.flash(<span class="string">'info'</span>, &#123; msg: <span class="string">'成功绑定GitHub账号!'</span> &#125;);</div><div class="line"> done(err, user);</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;);</div><div class="line"> &#125; <span class="keyword">else</span> &#123;</div><div class="line"> User.findOne(&#123; github: profile.id &#125;, <span class="function"><span class="keyword">function</span>(<span class="params">err, existingUser</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (existingUser) <span class="keyword">return</span> done(<span class="literal">null</span>, existingUser);</div><div class="line"> User.findOne(&#123; email: profile._json.email &#125;, <span class="function"><span class="keyword">function</span>(<span class="params">err, existingEmailUser</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (existingEmailUser) &#123;</div><div class="line"> req.flash(<span class="string">'errors'</span>, &#123; msg: <span class="string">'该用户已注册,请登陆后再绑定GitHub账号'</span> &#125;);</div><div class="line"> done(err);</div><div class="line"> &#125; <span class="keyword">else</span> &#123;</div><div class="line"> <span class="keyword">var</span> user = <span class="keyword">new</span> User();</div><div class="line"> user.email = profile._json.email;</div><div class="line"> user.github = profile.id;</div><div class="line"> user.tokens.push(&#123; kind: <span class="string">'github'</span>, accessToken: accessToken &#125;);</div><div class="line"> user.profile.name = profile.displayName;</div><div class="line"> user.profile.picture = profile._json.avatar_url;</div><div class="line"> user.profile.location = profile._json.location;</div><div class="line"> user.profile.website = profile._json.blog;</div><div class="line"> user.save(<span class="function"><span class="keyword">function</span>(<span class="params">err</span>) </span>&#123;</div><div class="line"> done(err, user);</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line"> &#125;</div><div class="line">&#125;));</div><div class="line"></div><div class="line"><span class="comment">/**</span></div><div class="line"> * 在Controller中使用的判断是否已经登陆验证的Helper</div><div class="line"> */</div><div class="line">exports.isAuthenticated = <span class="function"><span class="keyword">function</span>(<span class="params">req, res, next</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (req.isAuthenticated()) <span class="keyword">return</span> next();</div><div class="line"> res.redirect(<span class="string">'/login'</span>);</div><div class="line">&#125;;</div><div class="line"></div><div class="line"><span class="comment">/**</span></div><div class="line"> * 在Controller中使用的判断是否已经得到第三方应用授权的Helper</div><div class="line"> */</div><div class="line">exports.isAuthorized = <span class="function"><span class="keyword">function</span>(<span class="params">req, res, next</span>) </span>&#123;</div><div class="line"> <span class="keyword">var</span> provider = req.path.split(<span class="string">'/'</span>).slice(<span class="number">-1</span>)[<span class="number">0</span>];</div><div class="line"> <span class="keyword">if</span> (_.find(req.user.tokens, &#123; kind: provider &#125;)) &#123;</div><div class="line"> next();</div><div class="line"> &#125; <span class="keyword">else</span> &#123;</div><div class="line"> res.redirect(<span class="string">'/auth/'</span> + provider);</div><div class="line"> &#125;</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>这样Passport的设置就完成了。之后则是要在各个controller调用相应的方法。</p>
<p>首先在<code>app.js</code>中调用刚刚在<code>passport.js</code>中定义的authentication及authorization的方法 - </p>
<p><code>app.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div></pre></td><td class="code"><pre><div class="line"><span class="comment">/**</span></div><div class="line"> * API keys and Passport configuration.</div><div class="line"> */</div><div class="line"><span class="keyword">var</span> secrets = <span class="built_in">require</span>(<span class="string">'./config/secrets'</span>);</div><div class="line"><span class="keyword">var</span> passportConf = <span class="built_in">require</span>(<span class="string">'./config/passport'</span>);</div><div class="line"></div><div class="line">app.get(<span class="string">'/auth/github'</span>, passport.authenticate(<span class="string">'github'</span>));</div><div class="line">app.get(<span class="string">'/auth/github/callback'</span>, passport.authenticate(<span class="string">'github'</span>, &#123; failureRedirect: <span class="string">'/login'</span> &#125;), <span class="function"><span class="keyword">function</span>(<span class="params">req, res</span>) </span>&#123;</div><div class="line"> res.redirect(req.session.returnTo || <span class="string">'/'</span>);</div><div class="line">&#125;);</div><div class="line"></div><div class="line">app.get(<span class="string">'/api/github'</span>, passportConf.isAuthenticated, passportConf.isAuthorized, apiController.getGithub);</div></pre></td></tr></table></figure>
<p>上面一共定义了三个Controller,第一个用于处理GitHub登陆,调用Passport中的GitHub登陆策略。第二个用于处理GitHub登陆后的回调。第三个用于在用户登录后调用第三方API获取更多用户信息,在调用其他方法前先检查用户是否已经登录及是否已经获得了授权。其中第三个controller在得到正确授权后就可以调用其他API了 - </p>
<p><code>controllers/api.js</code></p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div></pre></td><td class="code"><pre><div class="line"><span class="keyword">var</span> User = <span class="built_in">require</span>(<span class="string">'../models/User'</span>);</div><div class="line"></div><div class="line"><span class="keyword">var</span> Github;</div><div class="line">exports.getGithub = <span class="function"><span class="keyword">function</span>(<span class="params">req, res, next</span>) </span>&#123;</div><div class="line"> Github = <span class="built_in">require</span>(<span class="string">'github-api'</span>);</div><div class="line"></div><div class="line"> <span class="keyword">var</span> token = _.find(req.user.tokens, &#123; kind: <span class="string">'github'</span> &#125;);</div><div class="line"> <span class="keyword">var</span> github = <span class="keyword">new</span> Github(&#123; token: token.accessToken &#125;);</div><div class="line"> </div><div class="line"> <span class="comment">/**</span></div><div class="line"> * Do whatever you want with the GitHub API below</div><div class="line"> */</div><div class="line"> </div><div class="line"> <span class="keyword">var</span> repo = github.getRepo(<span class="string">'sahat'</span>, <span class="string">'requirejs-library'</span>);</div><div class="line"> repo.show(<span class="function"><span class="keyword">function</span>(<span class="params">err, repo</span>) </span>&#123;</div><div class="line"> <span class="keyword">if</span> (err) <span class="keyword">return</span> next(err);</div><div class="line"> res.render(<span class="string">'api/github'</span>, &#123;</div><div class="line"> title: <span class="string">'GitHub API'</span>,</div><div class="line"> repo: repo</div><div class="line"> &#125;);</div><div class="line"> &#125;);</div><div class="line">&#125;;</div></pre></td></tr></table></figure>
<p>好了,后端的配置全部完成了,然后就是前端了。代码这里不贴了,基本上就是在登陆页面上加上第三方应用的登陆按钮,指向<code>/auth/github</code>,然后在你需要展示第三方应用数据的页面前调用<code>/api/github</code>获取数据渲染就可以了。</p>
</content>
<summary type="html">
<p><a href="http://passportjs.org" target="_blank" rel="external">Passport</a>是Node.js下管理用户验证及第三方应用授权的包。利用它可以快速开发应用里的Authentication及Authoriz
</summary>
<category term="Snippet" scheme="http://xraywu.github.io/categories/Snippet/"/>
<category term="Coding" scheme="http://xraywu.github.io/tags/Coding/"/>
<category term="Node.js" scheme="http://xraywu.github.io/tags/Node-js/"/>
</entry>
<entry>
<title>关于Hackathon的一些Learnings</title>
<link href="http://xraywu.github.io/2015/11/06/hackathon-thoughts/"/>
<id>http://xraywu.github.io/2015/11/06/hackathon-thoughts/</id>
<published>2015-11-06T07:15:33.000Z</published>
<updated>2016-07-17T09:08:36.000Z</updated>
<content type="html"><p>这周公司组织了个Hackathon活动,叫上了两家Vendor分别组队比赛。作为一家逐渐从外包转向In-house的甲方公司,是不错的尝试。然而作为公司队一员的我深切的感受到,我们在内部开发的路上还有很远的路要走。与Vendor相比,差距不仅仅是在团队编程能力上的,更体现在项目管理、团队协作、DevOps等方方面面。</p>
<p><strong>1. 总体参后感</strong></p>
<p>因为公司性质,公司团队是跨国组成的(英国 - 1个PM、1个设计师、3个Developer,美国 - 2个Developer, 中国 - 1个Developer,本人)。跨国团队的最大好处是可以保证24小时都有人能精力充沛的工作,但坏处也很明显,沟通成本实在太高了。比赛的主赛场在英国,因此宣布比赛题目时,美国和中国都并不在线,无法参与初始讨论及Brainstorming,所以对整体项目的贡献几乎仅限于Coding。于此同时英国团队几乎主宰了整个Hackaton过程的走向 - 例如我写完的功能模块因为视觉原因被在英国的设计师否定了,(中国时间)一夜之间英国团队又重写了同样的模块,只是用了不同的前端实现。这对仅仅2天的Hackathon活动几乎是致命的,有这点时间英国团队完全可以添加更多的功能模块 - 这对我们最终的Delivery产生了重大的影响,而重写这一决定是完全没有involve中国讨论的。总的来说,在Hackathon这样短时间高强度的活动里,PM和设计师(几乎就是半个Product Owner了)需要非常清晰地控制进度及定义任务。对于跨国团队更是如此,尤其是其中大半时间因为时差原因他们对其他团队成员是unavailable的。顺带一提,Hackathon这样的比赛我们真的需要一个专职PM吗?Hackathon本身再适合agile不过了(同时公司里大家都自称懂agile),却找来一个发挥不了作用的PM导致最后大家几乎都只能自由发挥也是无力吐槽……</p>
<p>同时,应该有人整体负责技术的选型,而这个人不应该是(不懂开发的)PM - 本身在Hackathon开始前我们是选定了一个基于<code>MEAN Stack</code>的框架的,然而队伍中相当多的成员没有事先阅读文档熟悉框架,导致实际开发中只能各自使用擅长的技术,极大地拉低了开发效率。顺带一提,这个框架看下来还是相当适合Hackathon的 - <a href="https://github.com/jxm262/hackathon-starter-ejs" target="_blank" rel="external">Hackathon-Starter-EJS</a>。</p>
<p><strong>2. 代码管理</strong></p>
<p>Git! Git! Git! 重要的事情要说三遍。虽说Git已经是普及的不能再普及了,可是似乎学校里仍然不会教,中外莫不如此。我们的英国团队虽然有3个Developer,但两个都是非常Junior的学生级开发。虽然编程能力不差,但习惯了单打独斗的他们居!然!完!全!不!会!用!Git!! 这在这种虚拟合作的环境下几乎是致命的。第一天中国时间结束交接到英国后,他们几乎弄乱了所有的Branch - 更糟糕的是他们做出了删除全部Branch重新push code并只在master branch上工作的决定。。。这导致我们损失了不少其他Branch上的代码,只能再找Working copy去手工merge。说到底代码管理真的没什么可说的,每个人都应该熟悉Git和Best practice。</p>
<p><strong>3. 团队协作</strong></p>
<p>这次Hackathon中我们选用了Slack和Trello用于团队合作。工具是好工具,可是实际使用中因为种种原因效果大打折扣。Slack中由于消息比较多,经常导致别人不读离线时的信息,哪怕@本人的信息,从而导致错过一些重要的信息。当然,现在看来,Slack中就不应该放太过重要的信息。要么放到Git的Readme中,要么放到Trello中。</p>
<p>至于Trello的使用,和PM如何管项目是息息相关的。在Hackathon的第一天中大家就应该清楚地把要实现的功能都定义清楚,并好好分配任务和Backlog。然后大家才知道自己要做什么,如果有多余的精力又可以帮助到哪里,而不是走一步看一步,不停地修改Trello cards。</p>
<p>另外则是团队的交接问题。因为团队地理问题,交接的大概顺序是 <code>英国 -&gt; 美国 -&gt; 中国 -&gt; 英国</code>。由于交接时间都是一方的大清晨,前一天晚上工作到比较晚的开发时常不能参加,而这是相当致命的。两次传达以后信息的损失是相当严重的,尤其是当第一手信息只能传达给PM或设计师时,无法直接澄清相当多的技术细节问题。</p>
<p><strong>4. DevOps</strong></p>
<p>在Hackathon中,纯DevOps不那么重要,尤其是整个内部团队对DevOps还很不熟悉的时候(当然上述的方方面面理论上都属于DevOps)。然而具体到技术上,看起来如果能使用自动化部署还是相当有帮助的 - 团队的不少开发并不熟悉AWS和Linux,导致他们在简单地部署上浪费了太多时间(正常情况下他们只需要SSH到远程机器上并Pull master就好)。如果我们一开始就设置了自动化部署,就可以把这一步完全省略了。虽然这只是一个非常简单的任务。</p>
<p><strong>Technology Notes</strong></p>
<ul>
<li>AWS上Bitnami的MEAN Stack Image是有Application Password的,主要用于登陆MongoDB Admin账号,可以在EC2的system log里面查到。</li>
<li>MongoDB的客户端选择上使用<code>MongoChef</code>,很流行的<code>Robomongo</code>似乎远程连接时有authentication问题。Bitnami stack自带的Web界面的<code>Mongorock</code>需要SSL Tunnel,同时也有一些使用上的问题。</li>
<li><code>Nodemon</code>是一个好包,一旦代码有修改能自动重启node.js web server。安装:<code>npm install nodemon -g</code>;运行:<code>nodemon app.js</code></li>
</ul>
</content>
<summary type="html">
<p>这周公司组织了个Hackathon活动,叫上了两家Vendor分别组队比赛。作为一家逐渐从外包转向In-house的甲方公司,是不错的尝试。然而作为公司队一员的我深切的感受到,我们在内部开发的路上还有很远的路要走。与Vendor相比,差距不仅仅是在团队编程能力上的,更体现在
</summary>
<category term="人月神话" scheme="http://xraywu.github.io/categories/%E4%BA%BA%E6%9C%88%E7%A5%9E%E8%AF%9D/"/>
<category term="Hackathon" scheme="http://xraywu.github.io/tags/Hackathon/"/>
<category term="Agile" scheme="http://xraywu.github.io/tags/Agile/"/>
</entry>
</feed>