From 93cb46eee335286ee03588bccc9d4474ae059a75 Mon Sep 17 00:00:00 2001 From: TwoSquirrels Date: Tue, 16 Jul 2024 02:27:01 +0900 Subject: [PATCH] [WIP] Add M5Atom Car example --- example/m5atom-car/.gitignore | 3 + example/m5atom-car/README.md | 3 + example/m5atom-car/SimpleHTTPServer.h | 133 ++++++++++++++++++++++++++ example/m5atom-car/gen-index-html.bat | 12 +++ example/m5atom-car/gen-index-html.sh | 11 +++ example/m5atom-car/m5atom-car.ino | 84 ++++++++++++++++ package.json | 3 +- pnpm-lock.yaml | 61 ++++++++++++ src/index.html | 6 +- src/main.js | 4 +- vite.config.js | 3 + 11 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 example/m5atom-car/.gitignore create mode 100644 example/m5atom-car/README.md create mode 100644 example/m5atom-car/SimpleHTTPServer.h create mode 100644 example/m5atom-car/gen-index-html.bat create mode 100644 example/m5atom-car/gen-index-html.sh create mode 100644 example/m5atom-car/m5atom-car.ino diff --git a/example/m5atom-car/.gitignore b/example/m5atom-car/.gitignore new file mode 100644 index 0000000..5603bfe --- /dev/null +++ b/example/m5atom-car/.gitignore @@ -0,0 +1,3 @@ +.env.h +index.html +index-html.h diff --git a/example/m5atom-car/README.md b/example/m5atom-car/README.md new file mode 100644 index 0000000..331c9de --- /dev/null +++ b/example/m5atom-car/README.md @@ -0,0 +1,3 @@ +# M5Atom WiFi Control Car Example + +TODO: 説明を書く diff --git a/example/m5atom-car/SimpleHTTPServer.h b/example/m5atom-car/SimpleHTTPServer.h new file mode 100644 index 0000000..2ad66ef --- /dev/null +++ b/example/m5atom-car/SimpleHTTPServer.h @@ -0,0 +1,133 @@ +// SimpleHTTPServer written by ChatGPT + +#include + +#define MAX_HANDLERS (16) + +class SimpleHTTPServer { +private: + struct Handler { + const char* path; + void (*handler)(WiFiClient&, const String&); + }; + + const char* ssid; + const char* password; + WiFiServer server; + Handler getHandlers[MAX_HANDLERS]; + Handler postHandlers[MAX_HANDLERS]; + int getHandlerCount; + int postHandlerCount; + +public: + SimpleHTTPServer(const char* ssid, const char* password, int port = 80) + : ssid(ssid), password(password), server(port), getHandlerCount(0), postHandlerCount(0) {} + + void begin() { + Serial.begin(115200); + connectToWiFi(); + server.begin(); + Serial.println("Server started"); + Serial.print("IP Address: "); + Serial.println(WiFi.localIP()); + } + + void handleClient() { + WiFiClient client = server.available(); + if (client) { + Serial.println("New Client."); + String currentLine = ""; + String requestType = ""; + String path = ""; + String queryString = ""; + + while (client.connected()) { + if (client.available()) { + char c = client.read(); + Serial.write(c); + if (c == '\n') { + if (currentLine.length() == 0) { + if (requestType == "GET") { + handleRequest(path, queryString, client, getHandlers, getHandlerCount); + } else if (requestType == "POST") { + handleRequest(path, queryString, client, postHandlers, postHandlerCount); + } + break; + } else { + currentLine = ""; + } + } else if (c != '\r') { + currentLine += c; + if (currentLine.startsWith("GET ")) { + requestType = "GET"; + path = extractPathAndQuery(currentLine, queryString); + } else if (currentLine.startsWith("POST ")) { + requestType = "POST"; + path = extractPathAndQuery(currentLine, queryString); + } + } + } + } + client.stop(); + Serial.println("Client Disconnected."); + } + } + + void get(const char* path, void (*handler)(WiFiClient&, const String&)) { + if (getHandlerCount < MAX_HANDLERS) { + getHandlers[getHandlerCount++] = { path, handler }; + } + } + + void post(const char* path, void (*handler)(WiFiClient&, const String&)) { + if (postHandlerCount < MAX_HANDLERS) { + postHandlers[postHandlerCount++] = { path, handler }; + } + } + +private: + void connectToWiFi() { + Serial.print("Connecting to "); + Serial.println(ssid); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) { + delay(1000); + Serial.print("."); + } + Serial.println(""); + Serial.println("WiFi connected."); + } + + String extractPathAndQuery(const String& requestLine, String& queryString) { + int firstSpace = requestLine.indexOf(' '); + int secondSpace = requestLine.indexOf(' ', firstSpace + 1); + String fullPath = requestLine.substring(firstSpace + 1, secondSpace); + + int queryIndex = fullPath.indexOf('?'); + if (queryIndex != -1) { + queryString = fullPath.substring(queryIndex + 1); + return fullPath.substring(0, queryIndex); + } else { + queryString = ""; + return fullPath; + } + } + + void handleRequest(const String& path, const String& queryString, WiFiClient& client, Handler* handlers, int handlerCount) { + for (int i = 0; i < handlerCount; i++) { + if (path == handlers[i].path) { + handlers[i].handler(client, queryString); + return; + } + } + defaultResponse(client); + } + + void defaultResponse(WiFiClient& client) { + client.println("HTTP/1.1 404 Not Found"); + client.println("Content-type:text/html"); + client.println(); + client.print("

404 Not Found

"); + client.println(); + } +}; diff --git a/example/m5atom-car/gen-index-html.bat b/example/m5atom-car/gen-index-html.bat new file mode 100644 index 0000000..cf43ef9 --- /dev/null +++ b/example/m5atom-car/gen-index-html.bat @@ -0,0 +1,12 @@ +@echo off +setlocal EnableDelayedExpansion + +powershell -Command "(New-Object Net.WebClient).DownloadFile('https://twosquirrels.github.io/virtual-gamepad/', 'index.html')" + +( + echo const char index_html[] = R"***( + type index.html + echo )***"; +) > index-html.h + +echo index-html.h has been created successfully. diff --git a/example/m5atom-car/gen-index-html.sh b/example/m5atom-car/gen-index-html.sh new file mode 100644 index 0000000..9e19d38 --- /dev/null +++ b/example/m5atom-car/gen-index-html.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +curl -o index.html https://twosquirrels.github.io/virtual-gamepad/ + +{ + echo 'const char index_html[] = R"***(' + cat index.html + echo ')***";' +} > index-html.h + +echo "index-html.h has been created successfully." diff --git a/example/m5atom-car/m5atom-car.ino b/example/m5atom-car/m5atom-car.ino new file mode 100644 index 0000000..d816d2f --- /dev/null +++ b/example/m5atom-car/m5atom-car.ino @@ -0,0 +1,84 @@ +// WiFi Control Car Example + +#include "SimpleHTTPServer.h" +#include + +#define LEDright (22) +#define LEDleft (19) +#define MotorA (23) +#define MotorB (33) + +const double PWM_freq = 2000; // PWM周波数. +const uint8_t PWM_res = 8; // PWM分解能 16bit(0~256). +const uint8_t PWM_CH_A = 1; // チャンネル. +const uint8_t PWM_CH_B = 2; // チャンネル. + +#if __has_include(".env.h") +# include ".env.h" +#else +const char ssid[] = "YOUR_SSID"; +const char password[] = "YOUR_PASSWORD"; +#endif + +#if __has_include("index-html.h") +# include "index-html.h" +#else +const char index_html[] = "

There is not index-html.h

"; +#endif + +SimpleHTTPServer server(ssid, password); + +void handleRoot(WiFiClient& client, const String& query) { + client.println("HTTP/1.1 200 OK"); + client.println("Content-Type: text/html"); + client.println(); + client.println(index_html); +} + +void respondOK(WiFiClient& client) { + client.println("HTTP/1.1 200 OK"); + client.println("Content-Type: text/plain"); + client.println(); + client.println("OK"); +} + +void handlePostA(WiFiClient& client, const String& query) { + respondOK(client); + Serial.println("======== Pushed A button! ========"); +} + +void handlePostB(WiFiClient& client, const String& query) { + respondOK(client); + Serial.println("======== Pushed B button! ========"); +} + +void setup() { + M5.begin(false, false, true); + Serial.begin(115200); + M5.dis.drawpix(0, 0x0000FF); + pinMode(LEDright, OUTPUT); + pinMode(LEDleft, OUTPUT); + pinMode(MotorA, OUTPUT); + pinMode(MotorB, OUTPUT); + //チャンネル1と周波数の分解能を設定. + ledcSetup(PWM_CH_A, PWM_freq, PWM_res); + //チャンネル2と周波数の分解能を設定. + ledcSetup(PWM_CH_B, PWM_freq, PWM_res); + + //モータのピンとチャンネルの設定. + ledcAttachPin(MotorA, PWM_CH_A); + ledcAttachPin(MotorB, PWM_CH_B); + + delay(10); + + server.begin(); + + server.get("/", handleRoot); + server.post("/a", handlePostA); + server.post("/b", handlePostB); +} + +void loop() { + server.handleClient(); + M5.update(); +} diff --git a/package.json b/package.json index b6a642b..5d91c66 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "preview": "vite preview" }, "devDependencies": { - "vite": "^5.3.1" + "vite": "^5.3.1", + "vite-plugin-singlefile": "^2.0.2" }, "dependencies": { "nipplejs": "^0.10.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28d7297..5bedae3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: vite: specifier: ^5.3.1 version: 5.3.3 + vite-plugin-singlefile: + specifier: ^2.0.2 + version: 2.0.2(rollup@4.18.1)(vite@5.3.3) packages: @@ -242,16 +245,32 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} hasBin: true + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -263,6 +282,10 @@ packages: picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + postcss@8.4.39: resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==} engines: {node: ^10 || ^12 || >=14} @@ -279,6 +302,17 @@ packages: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + vite-plugin-singlefile@2.0.2: + resolution: {integrity: sha512-Z2ou6HcvED5CF0hM+vcFSaFa+klyS8RyyLxW0PbMRLnMbvzTI6ueWyxdYNFhpuXZgz/aj6+E/dHFTdEcw6gb9w==} + engines: {node: '>18.0.0'} + peerDependencies: + rollup: ^4.18.0 + vite: ^5.3.1 + vite@5.3.3: resolution: {integrity: sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==} engines: {node: ^18.0.0 || >=20.0.0} @@ -428,6 +462,10 @@ snapshots: '@types/estree@1.0.5': {} + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -454,15 +492,28 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + fsevents@2.3.3: optional: true + is-number@7.0.0: {} + + micromatch@4.0.7: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + nanoid@3.3.7: {} nipplejs@0.10.2: {} picocolors@1.0.1: {} + picomatch@2.3.1: {} + postcss@8.4.39: dependencies: nanoid: 3.3.7 @@ -495,6 +546,16 @@ snapshots: source-map-js@1.2.0: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + vite-plugin-singlefile@2.0.2(rollup@4.18.1)(vite@5.3.3): + dependencies: + micromatch: 4.0.7 + rollup: 4.18.1 + vite: 5.3.3 + vite@5.3.3: dependencies: esbuild: 0.21.5 diff --git a/src/index.html b/src/index.html index 2581969..4f200fb 100644 --- a/src/index.html +++ b/src/index.html @@ -2,7 +2,7 @@ - + Virtual Gamepad @@ -25,11 +25,11 @@

Settings

- +

- ms + ms

diff --git a/src/main.js b/src/main.js index 2281333..749275d 100644 --- a/src/main.js +++ b/src/main.js @@ -30,7 +30,7 @@ for (const burger of elms.hamburgers) { /// poster -let host = params.get("host") ?? "http://localhost:3000"; +let host = params.get("host") ?? window.location.origin; let interval = Number(params.get("interval")) || 125; let showButtons = true; @@ -85,12 +85,14 @@ const joystickPoster = { /// settings elms.settings.host.value = host; +elms.settings.host.placeholder = host; elms.settings.host.oninput = ({ target: input }) => { console.log(`Modify host ${host} -> ${input.value}`); host = input.value; }; elms.settings.interval.value = interval; +elms.settings.interval.placeholder = interval; elms.settings.interval.oninput = ({ target: input }) => { console.log(`Modify interval ${interval} -> ${input.value}`); interval = input.value; diff --git a/vite.config.js b/vite.config.js index 7309873..0110462 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,3 +1,5 @@ +import { viteSingleFile } from "vite-plugin-singlefile"; + export default { base: process.env.GITHUB_PAGES ? "REPOSITORY_NAME" : "./", root: "./src", @@ -6,4 +8,5 @@ export default { outDir: "../dist", emptyOutDir: true, }, + plugins: [viteSingleFile()], };