Skip to content

Commit

Permalink
Add M5Atom Car example
Browse files Browse the repository at this point in the history
  • Loading branch information
TwoSquirrels committed Jul 15, 2024
1 parent 7d4be49 commit 5fddb37
Show file tree
Hide file tree
Showing 12 changed files with 461 additions and 19 deletions.
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

マイコンカー操作などのための、スマホ用バーチャルコントローラーです。(もちろん PC も対応しています)

<https://twosquirrels.github.io/virtual-gamepad/> から使用できますが、自分用に作ったので汎用性は低いかもしれません。気に入らない場合は Fork して改造してください。Pull Request 大歓迎です。
<https://twosquirrels.github.io/virtual-gamepad/> から HTML をダウンロードできますが、自分用に作ったので汎用性は低いかもしれません。気に入らない場合は Fork して改造してください。Pull Request 大歓迎です。

> [!TIP]
> ジョイスティック部分は [nippleJS](https://github.com/yoannmoinet/nipplejs) を使っています。
Expand All @@ -17,20 +17,30 @@

### サーバー側 (マイコン等)

1. [以下の規格](#Protocol) の形式の HTTP リクエストを処理できるプログラムを走らせます。(TODO: Arduino WiFi.h によるサンプル)
2. 対象のサーバーと通信できるか確認します
1. [以下の規格](#Protocol) の形式の HTTP リクエストを処理できるプログラムを走らせます。([参考: M5Atom によるマイコンカーのサンプル](example/m5atom-car/))
2. 対象のサーバーのアドレスを確認します

### コントローラー側

3. Web ページを開き、対象サーバー (マイコン等) の URL を host に入力します
4. ジョイスティックの送信間隔 (デフォルト $125~\mathrm{ms}$) を調整します。
3. 対象サーバー (マイコン等) のアドレスをブラウザで開きます
4. ジョイスティックの送信間隔 (デフォルト $100~\mathrm{ms}$) を調整します。通信が詰まらない程度に小さくすることをおすすめします
5. A, B ボタンを使わない場合は隠します。
6. 設定画面を閉じ、スワイプまたは WASD (+ Shift) で操作します。

## Protocol

> [!WARNING]
> このサイトのクローンを対象サーバー上でホスティングする場合などを除き、基本的にレスポンスヘッダーに `Access-Control-Allow-Origin: https://twosquirrels.github.io` 等を加えないとエラーが出て上手く動作しない可能性があります。
> github.io 上など HTTPS のページから HTTP のホストに対して通信をするとセキュリティエラーが出ます。これはブラウザの設定で対処することもできますが、API と同じホストで HTML を配信してそちらからページにアクセスすることを推奨します。
### GET `/`

#### Request

基本的にブラウザからアクセスされます。

#### Response

ゲームパッドの HTML を返してください。HTML は <https://twosquirrels.github.io/virtual-gamepad/> から最新の物をダウンロードできるようにすることをおすすめします。

### POST `/joystick?p=aaff`

Expand All @@ -57,6 +67,11 @@ body は常に空です。

デバッグコンソールに表示されることを除けば、無視されます。

## Examples

- [M5Atom によるマイコンカーのサンプル](example/m5atom-car/)
- その他のサンプル募集中!

## Tech Stack

**Site Builder:** [Vite](https://ja.vitejs.dev/)
Expand Down
3 changes: 3 additions & 0 deletions example/m5atom-car/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.env.h
index.html
index-html.h
24 changes: 24 additions & 0 deletions example/m5atom-car/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# M5Atom WiFi Control Car Example

M5Atom によるマイコンカーでの Virtual Gamepad 用サンプルプログラムです。

## How to RUN

まずこのディレクトリ (フォルダー) をダウンロードしてください。

これをコンパイルするためには、`./index-html.h` というファイルを用意してその中で `index_html` 定数に <https://twosquirrels.github.io/virtual-gamepad/> の HTML を `const char[]` 型で設定する必要があります。
この作業は Windows の場合は `./gen-index-html.bat` を、Mac 等 bash が動く環境の場合は `./gen-index-html.sh` を実行することで、自動で HTML ファイルをダウンロードしヘッダファイルを生成できるため、それを使うことを推奨します。

また WiFi の ssid と password は `./m5atom-car.ino` に直接書き込んでも良いですが、`./.env.h` を作りこのヘッダファイル内で以下のように定数を初期化することもできます。

```c
const char ssid[] = "YOUR_SSID";
const char password[] = "YOUR_PASSWORD";
```

シリアルモニタを開いた状態でスケッチを書き込むと、書き込みが完了しプログラムが実行されたタイミングで M5Atom のローカル IP アドレスが表示されるため、それをスマホ等のブラウザで開いてください。ゲームパッドの画面が開けたら成功です。

## Structure

`./SimpleHTTPServer.h` は ChatGPT で作った Arduino 用の簡易的な HTTP サーバーライブラリです。
`./m5atom-car.ino` ではジョイスティックのクエリからパラメータをパースし、`updateMoers` 関数でその値から左右のモーターの出力を設定しています。
133 changes: 133 additions & 0 deletions example/m5atom-car/SimpleHTTPServer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SimpleHTTPServer written by ChatGPT

#include <WiFi.h>

#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("<html><body><h1>404 Not Found</h1></body></html>");
client.println();
}
};
12 changes: 12 additions & 0 deletions example/m5atom-car/gen-index-html.bat
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions example/m5atom-car/gen-index-html.sh
Original file line number Diff line number Diff line change
@@ -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."
Loading

0 comments on commit 5fddb37

Please sign in to comment.