408|7

35

帖子

1

TA的资源

一粒金砂(中级)

楼主
 

【2024 DigiKey 创意大赛】自行车智能骑行助手 [复制链接]

  本帖最后由 Newhor 于 2024-11-27 09:38 编辑

自行车智能骑行助手

作者:newhor

 

一、作品简介

 

简介

        自行车智能骑行助手的想法是填补骑行配件功能的空缺,为户外骑行爱好者提供一个集胎压测量、体重监测分析及环境感知预警于一体的综合智能解决方案

市面上现有的骑行配件中,很少有监测骑行者长途骑行下的体重变化,胎压预警以及环境感知预警等功能。本作品主要特点如下:

①在骑行前,安装在自行车轮胎中的压力传感器采集的压力数据,可以监测轮胎的初始压力值,并通过ESP-NOW无线通信将数据传送到主控MCU,再通过前期的数据标定确定的函数关系,MCU将压力数据转换为胎压后,显示在屏幕上。当检测到胎压低于预设的安全范围(例如25PSI)时,智能助手会立即通过显示器提示骑行者进行胎压补充,确保骑行安全。具体实现包括:压力传感器安装在轮胎钢圈与内胎之间,测量初始压力值;ESP-NOW模块负责将传感器数据无线传输到主控MCU;主控MCU处理接收到的数据,并通过显示器显示给骑行者。

 

②在长时间骑行时,系统通过采集安装在轮胎内的压力传感器的压力值,通过前期的标定,将其转化为体重值。结合骑行时间和BME680传感器提供的环境温度数据,系统通过算法计算一段时间内体重的变化量,如果体重变化量超过预设阈值,会触发警报,通过显示屏提醒用户,同时提供休息和补水建议。

 

③通过拓展的BME680环境传感器,给骑行者提供户外骑行时的空气质量预警和高温预警。一旦发现空气质量下降或温度过高,智能助手将通过显示屏及时提醒骑行者采取相应措施,如减少运动强度或寻找遮阴处休息,确保骑行者的健康和安全。

 

④通过车把端的MCU采集并记录BME680传感器在一段时间内的气压和湿度数据,MCU对这些数据进行分析,当检测到气压持续降低且空气湿度升高超过一定阈值时,提供下雨预警,通过显示器告警骑行者,建议提前返回或寻找避雨场所,确保骑行安全。

 

⑤可记录骑行过程中的体重、胎压、环境变化数据保存在本地(后续添加手机端无线接收功能),并提供长期趋势分析,帮助骑行者更好地了解自身状况和改善骑行计划。

此外,该还系统具有强大的可拓展性,以后可以集成更多好玩的功能进来。

 

主要特点:

  • 胎压监测
  • 实时监测体重变化
  • 智能骑行建议
  • 恶劣天气预警
  • 可存储运动数据,分析长期数据
  • 硬件模功能块化设计,功能易扩展
  • 可太阳能充电
  • 开源

 

照片

(1)车把数据接收显示端

  

 

 

(2)轮胎数据采集发送端

       

 

 

二、系统框图

  1. 硬件说明

 

主要物料

  • 控制器:ESP32-C6,ESP32-S2MINI
  • 加速度传感器:4554、MMA8452
  • 湿度空气传感器:BME680
  • 显示屏幕:1.8寸TFT
  • 充电管理芯片:TP4054
  • 薄膜式压力传感器
  • 锂电池、太阳能板

 

硬件框图

主要分为车轮压力数据采集端、以及车把数据接收显示端,采用模块集成方式、方便更换与拓展。

 

 

    2.软件说明

 

    软件分为4个部分:

  • MCU数据处理
  • ESPNOW数据传输
  • 传感器数据采样控制
  • 液晶显示

 

三、各部分功能说明

  1. 硬件功能

(1)轮胎端:主要完成胎压数据采集、ESPnow数据发送等功能

 

  • 轮胎端主控使用ESP32-S2MINI,这是一款乐鑫一款入门级开发板,搭载Xtensa®32位LX7单核处理器,工作频率高达240MHz。可深度休眠,利用低功耗协处理器监测外设的状态变化或某些模拟量多的阈值,可通过外部中断唤醒,常用于可穿戴电子设备、智能家居等场景常用于物联网设备开发。在该作品中,利用其ADC功能,采集安装于轮胎内壁的薄膜式压力传感器的电压值,并通过ESPnow无线功能,实时将数据上传到车把端的MCU进行处理。
  • 加速度传感器采用了基础型号MMA8452,加速度传感器的中断输出引脚,连接到轮胎端ESP32MINI上可支持休眠唤醒的IO口,并在采样程序中配置了休唤醒中断。当自行车静止一段时间后,ESP32MINI由于未检测到加速度传感器的运动中断,会进入深度休眠降低功耗,当检测到轮胎震动时,加速度传感器会立即产生运动中断唤醒MCU,开始采集压力数据,实现了轮胎端采样系统的低功耗。

 

 

  • 压力传感器采用薄膜式,方便安装于轮胎与钢圈之间,当轮胎由于胎压或骑行者体重产生形变时,薄膜式压力传感器受到压力产生相应的形变,其电阻值产生相应变化,MCU通过电阻分压电路的方式进行ADC采样,将压力值转换为电压值。

 

  • 供电部分,配备了电池,并基于TP4054芯片设计了可多路供电的防反充电电路,通过添加多个二极管防止反充,实现开发板的USB接口以及太阳能板双充电模式。

 

    (2)车把端:主要完成数据接收,处理、以及数据显示等功能

 

  • 车把端主控使用大赛提供的ESP32-C6开发板,是一款高性能、低功耗的单芯片解决方案。多达 22 个 GPIO 引脚,支持 SPI、I2C、UART 等多种通信协议,具有 Wi-Fi 6 和 蓝牙低能耗(Bluetooth LE 5.2) 的支持,主要面向现代无线通信需求。该作品采用该模块作为主控接收端MCU,能满足ESPNOW无线功能高频的数据接收和处理需求,并同时实现了对多个传感器的数据采集处理,以及显示。
  • 显示器使用ST7735驱动TFT屏,支持全彩显示、内容方向可调整,数据显示量够大,且观感更好。
  • 加速度传感器模块使用的是大赛提供的4554,ICM-20948 是一款高性能的9轴运动传感器,具有高精度与低噪声等特点,在该作品中,该模块用于判断自行车实时的骑行状态,是加减速还是匀速,帮助MCU处理数据,消除加减速带来的轮胎形变时产生的误差数据。
  • 环境传感器使用的是大赛提供的BME680模块,具有环境参数丰富,精度较高等特点。在该作品中,主要用于采样环境参数,包括温湿度、空气质量、大气压等。其中温度数据,用于辅助长时间骑行时体重变化量供相应的休息和补水建议;通过分析湿度和大气压数据下雨前的变化趋势,提前预测降雨的可能性,并通过屏幕向骑行者发出提醒;空气质量数据,实时转换为空气质量IAQ,给户外骑者相应的户外骑行建议。
  • 供电电路与轮胎端一致,采用电池供电,并支持USB与太阳能板双充电。

2.软件功能

    (1)数据标定以及转换部分

在胎压数据标定时,由于传感器的精度以及受到安装位置的影响,会出现相同胎压下前后轮的压力传感器初始压力不一致的现象,故前后轮的压力数据分开进行标定。压力传感器安装于车轮内部后,通过带压力显示的打气筒给轮胎补充到多组不同的胎压值,得到对应胎压下的传感器压力数值,作为空载状态下,不同胎压与采集电压的数据组合,通过数据拟合得到相应的函数关系后,实现通过空载状态下电压计算出当前胎压数值的功能。

在体重数据标定时,通过记录多个不同体重骑行者,骑行时与空载的压力差值,与相应体重进行数据组合,然后进行数据拟合,得到电压差与骑行者体重函数关系。

    (2)数据滤波部分

 

        骑行时压力传感器由于轮胎转动到不同的位置、路面颠簸、急加减速等因素,会产生部分误差数据。采用滑动平均法,对一段时间窗口内的数据进行平均,从而减少噪声或波动,使数据更加平滑。

 

四、作品源码

 

自行车智能骑行助手——作品源码-嵌入式开发相关资料下载-EEWORLD下载中心

    系统的硬件部分软件使用 micropython 进行开发,采用异步多任务形式。部分源码如下:

 

    文件:task_TFTshow.py

import asyncio
from machine import Pin, SoftSPI
from time import sleep_ms
from global_var import *
from cfg import *


async def TFTshow():
    while True:
        if gv.int_flag == 1:
#             pass
            lcd.clear(0)
            lcd.rotate(180)
            lcd.drawText('系统初始化中'+str(gv.int_cnt*10)+'%',0,50,font,0x0f0)
            lcd.drawText('请在10S内推动自',0,70,font,0xff0)
            lcd.drawText('行车采集初始数据',0,90,font,0xff0)
            lcd.show()
        elif gv.str_flag == 1 or 2 or 3:
            lcd.clear(0)
            lcd.rotate(180)
            lcd.drawText('温度:'+str(round(gv.tmp,1))+'°',0,0,font,0x0f0)
            lcd.drawText('湿度:'+str(round(gv.hum,1))+'%',0,20,font,0xff0)
            lcd.drawText('气压:'+str(round(gv.pre,1))+'Pa',0,40,font,0x00f)
            lcd.drawText('IAQ:'+str(gv.IAQ)+gv.iaq_leve,0,60,font,0xff0)          
            lcd.show()
            await asyncio.sleep(2)
            
            if  gv.weight_int !=0:
                lcd.clear(0)
                lcd.rotate(180)
                lcd.drawText('初始胎压psi:',0,0,font,0x00f)
                lcd.drawText('前:'+str(gv.P1_int)+'后:'+str(gv.P2_int),0,20,font,0x00f)
                lcd.drawText('初始体重:'+str(gv.weight_int)+'kg',0,40,font,0x0f0)               
                lcd.drawText('实时体重:'+str(gv.weight)+'kg',0,60,font,0x0f0)                
#                 lcd.drawText('体重差值:'+str(gv.weight_int-gv.weight)+'kg',0,80,font,0x00f)
                lcd.drawText('运动时长:'+str(round(gv.SportTime/60+0.7,1))+'min',0,100,font,0x00f)
                lcd.show()
                await asyncio.sleep(2)
                gv.SportTime+=2
            gv.SportTime+=2.1
        if gv.end_flag ==1:#运动结束
            lcd.clear(0)
            lcd.rotate(180)
            lcd.drawText('运动已结束',0,0,font,0x0f0)
            lcd.drawText('该次运动时长为',0,50,font,0xff0)
            lcd.drawText(str(round(gv.SportTime/60,1))+'min',0,70,font,0xff0)
            lcd.drawText('该次运动减重;',0,90,font,0xff0)
            lcd.drawText(str(gv.weight_int-gv.weightmin)+'kg',0,110,font,0xff0)
            lcd.show()
            import machine
            machine.reset()
        await asyncio.sleep(0.1)
# asyncio.run(TFTshow())

    文件:task_ESPnow.py

import network
import aioespnow
import asyncio
import neopixel
import binascii,time
from machine import Pin
from neopixel import NeoPixel
from time import localtime
from global_var import *
from cfg import *

np = neopixel.NeoPixel(Pin(8), 1)

# WLAN接口必须处于活动状态才能 send()/recv()
sta = network.WLAN(network.STA_IF) # 或 network.AP_IF
sta.active(True)
sta.disconnect() # 对于 ESP8266

e = aioespnow.AIOESPNow()
e.active(True)

peer = b'\xAA\xBB\xCC\xDD\xEE\xFF' # 配对设备wifi接口的MAC地址
e.add_peer(peer) # 发送前必须 add_peer()
# e.send(peer, '123')

def Pcvt1(x):
    if x>=0 and x<0.76:
        return 35
    if x>=0.76 and x<0.92:
        return 25
    if x>=0.92 and x<1.0:
        return 30
    if x>=1.0 and x<1.08:
        return 35
    if x>=1.08 and x<1.3:
        return 40
    if x>=1.3 and x<1.4:
        return 45
    if x>=1.4:
        return 50
    
    
def Pcvt2(x):
    if x>=0 and x<1.12:
        return 20
    if x>=1.12 and x<1.19:
        return 25
    if x>=1.19 and x<1.24:
        return 30
    if x>=1.24 and x<1.26:
        return 35
    if x>=1.26 and x<1.28:
        return 40
    if x>=1.28 and x<1.32:
        return 45
    if x>=1.32:
        return 50



def P_Vdata_int(r):#     初始电压平均
    global v1,v2,m,n
    print('接收的r[4:9]字节数据为:',r[4:9],type(r[4:9]))
    if r[2] == 49:
        gv.V1 = float(r[4:9].decode('utf-8'))
        print('转换后的整数数据为V1:',gv.V1)
        v1 = gv.V1+v1
        n=n+1
        gv.Vint1 = round(v1/n,3)
        print('gv.Vint1',gv.Vint1 )
    elif r[2] == 50:
        gv.V2 = float(r[4:9].decode('utf-8'))
        print('转换后的整数数据为V2:',gv.V2)
        v2 = gv.V2+v2
        m=m+1
        gv.Vint2 = round(v2/m,3)
        print('gv.Vint2',gv.Vint2)
        
def P_Vdata(x):#取峰值
    global r
    gv.st_cnt+=1
    if gv.st_cnt<=x:
        if r[2] == 49:
            gv.V1 = float(r[4:9].decode('utf-8'))
        elif r[2] == 50:
            gv.V2 = float(r[4:9].decode('utf-8'))
        if gv.V1 > gv.V1max:
            gv.V1max = gv.V1
        if gv.V2 > gv.V2max:
            gv.V2max = gv.V2
    else:
        gv.st_cnt = 0
        gv.V = gv.V1+gv.V2-gv.Vint1-gv.Vint2
        print('========gv.V:',gv.V)
        gv.weight = WGTcvt(gv.V)
        if gv.weight<gv.weightmin:
            gv.weightmin = gv.weight
    

def WGTcvt(y):
    if y>0.1:
        t = 57+y*12
    elif y< 0.1 and y >0.05:
        t = 57+y*21
    elif y <0.05 and y>0.01:
        t = 57+y*31
    elif y <0.01:
        t = 57+y*61
    print('^^^^^^^^^t',t)
    return t
    

async def e_recv():
    global r
    while True:
        if e.any():
            host, msg = e.recv()
            if len(msg)>=5 and len(msg)<=15:
                if msg[:2] == b'cc':
                    print('msg',msg)
                    np[0] = (0, 30, 0)
                    np.write()     
                    gv.int_cnt +=1
                    print('gv.int_cnt',gv.int_cnt)
                    if gv.int_cnt <= 10:#初始化时间10S
                        #gv.int_flag = 1
                        P_Vdata_int(msg)
                        print('gv.int_flag :',gv.int_flag)
                    elif gv.int_cnt>10 and gv.str_flag ==0:
                        gv.int_flag = 0
                        gv.str_flag = 1
                        print('gv.str_flag:',gv.str_flag)
                    else:
                        pass
                    
                        
                        
                    if gv.str_flag == 1:#初始化结束,胎压转换
                        gv.P1_int = Pcvt1(gv.Vint1)
                        print('gv.P1_int',gv.P1_int)
                        gv.P2_int = Pcvt2(gv.Vint2)
                        print('gv.P2_int',gv.P2_int)
                        gv.str_flag = 2
                        print('gv.str_flag :',gv.str_flag)
                    
                    if gv.str_flag == 2:#开始测量初始体重
                        print('---------------gv.str_flag :',gv.str_flag)
                        gv.waitcnt+=1
                        if gv.waitcnt>=100:
                            r = msg
                            P_Vdata(10)#取50次数据的峰值
                            if gv.weight>0 and gv.st_cnt ==0:
                                gv.weight_int = gv.weight
                                print('gv.weight_int',gv.weight_int)
                                gv.weight = 0
                                gv.str_flag = 3
                    if gv.str_flag == 3:#开始测量运动后体重
                        print('```````````gv.str_flag:',gv.str_flag)
                        r = msg
                        P_Vdata(20)#每100次数据更新一次体重

                    r = localtime()
                    s = '{:04}-{:02}-{:02} {:02}:{:02}:{:02} > {}'.format(r[0], r[1], r[2], r[3], r[4], r[5], str(msg))
                    f = open('称重数据1.txt', "a", encoding="UTF-8")
                    f.write( s+'\n')
                    f.close()
                    
                    await asyncio.sleep(0.05)
                    np[0] = (0, 0, 0)
                    np.write()
                else:
                    print('msg head error')
            else:
                print(' msg len error')
        else:
            print('等待数据,请在5秒内启动车辆')
            gv.endcnt+=1
            if gv.endcnt>=100:#(5秒内未收到数据,运动结束)
                gv.endcnt = 0
                gv.end_flag = 1

        await asyncio.sleep(0.05)

# asyncio.run(e_recv())

 

五、作品功能演示视频

自行车智能骑行助手——得捷大赛作品演示视频_哔哩哔哩_bilibili

自行车智能骑行助手——得捷大赛作品演示视频

 

六、项目总结

    参加这次活动之前,刚好买了一辆自行车,加入了骑行者的队伍,然后计划购买一些配件来改装车辆,和同事讨论时,想到胎压测量以及骑行时的体重监测等场景,在市面上没有找到类似功能的产品,所以想尝试做一下这个功能的作品,看看是否能够实现预期的效果。

    此次作品将最初想法能大致实现了,可以完成测量胎压和体重等功能,但由于各方面的限制,数据精度上但是还有很多不足,例如传感器精度不足以及线性度不够、安装位置无法保证受力均衡、轮胎的自然转动带来的数据波动,都会影响最后得到的数据精度。由于时间关系,还有运动数据上传到手机功能暂未实现,这个后续再添加。

    通过参加本次活动,收获还是不小的,真真切切的感受到了从一个想法到一个作品做好的难度,需要考虑的东西太多了,做到一半才发现很多东西与预想的差别很大,可能写程序、画板子还是其中最容易的环节,例如此次作品中,一个合适的传感器的选择和安装固定的方式,在前期就花了很多时间。

    最后,感谢得捷和EEworld,组织了这次活动,让大家的想法可以有个很好的机会来实现,并且能学习到很多其他人的有趣想法。

 

 

【2024 DigiKey 创意大赛】物料开箱帖 - DigiKey得捷技术专区 - 电子工程世界-论坛

【2024 DigiKey 创意大赛】自行车智能骑行助手 - DigiKey得捷技术专区 - 电子工程世界-论坛

 

自行车智能骑行助工程文件.zip (1.37 MB, 下载次数: 1)

链接已隐藏,如需查看请登录或者注册

 

最新回复

明白了,还能变相的称体重,想法确实很好。   详情 回复 发表于 2024-11-27 13:22
点赞 关注
 
 

回复
举报

755

帖子

5

TA的资源

纯净的硅(高级)

沙发
 

楼主分享的技术知识非常有实用价值,值得收藏学习借鉴

 
 
 

回复

6450

帖子

10

TA的资源

版主

板凳
 

这压力检测直接放进去了?不漏气吗?  

点评

可能传感器是夹在内胎和外胎之间  详情 回复 发表于 2024-11-26 18:47
个人签名

在爱好的道路上不断前进,在生活的迷雾中播撒光引

 
 
 

回复

7159

帖子

2

TA的资源

版主

4
 
秦天qintian0303 发表于 2024-11-26 12:17 这压力检测直接放进去了?不漏气吗?  

可能传感器是夹在内胎和外胎之间

点评

先将轮胎气放光,然后夹在钢圈和内胎之间,然后再充气固定的[attachimg]865081[/attachimg]    详情 回复 发表于 2024-11-26 21:30
 
 
 

回复

35

帖子

1

TA的资源

一粒金砂(中级)

5
 
wangerxian 发表于 2024-11-26 18:47 可能传感器是夹在内胎和外胎之间

先将轮胎气放光,然后夹在钢圈和内胎之间,然后再充气固定的

 

点评

原来如此,这样确实也能检测胎压,不过在骑行的过程压力会变化吧  详情 回复 发表于 2024-11-27 09:02
 
 
 

回复

7159

帖子

2

TA的资源

版主

6
 
Newhor 发表于 2024-11-26 21:30 先将轮胎气放光,然后夹在钢圈和内胎之间,然后再充气固定的  

原来如此,这样确实也能检测胎压,不过在骑行的过程压力会变化吧

点评

分为了两个阶段,初始未骑行的状态的测量值转换为胎压,骑行中的值转换为体重,用了一些算法对进行骑行时的数据进行滤波处理  详情 回复 发表于 2024-11-27 09:30
 
 
 

回复

35

帖子

1

TA的资源

一粒金砂(中级)

7
 
wangerxian 发表于 2024-11-27 09:02 原来如此,这样确实也能检测胎压,不过在骑行的过程压力会变化吧

分为了两个阶段,初始未骑行的状态的测量值转换为胎压,骑行中的值转换为体重,用了一些算法对进行骑行时的数据进行滤波处理

点评

明白了,还能变相的称体重,想法确实很好。  详情 回复 发表于 2024-11-27 13:22
 
 
 

回复

7159

帖子

2

TA的资源

版主

8
 
Newhor 发表于 2024-11-27 09:30 分为了两个阶段,初始未骑行的状态的测量值转换为胎压,骑行中的值转换为体重,用了一些算法对进行骑行时 ...

明白了,还能变相的称体重,想法确实很好。

 
 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/9 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表