- 2024-10-13
-
发表了主题帖:
【2024 DigiKey创意大赛】+ 用于车速估算的空速表制作
本帖最后由 顺竿爬 于 2024-10-13 13:31 编辑
一、作品简介
2SMPP03是一款气压计,这款气压计在外接空速管的基础上,可以作为空速计来使用。一般此类应用都会被应用在航模上,而我想到这种用法其实在汽车工业上也可以大有作为。
汽车在隧道等GPS信号不佳的地方行驶时,对于里程和位置的估计,只能依靠转速表,陀螺仪,加速度计等传感器进行测量。这些传感器都有一个共同的问题,就是测不准,会受到其他因素干扰:比如里程表会受到胎压影响,惯性传感器会受到路面情况干扰等。因此需要多个传感器参与,使用不同的方法来测量,并将数据通过算法进行融合,以此减少误差。增加空速计,可以增加另一个观测速度及速度变化的途径,因此我们可以在极低成本的情况下,增加速度及位置估计的准确度。
整个项目的硬件部分展示:
二、系统框图
整个项目我使用购买的2SMPP03模组与自己设计的电路板制作出空速计模块,用ESP32-C6-DEVKITC-1-N8上的ADC外设读取空速计读数,并经过计算转换为车速,最后再通过串口输出至使用树莓派zero2w制作的车载电脑,完成车速的图形化显示。
三、各部分功能说明
首先是硬件制作。2SMPP03空速计由于是最基础的电阻网络结构,因此还需要不少模拟电路外设才可以正常工作。
外设电路设计一共需要三个运放。首先是仪表放大器,需要把2SMPP03输出的差分电压进行放大:
电阻R8的作用是用来调节放大倍数。
另外还需要一个电压跟随器,用来给仪表放大器提供参考电压。参考电压我设置为1/2*vcc,这样可以做到正负压双向测量。
最后一个运放是用来做一个恒流源,给2SMPP03提供恒流供电。这两个运放我使用一个双运放来完成。
在2SMPP03电源回路上也接一个变阻器,用来调整恒流值大小:
这样电路原理图部分设计就完成了,绘制成电路板如下:
下面开始写ESP32-C6-DEVKITC-1-N8上的代码。代码读取ADC数据,进行计算后通过串口发送出去。这里主要需要注意的是计算方法。
首先根据2SMPP03规格书,可以找到电阻与气压的关系:
通过调整横流电阻,把电流调整到100uA,就可以得到上面的线性关系。
接着再通过调整仪表放大器的放大倍率,来使得输出电压匹配ESP32-C6-DEVKITC-1-N8的ADC量程范围。
最后我们找到风压与风速的关系:
wp = 0.5*r*(v^2)*1000/g
其中wp是风压,r是空气重度,g是重力加速度,v是风速。调整一下公式,就可以得到风速的计算公式:
v = (wp / 1000 / (0.5 * r / g))^0.5
使用这个公式,就可以完成代码编写:
void setup() {
Serial.begin(115200);
}
void loop() {
unsigned int sensorValue = analogRead(4);
if (sensorValue < 920) {
sensorValue = 920;
} else if (sensorValue > 1500) {
sensorValue = 1500;
}
unsigned int pressure = map(sensorValue, 920, 1500, 0, 390);
float volecity = sqrt((float)pressure / 1000 / (0.5 * 0.01225 / 9.8));
// print the results to the Serial Monitor:
// Serial.print("sensor = ");
Serial.println(volecity);
// wait 2 milliseconds before the next loop for the analog-to-digital
// converter to settle after the last reading:
delay(100);
}
按照设计焊接好电路板:
我使用的是4号引脚,属于ADC1外设。因此接线时注意把信号线接到4号脚上
最后是树莓派上位机的图形化显示源码。使用python自带的库就可以完成全部设计。
import tkinter as tk
from tkinter import Canvas
import serial
import math
root = tk.Tk()
root.title("SPEED")
ser = serial.Serial("/dev/ttyUSB0", 115200, timeout=1)
canvas = Canvas(root, width=400, height=400)
canvas.pack()
canvas.create_arc(
50, 50, 350, 350, start=225, extent=-270, outline="lightgrey", width=10
)
for i in range(0, 26, 5):
angle = 225 - (i / 25) * 270
x = 200 + 140 * math.cos(math.radians(angle))
y = 200 - 140 * math.sin(math.radians(angle))
canvas.create_text(x, y, text=str(i), font=("Arial", 10))
needle_length = 100
needle = canvas.create_line(200, 200, 200, 100, fill="red", width=3)
def update_gauge(speed):
angle = 225 - (speed / 25) * 270
x = 200 + needle_length * math.cos(math.radians(angle))
y = 200 - needle_length * math.sin(math.radians(angle))
canvas.coords(needle, 200, 200, x, y)
def read_serial():
try:
line = ser.readline().decode("utf-8").strip()
if line:
speed = float(line)
if 0 <= speed <= 25:
update_gauge(speed)
except Exception as e:
print(f"Error reading serial: {e}")
root.after(50, read_serial)
read_serial()
root.mainloop()
将C6和树莓派的TX/RX交叉连接,并连接地线后,在树莓派上运行程序,最终显示效果如下:
四、作品源码
PCB设计文件:
ESP32-C6-DEVKITC-1-N8代码:
树莓派zero2W代码:
五、作品功能演示视频
[localvideo]d61646c707991c0dc81986c572f8ac1a[/localvideo]
六、项目总结
项目的所有代码及设计文件均已在上面上传,所有的细节都写在本文中,下面附上word版的项目报告。
- 2024-09-04
-
加入了学习《【Follow me第二季第1期】使用Makecode图形化完成任务》,观看 【Follow me第二季第1期】使用Makecode图形化完成任务
- 2024-08-20
-
发表了主题帖:
【2024 DigiKey 创意大赛】ESP32-C6-DEVKITC-1-N8和2SMPP03 开箱贴
由于物流耽误,这次的快递过了非常久才到货。到货后拆开,包装还是一如既往的好,其中C3开发板还贴了防拆贴纸。
旁边小袋子装的是2SMPP03气压传感器,这是个非常精密且脆弱的传感器,原理差不多是在一张非常薄以至于有弹性的硅片上装了四个非常薄的压敏电阻,当膜两侧有气压差发生形变时,就会牵引电阻发生形变,从而使阻值发生轻微改变。从原理上看要想使用,免不了各种运放外围电路的设计,又到了头疼的模拟电路环节。
把所有的东西拆出来,可以看到传感器全貌,以及精致的乐鑫原厂开发板。
- 2024-08-07
-
上传了资料:
【Follow me第二季第1期】使用Makecode图形化完成任务
-
发表了主题帖:
【Follow me第二季第1期】使用Makecode图形化完成任务
本帖最后由 顺竿爬 于 2024-9-4 01:35 编辑
项目介绍
先看一下视频:
本次项目使用到了Adafruit Circuit Playground Express和树莓派zero2w。
由于使用的是图形化编程,每个任务的软件流程图即为图形化程序本身,因此在这不再单独列出。
入门任务:开发环境搭建,板载LED点亮
这个任务我们仅需使用到Adafruit Circuit Playground Express就可以完成。Makecode开发环境是部署在云端的,因此无需搭建可以直接使用。
这次的项目我计划使用树莓派zero作为上位机使用,先下载官方镜像烧录器,打开后,选择烧录设备,操作系统,以及准备安装树莓派系统的储存卡,点击下一步烧录即可。
烧录完成后,把储存卡插进树莓派中,安装散热器,再接上显示器,并通过otg线接上我们的Adafruit Circuit Playground Express,就算大功告成,直接上电即可。注意电源最好使用充电器,这样确保供电能力充足。
编写代码是在网页端进行的,因此我们需要使用浏览器。但是需要注意的是,在linux系统下每个账户和应用都有严格的权限管理,而由于我们需要在浏览器中连接Adafruit Circuit Playground Express,但这个硬件级别的操作需要用到root权限,因此我们不能直接点击浏览器图表启动,而应该使用终端用sudo方式启动。
打开终端后,输入以下命令启动root权限的浏览器:
sudo chromium-browser -no-sandbox
只需要打开官方网站Adafruit Circuit Playground Express - Blocks / Javascript editor,然后点击New Project新建一个项目:
就可以进入编程页面,我们先什么都不做,直接点击左下角的下载,下载一个基础固件,然后按一下板载的reset按钮,进入刷机模式,将下载好的固件拖进去就完成了基础固件的刷写。
有了基础固件后,点击右上角的齿轮,配对设备,成功后就可以开始创作。
点灯我们可以通过LIGHT选项中的第一个选择得到,将它拖动到on start中,并下载固件,就可以在程序运行把所有LED设置成红色。
点击页面上的JAVASCRIPT,可以看到刚刚图形化的程序原始脚本的样子。我们可以把它复制出来保存一下。这样以后再需要这个程序时,直接复制这个脚本放入JAVASCRIPT就可以。所有任务的脚本我会在文章末尾作为源代码添加进附件中供大家下载使用。
基础任务一:控制板载炫彩LED,跑马灯点亮和颜色变换
这个任务因为我们希望跑马灯可以一直持续,因此需要把on start换成forever,然后选择LIGHT下的show animation拖进去就可以了。
基础任务二(必做):监测环境温度和光线,通过板载LED展示舒适程度
利用板载的光线和温度传感器,同时点亮两个指示灯。先通过点击左边的show console device来查看串口数据。据此来设置合适的温度强度阈值,然后完成程序,当温度适宜,光强不刺眼时两个指示灯都显示为绿,如果哪个传感器条件不再适宜,则对应指示灯变为红色。
可以看到,我用手指来加热温度传感器,若有强光出现或温度上升,对应的舒适度指示灯会变成红色。
基础任务三:接近检测——设定安全距离并通过板载LED展示,检测到入侵时,发起声音报警
板子上并没有测距模块可以试用。但是我们看到板上既有LED,又有光纤传感器。如果让LED全亮,那么在光线不强的环境里,如果板子面前有物体,这个物体越亮,代表反射的光越多,也就代表离板子越近。通过这个思路,我们可以利用光线传感器来测量距离。
我们在这个程序中同样使用的控制台输出的功能,因此可以通过点击左边的show console device来查看串口数据。据此来设置合适的光线强度阈值。
进阶任务:制作不倒翁——展示不倒翁运动过程中的不同灯光效果
可以通过加速度计的Z轴读数,来表示Adafruit Circuit Playground Express的倾斜程度。当倾斜程度过大时,让红色报警指示灯亮起。同样的倾斜阈值由串口数据得到。
可以看到板子平放时没有灯亮起,而当板子倾斜时则指示灯亮起。
创意任务一:有创意的可穿戴装饰——可结合多种传感器和灯光效果展示
我用Adafruit Circuit Playground Express做了个创意小挂坠,正常情况下,它会显示各种各样的光效,如果点击两个按钮,会发出相应的声音。同时,板载还有一个开关,推动开关,可以把它变成一个手电筒使用。
心得体会
这次活动选的板子非常适合新手入门,官方准备了非常直观的工具,可以让0基础的同学也立马体验到电子开发的乐趣。同时,板子上也集成了足够多的外设,可玩性非常高。
每个任务的脚本代码: https://download.eeworld.com.cn/detail/%E9%A1%BA%E7%AB%BF%E7%88%AC/633948
- 2024-05-17
-
发表了主题帖:
【2023 DigiKey大赛参与奖】开箱帖 树莓派5
感谢得捷,感谢EEWORLD。300报销奖金,看了一圈还是树莓派5最香,自己补一点直接上。
- 2024-03-03
-
加入了学习《【得捷电子Follow me第4期】全部任务合集》,观看 【得捷电子Follow me第4期】全部任务合集
- 2024-02-21
-
加入了学习《【得捷Follow me第4期】项目任务提交》,观看 视频
- 2024-02-12
-
回复了主题帖:
【得捷电子Follow me第4期】全部任务合集
Jacktang 发表于 2024-2-7 09:09
传大图片会报错是因为什么
好像是官方w5500的mpy驱动问题,测试了一下如果单纯使用socket收发大量数据的话一样也会莫名出错,出现os error。在esp上则无相关问题,说明问题与mpy的socket方法无关,那大概率就是驱动问题了。
- 2024-02-06
-
回复了主题帖:
【得捷电子Follow me第4期】基础任务一(补充):ping通互联网站点
电子烂人 发表于 2024-2-4 10:37
这个我也不太清楚,请您看下微雪屏幕的源代码,我的代码都是直接调用的其中的函数
该屏幕的资源:Pico ...
看明白了,text方法是写在父类framebuf.FrameBuffer中的
-
上传了资料:
【得捷电子Follow me第4期】全部任务合集
- 2024-02-05
-
发表了主题帖:
【得捷电子Follow me第4期】全部任务合集
这次活动满满当当买了一堆东西,本来看到W5500-PICO管脚与原版树莓派pico一致,欢天喜地的配了一个pico专用的墨水屏,想着做显示器使用。但拿来才发现尴尬了,墨水屏和板载的W5500都使用的是pico默认的spi0,而且一些其他的控制管脚也都是重叠的。这下失算了,靠软件是没法解决问题了,只能用最原始的方法——面包板来解决问题。
入门任务:开发环境搭建,BLINK,驱动液晶显示器进行显示。
这个项目我使用vscode+platformio进行开发。首先要先在vscode中安装platformio插件
安装完成后,进入插件首页,创建新项目,板子选择raspberry pico就可以。
接着下载所需的库文件,这里我都整理好了,大家只需要在项目文件夹下的platformio.ini文件写入以下内容并保存,platform就会自动下载所需要的这些包。
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:pico]
platform = raspberrypi
board = pico
framework = arduino
lib_deps =
zinggjm/GxEPD2@^1.5.5
https://github.com/masterx1981/Ethernet.git
adafruit/Adafruit NeoPixel@^1.12.0
adafruit/Adafruit INA219@^1.2.3
特别要注意的是,我们Ethernet用的并不是官方的库,而是第三方库。主要的原因是官方的库中缺少了使用icmp来ping的方法。这个具体到后面再说。我们先把灯点起来。
这里我点的灯是一颗外接的W2812B,接在28号引脚上。驱动代码非常简单,使用adafruit的库就可以轻松实现:
// Neopixel
#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
Adafruit_NeoPixel pixels(1, 28, NEO_GRB + NEO_KHZ800);
void setup(){
pixels.begin();
pixels.clear();
pixels.show();}
void loop(){
pixels.setPixelColor(0, pixels.Color(0, 100, 0);
pixels.show();
delay(1000);
pixels.setPixelColor(0, pixels.Color(0, 0, 0);
pixels.show();
delay(1000);}
这样就可以实现简单的绿灯闪烁。
点亮这块屏幕稍微麻烦一点,这块屏是一块2.9寸的单色墨水屏,由于我们把引脚改到了SPI1上,因此所有现成的例程都是无法使用的。这里我们要先在代码中定义好引脚,实际的接线方式大家可以参考这个代码:
arduino::MbedSPI SPIn(/*MISO=RX*/ 12, /*MOSI=TX*/ 15, /*SCK*/ 14);
GxEPD2_BW<GxEPD2_290_T5, GxEPD2_290_T5::HEIGHT> display(GxEPD2_290_T5(/*CS*/ 13, /*DC*/ 11, /*RST*/ 10, /*BUSY*/ 9));
切记,一定要在platformio中使用上述代码,因为arduino中pico的spi方式使用的并不上Mbed,因此上述代码并不会起作用。
接好线后,使用以下代码,我们就可打印信息在屏幕上:
#include <Arduino.h>
#include <GxEPD2_BW.h>
#include <Fonts/FreeMonoBold9pt7b.h>
arduino::MbedSPI SPIn(/*MISO=RX*/ 12, /*MOSI=TX*/ 15, /*SCK*/ 14);
GxEPD2_BW<GxEPD2_290_T5, GxEPD2_290_T5::HEIGHT> display(GxEPD2_290_T5(/*CS*/ 13, /*DC*/ 11, /*RST*/ 10, /*BUSY*/ 9));
void text(const char *string)
{
display.setRotation(1);
display.setFont(&FreeMonoBold9pt7b);
display.setTextColor(GxEPD_BLACK);
int16_t tbx, tby;
uint16_t tbw, tbh;
display.getTextBounds(string, 0, 0, &tbx, &tby, &tbw, &tbh);
// center the bounding box by transposition of the origin:
uint16_t x = ((display.width() - tbw) / 2) - tbx;
uint16_t y = ((display.height() - tbh) / 2) - tby;
display.setFullWindow();
display.firstPage();
do
{
display.fillScreen(GxEPD_WHITE);
display.setCursor(x, y);
display.print(string);
} while (display.nextPage());
}
void setup(){
display.epd2.selectSPI(SPIn, SPISettings(4000000, MSBFIRST, SPI_MODE0));
display.init(115200); // default 10ms reset pulse, e.g. for bare panels with DESPI-C02
text("Hello EEWorld!")
}
void loop(){;}
更有意思的是,墨水屏显示内容并不需要通电保持,因此即使我们把屏幕拔下来,内容也还正常的显示在屏幕上。
基础任务一:完成主控板W5500初始化(静态IP配置),并能使用局域网电脑ping通,同时W5500可以ping通互联网站点;通过抓包软件(Wireshark、Sniffer等)抓取本地PC的ping报文,展示并分析。
网络配置这块比较简单,我们直接把他封装成一个函数,放到setup中即可。
#include <Arduino.h>
#include <Ethernet.h>
// 网络配置
byte mac[] = {0xDE, 0xAD, 0xBE, 0xE1, 0xF2, 0xE3};
// 仅静态配置有效
IPAddress ip(192, 168, 50, 173);
IPAddress gateway(192, 168, 50, 1);
IPAddress myDns(8, 8, 8, 8);
IPAddress subnet(255, 255, 255, 0);
IPAddress setNet()
{
Serial.println("Ethernet Begin");
// Ethernet.init(17); // Uncomment if use custom cs pin (default is 17)
// DHCP
// Ethernet.begin(mac);
// 静态IP设置
Ethernet.begin(mac, ip, myDns, gateway, subnet);
// 打印INFO
Serial.print("My IP address: ");
Serial.println(Ethernet.localIP());
Serial.print("My subnet: ");
Serial.println(Ethernet.subnetMask());
Serial.print("My DNS address: ");
Serial.println(Ethernet.dnsServerIP());
Serial.print("My GateWay address: ");
Serial.println(Ethernet.gatewayIP());
Serial.print("My Mac address: ");
byte macBuffer[6];
Ethernet.MACAddress(macBuffer);
for (byte octet = 0; octet < 6; octet++)
{
Serial.print(macBuffer[octet], HEX);
if (octet < 5)
{
Serial.print('-');
}
}
Serial.println("");
return Ethernet.localIP();
}
void setup()
{
// 配置串口
Serial.begin(115200);
Serial.println("");
setNet();
}
void loop(){;}
开发板其中mac码只需要符合一般规则,其余的可以任意填写。静态的ip和网关需要根据大家自己的路由器情况自行填写。当然如果不光是为了完成任务的话,更常见的方式是DHCP。我在手动配置的上面已经写了dhcp的方法,只需要将手动配置代码注释,dhcp解除注释即可。
接着我们打开Wiresharp,选择自己电脑当前连接路由器的网卡,根据手动填写的ip地址,在筛选框填入以下内容:
ip.src==192.168.50.173 or ip.dst==192.168.50.173
按右边箭头应用筛选后,我们应该会看到没有东西刷新了。这时候打开电脑的cmd,ping一下该地址,我们就可以看到抓取到了对应的包:
双击任意一条,打开后可以清晰看到是基于icmp的ping reply
接下来是用板子来ping互联网站点。这其实是由两个步骤完成的,第一步是DNS解析站点,将域名转化为ip,第二部是ping。实现ping需要导入EthernetICMP.h,但不知道为什么似乎这个库和SPI,I2C等外设库冲突,只要同时存在,就会导致板子不停重置,代码无法运行。因此我把ping这块都写在了一起,封装成了函数,不需要用的时候全部注释掉即可。
// Ping
#include <Dns.h>
DNSClient dnClient;
#include <EthernetICMP.h> // 与i2c冲突
SOCKET pingSocket = 4;
EthernetICMPPing ping(pingSocket, (uint8_t)random(0, 255));
void pingTest(const IPAddress &dstip)
{
EthernetICMPEchoReply echoReply = ping(dstip, 4);
char buffer[256];
if (echoReply.status == SUCCESS)
{
sprintf(buffer,
"Reply[%d] from: %d.%d.%d.%d: bytes=%d time=%ldms TTL=%d",
echoReply.data.seq,
echoReply.addr[0],
echoReply.addr[1],
echoReply.addr[2],
echoReply.addr[3],
REQ_DATASIZE,
millis() - echoReply.data.time,
echoReply.ttl);
}
else
{
sprintf(buffer, "Echo request failed; %d", echoReply.status);
}
Serial.println(buffer);
}
void pingloop()
{
dnClient.begin(Ethernet.dnsServerIP());
const char domains[3][20] = {"www.eeworld.com.cn", "www.digikey.cn", "www.digikey.com"};
IPAddress dstip;
for (int i = 0; i < 3; i++)
{
if (dnClient.getHostByName(domains[i], dstip) == 1)
{
Serial.print(domains[i]);
Serial.print(" = ");
Serial.println(dstip);
pingTest(dstip);
}
else
Serial.println(F("dns lookup failed"));
}
}
在pingloop中我解析了三个网址,并分别对三个网站进行了一次ping。将pingloop放到loop中,每隔几秒运行一次,就可以看到ping的结果:
基础任务二:主控板建立TCPIP或UDP服务器,局域网PC使用TCPIP或UDP客户端进行连接并发送数据,主控板接收到数据后,送液晶屏显示(没有则通过串口打印显示);通过抓包软件抓取交互报文,展示并分析。
由于http服务是基于tcp的,因此这个任务我打算直接在pico上搭建一个http服务器,然后用电脑从网页端访问,相当于电脑作为客户端发送数据,以此来接收传感器读数,并控制led。
http协议如果将他简化,就只需要一个头文件的http报文就足够了。这里我们用最基础的tcp发送报文方式实现。
EthernetServer server(80);
void webs()
{
EthernetClient client = server.available();
if (client)
{
Serial.println("new client");
String HTTP_req;
// an http request ends with a blank line
bool currentLineIsBlank = true;
while (client.connected())
{
if (client.available())
{
char c = client.read();
HTTP_req += String(c);
// if you've gotten to the end of the line (received a newline
// character) and the line is blank, the http request has ended,
// so you can send a reply
if (c == '\n' && currentLineIsBlank)
{
// send a standard http response header
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println();
client.println("<!DOCTYPE HTML>");
client.println("<html>");
get();
client.println("Bus Voltage:");
client.println(busvoltage);
client.println("V<br />");
client.println("Shunt Voltage:");
client.println(shuntvoltage);
client.println("V<br />");
client.println("Load Voltage:");
client.println(loadvoltage);
client.println("V<br />");
client.println("Current:");
client.println(current_mA);
client.println("mA<br />");
client.println("Power:");
client.println(power_mW);
client.println("mW<br />");
client.println("<a href='?R=on'><button>Red ON</button></a> <a href='?R=off'><button>Red OFF</button></a><br /><br />");
client.println("<a href='?G=on'><button>Green ON</button></a> <a href='?G=off'><button>Green OFF</button></a><br /><br />");
client.println("<a href='?B=on'><button>Blue ON</button></a> <a href='?B=off'><button>Blue OFF</button></a><br /><br />");
client.println("<a href='?ntp'><button>NTP time update</button></a><br /><br />");
client.println("</html>");
ProcessRequest(HTTP_req);
break;
}
if (c == '\n')
{
// you're starting a new line
currentLineIsBlank = true;
}
else if (c != '\r')
{
// you've gotten a character on the current line
currentLineIsBlank = false;
}
}
}
}
}
页面内容可以用简单的html来实现。
接着我们再接入传感器,传感器我使用的是INA219,可以用来检测开发板供电电压,用来在电压不足时报警。这个传感器依靠的是I2C通信。
// INA 219
#include <Adafruit_INA219.h>
float shuntvoltage = 0;
float busvoltage = 0;
float current_mA = 0;
float loadvoltage = 0;
float power_mW = 0;
TwoWire Wire1(2, 3);
Adafruit_INA219 ina219;
void get()
{
busvoltage = ina219.getBusVoltage_V();
shuntvoltage = ina219.getShuntVoltage_mV();
loadvoltage = busvoltage + (shuntvoltage / 1000);
current_mA = ina219.getCurrent_mA();
power_mW = ina219.getPower_mW();
Serial.print("Bus Voltage: ");
Serial.print(busvoltage);
Serial.println(" V");
Serial.print("Shunt Voltage: ");
Serial.print(shuntvoltage);
Serial.println(" mV");
Serial.print("Load Voltage: ");
Serial.print(loadvoltage);
Serial.println(" V");
Serial.print("Current: ");
Serial.print(current_mA);
Serial.println(" mA");
Serial.print("Power: ");
Serial.print(power_mW);
Serial.println(" mW");
Serial.println("");
}
接着我们把之前导入的WS2812和传感器数据,加上接下来会细讲的ntp用html写入网页中,打开网页就可以看到如下界面,按动按钮就可以进行控制。
进阶任务:从NTP服务器(注意数据交互格式的解析)同步时间,获取时间送显示屏(串口)显示。
NTP基于的是UDP协议进行的,这里我们先定义了基本的获取NTP时间的函数,获取完成后,再将获取到的信息转换成字符串,更新到墨水屏上,实现在网页端按一下按钮,就可以在墨水屏上刷新当前时间的效果。
// A UDP instance to let us send and receive packets over UDP
#include <EthernetUdp.h>
EthernetUDP Udp;
const uint8_t NTP_PACKET_SIZE = 48;
byte packetBuffer[NTP_PACKET_SIZE];
char bjtime[256];
void sendNTPpacket(const char *address)
{
// set all bytes in the buffer to 0
memset(packetBuffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form NTP request
// (see URL above for details on the packets)
packetBuffer[0] = 0b11100011; // LI, Version, Mode
packetBuffer[1] = 0; // Stratum, or type of clock
packetBuffer[2] = 6; // Polling Interval
packetBuffer[3] = 0xEC; // Peer Clock Precision
// 8 bytes of zero for Root Delay & Root Dispersion
packetBuffer[12] = 49;
packetBuffer[13] = 0x4E;
packetBuffer[14] = 49;
packetBuffer[15] = 52;
// all NTP fields have been given values, now
// you can send a packet requesting a timestamp:
Udp.beginPacket(address, 123); // NTP requests are to port 123
Udp.write(packetBuffer, NTP_PACKET_SIZE);
Udp.endPacket();
}
void getntp()
{
while (true)
{
sendNTPpacket("time.nist.gov"); // send an NTP packet to a time server
// wait to see if a reply is available
delay(1000);
if (Udp.parsePacket())
{
// We've received a packet, read the data from it
Udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
// the timestamp starts at byte 40 of the received packet and is four bytes,
// or two words, long. First, extract the two words:
unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
// combine the four bytes (two words) into a long integer
// this is NTP time (seconds since Jan 1 1900):
unsigned long secsSince1900 = highWord << 16 | lowWord;
Serial.print("Seconds since Jan 1 1900 = ");
Serial.println(secsSince1900);
// now convert NTP time into everyday time:
Serial.print("Unix time = ");
// Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
const unsigned long seventyYears = 2208988800UL;
// subtract seventy years:
unsigned long epoch = secsSince1900 - seventyYears;
// print Unix time:
Serial.println(epoch);
// print the hour, minute and second:
sprintf(bjtime, "Current Time %d : %d : %d",
(epoch % 86400L) / 3600 + 8,
(epoch % 3600) / 60,
epoch % 60);
Serial.println(bjtime);
return;
}
// wait 3 seconds before asking for the time again
Serial.println("Retry in 3 seconds...");
delay(3000);
}
}
这时候我们再用Wireshark抓包看一下,就可以看到tcp数据包和pico返回的http ok的报文。
终极任务二:使用外部存储器,组建简易FTP文件服务器,并能正常上传下载文件。
这个任务我原本想一并在platformio中完成,但无奈arduino中文件系统一直无法在rp2040上成功编译,且还需要外接SD模块。因此这块我是用micropython来完成。micropython有一些ftp的实现方法,但是是给esp9266用的,在开头需要导入wifi库。但幸运的是,代码实现全部依靠纯python使用socket发送ftp命令,wifi库仅用来获取ip。这下就简单了,我们只需要改一下ip的获取方式,就可以直接使用了。完整代码如下:
import socket
import network
import uos
import gc
from time import localtime
from machine import Pin,SPI
import time
def w5x00_init():
#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)#network active
nic.ifconfig(('192.168.50.213','255.255.255.0','192.168.50.1','8.8.8.8'))#Set static network address information
while not nic.isconnected():
time.sleep(1)
print(nic.regs())#Print register information
#Print network address information
print("IP Address:",nic.ifconfig()[0])
print("Subnet Mask:",nic.ifconfig()[1])
print("Gateway:",nic.ifconfig()[2])
print("DNS:",nic.ifconfig()[3])
return nic
month_name = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
def send_list_data(path, dataclient, full):
try: # whether path is a directory name
for fname in uos.listdir(path):
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 uos.listdir(path):
if fncmp(fname, pattern) == True:
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]
tm = localtime(stat[7])
if tm[0] != localtime()[0]:
description = "{} 1 owner group {:>10} {} {:2} {:>5} {}\r\n".format(
file_permissions, file_size, month_name[tm[1]], tm[2], tm[0], fname)
else:
description = "{} 1 owner group {:>10} {} {:2} {:02}:{:02} {}\r\n".format(
file_permissions, file_size, month_name[tm[1]], tm[2], tm[3], tm[4], fname)
else:
description = fname + "\r\n"
return description
def send_file_data(path, dataclient):
with open(path, "r") as file:
chunk = file.read(512)
while len(chunk) > 0:
dataclient.sendall(chunk)
chunk = file.read(512)
def save_file_data(path, dataclient, mode):
with open(path, mode) as file:
chunk = dataclient.read(512)
while len(chunk) > 0:
file.write(chunk)
chunk = dataclient.read(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:]) == True:
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():
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", 21)[0][4])
datasocket.bind(socket.getaddrinfo("0.0.0.0", DATA_PORT)[0][4])
ftpsocket.listen(1)
datasocket.listen(1)
datasocket.settimeout(10)
msg_250_OK = '250 OK\r\n'
msg_550_fail = '550 Failed\r\n'
try:
dataclient = None
fromname = None
while True:
cl, remote_addr = ftpsocket.accept()
cl.settimeout(300)
cwd = '/'
try:
# print("FTP connection from:", remote_addr)
cl.sendall("220 Hello, this is the ESP8266.\r\n")
while True:
gc.collect()
data = cl.readline().decode("utf-8").rstrip("\r\n")
if len(data) <= 0:
print("Client disappeared")
break
command = data.split(" ")[0].upper()
payload = data[len(command):].lstrip()
path = get_absolute_path(cwd, payload)
print("Command={}, Payload={}, Path={}".format(command, payload, path))
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":
cl.sendall('257 "{}"\r\n'.format(cwd))
elif command == "CWD":
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')
break
elif command == "PASV":
addr = nic.ifconfig()[0]
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)
elif command == "LIST" or command == "NLST":
if not payload.startswith("-"):
place = path
else:
place = cwd
try:
send_list_data(place, dataclient, command == "LIST" or payload == "-l")
cl.sendall("150 Here comes the directory listing.\r\n")
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:
send_file_data(path, dataclient)
cl.sendall("150 Opening data connection.\r\n")
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, "w")
cl.sendall("226 Transfer complete.\r\n")
except:
cl.sendall(msg_550_fail)
if dataclient is not None:
dataclient.close()
dataclient = None
elif command == "APPE":
try:
cl.sendall("150 Ok to send data.\r\n")
save_file_data(path, dataclient, "a")
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":
try:
uos.rmdir(path)
cl.sendall(msg_250_OK)
except:
cl.sendall(msg_550_fail)
elif command == "MKD":
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
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
finally:
datasocket.close()
ftpsocket.close()
if dataclient is not None:
dataclient.close()
nic = w5x00_init()
ftpserver()
特别注意,目前在测试传送py代码文件时一切正常,如果传大图片会报错。
源代码 https://download.eeworld.com.cn/detail/%E9%A1%BA%E7%AB%BF%E7%88%AC/6311
活动心得
借着fm4活动的机会,我尝试了之前从没有使用过的墨水屏。说实话墨水屏的刷新之慢确实有点出乎意料,但驱动的过程还是挺有趣的。得利于开源社区各种各样的现成的库,才能在这么短的时间内完成任务。
- 2024-01-02
-
回复了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】6,基于爬虫的ChatGPT API调试
吾妻思萌 发表于 2023-12-21 16:59
你买了gpt3.5的接口吗?
没有,这是基于爬虫的
-
回复了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】3,语音识别模型调试
chejm 发表于 2023-12-27 23:16
首先感谢楼主的无私分享,想了解下这个语音识别模型的运行占用的资源大吗?谢谢
还可以,树莓派可以完美运行,感觉不到明显的吃力
- 2023-12-21
-
发表了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】项目汇总提交+演示视频+word报告
钢铁侠的智能助手贾维斯
作者:顺竿爬
这个项目虽然成功实现了全部功能,但由于树莓派的平台限制,有些地方需要对库中的源代码进行调整,而有些库的安装需要通过一些特别的方式。因此如果想复现项目,请务必仔细阅读项目报告中的每个功能模块的实现方法。
论坛链接如下:
【DigiKey“智造万物,快乐不停”创意大赛】开箱 基于ESP32S3和机器视觉打造贾维斯:https://bbs.eeworld.com.cn/thread-1262757-1-1.html
【DigiKey“智造万物,快乐不停”创意大赛】1,周边硬件制作:https://bbs.eeworld.com.cn/thread-1264210-1-1.html
【DigiKey“智造万物,快乐不停”创意大赛】2,I2S HAT驱动,并用python控制:https://bbs.eeworld.com.cn/thread-1267600-1-1.html
【DigiKey“智造万物,快乐不停”创意大赛】3,语音识别模型调试:https://bbs.eeworld.com.cn/thread-1267601-1-1.html
【DigiKey“智造万物,快乐不停”创意大赛】4,OpenCV获取webcam视频流:https://bbs.eeworld.com.cn/thread-1267601-1-1.html
【DigiKey“智造万物,快乐不停”创意大赛】5,在树莓派上搭建YOLOv8:https://bbs.eeworld.com.cn/thread-1267603-1-1.html
【DigiKey“智造万物,快乐不停”创意大赛】6,基于爬虫的ChatGPT API调试:https://bbs.eeworld.com.cn/thread-1267604-1-1.html
【DigiKey“智造万物,快乐不停”创意大赛】7,TTS功能实现:https://bbs.eeworld.com.cn/thread-1267634-1-1.html
Word报告:
作品源码:
https://download.eeworld.com.cn/detail/%E9%A1%BA%E7%AB%BF%E7%88%AC/630319
-
发表了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】7,TTS功能实现
TTS功能的实现基于的是edge-tts这个库,这个库是基于爬虫方法进行实现的。
首先是安装这个库:
```
pip3 install edge-tts
```
用以下命令可以列出所有支持的语言角色:
···
edge-tts --list-voices
···
测试代码如下:
```
#!/usr/bin/env python3
import edge_tts
import pydub
import io
async def tts(text, actor = "zh-CN-XiaoyiNeural", fmt = "mp3"):
_voices = await edge_tts.VoicesManager.create()
_voices = _voices.find(ShortName=actor)
_communicate = edge_tts.Communicate(text, _voices[0]["Name"])
_out = bytes()
async for _chunk in _communicate.stream():
if _chunk["type"] == "audio":
# print(chunk["data"])
_out += _chunk["data"]
elif _chunk["type"] == "WordBoundary":
# print(f"WordBoundary: {chunk}")
pass
if fmt == "mp3":
return _out
if fmt == "wav":
_raw = pydub.AudioSegment.from_file(io.BytesIO(_out))
_raw = _raw.set_frame_rate(16000)
_wav = io.BytesIO()
_raw.export(_wav, format="wav")
# for i in range(len(_wav.getvalue())-1,-1,-1):
# if _wav.getvalue()[i] != 0x00:
# break
return _wav.getvalue()#[:i+1]
if __name__ == "__main__":
import asyncio
import pydub.playback
while True:
text_in = input(">说点什么:")
raw_wav = asyncio.run(tts(text_in, actor = "zh-CN-XiaoyiNeural", fmt = "wav"))
wav = pydub.AudioSegment.from_file(io.BytesIO(raw_wav))
pydub.playback. _play_with_pyaudio (wav)
```
这里强制使用了pyaudio方法来播,是为了指定播放设备,因为我们这里要使用I2S HAT来进行播放,而不是默认设备。先使用aplay -l查询设备编号,接着修改库的源代码,在site-packages/pydub/playback.py中第26行中添加output_device_index=1, 完整函数如下:
···
def _play_with_pyaudio(seg):
import pyaudio
p = pyaudio.PyAudio()
stream = p.open(format=p.get_format_from_width(seg.sample_width),
channels=seg.channels,
rate=seg.frame_rate,
output_device_index=1,
output=True)
# Just in case there were any exceptions/interrupts, we release the resource
# So as not to raise OSError: Device Unavailable should play() be used again
try:
# break audio into half-second chunks (to allows keyboard interrupts)
for chunk in make_chunks(seg, 500):
stream.write(chunk._data)
finally:
stream.stop_stream()
stream.close()
p.terminate()
···
-
回复了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】3,语音识别模型调试
wangerxian 发表于 2023-12-21 09:47
这是Python的离线语音模型吧?识别率怎么样?
成功配置成中文模式后识别率还是很高的
-
加入了学习《【DigiKey“智造万物,快乐不停”创意大赛】 钢铁侠的智能助手贾维斯》,观看 【DigiKey“智造万物,快乐不停”创意大赛】 钢铁侠的智能助手贾维斯
-
发表了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】6,基于爬虫的ChatGPT API调试
ChatGPT接入我使用的是poe-api-wrapper这个库,这个库是基于爬虫方式实现用python接入poe的服务。库是标准的python库,使用pip进行安装:
```
pip install -U poe-api-wrapper # -U for upgrade
```
使用这个库的话需要我们再做一些准备工作来配置个人的身份验证信息,也就是调用时需要用到的token。官方提供的步骤如下:
Sign in at https://www.quora.com/
F12 for Devtools (Right-click + Inspect)
- Chromium: Devtools > Application > Cookies > quora.com
- Firefox: Devtools > Storage > Cookies
- Safari: Devtools > Storage > Cookies
Copy the "value" of `m-b` cookie
按照上述步骤获取到token后,我们可以用以下代码测试一下:
···
from poe_api_wrapper import PoeApi
token = "xxxxxxxxxxx"
bot = "acouchy"
message = "你好”
client = PoeApi(token)
for chunk in client.send_message(bot, message):
pass
answer = chunk["text"]
print("AI said: " + answer)
···
如果我们可以从terminal中看到ai返回的对话,那么说明一切正常。
-
发表了主题帖:
【DigiKey“智造万物,快乐不停”创意大赛】5,在树莓派上搭建YOLOv8
YOLOv8安装起来比较简单,仅需一行代码:
```
pip install ultralytics
```
但这里面有个大坑,运行会报错Segmentation fault,没有其他任何信息。查阅了大量资料后终于发现这是由于Pytorch的版本兼容问题导致,在安装完成ultralytics后,还需要运行以下命令重新安装一下Pytorch:
```
pip uninstall torch torchvision
pip install torch==2.0.1 torchvision==0.15.2 torchaudio==2.0.2
```
完成好这一步,才算真正完成了安装。接着我们测试一下物体识别算法能否工作:
```
yolo predict model=yolov8n.pt source='https://ultralytics.com/images/bus.jpg'
```
运行完成后打开结果输出文件夹,看到以下图片,说明运行一切正常。