プレイグラウンド、サンドボックス、使い捨てスクリプト置き場
0

Configure Feed

Select the types of activity you want to include in your feed.

create m5a

Kohei Watanabe (Sep 29, 2025, 9:43 AM +0900) 19c38f34 0e7d97ed

+285
+43
m5a/README.md
··· 1 + # M5A Device Setup 2 + 3 + ## 1. Configure WiFi Settings 4 + 5 + Before deploying to the device, edit the WiFi credentials in `main.py`: 6 + 7 + ```python 8 + # ------------- 設定 ------------- 9 + SSID = "YOUR_SSID" # Replace with your WiFi network name 10 + PASSWORD = "YOUR_PASS" # Replace with your WiFi password 11 + ``` 12 + 13 + ## 2. Modify Frontend 14 + 15 + ```diff 16 + diff a/static/main.js b/static/main.js 17 + index 3feb608..6f0b7d8 100644 18 + --- a/static/main.js 19 + +++ b/static/main.js 20 + @@ -254,6 +254,9 @@ function vr_function() { 21 + }; 22 + 23 + recognition.onresult = function(event) { 24 + + // M5Atomに通知 25 + + fetch("/api/smile", { method: "POST" }).catch((err) => console.error(err)); 26 + + 27 + var results = event.results; 28 + var current_transcripts = ''; // resultsが複数ある場合は全て連結する。 29 + var need_reset = false; 30 + ``` 31 + 32 + ## 3. Deploy to Device 33 + 34 + ```sh 35 + uvx mpremote fs cp main.py :main.py 36 + uvx mpremote fs mkdir :static 37 + uvx mpremote fs cp static/index.html :static/index.html 38 + uvx mpremote fs cp static/main.js :static/main.js 39 + uvx mpremote fs cp static/style.css :static/style.css 40 + uvx mpremote fs mkdir :static/kuromoji 41 + uvx mpremote fs mkdir :static/kuromoji/build 42 + uvx mpremote fs cp static/kuromoji/build/kuromoji.js :static/kuromoji/build/kuromoji.js 43 + ```
+203
m5a/main.py
··· 1 + import os 2 + import time 3 + 4 + import neopixel 5 + import network 6 + import uasyncio as asyncio 7 + import ujson as json 8 + from machine import Pin 9 + 10 + # ------------- 設定 ------------- 11 + SSID = "YOUR_SSID" 12 + PASSWORD = "YOUR_PASS" 13 + PORT = 80 14 + 15 + # ==== 設定 ==== 16 + NUM_PIXELS = 25 17 + PIN_NEOPIXEL = 27 # Atom Lite の 5x5 LED 18 + PORT = 80 19 + DURATION = 3 # 点灯する秒数 20 + 21 + # ==== 初期化 ==== 22 + np = neopixel.NeoPixel(Pin(PIN_NEOPIXEL), NUM_PIXELS) 23 + # --- GPIO制御 --- 24 + gpio26 = Pin(26, Pin.OUT) 25 + gpio32 = Pin(32, Pin.OUT) 26 + 27 + # GPIO pins that must be set both H or both L 28 + gpio26 = Pin(26, Pin.OUT) 29 + gpio32 = Pin(32, Pin.OUT) 30 + 31 + 32 + # ------------- ユーティリティ ------------- 33 + def set_gpio_both(on: bool): 34 + v = 1 if on else 0 35 + gpio26.value(v) 36 + gpio32.value(v) 37 + 38 + 39 + def clear(): 40 + for i in range(NUM_PIXELS): 41 + np[i] = (0, 0, 0) 42 + np.write() 43 + gpio26.value(0) 44 + gpio32.value(0) 45 + 46 + 47 + def smile(color=(0, 50, 0)): 48 + """緑のにっこりマークを表示""" 49 + clear() 50 + eyes = [(1, 1), (3, 1)] 51 + mouth = [(0, 3), (1, 4), (2, 4), (3, 4), (4, 3)] 52 + 53 + def idx(x, y): 54 + return y * 5 + x 55 + 56 + for x, y in eyes + mouth: 57 + np[idx(x, y)] = color 58 + np.write() 59 + gpio26.value(1) 60 + gpio32.value(1) 61 + 62 + 63 + def trigger(): 64 + """スマイルを表示し、一定時間後に消灯""" 65 + smile() 66 + time.sleep(DURATION) 67 + clear() 68 + 69 + 70 + # ------------- Wi-Fi 接続 ------------- 71 + def wifi_connect(): 72 + wlan = network.WLAN(network.STA_IF) 73 + wlan.active(True) 74 + wlan.connect(SSID, PASSWORD) 75 + while not wlan.isconnected(): 76 + asyncio.sleep_ms(200) 77 + ip = wlan.ifconfig()[0] 78 + print("Connected, IP:", ip) 79 + return ip 80 + 81 + 82 + # ------------- HTTP サーバ(uasyncio.Stream)------------- 83 + MIME_TYPES = { 84 + "html": "text/html", 85 + "htm": "text/html", 86 + "js": "application/javascript", 87 + "css": "text/css", 88 + "png": "image/png", 89 + "jpg": "image/jpeg", 90 + "jpeg": "image/jpeg", 91 + "svg": "image/svg+xml", 92 + "json": "application/json", 93 + "ico": "image/x-icon", 94 + } 95 + 96 + 97 + async def send_file(writer, path): 98 + try: 99 + # バイナリ読みでOK 100 + with open(path, "rb") as f: 101 + # Content-Type 102 + ext = path.rsplit(".", 1)[-1].lower() 103 + ctype = MIME_TYPES.get(ext, "application/octet-stream") 104 + data = f.read() 105 + await writer.awrite("HTTP/1.1 200 OK\r\n") 106 + await writer.awrite("Content-Type: {}\r\n".format(ctype)) 107 + await writer.awrite("Content-Length: {}\r\n".format(len(data))) 108 + await writer.awrite("Connection: close\r\n") 109 + await writer.awrite("\r\n") 110 + await writer.awrite(data) 111 + except Exception: 112 + await writer.awrite("HTTP/1.1 404 NOT FOUND\r\n\r\n404") 113 + 114 + 115 + async def handle_api(writer, method, path, headers, body_bytes): 116 + # path like /api/heart, /api/gpio, /api/color, /api/vad 117 + try: 118 + if path.startswith("/api/smile"): 119 + trigger() 120 + else: 121 + return {"status": "error", "reason": "unknown api"} 122 + except Exception as e: 123 + return {"status": "error", "exception": str(e)} 124 + 125 + 126 + async def http_handler(reader, writer): 127 + try: 128 + request_line = await reader.readline() 129 + if not request_line: 130 + await writer.aclose() 131 + return 132 + request_line = request_line.decode().strip() 133 + method, raw_path, _ = request_line.split() 134 + print("Request:", method, raw_path) 135 + # ヘッダー読む 136 + headers = {} 137 + while True: 138 + line = await reader.readline() 139 + if not line or line == b"\r\n": 140 + break 141 + l = line.decode() 142 + if ":" in l: 143 + k, v = l.split(":", 1) 144 + headers[k.strip().lower()] = v.strip() 145 + # パスとクエリ分離 146 + path = raw_path.split("?", 1)[0] 147 + # ボディ読み(Content-Length があれば) 148 + body = b"" 149 + cl = int(headers.get("content-length", "0")) 150 + if cl: 151 + body = await reader.readexactly(cl) 152 + 153 + # API ハンドリング 154 + if path.startswith("/api/"): 155 + res = await handle_api(writer, method, path, headers, body) 156 + b = json.dumps(res) 157 + await writer.awrite("HTTP/1.1 200 OK\r\n") 158 + await writer.awrite("Content-Type: application/json\r\n") 159 + await writer.awrite("Content-Length: {}\r\n".format(len(b))) 160 + await writer.awrite("Connection: close\r\n") 161 + await writer.awrite("\r\n") 162 + await writer.awrite(b) 163 + await writer.aclose() 164 + return 165 + 166 + # 静的ファイル: "/" -> "/index.html"(または /index.html) 167 + if path.endswith("/"): 168 + path += "index.html" 169 + fs_path = "./static" + path 170 + if fs_path.endswith("/"): 171 + fs_path += "index.html" 172 + try: 173 + os.stat(fs_path) 174 + await send_file(writer, fs_path) 175 + except OSError: 176 + # 404 177 + await writer.awrite("HTTP/1.1 404 NOT FOUND\r\n") 178 + await writer.awrite("Content-Type: text/plain\r\n\r\n") 179 + await writer.awrite("404 Not Found") 180 + await writer.aclose() 181 + except Exception as e: 182 + try: 183 + await writer.awrite("HTTP/1.1 500 INTERNAL\r\n\r\n") 184 + await writer.awrite("Error: " + str(e)) 185 + await writer.aclose() 186 + except: 187 + pass 188 + 189 + 190 + async def main(): 191 + wifi_connect() 192 + print("Starting server on port", PORT) 193 + await asyncio.start_server(http_handler, "0.0.0.0", PORT) 194 + # サーバはバックグラウンドで動く 195 + while True: 196 + await asyncio.sleep(1) 197 + 198 + 199 + # 実行 200 + try: 201 + asyncio.run(main()) 202 + finally: 203 + asyncio.new_event_loop()
+35
m5a/static/0001-M5Atom.patch
··· 1 + From 10391b72832a244cbffdf7851fc6226ab32efb79 Mon Sep 17 00:00:00 2001 2 + From: Kohei Watanabe <nebel@fogtype.com> 3 + Date: Sat, 27 Sep 2025 23:15:42 +0900 4 + Subject: [PATCH] =?UTF-8?q?M5Atom=E3=81=B8=E3=81=AE=E9=80=9A=E7=9F=A5?= 5 + =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= 6 + MIME-Version: 1.0 7 + Content-Type: text/plain; charset=UTF-8 8 + Content-Transfer-Encoding: 8bit 9 + 10 + --- 11 + main.js | 8 ++++++++ 12 + 1 file changed, 8 insertions(+) 13 + 14 + diff --git a/main.js b/main.js 15 + index 3feb608..267f869 100644 16 + --- a/main.js 17 + +++ b/main.js 18 + @@ -260,6 +260,14 @@ function vr_function() { 19 + for (var i = event.resultIndex; i < results.length; i++) { 20 + if (results[i].isFinal) { 21 + last_finished = results[i][0].transcript; 22 + + 23 + + // M5Atomに通知 24 + + fetch("/api/smile", { 25 + + method: "POST", 26 + + headers: { "Content-Type": "application/json" }, 27 + + body: JSON.stringify({ text: transcript }), 28 + + }).catch((err) => console.error(err)); 29 + + 30 + const is_end_of_sentence = last_finished.endsWith('。') || last_finished.endsWith('?') || last_finished.endsWith('!'); 31 + if (lang == 'ja-JP' && is_end_of_sentence != true) { 32 + last_finished += '。'; 33 + -- 34 + 2.43.0 35 +
+1
m5a/static/index.html
··· 1 + ../speech-to-text-webcam-overlay/index.html
+1
m5a/static/kuromoji/build/kuromoji.js
··· 1 + ../speech-to-text-webcam-overlay/kuromoji/build/kuromoji.js
+1
m5a/static/main.js
··· 1 + ../speech-to-text-webcam-overlay/main.js
+1
m5a/static/style.css
··· 1 + ../speech-to-text-webcam-overlay/style.css