根据华东师范大学《创客实践》课程期末考查方案相关要求,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
导线若干
根据电路图连接,如图所示:

实物连接如下:

二、软件部分
下面我将对代码进行分析说明:
- 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; int distance = getAvgDistance();
unsigned long currentMillis = millis(); static unsigned long previousMillis = 0; const long interval = 1000;
int currentButtonStatus = digitalRead(BUTTON_PIN); if (currentButtonStatus != lastButtonStatus && millis() - lastDebounceTime > debounceDelay) { lastDebounceTime = millis(); lastButtonStatus = currentButtonStatus; if (currentButtonStatus == LOW) { (fanStatus ? button2OFF : button2ON) = true; lastButtonPressTime = millis(); } }
if(BTSerial.available()){ String state = BTSerial.readString(); if(state == "on"){ controlByBT = true; } else if(state == "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; }
|
概括地说,这段代码包含以下四个关键部分的实现:超声波距离感测、按钮控制、蓝牙通信控制,以及防抖处理。
- 超声波距离感测:超声波传感器HC-SR04用于测量对象到传感器的距离。当触发脚(TrigPin)接收到高电平脉冲信号时,HC-SR04会发射一系列超声波脉冲。这些脉冲遇到障碍物后会反射回来,被回声脚(EchoPin)接收。通过计算超声波被发射和接收的时间差,可以根据声波在空气中的传播速度计算出距离。这部分代码首先将超声波信号发射出去,然后测量接收到回声的时间,从而计算出距离。
- 按钮控制:按钮控制部分通过监听按钮引脚的电平变化来控制风扇开关状态。由于物理按钮在状态变化时会存在接触抖动,因此需要进行防抖处理,以避免误判。这里当检测到按钮状态改变,并且距离上 一次状态变化超过一定的防抖延时(debounceDelay),才认为是有效的按钮操作。
- 蓝牙通信控制:通过SoftWareSerial库创建的软件串口BTSerial实现与蓝牙模块HC-06的通信。系统通过读取蓝牙模块传来的字符串命令(“on"或"off”),来控制风扇的开启和关闭。
- 防抖处理:采用多次测量并排序取中位数的方法,可以有效减少偶发错误读数的影响,提高测量的准确性。
基于这些输入,系统实现了一个既能自动响应环境变化,又能接受外部指令或手动控制的智能控制逻辑。
- 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; 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 { 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); 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) { if (!running) { break; }
} catch (Exception e) { } } } }
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设备建立连接并进行交互。
- 首先初始化应用界面,包括设置按钮监听器,用于开始和结束与设备的连接。
2. 为了在Android 6.0及以上版本上正常运作,还要请求用户授权蓝牙和位置访问权限。得到权限后,应用通过蓝牙适配器查找并连接到指定的Arduino设备,使用一个预定义的UUID建立一个安全的RFComm(串行端口服务)连接。
3. 连接成功后,应用可以获取输出流和输入流,以向设备发送指令(比如“开”或“关”指令)并读取设备返回的数据。发送和接收数据的功能在一个叫做GetData的线程中实现,以避免主线程阻塞。
4. 之后用户可以通过点击界面上的按钮发送特定的指令到Arduino设备,应用将根据从Arduino设备接收到的数据更新界面上的显示内容。
运行结果如下:

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