查看: 2020|回复: 2
打印 上一主题 下一主题

ULTIMATE口香糖机

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


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


硬件部件

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功能?所有这些都在一台口香糖球机里,怎么样?



设计

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




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


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

外壳和制造

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


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


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



网页

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



Web服务器

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


电子器件

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



软件

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

使用方法

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



代码

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  高级技匠

发表于 2018-8-14 10:49:48

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

使用道具 举报

板凳

gada888  版主

发表于 2018-8-25 22:00:33

好强的教程
回复 支持 反对

使用道具 举报

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

本版积分规则

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

硬件清单

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

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

mail