查看: 337|回复: 1

ULTIMATE口香糖机

[复制链接]
本帖最后由 粒子 于 2018-8-6 18:47 编辑


LEDWiFi、自动弹出、LCD屏幕都备齐了。
这款口香糖球机能与客户通过网页互动。

dsc_0497_jbqGfnByzH.JPG

硬件部件

2.8英寸TFT触摸屏,带4MB闪存,用于Arduinomedde
Teensy 3.5
ESP8266 Thing - 开发板
WS2812 LED灯带
白色街机按钮
用于3D打印机的混合式步进电机
用于Theremino系统的步进电机的驱动器DRV8825

软件应用程序和在线服务

Node.js  
Autodesk Fusion 360
Arduino IDE
手工工具和制造机器
3D 打印机(通用)
烙铁(通用)
数控雕刻机
竖锯


设想

Ultimate是什么?无限RGB? 一个很酷的LCD触摸屏怎么样?
甚至是一些完全不必要的WiFi功能?所有这些都在一台口香糖球机里,怎么样?

dsc_0476_nsiIShRudp.JPG
dsc_8615_pitXdEAAHL.JPG

设计

像往常一样,几乎所有比制作一些简单连接和基本盒子更复杂的东西都需要在 Fusion 360 中进行设计。
我开始草拟我希望机器看起来像什么。
它需要很高,有足够的空间容纳所有的电子设备,也能支撑12磅口香糖球的重量。


fusion1_nottVv4Wqh.PNG
fusion2_IeiUmdzRgQ.PNG


于是我试着做了一个简单而优雅的弹出机构。
它一次只能弹出一个口香糖球,不会被卡住,不要让一个以上的口香糖球从它转动的地方掉下来。
我意识到我所需要的只是一个有4个孔的简单轮子,并且弹出孔的顶部将具有盖子,以防止多余的口香糖球掉落。

fusion3_BqkxsvfEKo.PNG
fusion5_N1mdXVC32z.PNG

设计完成后,我导出了所有可3D打印零件和生成的刀具路径,
用于在外壳上进行数控雕刻。

外壳和制造

我从收集口香糖球胶球机腿的尺寸开始,然后在一张巨大的胶合板上勾画出来。
然后我拿起竖锯,锯出四条腿。
我还用数控雕刻机,用胶合板锯出主外壳。

dsc_8598_0iwcSIYhsQ.JPG
dsc_8606_y1VaGZ13Da.JPG

然后,我在所有的东西上钻孔,把它涂成红色。

dsc_8602_WOZ6bREhix.JPG
dsc_8630_ovvirEG7YL.JPG

LED灯带粘在底板上,这样它就可以在机器下面的支架上发出很好的光。

dsc_9117_3W9p357S3t.JPG

网页

为了让用户与口香糖球机进行交互,需要有一个简单的界面。
我选择了创建一个简单的网页,让用户弹出口香糖球,并改变 LED 的颜色。
动作发生后,网页通过 AJAX 将数据发布到自定义 Node.js web服务器。

webpage_SZODGiO3PN.png

Web服务器

我需要一个web服务器来充当网页用户和口香糖球机之间的中介。
因此,我决定使用Node.js发送和接收数据。
用户发送POST请求以控制LED颜色和弹出。然后,ESP8266发送GET请求以获取机器的状态。
如果有人不断点击“弹出”会发生什么?服务器跟踪点击“弹出”按钮的所有IP,并阻止他们两次弹出。


电子器件

TFT屏幕需要很大的处理能力来驱动,所以我不得不选择快速且功能强大的主板,引导我使用Teensy 3.5
但现在你可能会想:Teensy如何使用WiFi?”这是很难解决的问题。
我需要让Teensy 侦听本地服务器,以了解用户所做的更改。
然后,我突然脑洞大开,只用ESP8266来检查服务器,在通过串口与Teensy通信,这让事情变得容易多了。

dsc_9114_ffWC63HZ68.JPG
dsc_9118_ShO4HDVM4R.JPG
dsc_9112_wqFuyqxNA8.JPG

软件

Teensy运行一个简单的脚本,首先从SD卡加载图像,并在屏幕上显示。
然后检查串行数据,看看是否需要改变LED的颜色或弹出。

使用方法

使用口香糖球机非常简单:只需转到网页并单击“弹出”按钮。
或者跟简单些,按一下上方的按钮,就能够到你应得的奖品。

dsc_0500_YzThUq1NYC.JPG

代码

1.Teensy CodeC/C++


#define TFT_CS 2
#define TFT_DC 5
#define SD_CS   BUILTIN_SDCARD
#define DIR_PIN 16
#define STEP_PIN 17
#define LED_PIN 20
#define BUTTON_PIN 30
#define NUMPIXELS 20
#define RPM 60
#define MOTOR_STEPS 200
#define MICROSTEPS 1
//#define rxPin 36
//#define txPin 37

#include <SPI.h>
#include <SD.h>
#include <Adafruit_ILI9341.h>
#include <Adafruit_GFX.h>
#include <Adafruit_NeoPixel.h>
#include <Arduino.h>
#include "BasicStepperDriver.h"
//#include <SoftwareSerial.h>

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, LED_PIN, NEO_GRB + NEO_KHZ800);
int LEDColors[] = {pixels.Color(255,0,0),pixels.Color(0,255,0),pixels.Color(0,0,255),pixels.Color(200,0,200),pixels.Color(0,0,0)};

BasicStepperDriver stepper(MOTOR_STEPS, DIR_PIN, STEP_PIN);

//SoftwareSerial Serial4 = SoftwareSerial(rxPin, txPin);

bool button_pressed = false;

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);

void setup(){
    //pinMode(rxPin, INPUT);
    //pinMode(txPin, OUTPUT);
    Serial.begin(9600);
    Serial4.begin(9600);
    delay(500);
    SPI.setMOSI(7);
    SPI.setSCK(14);
    if(SD.begin(SD_CS)){
      Serial.println("Success");
      File entry = SD.open("MAINPAGE.BMP");
      if(entry != NULL){
      Serial.println(entry.name());
      }
      entry.close();
  }
  stepper.begin(RPM, MICROSTEPS);
  tft.begin();
  pixels.begin();
  tft.setRotation(1);
  pixels.setBrightness(75);
  pinMode(BUTTON_PIN,INPUT_PULLUP);
  attachInterrupt(BUTTON_PIN,set_button,FALLING);
  tft.setTextColor(0x0000);
  tft.fillScreen(0xFFFF);
  tft.setCursor(0,0);
  tft.print("HI");
  setAllPixels(0);
  //displayDispensing();
  delay(3000);
  loadMainPage();
}

elapsedMillis timer1;

void loop(){
    if(button_pressed){
        button_pressed = false;
        dispense();
    }
    if(Serial4.available()){
        String cmd = Serial4.readStringUntil(',');
        Serial.print(cmd);
        Serial.print(',');
        if(cmd=="LED"){
            String color = Serial4.readStringUntil('\n');
            Serial.println(color);
            if(color=="RED\r"){
                setAllPixels(0);
            }
            else if(color=="GREEN\r"){
                setAllPixels(1);
            }
            else if(color=="BLUE\r"){
                setAllPixels(2);
            }
            else if(color=="PURPLE\r"){
                setAllPixels(3);
            }
            else if(color=="BLACK\r"){
                setAllPixels(4);
            }
        }
        else if(cmd=="DISPENSE"){
            String cmd2 = Serial4.readStringUntil('\n');
            Serial.println(cmd2);
            if(cmd2=="true\r"){
                if(timer1>5000){
                //displayDispensing();
                dispense();
                }
            }
        }
    }
   
}

void loadMainPage(){
    tft.fillScreen(0xFFFF);
    bmpDraw("MAINPAGE.BMP",0,0);
}

void displayDispensing(){
    tft.fillScreen(0xFFFF);
    tft.setTextSize(3);
    tft.setCursor(90,30);
    tft.print("Dispensing \n");
    tft.setCursor(100,80);
    tft.print("gumball!");
    delay(3000);
    loadMainPage();
}

void dispense(){
    Serial.println("Dispensing");
    displayDispensing();
    stepper.move(259);
    delay(3000);
    loadMainPage();
}

void set_button(){
    button_pressed = true;
    delay(200);
}

void setAllPixels(int colorNum){
    Serial.print("Setting pixels to ");Serial.println(colorNum);
    for(int i=0;i<NUMPIXELS;i++){
        pixels.setPixelColor(i, LEDColors[colorNum]);
    }
    pixels.show();
}

#define BUFFPIXEL 20

void bmpDraw(char *filename, int16_t x, int16_t y) {

  File     bmpFile;
  int      bmpWidth, bmpHeight;   // W+H in pixels
  uint8_t  bmpDepth;              // Bit depth (currently must be 24)
  uint32_t bmpImageoffset;        // Start of image data in file
  uint32_t rowSize;               // Not always = bmpWidth; may have padding
  uint8_t  sdbuffer[3*BUFFPIXEL]; // pixel buffer (R+G+B per pixel)
  uint8_t  buffidx = sizeof(sdbuffer); // Current position in sdbuffer
  boolean  goodBmp = false;       // Set to true on valid header parse
  boolean  flip    = true;        // BMP is stored bottom-to-top
  int      w, h, row, col, x2, y2, bx1, by1;
  uint8_t  r, g, b;
  uint32_t pos = 0, startTime = millis();

  if((x >= tft.width()) || (y >= tft.height())) return;

  Serial.println();
  Serial.print(F("Loading image '"));
  Serial.print(filename);
  Serial.println('\'');

  // Open requested file on SD card
  bmpFile = SD.open(filename);
  /*if ((bmpFile = SD.open(filename)) == NULL) {
    Serial.print(F("File not found"));
    return;
  }*/

  // Parse BMP header
  if(read16(bmpFile) == 0x4D42) { // BMP signature
    Serial.print(F("File size: ")); Serial.println(read32(bmpFile));
    (void)read32(bmpFile); // Read & ignore creator bytes
    bmpImageoffset = read32(bmpFile); // Start of image data
    Serial.print(F("Image Offset: ")); Serial.println(bmpImageoffset, DEC);
    // Read DIB header
    Serial.print(F("Header size: ")); Serial.println(read32(bmpFile));
    bmpWidth  = read32(bmpFile);
    bmpHeight = read32(bmpFile);
    if(read16(bmpFile) == 1) { // # planes -- must be '1'
      bmpDepth = read16(bmpFile); // bits per pixel
      Serial.print(F("Bit Depth: ")); Serial.println(bmpDepth);
      if((bmpDepth == 24) && (read32(bmpFile) == 0)) { // 0 = uncompressed

        goodBmp = true; // Supported BMP format -- proceed!
        Serial.print(F("Image size: "));
        Serial.print(bmpWidth);
        Serial.print('x');
        Serial.println(bmpHeight);

        // BMP rows are padded (if needed) to 4-byte boundary
        rowSize = (bmpWidth * 3 + 3) & ~3;

        // If bmpHeight is negative, image is in top-down order.
        // This is not canon but has been observed in the wild.
        if(bmpHeight < 0) {
          bmpHeight = -bmpHeight;
          flip      = false;
        }

        // Crop area to be loaded
        x2 = x + bmpWidth  - 1; // Lower-right corner
        y2 = y + bmpHeight - 1;
        if((x2 >= 0) && (y2 >= 0)) { // On screen?
          w = bmpWidth; // Width/height of section to load/display
          h = bmpHeight;
          bx1 = by1 = 0; // UL coordinate in BMP file
          if(x < 0) { // Clip left
            bx1 = -x;
            x   = 0;
            w   = x2 + 1;
          }
          if(y < 0) { // Clip top
            by1 = -y;
            y   = 0;
            h   = y2 + 1;
          }
          if(x2 >= tft.width())  w = tft.width()  - x; // Clip right
          if(y2 >= tft.height()) h = tft.height() - y; // Clip bottom
  
          // Set TFT address window to clipped image bounds
          tft.startWrite(); // Requires start/end transaction now
          tft.setAddrWindow(x, y, w, h);
  
          for (row=0; row<h; row++) { // For each scanline...
  
            // Seek to start of scan line.  It might seem labor-
            // intensive to be doing this on every line, but this
            // method covers a lot of gritty details like cropping
            // and scanline padding.  Also, the seek only takes
            // place if the file position actually needs to change
            // (avoids a lot of cluster math in SD library).
            if(flip) // Bitmap is stored bottom-to-top order (normal BMP)
              pos = bmpImageoffset + (bmpHeight - 1 - (row + by1)) * rowSize;
            else     // Bitmap is stored top-to-bottom
              pos = bmpImageoffset + (row + by1) * rowSize;
            pos += bx1 * 3; // Factor in starting column (bx1)
            if(bmpFile.position() != pos) { // Need seek?
              tft.endWrite(); // End TFT transaction
              bmpFile.seek(pos);
              buffidx = sizeof(sdbuffer); // Force buffer reload
              tft.startWrite(); // Start new TFT transaction
            }
            for (col=0; col<w; col++) { // For each pixel...
              // Time to read more pixel data?
              if (buffidx >= sizeof(sdbuffer)) { // Indeed
                tft.endWrite(); // End TFT transaction
                bmpFile.read(sdbuffer, sizeof(sdbuffer));
                buffidx = 0; // Set index to beginning
                tft.startWrite(); // Start new TFT transaction
              }
              // Convert pixel from BMP to TFT format, push to display
              b = sdbuffer[buffidx++];
              g = sdbuffer[buffidx++];
              r = sdbuffer[buffidx++];
              tft.writePixel(tft.color565(r,g,b));
            } // end pixel
          } // end scanline
          tft.endWrite(); // End last TFT transaction
        } // end onscreen
        Serial.print(F("Loaded in "));
        Serial.print(millis() - startTime);
        Serial.println(" ms");
      } // end goodBmp
    }
  }

  bmpFile.close();
  if(!goodBmp) Serial.println(F("BMP format not recognized."));
}

uint16_t read16(File &f) {
  uint16_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read(); // MSB
  return result;
}

uint32_t read32(File &f) {
  uint32_t result;
  ((uint8_t *)&result)[0] = f.read(); // LSB
  ((uint8_t *)&result)[1] = f.read();
  ((uint8_t *)&result)[2] = f.read();
  ((uint8_t *)&result)[3] = f.read(); // MSB
  return result;
}


2. ESP8266 CodeC/C++

#include <Arduino.h>

#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>

ESP8266WiFiMulti WiFiMulti;

void setup(){
    Serial.begin(9600);
    WiFiMulti.addAP("SSID", "PSK");
}

void loop(){
    if((WiFiMulti.run() == WL_CONNECTED)){
        HTTPClient http;
        
        http.begin("http://local_ip (change these values):3010/status/led");
        int httpCode = http.GET();
        
        String payload = http.getString();
        if(payload){
            Serial.print("LED,");
            Serial.println(payload);
        }
        http.end();
        
        http.begin("http://local_ip:3010/status/dispense");
        httpCode = http.GET();
        
        payload = http.getString();
        if(payload){
            Serial.print("DISPENSE,");
            Serial.println(payload);
            if(payload=="true"){
                delay(4000);
            }
        }
        http.end();
        delay(1000);
    }
}

3.Node JS Server Code

var express = require('express');
var myParser = require('body-parser');
var app = express();
const cors = require('cors');

var latestColor = "RED";
var dispense_active = false;
var usedIPs = [];

const whitelist = ['::ffff:local_ip']

app.use(myParser.json({extended: true}));
app.use(cors());
app.options('*',cors());
app.post("/gumball", function(request, response){
    console.log(request.body);
    response.send("OK, 200");
    if(typeof request.body.LED !=='undefined'){
        latestColor = request.body.LED;
    }
     if(typeof request.body.DISPENSE !=='undefined'){
        dispense_active = request.body.DISPENSE;
        if(dispense_active = true){
            if(usedIPs.indexOf(request.ip)==-1){
            usedIPs.push(request.ip);
            console.log(usedIPs);
            }
            else if(whitelist.indexOf(request.ip)>-1){
                dispense_active = true;
            }
            else{
                dispense_active = false;
            }
        }
    }
});

app.get('/status/dispense',function(req,res){
    res.send(dispense_active);
    dispense_active = false;
});

app.get('/status/led',function(req,res){
    res.send(latestColor);
});

app.listen(3010);

4.Webpage main HTML

<html>
        <head>
        <title>IoT Gumball Machine</title>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js">
        </script>
        <link rel="stylesheet" type="text/css" href="style.css">
        </head>
<body>
        <script>
                function changeLED(color){
                        console.log(color.toUpperCase());
                        var colorName = color.toUpperCase();
                        var obj = {"LED": colorName};
                        $.ajax('http://local_ip:3010/gumball',{
                                data: JSON.stringify(obj),
                                contentType: 'application/json',
                                type: 'POST'
                        });
                }
                function dispense(){
                        console.log("Dispensing");
                        var obj = {"DISPENSE": true};
                        $.ajax('http://local_ip (change these values):3010/gumball',{
                                data: JSON.stringify(obj),
                                contentType: 'application/json',
                                type: 'POST'
                        });
                }
                function changeColor(color){ document.getElementById("color_list").style.color = color;
                        console.log(color);
                }
        
        
        
        </script>
        <div class="centered">
        <form id="LED_change">
                <select id="color_list" name="color_list">
                <option class="sRed" value="red">Red</option>
                <option class="sGreen" value="green">Green</option>
                <option class="sBlue" value="blue">Blue</option>
                <option class="sPurple" value="purple">Purple</option>
                <option class="sOff" value="black">Off</option>
                </select>
                <input type=submit value="Change color">
        </form>
        <button id="dButton">Dispense gumball</button>
        </div>
        
</body>


</html>


5. Webpage CSS

#dButton{
        width: 200px;
        height: 100px;
        font-size: 18px;
        color: black;
        background-color: white;
        border-color: lightgray;
        border-radius: 18px;
        margin:30px 10px;
}

#dButton:hover{
        cursor: pointer;
}

select{
        width: 100px;
        font-size: 18px;
}

#LED_change>input[type=submit]{
        width: 100px;
        height: 40px;
        background-color: white;
        border-color: black;
        border-radius: 4px;
        margin-left: 30px;
}

#LED_change>input[type=submit]:hover{
        cursor: pointer;
}

.centered{
        position: fixed;
        top: 40%;
        left: 40%;
}

.sRed{
        color: red;
}

.sGreen{
        color: green;
}

.sBlue{
        color: blue;
}

.sPurple{
        color: purple;
}

.sOff{
        color: black;
}

body{
        font-family: Verdana;
        font-size: 20px;
}



gray6666  中级技匠

发表于 昨天 10:49

好棒。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
回复 支持 反对

使用道具 举报

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

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
wifi气象站

硬件清单

btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2016 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail