项目作者: ybw2016v

项目描述 :
基于arduino、树莓派、flask的光污染探测分析仪
高级语言: Jupyter Notebook
项目地址: git://github.com/ybw2016v/arduino.git
创建时间: 2018-01-29T23:56:46Z
项目社区:https://github.com/ybw2016v/arduino

开源协议:GNU General Public License v3.0

关键词:
arduino flask raspberrypi

下载


系统控制与结果查询

系统设计

设计目的

光污染是继废水、废气、废渣和噪声等污染之后的一种新的环境污染源。光污染不仅仅严重影响了人类的身体健康和其他生物的自然生活规律。除此之外,光污染还对科学探测特别是天文观测产生了严重的影响。

新《中华人民共和国环境保护法》(2015年)中第四十二条明确规定,要防治企业事业单位在生产中产生的包括光污染在内的多种污染。由此可见,我国渐渐认识到了光污染的严重危害,保护环境、治理光污染势在必行。

我们利用在单片机课程上学到的知识,用arduino单片机制作一个可以对光污染进行探测和分析的探测仪器,不仅可以帮助人们保护身体健康,践行绿色环保可持续的发展理念,而且可以在科学探测中保障探测结果的准确。

系统功能

基于arduino的光污染探测分析仪具备以下功能:

  1. 对整个空间的光强情况进行扫描,寻找出空间中光强最大的方向,也就是污染源的方向。

  2. 对于光强探测的结果,既能输出准确的数值,也能进行可视化输出。

  3. 网络查询功能:用树莓派做一个服务器,搭建一个网页,用户可以在网页上对探测器进行控制,探测器可以把扫描得到的空间中的光强分布以图片的形式发送到网页上,并且可以对光强最大的污染源拍照并且发送到网页上,方便用户查询。

主要硬件介绍

Arduino uno 开发板:

以ATmega328 MCU控制器为基础,具备14路数字输入/输出引脚(其中6路可用于PWM输出)、6路模拟输入、一个16MHz陶瓷谐振器、一个USB接口、一个电源插座、一个ICSP接头和一个复位按钮。它采用Atmega16U2芯片进行USB到串行数据的转换。它包含了组成微控制器的所有结构,同时,只需要一条USB数据线连接至电脑。目前,Arduino Uno已成为Arduino主推的产品。

1

树莓派3代B+型

这是树莓派的最新型号,主要参数chaxub如下:

  • 博通BCM2837B0 SoC,集成四核ARM Cortex-A53(ARMv8)64位@ 1.4GHz CPU,集成博通 Videocore-IV GPU

  • 内存:1GB LPDDR2 SDRAM

  • 有线网络:千兆以太网(通过USB2.0通道,最大吞吐量 300Mbps)

  • 无线网络:2.4GHz和5GHz 双频Wi-Fi,支持802.11b/g/n/ac

  • 蓝牙:蓝牙4.2&低功耗蓝牙(BLE)

  • 存储:Micro-SD

  • 其他接口:HDMI,3.5mm模拟音频视频插孔,4x USB 2.0,以太网,摄像机串行接口(CSI),显示器串行接口(DSI),MicroSD卡座,40pin扩展双排插针

  • 尺寸:82mmx 56mmx 19.5mm,50克

./pic/002.png

SG90舵机

  • 反应转速:0.12~0.13秒/60°;
  • 工作扭矩:1.6KG/CM;
  • 转动角度:180°

./pic/002.png

光强模块GY-30

通过光敏电阻的原理对光强进行探测,光强越大,电流越小。

./pic/002.png

硬件设计思路和电路图

硬件设计思路

  1. 将两个舵机按照转轴相互垂直的方向粘合起来,再把四个光强传感器组成探头粘在上方的舵机的转臂上。这样使得探头能够具有四个自由度,可以扫描整个空间。
  2. 舵机的转速通过arduino的PWM输出引脚输出PWM信号控制。、
  3. 在探头上加一个树莓派专用的摄像头,用于对光污染源拍照。将摄像头和树莓派相连,从而可以将拍到的照片通过树莓派传输的网页端。

电路图

探测器整体图

./pic/002.png

软件设计思路

软件设计

项目主要硬件可以划分为两个部分。arduino部分和raspberry pi部分。两个部分分别是不同的硬件架构。arduino是基于avr的单片机,然而,树莓派是基于arm架构的cpu。
硬件架构的不同导致主要的软件也被划分两部分,再加上显示在用户端的HTML,本项目的软件部分可以划分为三部分。

arduino部分

arduino主要负责硬件的控制与软件的采集。arduino通过串口接受来自树莓派的指令。

当arduino接收到树莓派的执行光污染源寻找指令时。arduino执行相关函数,首先通过pwm信号操纵水平方向舵机与竖直方向舵机进行转动,转动到初始位置。然后读取光强传感器阵列的数据。之后主次扫描过均匀分布的100各点。并从中找到光强最大的点,并记录位置。然后控制舵机旋转至该位置,然后传送信息至arduino。

当arduino接收到进行全角度扫描时,arduino将控制舵机转动至初始位置。然后开始对空间当中逐点扫描,每转到一个空间位置,遂及测量该点的亮度。并实时传回树莓派,直到遍历空间之中的所有点。扫描完毕之后将结束信号回传。

树莓派部分

树莓派作为整个仪器与用户之间的接口,是非常重要的。树莓派对外作为一个web服务器,负责给用户的浏览器分发内容,通过互联网接受用户指令,并根据不同指令返回用户结果或操纵单片机进行相关动作的执行。

web服务端采用nginx+uwsig+flask架构

Nginx 是一个很强大的轻量级的高性能Web和反向代理服务器。非常适合在树莓派上用于搭建小型网站。

uWSGI是一个Web服务器,它实现了WSGI协议、uwsgi、http等协议。Nginx中HttpUwsgiModule的作用是与uWSGI服务器进行交换。WSGI是一种Web服务器网关接口。它是一个Web服务器(如nginx,uWSGI等服务器)与web应用(如用Flask框架写的程序)通信的一种规范。uwsgi用于nginx与flask之间的通讯。

Flask是一个Python编写的Web 微框架,让我们可以使用Python语言快速实现一个网站或Web服务。flask在本项目之中可以用作动态网页服务器。用户浏览器发出的指令是由flask处理并执行。

本项目之中树莓派与arduino之间的互动是由python语言完成的。在python中导入了pyserial串口通讯模块用于与arduino通讯。还引入了numpy与matplotlib模块用于处理数据并画图。

系统技术测试

经过测试,光强探测器运行一切正常。探测器的探头可以扫描到探头上方整个空间,并且能够绘制出空间中的光强分布。如下图所示:

./pic/002.png

经过多次测试,探测器的光源寻找功能也能正常工作。能够用树莓派把拍摄到的照片传到网页上。如下所示:

./pic/002.png

我们根据探测到的光强的范围,设置了一个光强标准,光强范围为0~1000,数值越大,广强越强。

软件源代码

arduino部分

f7.h

  1. #include <Servo.h>
  2. struct pw
  3. {
  4. int x;
  5. int y;
  6. float l;
  7. struct pw * next;
  8. };
  9. struct pw * h = NULL;
  10. int a0;
  11. int a1;
  12. int b0;
  13. int b1;
  14. Servo s0;
  15. Servo s1;
  16. char com;

f7.ino

  1. #include "f7.h"
  2. void readlight();
  3. void findmaxq();
  4. void saomiao();
  5. void sf();
  6. void showlight();
  7. void setup()
  8. {
  9. // 初始化舵机和串口
  10. s0.attach(7);
  11. s1.attach(8);
  12. Serial.begin(9600);
  13. }
  14. void loop()
  15. {
  16. if (Serial.available())
  17. {
  18. com=Serial.read();
  19. switch (com)
  20. {
  21. case 's':
  22. saomiao();
  23. break;
  24. case 'f':
  25. findmaxq();
  26. break;
  27. case 'q':
  28. sf();
  29. break;
  30. case 'l':
  31. showlight();
  32. break;
  33. default:
  34. Serial.println("bad com");
  35. break;
  36. }
  37. }
  38. }
  39. void readlight()
  40. {
  41. // 光强获取函数。
  42. a0=analogRead(A0)-5;
  43. a1=analogRead(A1)+5;
  44. b0=analogRead(A2)-5;
  45. b1=analogRead(A3)+5;
  46. }
  47. void saomiao()
  48. {
  49. // 光强扫描函数
  50. float l;
  51. int i,j;
  52. for( i = 0; i < 180; i++)
  53. {
  54. for( j = 0; j < 180; j++)
  55. {
  56. delay(10);
  57. s0.write(i);
  58. delay(10);
  59. s1.write(j);
  60. delay(5);
  61. readlight();
  62. l=(a0+a1+b0+b1);
  63. Serial.print(l,2);
  64. Serial.print(',');
  65. }
  66. Serial.print('\n');
  67. delay(10);
  68. }
  69. }
  70. void findmaxq()
  71. {
  72. // 空间光强极值获取函数。
  73. int flog1=0;
  74. int flog2=0;
  75. int i=90;
  76. int j=90;
  77. s0.write(i);
  78. s1.write(j);
  79. for(int ip = 0; ip <= 4096; ip++)
  80. {
  81. readlight();
  82. delay(2);
  83. if (a0>(a1+3))
  84. {
  85. // i++;
  86. if (i<=179)
  87. {
  88. i++;
  89. }
  90. else
  91. {
  92. flog1=1;
  93. }
  94. }
  95. if (a0<(a1-3))
  96. {
  97. // i--;
  98. if (i>=1)
  99. {
  100. i--;
  101. Serial.print("*");
  102. }
  103. else
  104. {
  105. flog1=1;
  106. }
  107. }
  108. if ((a0<=(a1+3))&&(a0>=(a1-3)))
  109. {
  110. flog1=1;
  111. }
  112. Serial.print(i);
  113. Serial.print(',');
  114. Serial.print(flog1);
  115. Serial.print(',');
  116. s0.write(i);
  117. if (b0>(b1+3))
  118. {
  119. // j++;
  120. if (j<=179)
  121. {
  122. j++;
  123. }
  124. else
  125. {
  126. flog2=1;
  127. }
  128. }
  129. if (b0<=(b1-3))
  130. {
  131. // i--;
  132. if (j>=1)
  133. {
  134. j--;
  135. }
  136. else
  137. {
  138. flog2=1;
  139. }
  140. }
  141. if (b0<=(b1+3)&&b0>=(b1-3))
  142. {
  143. flog2=1;
  144. }
  145. Serial.print(flog2);
  146. Serial.print(',');
  147. Serial.println(j);
  148. s1.write(j);
  149. if (flog1==1&&flog2==1)
  150. {
  151. Serial.println('p');
  152. Serial.print(i);
  153. Serial.print(',');
  154. Serial.print(j);
  155. break;
  156. }
  157. }
  158. }
  159. void sf()
  160. {
  161. // 光强极大值扫描函数
  162. float l,ml;
  163. int i,j;
  164. int mx,my;
  165. ml=100000;
  166. mx=0;
  167. my=0;
  168. for( i = 0; i <= 180; i=i+10)
  169. {
  170. for( j = 0; j < 180; j=j+10)
  171. {
  172. delay(10);
  173. s0.write(i);
  174. delay(10);
  175. s1.write(j);
  176. delay(100);
  177. readlight();
  178. l=(a0+a1+b0+b1);
  179. // Serial.print(l,2);
  180. // Serial.print(',');
  181. if (ml>=l)
  182. {
  183. ml=l;
  184. mx=i;
  185. my=j;
  186. }
  187. }
  188. // Serial.print('\n');
  189. delay(10);
  190. }
  191. delay(1000);
  192. s0.write(mx);
  193. s1.write(my);
  194. delay(20);
  195. Serial.println('p');
  196. }
  197. void showlight()
  198. {
  199. float l;
  200. readlight();
  201. delay(10);
  202. l=a0+a1+b0+b1;
  203. Serial.println(l);
  204. delay(2);
  205. }

树莓派部分

ardlib.py

单片机操纵模块

  1. #!/usr/bin/env python3
  2. import serial
  3. import numpy as np
  4. import os
  5. import time
  6. import datetime
  7. import shutil
  8. import matplotlib
  9. matplotlib.use("Pdf")
  10. import matplotlib.pyplot as plt
  11. class ard(object):
  12. #面向对象的编程语言
  13. def __init__(self):#初始化函数
  14. self.ser=serial.Serial('/dev/ttyUSB0', 9600)
  15. time.sleep(1)
  16. sser=self.ser
  17. try:
  18. sser.open()
  19. except:
  20. pass
  21. def saomiao(self):#扫描函数
  22. nmp=np.zeros([180,180])
  23. self.nowTime=str(datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
  24. sser=self.ser
  25. sser.write('s'.encode())
  26. sser.flushInput()
  27. for i in range(0,180):
  28. time.sleep(1)
  29. shi = str(sser.readline())
  30. shi=shi[2:len(shi)-5]
  31. shi=shi.strip()
  32. shi=shi.split(',')
  33. nmp[i,:]=np.array(shi)
  34. pass
  35. p=os.path.abspath('.')
  36. ba=os.popen('mkdir '+p+'/static/res2/'+self.nowTime)
  37. time.sleep(0.001)
  38. plt.figure(dpi=500)
  39. plt.imshow(nmp)
  40. plt.title('space')
  41. plt.colorbar()
  42. plt.savefig('data.jpg')
  43. b=os.popen('cp data.jpg '+p+'/static/res2/'+self.nowTime)
  44. shutil.copyfile('./static/res2.html','./static/res2/'+self.nowTime+r'/res.html')
  45. def qfm(self):#光强最大值寻找函数
  46. self.nowTime=str(datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
  47. sser=self.ser
  48. time.sleep(1)
  49. sser.write(b'q')
  50. self.takephoto()
  51. p=os.path.abspath('.')
  52. print(p)
  53. p2=os.path.join(p,'/static/res1/'+self.nowTime+r'/res1.html')
  54. b=os.popen('mkdir '+p+'/static/res1/'+self.nowTime)
  55. time.sleep(0.001)
  56. a=os.popen('touch '+p+p2)
  57. print(b)
  58. print(a)
  59. print(p+p2)
  60. with open(p+p2,'w') as f:
  61. f.write('dog\n'+self.nowTime)
  62. shutil.copyfile('./static/res1.html','./static/res1/'+self.nowTime+r'/res.html')
  63. def takephoto(self):#拍照函数
  64. timedog=time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
  65. str = os.popen("echo takephoto"+timedog).read()
  66. print(str)
  67. pass
  68. def test(self):
  69. print("OK")
  70. pass
  71. pass

testlib.py

测试模块

  1. mport time
  2. import datetime
  3. import os
  4. import shutil
  5. class testlib(object):
  6. def __init__(self):
  7. self.nowTime=str(datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
  8. pass
  9. def tes1(self):
  10. self.nowTime=str(datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
  11. print("开始执行全域扫描")
  12. p=os.path.abspath('.')
  13. print(p)
  14. p2=os.path.join(p,'/static/res1/'+self.nowTime+r'/res1.html')
  15. # os.mkdir(os.path.join(p,'/static/res1/'+self.nowTime))
  16. b=os.popen('mkdir '+p+'/static/res1/'+self.nowTime)
  17. time.sleep(0.001)
  18. a=os.popen('touch '+p+p2)
  19. print(b)
  20. print(a)
  21. print(p+p2)
  22. with open(p+p2,'w') as f:
  23. f.write('dog\n'+self.nowTime)
  24. shutil.copyfile('./static/res1.html','./static/res1/'+self.nowTime+r'/res.html')
  25. def tes2(self):
  26. print("开始寻找光污染源")
  27. pass
  28. def test3(self):
  29. return self.nowTime
  30. pass

fweb.py

互联网模块

  1. import numpy as np
  2. from flask import (Flask, abort, flash, redirect, render_template, request,
  3. url_for)
  4. import datetime
  5. from ardlib import *
  6. from testlib import *
  7. import json
  8. import os
  9. app = Flask(__name__)
  10. @app.route('/')
  11. def hello_world():
  12. nowTime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  13. a=[]
  14. a.append(nowTime)
  15. a.append("you are a dog")
  16. return render_template('test.html',time=a)
  17. @app.route('/fweb',methods=['GET', 'POST'])
  18. def fweb():
  19. if request.method == 'POST':
  20. s.tes1()
  21. else:
  22. abort(403)
  23. # s=ard()
  24. # s.qfm()
  25. # abort(401)
  26. return "dog"
  27. @app.route('/test1/',methods=['GET', 'POST'])
  28. def test1():
  29. if request.method == 'POST':
  30. print("666")
  31. s.tes1()
  32. return redirect(url_for('hello_world'))
  33. else:
  34. abort(403)
  35. @app.route('/test2/',methods=['GET', 'POST'])
  36. def test2():
  37. if request.method == 'POST':
  38. s.tes2()
  39. else:
  40. abort(403)
  41. pass
  42. @app.route('/real1',methods=['GET','POST'])
  43. def real1():
  44. if request.method == 'POST':
  45. ss=ard()
  46. a=ss.qfm()
  47. return redirect(url_for('hello_world'))
  48. else:
  49. abort(403)
  50. @app.route('/real2',methods=['GET','POST'])
  51. def real2():
  52. if request.method == 'POST':
  53. ss=ard()
  54. time.sleep(1)
  55. a=ss.saomiao()
  56. return redirect(url_for('hello_world'))
  57. else:
  58. abort(403)
  59. @app.route('/res1')
  60. def res1():
  61. a=os.listdir('./static/res1/')
  62. bu=sorted(a,reverse = True)
  63. return render_template('result.html',urls=bu)
  64. @app.route('/res2')
  65. def res2():
  66. a=os.listdir('./static/res2/')
  67. bu=sorted(a,reverse = True)
  68. return render_template('result2.html',urls=bu)
  69. if __name__ == '__main__':
  70. app.run(debug=True,host='0.0.0.0')

HTML部分

result.html

结果显示模块

  1. <!DOCTYPE html>
  2. 光强测试结果
  3. <ul>
  4. {% for url in urls %}
  5. <li>{{url}}</li><a href="/static/res1/{{url}}/res1.html">{{url}}</a>
  6. {% endfor %}
  7. </ul>
  1. <!DOCTYPE html>
  2. 全域扫描结果
  3. <ul>
  4. {% for url in urls %}
  5. <li>{{url}}</li><a href="/static/res2/{{url}}/res.html">{{url}}</a>
  6. {% endfor %}
  7. </ul>

test.html

测试页

  1. <!DOCTYPE html>
  2. <!-- <form>
  3. First name: <input type="text" name="firstname"><br>
  4. Last name: <input type="text" name="lastname">
  5. </form>
  6. <form name="input" action="/fweb" method="post">
  7. Username: <input type="text" name="user">
  8. <input type="submit" value="Submit">
  9. </form>
  10. <title>My Application</title>
  11. {% with messages = get_flashed_messages() %}
  12. {% if messages %}
  13. <ul class=flashes>
  14. {% for message in messages %}
  15. <li>{{ message }}</li>
  16. {% endfor %}
  17. </ul>
  18. {% endif %}
  19. {% endwith %}
  20. {% block body %}{% endblock %} -->
  21. <html>
  22. <!-- <head>
  23. <meta charset="utf-8">
  24. <title>菜鸟教程(runoob.com)</title>
  25. </head> -->
  26. <body>
  27. <form action="/test1" method="post" onclick="alert('光强测试请求已受理,请稍后查看结果。')">
  28. <input type="button" value="开始光强测试">
  29. </form>
  30. </body>
  31. </html>
  32. <button type="button" action="/fweb" method="post" onclick="alert('你好,世界!')">点我!</button>
  33. <form name="input" action="/test1" method="post" onclick="alert('光强测试请求已受理,请稍后查看结果。')">
  34. <input type="submit" value="Submit">
  35. </form>
  36. <form name="input" action="/real1" method="post" onclick="alert('光强测试请求已受理,请稍后查看结果。')">
  37. <input type="submit" value="光源寻找">
  38. </form>
  39. <form name="input" action="/real2" method="post" onclick="alert('光强测试请求已受理,请稍后查看结果。')">
  40. <input type="submit" value="全域扫描">
  41. </form>
  42. <a href="/res1">光源寻找结果</a>
  43. <br>
  44. <a href="/res2">全域扫描结果</a>

index.html

主页

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <meta name="description" content="None">
  8. <link rel="shortcut icon" href="./img/favicon.ico">
  9. <title>光强传感器</title>
  10. <link href="./css/bootstrap-custom.min.css" rel="stylesheet">
  11. <link href="./css/font-awesome-4.5.0.css" rel="stylesheet">
  12. <link href="./css/base.css" rel="stylesheet">
  13. <link rel="stylesheet" href="./css/highlight.css">
  14. <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
  15. <!--[if lt IE 9]>
  16. <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
  17. <script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
  18. <![endif]-->
  19. <script src="./js/jquery-1.10.2.min.js"></script>
  20. <script src="./js/bootstrap-3.0.3.min.js"></script>
  21. <script src="./js/highlight.pack.js"></script>
  22. </head>
  23. <body class="homepage">
  24. <div class="navbar navbar-default navbar-fixed-top" role="navigation">
  25. <div class="container">
  26. <!-- Collapsed navigation -->
  27. <div class="navbar-header">
  28. <a class="navbar-brand" href=".">光强传感器</a>
  29. </div>
  30. <!-- Expanded navigation -->
  31. <div class="navbar-collapse collapse">
  32. <ul class="nav navbar-nav navbar-right">
  33. <li>
  34. <a href="#" data-toggle="modal" data-target="#mkdocs_search_modal">
  35. <i class="fa fa-search"></i> Search
  36. </a>
  37. </li>
  38. </ul>
  39. </div>
  40. </div>
  41. </div>
  42. <div class="container">
  43. <div class="col-md-3"><div class="bs-sidebar hidden-print affix well" role="complementary">
  44. <ul class="nav bs-sidenav">
  45. <li class="main active"><a href="#_1">系统控制与结果查询</a></li>
  46. <li class="main "><a href="#_2">系统设计</a></li>
  47. <li><a href="#_3">设计目的</a></li>
  48. <li class="main "><a href="#_4">系统功能</a></li>
  49. <li class="main "><a href="#_5">主要硬件介绍</a></li>
  50. <li><a href="#arduino-uno">Arduino uno 开发板:</a></li>
  51. <li><a href="#3b">树莓派3代B+型</a></li>
  52. <li><a href="#sg90">SG90舵机</a></li>
  53. <li><a href="#gy-30">光强模块GY-30</a></li>
  54. <li class="main "><a href="#_6">硬件设计思路和电路图</a></li>
  55. <li><a href="#_7">硬件设计思路</a></li>
  56. <li><a href="#_8">电路图</a></li>
  57. <li><a href="#_9">探测器整体图</a></li>
  58. <li class="main "><a href="#_10">软件设计思路</a></li>
  59. <li><a href="#_11">软件设计</a></li>
  60. <li class="main "><a href="#_13">系统技术测试</a></li>
  61. <li class="main "><a href="#_14">软件源代码</a></li>
  62. <li><a href="#arduino_1">arduino部分</a></li>
  63. <li><a href="#_15">树莓派部分</a></li>
  64. <li><a href="#html">HTML部分</a></li>
  65. </ul>
  66. </div></div>
  67. <div class="col-md-9" role="main">
  68. <h1 id="_1">系统控制与结果查询</h1>
  69. <form name="input" action="/real1" method="post" onclick="alert('光强测试请求已受理,请稍后查看结果。')">
  70. <input type="submit" value="光源寻找">
  71. </form>
  72. <form name="input" action="/real2" method="post" onclick="alert('光强测试请求已受理,请稍后查看结果。')">
  73. <input type="submit" value="全域扫描">
  74. </form>
  75. <a href="/res1">光源寻找结果</a>
  76. <br>
  77. <a href="/res2">全域扫描结果</a>
  78. <h1 id="_2">系统设计</h1>
  79. <h2 id="_3">设计目的</h2>
  80. <p>光污染是继废水、废气、废渣和噪声等污染之后的一种新的环境污染源。光污染不仅仅严重影响了人类的身体健康和其他生物的自然生活规律。除此之外,光污染还对科学探测特别是天文观测产生了严重的影响。</p>
  81. <p>新《中华人民共和国环境保护法》(2015年)中第四十二条明确规定,要防治企业事业单位在生产中产生的包括光污染在内的多种污染。由此可见,我国渐渐认识到了光污染的严重危害,保护环境、治理光污染势在必行。</p>
  82. <p>我们利用在单片机课程上学到的知识,用arduino单片机制作一个可以对光污染进行探测和分析的探测仪器,不仅可以帮助人们保护身体健康,践行绿色环保可持续的发展理念,而且可以在科学探测中保障探测结果的准确。</p>
  83. <h1 id="_4">系统功能</h1>
  84. <p>基于arduino的光污染探测分析仪具备以下功能:</p>
  85. <ol>
  86. <li>
  87. <p>对整个空间的光强情况进行扫描,寻找出空间中光强最大的方向,也就是污染源的方向。</p>
  88. </li>
  89. <li>
  90. <p>对于光强探测的结果,既能输出准确的数值,也能进行可视化输出。</p>
  91. </li>
  92. <li>
  93. <p>网络查询功能:用树莓派做一个服务器,搭建一个网页,用户可以在网页上对探测器进行控制,探测器可以把扫描得到的空间中的光强分布以图片的形式发送到网页上,并且可以对光强最大的污染源拍照并且发送到网页上,方便用户查询。</p>
  94. </li>
  95. </ol>
  96. <h1 id="_5">主要硬件介绍</h1>
  97. <h2 id="arduino-uno">Arduino uno 开发板:</h2>
  98. <p>以ATmega328 MCU控制器为基础,具备14路数字输入/输出引脚(其中6路可用于PWM输出)、6路模拟输入、一个16MHz陶瓷谐振器、一个USB接口、一个电源插座、一个ICSP接头和一个复位按钮。它采用Atmega16U2芯片进行USB到串行数据的转换。它包含了组成微控制器的所有结构,同时,只需要一条USB数据线连接至电脑。目前,Arduino Uno已成为Arduino主推的产品。</p>
  99. <p><img alt="1" src="./pic/001.png" /></p>
  100. <h2 id="3b">树莓派3代B+型</h2>
  101. <p>这是树莓派的最新型号,主要参数chaxub如下:</p>
  102. <ul>
  103. <li>
  104. <p>博通BCM2837B0 SoC,集成四核ARM Cortex-A53(ARMv8)64位@ 1.4GHz CPU,集成博通 Videocore-IV GPU</p>
  105. </li>
  106. <li>
  107. <p>内存:1GB LPDDR2 SDRAM</p>
  108. </li>
  109. <li>
  110. <p>有线网络:千兆以太网(通过USB2.0通道,最大吞吐量 300Mbps)</p>
  111. </li>
  112. <li>
  113. <p>无线网络:2.4GHz和5GHz 双频Wi-Fi,支持802.11b/g/n/ac</p>
  114. </li>
  115. <li>
  116. <p>蓝牙:蓝牙4.2&低功耗蓝牙(BLE)</p>
  117. </li>
  118. <li>
  119. <p>存储:Micro-SD</p>
  120. </li>
  121. <li>
  122. <p>其他接口:HDMI,3.5mm模拟音频视频插孔,4x USB 2.0,以太网,摄像机串行接口(CSI),显示器串行接口(DSI),MicroSD卡座,40pin扩展双排插针</p>
  123. </li>
  124. <li>
  125. <p>尺寸:82mmx 56mmx 19.5mm,50克</p>
  126. </li>
  127. </ul>
  128. <p><img alt="./pic/002.png" src="./pic/002.png" /></p>
  129. <h2 id="sg90">SG90舵机</h2>
  130. <ul>
  131. <li>反应转速:0.12~0.13秒/60°;</li>
  132. <li>工作扭矩:1.6KG/CM;</li>
  133. <li>转动角度:180°</li>
  134. </ul>
  135. <p><img alt="./pic/002.png" src="./pic/007.png" /></p>
  136. <h2 id="gy-30">光强模块GY-30</h2>
  137. <p>通过光敏电阻的原理对光强进行探测,光强越大,电流越小。</p>
  138. <p><img alt="./pic/002.png" src="./pic/003.png" /></p>
  139. <h1 id="_6">硬件设计思路和电路图</h1>
  140. <h2 id="_7">硬件设计思路</h2>
  141. <ol>
  142. <li>将两个舵机按照转轴相互垂直的方向粘合起来,再把四个光强传感器组成探头粘在上方的舵机的转臂上。这样使得探头能够具有四个自由度,可以扫描整个空间。</li>
  143. <li>舵机的转速通过arduino的PWM输出引脚输出PWM信号控制。、</li>
  144. <li>在探头上加一个树莓派专用的摄像头,用于对光污染源拍照。将摄像头和树莓派相连,从而可以将拍到的照片通过树莓派传输的网页端。</li>
  145. </ol>
  146. <h2 id="_8">电路图</h2>
  147. <h2 id="_9">探测器整体图</h2>
  148. <p><img alt="./pic/002.png" src="./pic/004.png" /></p>
  149. <h1 id="_10">软件设计思路</h1>
  150. <h2 id="_11">软件设计</h2>
  151. <p>项目主要硬件可以划分为两个部分。arduino部分和raspberry pi部分。两个部分分别是不同的硬件架构。arduino是基于avr的单片机,然而,树莓派是基于arm架构的cpu。
  152. 硬件架构的不同导致主要的软件也被划分两部分,再加上显示在用户端的HTML,本项目的软件部分可以划分为三部分。</p>
  153. <h3 id="arduino">arduino部分</h3>
  154. <p>arduino主要负责硬件的控制与软件的采集。arduino通过串口接受来自树莓派的指令。</p>
  155. <p>当arduino接收到树莓派的执行光污染源寻找指令时。arduino执行相关函数,首先通过pwm信号操纵水平方向舵机与竖直方向舵机进行转动,转动到初始位置。然后读取光强传感器阵列的数据。之后主次扫描过均匀分布的100各点。并从中找到光强最大的点,并记录位置。然后控制舵机旋转至该位置,然后传送信息至arduino。</p>
  156. <p>当arduino接收到进行全角度扫描时,arduino将控制舵机转动至初始位置。然后开始对空间当中逐点扫描,每转到一个空间位置,遂及测量该点的亮度。并实时传回树莓派,直到遍历空间之中的所有点。扫描完毕之后将结束信号回传。</p>
  157. <h3 id="_12">树莓派部分</h3>
  158. <p>树莓派作为整个仪器与用户之间的接口,是非常重要的。树莓派对外作为一个web服务器,负责给用户的浏览器分发内容,通过互联网接受用户指令,并根据不同指令返回用户结果或操纵单片机进行相关动作的执行。</p>
  159. <h4 id="webnginxuwsigflask">web服务端采用nginx+uwsig+flask架构</h4>
  160. <p>Nginx 是一个很强大的轻量级的高性能Web和反向代理服务器。非常适合在树莓派上用于搭建小型网站。</p>
  161. <p>uWSGI是一个Web服务器,它实现了WSGI协议、uwsgi、http等协议。Nginx中HttpUwsgiModule的作用是与uWSGI服务器进行交换。WSGI是一种Web服务器网关接口。它是一个Web服务器(如nginx,uWSGI等服务器)与web应用(如用Flask框架写的程序)通信的一种规范。uwsgi用于nginx与flask之间的通讯。</p>
  162. <p>Flask是一个Python编写的Web 微框架,让我们可以使用Python语言快速实现一个网站或Web服务。flask在本项目之中可以用作动态网页服务器。用户浏览器发出的指令是由flask处理并执行。</p>
  163. <p>本项目之中树莓派与arduino之间的互动是由python语言完成的。在python中导入了pyserial串口通讯模块用于与arduino通讯。还引入了numpy与matplotlib模块用于处理数据并画图。</p>
  164. <h1 id="_13">系统技术测试</h1>
  165. <p>经过测试,光强探测器运行一切正常。探测器的探头可以扫描到探头上方整个空间,并且能够绘制出空间中的光强分布。如下图所示:</p>
  166. <p><img alt="./pic/002.png" src="./pic/005.png" /></p>
  167. <p>经过多次测试,探测器的光源寻找功能也能正常工作。能够用树莓派把拍摄到的照片传到网页上。如下所示:</p>
  168. <p><img alt="./pic/002.png" src="./pic/008.png" /></p>
  169. <p>我们根据探测到的光强的范围,设置了一个光强标准,光强范围为0~1000,数值越大,广强越强。</p>
  170. <h1 id="_14">软件源代码</h1>
  171. <h2 id="arduino_1">arduino部分</h2>
  172. <h3 id="f7h">f7.h</h3>
  173. <pre><code class="C">#include <Servo.h>
  174. struct pw
  175. {
  176. int x;
  177. int y;
  178. float l;
  179. struct pw * next;
  180. };
  181. struct pw * h = NULL;
  182. int a0;
  183. int a1;
  184. int b0;
  185. int b1;
  186. Servo s0;
  187. Servo s1;
  188. char com;
  189. </code></pre>
  190. <h3 id="f7ino">f7.ino</h3>
  191. <pre><code class="C">#include "f7.h"
  192. void readlight();
  193. void findmaxq();
  194. void saomiao();
  195. void sf();
  196. void showlight();
  197. void setup()
  198. {
  199. // 初始化舵机和串口
  200. s0.attach(7);
  201. s1.attach(8);
  202. Serial.begin(9600);
  203. }
  204. void loop()
  205. {
  206. if (Serial.available())
  207. {
  208. com=Serial.read();
  209. switch (com)
  210. {
  211. case 's':
  212. saomiao();
  213. break;
  214. case 'f':
  215. findmaxq();
  216. break;
  217. case 'q':
  218. sf();
  219. break;
  220. case 'l':
  221. showlight();
  222. break;
  223. default:
  224. Serial.println("bad com");
  225. break;
  226. }
  227. }
  228. }
  229. void readlight()
  230. {
  231. // 光强获取函数。
  232. a0=analogRead(A0)-5;
  233. a1=analogRead(A1)+5;
  234. b0=analogRead(A2)-5;
  235. b1=analogRead(A3)+5;
  236. }
  237. void saomiao()
  238. {
  239. // 光强扫描函数
  240. float l;
  241. int i,j;
  242. for( i = 0; i < 180; i++)
  243. {
  244. for( j = 0; j < 180; j++)
  245. {
  246. delay(10);
  247. s0.write(i);
  248. delay(10);
  249. s1.write(j);
  250. delay(5);
  251. readlight();
  252. l=(a0+a1+b0+b1);
  253. Serial.print(l,2);
  254. Serial.print(',');
  255. }
  256. Serial.print('\n');
  257. delay(10);
  258. }
  259. }
  260. void findmaxq()
  261. {
  262. // 空间光强极值获取函数。
  263. int flog1=0;
  264. int flog2=0;
  265. int i=90;
  266. int j=90;
  267. s0.write(i);
  268. s1.write(j);
  269. for(int ip = 0; ip <= 4096; ip++)
  270. {
  271. readlight();
  272. delay(2);
  273. if (a0>(a1+3))
  274. {
  275. // i++;
  276. if (i<=179)
  277. {
  278. i++;
  279. }
  280. else
  281. {
  282. flog1=1;
  283. }
  284. }
  285. if (a0<(a1-3))
  286. {
  287. // i--;
  288. if (i>=1)
  289. {
  290. i--;
  291. Serial.print("*");
  292. }
  293. else
  294. {
  295. flog1=1;
  296. }
  297. }
  298. if ((a0<=(a1+3))&&(a0>=(a1-3)))
  299. {
  300. flog1=1;
  301. }
  302. Serial.print(i);
  303. Serial.print(',');
  304. Serial.print(flog1);
  305. Serial.print(',');
  306. s0.write(i);
  307. if (b0>(b1+3))
  308. {
  309. // j++;
  310. if (j<=179)
  311. {
  312. j++;
  313. }
  314. else
  315. {
  316. flog2=1;
  317. }
  318. }
  319. if (b0<=(b1-3))
  320. {
  321. // i--;
  322. if (j>=1)
  323. {
  324. j--;
  325. }
  326. else
  327. {
  328. flog2=1;
  329. }
  330. }
  331. if (b0<=(b1+3)&&b0>=(b1-3))
  332. {
  333. flog2=1;
  334. }
  335. Serial.print(flog2);
  336. Serial.print(',');
  337. Serial.println(j);
  338. s1.write(j);
  339. if (flog1==1&&flog2==1)
  340. {
  341. Serial.println('p');
  342. Serial.print(i);
  343. Serial.print(',');
  344. Serial.print(j);
  345. break;
  346. }
  347. }
  348. }
  349. void sf()
  350. {
  351. // 光强极大值扫描函数
  352. float l,ml;
  353. int i,j;
  354. int mx,my;
  355. ml=100000;
  356. mx=0;
  357. my=0;
  358. for( i = 0; i <= 180; i=i+10)
  359. {
  360. for( j = 0; j < 180; j=j+10)
  361. {
  362. delay(10);
  363. s0.write(i);
  364. delay(10);
  365. s1.write(j);
  366. delay(100);
  367. readlight();
  368. l=(a0+a1+b0+b1);
  369. // Serial.print(l,2);
  370. // Serial.print(',');
  371. if (ml>=l)
  372. {
  373. ml=l;
  374. mx=i;
  375. my=j;
  376. }
  377. }
  378. // Serial.print('\n');
  379. delay(10);
  380. }
  381. delay(1000);
  382. s0.write(mx);
  383. s1.write(my);
  384. delay(20);
  385. Serial.println('p');
  386. }
  387. void showlight()
  388. {
  389. float l;
  390. readlight();
  391. delay(10);
  392. l=a0+a1+b0+b1;
  393. Serial.println(l);
  394. delay(2);
  395. }
  396. </code></pre>
  397. <h2 id="_15">树莓派部分</h2>
  398. <h3 id="ardlibpy">ardlib.py</h3>
  399. <p>单片机操纵模块</p>
  400. <pre><code class="python">#!/usr/bin/env python3
  401. import serial
  402. import numpy as np
  403. import os
  404. import time
  405. import datetime
  406. import shutil
  407. import matplotlib
  408. matplotlib.use("Pdf")
  409. import matplotlib.pyplot as plt
  410. class ard(object):
  411. #面向对象的编程语言
  412. def __init__(self):#初始化函数
  413. self.ser=serial.Serial('/dev/ttyUSB0', 9600)
  414. time.sleep(1)
  415. sser=self.ser
  416. try:
  417. sser.open()
  418. except:
  419. pass
  420. def saomiao(self):#扫描函数
  421. nmp=np.zeros([180,180])
  422. self.nowTime=str(datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
  423. sser=self.ser
  424. sser.write('s'.encode())
  425. sser.flushInput()
  426. for i in range(0,180):
  427. time.sleep(1)
  428. shi = str(sser.readline())
  429. shi=shi[2:len(shi)-5]
  430. shi=shi.strip()
  431. shi=shi.split(',')
  432. nmp[i,:]=np.array(shi)
  433. pass
  434. p=os.path.abspath('.')
  435. ba=os.popen('mkdir '+p+'/static/res2/'+self.nowTime)
  436. time.sleep(0.001)
  437. plt.figure(dpi=500)
  438. plt.imshow(nmp)
  439. plt.title('space')
  440. plt.colorbar()
  441. plt.savefig('data.jpg')
  442. b=os.popen('cp data.jpg '+p+'/static/res2/'+self.nowTime)
  443. shutil.copyfile('./static/res2.html','./static/res2/'+self.nowTime+r'/res.html')
  444. def qfm(self):#光强最大值寻找函数
  445. self.nowTime=str(datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
  446. sser=self.ser
  447. time.sleep(1)
  448. sser.write(b'q')
  449. self.takephoto()
  450. p=os.path.abspath('.')
  451. print(p)
  452. p2=os.path.join(p,'/static/res1/'+self.nowTime+r'/res1.html')
  453. b=os.popen('mkdir '+p+'/static/res1/'+self.nowTime)
  454. time.sleep(0.001)
  455. a=os.popen('touch '+p+p2)
  456. print(b)
  457. print(a)
  458. print(p+p2)
  459. with open(p+p2,'w') as f:
  460. f.write('dog\n'+self.nowTime)
  461. shutil.copyfile('./static/res1.html','./static/res1/'+self.nowTime+r'/res.html')
  462. def takephoto(self):#拍照函数
  463. timedog=time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
  464. str = os.popen("echo takephoto"+timedog).read()
  465. print(str)
  466. pass
  467. def test(self):
  468. print("OK")
  469. pass
  470. pass
  471. </code></pre>
  472. <h3 id="testlibpy">testlib.py</h3>
  473. <p>测试模块</p>
  474. <pre><code class="python">mport time
  475. import datetime
  476. import os
  477. import shutil
  478. class testlib(object):
  479. def __init__(self):
  480. self.nowTime=str(datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
  481. pass
  482. def tes1(self):
  483. self.nowTime=str(datetime.datetime.now().strftime('%Y%m%d%H%M%S'))
  484. print("开始执行全域扫描")
  485. p=os.path.abspath('.')
  486. print(p)
  487. p2=os.path.join(p,'/static/res1/'+self.nowTime+r'/res1.html')
  488. # os.mkdir(os.path.join(p,'/static/res1/'+self.nowTime))
  489. b=os.popen('mkdir '+p+'/static/res1/'+self.nowTime)
  490. time.sleep(0.001)
  491. a=os.popen('touch '+p+p2)
  492. print(b)
  493. print(a)
  494. print(p+p2)
  495. with open(p+p2,'w') as f:
  496. f.write('dog\n'+self.nowTime)
  497. shutil.copyfile('./static/res1.html','./static/res1/'+self.nowTime+r'/res.html')
  498. def tes2(self):
  499. print("开始寻找光污染源")
  500. pass
  501. def test3(self):
  502. return self.nowTime
  503. pass
  504. </code></pre>
  505. <h3 id="fwebpy">fweb.py</h3>
  506. <p>互联网模块</p>
  507. <pre><code class="python">
  508. import numpy as np
  509. from flask import (Flask, abort, flash, redirect, render_template, request,
  510. url_for)
  511. import datetime
  512. from ardlib import *
  513. from testlib import *
  514. import json
  515. import os
  516. app = Flask(__name__)
  517. @app.route('/')
  518. def hello_world():
  519. nowTime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  520. a=[]
  521. a.append(nowTime)
  522. a.append("you are a dog")
  523. return render_template('test.html',time=a)
  524. @app.route('/fweb',methods=['GET', 'POST'])
  525. def fweb():
  526. if request.method == 'POST':
  527. s.tes1()
  528. else:
  529. abort(403)
  530. # s=ard()
  531. # s.qfm()
  532. # abort(401)
  533. return "dog"
  534. @app.route('/test1/',methods=['GET', 'POST'])
  535. def test1():
  536. if request.method == 'POST':
  537. print("666")
  538. s.tes1()
  539. return redirect(url_for('hello_world'))
  540. else:
  541. abort(403)
  542. @app.route('/test2/',methods=['GET', 'POST'])
  543. def test2():
  544. if request.method == 'POST':
  545. s.tes2()
  546. else:
  547. abort(403)
  548. pass
  549. @app.route('/real1',methods=['GET','POST'])
  550. def real1():
  551. if request.method == 'POST':
  552. ss=ard()
  553. a=ss.qfm()
  554. return redirect(url_for('hello_world'))
  555. else:
  556. abort(403)
  557. @app.route('/real2',methods=['GET','POST'])
  558. def real2():
  559. if request.method == 'POST':
  560. ss=ard()
  561. time.sleep(1)
  562. a=ss.saomiao()
  563. return redirect(url_for('hello_world'))
  564. else:
  565. abort(403)
  566. @app.route('/res1')
  567. def res1():
  568. a=os.listdir('./static/res1/')
  569. bu=sorted(a,reverse = True)
  570. return render_template('result.html',urls=bu)
  571. @app.route('/res2')
  572. def res2():
  573. a=os.listdir('./static/res2/')
  574. bu=sorted(a,reverse = True)
  575. return render_template('result2.html',urls=bu)
  576. if __name__ == '__main__':
  577. app.run(debug=True,host='0.0.0.0')
  578. </code></pre>
  579. <h2 id="html">HTML部分</h2>
  580. <h3 id="resulthtml">result.html</h3>
  581. <p>结果显示模块</p>
  582. <pre><code class="html"><!DOCTYPE html>
  583. 光强测试结果
  584. <ul>
  585. {% for url in urls %}
  586. <li>{{url}}</li><a href="/static/res1/{{url}}/res1.html">{{url}}</a>
  587. {% endfor %}
  588. </ul>
  589. </code></pre>
  590. <pre><code class="html"><!DOCTYPE html>
  591. 全域扫描结果
  592. <ul>
  593. {% for url in urls %}
  594. <li>{{url}}</li><a href="/static/res2/{{url}}/res.html">{{url}}</a>
  595. {% endfor %}
  596. </ul>
  597. </code></pre>
  598. <h3 id="testhtml">test.html</h3>
  599. <p>测试页</p>
  600. <pre><code class="html"><!DOCTYPE html>
  601. <!-- <form>
  602. First name: <input type="text" name="firstname"><br>
  603. Last name: <input type="text" name="lastname">
  604. </form>
  605. <form name="input" action="/fweb" method="post">
  606. Username: <input type="text" name="user">
  607. <input type="submit" value="Submit">
  608. </form>
  609. <title>My Application</title>
  610. {% with messages = get_flashed_messages() %}
  611. {% if messages %}
  612. <ul class=flashes>
  613. {% for message in messages %}
  614. <li>{{ message }}</li>
  615. {% endfor %}
  616. </ul>
  617. {% endif %}
  618. {% endwith %}
  619. {% block body %}{% endblock %} -->
  620. <html>
  621. <!-- <head>
  622. <meta charset="utf-8">
  623. <title>菜鸟教程(runoob.com)</title>
  624. </head> -->
  625. <body>
  626. <form action="/test1" method="post" onclick="alert('光强测试请求已受理,请稍后查看结果。')">
  627. <input type="button" value="开始光强测试">
  628. </form>
  629. </body>
  630. </html>
  631. <button type="button" action="/fweb" method="post" onclick="alert('你好,世界!')">点我!</button>
  632. <form name="input" action="/test1" method="post" onclick="alert('光强测试请求已受理,请稍后查看结果。')">
  633. <input type="submit" value="Submit">
  634. </form>
  635. <form name="input" action="/real1" method="post" onclick="alert('光强测试请求已受理,请稍后查看结果。')">
  636. <input type="submit" value="光源寻找">
  637. </form>
  638. <form name="input" action="/real2" method="post" onclick="alert('光强测试请求已受理,请稍后查看结果。')">
  639. <input type="submit" value="全域扫描">
  640. </form>
  641. <a href="/res1">光源寻找结果</a>
  642. <br>
  643. <a href="/res2">全域扫描结果</a>
  644. </code></pre></div>
  645. </div>
  646. <footer class="col-md-12">
  647. <hr>
  648. <p>Documentation built with <a href="http://www.mkdocs.org/">MkDocs</a>.</p>
  649. </footer>
  650. <script>var base_url = '.';</script>
  651. <script src="./js/base.js"></script>
  652. <script src="./search/require.js"></script>
  653. <script src="./search/search.js"></script>
  654. <div class="modal" id="mkdocs_search_modal" tabindex="-1" role="dialog" aria-labelledby="Search Modal" aria-hidden="true">
  655. <div class="modal-dialog">
  656. <div class="modal-content">
  657. <div class="modal-header">
  658. <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
  659. <h4 class="modal-title" id="exampleModalLabel">Search</h4>
  660. </div>
  661. <div class="modal-body">
  662. <p>
  663. From here you can search these documents. Enter
  664. your search terms below.
  665. </p>
  666. <form role="form">
  667. <div class="form-group">
  668. <input type="text" class="form-control" placeholder="Search..." id="mkdocs-search-query">
  669. </div>
  670. </form>
  671. <div id="mkdocs-search-results"></div>
  672. </div>
  673. <div class="modal-footer">
  674. </div>
  675. </div>
  676. </div>
  677. </div><div class="modal" id="mkdocs_keyboard_modal" tabindex="-1" role="dialog" aria-labelledby="Keyboard Shortcuts Modal" aria-hidden="true">
  678. <div class="modal-dialog">
  679. <div class="modal-content">
  680. <div class="modal-header">
  681. <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
  682. <h4 class="modal-title" id="exampleModalLabel">Keyboard Shortcuts</h4>
  683. </div>
  684. <div class="modal-body">
  685. <table class="table">
  686. <thead>
  687. <tr>
  688. <th style="width: 20%;">Keys</th>
  689. <th>Action</th>
  690. </tr>
  691. </thead>
  692. <tbody>
  693. <tr>
  694. <td><kbd>?</kbd></td>
  695. <td>Open this help</td>
  696. </tr>
  697. <tr>
  698. <td><kbd></kbd></td>
  699. <td>Previous page</td>
  700. </tr>
  701. <tr>
  702. <td><kbd></kbd></td>
  703. <td>Next page</td>
  704. </tr>
  705. <tr>
  706. <td><kbd>s</kbd></td>
  707. <td>Search</td>
  708. </tr>
  709. </tbody>
  710. </table>
  711. </div>
  712. <div class="modal-footer">
  713. </div>
  714. </div>
  715. </div>
  716. </div>
  717. </body>
  718. </html>