#include #include #include #include #include #include #include #include #include #ifdef __AVR__ #include #endif //The GPIO pin for the WS2812 leds #define DATA_PIN 14 // Total number of leds on the string #define NUM_LEDS 8 // Total number of targets to allocate memory for #define TARGET_MAX 8 // Web scoreboard keeps a history of hits, this is how many to store #define HIT_HISTORY_COUNT 21 // Game name to number maps #define ALL 0 #define AROUND 1 #define BLACKOUT 2 #define MOLE 3 // MDNS name. Allows you to connect to nerf.local in the web browser // instead of remembering the IP const char* mDNSid = "nerf"; // The server's AP name const char* AP_NAME = "NERF"; // AP password const char* AP_PASS = ""; // AP channel (not sure if this works, I haven't had success) uint8_t AP_CHANNEL = 3; // Should we broadcast our SSID? 0 = YES, 1 = NO // Probably good to leave this at 0 so web based clients can // find the scoreboard. The target clients have hard coded // AP name so they should be fine uint8_t AP_HIDDEN = 0; // websockets are used for updating all data for both targets // and web scoreboards WebSocketsServer webSocket = WebSocketsServer(81); // web server is used to serve html/js to web scoreboard ESP8266WebServer server(80); // FastLED used for WS2811 CRGB leds[NUM_LEDS]; // Used to store count of targets that we currently have uint8_t totalTargets = 0; // Each targets attributes struct Target { uint32_t ID; uint32_t Color; uint32_t Hits; String Name; }; // Array of targets struct Target knownTargets[TARGET_MAX]; uint32_t hitHistory[HIT_HISTORY_COUNT]; uint8_t currentGame = ALL; uint8_t moleDelay = 4; uint32_t gameTimer; uint8_t gameCurrentTarget; uint8_t blackoutRemain; // shift all previous recorded hits one to the right void shiftHitHistory() { uint8_t totalTargets; for (int i=HIT_HISTORY_COUNT-1; i>0; i--) { if (hitHistory[i-1]) { hitHistory[i] = hitHistory[i-1]; } } } // identify file by extension and return appropriate content-type String getContentType(String filename) { if (server.hasArg("download")) return "application/octet-stream"; else if (filename.endsWith(".htm")) return "text/html"; else if (filename.endsWith(".html")) return "text/html"; else if (filename.endsWith(".css")) return "text/css"; else if (filename.endsWith(".js")) return "application/javascript"; else if (filename.endsWith(".png")) return "image/png"; else if (filename.endsWith(".gif")) return "image/gif"; else if (filename.endsWith(".jpg")) return "image/jpeg"; else if (filename.endsWith(".ico")) return "image/x-icon"; else if (filename.endsWith(".xml")) return "text/xml"; else if (filename.endsWith(".pdf")) return "application/x-pdf"; else if (filename.endsWith(".zip")) return "application/x-zip"; else if (filename.endsWith(".gz")) return "text/plain\r\nCache-Control: public, max-age=3600"; return "text/plain"; } bool handleFileRead(String path) { Serial.println("handleFileRead: " + path); if (path.endsWith("/")) path += "index.htm"; String contentType = getContentType(path); String pathWithGz = path + ".gz"; if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) { if (SPIFFS.exists(pathWithGz)) path += ".gz"; File file = SPIFFS.open(path, "r"); size_t sent = server.streamFile(file, contentType); file.close(); return true; } return false; } //format bytes String formatBytes(size_t bytes) { if (bytes < 1024) { return String(bytes)+"B"; } else if (bytes < (1024 * 1024)) { return String(bytes/1024.0)+"KB"; } else if (bytes < (1024 * 1024 * 1024)) { return String(bytes/1024.0/1024.0)+"MB"; } else { return String(bytes/1024.0/1024.0/1024.0)+"GB"; } } uint32_t x2i(char *s) { int x = 0; for(;;) { char c = *s; if (c >= '0' && c <= '9') { x *= 16; x += c - '0'; } else if (c >= 'A' && c <= 'F') { x *= 16; x += (c - 'A') + 10; } else if (c >= 'a' && c <= 'f') { x *= 16; x += (c - 'a') + 10; } else break; s++; } return x; } // Spews out all of our current info via json. This is read by any web scoreboard // and used to display the current state of our game and target info. void wsSendJson() { // At some point this 1000 byte buffer might not be large enough StaticJsonBuffer<1000> jsonBuffer; JsonObject& root = jsonBuffer.createObject(); root["clientTotal"] = totalTargets; root["currentGame"] = currentGame; root["moleDelay"] = moleDelay; if (totalTargets > 0) { JsonArray& clients = root.createNestedArray("clients"); for (int i=0; i> 16; long g = newColor >> 8 & 0xFF; long b = newColor & 0xFF; */ byte knowThisGuy = -1; for (int i=0; i= 0) { knownTargets[knowThisGuy].Name = name; //Serial.print("CNAME2 "); //Serial.println(clientName[knowThisGuy]); String out = "N"; out += knownTargets[knowThisGuy].ID; out += "I"; out += knownTargets[knowThisGuy].Name; out += "*"; webSocket.broadcastTXT(out); } } else if (payload[0] == 'H') { Serial.println("Registered hit"); // Parse the incoming message char cPayload[length]; memcpy(cPayload, payload, length); //String temp2 = temp; //Serial.println(temp); String id; String force; int i = 1; while (cPayload[i] != 'F') { id += cPayload[i]; i++; } i++; while (cPayload[i] != '*') { force += cPayload[i]; i++; } //Serial.println(id); //Serial.println(force); byte knowThisGuy = -1; for (int i=0; i 1) { leds[knowThisGuy] = CRGB::Black; FastLED.show(); blackoutRemain--; Serial.print("BLACKOUT REMAIN IS: "); Serial.println(blackoutRemain); } else { Serial.println("BLACKOUT!"); blackoutRemain = totalTargets; for (int i=0; i < 4; i++) { enableAll(); delay(100); disableAll(); delay(100); } enableAll(); String out = "G"; out += BLACKOUT; webSocket.broadcastTXT(out); } } } else if (payload[0] == 'X') { Serial.println("RESET HIT COUNTER EVENT"); // Parse the incoming message char cPayload[length]; memcpy(cPayload, payload, length); String id; int i = 1; while (cPayload[i] != '*') { id += cPayload[i]; i++; } Serial.println(id); byte knowThisGuy = -1; for (int i=0; i (moleDelay * 1000)) { Serial.println("MOLE TIMER EXPIRED"); disableAll(); fill_solid(leds, NUM_LEDS, CRGB::Black); // Choose the next target but make sure it's not the same as the previous int nextTarget = ESP8266TrueRandom.random(0, totalTargets); while (nextTarget == gameCurrentTarget) { Serial.println("RNG got the same number as before, re-roll"); nextTarget = ESP8266TrueRandom.random(0, totalTargets); } gameCurrentTarget = nextTarget; leds[nextTarget] = knownTargets[nextTarget].Color; FastLED.show(); Serial.print("SETTING NEXT TARGET="); Serial.println(nextTarget); gameTimer = millis(); String out = "A"; out += knownTargets[nextTarget].ID; out += "I"; webSocket.broadcastTXT(out); } } } // Tell all targets they are enabled for the current game void enableAll() { String out = "Y"; webSocket.broadcastTXT(out); for (int i=0; i(leds, NUM_LEDS); // If FastLED green/red are backwards //FastLED.addLeds(leds, NUM_LEDS); // Turn all leds off fill_solid(leds, NUM_LEDS, CRGB::Black); FastLED.show(); // Init our file system int test = SPIFFS.begin(); Serial.println(test); delay(100); { Dir dir = SPIFFS.openDir("/"); while (dir.next()) { String fileName = dir.fileName(); size_t fileSize = dir.fileSize(); Serial.printf("FS File: %s, size: %s\r\n", fileName.c_str(), formatBytes(fileSize).c_str()); } Serial.printf("\n"); } // Setup 404 handle on the web server server.onNotFound([]() { if (!handleFileRead(server.uri())) server.send(404, "text/plain", "FileNotFound"); }); // Start web server server.begin(); Serial.println("HTTP server started"); // Add service to MDNS if (!MDNS.begin("nerf", WiFi.localIP())) { Serial.println("Error setting up MDNS responder!"); // If we get here we spinlock and never recover. Something more graceful // should probably happen while(1) { delay(1000); } } Serial.println("mDNS responder started"); MDNS.addService("http", "tcp", 80); MDNS.addService("ws", "tcp", 81); } void loop() { webSocket.loop(); server.handleClient(); runMoleTimer(); }