7999| 4
|
ULTIMATE口香糖机 |
本帖最后由 粒子 于 2018-8-6 18:47 编辑 LED、WiFi、自动弹出、LCD屏幕都备齐了。 这款口香糖球机能与客户通过网页互动。 ![]() 硬件部件 2.8英寸TFT触摸屏,带4MB闪存,用于Arduino和medde 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; } |
© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed