回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

【Arduino 动手做】Piko:反应灵敏、充满个性的健身好伙伴 简单

头像 驴友花雕 2025.07.15 6 0

认识 Piko — 您的小巧像素化健身伴侣,它住在您的手腕上,全天为您加油。他不仅可爱——他反应灵敏、反应灵敏、充满个性!
Piko 可以实时检测您的活动,无论您是休息、步行、慢跑还是短跑。每次你移动,他也会移动。他会在您身边蹦蹦跳跳、行进或喧嚣 - 将您的脚步变成动画。但不要太懒......如果 Piko 没有达到他的每日步数目标,他就会关闭。如果他睡着了,那么......他可能不会再醒来。

用品:
• 烙铁
• 焊锡丝
• 剥线钳
• 电线 (公 - 公)
• 万用表(带有连续性测试仪的手表)
• 热胶枪
• 热胶枪棒
组件:
• LCD 显示屏
• ESP32 甲壳虫 C6
• 加速度计
• 200mAh 锂电池
• 3 针 SPDT 开关
• 一条表带(我用的是我旧破手表中的一条)
在零件集成方面,来自 DFRobot 的组件运行得非常好

Piko 使用板载加速度计跟踪您的运动,应用一些巧妙的数学和物理学来估计您走步的速度和频率。我们稍后将深入探讨技术细节,但简而言之,通过分析加速度的变化,Piko 可以弄清楚你在做什么。

一旦他检测到您的动作发生变化,例如从步行切换到跑步,ESP32 就会启动,选择正确的动画来匹配您的活动。这就是您了解 Piko 跟上步伐的方式:他的显示会随着您的变化而变化。

Piko 的设计灵感来自 Tamagotchi 角色的怀旧魅力和极简主义像素艺术的简洁美学。目标是创造一个让人感到熟悉、舒适且视觉永恒的伴侣。
他被有意设计为:
• 瞬间可爱,带有一丝面无表情的个性
• 平易近人且不令人生畏
• 视觉简单,仅使用黑色和白色,以获得最大的清晰度和魅力

当您走路、跑步或冲刺时,您是在推动地面向前移动。这种推力会产生一种力,这会导致你的速度发生变化——在物理学中,速度的变化被称为......你猜对了:加速。
这个想法是这样的:
• 使用加速度计测量加速度变化
• 过滤和处理原始数据以减少噪音
• 将数据与空闲、步行、慢跑、冲刺和睡眠模式的预定义阈值进行比较。
• 根据结果,更新 Piko 的动画并计算所走的步数,这显示在 Piko 正下方的进度条上
这是一个简单的循环,但它让一切变得不同,将您的现实世界的动作变成 Piko 可以理解和响应的事物。

需要注意的主要部分是我的思想和身体深深交织在一起,重要的是像我的显示这样的功能不要打断我的思考,否则我可能会在不该睡着的时候睡着,或者在你完成慢跑后开始跑步。
像这样将所有文件放在一个干净的文件夹中也很重要,这样当您在 Arduino IDE 中单击编译和上传时,我大脑的每个部分都知道其他部分在哪里。
此外,您需要在 Arduino IDE 工具下拉菜单中启用 USB CDC-On-Boot,然后才能将我的 Spirit(固件)连接到我的大脑(硬件)。
完成此作,并安装并下载了您的所有库,我应该会思考、看起来和感觉非常棒。
(关于软件:所有基于 GIF 的库都是 Arduino IDE 原生的,可以通过库管理器找到,而加速度计和滤波器库可以从 GitHub 的加速步骤 :) 的评论中找到)

构建和编码所有内容后,是时候在 Piko 的实际应用中进行测试了。试着轻轻摇晃他或将他绑在你的手腕上四处走动。如果一切都正确连接和编码,他应该切换动画状态以反映您的动作。

 

01.jpg
02.jpg
03.jpg
04.jpg
05.jpg
06.jpg
07.jpg
F3JSITVMB3O5WGO.png
FBAW5K2MB3O5XDE.gif
FICI4QQMB3O5XX0.gif
FR255EHMB3O5W4A.gif
FRUCKDSMB3O5WGF.jpg
FSCHI06MB6J1ZWB.gif
FTJP7ENMB53MOU1.gif
FUXVEG1MB53MPVH.gif
FVLM0YQMB53MQQS.gif

项目代码

 

代码
#include <FiltersFromGit.h>
#include <DFRobot_LIS.h>
#include "PikoAccelerate.h"
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <AnimatedGIF.h>
#include "piko_sleep.h"
#include "piko_idle.h"  // Replace with your actual .h gif files
#include "piko_walk.h"
#include "piko_jog.h"
#include "piko_sprint.h"

// Define your MACROS for the LCD
#define TFT_CS     5
#define TFT_RST    6
#define TFT_DC     7
#define SLEEP_THRESHOLD 10000

//Function declarations:
void GIFDraw(GIFDRAW *pDraw); //Displays the GIF on the LCD
void accelerationJob(void); //manages all acceleration absed activities
void drawProgressBar(int steps) ;//manages the loading bar based of steps

//Object initilisations
DFRobot_LIS331HH_I2C acce(&Wire, I2C_ACCE_ADDRESS); //creates an accelerometer object that communicates via I2C
FilterOnePole myAccelerationFilter(LOWPASS, fc); //creates the filter object for accelerometer data
RunningStatistics myAccelerationStats;//creates an object that continously monitors acceleration mean and std
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
AnimatedGIF gif;

//Global vars
char* overlayText = "0";

unsigned long lastSampleTime = 0;
unsigned long sampleRate = 20; //ensures samples every ~20ms
MotionState previousState = NONE; //ensures that the first GIF will run

unsigned long lastFrameTime = 0;
int frameDelay = 0; //DO NOT CHANGE unknowingly. Ensures playfram function that draws GIF is non-blocking
int FPS = 9; //Desired frame rate

unsigned long sleeptimeCounter = 0;
unsigned long lastsleepcheckTime = 0;

bool gifPlaying = false;

// Data arrays (replace with your actual GIF names)
// THESE MUST BE IN THIS ORDER, since indexed by motionType 
const uint8_t* gifData[] = { idle_v2, walk_v2, jog_v2, sprint_v2, sleep_v2};
size_t gifSize[] = { sizeof(idle_v2), sizeof(walk_v2), sizeof(jog_v2),sizeof(sprint_v2), sizeof(sleep_v2)};

const int MAX_STEPS = 200; //Number of steps to fill the progress bar

void setup() {
  //Serial set
  Serial.begin(115200);
  while(!Serial){};
  while(!acce.begin()){
    Serial.println("Initialization failed, please check the connection and I2C address - must be");
  }

  //take statistics averages/std's set-up
  myAccelerationStats.setWindowSecs(WINDOW);
  motionType = idling;

  //accelerometer set up
  Serial.print("chip id : ");
  Serial.println(acce.getID(),HEX);
  acce.setRange(/*range = */DFRobot_LIS::eLis331hh_12g);
  acce.setAcquireRate(/*rate = */DFRobot_LIS::eNormal_50HZ);

    // Initialize display
  tft.init(240, 240);  // Use your screen resolution
  tft.setRotation(2);  // Adjust rotation if needed
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextColor(ST77XX_WHITE);       // Choose your text color
  tft.setTextSize(2);                   // Adjust as needed
  tft.setCursor(10, 10);                // X, Y position
  tft.invertDisplay(false);

  // Initialize GIF decoder
  gif.begin();  // No endian flag needed for Adafruit library
}

void loop() {

  unsigned long now = millis();

  //Update state every 20ms
  if (now - lastSampleTime >= sampleRate) {
    lastSampleTime = now;
    accelerationJob();
  }
  //Handles if it needs to go into a sleep state.
  if(motionType == idling){
    sleeptimeCounter = sleeptimeCounter+now-lastsleepcheckTime;
    if(sleeptimeCounter>=SLEEP_THRESHOLD){
      motionType=sleeping;
    }
    lastsleepcheckTime = now;
  }
  else{
    sleeptimeCounter=0;
    lastsleepcheckTime = now;
  }
  // If state changed, open new GIF
  if (motionType != previousState) {
    gif.close(); // Close previous GIF
    if (gif.open((uint8_t*)gifData[motionType], gifSize[motionType], GIFDraw)) {
      gifPlaying = true;
      lastFrameTime = now;
      frameDelay = 0;
      previousState = motionType;
    } else {
      Serial.println("Failed to open GIF");
      gifPlaying = false;
    }
  }

  // 3. Non-blocking GIF frame playback
  if (gifPlaying && now - lastFrameTime >= 1/FPS) {
    int result = gif.playFrame(false, &frameDelay);
    lastFrameTime = now;
    drawProgressBar(steps);
    if (result == 0) {
      gif.reset();  // Or gifPlaying = false if you don't want to loop
    }
  }
}



/********************************************************************************************************************/
/************************************************Function Definitions************************************************/
/********************************************************************************************************************/


void accelerationJob(void){
  
  //Acceleration Raw Data
  ax = acce.readAccX();
  ay = acce.readAccY();
  az = acce.readAccZ();

  a = getMagnitude(ax,ay,az)-1000;

  //Filters through Lowpass to remove noise
  myAccelerationFilter.input(a);
  afiltered = myAccelerationFilter.output();

  //Get running statistics
  myAccelerationStats.input(afiltered);
  a_ave = myAccelerationStats.mean();
  a_std = myAccelerationStats.sigma();
  
  //Acceleration Logic
  motionType = determineMovementType(a_ave, a_std);
  countSteps(afiltered, motionType);
}

void GIFDraw(GIFDRAW *pDraw) {
  if (pDraw->y >= tft.height()-37) return; //-37 ensures gif doesn't overdraw on the loading bar

  static uint16_t lineBuffer[320];  // Enough for full width

  uint8_t *s = pDraw->pPixels;
  uint8_t *pal = (uint8_t *)pDraw->pPalette;

  for (int x = 0; x < pDraw->iWidth; x++) {
    if (pDraw->ucHasTransparency && *s == pDraw->ucTransparent) {
      lineBuffer[x] = tft.color565(0, 0, 0);  // Optional: treat as black 
      s++;
      continue;
    }
    uint8_t index = *s++;
    lineBuffer[x] = tft.color565(pal[index * 3], pal[index * 3 + 1], pal[index * 3 + 2]);
  }

  tft.drawRGBBitmap(pDraw->iX, pDraw->iY + pDraw->y, lineBuffer, pDraw->iWidth, 1);
  if (pDraw->y == (pDraw->iHeight - 1)) {
    tft.setTextColor(ST77XX_WHITE, ST77XX_WHITE); // Optional: erase previous text background
    tft.setTextSize(2);
    tft.setCursor(10, 10);
    tft.print(String(steps));
  }
}

void drawProgressBar(int steps) {
  Serial.println("I am in draw bar fn");
  static int lastFillWidth = -1; // remember the last fill width (ensure static)

  int barWidth = 160;
  int barHeight = 18;
  int thickness = 2;
  int bottomPadding = 15;
  int x = (tft.width() - barWidth) / 2;
  int y = tft.height() - barHeight - bottomPadding;

  uint16_t barColor = tft.color565(216, 217, 217);

  int clampedsteps = constrain(steps,0,MAX_STEPS);
  int fillInset = thickness;
  int fillWidth = map(clampedsteps, 0, MAX_STEPS, 0, barWidth - 2 * fillInset);

  // Only redraw if the fill width changed -better speed
  if (fillWidth == lastFillWidth) return;
  lastFillWidth = fillWidth;

  // Draw thicker outline via multiple rectangles
  for (int i = 0; i < thickness; i++) {
    tft.drawRect(x - i, y - i, barWidth + 2 * i, barHeight + 2 * i, barColor);
  }

  // Clear previous fill area
  tft.fillRect(x + fillInset, y + fillInset, barWidth - 2 * fillInset, barHeight - 2 * fillInset, ST77XX_BLACK);

  // Draw current fill
  tft.fillRect(x + fillInset, y + fillInset, fillWidth, barHeight - 2 * fillInset, barColor);
}

【Arduino 动手做】Piko:反应灵敏、充满个性的健身好伙伴
项目链接:https://www.instructables.com/Piko-Your-Friendly-Fitness-Buddy/
项目作者:Iloke Alusala

项目视频:https://www.youtube.com/watch?v=1BNUNgcb1k0
项目代码:https://github.com/Iloke-Alusala/ESP_Piko_Firmware
3D打印文件:
https://content.instructables.com/FNC/0BZV/MB3O60DR/FNC0BZVMB3O60DR.step
https://content.instructables.com/FCC/WXHM/MB3O60EF/FCCWXHMMB3O60EF.step

 

08.jpg
10.jpg
00188---0.gif

评论

user-avatar
icon 他的勋章
    展开更多