不到50元如何自制智能开关?

前言

家里床头离开关太远了,每次躺在床上玩手机准备睡觉时候还的下去关灯,着实麻烦,所以用目前仅有的一点单片机知识,做了一个小小小的智能开关,他有三个模块构成,如下:

  1. 主模块是ESP32(20元)

    他是一个低成本、低功耗的微控制器,集成了 Wi-Fi 和蓝牙功能,因为我们需要通过网络去开/关灯,还有一个ESP8266,比这个便宜,大概6 块钱,但是他烧录程序的时候比较慢。

  1. 光电开关(10元)

    这个可有可无,这个是用来当晚上下班回家后,开门自动开灯使用的,如果在他前面有遮挡,他的信号线会输出高/低电压,这个取决于买的是常开还是常闭,我买的是常开,当有物体遮挡时,会输出低电压,所以当开门时,门挡住了它,它输出低电压给ESP32,ESP32读取到电压状态后触发开灯动作。

  1. 舵机 SG90(5元)

    这是用来触发开/关灯动作的设备,需要把它用胶粘在开关上,他可以旋转0-180度,力度也还行,对于开关足够了。还有一个MG90舵机,力度特别大,但是一定要买180度的,360度的舵机只能正转和反转,不能控制角度。

  1. 杜邦线(3元)

Arduino Ide

Arduino是什么就不说了,要烧录代码到ESP32,需要使用官方乐鑫科技提供的ESP-IDF工具,它是用来开发面向ESP32和ESP32-S系列芯片的开发框架,但是,Arduino Ide提供了一个核心,封装了ESP-IDF一些功能,便于我们更方便的开发,当然Arduino还有适用于其他开发板的库。

Arduino配置ESP32的开发环境比较简单,就是点点点、选选选即可。

接线

下面就是接线环节,先看下ESP32的引脚,他共有30个引脚,有25个GPIO(通用输入输出)引脚,如下图中紫色的引脚,在我们的这个设备里,舵机和光电开关都需要接入正负级到下图中的红色(VCC)和黑色(GND)引脚上,而他们都需要在接入一个信号作为输出/输入点,可以在着25个中选择一个,但还是有几个不能使用的,比如有一些引脚无法配置为输出,只用于作输入,还有RX和TX,我们这里使用26(光电开关)和27(舵机)引脚就可以了。

esp32代码

下面写一点点代码,主要逻辑很简单,创建一个http服务器,用于通过外部去控制舵机的转向,外部通过http请求并附带一个角度参数,在通过ESP32Servo这个库去使舵机角度发生改变。

esp32的wifi有以下几种模式。

  1. Station Mode(STA模式): 在STA模式下,esp32可以连接到一个wifi,获取一个ip地址,并且可以与网络中的其他设备进行通信。
  2. Access Point Mode(AP模式): 在AP模式下,它充当wifi热点,其他设备可以连接到esp32,就像连接到普通路由器一样,一般用作配置模式使用,经常买到的智能设备,进入配置模式和后,他会开一个热点,你的手机连接到这个热点后,在通过他们提供的app去配置,就是用这种模式。
  3. Soft Access Point Mode(SoftAP模式): 同时工作在STA模式和AP模式下。

下一步根据自己的逻辑,比如当光电开关被遮挡时,并且又是xxxx时,就开灯,或者当xxx点后就关灯。

c 复制代码
#include <WiFi.h>
#include <ESP32Servo.h>
#include <Time.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebSrv.h>
#define SERVO_PIN_NUMBER 27
#define STATE_PIN_NUMBER 26
#define CLOSE_VALUE 40
#define OPEN_VALUE 150
const char* ssid = "wifi名称";
const char* password = "wifi密码";

AsyncWebServer server(80);
Servo systemServo;

bool openState = false;
void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_AP_STA);
  WiFi.begin(ssid, password);
  Serial.println("\nConnecting");

  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(100);

  }
  systemServo.attach(SERVO_PIN_NUMBER);
  systemServo.write(90);
  openState = false;
  write_state(CLOSE_VALUE);//启动时候将灯关闭

  Serial.print("Local ESP32 IP: ");
  Serial.println(WiFi.localIP());
  pinMode(STATE_PIN_NUMBER, INPUT);
  int timezone = 8 * 3600;
  configTime(timezone, 0, "pool.ntp.org");

  server.on("/set_value", HTTP_GET, [](AsyncWebServerRequest * request) {
    if (request->hasParam("value")) {
      String value = request->getParam("value")->value();
      int intValue = value.toInt();
      write_state(intValue);
      request->send(200, "text/plain", "value: " + String(intValue));
    } else {
      request->send(400, "text/plain", "error");
    }
  });
  server.begin();
}

void write_state(int value) {
  openState = value < 90 ? false : true;

  systemServo.write(value);
  delay(100);
  systemServo.write(90);
}
void loop() {
  time_t now = time(nullptr);
  struct tm *timeinfo;
  timeinfo = localtime(&now);

  //指定时间关灯
  int currentMin = timeinfo->tm_min;
  int currentHour = timeinfo->tm_hour;
  if (currentHour == 23 && currentMin == 0 && openState ) {
    write_state(CLOSE_VALUE);
    openState = false;
  }
  //下班开灯
  if (digitalRead(STATE_PIN_NUMBER) == 0 && currentHour > 18 && !openState) {
    write_state(OPEN_VALUE);
    openState = true;
  }
}

Android下控制

当然,还得需要通过外部设备进行手动开关,这里就简单写一个Android程序,上面写了一个http服务,访问esp32的ip地址,发起一个http请求就可以了,所以浏览器也可以,但更方便的是app,效果如下。

kotlin 复制代码
package com.example.composedemo

import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import com.example.composedemo.ui.theme.ComposeDemoTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL

class MainActivity : ComponentActivity() {
    private val state = State()
    private lateinit var sharedPreferences: SharedPreferences
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedPreferences = getPreferences(Context.MODE_PRIVATE)
        state.ipAddressChange = {
            with(sharedPreferences.edit()) {
                putString("ipAddress", it)
                apply()
            }
        }
        state.slideChange = {setValue(it) }
        state.lightChange = {
            Log.i(TAG, "onCreate: $it")
            if (it) openLight()
            if (!it) closeLight()
        }
        state.esp32IpAddress.value = sharedPreferences.getString("ipAddress", "")!!

        setContent {
            ComposeDemoTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    SlidingOvalLayout(state)
                }
            }
        }
    }

    private fun closeLight() =setValue(40)

    private fun openLight() = setValue(150)

    private fun setValue(value: Int) {
        sendHttpRequest("http://${state.esp32IpAddress.value}/set_value/?value=$value:")
    }

    private fun sendHttpRequest(url: String) {
        GlobalScope.launch(Dispatchers.IO) {
            try {
                val connection = URL(url).openConnection() as HttpURLConnection
                connection.requestMethod = "GET"
                connection.connect()
                if (connection.responseCode == HttpURLConnection.HTTP_OK) {
                    val response = connection.inputStream.bufferedReader().readText()
                    withContext(Dispatchers.Main) {
                    }
                } else {
                    withContext(Dispatchers.Main) {
                    }
                }
                connection.disconnect()
            } catch (e: Exception) {
                e.printStackTrace()
                withContext(Dispatchers.Main) {
                }
            }
        }
    }
}


@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    ComposeDemoTheme {
    }
}

ui组件

kotlin 复制代码
package com.example.composedemo

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.layout
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.example.composedemo.ui.theme.ComposeDemoTheme

const val TAG = "TAG"

@Composable
fun SlidingOvalLayout(state: State) {
    var offset by remember { mutableStateOf(Offset(0f, 0f)) }
    var parentWidth by remember { mutableStateOf(0) }
    var sliderValue by remember { mutableStateOf(0) }
    var closeStateColor by remember { mutableStateOf(Color(0xFFDF2261)) }
    var openStateColor by remember { mutableStateOf(Color(0xFF32A34B)) }
    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(40.dp)
            .width(100.dp)
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp),
            verticalArrangement = Arrangement.SpaceBetween,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Column {
                Box() {
                    TextField(
                        value = state.esp32IpAddress.value,
                        onValueChange = {
                            state.esp32IpAddress.value = it
                            state.ipAddressChange(it)
                        },
                        colors = TextFieldDefaults.textFieldColors(
                            disabledTextColor = Color.Transparent,
                            backgroundColor = Color(0xFFF1EEF1),
                            focusedIndicatorColor = Color.Transparent,
                            unfocusedIndicatorColor = Color.Transparent,
                            disabledIndicatorColor = Color.Transparent
                        ),
                        modifier = Modifier
                            .fillMaxWidth()
                            .background(color = Color(0xFFF1EEF1))
                    )

                }
                Box() {
                    Column() {
                        Text(text = sliderValue.toString())
                        Row(
                            verticalAlignment = Alignment.CenterVertically,
                            horizontalArrangement = Arrangement.spacedBy(8.dp)
                        ) {
                            Slider(
                                value = sliderValue.toFloat(),
                                onValueChange = {
                                    sliderValue = it.toInt()
                                    state.slideChange(sliderValue)},
                                valueRange = 0f..180f,
                                onValueChangeFinished = {

                                },
                                colors = SliderDefaults.colors(
                                    thumbColor = Color.Blue,
                                    activeTrackColor = Color.Blue
                                )
                            )
                        }
                    }

                }
            }
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(70.dp)
                    .shadow(10.dp, shape = RoundedCornerShape(100.dp))
                    .background(color = Color(0xFFF1EEF1))
                    .layout { measurable, constraints ->
                        val placeable = measurable.measure(constraints)
                        parentWidth = placeable.width
                        layout(placeable.width, placeable.height) {
                            placeable.place(0, 0)
                        }
                    }
            ) {
                Box(
                    modifier = Modifier
                        .offset {
                            if (state.lightValue.value) {
                                IntOffset((parentWidth - 100.dp.toPx()).toInt(), 0)
                            } else {
                                IntOffset(0, 0)
                            }
                        }
                        .graphicsLayer {
                            translationX = offset.x
                        }
                        .clickable() {
                            state.lightValue.value = !state.lightValue.value
                            state.lightChange(state.lightValue.value )
                        }
                        .pointerInput(Unit) {
                        }
                        .background(
                            color = if(state.lightValue.value) openStateColor  else closeStateColor,
                            shape = RoundedCornerShape(100.dp)
                        )
                        .size(Dp(100f), Dp(80f))
                )
            }
        }
    }
}

@Preview
@Composable
fun PreviewSlidingOvalLayout() {
    ComposeDemoTheme {
    }
}
kotlin 复制代码
class State {
    var esp32IpAddress: MutableState<String> = mutableStateOf("")
    var lightValue :MutableState<Boolean> = mutableStateOf(false)

    var ipAddressChange :(String)->Unit={}

    var slideChange:(Int)->Unit={}

    var lightChange:(Boolean)->Unit={}

}
相关推荐
九圣残炎2 小时前
【springboot】简易模块化开发项目整合Redis
spring boot·redis·后端
.生产的驴2 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
爱学的小涛2 小时前
【NIO基础】基于 NIO 中的组件实现对文件的操作(文件编程),FileChannel 详解
java·开发语言·笔记·后端·nio
爱学的小涛2 小时前
【NIO基础】NIO(非阻塞 I/O)和 IO(传统 I/O)的区别,以及 NIO 的三大组件详解
java·开发语言·笔记·后端·nio
北极无雪2 小时前
Spring源码学习:SpringMVC(4)DispatcherServlet请求入口分析
java·开发语言·后端·学习·spring
爱码少年3 小时前
springboot工程中使用tcp协议
spring boot·后端·tcp/ip
2401_8576226610 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_8575893610 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没12 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch12 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j