抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Hello World

人よ、幸福に生きろ!

根据华东师范大学《创客实践》课程期末考查方案相关要求,ArdiFan是基于Arduino Nano 3开发板的智能软硬件系统,旨在实现风扇的智能控制,本项目为本人作业上传。

项目简介

ArdiFan是基于Arduino Nano 3开发板的智能软硬件系统,旨在实现风扇的智能控制。

ArdiFan名称Ardi取自Arduino、Remote、Detection、Intelligent的缩写,并且简要概括了该智能软硬件系统的核心功能。Fan取自项目类型,即电动风扇。

本项目包含三个组成部分:ArdiFan为主体硬件,ArdiFan.ino为arduino上运行的系统程序,
AndroidClient是蓝牙控制Android平台的app工程源文件。

本项目包含超声波、按钮与蓝牙三种控制方式:当超声波检测到距离小于40cm时,自动启动风扇;距离大于40cm并持续3秒后,风扇自动关闭;按钮控制需要在超声波检测距离小于40cm的情况下方可操作。此外,蓝牙模块可通过手机APP远程控制风扇的开闭,进一步提升使用便捷性。本系统采用直流电机和继电器进行简单、低成本的风扇操作,且具备节能、远程控制等优势,适用于公共交通和车载服务等场景。

项目背景

随着科学技术的日益发展,智能家居成为了一种趋势,从而对传统家电进行智能升级成为了一个热门话题。ArdiFan应运而生,它通过现代通信蓝牙技术为用户提供了便捷的风扇控制方式。

主要功能

1.超声波自动控制:当超声波传感器检测到距离小于40cm时,风扇自动启动;距离大于40cm且持续3秒时,风扇自动关闭。
2.按钮手动控制:当超声波检测到距离小于40cm时,用户可以通过按钮对风扇进行手动控制。
3.蓝牙远程控制:通过蓝牙模块与手机APP连接,用户在任何距离下都能远程控制风扇的开关。

应用场景

下面将以车载服务中的智能风扇控制系统为例,介绍本系统进行的简单、低成本的风扇操作,且具备节能、远程控制等优势,适用于公共交通和车载服务等场景。

在车载服务场景中,乘客的舒适度直接影响服务体验。尤其在炎热的夏季,车内的温度管理成为了提高乘客满意度的关键因素之一。传统的车内风扇控制多依赖于驾驶员手动操作或乘客的口头请求,这在实际应用中可能无法及时响应乘客的需求,尤其是在乘客密集或驾驶员注意力集中于道路情况时。

ArdiFan智能风扇控制系统针对的正是上述问题。通过在车内安装基于Arduino Nano 3开发板的ArdiFan系统,配合超声波传感器、蓝牙模块和相关控制逻辑,能够实现以下功能:

1.自动调节风扇开关:当超声波传感器检测到车内特定区域(如乘客座位区)有乘客存在(距离小于40cm)时,自动启动风扇,当乘客离开该区域且无新乘客进入超过3秒后,风扇自动关闭。这使得车内温度管理更加智能化和自动化,即时响应乘客的舒适度需求。
2.手动控制机制:在需要手动调节风扇开关的情况下,乘客或驾驶员可以直接通过安装于车内方便触达的按钮进行操作,或通过预先配对的蓝牙设备(如智能手机APP)远程控制风扇,满足特定需求。
3.智能节能:通过自动控制风扇的开闭,ArdiFan能够根据实时需求调整风扇运行,避免无效的电能消耗,实现节能减排的同时保障乘客舒适。
在公共交通工具(如公交车、出租车)和车载服务(如网约车)场景中,ArdiFan的部署能极大提升乘车体验,尤其在炎热或拥挤的条件下,自动和智能化的风扇控制能为乘客提供即时的凉爽环境,增加乘客满意度,对提升服务品质和竞争力具有积极的推动作用。同时,节能特性也符合现代环保理念,有助于减少能源浪费,推动绿色出行。

通过在车载服务场景中引入ArdiFan这样的智能风扇控制系统,体现了智能科学技术在提升传统服务领域用户体验中的巨大潜力和应用价值。

技术原理

项目基于Arduino Nano 3开发板,利用超声波传感器HC-SR04进行距离检测,通过继电器模块控制风扇电源开关。蓝牙通信部分采用HC-06模块,实现与Android客户端的数据交换。

Android客户端的技术原理则主要围绕蓝牙通信、动态权限请求、数据流的读写操作、多线程管理以及界面用户交互(UI)更新几个关键点展开。

代码部分将会在实现方法与步骤说明

实现方法与步骤

一、硬件部分

使用器材:

Arduino Nano 3 --1

Bluetooth Wireless HC-06 --1

Distance sensor HC-SR04 --1

R3000C DC motor --1

Button switch --1

1 Channel 5v Relay Module --1

AAA Battery Holders --1

Breadboard half-size --2

AAA Battery --2

导线若干

根据电路图连接,如图所示:

实物连接如下:

二、软件部分

下面我将对代码进行分析说明:

  1. Arduino部分
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
#include <SoftwareSerial.h>

const int TrigPin = 2;
const int EchoPin = 3;
const int FAN_PIN = 4;
const int BUTTON_PIN = 5;

unsigned long fanOffBeginTime = 0;
unsigned long lastDebounceTime = 0; // 上一次防抖时间
unsigned long lastButtonPressTime = 0; // 上次按钮按下的时间
const unsigned long debounceDelay = 50; // 防抖延迟常量

bool controlByBT = true; // 蓝牙总控制

// 按钮操作标志
bool button2ON = false;
bool button2OFF = false;

SoftwareSerial BTSerial(10, 11); // 创建软件串口

void setup() {
pinMode(TrigPin, OUTPUT);
pinMode(EchoPin, INPUT);
pinMode(FAN_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP); // 启用内部上拉电阻
Serial.begin(9600);
BTSerial.begin(9600);
}

void loop() {
static bool fanStatus = false;
static int lastButtonStatus = HIGH; // 默认状态是 HIGH,因为启用了内部上拉
int distance = getAvgDistance();

unsigned long currentMillis = millis();
static unsigned long previousMillis = 0;
const long interval = 1000; // 设置时间间隔为100毫秒

// 按钮模块
int currentButtonStatus = digitalRead(BUTTON_PIN);
if (currentButtonStatus != lastButtonStatus && millis() - lastDebounceTime > debounceDelay) {
lastDebounceTime = millis();
lastButtonStatus = currentButtonStatus;
if (currentButtonStatus == LOW) { // 按钮从 HIGH 变为 LOW
(fanStatus ? button2OFF : button2ON) = true;
lastButtonPressTime = millis(); // 记录按钮按下时间
}
}

// 蓝牙板块
if(BTSerial.available()){
String state = BTSerial.readString();
if(state == "on"){
controlByBT = true; // 如果接收到"on",则 controlByBT 设置为 true
}
else if(state == "off"){
controlByBT = false; // 如果接收到"off",则 controlByBT 设置为 false,关闭风扇
fanStatus = false;
button2ON = false;
button2OFF = false;
}
}

// 距离控制风扇逻辑
if (controlByBT) {
if (distance < 40) {
if(button2OFF){
fanStatus = false;
}
if(button2ON){
fanStatus = true;
button2ON = false;
button2OFF = false;
}
if(!button2ON && !button2OFF){
fanStatus = true;
}
fanOffBeginTime = millis();
}
if (millis() - fanOffBeginTime > 3000) {
fanStatus = false;
button2ON = false;
button2OFF = false;
}
}

digitalWrite(FAN_PIN, fanStatus ? HIGH : LOW); // 控制风扇

//蓝牙串口输出
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
BTSerial.print(distance);
BTSerial.print(" ");
BTSerial.println(fanStatus ? "on" : "off");
}

}

// 防抖 多次测量求中位数
int getAvgDistance() {
int readings[10];
for (int i = 0; i < 10; i++) {
digitalWrite(TrigPin, LOW);
delayMicroseconds(5);
digitalWrite(TrigPin, HIGH);
delayMicroseconds(10);
digitalWrite(TrigPin, LOW);
readings[i] = pulseIn(EchoPin, HIGH) / 58.0;
delay(10); }
qsort(readings, 10, sizeof(int), cmp);
return readings[5]; // 返回中位数
}

int cmp(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}

概括地说,这段代码包含以下四个关键部分的实现:超声波距离感测、按钮控制、蓝牙通信控制,以及防抖处理。

  1. 超声波距离感测:超声波传感器HC-SR04用于测量对象到传感器的距离。当触发脚(TrigPin)接收到高电平脉冲信号时,HC-SR04会发射一系列超声波脉冲。这些脉冲遇到障碍物后会反射回来,被回声脚(EchoPin)接收。通过计算超声波被发射和接收的时间差,可以根据声波在空气中的传播速度计算出距离。这部分代码首先将超声波信号发射出去,然后测量接收到回声的时间,从而计算出距离。
  2. 按钮控制:按钮控制部分通过监听按钮引脚的电平变化来控制风扇开关状态。由于物理按钮在状态变化时会存在接触抖动,因此需要进行防抖处理,以避免误判。这里当检测到按钮状态改变,并且距离上 一次状态变化超过一定的防抖延时(debounceDelay),才认为是有效的按钮操作。
  3. 蓝牙通信控制:通过SoftWareSerial库创建的软件串口BTSerial实现与蓝牙模块HC-06的通信。系统通过读取蓝牙模块传来的字符串命令(“on"或"off”),来控制风扇的开启和关闭。
  4. 防抖处理:采用多次测量并排序取中位数的方法,可以有效减少偶发错误读数的影响,提高测量的准确性。

基于这些输入,系统实现了一个既能自动响应环境变化,又能接受外部指令或手动控制的智能控制逻辑。

  1. Android Studio部分
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
package com.example.heart;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.cardview.widget.CardView;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;

import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import java.util.UUID;

public class MainActivity extends AppCompatActivity {
LinearLayout ll_start,ll_stop;

// 获取到蓝牙适配器
private BluetoothAdapter mBluetoothAdapter;
// UUID,蓝牙建立链接需要的
private final UUID MY_UUID = UUID
.fromString("00001101-0000-1000-8000-00805F9B34FB");

// 选中发送数据的蓝牙设备,全局变量,否则连接在方法执行完就结束了
private BluetoothDevice selectDevice;
// 获取到选中设备的客户端串口,全局变量,否则连接在方法执行完就结束了
private BluetoothSocket clientSocket;
// 获取到向设备写的输出流,全局变量,否则连接在方法执行完就结束了
public static InputStream is;
public static OutputStream os;
private String receivedData="";
private Thread thread;
private volatile boolean running = true; // 线程运行控制标志

private TextView tv_1,tv_2;
private static final int PERMISSION_REQUEST_CODE = 100;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ll_start = findViewById(R.id.ll_start);
ll_stop = findViewById(R.id.ll_stop);


tv_1 = findViewById(R.id.textView1);
tv_2 = findViewById(R.id.textView2);



ll_start.setOnClickListener(view -> {
Toast.makeText(this, "开始连接", Toast.LENGTH_SHORT).show();
init蓝牙();
});
ll_stop.setOnClickListener(view -> {
running = false; // 安全地停止线程
});
// 检查并申请权限
if (!hasRequiredPermissions()) {
requestRequiredPermissions();
}

findViewById(R.id.open).setOnClickListener(v -> {
if (os == null) {
Toast.makeText(this, "请先连接", Toast.LENGTH_SHORT).show();
return;
}
new Thread(new Runnable() {
@Override
public void run() {
try {
os.write("on".getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
Toast.makeText(this, "发送成功", Toast.LENGTH_SHORT).show();
});
findViewById(R.id.close).setOnClickListener(v -> {
if (os == null) {
Toast.makeText(this, "请先连接", Toast.LENGTH_SHORT).show();
return;
}
new Thread(new Runnable() {
@Override
public void run() {
try {
os.write("off".getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
Toast.makeText(this, "发送成功", Toast.LENGTH_SHORT).show();
});
}
private boolean hasRequiredPermissions() {
return ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}

private void requestRequiredPermissions() {
ActivityCompat.requestPermissions(this,
new String[] {
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
},
PERMISSION_REQUEST_CODE);
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
boolean allPermissionsGranted = true;
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allPermissionsGranted = false;
break;
}
}
if (allPermissionsGranted) {
// 所有权限都已授予,执行需要权限的操作
} else {
// 权限被拒绝,处理拒绝情况
}
}
}
private void init蓝牙() {
// 获取到蓝牙默认的适配器
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
// 对其进行分割,获取到这个设备的地址
String address = "98:D3:61:F9:4F:B8";
// 判断当前是否还是正在搜索周边设备,如果是则暂停搜索
if (ActivityCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
//判断是否正在搜索
}
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
}
// 如果选择设备为空则代表还没有选择设备
if (selectDevice == null) {
//通过地址获取到该设备
selectDevice = mBluetoothAdapter.getRemoteDevice(address);
}
// 这里需要try catch一下,以防异常抛出
try {
// 判断客户端接口是否为空
if (clientSocket == null) {
clientSocket = selectDevice
.createRfcommSocketToServiceRecord(MY_UUID);
// 向服务端发送连接
clientSocket.connect();
// 获取到输出流,向外写数据
os = clientSocket.getOutputStream();

// 获取到输入流,用于接收数据
is = clientSocket.getInputStream();


}
// 吐司一下,告诉用户成功
if (os != null){
Toast.makeText(MainActivity.this, "连接成功", Toast.LENGTH_SHORT).show();
running = true; // 安全地开始线程
thread = new GetData();
thread.start();
}else {
Toast.makeText(MainActivity.this, "建立连接失败", Toast.LENGTH_SHORT).show();
}

} catch (IOException e) {
e.printStackTrace();
// 如果发生异常则告诉用户发送失败
Toast.makeText(MainActivity.this, "连接失败", Toast.LENGTH_SHORT).show();
}
}
class GetData extends Thread {
@Override
public void run() {
while (running) {
try {
byte[] buffer = new byte[1024];
int bytesRead;
StringBuilder stringBuilder = new StringBuilder();

while ((bytesRead = is.read(buffer)) != -1) {
receivedData = new String(buffer, 0, bytesRead);
stringBuilder.append(receivedData);
//System.out.println(receivedData);
if (!running)break;
if(receivedData!=null){
if (receivedData.contains("\n"))
receivedData = receivedData.split("\n")[0];
if (receivedData.contains("on")||receivedData.contains("of")){
String[] a = receivedData.trim().split(" ");
for (int i = 0; i < a.length; i++) {
if (a[i].contains("on")||a[i].contains("of")){
tv_2.setText("运行状态: "+a[1]);
}else {
if (!a[i].isEmpty()){
tv_1.setText("检测距离: "+a[0]+" cm");
}
}
}
System.out.println("data:"+receivedData);
}
}
}
} catch (IOException e) {
// e.printStackTrace();
if (!running) {
// 如果线程是因为调用 stopRunning 而中断的,则退出循环
break;
}

} catch (Exception e) {
//e.printStackTrace();
}
}
}
}


public static String getCurrentTimeString() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// 获取当前日期和时间
LocalDateTime now = null;
now = LocalDateTime.now();
// 定义日期时间格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

// 将当前日期时间格式化为字符串
return now.format(formatter);
}else {
return "2023-12-09 16:06:34";
}
}

这段代码实现了通过蓝牙与Arduino设备建立连接并进行交互。

  1. 首先初始化应用界面,包括设置按钮监听器,用于开始和结束与设备的连接。
    2. 为了在Android 6.0及以上版本上正常运作,还要请求用户授权蓝牙和位置访问权限。得到权限后,应用通过蓝牙适配器查找并连接到指定的Arduino设备,使用一个预定义的UUID建立一个安全的RFComm(串行端口服务)连接。
    3. 连接成功后,应用可以获取输出流和输入流,以向设备发送指令(比如“开”或“关”指令)并读取设备返回的数据。发送和接收数据的功能在一个叫做GetData的线程中实现,以避免主线程阻塞。
    4. 之后用户可以通过点击界面上的按钮发送特定的指令到Arduino设备,应用将根据从Arduino设备接收到的数据更新界面上的显示内容。

运行结果如下:

  1. 距离检测和风扇状态显示: Android客户端的最上方两行分别显示当前超声波检测到的距离(单位:cm)和风扇的运行状态(on或off)。
  2. 风扇控制: 在客户端的中间区域,提供两个按钮,分别用于手动控制风扇的开闭状态。
  3. 蓝牙连接管理: 客户端的最下方两行用于蓝牙功能,其中一个按钮用于连接蓝牙模块,另一个按钮用于断开连接。

关键技术

  1. 多模式控制机制:ArdiFan系统集成了超声波感应、物理按钮操作和蓝牙远程控制三种控制模式,为用户提供了多样化的操作选择。这种多模式的设计使得ArdiFan能够在不同情境下灵活使用,例如自动感应开关功能适用于无手触控的环境,而蓝牙控制则便于用户在远距离进行操作。
  2. 智能节能策略:通过集成的超声波传感器自动检测用户的存在与否,ArdiFan能在用户靠近时自动开启风扇,在用户离开一定时间后自动关闭风扇,从而实现节能减排的目的。这种自动化控制策略有效减少了能源浪费。
  3. 跨平台远程控制应用:ArdiFan项目包含了一个Android客户端,使得用户可以通过智能手机远程控制风扇。这种跨平台的应用开发为Arduino项目提供了更广阔的使用场景,增加了用户交互的便捷性。
  4. 低成本实现高价值功能:整个ArdiFan系统的实现基于成本相对低廉的Arduino板和传感元件,但却实现了市面上高端智能风扇才具备的功能,展现了利用开源硬件和软件平台进行智能家居项目开发的巨大潜力。

评论