Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3257d40722 | |||
| f9d100f918 | |||
| 0fc20da274 |
@@ -131,7 +131,15 @@ De sterkere speler kan zo winnende zetten vinden die de zwakkere mist. Wie sterk
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Snelle Bediening
|
## 7. Blunder-modus
|
||||||
|
|
||||||
|
Normaal speelt de AI altijd de beste zet die hij kan vinden. Maar dat kan frustrerend zijn voor jongere of minder ervaren spelers die nooit winnen. De **blunder-modus** geeft de AI een instelbare kans (bijvoorbeeld 20%) om een willekeurige zet te doen in plaats van diep na te denken. Als er een blunder gebeurt, slaat de AI zijn slimme analyse over en laat hij een schijfje in een willekeurige open kolom vallen. De rest van de tijd speelt hij gewoon op volle kracht — maar af en toe maakt hij een domme fout die een oplettende speler kan afstraffen.
|
||||||
|
|
||||||
|
Blunders gaan nooit boven een directe winst of blokkade. Als de AI nu kan winnen, of als de tegenstander op het punt staat te winnen, maakt de AI altijd de juiste zet. Blunders vervangen alleen de diepe zoektocht op beurten waar er geen directe dreiging is.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Snelle Bediening
|
||||||
|
|
||||||
De ESP32-C3 heeft maar één kern. Als de AI nadenkt, kan hij de bediening een paar seconden blokkeren.
|
De ESP32-C3 heeft maar één kern. Als de AI nadenkt, kan hij de bediening een paar seconden blokkeren.
|
||||||
Twee trucs zorgen ervoor dat het spel soepel blijft:
|
Twee trucs zorgen ervoor dat het spel soepel blijft:
|
||||||
|
|||||||
@@ -112,7 +112,13 @@ This three-phase approach makes the AI both fast (instant reactions to obvious m
|
|||||||
|
|
||||||
In demo mode, two AI players play against each other. To make the games interesting (rather than always ending in a draw), each player is randomly assigned a different search depth. One player might look 5 moves ahead while the other only looks 3 moves ahead. The stronger player can find winning setups that the weaker one misses, leading to exciting games with real winners. Who gets the advantage is randomized each game.
|
In demo mode, two AI players play against each other. To make the games interesting (rather than always ending in a draw), each player is randomly assigned a different search depth. One player might look 5 moves ahead while the other only looks 3 moves ahead. The stronger player can find winning setups that the weaker one misses, leading to exciting games with real winners. Who gets the advantage is randomized each game.
|
||||||
|
|
||||||
## 7. Responsive Controls
|
## 7. Blunder Mode
|
||||||
|
|
||||||
|
Normally, the AI always plays the best move it can find. But that can be frustrating for younger or casual players who never get to win. **Blunder mode** gives the AI a configurable chance (for example 20%) to make a random move instead of running the deep minimax search. When a blunder happens, the AI simply drops a disc in a random open column. It still plays normally the rest of the time, so the game feels real - but every now and then the AI makes a silly mistake that a sharp player can punish.
|
||||||
|
|
||||||
|
Blunders never override an instant win or block. If the AI can win right now, or if the opponent is about to win, the AI always makes the correct move. Blunders only replace the deep search on turns where there is no immediate threat.
|
||||||
|
|
||||||
|
## 8. Responsive Controls
|
||||||
|
|
||||||
The ESP32-C3 is a single-core processor. When the AI is thinking, it could block all input for several seconds. Two techniques keep the game responsive:
|
The ESP32-C3 is a single-core processor. When the AI is thinking, it could block all input for several seconds. Two techniques keep the game responsive:
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ When idle (no input for the configured timeout), the board enters demo mode wher
|
|||||||
|
|
||||||
The ESP32 creates a WiFi access point:
|
The ESP32 creates a WiFi access point:
|
||||||
|
|
||||||
- **Network:** `Connect4-Config`
|
- **Network:** Configured via `WIFI_SSID` build flag (default: `Connect4`)
|
||||||
- **Password:** Configured via `WIFI_PASSWORD` build flag (default: `youlose4`)
|
- **Password:** Configured via `WIFI_PASSWORD` build flag (default: `youlose4`)
|
||||||
- **Admin page:** Connect to the network and open `http://192.168.4.1`
|
- **Admin page:** Connect to the network and open `http://192.168.4.1`
|
||||||
|
|
||||||
@@ -64,8 +64,7 @@ The ESP32 creates a WiFi access point:
|
|||||||
| **Base AI Ply** | Search depth for the AI (1-10). Higher = stronger. |
|
| **Base AI Ply** | Search depth for the AI (1-10). Higher = stronger. |
|
||||||
| **Brightness** | LED brightness (0-255). |
|
| **Brightness** | LED brightness (0-255). |
|
||||||
| **Idle Timeout** | Seconds of inactivity before demo mode starts. |
|
| **Idle Timeout** | Seconds of inactivity before demo mode starts. |
|
||||||
| **Blunders** | Reserved for future use. |
|
| **Blunders** | AI randomly picks a bad move at the configured chance %. |
|
||||||
| **Evolution** | Progressive difficulty: AI gets stronger as game goes on.|
|
|
||||||
|
|
||||||
Settings are saved to flash (NVS) and persist across reboots.
|
Settings are saved to flash (NVS) and persist across reboots.
|
||||||
|
|
||||||
@@ -100,7 +99,7 @@ pio device monitor
|
|||||||
All configurable parameters are defined as `-D` flags in `platformio.ini`:
|
All configurable parameters are defined as `-D` flags in `platformio.ini`:
|
||||||
|
|
||||||
| Flag | Default | Description |
|
| Flag | Default | Description |
|
||||||
| :--------------------- | :------ | :--------------------------------------------- |
|
| :--------------------- | :--------- | :------------------------------------------------- |
|
||||||
| `LED_PIN` | `4` | GPIO pin for NeoPixel data line |
|
| `LED_PIN` | `4` | GPIO pin for NeoPixel data line |
|
||||||
| `ENC_A` | `0` | GPIO pin for encoder CLK |
|
| `ENC_A` | `0` | GPIO pin for encoder CLK |
|
||||||
| `ENC_B` | `1` | GPIO pin for encoder DT |
|
| `ENC_B` | `1` | GPIO pin for encoder DT |
|
||||||
@@ -110,7 +109,9 @@ All configurable parameters are defined as `-D` flags in `platformio.ini`:
|
|||||||
| `DEFAULT_LOOK_AHEAD` | `8` | Default AI search depth (plies) |
|
| `DEFAULT_LOOK_AHEAD` | `8` | Default AI search depth (plies) |
|
||||||
| `DEFAULT_BRIGHTNESS` | `25` | Default LED brightness (0-255) |
|
| `DEFAULT_BRIGHTNESS` | `25` | Default LED brightness (0-255) |
|
||||||
| `DEFAULT_IDLE_TIMEOUT` | `45` | Seconds before demo mode activates |
|
| `DEFAULT_IDLE_TIMEOUT` | `45` | Seconds before demo mode activates |
|
||||||
|
| `DEMO_RESET_PAUSE` | `30000` | Milliseconds before finished game enters demo |
|
||||||
| `MAX_GAME_LOG` | `5` | Number of games stored in the game log |
|
| `MAX_GAME_LOG` | `5` | Number of games stored in the game log |
|
||||||
|
| `WIFI_SSID` | `Connect4` | SSID for the WiFi access point |
|
||||||
| `WIFI_PASSWORD` | `youlose4` | Password for the WiFi access point |
|
| `WIFI_PASSWORD` | `youlose4` | Password for the WiFi access point |
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|||||||
+1
-3
@@ -12,14 +12,12 @@ build_flags =
|
|||||||
-D ENC_SW=2
|
-D ENC_SW=2
|
||||||
-D SENSITIVITY=4
|
-D SENSITIVITY=4
|
||||||
-D SHOW_BORDER=0
|
-D SHOW_BORDER=0
|
||||||
-D BRIGHTNESS=25
|
|
||||||
-D IDLE_TIMEOUT=45000
|
|
||||||
-D DEMO_RESET_PAUSE=20000
|
-D DEMO_RESET_PAUSE=20000
|
||||||
-D DEBOUNCE_DELAY=50
|
|
||||||
-D DEFAULT_LOOK_AHEAD=8
|
-D DEFAULT_LOOK_AHEAD=8
|
||||||
-D DEFAULT_BRIGHTNESS=25
|
-D DEFAULT_BRIGHTNESS=25
|
||||||
-D DEFAULT_IDLE_TIMEOUT=45
|
-D DEFAULT_IDLE_TIMEOUT=45
|
||||||
-D MAX_GAME_LOG=100
|
-D MAX_GAME_LOG=100
|
||||||
|
-D WIFI_SSID=\"Connect4\"
|
||||||
-D WIFI_PASSWORD=\"youlose4\"
|
-D WIFI_PASSWORD=\"youlose4\"
|
||||||
lib_deps =
|
lib_deps =
|
||||||
fastled/FastLED @ 3.9.12
|
fastled/FastLED @ 3.9.12
|
||||||
|
|||||||
+70
-39
@@ -13,16 +13,48 @@
|
|||||||
#define SENSITIVITY 4
|
#define SENSITIVITY 4
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef LED_PIN
|
||||||
#define LED_PIN 4
|
#define LED_PIN 4
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ENC_A
|
||||||
#define ENC_A 0
|
#define ENC_A 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ENC_B
|
||||||
#define ENC_B 1
|
#define ENC_B 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef ENC_SW
|
||||||
#define ENC_SW 2
|
#define ENC_SW 2
|
||||||
|
#endif
|
||||||
|
|
||||||
#define NUM_LEDS 64
|
#define NUM_LEDS 64
|
||||||
|
|
||||||
#ifndef MAX_GAME_LOG
|
#ifndef MAX_GAME_LOG
|
||||||
#define MAX_GAME_LOG 5
|
#define MAX_GAME_LOG 5
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef DEFAULT_LOOK_AHEAD
|
||||||
|
#define DEFAULT_LOOK_AHEAD 8
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef DEFAULT_BRIGHTNESS
|
||||||
|
#define DEFAULT_BRIGHTNESS 25
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef DEFAULT_IDLE_TIMEOUT
|
||||||
|
#define DEFAULT_IDLE_TIMEOUT 60
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef DEMO_RESET_PAUSE
|
||||||
|
#define DEMO_RESET_PAUSE 30000
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef WIFI_SSID
|
||||||
|
#define WIFI_SSID "Connect4"
|
||||||
|
#endif
|
||||||
|
|
||||||
const int COLS = 7;
|
const int COLS = 7;
|
||||||
const int ROWS = 6;
|
const int ROWS = 6;
|
||||||
const int colOrder[] = {3, 2, 4, 1, 5, 0, 6};
|
const int colOrder[] = {3, 2, 4, 1, 5, 0, 6};
|
||||||
@@ -49,11 +81,11 @@ uint8_t demoPly[2] = {4, 4};
|
|||||||
bool abortAi = false;
|
bool abortAi = false;
|
||||||
bool lastButtonState = HIGH;
|
bool lastButtonState = HIGH;
|
||||||
|
|
||||||
uint8_t currentLookAhead = 6;
|
uint8_t currentLookAhead = DEFAULT_LOOK_AHEAD;
|
||||||
uint8_t currentBrightness = 30;
|
uint8_t currentBrightness = DEFAULT_BRIGHTNESS;
|
||||||
uint32_t currentIdleTimeoutMs = 60000;
|
uint32_t currentIdleTimeoutMs = DEFAULT_IDLE_TIMEOUT * 1000;
|
||||||
bool blunderEnabled = false;
|
bool blunderEnabled = false;
|
||||||
bool progressiveDifficulty = false;
|
uint8_t blunderChance = 20;
|
||||||
|
|
||||||
uint8_t aiBrightness = 0;
|
uint8_t aiBrightness = 0;
|
||||||
bool aiFadeUp = true;
|
bool aiFadeUp = true;
|
||||||
@@ -84,7 +116,6 @@ void renderBoard();
|
|||||||
void showMenu();
|
void showMenu();
|
||||||
int getFirstEmptyRow(int col);
|
int getFirstEmptyRow(int col);
|
||||||
bool isBoardFull();
|
bool isBoardFull();
|
||||||
int getDynamicPly();
|
|
||||||
int8_t scanBoard();
|
int8_t scanBoard();
|
||||||
bool checkGameEnd();
|
bool checkGameEnd();
|
||||||
void updateThinkingVisuals(int8_t pColor, int8_t column);
|
void updateThinkingVisuals(int8_t pColor, int8_t column);
|
||||||
@@ -217,13 +248,6 @@ bool isBoardFull() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int getDynamicPly() {
|
|
||||||
if (!progressiveDifficulty && gameState != DEMO) return currentLookAhead;
|
|
||||||
int count = 0;
|
|
||||||
for (int c = 0; c < COLS; c++) for (int r = 0; r < ROWS; r++) if (board[c][r] != 0) count++;
|
|
||||||
return constrain(currentLookAhead + (count / 7), 1, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
int8_t scanBoard() {
|
int8_t scanBoard() {
|
||||||
memset(winMask, 0, sizeof(winMask));
|
memset(winMask, 0, sizeof(winMask));
|
||||||
auto check = [&](int c, int r, int dc, int dr) {
|
auto check = [&](int c, int r, int dc, int dr) {
|
||||||
@@ -243,22 +267,15 @@ int8_t scanBoard() {
|
|||||||
|
|
||||||
bool checkGameEnd() {
|
bool checkGameEnd() {
|
||||||
winnerPlayer = scanBoard();
|
winnerPlayer = scanBoard();
|
||||||
if (winnerPlayer != 0) {
|
bool won = winnerPlayer != 0;
|
||||||
if (gameState != DEMO) logGame(winnerPlayer);
|
bool draw = !won && isBoardFull();
|
||||||
gameState = FINISHED_WIN;
|
if (!won && !draw) return false;
|
||||||
|
if (gameState != DEMO) logGame(won ? winnerPlayer : 0);
|
||||||
|
gameState = won ? FINISHED_WIN : FINISHED_DRAW;
|
||||||
demoResetTimer = millis();
|
demoResetTimer = millis();
|
||||||
lastActivityTime = millis();
|
lastActivityTime = millis();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (isBoardFull()) {
|
|
||||||
if (gameState != DEMO) logGame(0);
|
|
||||||
gameState = FINISHED_DRAW;
|
|
||||||
demoResetTimer = millis();
|
|
||||||
lastActivityTime = millis();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Animation ---
|
// --- Animation ---
|
||||||
|
|
||||||
@@ -337,18 +354,31 @@ void performAiMove(int8_t aiP) {
|
|||||||
int huP = (aiP == 1) ? 2 : 1;
|
int huP = (aiP == 1) ? 2 : 1;
|
||||||
int bestScore = -30000; int bestCol = 3;
|
int bestScore = -30000; int bestCol = 3;
|
||||||
int originalPly = currentLookAhead;
|
int originalPly = currentLookAhead;
|
||||||
currentLookAhead = (gameState == DEMO) ? demoPly[aiP - 1] : getDynamicPly();
|
if (gameState == DEMO) currentLookAhead = demoPly[aiP - 1];
|
||||||
|
|
||||||
for (int c = 0; c < COLS; c++) {
|
// Phase 1: always take an instant win or block an opponent's win
|
||||||
|
bool found = false;
|
||||||
|
for (int c = 0; c < COLS && !found; c++) {
|
||||||
int r = getFirstEmptyRow(c);
|
int r = getFirstEmptyRow(c);
|
||||||
if (r != -1) {
|
if (r != -1) {
|
||||||
board[c][r] = aiP; if (scanBoard() == aiP) { board[c][r]=0; bestCol=c; goto finalizeMove; }
|
board[c][r] = aiP; if (scanBoard() == aiP) { board[c][r]=0; bestCol=c; found=true; break; }
|
||||||
board[c][r] = huP; if (scanBoard() == huP) { board[c][r]=0; bestCol=c; goto finalizeMove; }
|
board[c][r] = huP; if (scanBoard() == huP) { board[c][r]=0; bestCol=c; found=true; break; }
|
||||||
board[c][r] = 0;
|
board[c][r] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2: blunder — pick a random column instead of deep search
|
||||||
|
if (!found && blunderEnabled && gameState != DEMO && (random(100) < blunderChance)) {
|
||||||
|
int validCols[COLS], count = 0;
|
||||||
|
for (int c = 0; c < COLS; c++) if (getFirstEmptyRow(c) != -1) validCols[count++] = c;
|
||||||
|
bestCol = validCols[random(count)];
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: deep minimax search
|
||||||
|
if (!found) {
|
||||||
for (int c : colOrder) {
|
for (int c : colOrder) {
|
||||||
if (abortAi) goto finalizeMove;
|
if (abortAi) break;
|
||||||
int r = getFirstEmptyRow(c);
|
int r = getFirstEmptyRow(c);
|
||||||
if (r != -1) {
|
if (r != -1) {
|
||||||
board[c][r] = aiP;
|
board[c][r] = aiP;
|
||||||
@@ -357,7 +387,8 @@ void performAiMove(int8_t aiP) {
|
|||||||
if (score > bestScore) { bestScore = score; bestCol = c; }
|
if (score > bestScore) { bestScore = score; bestCol = c; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finalizeMove:
|
}
|
||||||
|
|
||||||
currentLookAhead = originalPly;
|
currentLookAhead = originalPly;
|
||||||
if (!abortAi) { moveDiscToCol(activeCol, bestCol, aiP, 80); if (!abortAi) { delay(100); animateDrop(bestCol, aiP); } }
|
if (!abortAi) { moveDiscToCol(activeCol, bestCol, aiP, 80); if (!abortAi) { delay(100); animateDrop(bestCol, aiP); } }
|
||||||
}
|
}
|
||||||
@@ -377,8 +408,8 @@ void handleRoot() {
|
|||||||
html += "Base AI Ply:<input type='number' name='ply' value='" + String(currentLookAhead) + "'>";
|
html += "Base AI Ply:<input type='number' name='ply' value='" + String(currentLookAhead) + "'>";
|
||||||
html += "Brightness:<input type='number' name='br' value='" + String(currentBrightness) + "'>";
|
html += "Brightness:<input type='number' name='br' value='" + String(currentBrightness) + "'>";
|
||||||
html += "Idle Timeout (s):<input type='number' name='idle' value='" + String(currentIdleTimeoutMs / 1000) + "'>";
|
html += "Idle Timeout (s):<input type='number' name='idle' value='" + String(currentIdleTimeoutMs / 1000) + "'>";
|
||||||
html += "Blunders: <input type='checkbox' name='blunder' " + String(blunderEnabled ? "checked" : "") + "><br>";
|
html += "Blunders: <input type='checkbox' name='blunder' " + String(blunderEnabled ? "checked" : "") + ">";
|
||||||
html += "Evolution: <input type='checkbox' name='evolve' " + String(progressiveDifficulty ? "checked" : "") + "><br><br>";
|
html += " Chance (%):<input type='number' name='blunderPct' min='1' max='100' value='" + String(blunderChance) + "'><br><br>";
|
||||||
html += "<input type='submit' value='Save Settings' style='background:#28a745;color:white;'>";
|
html += "<input type='submit' value='Save Settings' style='background:#28a745;color:white;'>";
|
||||||
html += "</form></div>";
|
html += "</form></div>";
|
||||||
html += "<div class='card' style='margin-top:15px;text-align:left;'><h3 style='text-align:center;'>Game Log</h3>";
|
html += "<div class='card' style='margin-top:15px;text-align:left;'><h3 style='text-align:center;'>Game Log</h3>";
|
||||||
@@ -405,7 +436,7 @@ void handleSave() {
|
|||||||
if (server.hasArg("br")) { currentBrightness = server.arg("br").toInt(); FastLED.setBrightness(currentBrightness); prefs.putUChar("br", currentBrightness); }
|
if (server.hasArg("br")) { currentBrightness = server.arg("br").toInt(); FastLED.setBrightness(currentBrightness); prefs.putUChar("br", currentBrightness); }
|
||||||
if (server.hasArg("idle")) { currentIdleTimeoutMs = server.arg("idle").toInt() * 1000; prefs.putUInt("idle", currentIdleTimeoutMs / 1000); }
|
if (server.hasArg("idle")) { currentIdleTimeoutMs = server.arg("idle").toInt() * 1000; prefs.putUInt("idle", currentIdleTimeoutMs / 1000); }
|
||||||
blunderEnabled = server.hasArg("blunder"); prefs.putBool("blunder", blunderEnabled);
|
blunderEnabled = server.hasArg("blunder"); prefs.putBool("blunder", blunderEnabled);
|
||||||
progressiveDifficulty = server.hasArg("evolve"); prefs.putBool("evolve", progressiveDifficulty);
|
if (server.hasArg("blunderPct")) { blunderChance = constrain(server.arg("blunderPct").toInt(), 1, 100); prefs.putUChar("blPct", blunderChance); }
|
||||||
server.sendHeader("Location", "/"); server.send(303);
|
server.sendHeader("Location", "/"); server.send(303);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,7 +516,7 @@ void handleFinished() {
|
|||||||
}
|
}
|
||||||
FastLED.show();
|
FastLED.show();
|
||||||
}
|
}
|
||||||
if (millis() - demoResetTimer > 30000) {
|
if (millis() - demoResetTimer > DEMO_RESET_PAUSE) {
|
||||||
resetBoard();
|
resetBoard();
|
||||||
randomizeDemoPlies();
|
randomizeDemoPlies();
|
||||||
gameState = DEMO;
|
gameState = DEMO;
|
||||||
@@ -498,16 +529,16 @@ void handleFinished() {
|
|||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
prefs.begin("c4-game", false);
|
prefs.begin("c4-game", false);
|
||||||
currentLookAhead = prefs.getUChar("ply", 8);
|
currentLookAhead = prefs.getUChar("ply", DEFAULT_LOOK_AHEAD);
|
||||||
currentBrightness = prefs.getUChar("br", 25);
|
currentBrightness = prefs.getUChar("br", DEFAULT_BRIGHTNESS);
|
||||||
currentIdleTimeoutMs = prefs.getUInt("idle", 60) * 1000;
|
currentIdleTimeoutMs = prefs.getUInt("idle", DEFAULT_IDLE_TIMEOUT) * 1000;
|
||||||
blunderEnabled = prefs.getBool("blunder", false);
|
blunderEnabled = prefs.getBool("blunder", false);
|
||||||
progressiveDifficulty = prefs.getBool("evolve", false);
|
blunderChance = prefs.getUChar("blPct", 20);
|
||||||
loadGameLog();
|
loadGameLog();
|
||||||
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
|
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
|
||||||
FastLED.setBrightness(currentBrightness);
|
FastLED.setBrightness(currentBrightness);
|
||||||
pinMode(ENC_SW, INPUT_PULLUP);
|
pinMode(ENC_SW, INPUT_PULLUP);
|
||||||
WiFi.softAP("Connect4-Config", WIFI_PASSWORD);
|
WiFi.softAP(WIFI_SSID, WIFI_PASSWORD);
|
||||||
server.on("/", handleRoot);
|
server.on("/", handleRoot);
|
||||||
server.on("/save", HTTP_POST, handleSave);
|
server.on("/save", HTTP_POST, handleSave);
|
||||||
server.begin();
|
server.begin();
|
||||||
|
|||||||
Reference in New Issue
Block a user