- 2024-10-27
-
发表了主题帖:
【2024 DigiKey 创意大赛】移植openHASP项目,实现家庭物联网控制中心
本帖最后由 eew_dy9f48 于 2024-10-27 14:49 编辑
一、作品简介
openHASP是一个适配于homeassistant的显示面板,底层代码是基于lvgl编写的,特别对esp32-s3进行了优化。但比较遗憾的是目前这个项目并不支持ESP32-S3-LCD-EV-BOARD这块开发板。由于这块板子上的480*480屏幕,非常适合作为86面板嵌入在墙上,作为家庭物联网的控制中心,因此我想把openHASP这个项目移植到ESP32-S3-LCD-EV-BOARD开发板上。
移植完成后,我会用它来制作一个小型家庭灯光控制面板。同时用ESP32-S3-DEVKITC-1-N8R8开发板作为被控的灯光设备,来演示控制过程。
二、系统框图
项目分为三个部分完成。
第一部分是openhasp的移植,让它可以运行在ESP32-S3-LCD-EV-BOARD开发板上。
第二部分是openhasp的配置,在上面完成一个灯光控制的页面设计。
第三部分是使用ESP32-S3-DEVKITC-1-N8R8制作一个灯光控制器,作为openhasp的被控端来使用。
三、各部分功能说明
第一部分,移植openhasp:
先去OpenHASP仓库拿到所有源代码:
Git clone https://github.com/HASwitchPlate/openHASP
打开项目后,可以发现项目是使用platformio编写的,这对我们后续移植提供了很大的便利。
开发板的配置文件都存放在user_setups中。先找一个类似的配置文件,这里主要指的是通过并口驱动电容触摸屏的开发板。可以选择user_setups\esp32s3\esp32-s3-4848S040.ini。复制创建一个新的,然后可以开始修改。
主要的修改是几块,第一块是把文件内所有的开发板名称都修改,比如我改成了esp32-s3-4848s040_16MB-ev
。以此避免命名冲突。第二块是修改屏幕驱动,把原本的ST7701去掉,换成-D GC9503V_DRIVER=1。第三块是修改触摸驱动。我们开发板使用的触摸驱动是FT5X06,虽然openhasp项目中集成了这款驱动,但是如果想使用还需要下载额外的库,比较麻烦。经过实测FT6336的驱动也可以完美工作,因此我们只需要对应修改-D TOUCH_DRIVER=0x6336,并对应修改-D I2C_TOUCH_ADDRESS=0x38就可以了。最后一块是pin的修改,把LCD和触摸驱动引脚一一对应修改好就可以。这里特别需要注意一点,由于开发板默认的SPI配置引脚使用的是IO拓展器拓展出来的,因此我们需要修改一下硬件部分,让这三个引脚直接使用ESP32S3的引脚。具体修改要改动R92,93,94
按照我下面的方法把三个电阻移动一下位置就可以。
创建好配置修改好电路后,我们更改一下编译配置文件,就可以开始上传了。注意这时候不要动他原本的platformio.ini,而应该复制一份新的platformio_override-template.ini,然后重命名成platformio_override.ini,并在里面修改extra_configs = user_setups/esp32s3/*.ini以及extra_default_envs = esp32-s3-4848s040_16MB-ev。完成后编译上传,看到屏幕有了下面显示,就说明以及成功移植了openhasp项目。
第二部分,配置openhasp并绘制GUI:
首先会看到屏幕上显示一个二维码,用手机按照指示连接对应的热点并扫二维码进去配置wifi,完成后重启,设备就可以连上网络:
重启后默认当设备连上网后,会弹出设备IP地址。我们输入这个ip,就可以进入配置页面。在这个地方就可以完成对GUI的编写。
先进入configuration里的MQTT Settings配置一下MQTT服务器的设置。MQTT可以在自己电脑上搭建,也可以使用公共的。这里就不再赘述。
接着返回主页,进入File Editor,就可以开始编写GUI。
GUI的编写全部都在pages.jsonl这一个文件中完成。这里我只创建了一个组件,是一个按键矩阵。可以非常方便的使用配置语言在这里添加各种各样的组件。
写完后点击右上角的Reload Pages,就可以看到创建好的页面。
第三部分,灯光控制器制作。
现在开始用第二块开发板,ESP32-S3-DEVKITC-1-N8R8来进行开发。
这里为了方便快捷,我使用circuitpython进行开发。去到官方固件下载页面,可以直接使用open installer按钮进行固件刷新。
https://circuitpython.org/board/espressif_esp32s3_devkitc_1_n8r8/
接着参考官方MQTT示例和neopixel,稍作修改,当收到对应的MQTT指令后点亮对应的灯就可以。关键的回调函数逻辑如下:
def message(client, topic, message):
global state
# This method is called when a topic the client is subscribed to
# has a new message.
print(f"New message on topic {topic}: {message}")
payload = json.loads(message)
if payload["event"] != "up":
return
if payload["text"] == "RED":
state["red"] = (not state["red"]) * 255
elif payload["text"] == "GREEN":
state["green"] = (not state["green"]) * 255
elif payload["text"] == "BLUE":
state["blue"] = (not state["blue"]) * 255
elif payload["text"] == "ALL":
if state["red"] + state["green"] + state["blue"]:
state["red"] = 0
state["green"] = 0
state["blue"] = 0
else :
state["red"] = 255
state["green"] = 255
state["blue"] = 255
pixels[0] = (state["red"], state["green"], state["blue"])
四、作品源码
五、作品功能演示视频
[localvideo]92ba9e453aaf69831e782251d6a180b7[/localvideo]
六、项目总结
这个项目使用了一块非常强大的触屏开发板,可以用它作为家庭物联网人机交互中枢来控制所有家电。通过这个项目我成功移植了openhasp,可以大大简化后续实际使用的复杂度。
- 2024-10-06
-
发表了主题帖:
【2024 DigiKey 创意大赛】得捷物料开箱贴
这次活动的物料选择很好,有我心心念念的ESP32-S3-LCD开发板,终于可以尝试下移植openHASP项目,实现家庭物联网控制中心
为了完成这个项目,我一共购买了两个物料。多买的S3模块会作为传感器和控制节点来测试控制中心的显示情况:
由于收到物料的时间比较晚,留给项目完成的时间也不多了。希望可以按时完成。
- 2024-08-08
-
上传了资料:
【Follow me第二季第1期】在arduino环境下多app调度全部任务
-
发表了主题帖:
【Follow me第二季第1期】在arduino环境下多app调度全部任务
本帖最后由 eew_dy9f48 于 2024-8-8 18:40 编辑
源代码:https://download.eeworld.com.cn/detail/eew_dy9f48/633952
非常高兴可以参加follow me活动,这次活动我购买了一块 Adafruit Circuit Playground Express开发板和一块raspberry pi zero 2w开发板。
为了完成任务,我还自己准备了一个9g舵机和一个ADS1115模块。
开发环境我使用的是Arduino,因为在Arduino上有非常丰富的例程,其中就包括多app切换的例程,正好适合这次活动,每个任务写成一个独立的app,然后通过按键来在app之间进行切换。
首先我们要在arduino中下载对应的board和library:
接着连接上开发板,就可以开始编程了。程序是基于库中mega_demo这个例程修改得到的。
入门任务
开发环境在上面已经配置好,点灯十分简单,只要把下面这两行添加到setup函数中就可以了。
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
在完成其他任务前,先看一下主程序结构,看是如何实现多app切换的。然后我们再分别在各个app中完成后面的一系列任务。
下面是主程序的代码:
#include <Adafruit_CircuitPlayground.h>
#include <Wire.h>
#include <SPI.h>
#include "Adafruit_SleepyDog.h"
// Include all the demos, note that each demo is defined in a separate class to keep the sketch
// code below clean and simple.
#include "Demo.h"
#include "RainbowCycleDemo.h"
#include "VUMeterDemo.h"
#include "CapTouchDemo.h"
#include "TiltDemo.h"
#include "SensorDemo.h"
#include "ProximityDemo.h"
// Create an instance of each demo class.
RainbowCycleDemo rainbowCycleDemo;
VUMeterDemo vuMeterDemo;
CapTouchDemo capTouchDemo;
TiltDemo tiltDemo;
SensorDemo sensorDemo;
ProximityDemo proximityDemo;
// Make a list of all demo class instances and keep track of the currently selected one.
int currentDemo = 0;
Demo* demos[] = {
&rainbowCycleDemo,
&sensorDemo,
&proximityDemo,
&tiltDemo,
&capTouchDemo,
&vuMeterDemo
};
void setup() {
// Initialize serial port and circuit playground library.
Serial.begin(115200);
Serial.println("Circuit Playground MEGA Demo!");
CircuitPlayground.begin();
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
}
void loop() {
// Check if slide switch is on the left (false) and go to sleep.
while (!CircuitPlayground.slideSwitch()) {
// Turn off the pixels, then go into deep sleep for a second.
CircuitPlayground.clearPixels();
Watchdog.sleep(1000);
}
// Check for any button presses by checking their state twice with
// a delay inbetween. If the first press state is different from the
// second press state then something was pressed/released!
bool leftFirst = CircuitPlayground.leftButton();
bool rightFirst = CircuitPlayground.rightButton();
delay(10);
// Run current demo's main loop.
demos[currentDemo]->loop();
// Now check for buttons that were released.
bool leftSecond = CircuitPlayground.leftButton();
bool rightSecond = CircuitPlayground.rightButton();
// Left button will change the current demo.
if (leftFirst && !leftSecond) {
// Turn off all the pixels when entering new mode.
CircuitPlayground.clearPixels();
// Increment the current demo (looping back to zero if at end).
currentDemo += 1;
if (currentDemo >= (sizeof(demos)/sizeof(Demo*))) {
currentDemo = 0;
}
Serial.print("Changed to demo: "); Serial.println(currentDemo, DEC);
}
// Right button will change the mode of the current demo.
if (rightFirst && !rightSecond) {
demos[currentDemo]->modePress();
}
}
从以上代码中可以看到,我们可以把每个不同的任务写在一个单独的h文件中的class里面,然后把这些class在主程序中创建好实例,再把这些实例装入一个列表中。然后在主循环loop中读取按键的读数,如果按键被按一次,则将index加一,通过这个index来选择需要运行的app,然后进入app后运行app中的loop函数。看到这里就可以明白了,我们只需要在每个单独的app文件中定义一个class,在这个class里面定义一个构造函数,这就相当于是标准arduino的setup函数,只运行一次;另外再定义一个名为loop的成员函数,这个函数会在主程序的loop中被加载,那么也就相当于是标准arduino的loop函数。而另一个按钮用作mode变化的功能,用来触发app中的modePress函数。
除此以外,我们还需要写一个Demo.h,里面装着所有app的父类,确保所有app类里的方法都具备一致性;同时还定义了一个线性外推函数,这个函数会在多个app中被使用,这样就不需要重复定义:
#ifndef DEMO_H
#define DEMO_H
// Define each mode with the following interface for a loop and modePress
// function that will be called during the main loop and if the mode button
// was pressed respectively. It's up to each mode implementation to fill
// these in and add their logic.
class Demo {
public:
virtual ~Demo() {}
virtual void loop() = 0;
virtual void modePress() = 0;
};
// Linear interpolation function is handy for all the modes to use.
float lerp(float x, float xmin, float xmax, float ymin, float ymax) {
if (x >= xmax) {
return ymax;
}
if (x <= xmin) {
return ymin;
}
return ymin + (ymax-ymin)*((x-xmin)/(xmax-xmin));
}
#endif
基础任务一
跑马灯可以直接使用库中自带的CircuitPlayground.colorWheel函数来实现颜色的计算。在代码中可以添加一个速度选择功能,当另一个按钮安触发时,切换速度列表中的下一个速度。
新建一个RainbowCycleDemo.h文件,写入下面内容就可以了。
#ifndef RAINBOWCYCLEDEMO_H
#define RAINBOWCYCLEDEMO_H
#include "Demo.h"
// Animation speeds to loop through with mode presses. The current milliseconds
// are divided by this value so the smaller the value the faster the animation.
static int speeds[] = { 5, 10, 50, 100 };
class RainbowCycleDemo: public Demo {
public:
RainbowCycleDemo() { currentSpeed = 0; }
~RainbowCycleDemo() {}
virtual void loop() {
// Make an offset based on the current millisecond count scaled by the current speed.
uint32_t offset = millis() / speeds[currentSpeed];
// Loop through each pixel and set it to an incremental color wheel value.
for(int i=0; i<10; ++i) {
CircuitPlayground.strip.setPixelColor(i, CircuitPlayground.colorWheel(((i * 256 / 10) + offset) & 255));
}
// Show all the pixels.
CircuitPlayground.strip.show();
}
virtual void modePress() {
// Increment through the available speeds.
currentSpeed += 1;
if (currentSpeed >= sizeof(speeds)/sizeof(int)) {
currentSpeed = 0;
}
}
private:
int currentSpeed;
};
#endif
基础任务二
这个任务中我们使用CircuitPlayground库直接读取光线强度和温度,然后把板载的LED分成两部分,蓝色指代温度,红色指代光强,用LED亮起的数量来代表温度和光强的数值。mode按钮可以用来切换指示的区间。
#ifndef SENSORDEMO_H
#define SENSORDEMO_H
#include "Demo.h"
// Define small, medium, large range of light sensor values.
static int minLight[] = { 0, 0, 0 };
static int maxLight[] = { 50, 255, 1023 };
// Define small, medium, large range of temp sensor values (in Fahrenheit).
static float mintempC[] = { 30.0, 25.0, 20.0 };
static float maxtempC[] = { 35.0, 38.0, 40.0 };
// Define color for light sensor pixels.
#define LIGHT_RED 0xFF
#define LIGHT_GREEN 0x00
#define LIGHT_BLUE 0x00
// Define color for temp sensor pixels.
#define TEMP_RED 0x00
#define TEMP_GREEN 0x00
#define TEMP_BLUE 0xFF
class SensorDemo : public Demo {
public:
SensorDemo() {
mode = 0;
}
~SensorDemo() {}
virtual void loop() {
// Reset all lights to off.
for (int i = 0; i < 10; ++i) {
CircuitPlayground.strip.setPixelColor(i, 0);
}
// Measure the light level and use it to light up its LEDs (right half).
uint16_t light = CircuitPlayground.lightSensor();
int level = (int)lerp(light, minLight[mode], maxLight[mode], 0.0, 5.0);
for (int i = 9; i > 9 - level; --i) {
CircuitPlayground.strip.setPixelColor(i, LIGHT_RED, LIGHT_GREEN, LIGHT_BLUE);
}
// Measure the temperatue and use it to light up its LEDs (left half).
float tempC = CircuitPlayground.temperature();
level = (int)lerp(tempC, mintempC[mode], maxtempC[mode], 0.0, 5.0);
for (int i = 0; i < level; ++i) {
CircuitPlayground.strip.setPixelColor(i, TEMP_RED, TEMP_GREEN, TEMP_BLUE);
}
// Light up the pixels!
CircuitPlayground.strip.show();
Serial.print("Light: ");
Serial.print(light);
Serial.print("; ");
Serial.print("Temperature: ");
Serial.println(tempC);
}
virtual void modePress() {
// Switch to one of three modes for small, medium, big ranges of measurements.
mode += 1;
if (mode > 2) {
mode = 0;
}
}
private:
int mode;
};
#endif
当我用手指遮住亮度传感器时,可以看到红色指示灯的变化:
我把热风机的功率调到最小,轻轻吹一下热敏电阻,也可以看到蓝色的温度指示灯发生变化。
基础任务三
这个任务我们使用板载的IR发射管来发送一个脉冲,接着再用IR接收管来检测红外强度。当有障碍物时,反射回来的红外线会被IR接收管观测到,因此可以测量出一个模拟量来。我们同样用LED亮起的数量来显示接近传感的位置,超过阈值后驱动蜂鸣器发出报警。
#ifndef PROXIMITYDEMO_H
#define PROXIMITYDEMO_H
#include "Demo.h"
class ProximityDemo : public Demo {
public:
ProximityDemo() {
mode = 0;
pinMode(CPLAY_IR_EMITTER, OUTPUT);
pinMode(CPLAY_IR_EMITTER, LOW);
pinMode(A10, INPUT);
}
~ProximityDemo() {}
virtual void loop() {
// Reset all lights to off.
for (int i = 0; i < 10; ++i) {
CircuitPlayground.strip.setPixelColor(i, 0);
}
// Measure the proximity level and use it to light up its LEDs.
digitalWrite(CPLAY_IR_EMITTER, HIGH);
delay(1);
digitalWrite(CPLAY_IR_EMITTER, LOW);
int prox = analogRead(A10);
int level = (int)lerp(prox, 300, 500, 0.0, 10.0);
for (int i = 0; i < level; ++i) {
CircuitPlayground.strip.setPixelColor(i, 0, 0, 255);
}
// Light up the pixels!
CircuitPlayground.strip.show();
if (level > 5) {
CircuitPlayground.playTone(330, 100);
}
Serial.print("Proximity: ");
Serial.println(prox);
}
virtual void modePress() {
}
private:
int mode;
};
#endif
可以看到手指接近时,蓝灯通过亮起数量显示了手指接近的程度。这里选用蓝色主要是为了避免红光可能会对红外线带来的干扰。
进阶任务
如果把板子水平放置在不倒翁体内,那么此时板载加速度计的Z轴是竖直向下的,理论Z轴读取到的加速度应该为9.8,也就是重力加速度。当板子发生倾斜时,z轴不再正对地心,因此加速度值会有所减少。利用这个原理可以通过观察Z轴加速度的值来间接观察不倒翁的倾倒程度。当完全水平时,板载LED显示蓝色;若倾倒到最大,这里我设定的是Z轴加速度减小到8时,板载LED变为红色。LED会在红色和蓝色之间线性变化。通过按模式按钮,还可以切换对X轴和Y轴加速的观测。
#ifndef TILTDEMO_H
#define TILTDEMO_H
#include "Demo.h"
// Define range of possible acceleration values.
#define MIN_ACCEL 8.0
#define MAX_ACCEL 10.0
// Define range of colors (min color and max color) using their red, green, blue components.
// First the min color:
#define MIN_COLOR_RED 0xFF
#define MIN_COLOR_GREEN 0x00
#define MIN_COLOR_BLUE 0x00
// Then the max color:
#define MAX_COLOR_RED 0x00
#define MAX_COLOR_GREEN 0x00
#define MAX_COLOR_BLUE 0xFF
class TiltDemo: public Demo {
public:
TiltDemo() { mode = 2; }
~TiltDemo() {}
virtual void loop() {
// Grab the acceleration for the current mode's axis.
float accel = 0;
switch (mode) {
case 0:
accel = CircuitPlayground.motionX();
break;
case 1:
accel = CircuitPlayground.motionY();
break;
case 2:
accel = CircuitPlayground.motionZ();
break;
}
// Now interpolate the acceleration into a color for the pixels.
uint8_t red = (uint8_t)lerp(accel, MIN_ACCEL, MAX_ACCEL, MIN_COLOR_RED, MAX_COLOR_RED);
uint8_t green = (uint8_t)lerp(accel, MIN_ACCEL, MAX_ACCEL, MIN_COLOR_GREEN, MAX_COLOR_GREEN);
uint8_t blue = (uint8_t)lerp(accel, MIN_ACCEL, MAX_ACCEL, MIN_COLOR_BLUE, MAX_COLOR_BLUE);
// Gamma correction makes LED brightness appear more linear
red = CircuitPlayground.gamma8(red);
green = CircuitPlayground.gamma8(green);
blue = CircuitPlayground.gamma8(blue);
// Light up all the pixels the interpolated color.
for (int i=0; i<10; ++i) {
CircuitPlayground.strip.setPixelColor(i, red, green, blue);
}
CircuitPlayground.strip.show();
}
virtual void modePress() {
// Change the mode (axis being displayed) to a value inside 0-2 for X, Y, Z.
mode += 1;
if (mode > 2) {
mode = 0;
}
}
private:
int mode;
};
#endif
完全水平放置时,灯光为蓝色。
当发生倾斜时,灯光逐渐变红:
创意任务二
这个任务稍微复杂一些,板子通过读取麦克风的音量大小,来驱动LED灯指示音量变化。同时,驱动舵机来操作章鱼哥触角变化。但在任务中我并没有直接用开发板来驱动舵机,而是使用到了板载唯一的一个DAC接口A0。我将识别到的音量大小通过DAC以模拟量发送出去,由接在树莓派上的16位ADC模块ADS1115接收,再通过树莓派来根据接收到的模拟量驱动舵机旋转。
开发板的app代码:
// This demo is based on the vumeter demo in the Adafruit Circuit Playground library.
#ifndef VUMETERDEMO_H
#define VUMETERDEMO_H
#include <math.h>
#include "Demo.h"
#define SAMPLE_WINDOW 10 // Sample window for average level
#define PEAK_HANG 24 // Time of pause before peak dot falls
#define PEAK_FALL 4 // Rate of falling peak dot
#define INPUT_FLOOR 56 // Lower range of mic sensitivity in dB SPL
#define INPUT_CEILING 110 // Upper range of mic sensitivity in db SPL
static byte peak = 16; // Peak level of column; used for falling dots
static unsigned int sample;
static byte dotCount = 0; //Frame counter for peak dot
static byte dotHangCount = 0; //Frame counter for holding peak dot
static float mapf(float x, float in_min, float in_max, float out_min, float out_max);
static void drawLine(uint8_t from, uint8_t to, uint32_t c);
class VUMeterDemo : public Demo {
public:
VUMeterDemo() {
currentCeiling = 0;
}
~VUMeterDemo() {}
virtual void loop() {
int numPixels = CircuitPlayground.strip.numPixels();
float peakToPeak = 0; // peak-to-peak level
unsigned int c, y;
//get peak sound pressure level over the sample window
peakToPeak = CircuitPlayground.mic.soundPressureLevel(SAMPLE_WINDOW);
//limit to the floor value
peakToPeak = max(INPUT_FLOOR, peakToPeak);
// Serial.println(peakToPeak);
//Fill the strip with rainbow gradient
for (int i = 0; i <= numPixels - 1; i++) {
CircuitPlayground.strip.setPixelColor(i, CircuitPlayground.colorWheel(map(i, 0, numPixels - 1, 30, 150)));
}
c = mapf(peakToPeak, INPUT_FLOOR, INPUT_CEILING, numPixels, 0);
// Turn off pixels that are below volume threshold.
if (c < peak) {
peak = c; // Keep dot on top
dotHangCount = 0; // make the dot hang before falling
}
if (c <= numPixels) { // Fill partial column with off pixels
drawLine(numPixels, numPixels - c, CircuitPlayground.strip.Color(0, 0, 0));
}
// Set the peak dot to match the rainbow gradient
y = numPixels - peak;
CircuitPlayground.strip.setPixelColor(y - 1, CircuitPlayground.colorWheel(map(y, 0, numPixels - 1, 30, 150)));
CircuitPlayground.strip.show();
// Frame based peak dot animation
if (dotHangCount > PEAK_HANG) { //Peak pause length
if (++dotCount >= PEAK_FALL) { //Fall rate
peak++;
dotCount = 0;
}
} else {
dotHangCount++;
}
analogWrite(A0, mapf(peakToPeak, INPUT_FLOOR, INPUT_CEILING, 0, 255));
}
virtual void modePress() {
}
private:
int currentCeiling;
};
static float mapf(float x, float in_min, float in_max, float out_min, float out_max) {
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
//Used to draw a line between two points of a given color
static void drawLine(uint8_t from, uint8_t to, uint32_t c) {
uint8_t fromTemp;
if (from > to) {
fromTemp = from;
from = to;
to = fromTemp;
}
for (int i = from; i <= to; i++) {
CircuitPlayground.strip.setPixelColor(i, c);
}
}
#endif
下面是树莓派部分。我们同样使用adafruit的blinka来驱动ADS1115以及舵机。先进行blinka以及DAS1115库的安装和配置:
sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get install python3-pip
sudo apt install --upgrade python3-setuptools
cd ~
sudo apt install python3-venv
python3 -m venv env --system-site-packages
source env/bin/activate
pip3 install --upgrade adafruit-python-shell
wget https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/raspi-blinka.py
sudo -E env PATH=$PATH python3 raspi-blinka.py
pip3 install adafruit-circuitpython-ads1x15
完成上面步骤后,可以在树莓派中新建一个main.py的文件,开始编写代码。代码读取到ADS1115的值后,转化成500-2500之间的脉宽,并通过18号脚发送出去。
import time
import board
import pwmio
import busio
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn
# Create the I2C bus
i2c = busio.I2C(board.SCL, board.SDA)
# Create the ADC object using the I2C bus
ads = ADS.ADS1115(i2c)
# Create single-ended input on channel 0
A0 = AnalogIn(ads, ADS.P0)
# Initialize PWM output for the servo (on pin BCM18):
servo = pwmio.PWMOut(board.D18, frequency=50)
# Create a function to simplify setting PWM duty cycle for the servo:
def servo_duty_cycle(pulse_us, frequency=50):
period_us = 1.0 / frequency * 1000000.0
duty_cycle = int(pulse_us / (period_us / 65535.0))
return duty_cycle
print("{:>5}\t{:>5}".format('raw', 'volt'))
while True:
try:
value = A0.value
voltage = A0.voltage
print("{:>5}\t{:>5.3f}".format(value, voltage))
dc = value / 16000.0 * 2000.0 + 500.0
print(dc)
servo.duty_cycle = servo_duty_cycle(dc)
except:
pass
finally:
time.sleep(0.1)
接线的话首先把playground开发板,树莓派,ADS1115模块和舵机都共地。树莓派40pin接口上有多个GND这时候就可以派上用途。接着SDA和SCL分别接在BCM2和BCM3上,舵机接在BCM18上,舵机使用树莓派5V引脚供电,ADS1115模块使用3.3V引脚供电。
只需要对板子吹气,就可以在麦克风处产生噪音,舵机和LED都做出了相应的反应:
创意任务三
水果钢琴是利用了板子的引脚在被触摸后电容发生变化的原理实现的,板子上只要支持电容测量的引脚都支持作为水果钢琴的键盘。这里由于我没有额外的接线夹,因此就不连接水果了,直接触摸引脚。当某个引脚被触发后,亮起相应的LED,并播放对应的声音,就可以实现钢琴效果。
#ifndef CAPTOUCHDEMO_H
#define CAPTOUCHDEMO_H
#include "Demo.h"
#define CAP_SAMPLES 20 // Number of samples to take for a capacitive touch read.
#define TONE_DURATION_MS 100 // Duration in milliseconds to play a tone when touched.
class CapTouchDemo: public Demo {
public:
uint16_t CAP_THRESHOLD = 200; // Threshold for a capacitive touch (higher = less sensitive).
CapTouchDemo() {
playSound = true;
if (CircuitPlayground.isExpress()) {
CAP_THRESHOLD = 800;
} else {
CAP_THRESHOLD = 200;
}
}
~CapTouchDemo() {}
virtual void loop() {
// Clear all the neopixels.
for (int i=0; i<10; ++i) {
CircuitPlayground.strip.setPixelColor(i, 0);
}
// Check if any of the cap touch inputs are pressed and turn on those pixels.
// Also play a tone if in tone playback mode.
if (CircuitPlayground.readCap(0, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(3, CircuitPlayground.colorWheel(256/10*3));
if (playSound) {
CircuitPlayground.playTone(330, TONE_DURATION_MS); // 330hz = E4
}
}
if (CircuitPlayground.readCap(1, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(4, CircuitPlayground.colorWheel(256/10*4));
if (playSound) {
CircuitPlayground.playTone(349, TONE_DURATION_MS); // 349hz = F4
}
}
if (CircuitPlayground.readCap(2, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(1, CircuitPlayground.colorWheel(256/10));
if (playSound) {
CircuitPlayground.playTone(294, TONE_DURATION_MS); // 294hz = D4
}
}
if (CircuitPlayground.readCap(3, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(0, CircuitPlayground.colorWheel(0));
if (playSound) {
CircuitPlayground.playTone(262, TONE_DURATION_MS); // 262hz = C4
}
}
if (CircuitPlayground.readCap(6, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(6, CircuitPlayground.colorWheel(256/10*6));
if (playSound) {
CircuitPlayground.playTone(440, TONE_DURATION_MS); // 440hz = A4
}
}
if (CircuitPlayground.readCap(9, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(8, CircuitPlayground.colorWheel(256/10*8));
if (playSound) {
CircuitPlayground.playTone(494, TONE_DURATION_MS); // 494hz = B4
}
}
if (CircuitPlayground.readCap(10, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(9, CircuitPlayground.colorWheel(256/10*9));
if (playSound) {
CircuitPlayground.playTone(523, TONE_DURATION_MS); // 523hz = C5
}
}
if (CircuitPlayground.readCap(12, CAP_SAMPLES) >= CAP_THRESHOLD) {
CircuitPlayground.strip.setPixelColor(5, CircuitPlayground.colorWheel(256/10*5));
if (playSound) {
CircuitPlayground.playTone(392, TONE_DURATION_MS); // 392hz = G4
}
}
// Light up the pixels.
CircuitPlayground.strip.show();
}
virtual void modePress() {
// Turn sound on/off.
playSound = !playSound;
}
private:
bool playSound;
};
#endif
至此,就完了所有的任务。在开篇的主程序文件中,我们已经把这些任务的文件都include在了主程序中,这样就可以通过板载的D4按钮来按顺序切换任务。
这次活动让我学会了多app管理调度的方法,我觉得收获非常大,希望下次活动还能继续参加,收获更多。
- 2024-05-10
-
发表了主题帖:
【2023 DigiKey大赛参与奖】开箱帖 Raspberry Pi 5 4G
感谢得捷和EEWORLD,这次活动获得了参与奖。我用参与奖购买了一个Raspberry Pi 5 4G
- 2024-03-02
-
加入了学习《FM4演示视频》,观看 FM4演示视频配音
-
加入了学习《FM4演示视频》,观看 FM4演示视频
-
加入了学习《【得捷电子Follow me第4期】使用micropython完成全部任务》,观看 【得捷电子Follow me第4期】使用micropython完成全部任务
- 2024-02-22
-
上传了资料:
FM4源代码
-
发表了主题帖:
【得捷Follow me第4期】汇总提交
Code:https://download.eeworld.com.cn/detail/eew_dy9f48/631268
前言:
在这个项目中,我使用到了wiznet W5500-EVB-Pico和M5stack Cardputer两块开发板。其中W5500-EVB-Pico作为主控,Cardputer作为显示器使用。两者通信使用的是tcp进行的。Cardputer由于内置了电池,因此不需要连线就可以使用,可以作为一款很不错的无线移动设备使用。
入门任务:
开发环境的搭建没有任何门槛,直接去micropython官网下载好W5500-EVB-Pico的uf2固件,第一次上电W5500-EVB-Pico时会自动跳出一个U盘,将uf2文件拷贝进去就可以完成固件烧写。IDE使用Thonny就可以,可以直接去官网下载安装包安装即可。
电灯的任务比较简单,用以下代码创建一个led对象,然后在循环中点亮,延迟,熄灭,再延迟就可以。需要注意的是,由于活动的任务较多,而这些任务按顺序执行,会导致执行到后面的任务时内存被占满。因此这里我们需要使用到gc库,当一个任务完成时,及时清理掉不再需要的对象,并释放内存,以免内存碎片化。
led = machine.Pin(25, machine.Pin.OUT)
for i in range(10):
led.high()
time.sleep(0.5)
led.low()
time.sleep(0.5)
屏幕显示的任务较为麻烦。因为两块开发板之间通讯依靠了tcp,因此我必须在这个任务中就完成联网的工作。具体联网方式会在下一个任务中详述,这里我们先讲讲联网完成后要怎么做。
连接网络后,我们要把W5500-EVB-Pico作为一个socket client,向cardputer发送需要显示的内容,Cardputer作为socket server,接收到信息后将其展示在屏幕上。
W5500-EVB-Pico部分代码如下:
import socket
def send(addr,port,text):
s = socket.socket()
try:
s.connect(socket.getaddrinfo(addr, port)[0][-1])
except Exception as e:
print(e)
try:
s.send(bytes(text, 'utf8'))
except Exception as e:
print(e)
而Cardputer比较麻烦,因为Cardputer的屏幕使用了ESP32S3的33,34这两个引脚。而由于这两个引脚在PSRAM版本上会有占用,因此乐鑫官方的模块根本就没有引出他们,而导致市面上现成的固件,无论是micropython或是circuitpython,都缺少这两个引脚的定义。这也就意味着我们需要重新编译固件。
由于我电脑上有现成的circuitpython编译环境,因此这里我就使用了circuitpython。具体编译的细节由于和任务关系不大,这里就不提了。附件中我会放入编译好的固件。
代码部分主要分了两块,一块是socket server部分,负责接收从W5500-EVB-Pico传来的字符串;另一部分是显示部分,显示器对象我已经直接编译在了固件中,因此这里不需要再初始化,直接使用即可。为了显示中文,这里还需要再导入一个wenquanyi的pcf字体文件。
import board
import wifi
import socketpool
import select
import displayio
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text import label, wrap_text_to_pixels
while not wifi.radio.ipv4_address:
pass
RES = (240, 135)
pool = socketpool.SocketPool(wifi.radio)
sock = pool.socket(pool.AF_INET, pool.SOCK_STREAM)
sock.settimeout(None)
sock.setblocking(True)
sock.bind(("0.0.0.0", 8888))
sock.listen(1)
display = board.DISPLAY
group = displayio.Group()
display.root_group = group
COLOR = {
"black": 0x000000,
"white": 0xFFFFFF,
"red": 0xFF0000,
"green": 0x00FF00,
"blue": 0x0000FF,
"cyan": 0x00FFFF,
"magenta": 0xFF00FF,
"yellow": 0xFFFF00,
}
bg_bitmap = displayio.Bitmap(RES[0], RES[1], 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = COLOR["magenta"]
bg_tg = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette, x=0, y=0)
group.append(bg_tg)
FONT = bitmap_font.load_font("/wenquanyi_13px.pcf")
text_tg = label.Label(FONT, color=COLOR["yellow"])
text_tg.anchor_point = (0.5, 0.5)
text_tg.anchored_position = (RES[0] / 2, RES[1] / 2)
text_tg.background_color = None
text_tg.line_spacing = 1.1
text_tg.scale = 2
group.append(text_tg)
def handlereq():
data = None
r, w, err = select.select((sock,), (), (), 0)
if r:
for readable in r:
conn, addr = sock.accept()
with conn:
print("Connected by", addr)
data = bytearray(1024)
print("Receiving")
conn.settimeout(None)
numbytes = conn.recv_into(data)
data = data[:numbytes]
print(data)
return data
def loop():
data = handlereq()
if data is not None:
text = data.decode("utf-8")
text_list = wrap_text_to_pixels(text, RES[0] / text_tg.scale, font=FONT)
show = ""
for i in text_list:
show += i.replace("-", "") + "\n"
print(text_list)
text_tg.text = show
text_tg.text = str(wifi.radio.ipv4_address)
print("Ready")
while True:
try:
loop()
except Exception as e:
print(e)
当然,从首行代码就可以看出来,这个代码要想顺利运行,需要先确保cardputer已经连上网络。circuitpython联网非常简单,在settings.toml中写入以下内容,然后按rst重置即可。
CIRCUITPY_WIFI_SSID="your_ssid"
CIRCUITPY_WIFI_PASSWORD="your_password"
一切顺利的话,重置过后代码自动运行,我们就可以看到cardputer的ip地址显示在屏幕上,我们就可以用这个地址来给它发消息。
lcd = "192.168.199.139"
lcd_port = 8888
send(lcd, lcd_port, "入门任务")
基础任务一
联网的方法在官方的github中,可以直接拿出来用。这里我把它写在一个单独的文件中,在主程序中直接导入调用就可以,这样可以让代码更加清晰易读。
from machine import Pin,SPI
import network
import time
def w5500(config): # could be "dhcp" or ('192.168.x.xxx','255.255.255.0','192.168.x.1','8.8.8.8')
#spi init
spi=SPI(0,2_000_000, mosi=Pin(19),miso=Pin(16),sck=Pin(18))
nic = network.WIZNET5K(spi,Pin(17),Pin(20)) #spi,cs,reset pin
nic.active(True)#nicwork active
nic.ifconfig(config)
while not nic.isconnected():
time.sleep(1)
# print(nic.regs())#Print register information
#Print nicwork address information
print("IP Address:",nic.ifconfig()[0])
print("Subnic Mask:",nic.ifconfig()[1])
print("Gateway:",nic.ifconfig()[2])
print("DNS:",nic.ifconfig()[3])
return nic
from w5500 import w5500
from ping import ping
ip = "192.168.199.121"
gate = ip.split(".")
gate[-1] = "1"
gate = ".".join(gate)
nic = w5500((ip, "255.255.255.0", gate, "8.8.8.8"))
ping(gate, quiet=True)
最后的quiet ping是为了阻塞代码,确保已经联网后再执行后续代码。ping方法使用的是基于8266的uping修改而来,两者socket方法一致,修改的仅仅是ping的超时判断逻辑,以及增加使用方法,修改默认ping次数。同样我们把它单独放在一个文件中,导入使用。
ping完后,将汇总信息显示到LCD上:
# μPing (MicroPing) for MicroPython
# copyright (c) 2018 Shawwwn <shawwwn1@gmail.com>
# License: MIT
# Internet Checksum Algorithm
# Author: Olav Morken
# https://github.com/olavmrk/python-ping/blob/master/ping.py
# @data: bytes
def checksum(data):
if len(data) & 0x1: # Odd number of bytes
data += b'\0'
cs = 0
for pos in range(0, len(data), 2):
b1 = data[pos]
b2 = data[pos + 1]
cs += (b1 << 8) + b2
while cs >= 0x10000:
cs = (cs & 0xffff) + (cs >> 16)
cs = ~cs & 0xffff
return cs
def ping(host, count=4, timeout=30, interval=1, quiet=False, size=64):
import time
import select
import uctypes
import socket
import struct
import random
# prepare packet
assert size >= 16, "pkt size too small"
pkt = b'Q'*size
pkt_desc = {
"type": uctypes.UINT8 | 0,
"code": uctypes.UINT8 | 1,
"checksum": uctypes.UINT16 | 2,
"id": uctypes.UINT16 | 4,
"seq": uctypes.INT16 | 6,
"timestamp": uctypes.UINT64 | 8,
} # packet header descriptor
h = uctypes.struct(uctypes.addressof(pkt), pkt_desc, uctypes.BIG_ENDIAN)
h.type = 8 # ICMP_ECHO_REQUEST
h.code = 0
h.checksum = 0
h.id = random.getrandbits(16)
h.seq = 1
# init socket
sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, 1)
sock.setblocking(0)
sock.settimeout(timeout/1000)
c = 0
addr = None
while c < 10:
try:
c += 1
time.sleep_ms(100)
addr = socket.getaddrinfo(host, 1)[0][-1][0] # ip address
break
except:
pass
if not addr:
print("DNS lookup failed")
return
sock.connect((addr, 1))
not quiet and print("PING %s (%s): %u data bytes" % (host, addr, len(pkt)))
seqs = list(range(1, count+1)) # [1,2,...,count]
c = 1
t = time.time()
tstart = time.time()
n_trans = 0
n_recv = 0
finish = False
while time.time() - tstart < timeout:
if time.time() - t >= interval and c<=count:
# send packet
h.checksum = 0
h.seq = c
h.timestamp = time.ticks_us()
h.checksum = checksum(pkt)
if sock.send(pkt) == size:
n_trans += 1
t = time.time() # reset timeout
else:
seqs.remove(c)
# recv packet
socks, _, _ = select.select([sock], [], [], 0)
if socks:
resp = socks[0].recv(4096)
resp_mv = memoryview(resp)
h2 = uctypes.struct(uctypes.addressof(resp_mv[20:]), pkt_desc, uctypes.BIG_ENDIAN)
# TODO: validate checksum (optional)
seq = h2.seq
if h2.type==0 and h2.id==h.id and (seq in seqs): # 0: ICMP_ECHO_REPLY
t_elasped = (time.ticks_us()-h2.timestamp) / 1000
ttl = struct.unpack('!B', resp_mv[8:9])[0] # time-to-live
n_recv += 1
c += 1
not quiet and print("%u bytes from %s: icmp_seq=%u, ttl=%u, time=%f ms" % (len(resp), addr, seq, ttl, t_elasped))
seqs.remove(seq)
if len(seqs) == 0:
finish = True
if finish:
break
# close
sock.close()
ret = (n_trans, n_recv)
not quiet and print("%u packets transmitted, %u packets received" % (n_trans, n_recv))
return (n_trans, n_recv)
send(lcd, lcd_port, "基础任务一")
time.sleep(2)
send(lcd, lcd_port, "Pinging")
(n_trans, n_recv) = ping("digikey.cn")
send(lcd, lcd_port, "Ping digikey.cn\n%u transmitted\n%u received" % (n_trans, n_recv))
time.sleep(5)
del led
del ping
gc.collect()
input("Input anything to continue.")
在代码最后我们通过input方式阻塞程序,这样我们有时间可以去完成电脑ping开发板并抓包的任务
打开wireshark,开始抓包,然后设定筛选内容为ip.src==192.168.199.121 or ip.dst==192.168.199.121,这里的ip是在上面联网功能中设定的。
接着打开cmd ping 192.168.199.121,看到下面输出,就说明成功ping通。
查看wireshark抓到的包,就可以看到具体数据包。协议是ICMP, INFO是 echo ping,说明一切正常。
基础任务二:
我们之前让TCP的显示屏正常,说明已经完成了TCP Client和TCP Server的实现。但TCP Server并不在W5500-EVB-Pico上。因此这里我们再在W5500-EVB-Pico上建立一个TCP 服务器,由电脑向该服务器发送文字,W5500-EVB-Pico收到后,再作为TCP Client,把该文字发送给cardputer进行显示。
send(lcd, lcd_port, "基础任务二")
addr = socket.getaddrinfo('0.0.0.0', 8080)[0][-1]
server = socket.socket()
server.bind(addr)
server.listen(1)
print('listening on', addr)
for i in range(3):
conn, addr = server.accept()
print("Connected by", addr)
data = conn.recv(1024)
conn.close()
send(lcd, lcd_port, data.decode())
del conn
del addr
del data
gc.collect()
server.close()
del server
gc.collect()
time.sleep(2)
在pc端我们使用windows工具TCPUDP网络调试助手来发送信息。
发送完该信息后,我们看到串口显示了服务器接收到内容,同时cardputer也显示了我们发送的文字。
再看看wireshark,也成功抓到了我们发送的包,信息的具体内容以NTF-8编码的形式显示。
如果我们发送的是英文信息,属于ASCII,就可以直接显示出来。
进阶任务:
ntp实现不再需要自己写库,micropython中内置的ntptime可以直接实现。
import ntptime
send(lcd, lcd_port, "进阶任务")
time.sleep(2)
for i in range(10):
try:
ntptime.settime()
t = time.localtime(time.time() + 8*3600)
output = "%s-%s-%s %s:%s:%s" % (t[0],t[1],t[2],t[3],t[4],t[5])
print("ntp time(BeiJing): " + output)
send(lcd, lcd_port, "北京时间:\n" + output)
break
except:
print("Can not get time!")
time.sleep_ms(1000)
time.sleep(5)
del ntptime
del t
del output
gc.collect()
运行过后,就可以在串口看到时间,屏幕也会打印出来。
终极任务:
这个任务跟ping一样,我们同样使用8266的uftp库进行一些修改。socket方法两者都一致,唯一的不同是我们需要外部传入W5500-EVB-PICO的ip地址,来组装PASV命令的返回信息。
#
# Small ftp server for ESP8266 ans ESP32 Micropython
#
# Based on the work of chrisgp - Christopher Popp and pfalcon - Paul Sokolovsky
#
# The server accepts passive mode only.
# It runs in foreground and quits, when it receives a quit command
# Start the server with:
#
# import ftp
#
# Copyright (c) 2016 Christopher Popp (initial ftp server framework)
# Copyright (c) 2016 Robert Hammelrath (putting the pieces together
# and a few extensions)
# Distributed under MIT License
#
import socket
import network
import uos
import gc
def send_list_data(path, dataclient, full):
try: # whether path is a directory name
for fname in sorted(uos.listdir(path), key=str.lower):
dataclient.sendall(make_description(path, fname, full))
except: # path may be a file name or pattern
pattern = path.split("/")[-1]
path = path[:-(len(pattern) + 1)]
if path == "":
path = "/"
for fname in sorted(uos.listdir(path), key=str.lower):
if fncmp(fname, pattern):
dataclient.sendall(make_description(path, fname, full))
def make_description(path, fname, full):
if full:
stat = uos.stat(get_absolute_path(path, fname))
file_permissions = ("drwxr-xr-x"
if (stat[0] & 0o170000 == 0o040000)
else "-rw-r--r--")
file_size = stat[6]
description = "{} 1 owner group {:>10} Jan 1 2000 {}\r\n".format(
file_permissions, file_size, fname)
else:
description = fname + "\r\n"
return description
def send_file_data(path, dataclient):
with open(path, "rb") as file:
chunk = file.read(512)
while len(chunk) > 0:
dataclient.sendall(chunk)
chunk = file.read(512)
def save_file_data(path, dataclient):
with open(path, "wb") as file:
chunk = dataclient.recv(512)
while len(chunk) > 0:
file.write(chunk)
chunk = dataclient.recv(512)
def get_absolute_path(cwd, payload):
# Just a few special cases "..", "." and ""
# If payload start's with /, set cwd to /
# and consider the remainder a relative path
if payload.startswith('/'):
cwd = "/"
for token in payload.split("/"):
if token == '..':
if cwd != '/':
cwd = '/'.join(cwd.split('/')[:-1])
if cwd == '':
cwd = '/'
elif token != '.' and token != '':
if cwd == '/':
cwd += token
else:
cwd = cwd + '/' + token
return cwd
# compare fname against pattern. Pattern may contain
# wildcards ? and *.
def fncmp(fname, pattern):
pi = 0
si = 0
while pi < len(pattern) and si < len(fname):
if (fname[si] == pattern[pi]) or (pattern[pi] == '?'):
si += 1
pi += 1
else:
if pattern[pi] == '*': # recurse
if (pi + 1) == len(pattern):
return True
while si < len(fname):
if fncmp(fname[si:], pattern[pi+1:]):
return True
else:
si += 1
return False
else:
return False
if pi == len(pattern.rstrip("*")) and si == len(fname):
return True
else:
return False
def ftpserver(net, port=21, timeout=None):
DATA_PORT = 13333
ftpsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
datasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ftpsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
datasocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ftpsocket.bind(socket.getaddrinfo("0.0.0.0", port)[0][4])
datasocket.bind(socket.getaddrinfo("0.0.0.0", DATA_PORT)[0][4])
ftpsocket.listen(1)
ftpsocket.settimeout(timeout)
datasocket.listen(1)
datasocket.settimeout(None)
msg_250_OK = '250 OK\r\n'
msg_550_fail = '550 Failed\r\n'
addr = net.ifconfig()[0]
print("FTP Server started on ", addr)
try:
dataclient = None
fromname = None
do_run = True
while do_run:
cl, remote_addr = ftpsocket.accept()
cl.settimeout(300)
cwd = '/'
try:
# print("FTP connection from:", remote_addr)
cl.sendall("220 Hello, this is Micropython.\r\n")
while True:
gc.collect()
data = cl.readline().decode("utf-8").rstrip("\r\n")
if len(data) <= 0:
print("Client disappeared")
do_run = False
break
command = data.split(" ")[0].upper()
payload = data[len(command):].lstrip()
path = get_absolute_path(cwd, payload)
print("Command={}, Payload={}".format(command, payload))
if command == "USER":
cl.sendall("230 Logged in.\r\n")
elif command == "SYST":
cl.sendall("215 UNIX Type: L8\r\n")
elif command == "NOOP":
cl.sendall("200 OK\r\n")
elif command == "FEAT":
cl.sendall("211 no-features\r\n")
elif command == "PWD" or command == "XPWD":
cl.sendall('257 "{}"\r\n'.format(cwd))
elif command == "CWD" or command == "XCWD":
try:
files = uos.listdir(path)
cwd = path
cl.sendall(msg_250_OK)
except:
cl.sendall(msg_550_fail)
elif command == "CDUP":
cwd = get_absolute_path(cwd, "..")
cl.sendall(msg_250_OK)
elif command == "TYPE":
# probably should switch between binary and not
cl.sendall('200 Transfer mode set\r\n')
elif command == "SIZE":
try:
size = uos.stat(path)[6]
cl.sendall('213 {}\r\n'.format(size))
except:
cl.sendall(msg_550_fail)
elif command == "QUIT":
cl.sendall('221 Bye.\r\n')
do_run = False
break
elif command == "PASV":
cl.sendall('227 Entering Passive Mode ({},{},{}).\r\n'.
format(addr.replace('.', ','), DATA_PORT >> 8,
DATA_PORT % 256))
dataclient, data_addr = datasocket.accept()
print("FTP Data connection from:", data_addr)
DATA_PORT = 13333
active = False
elif command == "PORT":
items = payload.split(",")
if len(items) >= 6:
data_addr = '.'.join(items[:4])
# replace by command session addr
if data_addr == "127.0.1.1":
data_addr = remote_addr
DATA_PORT = int(items[4]) * 256 + int(items[5])
dataclient = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
dataclient.settimeout(10)
dataclient.connect((data_addr, DATA_PORT))
print("FTP Data connection with:", data_addr)
cl.sendall('200 OK\r\n')
active = True
else:
cl.sendall('504 Fail\r\n')
elif command == "LIST" or command == "NLST":
if not payload.startswith("-"):
place = path
else:
place = cwd
try:
cl.sendall("150 Here comes the directory listing.\r\n")
send_list_data(place, dataclient,
command == "LIST" or payload == "-l")
cl.sendall("226 Listed.\r\n")
except:
cl.sendall(msg_550_fail)
if dataclient is not None:
dataclient.close()
dataclient = None
elif command == "RETR":
try:
cl.sendall("150 Opening data connection.\r\n")
send_file_data(path, dataclient)
cl.sendall("226 Transfer complete.\r\n")
except:
cl.sendall(msg_550_fail)
if dataclient is not None:
dataclient.close()
dataclient = None
elif command == "STOR":
try:
cl.sendall("150 Ok to send data.\r\n")
save_file_data(path, dataclient)
cl.sendall("226 Transfer complete.\r\n")
except:
cl.sendall(msg_550_fail)
if dataclient is not None:
dataclient.close()
dataclient = None
elif command == "DELE":
try:
uos.remove(path)
cl.sendall(msg_250_OK)
except:
cl.sendall(msg_550_fail)
elif command == "RMD" or command == "XRMD":
try:
uos.rmdir(path)
cl.sendall(msg_250_OK)
except:
cl.sendall(msg_550_fail)
elif command == "MKD" or command == "XMKD":
try:
uos.mkdir(path)
cl.sendall(msg_250_OK)
except:
cl.sendall(msg_550_fail)
elif command == "RNFR":
fromname = path
cl.sendall("350 Rename from\r\n")
elif command == "RNTO":
if fromname is not None:
try:
uos.rename(fromname, path)
cl.sendall(msg_250_OK)
except:
cl.sendall(msg_550_fail)
else:
cl.sendall(msg_550_fail)
fromname = None
elif command == "MDTM":
try:
tm=localtime(uos.stat(path)[8])
cl.sendall('213 {:04d}{:02d}{:02d}{:02d}{:02d}{:02d}\r\n'.format(*tm[0:6]))
except:
cl.sendall('550 Fail\r\n')
elif command == "STAT":
if payload == "":
cl.sendall("211-Connected to ({})\r\n"
" Data address ({})\r\n"
"211 TYPE: Binary STRU: File MODE:"
" Stream\r\n".format(
remote_addr[0], addr))
else:
cl.sendall("213-Directory listing:\r\n")
send_list_data(path, cl, True)
cl.sendall("213 Done.\r\n")
else:
cl.sendall("502 Unsupported command.\r\n")
print("Unsupported command {} with payload {}".format(
command, payload))
except Exception as err:
print(err)
finally:
cl.close()
cl = None
except Exception as e:
print(e)
finally:
datasocket.close()
ftpsocket.close()
if dataclient is not None:
dataclient.close()
# ftpserver()
主程序导入并运行后,我们就可用window的文件资源管理器来上传下载文件,只需要输入地址ftp://192.168.199.121
活动心得体会
这次的任务由于发现了一块以前没有听说过的cardputer,实现起来变得有意思了很多。对于主控板wiznet W5500-EVB-Pico来说,还是有一些美中不足的地方。原本我打算全部在circuitpython上实现功能的,但在使用的过程中发现,虽然官方有支持circuitpython,但里面的socket方法有很多并不全,且circuitpython本身并不支持uastype,导致在移植micropython的ping方法时发现不太可行,由于时间所限,不得已才按论坛中大部分人的做法使用了micropython。未来有时间,我想要学习一下网络底层的用法,看看是否能补全官方库,让这块开发板在circuitpython环境下也能够顺畅跑起来。
- 2024-01-11
-
发表了主题帖:
DigiKey“智造万物,快乐不停”创意大赛】提交:机器视觉打造全自动老板键智能键盘
本帖最后由 eew_dy9f48 于 2024-1-11 12:26 编辑
利用机器视觉打造带有全自动老板键的智能键盘
作者:eew_dy9f48
一、作品简介
自带键盘的树莓派Pi400,其实可以看作一块强大的智能键盘,作为电脑的辅助;按照项目内容层层递进,首先,先配置树莓派p400,让它作为一块普通电脑键盘;接下来,我想尝试结合一些人工智能实现键盘命令的自动执行,比如全自动老板键,通过ESP32-CAM作为树莓派的网络摄像头,这样可以脱离连线限制,可以部署在任何地方。然后一旦识别到老板人脸,就自动发送Alt+Tab等老板键,瞬间切换至工作界面。除此之外,由于在上述任务中我们已经实现了树莓派HID键盘的配置,因此我们还可以使用它来作为游戏助手,实现键盘宏,按一个按键打出一整套combo。由于宏是内建在键盘中的,在pc端并没有任何相关进程,所以不会被游戏判定为使用辅助工具。这就可以实现很多功能上的延申,大家可以自行玩耍。
二、系统框图
功能模块一共三部分:
Pi400 HID键盘功能实现。
Pi400 键盘动作的捕捉与独占。
人脸识别在Pi400上的实现。
三、各部分功能说明
首先,先介绍下项目中包含的硬件设计。
这个项目包括了无线网络摄像头的制作。虽然网上有现成的ESP32CAM模块售卖,且价格非常便宜。但是由于做工良莠不齐,导致经常翻车。而且,ESP32性能较弱,且不支持USB,如果未来想做一些其他的开发也可能会力不从心。因此,趁这个项目的机会,我打算直接制作一块ESP32S2 CAM开发板出来。
这个摄像头开发板其实我后面还重新绘制了第二个版本,增加了tf卡槽,同时修复了飞线的问题,并由于板子面积有限将所有封装换成了0402。但由于新版的板子打样回来焊接难度有点大,还一直迟迟没有动手,因此先使用老版本的板子完成项目。
接下来说说软件方面,我们具体介绍一下每一个功能模块的实现方法。
1,Pi400 HID 键盘功能的实现。
在github上有一个zero_hid的库,可以实现使用树莓派zero模拟hid键盘。但这个库有一些问题,直接使用在组合键上会出很多的问题,因此我参考这个项目,重写了一下这个库。
首先科普一下HID协议,HID键盘协议是一种基于报文的协议,通过在USB总线上进行通信。当用户按下键盘上的按键时,键盘将生成一个HID报文,并将其发送到计算机。计算机收到报文后,根据报文的内容来模拟相应的键盘操作,例如在文本编辑器中输入字符或执行特定的功能。
HID键盘报文包含多个字段,其中最重要的是按键码(Keycode)。按键码表示按下的键的唯一标识符,例如“A”键的按键码是0x04。除了按键码外,报文还可以包含其他信息,如修饰键(如Shift、Ctrl和Alt键)的状态和组合键的状态。
因此,在合成报文前,我们先要知道我们想输入的按键哪些是修饰键,而哪些是按键,他们要分开进行处理。
在进入代码部分前,我们需要先安装一下驱动。首先先新建一个文件,命名为isticktoit_usb,添加可执行权限,并填入以下内容:
···
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p isticktoit
cd isticktoit
echo 0x1d6b > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2
mkdir -p strings/0x409
echo "fedcba9876543210" > strings/0x409/serialnumber
echo "Tobias Girstmair" > strings/0x409/manufacturer
echo "iSticktoit.net USB Device" > strings/0x409/product
mkdir -p configs/c.1/strings/0x409
echo "Config 1: ECM network" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# Add functions here
mkdir -p functions/hid.usb0
echo 1 > functions/hid.usb0/protocol
echo 1 > functions/hid.usb0/subclass
echo 8 > functions/hid.usb0/report_length
echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > functions/hid.usb0/report_desc
ln -s functions/hid.usb0 configs/c.1/
# End functions
ls /sys/class/udc > UDC
···
接着运行以下命令,完成驱动配置:
···
#!/bin/bash
echo "" | sudo tee -a /boot/config.txt
echo "# BEGIN HID Keyboard Simulation" | sudo tee -a /boot/config.txt
echo "dtoverlay=dwc2" | sudo tee -a /boot/config.txt
echo "# END HID Keyboard Simulation" | sudo tee -a /boot/config.txt
echo "" | sudo tee -a /etc/modules
echo "# BEGIN HID Keyboard Simulation" | sudo tee -a /etc/modules
echo "dwc2" | sudo tee -a /etc/modules
echo "libcomposite" | sudo tee -a /etc/modules
echo "# END HID Keyboard Simulation" | sudo tee -a /etc/modules
# Move to before exit 0
echo "" | sudo tee -a /etc/rc.local
echo "# BEGIN HID Keyboard Simulation" | sudo tee -a /etc/rc.local
echo "sudo ./isticktoit_usb" | sudo tee -a /etc/rc.local
echo "# END HID Keyboard Simulation" | sudo tee -a /etc/rc.local
···
完成后,以后每次重启完成,只需要运行一下isticktoit_usb即可。
处理报文部分的代码如下:
···
from typing import List
from .hid import hidwrite
from .hid.keycodes import KeyCodes
from time import sleep
import json
import pkgutil
import os
import pathlib
class Keyboard:
def __init__(self, dev='/dev/hidg0') -> None:
self.dev = dev
self.set_layout()
self.control_pressed = []
self.key_pressed = []
def list_layout(self):
keymaps_dir = pathlib.Path(__file__).parent.absolute() / 'keymaps'
keymaps = os.listdir(keymaps_dir)
files = [f for f in keymaps if f.endswith('.json')]
for count, fname in enumerate(files, 1):
with open(keymaps_dir / fname , encoding='UTF-8') as f:
content = json.load(f)
name, desc = content['Name'], content['Description']
print(f'{count}. {name}: {desc}')
def set_layout(self, language='US'):
self.layout = json.loads( pkgutil.get_data(__name__, f"keymaps/{language}.json").decode() )
def gen_list(self, keys = []):
_control_pressed = []
_key_pressed = []
for key in keys:
if key[:3] == "MOD":
_control_pressed.append(KeyCodes[key])
else:
_key_pressed.append(KeyCodes[key])
return _control_pressed, _key_pressed
def gen_buf(self):
self.buf = [sum(self.control_pressed),0] + self.key_pressed
self.buf += [0] * (8 - len(self.buf)) # fill to lenth 8
##########################################################################
# For user
def press(self, keys = [], additive=False, hold=False):
_control_pressed, _key_pressed = self.gen_list(keys)
if not additive:
self.control_pressed = []
self.key_pressed = []
self.control_pressed.extend(_control_pressed)
self.control_pressed = list(set(self.control_pressed)) # remove repeated items
self.key_pressed.extend(_key_pressed)
self.key_pressed = list(set(self.key_pressed))[:6] # remove repeated items and cut until 6 items
self.gen_buf()
hidwrite.write_to_hid_interface(self.dev, self.buf)
if not hold:
self.release(keys)
def release(self, keys = []):
_control_pressed, _key_pressed = self.gen_list(keys)
try:
self.control_pressed = list(set(self.control_pressed) - set(_control_pressed))
except:
pass
try:
self.key_pressed = list(set(self.key_pressed) - set(_key_pressed))
except:
pass
self.gen_buf()
hidwrite.write_to_hid_interface(self.dev, self.buf)
def release_all(self):
self.control_pressed = []
self.key_pressed = []
self.gen_buf()
hidwrite.write_to_hid_interface(self.dev, self.buf)
def text(self, string, delay=0):
for c in string:
key_map = self.layout['Mapping'][c]
key_map = key_map[0]
mods = key_map['Modifiers']
keys = key_map['Keys']
self.press(mods + keys)
sleep(delay)
···
上面这段代码把想要输出的按键分为control(修饰按键)和key(普通按键)两块,再组合形成报文列表。使用的逻辑是输入当前想要按下的按键状态,然后程序发送对应的报文。
测试一下:
···
import os
import zero_hid
if os.geteuid() != 0:
raise ImportError('You must be root to use this library on linux.')
k = zero_hid.Keyboard()
k.press(["KEY_H"], additive=False, hold=False)
k.press(["KEY_E"], additive=False, hold=False)
k.press(["KEY_L"], additive=False, hold=False)
k.press(["KEY_L"], additive=False, hold=False)
k.press(["KEY_O"], additive=False, hold=False)
···
press方法中填入的是一个list,表示当前按下的所有按键。具体的键值列表在zero_hid/keymaps/US.json中。
如果电脑成功打印,表示功能正常。
2、Pi400 键盘动作的捕捉与独占。
一般在python中捕获键盘动作,大家使用的都是keyboard库,简单好用。但keyboard库有个致命的问题,就是无法独占键盘。这在我们当前的应用中是无法接受的。试想一下,当我们想发送ctrl+alt+del时,一旦按下,树莓派和电脑都进入了安全模式。你无法预期在键盘上的操作会在树莓派系统中整出什么幺蛾子。因此,我们需要在捕捉键盘动作的同时,对键盘资源进行独占,以此避免按键被其他的进程捕获。在这里我们使用evdev库来实现。
···
import os
import evdev
if os.geteuid() != 0:
raise ImportError('You must be root to use this library on linux.')
dev = evdev.InputDevice('/dev/input/event0')
dev.grab() # grab 是为了独占,保证此设备不会被别的进程捕获
for event in dev.read_loop():
key = evdev.categorize(event)
if isinstance(key, evdev.KeyEvent) and key.keystate != 2:
print(key.keycode)
···
按下按键,我们就可以看到对应的键值被打印在终端里。
接下来只需要把抓取到的键值组合成列表,发送到我们上一步实现的hid中即可。
细心的同学可能会意识到,evdev抓取到的的键值如果和hid的键值不匹配怎么办?这里我们就需要人工进行匹配,创建一个文件,将他们一一对应起来。
在项目文件夹下创建一个codemap.csv文件,写入以下对应:
···
KEY_LEFTCTRL,MOD_LEFT_CONTROL
KEY_RIGHTCTRL,MOD_RIGHT_CONTROL
KEY_LEFTALT,MOD_LEFT_ALT
KEY_RIGHTALT,MOD_RIGHT_ALT
KEY_LEFTSHIFT,MOD_LEFT_SHIFT
KEY_RIGHTSHIFT,MOD_RIGHT_SHIFT
,
KEY_LEFTMETA,MOD_LEFT_GUI
,
,
KEY_ESC,KEY_ESC
KEY_TAB,KEY_TAB
KEY_CAPSLOCK,KEY_CAPSLOCK
,
KEY_NUMLOCK,KEY_NUMLOCK
KEY_SYSRQ,KEY_SYSRQ
KEY_DELETE,KEY_DELETE
KEY_INSERT,KEY_INSERT
KEY_BACKSPACE,KEY_BACKSPACE
KEY_ENTER,KEY_ENTER
,
KEY_SPACE,KEY_SPACE
,
KEY_UP,KEY_UP
KEY_DOWN,KEY_DOWN
KEY_LEFT,KEY_LEFT
KEY_RIGHT,KEY_RIGHT
,
KEY_PAGEUP,KEY_PAGEUP
KEY_PAGEDOWN,KEY_PAGEDOWN
KEY_HOME,KEY_HOME
KEY_END,KEY_END
,
KEY_F1,KEY_F1
KEY_F2,KEY_F2
KEY_F3,KEY_F3
KEY_F4,KEY_F4
KEY_F5,KEY_F5
KEY_F6,KEY_F6
KEY_F7,KEY_F7
KEY_F8,KEY_F8
KEY_F9,KEY_F9
KEY_F10,KEY_F10
KEY_F11,KEY_F11
KEY_F12,KEY_F12
,
KEY_GRAVE,KEY_GRAVE
KEY_1,KEY_1
KEY_2,KEY_2
KEY_3,KEY_3
KEY_4,KEY_4
KEY_5,KEY_5
KEY_6,KEY_6
KEY_7,KEY_7
KEY_8,KEY_8
KEY_9,KEY_9
KEY_0,KEY_0
KEY_MINUS,KEY_MINUS
KEY_EQUAL,KEY_EQUAL
,
KEY_Q,KEY_Q
KEY_W,KEY_W
KEY_E,KEY_E
KEY_R,KEY_R
KEY_T,KEY_T
KEY_Y,KEY_Y
KEY_U,KEY_U
KEY_I,KEY_I
KEY_O,KEY_O
KEY_P,KEY_P
KEY_A,KEY_A
KEY_S,KEY_S
KEY_D,KEY_D
KEY_F,KEY_F
KEY_G,KEY_G
KEY_H,KEY_H
KEY_J,KEY_J
KEY_K,KEY_K
KEY_L,KEY_L
KEY_Z,KEY_Z
KEY_X,KEY_X
KEY_C,KEY_C
KEY_V,KEY_V
KEY_B,KEY_B
KEY_N,KEY_N
KEY_M,KEY_M
,
KEY_LEFTBRACE,KEY_LEFTBRACE
KEY_RIGHTBRACE,KEY_RIGHTBRACE
KEY_BACKSLASH,KEY_BACKSLASH
KEY_SEMICOLON,KEY_SEMICOLON
KEY_APOSTROPHE,KEY_APOSTROPHE
KEY_COMMA,KEY_COMMA
KEY_DOT,KEY_DOT
KEY_SLASH,KEY_SLASH
,
KEY_KP0,KEY_KP0
KEY_KP1,KEY_KP1
KEY_KP2,KEY_KP2
KEY_KP3,KEY_KP3
KEY_KP4,KEY_KP4
KEY_KP5,KEY_KP5
KEY_KP6,KEY_KP6
KEY_KP7,KEY_KP7
KEY_KP8,KEY_KP8
KEY_KP9,KEY_KP9
KEY_KPASTERISK,KEY_KPASTERISK
KEY_KPMINUS,KEY_KPMINUS
KEY_KPPLUS,KEY_KPPLUS
KEY_KPDOT,KEY_KPDOT
KEY_KPSLASH,KEY_KPSLASH
···
接着在代码中,我们只需要打开该文件,转换为字典,删除空白项,即可制作好对应的字典。每次捕捉到按键后,利用字典翻译一下即可。
···
with open('./codemap.csv', 'r') as file:
reader = csv.reader(file)
codemap = {rows[0]:rows[1] for rows in reader}
del codemap[""]
···
3、人脸识别在Pi400上的实现。
实现人脸识别我们使用的工具是ultralytics。Ultralytics安装非常简单,只需要pip install ultralytics即可。唯一需要注意的是我们需要更换一下pytorch的版本,否则会出现Segmentation fault
···
pip uninstall torch torchvision
pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2
···
完成安装后,我们要使用stream的方法,从网络推流中获取到视频流。视频流来源是我们一开始制作的ESP32 S2 CAM开发板。开发板上烧录的是arduino ide上的官方CameraWebServer例程。除了常规的选择对应开发板并修改wifi信息外,我们还需要自定义一下开发板引脚。假设我们这里选择#define CAMERA_MODEL_ESP32S2_CAM_BOARD,那么我们要把camera_pins.h中的对应部分改成:
···
#elif defined(CAMERA_MODEL_ESP32S2_CAM_BOARD)
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 2
#define SIOD_GPIO_NUM 42
#define SIOC_GPIO_NUM 41
#define Y9_GPIO_NUM 1
#define Y8_GPIO_NUM 3
#define Y7_GPIO_NUM 4
#define Y6_GPIO_NUM 6
#define Y5_GPIO_NUM 8
#define Y4_GPIO_NUM 14
#define Y3_GPIO_NUM 9
#define Y2_GPIO_NUM 7
#define VSYNC_GPIO_NUM 16
#define HREF_GPIO_NUM 15
#define PCLK_GPIO_NUM 5
#define LED_GPIO_NUM 45
···
按照下图所示配置进行烧录即可。
Pi400这边的代码比较简单,ultralytics已经被设计的非常易于使用。
···
from ultralytics import YOLO
import requests
import time
url = "http://192.168.8.171"
model = YOLO("yolov8n.pt")
requests.get(url+"/control?var=framesize&val=" + str(8))
results = model.predict(url+":81/stream", stream=True, show=True, conf = 0.5)
for result in results:
for box in result.boxes:
class_id = result.names[box.cls[0].item()]
cords = box.xyxy[0].tolist()
cords = [round(x) for x in cords]
conf = round(box.conf[0].item(), 2)
print("Object type:", class_id)
print("Coordinates:", cords)
print("Probability:", conf)
print("---")
···
如果所安装的树莓派系统是桌面版本,我们在桌面版本上运行以上程序,就可以看到画面。如果是仅有terminal的系统,terminal中也会有相应信息打印。
最后我们只需要整合上述功能,就可以实现带有全自动老板键的智能键盘。
完整主程序代码如下:
···
import zero_hid
import evdev
import csv
import signal
import os
import threading
if os.geteuid() != 0:
raise ImportError('You must be root to use this library on linux.')
k = zero_hid.Keyboard()
# dev = evdev.InputDevice('/dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd')
dev = evdev.InputDevice('/dev/input/event0')
dev.grab() # grab 是为了独占,保证此设备不会被别的进程捕获
with open('./codemap.csv', 'r') as file:
reader = csv.reader(file)
codemap = {rows[0]:rows[1] for rows in reader}
del codemap[""]
curr_pressed = []
def key_input(key):
global curr_pressed
if isinstance(key, evdev.KeyEvent) and key.keystate != 2:
if key.keystate == 1:
curr_pressed.append(key.keycode)
if key.keystate == 0:
curr_pressed.remove(key.keycode)
print("\r" + "CODE: " + key.keycode + " ;STAT: " + str(key.keystate) + " "*40, end="")
keys = [codemap[i] for i in curr_pressed]
k.press(keys, additive=False, hold=True)
def handler(signal, frame):
k.release_all()
dev.ungrab()
dev.close()
exit()
signal.signal(signal.SIGTSTP, handler) # Ctrl+Z
signal.signal(signal.SIGINT, handler) # Ctrl+C
def thread1():
for event in dev.read_loop():
try:
key_input(evdev.categorize(event))
except Exception as error:
print(error)
t1 = threading.Thread(target=thread1, daemon=True)
t1.start()
from ultralytics import YOLO
import requests
url = "http://192.168.8.171"
model = YOLO("yolov8n.pt")
requests.get(url+"/control?var=framesize&val=" + str(8))
results = model.predict(url+":81/stream", stream=True, show=True, conf = 0.5)
delay = 1
count = 0
mode = 0
pre_mode = 0
for result in results:
try:
for box in result.boxes:
class_id = result.names[box.cls[0].item()]
cords = box.xyxy[0].tolist()
cords = [round(x) for x in cords]
conf = round(box.conf[0].item(), 2)
print("Object type:", class_id)
print("Coordinates:", cords)
print("Probability:", conf)
print("---")
if class_id == "person":
mode = 1
count = 0
if mode != pre_mode:
pre_mode = mode
k.press(["MOD_LEFT_ALT","KEY_TAB"], additive=False, hold=False)
print("triggered!!!")
count += 1
if count > delay:
mode = 0
pre_mode = mode
except Exception as error:
print(error)
···
作品源码和报告
https://download.eeworld.com.cn/detail/eew_dy9f48/630641
五、作品功能演示视频
https://training.eeworld.com.cn/video/38943
六、项目总结
【DigiKey“智造万物,快乐不停”创意大赛】1,ESP32S2摄像头开发板设计
https://bbs.eeworld.com.cn/thread-1264213-1-1.html
【DigiKey“智造万物,快乐不停”创意大赛】2,Pi400 HID 键盘功能的实现
https://bbs.eeworld.com.cn/thread-1268871-1-1.html
【DigiKey“智造万物,快乐不停”创意大赛】3,Pi400 键盘动作的捕捉与独占。
https://bbs.eeworld.com.cn/thread-1268872-1-1.html
【DigiKey“智造万物,快乐不停”创意大赛】4,人脸识别在Pi400上的实现。
https://bbs.eeworld.com.cn/thread-1268873-1-1.html
【DigiKey“智造万物,快乐不停”创意大赛】5,机器视觉打造带有全自动老板键的智能键盘
https://bbs.eeworld.com.cn/thread-1268874-1-1.html
-
回复了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】1,ESP32S2摄像头开发板设计
iysheng 发表于 2024-1-7 19:21
楼主,请问这个 ESP32 的驱动在 linux 有现成的么?你使用的是什么接口和控制板通信的呢?
就是一块S2开发板,用法一样
- 2024-01-05
-
加入了学习《利用机器视觉打造带有全自动老板键的智能键盘》,观看 利用机器视觉打造带有全自动老板键的智能键盘
-
发表了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】汇总:机器视觉打造全自动老板键智能键盘
本帖最后由 eew_dy9f48 于 2024-1-5 08:23 编辑
汇总一下每个步骤的实现帖子:
【DigiKey“智造万物,快乐不停”创意大赛】1,ESP32S2摄像头开发板设计
【DigiKey“智造万物,快乐不停”创意大赛】2,Pi400 HID 键盘功能的实现
【DigiKey“智造万物,快乐不停”创意大赛】3,Pi400 键盘动作的捕捉与独占
【DigiKey“智造万物,快乐不停”创意大赛】4,人脸识别在Pi400上的实现。
最后我们只需要整合上述功能,就可以实现带有全自动老板键的智能键盘。
完整主程序代码如下:
···
import zero_hid
import evdev
import csv
import signal
import os
import threading
if os.geteuid() != 0:
raise ImportError('You must be root to use this library on linux.')
k = zero_hid.Keyboard()
# dev = evdev.InputDevice('/dev/input/by-id/usb-_Raspberry_Pi_Internal_Keyboard-event-kbd')
dev = evdev.InputDevice('/dev/input/event0')
dev.grab() # grab 是为了独占,保证此设备不会被别的进程捕获
with open('./codemap.csv', 'r') as file:
reader = csv.reader(file)
codemap = {rows[0]:rows[1] for rows in reader}
del codemap[""]
curr_pressed = []
def key_input(key):
global curr_pressed
if isinstance(key, evdev.KeyEvent) and key.keystate != 2:
if key.keystate == 1:
curr_pressed.append(key.keycode)
if key.keystate == 0:
curr_pressed.remove(key.keycode)
print("\r" + "CODE: " + key.keycode + " ;STAT: " + str(key.keystate) + " "*40, end="")
keys = [codemap[i] for i in curr_pressed]
k.press(keys, additive=False, hold=True)
def handler(signal, frame):
k.release_all()
dev.ungrab()
dev.close()
exit()
signal.signal(signal.SIGTSTP, handler) # Ctrl+Z
signal.signal(signal.SIGINT, handler) # Ctrl+C
def thread1():
for event in dev.read_loop():
try:
key_input(evdev.categorize(event))
except Exception as error:
print(error)
t1 = threading.Thread(target=thread1, daemon=True)
t1.start()
from ultralytics import YOLO
import requests
url = "http://192.168.8.171"
model = YOLO("yolov8n.pt")
requests.get(url+"/control?var=framesize&val=" + str(8))
results = model.predict(url+":81/stream", stream=True, show=True, conf = 0.5)
delay = 1
count = 0
mode = 0
pre_mode = 0
for result in results:
try:
for box in result.boxes:
class_id = result.names[box.cls[0].item()]
cords = box.xyxy[0].tolist()
cords = [round(x) for x in cords]
conf = round(box.conf[0].item(), 2)
print("Object type:", class_id)
print("Coordinates:", cords)
print("Probability:", conf)
print("---")
if class_id == "person":
mode = 1
count = 0
if mode != pre_mode:
pre_mode = mode
k.press(["MOD_LEFT_ALT","KEY_TAB"], additive=False, hold=False)
print("triggered!!!")
count += 1
if count > delay:
mode = 0
pre_mode = mode
except Exception as error:
print(error)
···
项目源码:https://download.eeworld.com.cn/detail/eew_dy9f48/630641
项目文档:
-
发表了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】4,人脸识别在Pi400上的实现
本帖最后由 eew_dy9f48 于 2024-1-5 08:19 编辑
实现人脸识别我们使用的工具是ultralytics。Ultralytics安装非常简单,只需要pip install ultralytics即可。唯一需要注意的是我们需要更换一下pytorch的版本,否则会出现Segmentation fault
···
pip uninstall torch torchvision
pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2
···
完成安装后,我们要使用stream的方法,从网络推流中获取到视频流。视频流来源是我们一开始制作的ESP32 S2 CAM开发板。开发板上烧录的是arduino ide上的官方CameraWebServer例程。除了常规的选择对应开发板并修改wifi信息外,我们还需要自定义一下开发板引脚。假设我们这里选择#define CAMERA_MODEL_ESP32S2_CAM_BOARD,那么我们要把camera_pins.h中的对应部分改成:
···
#elif defined(CAMERA_MODEL_ESP32S2_CAM_BOARD)
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 2
#define SIOD_GPIO_NUM 42
#define SIOC_GPIO_NUM 41
#define Y9_GPIO_NUM 1
#define Y8_GPIO_NUM 3
#define Y7_GPIO_NUM 4
#define Y6_GPIO_NUM 6
#define Y5_GPIO_NUM 8
#define Y4_GPIO_NUM 14
#define Y3_GPIO_NUM 9
#define Y2_GPIO_NUM 7
#define VSYNC_GPIO_NUM 16
#define HREF_GPIO_NUM 15
#define PCLK_GPIO_NUM 5
#define LED_GPIO_NUM 45
···
按照下图所示配置进行烧录即可。
Pi400这边的代码比较简单,ultralytics已经被设计的非常易于使用。
···
from ultralytics import YOLO
import requests
import time
url = "http://192.168.8.171"
model = YOLO("yolov8n.pt")
requests.get(url+"/control?var=framesize&val=" + str(8))
results = model.predict(url+":81/stream", stream=True, show=True, conf = 0.5)
for result in results:
for box in result.boxes:
class_id = result.names[box.cls[0].item()]
cords = box.xyxy[0].tolist()
cords = [round(x) for x in cords]
conf = round(box.conf[0].item(), 2)
print("Object type:", class_id)
print("Coordinates:", cords)
print("Probability:", conf)
print("---")
···
如果所安装的树莓派系统是桌面版本,我们在桌面版本上运行以上程序,就可以看到画面。如果是仅有terminal的系统,terminal中也会有相应信息打印。
-
发表了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】3,Pi400 键盘动作的捕捉与独占
本帖最后由 eew_dy9f48 于 2024-1-5 08:19 编辑
一般在python中捕获键盘动作,大家使用的都是keyboard库,简单好用。但keyboard库有个致命的问题,就是无法独占键盘。这在我们当前的应用中是无法接受的。试想一下,当我们想发送ctrl+alt+del时,一旦按下,树莓派和电脑都进入了安全模式。你无法预期在键盘上的操作会在树莓派系统中整出什么幺蛾子。因此,我们需要在捕捉键盘动作的同时,对键盘资源进行独占,以此避免按键被其他的进程捕获。在这里我们使用evdev库来实现。
···
import os
import evdev
if os.geteuid() != 0:
raise ImportError('You must be root to use this library on linux.')
dev = evdev.InputDevice('/dev/input/event0')
dev.grab() # grab 是为了独占,保证此设备不会被别的进程捕获
for event in dev.read_loop():
key = evdev.categorize(event)
if isinstance(key, evdev.KeyEvent) and key.keystate != 2:
print(key.keycode)
···
按下按键,我们就可以看到对应的键值被打印在终端里。
接下来只需要把抓取到的键值组合成列表,发送到我们上一步实现的hid中即可。
细心的同学可能会意识到,evdev抓取到的的键值如果和hid的键值不匹配怎么办?这里我们就需要人工进行匹配,创建一个文件,将他们一一对应起来。
在项目文件夹下创建一个codemap.csv文件,写入以下对应:
···
KEY_LEFTCTRL,MOD_LEFT_CONTROL
KEY_RIGHTCTRL,MOD_RIGHT_CONTROL
KEY_LEFTALT,MOD_LEFT_ALT
KEY_RIGHTALT,MOD_RIGHT_ALT
KEY_LEFTSHIFT,MOD_LEFT_SHIFT
KEY_RIGHTSHIFT,MOD_RIGHT_SHIFT
,
KEY_LEFTMETA,MOD_LEFT_GUI
,
,
KEY_ESC,KEY_ESC
KEY_TAB,KEY_TAB
KEY_CAPSLOCK,KEY_CAPSLOCK
,
KEY_NUMLOCK,KEY_NUMLOCK
KEY_SYSRQ,KEY_SYSRQ
KEY_DELETE,KEY_DELETE
KEY_INSERT,KEY_INSERT
KEY_BACKSPACE,KEY_BACKSPACE
KEY_ENTER,KEY_ENTER
,
KEY_SPACE,KEY_SPACE
,
KEY_UP,KEY_UP
KEY_DOWN,KEY_DOWN
KEY_LEFT,KEY_LEFT
KEY_RIGHT,KEY_RIGHT
,
KEY_PAGEUP,KEY_PAGEUP
KEY_PAGEDOWN,KEY_PAGEDOWN
KEY_HOME,KEY_HOME
KEY_END,KEY_END
,
KEY_F1,KEY_F1
KEY_F2,KEY_F2
KEY_F3,KEY_F3
KEY_F4,KEY_F4
KEY_F5,KEY_F5
KEY_F6,KEY_F6
KEY_F7,KEY_F7
KEY_F8,KEY_F8
KEY_F9,KEY_F9
KEY_F10,KEY_F10
KEY_F11,KEY_F11
KEY_F12,KEY_F12
,
KEY_GRAVE,KEY_GRAVE
KEY_1,KEY_1
KEY_2,KEY_2
KEY_3,KEY_3
KEY_4,KEY_4
KEY_5,KEY_5
KEY_6,KEY_6
KEY_7,KEY_7
KEY_8,KEY_8
KEY_9,KEY_9
KEY_0,KEY_0
KEY_MINUS,KEY_MINUS
KEY_EQUAL,KEY_EQUAL
,
KEY_Q,KEY_Q
KEY_W,KEY_W
KEY_E,KEY_E
KEY_R,KEY_R
KEY_T,KEY_T
KEY_Y,KEY_Y
KEY_U,KEY_U
KEY_I,KEY_I
KEY_O,KEY_O
KEY_P,KEY_P
KEY_A,KEY_A
KEY_S,KEY_S
KEY_D,KEY_D
KEY_F,KEY_F
KEY_G,KEY_G
KEY_H,KEY_H
KEY_J,KEY_J
KEY_K,KEY_K
KEY_L,KEY_L
KEY_Z,KEY_Z
KEY_X,KEY_X
KEY_C,KEY_C
KEY_V,KEY_V
KEY_B,KEY_B
KEY_N,KEY_N
KEY_M,KEY_M
,
KEY_LEFTBRACE,KEY_LEFTBRACE
KEY_RIGHTBRACE,KEY_RIGHTBRACE
KEY_BACKSLASH,KEY_BACKSLASH
KEY_SEMICOLON,KEY_SEMICOLON
KEY_APOSTROPHE,KEY_APOSTROPHE
KEY_COMMA,KEY_COMMA
KEY_DOT,KEY_DOT
KEY_SLASH,KEY_SLASH
,
KEY_KP0,KEY_KP0
KEY_KP1,KEY_KP1
KEY_KP2,KEY_KP2
KEY_KP3,KEY_KP3
KEY_KP4,KEY_KP4
KEY_KP5,KEY_KP5
KEY_KP6,KEY_KP6
KEY_KP7,KEY_KP7
KEY_KP8,KEY_KP8
KEY_KP9,KEY_KP9
KEY_KPASTERISK,KEY_KPASTERISK
KEY_KPMINUS,KEY_KPMINUS
KEY_KPPLUS,KEY_KPPLUS
KEY_KPDOT,KEY_KPDOT
KEY_KPSLASH,KEY_KPSLASH
···
接着在代码中,我们只需要打开该文件,转换为字典,删除空白项,即可制作好对应的字典。每次捕捉到按键后,利用字典翻译一下即可。
···
with open('./codemap.csv', 'r') as file:
reader = csv.reader(file)
codemap = {rows[0]:rows[1] for rows in reader}
del codemap[""]
···
-
发表了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】2,Pi400 HID 键盘功能的实现
在github上有一个zero_hid的库,可以实现使用树莓派zero模拟hid键盘。但这个库有一些问题,直接使用在组合键上会出很多的问题,因此我参考这个项目,重写了一下这个库。
首先科普一下HID协议,HID键盘协议是一种基于报文的协议,通过在USB总线上进行通信。当用户按下键盘上的按键时,键盘将生成一个HID报文,并将其发送到计算机。计算机收到报文后,根据报文的内容来模拟相应的键盘操作,例如在文本编辑器中输入字符或执行特定的功能。
HID键盘报文包含多个字段,其中最重要的是按键码(Keycode)。按键码表示按下的键的唯一标识符,例如“A”键的按键码是0x04。除了按键码外,报文还可以包含其他信息,如修饰键(如Shift、Ctrl和Alt键)的状态和组合键的状态。
因此,在合成报文前,我们先要知道我们想输入的按键哪些是修饰键,而哪些是按键,他们要分开进行处理。
在进入代码部分前,我们需要先安装一下驱动。首先先新建一个文件,命名为isticktoit_usb,添加可执行权限,并填入以下内容:
···
#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p isticktoit
cd isticktoit
echo 0x1d6b > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2
mkdir -p strings/0x409
echo "fedcba9876543210" > strings/0x409/serialnumber
echo "Tobias Girstmair" > strings/0x409/manufacturer
echo "iSticktoit.net USB Device" > strings/0x409/product
mkdir -p configs/c.1/strings/0x409
echo "Config 1: ECM network" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower
# Add functions here
mkdir -p functions/hid.usb0
echo 1 > functions/hid.usb0/protocol
echo 1 > functions/hid.usb0/subclass
echo 8 > functions/hid.usb0/report_length
echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > functions/hid.usb0/report_desc
ln -s functions/hid.usb0 configs/c.1/
# End functions
ls /sys/class/udc > UDC
···
接着运行以下命令,完成驱动配置:
···
#!/bin/bash
echo "" | sudo tee -a /boot/config.txt
echo "# BEGIN HID Keyboard Simulation" | sudo tee -a /boot/config.txt
echo "dtoverlay=dwc2" | sudo tee -a /boot/config.txt
echo "# END HID Keyboard Simulation" | sudo tee -a /boot/config.txt
echo "" | sudo tee -a /etc/modules
echo "# BEGIN HID Keyboard Simulation" | sudo tee -a /etc/modules
echo "dwc2" | sudo tee -a /etc/modules
echo "libcomposite" | sudo tee -a /etc/modules
echo "# END HID Keyboard Simulation" | sudo tee -a /etc/modules
# Move to before exit 0
echo "" | sudo tee -a /etc/rc.local
echo "# BEGIN HID Keyboard Simulation" | sudo tee -a /etc/rc.local
echo "sudo ./isticktoit_usb" | sudo tee -a /etc/rc.local
echo "# END HID Keyboard Simulation" | sudo tee -a /etc/rc.local
···
完成后,以后每次重启完成,只需要运行一下isticktoit_usb即可。
处理报文部分的代码如下:
···
from typing import List
from .hid import hidwrite
from .hid.keycodes import KeyCodes
from time import sleep
import json
import pkgutil
import os
import pathlib
class Keyboard:
def __init__(self, dev='/dev/hidg0') -> None:
self.dev = dev
self.set_layout()
self.control_pressed = []
self.key_pressed = []
def list_layout(self):
keymaps_dir = pathlib.Path(__file__).parent.absolute() / 'keymaps'
keymaps = os.listdir(keymaps_dir)
files = [f for f in keymaps if f.endswith('.json')]
for count, fname in enumerate(files, 1):
with open(keymaps_dir / fname , encoding='UTF-8') as f:
content = json.load(f)
name, desc = content['Name'], content['Description']
print(f'{count}. {name}: {desc}')
def set_layout(self, language='US'):
self.layout = json.loads( pkgutil.get_data(__name__, f"keymaps/{language}.json").decode() )
def gen_list(self, keys = []):
_control_pressed = []
_key_pressed = []
for key in keys:
if key[:3] == "MOD":
_control_pressed.append(KeyCodes[key])
else:
_key_pressed.append(KeyCodes[key])
return _control_pressed, _key_pressed
def gen_buf(self):
self.buf = [sum(self.control_pressed),0] + self.key_pressed
self.buf += [0] * (8 - len(self.buf)) # fill to lenth 8
##########################################################################
# For user
def press(self, keys = [], additive=False, hold=False):
_control_pressed, _key_pressed = self.gen_list(keys)
if not additive:
self.control_pressed = []
self.key_pressed = []
self.control_pressed.extend(_control_pressed)
self.control_pressed = list(set(self.control_pressed)) # remove repeated items
self.key_pressed.extend(_key_pressed)
self.key_pressed = list(set(self.key_pressed))[:6] # remove repeated items and cut until 6 items
self.gen_buf()
hidwrite.write_to_hid_interface(self.dev, self.buf)
if not hold:
self.release(keys)
def release(self, keys = []):
_control_pressed, _key_pressed = self.gen_list(keys)
try:
self.control_pressed = list(set(self.control_pressed) - set(_control_pressed))
except:
pass
try:
self.key_pressed = list(set(self.key_pressed) - set(_key_pressed))
except:
pass
self.gen_buf()
hidwrite.write_to_hid_interface(self.dev, self.buf)
def release_all(self):
self.control_pressed = []
self.key_pressed = []
self.gen_buf()
hidwrite.write_to_hid_interface(self.dev, self.buf)
def text(self, string, delay=0):
for c in string:
key_map = self.layout['Mapping'][c]
key_map = key_map[0]
mods = key_map['Modifiers']
keys = key_map['Keys']
self.press(mods + keys)
sleep(delay)
···
上面这段代码把想要输出的按键分为control(修饰按键)和key(普通按键)两块,再组合形成报文列表。使用的逻辑是输入当前想要按下的按键状态,然后程序发送对应的报文。
测试一下:
···
import os
import zero_hid
if os.geteuid() != 0:
raise ImportError('You must be root to use this library on linux.')
k = zero_hid.Keyboard()
k.press(["KEY_H"], additive=False, hold=False)
k.press(["KEY_E"], additive=False, hold=False)
k.press(["KEY_L"], additive=False, hold=False)
k.press(["KEY_L"], additive=False, hold=False)
k.press(["KEY_O"], additive=False, hold=False)
···
press方法中填入的是一个list,表示当前按下的所有按键。具体的键值列表在zero_hid/keymaps/US.json中。
如果电脑成功打印,表示功能正常。
- 2024-01-04
-
上传了资料:
利用机器视觉打造带有全自动老板键的智能键盘