From a75b3f8a4acd5bfcd5c124f6d888ec65b3b113f5 Mon Sep 17 00:00:00 2001 From: Atsushi Morimoto <74th.tech@gmail.com> Date: Sun, 19 Apr 2026 16:18:52 +0900 Subject: [PATCH 1/2] feat: add firmware and server metadata handling - Implemented firmware metadata initialization and notification in main application. - Added server metadata handling in WebSocket event processing. - Created new metadata structures and functions in metadata.cpp for managing firmware and server metadata. - Updated protobuf definitions to include firmware and server metadata messages. - Enhanced WebSocket message encoding to support new metadata types. - Introduced versioning for the stackchan server. --- firmware/include/metadata.hpp | 34 ++++++ .../generated_protobuf/websocket-message.pb.c | 10 ++ .../generated_protobuf/websocket-message.pb.h | 98 +++++++++++++++- firmware/src/main.cpp | 25 +++++ firmware/src/metadata.cpp | 105 ++++++++++++++++++ platformio.ini | 1 + protobuf/websocket-message.options | 2 + protobuf/websocket-message.proto | 34 ++++++ stackchan_server/__init__.py | 3 + .../websocket_message_pb2.py | 76 +++++++------ stackchan_server/protobuf_ws.py | 17 +++ stackchan_server/ws_proxy.py | 69 ++++++++++++ 12 files changed, 436 insertions(+), 38 deletions(-) create mode 100644 firmware/include/metadata.hpp create mode 100644 firmware/src/metadata.cpp diff --git a/firmware/include/metadata.hpp b/firmware/include/metadata.hpp new file mode 100644 index 0000000..e4ec10d --- /dev/null +++ b/firmware/include/metadata.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "protocols.hpp" + +struct FirmwareMetadataState +{ + stackchan_websocket_v1_DeviceType device_type = stackchan_websocket_v1_DeviceType_DEVICE_TYPE_UNSPECIFIED; + uint32_t display_width = 0; + uint32_t display_height = 0; + bool has_device_wake_word = false; + bool has_led = false; + stackchan_websocket_v1_ServoType servo_type = stackchan_websocket_v1_ServoType_SERVO_TYPE_UNSPECIFIED; + bool supports_audio_duplex = false; + char firmware_version[64] = ""; +}; + +struct ServerMetadataState +{ + bool available = false; + bool has_server_wake_word = false; + char server_version[64] = ""; +}; + +extern FirmwareMetadataState g_firmware_metadata; +extern ServerMetadataState g_server_metadata; + +void initializeFirmwareMetadata(); +void resetServerMetadata(); +void setFirmwareMetadataMessage( + stackchan_websocket_v1_WebSocketMessage &message, + uint32_t seq); +void applyServerMetadata(const stackchan_websocket_v1_ServerMetadata &metadata); diff --git a/firmware/lib/generated_protobuf/websocket-message.pb.c b/firmware/lib/generated_protobuf/websocket-message.pb.c index e024c81..f70a79e 100644 --- a/firmware/lib/generated_protobuf/websocket-message.pb.c +++ b/firmware/lib/generated_protobuf/websocket-message.pb.c @@ -45,6 +45,16 @@ PB_BIND(stackchan_websocket_v1_ServoCommand, stackchan_websocket_v1_ServoCommand PB_BIND(stackchan_websocket_v1_ServoDoneEvent, stackchan_websocket_v1_ServoDoneEvent, AUTO) +PB_BIND(stackchan_websocket_v1_FirmwareMetadata, stackchan_websocket_v1_FirmwareMetadata, AUTO) + + +PB_BIND(stackchan_websocket_v1_ServerMetadata, stackchan_websocket_v1_ServerMetadata, AUTO) + + + + + + diff --git a/firmware/lib/generated_protobuf/websocket-message.pb.h b/firmware/lib/generated_protobuf/websocket-message.pb.h index ce13703..8e0c222 100644 --- a/firmware/lib/generated_protobuf/websocket-message.pb.h +++ b/firmware/lib/generated_protobuf/websocket-message.pb.h @@ -19,7 +19,9 @@ typedef enum _stackchan_websocket_v1_MessageKind { stackchan_websocket_v1_MessageKind_MESSAGE_KIND_STATE_EVT = 5, stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SPEAK_DONE_EVT = 6, stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVO_CMD = 7, - stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVO_DONE_EVT = 8 + stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVO_DONE_EVT = 8, + stackchan_websocket_v1_MessageKind_MESSAGE_KIND_FIRMWARE_METADATA = 9, + stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVER_METADATA = 10 } stackchan_websocket_v1_MessageKind; typedef enum _stackchan_websocket_v1_MessageType { @@ -42,6 +44,20 @@ typedef enum _stackchan_websocket_v1_ServoOperation { stackchan_websocket_v1_ServoOperation_SERVO_OPERATION_MOVE_Y = 2 } stackchan_websocket_v1_ServoOperation; +typedef enum _stackchan_websocket_v1_DeviceType { + stackchan_websocket_v1_DeviceType_DEVICE_TYPE_UNSPECIFIED = 0, + stackchan_websocket_v1_DeviceType_DEVICE_TYPE_M5STACK_CORES3 = 1, + stackchan_websocket_v1_DeviceType_DEVICE_TYPE_M5ATOM_S3R = 2, + stackchan_websocket_v1_DeviceType_DEVICE_TYPE_M5ATOM_ECHOS3R = 3 +} stackchan_websocket_v1_DeviceType; + +typedef enum _stackchan_websocket_v1_ServoType { + stackchan_websocket_v1_ServoType_SERVO_TYPE_UNSPECIFIED = 0, + stackchan_websocket_v1_ServoType_SERVO_TYPE_NONE = 1, + stackchan_websocket_v1_ServoType_SERVO_TYPE_SG90 = 2, + stackchan_websocket_v1_ServoType_SERVO_TYPE_SCS0009 = 3 +} stackchan_websocket_v1_ServoType; + /* Struct definitions */ typedef struct _stackchan_websocket_v1_AudioPcmStart { char dummy_field; @@ -96,6 +112,22 @@ typedef struct _stackchan_websocket_v1_ServoDoneEvent { bool done; } stackchan_websocket_v1_ServoDoneEvent; +typedef struct _stackchan_websocket_v1_FirmwareMetadata { + stackchan_websocket_v1_DeviceType device_type; + uint32_t display_width; + uint32_t display_height; + bool has_device_wake_word; + bool has_led; + stackchan_websocket_v1_ServoType servo_type; + bool supports_audio_duplex; + char firmware_version[64]; +} stackchan_websocket_v1_FirmwareMetadata; + +typedef struct _stackchan_websocket_v1_ServerMetadata { + bool has_server_wake_word; + char server_version[64]; +} stackchan_websocket_v1_ServerMetadata; + /* One WebSocket binary frame carries exactly one WebSocketMessage. Instead of concatenating two protobuf messages such as Header + Body, @@ -121,6 +153,8 @@ typedef struct _stackchan_websocket_v1_WebSocketMessage { stackchan_websocket_v1_SpeakDoneEvent speak_done_evt; stackchan_websocket_v1_ServoCommandSequence servo_cmd; stackchan_websocket_v1_ServoDoneEvent servo_done_evt; + stackchan_websocket_v1_FirmwareMetadata firmware_metadata; + stackchan_websocket_v1_ServerMetadata server_metadata; } body; } stackchan_websocket_v1_WebSocketMessage; @@ -131,8 +165,8 @@ extern "C" { /* Helper constants for enums */ #define _stackchan_websocket_v1_MessageKind_MIN stackchan_websocket_v1_MessageKind_MESSAGE_KIND_UNSPECIFIED -#define _stackchan_websocket_v1_MessageKind_MAX stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVO_DONE_EVT -#define _stackchan_websocket_v1_MessageKind_ARRAYSIZE ((stackchan_websocket_v1_MessageKind)(stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVO_DONE_EVT+1)) +#define _stackchan_websocket_v1_MessageKind_MAX stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVER_METADATA +#define _stackchan_websocket_v1_MessageKind_ARRAYSIZE ((stackchan_websocket_v1_MessageKind)(stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVER_METADATA+1)) #define _stackchan_websocket_v1_MessageType_MIN stackchan_websocket_v1_MessageType_MESSAGE_TYPE_UNSPECIFIED #define _stackchan_websocket_v1_MessageType_MAX stackchan_websocket_v1_MessageType_MESSAGE_TYPE_END @@ -146,6 +180,14 @@ extern "C" { #define _stackchan_websocket_v1_ServoOperation_MAX stackchan_websocket_v1_ServoOperation_SERVO_OPERATION_MOVE_Y #define _stackchan_websocket_v1_ServoOperation_ARRAYSIZE ((stackchan_websocket_v1_ServoOperation)(stackchan_websocket_v1_ServoOperation_SERVO_OPERATION_MOVE_Y+1)) +#define _stackchan_websocket_v1_DeviceType_MIN stackchan_websocket_v1_DeviceType_DEVICE_TYPE_UNSPECIFIED +#define _stackchan_websocket_v1_DeviceType_MAX stackchan_websocket_v1_DeviceType_DEVICE_TYPE_M5ATOM_ECHOS3R +#define _stackchan_websocket_v1_DeviceType_ARRAYSIZE ((stackchan_websocket_v1_DeviceType)(stackchan_websocket_v1_DeviceType_DEVICE_TYPE_M5ATOM_ECHOS3R+1)) + +#define _stackchan_websocket_v1_ServoType_MIN stackchan_websocket_v1_ServoType_SERVO_TYPE_UNSPECIFIED +#define _stackchan_websocket_v1_ServoType_MAX stackchan_websocket_v1_ServoType_SERVO_TYPE_SCS0009 +#define _stackchan_websocket_v1_ServoType_ARRAYSIZE ((stackchan_websocket_v1_ServoType)(stackchan_websocket_v1_ServoType_SERVO_TYPE_SCS0009+1)) + #define stackchan_websocket_v1_WebSocketMessage_kind_ENUMTYPE stackchan_websocket_v1_MessageKind #define stackchan_websocket_v1_WebSocketMessage_message_type_ENUMTYPE stackchan_websocket_v1_MessageType @@ -164,6 +206,10 @@ extern "C" { #define stackchan_websocket_v1_ServoCommand_op_ENUMTYPE stackchan_websocket_v1_ServoOperation +#define stackchan_websocket_v1_FirmwareMetadata_device_type_ENUMTYPE stackchan_websocket_v1_DeviceType +#define stackchan_websocket_v1_FirmwareMetadata_servo_type_ENUMTYPE stackchan_websocket_v1_ServoType + + /* Initializer values for message structs */ #define stackchan_websocket_v1_WebSocketMessage_init_default {_stackchan_websocket_v1_MessageKind_MIN, _stackchan_websocket_v1_MessageType_MIN, 0, 0, {stackchan_websocket_v1_AudioPcmStart_init_default}} @@ -179,6 +225,8 @@ extern "C" { #define stackchan_websocket_v1_ServoCommandSequence_init_default {0, {stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default, stackchan_websocket_v1_ServoCommand_init_default}} #define stackchan_websocket_v1_ServoCommand_init_default {_stackchan_websocket_v1_ServoOperation_MIN, 0, 0} #define stackchan_websocket_v1_ServoDoneEvent_init_default {0} +#define stackchan_websocket_v1_FirmwareMetadata_init_default {_stackchan_websocket_v1_DeviceType_MIN, 0, 0, 0, 0, _stackchan_websocket_v1_ServoType_MIN, 0, ""} +#define stackchan_websocket_v1_ServerMetadata_init_default {0, ""} #define stackchan_websocket_v1_WebSocketMessage_init_zero {_stackchan_websocket_v1_MessageKind_MIN, _stackchan_websocket_v1_MessageType_MIN, 0, 0, {stackchan_websocket_v1_AudioPcmStart_init_zero}} #define stackchan_websocket_v1_AudioPcmStart_init_zero {0} #define stackchan_websocket_v1_AudioPcmEnd_init_zero {0} @@ -192,6 +240,8 @@ extern "C" { #define stackchan_websocket_v1_ServoCommandSequence_init_zero {0, {stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero, stackchan_websocket_v1_ServoCommand_init_zero}} #define stackchan_websocket_v1_ServoCommand_init_zero {_stackchan_websocket_v1_ServoOperation_MIN, 0, 0} #define stackchan_websocket_v1_ServoDoneEvent_init_zero {0} +#define stackchan_websocket_v1_FirmwareMetadata_init_zero {_stackchan_websocket_v1_DeviceType_MIN, 0, 0, 0, 0, _stackchan_websocket_v1_ServoType_MIN, 0, ""} +#define stackchan_websocket_v1_ServerMetadata_init_zero {0, ""} /* Field tags (for use in manual encoding/decoding) */ #define stackchan_websocket_v1_AudioWavStart_sample_rate_tag 1 @@ -206,6 +256,16 @@ extern "C" { #define stackchan_websocket_v1_ServoCommand_duration_ms_tag 3 #define stackchan_websocket_v1_ServoCommandSequence_commands_tag 1 #define stackchan_websocket_v1_ServoDoneEvent_done_tag 1 +#define stackchan_websocket_v1_FirmwareMetadata_device_type_tag 1 +#define stackchan_websocket_v1_FirmwareMetadata_display_width_tag 2 +#define stackchan_websocket_v1_FirmwareMetadata_display_height_tag 3 +#define stackchan_websocket_v1_FirmwareMetadata_has_device_wake_word_tag 4 +#define stackchan_websocket_v1_FirmwareMetadata_has_led_tag 5 +#define stackchan_websocket_v1_FirmwareMetadata_servo_type_tag 6 +#define stackchan_websocket_v1_FirmwareMetadata_supports_audio_duplex_tag 7 +#define stackchan_websocket_v1_FirmwareMetadata_firmware_version_tag 8 +#define stackchan_websocket_v1_ServerMetadata_has_server_wake_word_tag 1 +#define stackchan_websocket_v1_ServerMetadata_server_version_tag 2 #define stackchan_websocket_v1_WebSocketMessage_kind_tag 1 #define stackchan_websocket_v1_WebSocketMessage_message_type_tag 2 #define stackchan_websocket_v1_WebSocketMessage_seq_tag 3 @@ -221,6 +281,8 @@ extern "C" { #define stackchan_websocket_v1_WebSocketMessage_speak_done_evt_tag 33 #define stackchan_websocket_v1_WebSocketMessage_servo_cmd_tag 34 #define stackchan_websocket_v1_WebSocketMessage_servo_done_evt_tag 35 +#define stackchan_websocket_v1_WebSocketMessage_firmware_metadata_tag 36 +#define stackchan_websocket_v1_WebSocketMessage_server_metadata_tag 37 /* Struct field encoding specification for nanopb */ #define stackchan_websocket_v1_WebSocketMessage_FIELDLIST(X, a) \ @@ -238,7 +300,9 @@ X(a, STATIC, ONEOF, MESSAGE, (body,wake_word_evt,body.wake_word_evt), 31) X(a, STATIC, ONEOF, MESSAGE, (body,state_evt,body.state_evt), 32) \ X(a, STATIC, ONEOF, MESSAGE, (body,speak_done_evt,body.speak_done_evt), 33) \ X(a, STATIC, ONEOF, MESSAGE, (body,servo_cmd,body.servo_cmd), 34) \ -X(a, STATIC, ONEOF, MESSAGE, (body,servo_done_evt,body.servo_done_evt), 35) +X(a, STATIC, ONEOF, MESSAGE, (body,servo_done_evt,body.servo_done_evt), 35) \ +X(a, STATIC, ONEOF, MESSAGE, (body,firmware_metadata,body.firmware_metadata), 36) \ +X(a, STATIC, ONEOF, MESSAGE, (body,server_metadata,body.server_metadata), 37) #define stackchan_websocket_v1_WebSocketMessage_CALLBACK NULL #define stackchan_websocket_v1_WebSocketMessage_DEFAULT NULL #define stackchan_websocket_v1_WebSocketMessage_body_audio_pcm_start_MSGTYPE stackchan_websocket_v1_AudioPcmStart @@ -253,6 +317,8 @@ X(a, STATIC, ONEOF, MESSAGE, (body,servo_done_evt,body.servo_done_evt), 3 #define stackchan_websocket_v1_WebSocketMessage_body_speak_done_evt_MSGTYPE stackchan_websocket_v1_SpeakDoneEvent #define stackchan_websocket_v1_WebSocketMessage_body_servo_cmd_MSGTYPE stackchan_websocket_v1_ServoCommandSequence #define stackchan_websocket_v1_WebSocketMessage_body_servo_done_evt_MSGTYPE stackchan_websocket_v1_ServoDoneEvent +#define stackchan_websocket_v1_WebSocketMessage_body_firmware_metadata_MSGTYPE stackchan_websocket_v1_FirmwareMetadata +#define stackchan_websocket_v1_WebSocketMessage_body_server_metadata_MSGTYPE stackchan_websocket_v1_ServerMetadata #define stackchan_websocket_v1_AudioPcmStart_FIELDLIST(X, a) \ @@ -318,6 +384,24 @@ X(a, STATIC, SINGULAR, BOOL, done, 1) #define stackchan_websocket_v1_ServoDoneEvent_CALLBACK NULL #define stackchan_websocket_v1_ServoDoneEvent_DEFAULT NULL +#define stackchan_websocket_v1_FirmwareMetadata_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, UENUM, device_type, 1) \ +X(a, STATIC, SINGULAR, UINT32, display_width, 2) \ +X(a, STATIC, SINGULAR, UINT32, display_height, 3) \ +X(a, STATIC, SINGULAR, BOOL, has_device_wake_word, 4) \ +X(a, STATIC, SINGULAR, BOOL, has_led, 5) \ +X(a, STATIC, SINGULAR, UENUM, servo_type, 6) \ +X(a, STATIC, SINGULAR, BOOL, supports_audio_duplex, 7) \ +X(a, STATIC, SINGULAR, STRING, firmware_version, 8) +#define stackchan_websocket_v1_FirmwareMetadata_CALLBACK NULL +#define stackchan_websocket_v1_FirmwareMetadata_DEFAULT NULL + +#define stackchan_websocket_v1_ServerMetadata_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, has_server_wake_word, 1) \ +X(a, STATIC, SINGULAR, STRING, server_version, 2) +#define stackchan_websocket_v1_ServerMetadata_CALLBACK NULL +#define stackchan_websocket_v1_ServerMetadata_DEFAULT NULL + extern const pb_msgdesc_t stackchan_websocket_v1_WebSocketMessage_msg; extern const pb_msgdesc_t stackchan_websocket_v1_AudioPcmStart_msg; extern const pb_msgdesc_t stackchan_websocket_v1_AudioPcmEnd_msg; @@ -331,6 +415,8 @@ extern const pb_msgdesc_t stackchan_websocket_v1_SpeakDoneEvent_msg; extern const pb_msgdesc_t stackchan_websocket_v1_ServoCommandSequence_msg; extern const pb_msgdesc_t stackchan_websocket_v1_ServoCommand_msg; extern const pb_msgdesc_t stackchan_websocket_v1_ServoDoneEvent_msg; +extern const pb_msgdesc_t stackchan_websocket_v1_FirmwareMetadata_msg; +extern const pb_msgdesc_t stackchan_websocket_v1_ServerMetadata_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ #define stackchan_websocket_v1_WebSocketMessage_fields &stackchan_websocket_v1_WebSocketMessage_msg @@ -346,6 +432,8 @@ extern const pb_msgdesc_t stackchan_websocket_v1_ServoDoneEvent_msg; #define stackchan_websocket_v1_ServoCommandSequence_fields &stackchan_websocket_v1_ServoCommandSequence_msg #define stackchan_websocket_v1_ServoCommand_fields &stackchan_websocket_v1_ServoCommand_msg #define stackchan_websocket_v1_ServoDoneEvent_fields &stackchan_websocket_v1_ServoDoneEvent_msg +#define stackchan_websocket_v1_FirmwareMetadata_fields &stackchan_websocket_v1_FirmwareMetadata_msg +#define stackchan_websocket_v1_ServerMetadata_fields &stackchan_websocket_v1_ServerMetadata_msg /* Maximum encoded size of messages (where known) */ #define STACKCHAN_WEBSOCKET_V1_WEBSOCKET_MESSAGE_PB_H_MAX_SIZE stackchan_websocket_v1_WebSocketMessage_size @@ -354,6 +442,8 @@ extern const pb_msgdesc_t stackchan_websocket_v1_ServoDoneEvent_msg; #define stackchan_websocket_v1_AudioPcmStart_size 0 #define stackchan_websocket_v1_AudioWavEnd_size 0 #define stackchan_websocket_v1_AudioWavStart_size 12 +#define stackchan_websocket_v1_FirmwareMetadata_size 87 +#define stackchan_websocket_v1_ServerMetadata_size 67 #define stackchan_websocket_v1_ServoCommandSequence_size 4080 #define stackchan_websocket_v1_ServoCommand_size 14 #define stackchan_websocket_v1_ServoDoneEvent_size 2 diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 8d41cca..5fffcb2 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -9,6 +9,7 @@ #include #include #include "config.h" +#include "../include/metadata.hpp" #include "../include/protocols.hpp" #include "../include/state_machine.hpp" #include "../include/speaking.hpp" @@ -107,6 +108,16 @@ void notifyWakeWordDetected() } } +void notifyFirmwareMetadata() +{ + auto &message = g_tx_message; + setFirmwareMetadataMessage(message, g_uplink_seq++); + if (!sendUplinkMessage(message)) + { + log_w("Failed to send FirmwareMetadata"); + } +} + void notifyCurrentState(StateMachine::State state) { auto &message = g_tx_message; @@ -242,6 +253,7 @@ void handleWsEvent(WStype_t type, uint8_t *payload, size_t length) case WStype_DISCONNECTED: // M5.Display.println("WS: disconnected"); log_i("WS disconnected"); + resetServerMetadata(); stateMachine.setState(StateMachine::Disconnected); break; case WStype_CONNECTED: @@ -252,6 +264,7 @@ void handleWsEvent(WStype_t type, uint8_t *payload, size_t length) stateMachine.setState(StateMachine::Idle); } markCommunicationActive(); + notifyFirmwareMetadata(); notifyCurrentState(stateMachine.getState()); break; case WStype_TEXT: @@ -322,6 +335,17 @@ void handleWsEvent(WStype_t type, uint8_t *payload, size_t length) log_w("ServoCmd protobuf body mismatch type=%u body=%u", (unsigned)rx.message_type, (unsigned)rx.which_body); } break; + case stackchan_websocket_v1_MessageKind_MESSAGE_KIND_SERVER_METADATA: + if (rx.message_type == stackchan_websocket_v1_MessageType_MESSAGE_TYPE_DATA && + rx.which_body == stackchan_websocket_v1_WebSocketMessage_server_metadata_tag) + { + applyServerMetadata(rx.body.server_metadata); + } + else + { + log_w("ServerMetadata protobuf body mismatch type=%u body=%u", (unsigned)rx.message_type, (unsigned)rx.which_body); + } + break; default: // M5.Display.printf("WS bin kind=%u len=%d\n", (unsigned)rx.kind, (int)length); break; @@ -365,6 +389,7 @@ void setup() notifyWakeWordDetected(); }); display.init(); + initializeFirmwareMetadata(); connectWiFi(); diff --git a/firmware/src/metadata.cpp b/firmware/src/metadata.cpp new file mode 100644 index 0000000..e4e5ef0 --- /dev/null +++ b/firmware/src/metadata.cpp @@ -0,0 +1,105 @@ +#include "../include/metadata.hpp" + +#include + +#include + +#include "config.h" + +FirmwareMetadataState g_firmware_metadata; +ServerMetadataState g_server_metadata; + +namespace +{ +stackchan_websocket_v1_DeviceType detectDeviceType() +{ +#if defined(ARDUINO_ATOM_ECHOS3R) + return stackchan_websocket_v1_DeviceType_DEVICE_TYPE_M5ATOM_ECHOS3R; +#elif defined(ARDUINO_M5STACK_ATOMS3R) + return stackchan_websocket_v1_DeviceType_DEVICE_TYPE_M5ATOM_S3R; +#elif defined(ARDUINO_M5STACK_CORES3) + return stackchan_websocket_v1_DeviceType_DEVICE_TYPE_M5STACK_CORES3; +#else + return stackchan_websocket_v1_DeviceType_DEVICE_TYPE_UNSPECIFIED; +#endif +} + +bool detectHasLed() +{ +#if defined(ARDUINO_ATOM_ECHOS3R) || defined(ARDUINO_M5STACK_ATOMS3R) + return true; +#else + return false; +#endif +} + +stackchan_websocket_v1_ServoType detectServoType() +{ +#if defined(USE_SERVO_SG90) + return stackchan_websocket_v1_ServoType_SERVO_TYPE_SG90; +#elif defined(USE_SERVO_SCS0009) + return stackchan_websocket_v1_ServoType_SERVO_TYPE_SCS0009; +#else + return stackchan_websocket_v1_ServoType_SERVO_TYPE_NONE; +#endif +} +} // namespace + +void initializeFirmwareMetadata() +{ + g_firmware_metadata.device_type = detectDeviceType(); + g_firmware_metadata.display_width = static_cast(M5.Display.width()); + g_firmware_metadata.display_height = static_cast(M5.Display.height()); + g_firmware_metadata.has_device_wake_word = true; + g_firmware_metadata.has_led = detectHasLed(); + g_firmware_metadata.servo_type = detectServoType(); + g_firmware_metadata.supports_audio_duplex = false; + snprintf( + g_firmware_metadata.firmware_version, + sizeof(g_firmware_metadata.firmware_version), + "%s", + STACKCHAN_FIRMWARE_VERSION); +} + +void resetServerMetadata() +{ + g_server_metadata = ServerMetadataState{}; +} + +void setFirmwareMetadataMessage( + stackchan_websocket_v1_WebSocketMessage &message, + uint32_t seq) +{ + message = stackchan_websocket_v1_WebSocketMessage_init_zero; + message.kind = stackchan_websocket_v1_MessageKind_MESSAGE_KIND_FIRMWARE_METADATA; + message.message_type = stackchan_websocket_v1_MessageType_MESSAGE_TYPE_DATA; + message.seq = seq; + message.which_body = stackchan_websocket_v1_WebSocketMessage_firmware_metadata_tag; + message.body.firmware_metadata.device_type = g_firmware_metadata.device_type; + message.body.firmware_metadata.display_width = g_firmware_metadata.display_width; + message.body.firmware_metadata.display_height = g_firmware_metadata.display_height; + message.body.firmware_metadata.has_device_wake_word = g_firmware_metadata.has_device_wake_word; + message.body.firmware_metadata.has_led = g_firmware_metadata.has_led; + message.body.firmware_metadata.servo_type = g_firmware_metadata.servo_type; + message.body.firmware_metadata.supports_audio_duplex = g_firmware_metadata.supports_audio_duplex; + snprintf( + message.body.firmware_metadata.firmware_version, + sizeof(message.body.firmware_metadata.firmware_version), + "%s", + g_firmware_metadata.firmware_version); +} + +void applyServerMetadata(const stackchan_websocket_v1_ServerMetadata &metadata) +{ + g_server_metadata.available = true; + g_server_metadata.has_server_wake_word = metadata.has_server_wake_word; + snprintf( + g_server_metadata.server_version, + sizeof(g_server_metadata.server_version), + "%s", + metadata.server_version); + log_i( + "Server metadata wakeword=%u version=%s", + static_cast(g_server_metadata.has_server_wake_word), + g_server_metadata.server_version); +} diff --git a/platformio.ini b/platformio.ini index 394794a..36452dd 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,6 +15,7 @@ monitor_filters = esp32_exception_decoder, time upload_speed = 1500000 build_flags = + -DSTACKCHAN_FIRMWARE_VERSION=\"0.1.0\" lib_deps = adafruit/Adafruit NeoPixel@^1.15.2 Links2004/WebSockets@^2.7.2 diff --git a/protobuf/websocket-message.options b/protobuf/websocket-message.options index 233e71d..e032f4f 100644 --- a/protobuf/websocket-message.options +++ b/protobuf/websocket-message.options @@ -1,2 +1,4 @@ stackchan.websocket.v1.AudioChunk.pcm_bytes max_size:4096 stackchan.websocket.v1.ServoCommandSequence.commands max_count:255 +stackchan.websocket.v1.FirmwareMetadata.firmware_version max_length:63 +stackchan.websocket.v1.ServerMetadata.server_version max_length:63 diff --git a/protobuf/websocket-message.proto b/protobuf/websocket-message.proto index d1de065..c643673 100644 --- a/protobuf/websocket-message.proto +++ b/protobuf/websocket-message.proto @@ -29,6 +29,8 @@ message WebSocketMessage { SpeakDoneEvent speak_done_evt = 33; ServoCommandSequence servo_cmd = 34; ServoDoneEvent servo_done_evt = 35; + FirmwareMetadata firmware_metadata = 36; + ServerMetadata server_metadata = 37; } } @@ -42,6 +44,8 @@ enum MessageKind { MESSAGE_KIND_SPEAK_DONE_EVT = 6; MESSAGE_KIND_SERVO_CMD = 7; MESSAGE_KIND_SERVO_DONE_EVT = 8; + MESSAGE_KIND_FIRMWARE_METADATA = 9; + MESSAGE_KIND_SERVER_METADATA = 10; } enum MessageType { @@ -64,6 +68,20 @@ enum ServoOperation { SERVO_OPERATION_MOVE_Y = 2; } +enum DeviceType { + DEVICE_TYPE_UNSPECIFIED = 0; + DEVICE_TYPE_M5STACK_CORES3 = 1; + DEVICE_TYPE_M5ATOM_S3R = 2; + DEVICE_TYPE_M5ATOM_ECHOS3R = 3; +} + +enum ServoType { + SERVO_TYPE_UNSPECIFIED = 0; + SERVO_TYPE_NONE = 1; + SERVO_TYPE_SG90 = 2; + SERVO_TYPE_SCS0009 = 3; +} + message AudioPcmStart {} message AudioPcmEnd {} @@ -108,3 +126,19 @@ message ServoCommand { message ServoDoneEvent { bool done = 1; } + +message FirmwareMetadata { + DeviceType device_type = 1; + uint32 display_width = 2; + uint32 display_height = 3; + bool has_device_wake_word = 4; + bool has_led = 5; + ServoType servo_type = 6; + bool supports_audio_duplex = 7; + string firmware_version = 8; +} + +message ServerMetadata { + bool has_server_wake_word = 1; + string server_version = 2; +} diff --git a/stackchan_server/__init__.py b/stackchan_server/__init__.py index e69de29..de48b44 100644 --- a/stackchan_server/__init__.py +++ b/stackchan_server/__init__.py @@ -0,0 +1,3 @@ +__version__ = "0.1.0" + +__all__ = ["__version__"] diff --git a/stackchan_server/generated_protobuf/websocket_message_pb2.py b/stackchan_server/generated_protobuf/websocket_message_pb2.py index 985a939..a7d7a4e 100644 --- a/stackchan_server/generated_protobuf/websocket_message_pb2.py +++ b/stackchan_server/generated_protobuf/websocket_message_pb2.py @@ -24,45 +24,53 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17websocket-message.proto\x12\x16stackchan.websocket.v1\"\x8c\x07\n\x10WebSocketMessage\x12\x31\n\x04kind\x18\x01 \x01(\x0e\x32#.stackchan.websocket.v1.MessageKind\x12\x39\n\x0cmessage_type\x18\x02 \x01(\x0e\x32#.stackchan.websocket.v1.MessageType\x12\x0b\n\x03seq\x18\x03 \x01(\r\x12@\n\x0f\x61udio_pcm_start\x18\n \x01(\x0b\x32%.stackchan.websocket.v1.AudioPcmStartH\x00\x12<\n\x0e\x61udio_pcm_data\x18\x0b \x01(\x0b\x32\".stackchan.websocket.v1.AudioChunkH\x00\x12<\n\raudio_pcm_end\x18\x0c \x01(\x0b\x32#.stackchan.websocket.v1.AudioPcmEndH\x00\x12@\n\x0f\x61udio_wav_start\x18\x14 \x01(\x0b\x32%.stackchan.websocket.v1.AudioWavStartH\x00\x12<\n\x0e\x61udio_wav_data\x18\x15 \x01(\x0b\x32\".stackchan.websocket.v1.AudioChunkH\x00\x12<\n\raudio_wav_end\x18\x16 \x01(\x0b\x32#.stackchan.websocket.v1.AudioWavEndH\x00\x12\x39\n\tstate_cmd\x18\x1e \x01(\x0b\x32$.stackchan.websocket.v1.StateCommandH\x00\x12>\n\rwake_word_evt\x18\x1f \x01(\x0b\x32%.stackchan.websocket.v1.WakeWordEventH\x00\x12\x37\n\tstate_evt\x18 \x01(\x0b\x32\".stackchan.websocket.v1.StateEventH\x00\x12@\n\x0espeak_done_evt\x18! \x01(\x0b\x32&.stackchan.websocket.v1.SpeakDoneEventH\x00\x12\x41\n\tservo_cmd\x18\" \x01(\x0b\x32,.stackchan.websocket.v1.ServoCommandSequenceH\x00\x12@\n\x0eservo_done_evt\x18# \x01(\x0b\x32&.stackchan.websocket.v1.ServoDoneEventH\x00\x42\x06\n\x04\x62ody\"\x0f\n\rAudioPcmStart\"\r\n\x0b\x41udioPcmEnd\"6\n\rAudioWavStart\x12\x13\n\x0bsample_rate\x18\x01 \x01(\r\x12\x10\n\x08\x63hannels\x18\x02 \x01(\r\"\r\n\x0b\x41udioWavEnd\"\x1f\n\nAudioChunk\x12\x11\n\tpcm_bytes\x18\x01 \x01(\x0c\"E\n\x0cStateCommand\x12\x35\n\x05state\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.StackchanState\"!\n\rWakeWordEvent\x12\x10\n\x08\x64\x65tected\x18\x01 \x01(\x08\"C\n\nStateEvent\x12\x35\n\x05state\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.StackchanState\"\x1e\n\x0eSpeakDoneEvent\x12\x0c\n\x04\x64one\x18\x01 \x01(\x08\"N\n\x14ServoCommandSequence\x12\x36\n\x08\x63ommands\x18\x01 \x03(\x0b\x32$.stackchan.websocket.v1.ServoCommand\"f\n\x0cServoCommand\x12\x32\n\x02op\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.ServoOperation\x12\r\n\x05\x61ngle\x18\x02 \x01(\x11\x12\x13\n\x0b\x64uration_ms\x18\x03 \x01(\x11\"\x1e\n\x0eServoDoneEvent\x12\x0c\n\x04\x64one\x18\x01 \x01(\x08*\x99\x02\n\x0bMessageKind\x12\x1c\n\x18MESSAGE_KIND_UNSPECIFIED\x10\x00\x12\x1a\n\x16MESSAGE_KIND_AUDIO_PCM\x10\x01\x12\x1a\n\x16MESSAGE_KIND_AUDIO_WAV\x10\x02\x12\x1a\n\x16MESSAGE_KIND_STATE_CMD\x10\x03\x12\x1e\n\x1aMESSAGE_KIND_WAKE_WORD_EVT\x10\x04\x12\x1a\n\x16MESSAGE_KIND_STATE_EVT\x10\x05\x12\x1f\n\x1bMESSAGE_KIND_SPEAK_DONE_EVT\x10\x06\x12\x1a\n\x16MESSAGE_KIND_SERVO_CMD\x10\x07\x12\x1f\n\x1bMESSAGE_KIND_SERVO_DONE_EVT\x10\x08*p\n\x0bMessageType\x12\x1c\n\x18MESSAGE_TYPE_UNSPECIFIED\x10\x00\x12\x16\n\x12MESSAGE_TYPE_START\x10\x01\x12\x15\n\x11MESSAGE_TYPE_DATA\x10\x02\x12\x14\n\x10MESSAGE_TYPE_END\x10\x03*\x85\x01\n\x0eStackchanState\x12\x18\n\x14STACKCHAN_STATE_IDLE\x10\x00\x12\x1d\n\x19STACKCHAN_STATE_LISTENING\x10\x01\x12\x1c\n\x18STACKCHAN_STATE_THINKING\x10\x02\x12\x1c\n\x18STACKCHAN_STATE_SPEAKING\x10\x03*c\n\x0eServoOperation\x12\x19\n\x15SERVO_OPERATION_SLEEP\x10\x00\x12\x1a\n\x16SERVO_OPERATION_MOVE_X\x10\x01\x12\x1a\n\x16SERVO_OPERATION_MOVE_Y\x10\x02\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17websocket-message.proto\x12\x16stackchan.websocket.v1\"\x96\x08\n\x10WebSocketMessage\x12\x31\n\x04kind\x18\x01 \x01(\x0e\x32#.stackchan.websocket.v1.MessageKind\x12\x39\n\x0cmessage_type\x18\x02 \x01(\x0e\x32#.stackchan.websocket.v1.MessageType\x12\x0b\n\x03seq\x18\x03 \x01(\r\x12@\n\x0f\x61udio_pcm_start\x18\n \x01(\x0b\x32%.stackchan.websocket.v1.AudioPcmStartH\x00\x12<\n\x0e\x61udio_pcm_data\x18\x0b \x01(\x0b\x32\".stackchan.websocket.v1.AudioChunkH\x00\x12<\n\raudio_pcm_end\x18\x0c \x01(\x0b\x32#.stackchan.websocket.v1.AudioPcmEndH\x00\x12@\n\x0f\x61udio_wav_start\x18\x14 \x01(\x0b\x32%.stackchan.websocket.v1.AudioWavStartH\x00\x12<\n\x0e\x61udio_wav_data\x18\x15 \x01(\x0b\x32\".stackchan.websocket.v1.AudioChunkH\x00\x12<\n\raudio_wav_end\x18\x16 \x01(\x0b\x32#.stackchan.websocket.v1.AudioWavEndH\x00\x12\x39\n\tstate_cmd\x18\x1e \x01(\x0b\x32$.stackchan.websocket.v1.StateCommandH\x00\x12>\n\rwake_word_evt\x18\x1f \x01(\x0b\x32%.stackchan.websocket.v1.WakeWordEventH\x00\x12\x37\n\tstate_evt\x18 \x01(\x0b\x32\".stackchan.websocket.v1.StateEventH\x00\x12@\n\x0espeak_done_evt\x18! \x01(\x0b\x32&.stackchan.websocket.v1.SpeakDoneEventH\x00\x12\x41\n\tservo_cmd\x18\" \x01(\x0b\x32,.stackchan.websocket.v1.ServoCommandSequenceH\x00\x12@\n\x0eservo_done_evt\x18# \x01(\x0b\x32&.stackchan.websocket.v1.ServoDoneEventH\x00\x12\x45\n\x11\x66irmware_metadata\x18$ \x01(\x0b\x32(.stackchan.websocket.v1.FirmwareMetadataH\x00\x12\x41\n\x0fserver_metadata\x18% \x01(\x0b\x32&.stackchan.websocket.v1.ServerMetadataH\x00\x42\x06\n\x04\x62ody\"\x0f\n\rAudioPcmStart\"\r\n\x0b\x41udioPcmEnd\"6\n\rAudioWavStart\x12\x13\n\x0bsample_rate\x18\x01 \x01(\r\x12\x10\n\x08\x63hannels\x18\x02 \x01(\r\"\r\n\x0b\x41udioWavEnd\"\x1f\n\nAudioChunk\x12\x11\n\tpcm_bytes\x18\x01 \x01(\x0c\"E\n\x0cStateCommand\x12\x35\n\x05state\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.StackchanState\"!\n\rWakeWordEvent\x12\x10\n\x08\x64\x65tected\x18\x01 \x01(\x08\"C\n\nStateEvent\x12\x35\n\x05state\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.StackchanState\"\x1e\n\x0eSpeakDoneEvent\x12\x0c\n\x04\x64one\x18\x01 \x01(\x08\"N\n\x14ServoCommandSequence\x12\x36\n\x08\x63ommands\x18\x01 \x03(\x0b\x32$.stackchan.websocket.v1.ServoCommand\"f\n\x0cServoCommand\x12\x32\n\x02op\x18\x01 \x01(\x0e\x32&.stackchan.websocket.v1.ServoOperation\x12\r\n\x05\x61ngle\x18\x02 \x01(\x11\x12\x13\n\x0b\x64uration_ms\x18\x03 \x01(\x11\"\x1e\n\x0eServoDoneEvent\x12\x0c\n\x04\x64one\x18\x01 \x01(\x08\"\x99\x02\n\x10\x46irmwareMetadata\x12\x37\n\x0b\x64\x65vice_type\x18\x01 \x01(\x0e\x32\".stackchan.websocket.v1.DeviceType\x12\x15\n\rdisplay_width\x18\x02 \x01(\r\x12\x16\n\x0e\x64isplay_height\x18\x03 \x01(\r\x12\x1c\n\x14has_device_wake_word\x18\x04 \x01(\x08\x12\x0f\n\x07has_led\x18\x05 \x01(\x08\x12\x35\n\nservo_type\x18\x06 \x01(\x0e\x32!.stackchan.websocket.v1.ServoType\x12\x1d\n\x15supports_audio_duplex\x18\x07 \x01(\x08\x12\x18\n\x10\x66irmware_version\x18\x08 \x01(\t\"F\n\x0eServerMetadata\x12\x1c\n\x14has_server_wake_word\x18\x01 \x01(\x08\x12\x16\n\x0eserver_version\x18\x02 \x01(\t*\xdf\x02\n\x0bMessageKind\x12\x1c\n\x18MESSAGE_KIND_UNSPECIFIED\x10\x00\x12\x1a\n\x16MESSAGE_KIND_AUDIO_PCM\x10\x01\x12\x1a\n\x16MESSAGE_KIND_AUDIO_WAV\x10\x02\x12\x1a\n\x16MESSAGE_KIND_STATE_CMD\x10\x03\x12\x1e\n\x1aMESSAGE_KIND_WAKE_WORD_EVT\x10\x04\x12\x1a\n\x16MESSAGE_KIND_STATE_EVT\x10\x05\x12\x1f\n\x1bMESSAGE_KIND_SPEAK_DONE_EVT\x10\x06\x12\x1a\n\x16MESSAGE_KIND_SERVO_CMD\x10\x07\x12\x1f\n\x1bMESSAGE_KIND_SERVO_DONE_EVT\x10\x08\x12\"\n\x1eMESSAGE_KIND_FIRMWARE_METADATA\x10\t\x12 \n\x1cMESSAGE_KIND_SERVER_METADATA\x10\n*p\n\x0bMessageType\x12\x1c\n\x18MESSAGE_TYPE_UNSPECIFIED\x10\x00\x12\x16\n\x12MESSAGE_TYPE_START\x10\x01\x12\x15\n\x11MESSAGE_TYPE_DATA\x10\x02\x12\x14\n\x10MESSAGE_TYPE_END\x10\x03*\x85\x01\n\x0eStackchanState\x12\x18\n\x14STACKCHAN_STATE_IDLE\x10\x00\x12\x1d\n\x19STACKCHAN_STATE_LISTENING\x10\x01\x12\x1c\n\x18STACKCHAN_STATE_THINKING\x10\x02\x12\x1c\n\x18STACKCHAN_STATE_SPEAKING\x10\x03*c\n\x0eServoOperation\x12\x19\n\x15SERVO_OPERATION_SLEEP\x10\x00\x12\x1a\n\x16SERVO_OPERATION_MOVE_X\x10\x01\x12\x1a\n\x16SERVO_OPERATION_MOVE_Y\x10\x02*\x85\x01\n\nDeviceType\x12\x1b\n\x17\x44\x45VICE_TYPE_UNSPECIFIED\x10\x00\x12\x1e\n\x1a\x44\x45VICE_TYPE_M5STACK_CORES3\x10\x01\x12\x1a\n\x16\x44\x45VICE_TYPE_M5ATOM_S3R\x10\x02\x12\x1e\n\x1a\x44\x45VICE_TYPE_M5ATOM_ECHOS3R\x10\x03*i\n\tServoType\x12\x1a\n\x16SERVO_TYPE_UNSPECIFIED\x10\x00\x12\x13\n\x0fSERVO_TYPE_NONE\x10\x01\x12\x13\n\x0fSERVO_TYPE_SG90\x10\x02\x12\x16\n\x12SERVO_TYPE_SCS0009\x10\x03\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'websocket_message_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None - _globals['_MESSAGEKIND']._serialized_start=1522 - _globals['_MESSAGEKIND']._serialized_end=1803 - _globals['_MESSAGETYPE']._serialized_start=1805 - _globals['_MESSAGETYPE']._serialized_end=1917 - _globals['_STACKCHANSTATE']._serialized_start=1920 - _globals['_STACKCHANSTATE']._serialized_end=2053 - _globals['_SERVOOPERATION']._serialized_start=2055 - _globals['_SERVOOPERATION']._serialized_end=2154 + _globals['_MESSAGEKIND']._serialized_start=2016 + _globals['_MESSAGEKIND']._serialized_end=2367 + _globals['_MESSAGETYPE']._serialized_start=2369 + _globals['_MESSAGETYPE']._serialized_end=2481 + _globals['_STACKCHANSTATE']._serialized_start=2484 + _globals['_STACKCHANSTATE']._serialized_end=2617 + _globals['_SERVOOPERATION']._serialized_start=2619 + _globals['_SERVOOPERATION']._serialized_end=2718 + _globals['_DEVICETYPE']._serialized_start=2721 + _globals['_DEVICETYPE']._serialized_end=2854 + _globals['_SERVOTYPE']._serialized_start=2856 + _globals['_SERVOTYPE']._serialized_end=2961 _globals['_WEBSOCKETMESSAGE']._serialized_start=52 - _globals['_WEBSOCKETMESSAGE']._serialized_end=960 - _globals['_AUDIOPCMSTART']._serialized_start=962 - _globals['_AUDIOPCMSTART']._serialized_end=977 - _globals['_AUDIOPCMEND']._serialized_start=979 - _globals['_AUDIOPCMEND']._serialized_end=992 - _globals['_AUDIOWAVSTART']._serialized_start=994 - _globals['_AUDIOWAVSTART']._serialized_end=1048 - _globals['_AUDIOWAVEND']._serialized_start=1050 - _globals['_AUDIOWAVEND']._serialized_end=1063 - _globals['_AUDIOCHUNK']._serialized_start=1065 - _globals['_AUDIOCHUNK']._serialized_end=1096 - _globals['_STATECOMMAND']._serialized_start=1098 - _globals['_STATECOMMAND']._serialized_end=1167 - _globals['_WAKEWORDEVENT']._serialized_start=1169 - _globals['_WAKEWORDEVENT']._serialized_end=1202 - _globals['_STATEEVENT']._serialized_start=1204 - _globals['_STATEEVENT']._serialized_end=1271 - _globals['_SPEAKDONEEVENT']._serialized_start=1273 - _globals['_SPEAKDONEEVENT']._serialized_end=1303 - _globals['_SERVOCOMMANDSEQUENCE']._serialized_start=1305 - _globals['_SERVOCOMMANDSEQUENCE']._serialized_end=1383 - _globals['_SERVOCOMMAND']._serialized_start=1385 - _globals['_SERVOCOMMAND']._serialized_end=1487 - _globals['_SERVODONEEVENT']._serialized_start=1489 - _globals['_SERVODONEEVENT']._serialized_end=1519 + _globals['_WEBSOCKETMESSAGE']._serialized_end=1098 + _globals['_AUDIOPCMSTART']._serialized_start=1100 + _globals['_AUDIOPCMSTART']._serialized_end=1115 + _globals['_AUDIOPCMEND']._serialized_start=1117 + _globals['_AUDIOPCMEND']._serialized_end=1130 + _globals['_AUDIOWAVSTART']._serialized_start=1132 + _globals['_AUDIOWAVSTART']._serialized_end=1186 + _globals['_AUDIOWAVEND']._serialized_start=1188 + _globals['_AUDIOWAVEND']._serialized_end=1201 + _globals['_AUDIOCHUNK']._serialized_start=1203 + _globals['_AUDIOCHUNK']._serialized_end=1234 + _globals['_STATECOMMAND']._serialized_start=1236 + _globals['_STATECOMMAND']._serialized_end=1305 + _globals['_WAKEWORDEVENT']._serialized_start=1307 + _globals['_WAKEWORDEVENT']._serialized_end=1340 + _globals['_STATEEVENT']._serialized_start=1342 + _globals['_STATEEVENT']._serialized_end=1409 + _globals['_SPEAKDONEEVENT']._serialized_start=1411 + _globals['_SPEAKDONEEVENT']._serialized_end=1441 + _globals['_SERVOCOMMANDSEQUENCE']._serialized_start=1443 + _globals['_SERVOCOMMANDSEQUENCE']._serialized_end=1521 + _globals['_SERVOCOMMAND']._serialized_start=1523 + _globals['_SERVOCOMMAND']._serialized_end=1625 + _globals['_SERVODONEEVENT']._serialized_start=1627 + _globals['_SERVODONEEVENT']._serialized_end=1657 + _globals['_FIRMWAREMETADATA']._serialized_start=1660 + _globals['_FIRMWAREMETADATA']._serialized_end=1941 + _globals['_SERVERMETADATA']._serialized_start=1943 + _globals['_SERVERMETADATA']._serialized_end=2013 # @@protoc_insertion_point(module_scope) diff --git a/stackchan_server/protobuf_ws.py b/stackchan_server/protobuf_ws.py index 94b808c..8569004 100644 --- a/stackchan_server/protobuf_ws.py +++ b/stackchan_server/protobuf_ws.py @@ -102,6 +102,22 @@ def encode_state_command_message(seq: int, state_id: int) -> bytes: return message.SerializeToString() +def encode_server_metadata_message( + seq: int, + *, + has_server_wake_word: bool, + server_version: str, +) -> bytes: + message = _new_message( + ws_pb2.MESSAGE_KIND_SERVER_METADATA, + ws_pb2.MESSAGE_TYPE_DATA, + seq, + ) + message.server_metadata.has_server_wake_word = bool(has_server_wake_word) + message.server_metadata.server_version = server_version + return message.SerializeToString() + + def encode_servo_command_message(seq: int, commands: Sequence[ServoCommand]) -> bytes: normalized = list(commands) _ensure_range(len(normalized), minimum=0, maximum=255, label="servo command count") @@ -163,6 +179,7 @@ def encode_servo_command_message(seq: int, commands: Sequence[ServoCommand]) -> "encode_audio_wav_data_message", "encode_audio_wav_end_message", "encode_audio_wav_start_message", + "encode_server_metadata_message", "encode_servo_command_message", "encode_state_command_message", "parse_websocket_message", diff --git a/stackchan_server/ws_proxy.py b/stackchan_server/ws_proxy.py index 488f17c..db7e86b 100644 --- a/stackchan_server/ws_proxy.py +++ b/stackchan_server/ws_proxy.py @@ -4,6 +4,7 @@ import os from collections import deque from contextlib import suppress +from dataclasses import dataclass from enum import IntEnum, StrEnum from logging import getLogger from pathlib import Path @@ -12,9 +13,11 @@ from fastapi import WebSocket, WebSocketDisconnect from google.protobuf.message import DecodeError +from . import __version__ from .generated_protobuf import websocket_message_pb2 as _ws_pb2 from .listen import EmptyTranscriptError, ListenHandler, TimeoutError from .protobuf_ws import ( + encode_server_metadata_message, encode_servo_command_message, encode_state_command_message, parse_websocket_message, @@ -64,6 +67,24 @@ class ServoWaitType(StrEnum): ServoCommand: TypeAlias = ServoMoveCommand | ServoSleepCommand +@dataclass(frozen=True) +class FirmwareMetadata: + device_type: int + display_width: int + display_height: int + has_device_wake_word: bool + has_led: bool + servo_type: int + supports_audio_duplex: bool + firmware_version: str + + +@dataclass(frozen=True) +class ServerMetadata: + has_server_wake_word: bool + server_version: str + + class WsProxy: def __init__( self, @@ -100,6 +121,12 @@ def __init__( self._receiving_task: Optional[asyncio.Task] = None self._closed = False + self.firmware_metadata: FirmwareMetadata | None = None + self.server_metadata = ServerMetadata( + has_server_wake_word=False, + server_version=__version__, + ) + self._down_seq = 0 self._current_firmware_state: FirmwareState = FirmwareState.IDLE self._servo_done_counter = 0 @@ -189,6 +216,13 @@ async def wait_servo_complete(self, timeout_seconds: float | None = 120.0) -> No async def start(self) -> None: if self._receiving_task is None: + await self.ws.send_bytes( + encode_server_metadata_message( + self._next_down_seq(), + has_server_wake_word=self.server_metadata.has_server_wake_word, + server_version=self.server_metadata.server_version, + ) + ) self._receiving_task = asyncio.create_task(self._receive_loop()) async def close(self) -> None: @@ -254,6 +288,10 @@ async def _receive_loop(self) -> None: self._handle_wakeword_event(message) continue + if message.kind == ws_pb2.MESSAGE_KIND_FIRMWARE_METADATA: + self._handle_firmware_metadata(message) + continue + if message.kind == ws_pb2.MESSAGE_KIND_STATE_EVT: self._handle_state_event(message) continue @@ -283,6 +321,35 @@ def _handle_wakeword_event(self, message: Any) -> None: logger.info("Received wakeword event") self._wakeword_event.set() + def _handle_firmware_metadata(self, message: Any) -> None: + if message.message_type != ws_pb2.MESSAGE_TYPE_DATA: + return + if message.WhichOneof("body") != "firmware_metadata": + return + + metadata = message.firmware_metadata + self.firmware_metadata = FirmwareMetadata( + device_type=int(metadata.device_type), + display_width=int(metadata.display_width), + display_height=int(metadata.display_height), + has_device_wake_word=bool(metadata.has_device_wake_word), + has_led=bool(metadata.has_led), + servo_type=int(metadata.servo_type), + supports_audio_duplex=bool(metadata.supports_audio_duplex), + firmware_version=metadata.firmware_version, + ) + logger.info( + "Received firmware metadata device_type=%d display=%dx%d wakeword=%s led=%s servo_type=%d duplex=%s version=%s", + self.firmware_metadata.device_type, + self.firmware_metadata.display_width, + self.firmware_metadata.display_height, + self.firmware_metadata.has_device_wake_word, + self.firmware_metadata.has_led, + self.firmware_metadata.servo_type, + self.firmware_metadata.supports_audio_duplex, + self.firmware_metadata.firmware_version, + ) + def _handle_state_event(self, message: Any) -> None: if message.message_type != ws_pb2.MESSAGE_TYPE_DATA: return @@ -348,7 +415,9 @@ def _next_down_seq(self) -> int: __all__ = [ "WsProxy", + "FirmwareMetadata", "FirmwareState", + "ServerMetadata", "TimeoutError", "EmptyTranscriptError", "ServoCommand", From 0d39e597d734524fd59397664b7f35de60f8782d Mon Sep 17 00:00:00 2001 From: Atsushi Morimoto <74th.tech@gmail.com> Date: Sun, 19 Apr 2026 16:40:19 +0900 Subject: [PATCH 2/2] feat: implement wake word configuration and handling in firmware and server --- .env.template | 5 ++++ firmware/include/metadata.hpp | 1 + firmware/src/main.cpp | 21 ++++++++++++++-- firmware/src/metadata.cpp | 5 ++++ stackchan_server/ws_proxy.py | 46 ++++++++++++++++++++++++++++------- 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/.env.template b/.env.template index eadd276..18b2615 100644 --- a/.env.template +++ b/.env.template @@ -2,6 +2,11 @@ GCLOUD_PROJECT="your-gcloud-project-id" GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account-key.json" +# -- Wakeup Word -- +# STACKCHAN_NO_USE_CLIENT_WAKEUP_WORD=1 + +# STACKCHAN_USE_OPEN_WAKE_WORD=1 + # -- Speech Recognition -- # Google Cloud STT STACKCHAN_USE_GOOGLE_CLOUD_STT=1 diff --git a/firmware/include/metadata.hpp b/firmware/include/metadata.hpp index e4ec10d..f490abd 100644 --- a/firmware/include/metadata.hpp +++ b/firmware/include/metadata.hpp @@ -28,6 +28,7 @@ extern ServerMetadataState g_server_metadata; void initializeFirmwareMetadata(); void resetServerMetadata(); +bool shouldUseDeviceWakeWord(); void setFirmwareMetadataMessage( stackchan_websocket_v1_WebSocketMessage &message, uint32_t seq); diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 5fffcb2..9756ec0 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -340,6 +340,17 @@ void handleWsEvent(WStype_t type, uint8_t *payload, size_t length) rx.which_body == stackchan_websocket_v1_WebSocketMessage_server_metadata_tag) { applyServerMetadata(rx.body.server_metadata); + if (stateMachine.getState() == StateMachine::Idle) + { + if (shouldUseDeviceWakeWord()) + { + wakeUpWord.begin(); + } + else + { + wakeUpWord.end(); + } + } } else { @@ -405,7 +416,10 @@ void setup() // State entry/exit hooks stateMachine.addStateEntryEvent(StateMachine::Idle, [](StateMachine::State, StateMachine::State) { notifyCurrentState(StateMachine::Idle); - wakeUpWord.begin(); + if (shouldUseDeviceWakeWord()) + { + wakeUpWord.begin(); + } }); stateMachine.addStateExitEvent(StateMachine::Idle, [](StateMachine::State, StateMachine::State) { wakeUpWord.end(); @@ -443,7 +457,10 @@ void loop() switch (current) { case StateMachine::Idle: - wakeUpWord.loop(); + if (shouldUseDeviceWakeWord()) + { + wakeUpWord.loop(); + } break; case StateMachine::Listening: listening.loop(); diff --git a/firmware/src/metadata.cpp b/firmware/src/metadata.cpp index e4e5ef0..b37c5c9 100644 --- a/firmware/src/metadata.cpp +++ b/firmware/src/metadata.cpp @@ -66,6 +66,11 @@ void resetServerMetadata() g_server_metadata = ServerMetadataState{}; } +bool shouldUseDeviceWakeWord() +{ + return g_server_metadata.available && !g_server_metadata.has_server_wake_word; +} + void setFirmwareMetadataMessage( stackchan_websocket_v1_WebSocketMessage &message, uint32_t seq) diff --git a/stackchan_server/ws_proxy.py b/stackchan_server/ws_proxy.py index db7e86b..1c45236 100644 --- a/stackchan_server/ws_proxy.py +++ b/stackchan_server/ws_proxy.py @@ -12,6 +12,7 @@ from fastapi import WebSocket, WebSocketDisconnect from google.protobuf.message import DecodeError +from pydantic_settings import BaseSettings from . import __version__ from .generated_protobuf import websocket_message_pb2 as _ws_pb2 @@ -44,6 +45,17 @@ _DEBUG_RECORDING_ENABLED = os.getenv("DEBUG_RECODING") == "1" +class _WakeWordServerConfig(BaseSettings): + no_use_client_wakeup_word: bool = False + use_open_wake_word: bool = False + + class Config: + env_prefix = "STACKCHAN_" + + +_WAKEWORD_SERVER_CONFIG = _WakeWordServerConfig() + + class FirmwareState(IntEnum): IDLE = 0 LISTENING = 1 @@ -216,13 +228,6 @@ async def wait_servo_complete(self, timeout_seconds: float | None = 120.0) -> No async def start(self) -> None: if self._receiving_task is None: - await self.ws.send_bytes( - encode_server_metadata_message( - self._next_down_seq(), - has_server_wake_word=self.server_metadata.has_server_wake_word, - server_version=self.server_metadata.server_version, - ) - ) self._receiving_task = asyncio.create_task(self._receive_loop()) async def close(self) -> None: @@ -289,7 +294,7 @@ async def _receive_loop(self) -> None: continue if message.kind == ws_pb2.MESSAGE_KIND_FIRMWARE_METADATA: - self._handle_firmware_metadata(message) + await self._handle_firmware_metadata(message) continue if message.kind == ws_pb2.MESSAGE_KIND_STATE_EVT: @@ -321,7 +326,7 @@ def _handle_wakeword_event(self, message: Any) -> None: logger.info("Received wakeword event") self._wakeword_event.set() - def _handle_firmware_metadata(self, message: Any) -> None: + async def _handle_firmware_metadata(self, message: Any) -> None: if message.message_type != ws_pb2.MESSAGE_TYPE_DATA: return if message.WhichOneof("body") != "firmware_metadata": @@ -349,6 +354,29 @@ def _handle_firmware_metadata(self, message: Any) -> None: self.firmware_metadata.supports_audio_duplex, self.firmware_metadata.firmware_version, ) + self.server_metadata = self._build_server_metadata(self.firmware_metadata) + await self.ws.send_bytes( + encode_server_metadata_message( + self._next_down_seq(), + has_server_wake_word=self.server_metadata.has_server_wake_word, + server_version=self.server_metadata.server_version, + ) + ) + + def _build_server_metadata( + self, firmware_metadata: FirmwareMetadata + ) -> ServerMetadata: + should_use_server_wake_word = ( + _WAKEWORD_SERVER_CONFIG.use_open_wake_word + and ( + _WAKEWORD_SERVER_CONFIG.no_use_client_wakeup_word + or not firmware_metadata.has_device_wake_word + ) + ) + return ServerMetadata( + has_server_wake_word=should_use_server_wake_word, + server_version=__version__, + ) def _handle_state_event(self, message: Any) -> None: if message.message_type != ws_pb2.MESSAGE_TYPE_DATA: