Mind+积木编程控制小水泵给宠物喂水

前期用scratch,带着小朋友做了大鱼吃小鱼、桌面弹球、小学生计算器3个作品,小朋友收获不小。关键是小家伙感兴趣,做出来后给家人炫耀了一圈后,兴趣大增,嚷嚷着要做更好玩的。

最近,娃妈从抖音上买了个小猫喝水的容器,一通电,水龙头就自动出水,猫咪就跑过来了。小朋友又发现了新大陆,摆弄起了开关,逗着猫咪。看到这,我心想这玩意能搞成自动化,跟scratch结合起来,正好又卡着小朋友的兴趣点,连玩带学,一箭双雕。

以上为背景。


因为上学的时候折腾过单片机,初步判定这个功能用单片机控制肯定是可行的,大概搜了下scratch这类少儿编程软件也支持单片机,所以整体方案可行,初步制定了功能如下:

  • 基础功能:图形化编程,用积木控制,角色可以是小猫,运动来触发单片机
  • 增强功能:如果孩子感兴趣,继续增加灯光控制,与水龙头联动
  • 增强功能:如果孩子感兴趣,继续增加光电控制,用红外线或者声音触发水龙头开关

功能定义清楚后,然后就是寻找资料,分析技术可行性:

编程软件选型

上文提及了,孩子之前用的是Scratch,因为一些原因,Scratch在国内逐步被替换,可选的有如下:

  • Kittenblock
  • ClipCC
  • Mind+
  • PictoBlox

大概看了下,功能都差不多,基本与Scratch旗鼓相当,因为要连单片机,我主要关注扩展性,就选了Mind+,其实Kittenblock也差不多,先入为主就选了Mind+

官方网站:Mind+官网传送门

选择Mind+的另一个原因是,Mind+的开发者网站有大量电子积木的教程,与我的诉求匹配,无脑选了它了,

开发者网站:https://mindplus.dfrobot.com.cn/

扩展库选型

之所以要讨论这个,主要原因看Mind+教程,如果要控制单片机,无非就是usb口转串口,那么不得不用有线的方式连接单片机和控制设备。目前给小朋友编程用的是华为的平板,如果再连个usb线,恐怕小朋友会丧失玩下去的乐趣,所以要考虑无线连接,硬件方面(下个章节讨论)应该没问题,软件层面要支持无线连接的,从Mind+网站上看可以有python和nodejs两种,对应python和js两种语言,小朋友还没上小学,代码编程自然是不考虑的,所以要把上述两种语言"封装"到积木中,把涉及TCP数据包收发的逻辑的嵌入到积木中。显然js和python都可以胜任TCP收发任务。

在Mind+中,python扩展是包含在"Python"模式中的,每个积木对应着一段python代码,通过搭积木的方式组织代码。对于学龄前小朋友,这个可视化功能还是有点太深奥了,暂不考虑。另外,Python模式下,角色功能也不可用(猜测是python模式更偏代码逻辑,弱化了角色),而我家小朋友还处于启蒙阶段,没有了角色,兴趣也会大大减弱。

在Mind+中,nodejs扩展是集成到"实时"模式中的,实时模式就是普通的模式,包含了场景和角色,和我的诉求很贴合。从官方的说明,nodejs类里面可以扩展实时模式用户库,库里面可以构造一个或者多个block,可以自定义block的外观,以及自定义block的运行逻辑。

Mind+从V1.6.5开始开放 实时模式用户库 ,内部兼容 Scratch扩展语法,,本文档将介绍如何开发Mind+实时模式用户库,阅读此文档需要你掌握 JavaScript的基础知识以及了解一些ES6的新特性。
如果你在阅读过程中产生疑惑, 请加入官方交流群(671877416)。
在开发之前, 你需要知道 能做出什么东西 .
添加不依赖硬件的模块 可以很方便的使用第三方JS库
为mind+中已有的主板/套件增加硬件小模块
添加自己的硬件套件并通信

从上述官方教程引用看,实时模式用户库完全符合我的诉求,就选它了。

硬件选型

因为喝水的容器已经小水泵已经有了,如下图:

那么剩下的就是买单片机、电机驱动板、LED灯,以及必要的连接线了,Mind+官方支持Arduino,ESP32,ESP8266,这些都是市面上很常见的,

从节约成本的角度,就选ESP32了,其他的随便买。

元件列表


以下开始教程:


硬件连接

马云家买的元件,购买链接怎么连硬件基本说的很清楚了,需要注意的就是注意电压及正负极,别烧板子了。

如下 图:

ESP32固件:

基于Arduino的示例代码,简单改下即可,注释不多,凑合看,不复杂。

定义了3个gpio口

5 --输出口,连电机驱动板,用于控制小水泵的启动/停止

17 --输出口,直接连一个小LED灯

16 --输入口,连红外传感器,用于控制电机的启动/停止

代码运行逻辑为:水泵受两个管脚的控制,一个是来自上位机(Mind+)的指令,一个是红外传感器,红外传感器的优先级高于上位机指令,实现的效果就是小猫靠近,水龙头打开;小猫离开,水龙头由上位机控制。

cpp 复制代码
/*
 WiFi Web Server LED/Motor Control

 */

#include <WiFi.h>

const char* ssid     = "xxxxx";
const char* password = "xxxxx";
const int MOTOR = 5;
const int LIGHT = 17;
const int NEAR = 16;

WiFiServer server(80);

void setup()
{
    Serial.begin(115200);
    pinMode(MOTOR, OUTPUT); 
    pinMode(LIGHT, OUTPUT); 
    pinMode(NEAR, INPUT); 
    
    delay(10);

    // We start by connecting to a WiFi network

    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);

    WiFi.begin(ssid, password);

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

    Serial.println("");
    Serial.println("WiFi connected.");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
    
    server.begin();

}

int lastValue = LOW;

void loop(){

 int state = digitalRead(NEAR);
 
 
 if (state != lastValue ) {
  lastValue = state;
  if (state == HIGH){
    Serial.println("receive near signal: HIGH");
    digitalWrite(MOTOR, HIGH);
    return;
   } else {
    Serial.println("receive near signal: LOW");
    digitalWrite(MOTOR, LOW);
   }
 }

 sleep(1);
 WiFiClient client = server.available();   // listen for incoming clients
 Serial.println("server created, wating ...");  
 
  if (client) {                             // if you get a client,
    Serial.println("New Client.");           // print a message out the serial port
    String currentLine = "";                // make a String to hold incoming data from the client
    while (client.connected()) {            // loop while the client's connected
      if (client.available()) {             // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        //Serial.write(c);                    // print it out the serial monitor
        if (c == '\n') {                    // if the byte is a newline character

          if (currentLine.startsWith("motoron")) {
            digitalWrite(MOTOR, HIGH);               // GET /H turns the LED on
          }
          if (currentLine.startsWith("motoroff")) {
            digitalWrite(MOTOR, LOW);                // GET /L turns the LED off
          }

          if (currentLine.startsWith("lightoff")) {
            digitalWrite(LIGHT, LOW);                // GET /L turns the LED off
          }

          if (currentLine.startsWith("lighton")) {
            digitalWrite(LIGHT, HIGH);                // GET /L turns the LED off
          }
          
          Serial.println("receive msg:" + currentLine);  
          currentLine = "";
          break;
        } else if (c != '\r') {  // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }

        // Check to see if the client request was "GET /H" or "GET /L":
        
      }
    }
    // close the connection:
    client.stop();
    Serial.println("Client Disconnected.");
  }
}

代码写完后,上传到ESP32中(教程很多,不展开说明了),可以通过网络调试助手这类工具,发送"motoron"等指令,确保ESP工作正常。

Nodejs Block定义

Nodejs中定义两个Block,分别控制LED灯和水泵的开关,并且定义两个控制函数(motor和light),分别控制LED和水泵的运行逻辑,不复杂,直接上代码了。

javascript 复制代码
import ArgumentType from './extension-support/argument-type';
import BlockType from './extension-support/block-type';
import blockIconURI from './image/icon.svg';

class YourExtension{
    constructor(runtime) {
        this.runtime = runtime;
    }

    getInfo () {
        const operator_values = [["打开","1"],["关闭","0"]];
        return {
            name: '小猫喝水',
            blockIconURI: blockIconURI,
            blocks: [
                {  
                    opcode: 'motor',
                    blockType: BlockType.COMMAND,
                    text: ' [OPERATOR] 小猫喝水的龙头 ',
                    arguments: {
                        OPERATOR: {
                            type: ArgumentType.STRING,
                            onlyField: true,
                            options: operator_values,
                            defaultValue: operator_values[1][1]
                        }
                    }
                },
                {  
                    opcode: 'light',
                    blockType: BlockType.COMMAND,
                    text: ' [OPERATOR] 水龙头的灯泡 ',
                    arguments: {
                        OPERATOR: {
                            type: ArgumentType.STRING,
                            onlyField: true,
                            options: operator_values,
                            defaultValue: operator_values[1][1]
                        }
                    }
                },
            ],
        }
    }
    // block运行函数, 与opcode同名
    motor (args) {
        // NUM1与NUM2参数都是'ArgumentType.STRING'类型, 所以要parseInt解析成number
        var msg = '';
        switch (args.OPERATOR){
            case "1":
                msg = "motoron\n";
                break;
            case "0":
                msg = "motoroff\n";
                break;
            default:
                msg = "motoroff\n";
                break;
        }
        const net = require("net");
        const HOST = "192.168.5.130";
        const PORT = 80;

        const client = new net.Socket();


        client.connect(PORT, HOST, function() {
            //客户端向服务端socket发送数据
             client.write(msg);
            });
    }

    light (args) {
        // NUM1与NUM2参数都是'ArgumentType.STRING'类型, 所以要parseInt解析成number
        var msg = '';
        switch (args.OPERATOR){
            case "1":
                msg = "lighton\n";
                break;
            case "0":
                msg = "lightoff\n";
                break;
            default:
                msg = "lightoff\n";
                break;
        }
        const net = require("net");
        const HOST = "192.168.5.130";
        const PORT = 80;

        const client = new net.Socket();


        client.connect(PORT, HOST, function() {
            //客户端向服务端socket发送数据
             client.write(msg);
            });
    }
}
module.exports = YourExtension;

其中:

const HOST="192.168.5.130";要改成ESP在局域网下的真实IP地址,可以通过ESP上电后控制台打印的log查看。

ESP log

在代码目录下执行npm run build,确保编译成功

npm build

Mind+操作步骤

在上述nodejs编译成功后,在Mind+中添加用户库,具体教程参考,也不展开讲了。

https://mindplus.dfrobot.com.cn/ext-js

注意,官方教程有个坑,nodejs的版本不能用最新的,建议用14.16.0版本,配套的Mind+版本是1.7.3,笔者用这两个版本配合没有问题,用最新的nodejs就出怪异问题。

按官方教程操作,编译,添加扩展后,最终,在Mind+实时模式下,可以看到如下两个用户自定义Block

写了两份示例代码,当然,这个大家就可以发挥想象力了,没有固定的模板。

案例一:用空格键切换水泵的开启/关闭状态

案例2:用角色移动,控制水泵的开启/关闭

硬件也简单整理下,看上去像是一个具有实用价值的工具兼玩具。

运行效果视频

积木运行效果

教程结束,enjoy!

相关推荐
亚林瓜子1 天前
AWS Lambda 添加NodeJS依赖库层
npm·云计算·nodejs·node·aws·lambda
GDAL2 天前
腾讯云ubuntu安装nodejs环境
ubuntu·nodejs·腾讯云
Sean_woo19982 天前
Zephyr rtos ESP32系列BSP提交流程指南
stm32·单片机·esp32·wsl·zephyr·立创开发板
qdprobot7 天前
齐护AiTall pro ESP32S3 小智AI对话 MQTT MCP 开发板Mixly Scratch Steam图形化编程创客教育
人工智能·mqtt·scratch·mixly·mcp·小智ai·齐护机器人aitall pro
小叶子来了啊7 天前
少儿编程Scratch3.0教程——扩展篇(视频侦测)
人工智能·深度学习·计算机视觉·scratch
小叶子来了啊8 天前
少儿编程Scratch3.0教程——05 事件积木(项目练习)
scratch
喜喜安10 天前
ESP32简单介绍(包含与STM32、C51之间的联系)
esp32
CFZPL11 天前
esp32,stm32编译的不同
单片机·esp32
CFZPL11 天前
espidf用CMake文件构建项目
单片机·esp32