+
+#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
+
+void updateMoters(double angle, double force);
+
+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 respondBad(WiFiClient& client) {
+ client.println("HTTP/1.1 200 OK");
+ client.println("Content-Type: text/plain");
+ client.println();
+ client.println("OK");
+}
+
+void handlePostJoystick(WiFiClient& client, const String& query) {
+ if (query.length() != 6) {
+ respondBad(client);
+ return;
+ }
+ const int angle = strtol(query.substring(2, 4).c_str(), NULL, 16);
+ const int force = strtol(query.substring(4, 6).c_str(), NULL, 16);
+ updateMoters(angle * 360.0 / 256.0, force / 255.0);
+ Serial.print("======== Joystick (angle=");
+ Serial.print(angle);
+ Serial.print(", force=");
+ Serial.print(force);
+ Serial.println(") ========");
+ respondOk(client);
+}
+
+void handlePostA(WiFiClient& client, const String& query) {
+ M5.dis.drawpix(0, 0xFF0000);
+ Serial.println("======== Pushed A button! ========");
+ respondOk(client);
+}
+
+void handlePostB(WiFiClient& client, const String& query) {
+ M5.dis.drawpix(0, 0x00FF00);
+ Serial.println("======== Pushed B button! ========");
+ respondOk(client);
+}
+
+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);
+
+ // LED とモータのピンとチャンネルの設定.
+ ledcAttachPin(LEDright, PWM_CH_A);
+ ledcAttachPin(LEDleft, PWM_CH_B);
+ ledcAttachPin(MotorA, PWM_CH_A);
+ ledcAttachPin(MotorB, PWM_CH_B);
+
+ delay(10);
+
+ server.begin();
+
+ server.get("/", handleRoot);
+ server.post("/joystick", handlePostJoystick);
+ server.post("/a", handlePostA);
+ server.post("/b", handlePostB);
+}
+
+double moterRight = 0.0, moterLeft = 0.0;
+
+unsigned long prevTime = millis();
+
+void loop() {
+ const unsigned long deltaTime = millis() - prevTime;
+ prevTime = millis();
+
+ if (moterRight >= 0.0) moterRight = max(moterRight - deltaTime / 1000.0, 0.0);
+ else moterRight = min(moterRight + deltaTime / 250.0, 0.0);
+ if (moterLeft >= 0.0) moterLeft = max(moterLeft - deltaTime / 1000.0, 0.0);
+ else moterLeft = min(moterLeft + deltaTime / 250.0, 0.0);
+
+ server.handleClient();
+
+ ledcWrite(PWM_CH_A, (uint16_t)(sqrt(max(0.0, moterRight)) * 255));
+ ledcWrite(PWM_CH_B, (uint16_t)(sqrt(max(0.0, moterLeft)) * 255));
+
+ M5.update();
+}
+
+void updateMoters(double angle, double force) {
+ if (angle < 15 || angle >= 345) {
+ moterRight = -0.5;
+ moterLeft = 0.5;
+ } else if (angle >= 165 && angle < 195) {
+ moterRight = 0.5;
+ moterLeft = -0.5;
+ } else if (angle < 90) {
+ moterRight = constrain(angle / 40 - 0.875, -0.5, 1.0);
+ moterLeft = constrain(angle / 40 + 0.125, 0.5, 1.0);
+ } else if (angle < 180) {
+ moterRight = constrain(-angle / 40 + 4.625, 0.5, 1.0);
+ moterLeft = constrain(-angle / 40 + 3.625, -0.5, 1.0);
+ } else if (angle < 270) {
+ //moterRight = constrain(-angle / 40 + 4.375, -1.0, -0.5);
+ //moterLeft = constrain(-angle / 40 + 5.375, -1.0, 0.5);
+ moterRight = angle < 255 ? 0.5 : -0.5;
+ moterLeft = -0.5;
+ } else {
+ //moterRight = constrain(angle / 40 - 8.125, -1.0, 0.5);
+ //moterLeft = constrain(angle / 40 - 9.125, -1.0, -0.5);
+ moterRight = -0.5;
+ moterLeft = angle < 285 ? -0.5 : 0.5;
+ }
+
+ moterRight *= force;
+ moterLeft *= force;
+}
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..700c4d7 100644
--- a/src/index.html
+++ b/src/index.html
@@ -2,7 +2,7 @@
-
+
Virtual Gamepad
@@ -25,11 +25,11 @@ Settings
Host:
-
+
Joystick Request Intarval:
- ms
+ ms
Show buttons:
@@ -44,15 +44,23 @@
Server Side (Microcontroller, etc.)
Run a program that can handle HTTP requests in the format of the following protocol .
- (TODO: Sample using Arduino WiFi.h)
+ (An example microcomputer car with M5Atom )
- Confirm that you can communicate with the target server.
+ Check the address of the target server.
Controller Side
- Open the web page and enter the URL of the target server (microcontroller, etc.) in the host field.
- Adjust the joystick transmission interval (default 125 ms).
+ Open the address of the target server (e.g., microcomputer) in your browser.
+
+ Adjust the joystick transmission interval (default 100 ms). It is recommended to keep it small enough
+ that it does not choke communication.
+
Hide the A, B buttons if you are not using them.
Close the settings screen and operate with swipe or WASD (+ Shift).
@@ -62,11 +70,21 @@ Controller Side
Protocol
- Unless you are hosting a clone of this site on the target server, you will generally need to add
- Access-Control-Allow-Origin: https://twosquirrels.github.io
to the response headers to avoid
- errors.
+ When communicating from an HTTPS page, such as on github.io, to an HTTP host, a security error occurs. This
+ can be handled by configuring the browser, but it is recommended to serve HTML on the same host as the API and
+ access the page from there.
+
+ GET /
+
+ Request
+ Basically, it is accessed through a browser.
+
+ Response
+ Please return the gamepad HTML, we recommend that you download the latest HTML from this page.
+
+
POST /joystick?p=aaff
diff --git a/src/main.js b/src/main.js
index 2281333..c6db4d2 100644
--- a/src/main.js
+++ b/src/main.js
@@ -30,8 +30,8 @@ for (const burger of elms.hamburgers) {
/// poster
-let host = params.get("host") ?? "http://localhost:3000";
-let interval = Number(params.get("interval")) || 125;
+let host = params.get("host") ?? window.location.origin;
+let interval = Number(params.get("interval")) || 100;
let showButtons = true;
const joystickPoster = {
@@ -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()],
};