Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"istream": "cpp",
"numeric": "cpp",
"ostream": "cpp",
"sstream": "cpp"
"sstream": "cpp",
"chrono": "cpp"
}
}
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ async def talk_session(proxy: WsProxy):
await proxy.speak(resp.text)
```

`StackChanApp()` は既定で、WebSocket 接続直後に WakeWord 検出通知音をデバイスへ送信しようとします。
送信する音は環境変数 `STACKCHAN_WAKEWORD_SOUND_PATH` で指定した WAV ファイルから読み込みます。
読み込んだ WAV は送信前に 16-bit PCM / 24kHz / mono へ正規化されます。
さらに短い通知音でも再生しやすいよう、送信前に前後へ短い無音を付与し、最小再生長を確保します。
この値が未設定なら通知音は送信されません。
送信された音はデバイス側で SPIFFS に保存され、WakeWord 検出時にローカル再生されます。
接続時送信の機能自体を無効化したい場合は `StackChanApp(send_wakeword_sound_on_connect=False)` を使ってください。

## セットアップ

以下を確認ください。
Expand All @@ -101,7 +109,7 @@ async def talk_session(proxy: WsProxy):
- M5Stack CoreS3(SKU:K128, K128-Lite, K128-SE)
- M5Stack Atom S3R(SKU:C126) + Atomic Echo Base(SKU:A149)
- M5Stack公式StackChan(SKU:K151)
<!-- - M5Stack Atom EchoS3R -->
- M5Stack Atom EchoS3R
- サーボ(なくても動作します):
- Tower Pro SG90
- FEETECH SCS0009
Expand Down
24 changes: 24 additions & 0 deletions docs/websocket_protocols_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
| `SpeakDoneEvt` | CoreS3 → Server | 音声再生完了通知 |
| `ServoCmd` | Server → CoreS3 | サーボ動作シーケンス指示 |
| `ServoDoneEvt` | CoreS3 → Server | サーボ動作完了通知 |
| `StoredFile` | Server → CoreS3 | SPIFFS 保存用の汎用ファイル転送 |

### `MessageType` 一覧

Expand Down Expand Up @@ -136,6 +137,29 @@
- CoreS3 側の音声再生完了を通知します。
- Server はこの通知を待って `proxy.speak()` を完了させます。

## 保存ファイル転送 `StoredFile`

- 方向: Server → CoreS3
- 用途: バイナリファイルを WebSocket 経由で配布し、CoreS3 側で SPIFFS に保存するための汎用転送です。
- 1 転送の流れは `StoredFileStart` → `FileChunk` 複数回 → `StoredFileEnd` です。

### body 形式

| messageType | body |
| --- | --- |
| `START` | `StoredFileStart { file_id, content_type, total_size, sample_rate, channels }` |
| `DATA` | `FileChunk { chunk_bytes }` |
| `END` | `StoredFileEnd {}` |

### 現行実装メモ

- `file_id` はファイルの論理名です。現在の実装では `wakeword-detected-sound` が WakeUpWord 検出音に使われます。
- `content_type` は現在 `audio/pcm` をサポートします。
- `sample_rate` / `channels` は PCM 再生用の追加メタデータです。
- CoreS3 は受信完了後に SPIFFS へ保存し、**その WebSocket 接続中に受信したファイルだけ** を有効化します。
- 再接続後にサーバーが同じファイルを再送しない場合、SPIFFS に過去データが残っていても再生には使いません。
- WakeUpWord 検出時は、該当サウンドが現在の接続で有効になっている場合のみローカル再生してから `WakeWordEvt` を送信します。

## サーボ動作指示 `ServoCmd`

- 方向: Server → CoreS3
Expand Down
8 changes: 8 additions & 0 deletions firmware/include/protocols.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "../lib/generated_protobuf/websocket-message.pb.h"

constexpr size_t kProtoAudioChunkMaxBytes = 4096;
constexpr size_t kProtoFileChunkMaxBytes = 4096;
constexpr size_t kProtoServoCommandMaxCount = 255;
constexpr size_t kMaxEncodedWebSocketMessageBytes = stackchan_websocket_v1_WebSocketMessage_size;

Expand All @@ -18,6 +19,13 @@ bool setProtoAudioChunk(
const uint8_t *getProtoAudioChunkBytes(const stackchan_websocket_v1_AudioChunk &chunk);
size_t getProtoAudioChunkSize(const stackchan_websocket_v1_AudioChunk &chunk);

bool setProtoFileChunk(
stackchan_websocket_v1_FileChunk &chunk,
const uint8_t *data,
size_t data_len);
const uint8_t *getProtoFileChunkBytes(const stackchan_websocket_v1_FileChunk &chunk);
size_t getProtoFileChunkSize(const stackchan_websocket_v1_FileChunk &chunk);

bool encodeWebSocketMessage(
const stackchan_websocket_v1_WebSocketMessage &message,
std::vector<uint8_t> &encoded);
Expand Down
73 changes: 73 additions & 0 deletions firmware/include/stored_files.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#pragma once

#include <array>
#include <cstddef>
#include <cstdint>
#include <vector>

#include "protocols.hpp"

struct StoredFileView
{
const uint8_t *data = nullptr;
size_t size = 0;
uint32_t sample_rate = 0;
uint16_t channels = 0;
};

class StoredFiles
{
public:
void init();
void resetSession();

bool handleStart(uint32_t seq, const stackchan_websocket_v1_StoredFileStart &start);
bool handleData(uint32_t seq, const uint8_t *data, size_t data_len);
bool handleEnd(uint32_t seq);

bool getActivePcmFile(const char *fileId, StoredFileView &view);

private:
static constexpr size_t kMaxStoredFiles = 4;
static constexpr size_t kMaxStoredFileBytes = 256 * 1024;

struct PersistedSlot
{
bool used = false;
char file_id[64] = "";
char content_type[64] = "";
uint32_t sample_rate = 0;
uint32_t channels = 0;
uint32_t size = 0;
};

struct TransferState
{
bool active = false;
uint32_t next_seq = 0;
int slot_index = -1;
uint32_t received_bytes = 0;
uint32_t chunk_count = 0;
PersistedSlot slot{};
std::vector<uint8_t> payload;
};

bool storage_ready_ = false;
std::array<PersistedSlot, kMaxStoredFiles> slots_{};
std::array<bool, kMaxStoredFiles> session_active_{};
int cached_slot_index_ = -1;
std::vector<uint8_t> cached_payload_;
TransferState transfer_;

bool mountSpiffs();
bool loadIndex();
bool persistIndex();
bool persistSlotPayload(int slotIndex, const std::vector<uint8_t> &payload);
bool loadSlotPayload(int slotIndex, std::vector<uint8_t> &payload);
int findSlotById(const char *fileId) const;
int selectSlotForId(const char *fileId);
void resetTransfer();
bool activateSlot(int slotIndex, const std::vector<uint8_t> &payload);
static const char *payloadPathForSlot(int slotIndex);
static const char *indexPath();
};
9 changes: 9 additions & 0 deletions firmware/lib/generated_protobuf/websocket-message.pb.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ PB_BIND(stackchan_websocket_v1_AudioWavEnd, stackchan_websocket_v1_AudioWavEnd,
PB_BIND(stackchan_websocket_v1_AudioChunk, stackchan_websocket_v1_AudioChunk, 4)


PB_BIND(stackchan_websocket_v1_FileChunk, stackchan_websocket_v1_FileChunk, 4)


PB_BIND(stackchan_websocket_v1_StateCommand, stackchan_websocket_v1_StateCommand, AUTO)


Expand All @@ -36,6 +39,12 @@ PB_BIND(stackchan_websocket_v1_StateEvent, stackchan_websocket_v1_StateEvent, AU
PB_BIND(stackchan_websocket_v1_SpeakDoneEvent, stackchan_websocket_v1_SpeakDoneEvent, AUTO)


PB_BIND(stackchan_websocket_v1_StoredFileStart, stackchan_websocket_v1_StoredFileStart, AUTO)


PB_BIND(stackchan_websocket_v1_StoredFileEnd, stackchan_websocket_v1_StoredFileEnd, AUTO)


PB_BIND(stackchan_websocket_v1_ServoCommandSequence, stackchan_websocket_v1_ServoCommandSequence, 2)


Expand Down
81 changes: 77 additions & 4 deletions firmware/lib/generated_protobuf/websocket-message.pb.h

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions firmware/src/listening.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ void Listening::init()

void Listening::begin()
{
if (M5.Speaker.isPlaying())
{
log_i("Stopping speaker playback before listening start");
M5.Speaker.stop();
delay(20);
}
M5.Mic.begin();
startStreaming();
}
Expand Down
Loading
Loading