feat(lobby): implement bounded WebSocket sessions and countdown
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
idf_component_register(
|
||||
SRCS "maze32_lobby.c" "maze32_protocol.c"
|
||||
INCLUDE_DIRS "include"
|
||||
)
|
||||
@@ -0,0 +1,82 @@
|
||||
#ifndef MAZE32_LOBBY_H
|
||||
#define MAZE32_LOBBY_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MAZE32_PLAYER_LIMIT 2U
|
||||
#define MAZE32_OBSERVER_LIMIT 4U
|
||||
#define MAZE32_SESSION_LIMIT (MAZE32_PLAYER_LIMIT + MAZE32_OBSERVER_LIMIT)
|
||||
#define MAZE32_PLAYER_NAME_MAX_BYTES 32U
|
||||
#define MAZE32_SESSION_TOKEN_BYTES 16U
|
||||
#define MAZE32_SESSION_TOKEN_HEX_LENGTH (MAZE32_SESSION_TOKEN_BYTES * 2U)
|
||||
#define MAZE32_COUNTDOWN_DURATION_MS 3000U
|
||||
|
||||
typedef enum {
|
||||
MAZE32_ROLE_NONE = 0,
|
||||
MAZE32_ROLE_PLAYER,
|
||||
MAZE32_ROLE_OBSERVER,
|
||||
} maze32_role_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_LOBBY_WAITING = 0,
|
||||
MAZE32_LOBBY_COUNTDOWN,
|
||||
MAZE32_LOBBY_ACTIVE,
|
||||
} maze32_lobby_phase_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_LOBBY_OK = 0,
|
||||
MAZE32_LOBBY_INVALID_NAME,
|
||||
MAZE32_LOBBY_DUPLICATE_NAME,
|
||||
MAZE32_LOBBY_ROLE_FULL,
|
||||
MAZE32_LOBBY_INVALID_TOKEN,
|
||||
MAZE32_LOBBY_SESSION_CONNECTED,
|
||||
MAZE32_LOBBY_NOT_OWNER,
|
||||
MAZE32_LOBBY_NOT_READY,
|
||||
MAZE32_LOBBY_INVALID_PHASE,
|
||||
} maze32_lobby_result_t;
|
||||
|
||||
typedef struct {
|
||||
int connection_id;
|
||||
maze32_role_t role;
|
||||
unsigned int player_slot;
|
||||
int connected;
|
||||
char name[MAZE32_PLAYER_NAME_MAX_BYTES + 1U];
|
||||
char token[MAZE32_SESSION_TOKEN_HEX_LENGTH + 1U];
|
||||
} maze32_session_t;
|
||||
|
||||
typedef struct {
|
||||
maze32_session_t sessions[MAZE32_SESSION_LIMIT];
|
||||
maze32_lobby_phase_t phase;
|
||||
uint64_t countdown_started_ms;
|
||||
uint64_t activation_ms;
|
||||
uint32_t revision;
|
||||
} maze32_lobby_t;
|
||||
|
||||
void maze32_lobby_init(maze32_lobby_t* lobby);
|
||||
maze32_lobby_result_t maze32_lobby_join_player(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* name, const char* token,
|
||||
maze32_session_t** session);
|
||||
maze32_lobby_result_t maze32_lobby_join_observer(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* token, maze32_session_t** session);
|
||||
maze32_lobby_result_t maze32_lobby_restore(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* token, maze32_session_t** session);
|
||||
void maze32_lobby_disconnect(maze32_lobby_t* lobby, int connection_id);
|
||||
maze32_lobby_result_t maze32_lobby_start(maze32_lobby_t* lobby, int connection_id, uint64_t now_ms);
|
||||
int maze32_lobby_activate_if_due(maze32_lobby_t* lobby, uint64_t now_ms);
|
||||
const maze32_session_t* maze32_lobby_session_for_connection(const maze32_lobby_t* lobby,
|
||||
int connection_id);
|
||||
unsigned int maze32_lobby_connected_observers(const maze32_lobby_t* lobby);
|
||||
int maze32_lobby_ready(const maze32_lobby_t* lobby);
|
||||
int maze32_lobby_can_reconfigure(const maze32_lobby_t* lobby);
|
||||
const char* maze32_lobby_phase_name(maze32_lobby_phase_t phase);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef MAZE32_PROTOCOL_H
|
||||
#define MAZE32_PROTOCOL_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "maze32_lobby.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MAZE32_PROTOCOL_VERSION 1U
|
||||
#define MAZE32_PROTOCOL_MESSAGE_LIMIT 512U
|
||||
#define MAZE32_REQUEST_ID_MAX_BYTES 32U
|
||||
|
||||
typedef enum {
|
||||
MAZE32_MESSAGE_INVALID = 0,
|
||||
MAZE32_MESSAGE_JOIN,
|
||||
MAZE32_MESSAGE_RESTORE,
|
||||
MAZE32_MESSAGE_START,
|
||||
MAZE32_MESSAGE_HEARTBEAT,
|
||||
MAZE32_MESSAGE_SUGGEST_NAME,
|
||||
} maze32_message_type_t;
|
||||
|
||||
typedef enum {
|
||||
MAZE32_PROTOCOL_OK = 0,
|
||||
MAZE32_PROTOCOL_MALFORMED,
|
||||
MAZE32_PROTOCOL_TOO_LARGE,
|
||||
MAZE32_PROTOCOL_UNSUPPORTED_VERSION,
|
||||
MAZE32_PROTOCOL_UNKNOWN_TYPE,
|
||||
MAZE32_PROTOCOL_INVALID_PAYLOAD,
|
||||
} maze32_protocol_result_t;
|
||||
|
||||
typedef struct {
|
||||
maze32_message_type_t type;
|
||||
uint32_t sequence;
|
||||
char request_id[MAZE32_REQUEST_ID_MAX_BYTES + 1U];
|
||||
maze32_role_t role;
|
||||
char name[MAZE32_PLAYER_NAME_MAX_BYTES + 1U];
|
||||
char token[MAZE32_SESSION_TOKEN_HEX_LENGTH + 1U];
|
||||
} maze32_client_message_t;
|
||||
|
||||
maze32_protocol_result_t maze32_protocol_parse(const char* data, size_t length,
|
||||
maze32_client_message_t* message);
|
||||
const char* maze32_protocol_result_name(maze32_protocol_result_t result);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,263 @@
|
||||
#include "maze32_lobby.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static int is_valid_name(const char* name) {
|
||||
if (name == NULL) {
|
||||
return 0;
|
||||
}
|
||||
size_t length = strlen(name);
|
||||
if (length == 0U || length > MAZE32_PLAYER_NAME_MAX_BYTES || name[0] == ' ' ||
|
||||
name[length - 1U] == ' ') {
|
||||
return 0;
|
||||
}
|
||||
for (size_t index = 0U; index < length; ++index) {
|
||||
unsigned char byte = (unsigned char)name[index];
|
||||
if (byte < 0x20U || byte == 0x7fU) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int is_valid_token(const char* token) {
|
||||
if (token == NULL || strlen(token) != MAZE32_SESSION_TOKEN_HEX_LENGTH) {
|
||||
return 0;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_TOKEN_HEX_LENGTH; ++index) {
|
||||
char character = token[index];
|
||||
if (!((character >= '0' && character <= '9') || (character >= 'a' && character <= 'f'))) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static maze32_session_t* find_player_slot(maze32_lobby_t* lobby, unsigned int slot) {
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role == MAZE32_ROLE_PLAYER && session->player_slot == slot) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static maze32_session_t* find_free_session(maze32_lobby_t* lobby) {
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
if (lobby->sessions[index].role == MAZE32_ROLE_NONE) {
|
||||
return &lobby->sessions[index];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static maze32_session_t* find_reusable_observer(maze32_lobby_t* lobby) {
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role == MAZE32_ROLE_OBSERVER && !session->connected) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void maze32_lobby_init(maze32_lobby_t* lobby) {
|
||||
if (lobby == NULL) {
|
||||
return;
|
||||
}
|
||||
memset(lobby, 0, sizeof(*lobby));
|
||||
lobby->phase = MAZE32_LOBBY_WAITING;
|
||||
}
|
||||
|
||||
maze32_lobby_result_t maze32_lobby_join_player(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* name, const char* token,
|
||||
maze32_session_t** session) {
|
||||
if (lobby == NULL || session == NULL || !is_valid_name(name) || !is_valid_token(token)) {
|
||||
return MAZE32_LOBBY_INVALID_NAME;
|
||||
}
|
||||
if (lobby->phase != MAZE32_LOBBY_WAITING) {
|
||||
return MAZE32_LOBBY_INVALID_PHASE;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
if (lobby->sessions[index].role == MAZE32_ROLE_PLAYER &&
|
||||
strcmp(lobby->sessions[index].name, name) == 0) {
|
||||
return MAZE32_LOBBY_DUPLICATE_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int slot = find_player_slot(lobby, 1U) == NULL ? 1U : 2U;
|
||||
if (find_player_slot(lobby, slot) != NULL) {
|
||||
return MAZE32_LOBBY_ROLE_FULL;
|
||||
}
|
||||
maze32_session_t* target = find_free_session(lobby);
|
||||
if (target == NULL) {
|
||||
return MAZE32_LOBBY_ROLE_FULL;
|
||||
}
|
||||
target->connection_id = connection_id;
|
||||
target->role = MAZE32_ROLE_PLAYER;
|
||||
target->player_slot = slot;
|
||||
target->connected = 1;
|
||||
memcpy(target->name, name, strlen(name) + 1U);
|
||||
memcpy(target->token, token, MAZE32_SESSION_TOKEN_HEX_LENGTH + 1U);
|
||||
lobby->revision++;
|
||||
*session = target;
|
||||
return MAZE32_LOBBY_OK;
|
||||
}
|
||||
|
||||
maze32_lobby_result_t maze32_lobby_join_observer(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* token, maze32_session_t** session) {
|
||||
if (lobby == NULL || session == NULL || !is_valid_token(token)) {
|
||||
return MAZE32_LOBBY_INVALID_TOKEN;
|
||||
}
|
||||
if (maze32_lobby_connected_observers(lobby) >= MAZE32_OBSERVER_LIMIT) {
|
||||
return MAZE32_LOBBY_ROLE_FULL;
|
||||
}
|
||||
maze32_session_t* target = find_free_session(lobby);
|
||||
if (target == NULL) {
|
||||
target = find_reusable_observer(lobby);
|
||||
}
|
||||
if (target == NULL) {
|
||||
return MAZE32_LOBBY_ROLE_FULL;
|
||||
}
|
||||
memset(target, 0, sizeof(*target));
|
||||
target->connection_id = connection_id;
|
||||
target->role = MAZE32_ROLE_OBSERVER;
|
||||
target->connected = 1;
|
||||
memcpy(target->token, token, MAZE32_SESSION_TOKEN_HEX_LENGTH + 1U);
|
||||
lobby->revision++;
|
||||
*session = target;
|
||||
return MAZE32_LOBBY_OK;
|
||||
}
|
||||
|
||||
maze32_lobby_result_t maze32_lobby_restore(maze32_lobby_t* lobby, int connection_id,
|
||||
const char* token, maze32_session_t** session) {
|
||||
if (lobby == NULL || session == NULL || !is_valid_token(token)) {
|
||||
return MAZE32_LOBBY_INVALID_TOKEN;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
maze32_session_t* candidate = &lobby->sessions[index];
|
||||
if (candidate->role != MAZE32_ROLE_NONE && strcmp(candidate->token, token) == 0) {
|
||||
if (candidate->connected) {
|
||||
return MAZE32_LOBBY_SESSION_CONNECTED;
|
||||
}
|
||||
candidate->connected = 1;
|
||||
candidate->connection_id = connection_id;
|
||||
lobby->revision++;
|
||||
*session = candidate;
|
||||
return MAZE32_LOBBY_OK;
|
||||
}
|
||||
}
|
||||
return MAZE32_LOBBY_INVALID_TOKEN;
|
||||
}
|
||||
|
||||
void maze32_lobby_disconnect(maze32_lobby_t* lobby, int connection_id) {
|
||||
if (lobby == NULL) {
|
||||
return;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role != MAZE32_ROLE_NONE && session->connected &&
|
||||
session->connection_id == connection_id) {
|
||||
session->connected = 0;
|
||||
session->connection_id = -1;
|
||||
lobby->revision++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maze32_lobby_result_t maze32_lobby_start(maze32_lobby_t* lobby, int connection_id,
|
||||
uint64_t now_ms) {
|
||||
if (lobby == NULL || lobby->phase != MAZE32_LOBBY_WAITING) {
|
||||
return MAZE32_LOBBY_INVALID_PHASE;
|
||||
}
|
||||
const maze32_session_t* session = maze32_lobby_session_for_connection(lobby, connection_id);
|
||||
if (session == NULL || session->role != MAZE32_ROLE_PLAYER || session->player_slot != 1U) {
|
||||
return MAZE32_LOBBY_NOT_OWNER;
|
||||
}
|
||||
if (!maze32_lobby_ready(lobby)) {
|
||||
return MAZE32_LOBBY_NOT_READY;
|
||||
}
|
||||
lobby->phase = MAZE32_LOBBY_COUNTDOWN;
|
||||
lobby->countdown_started_ms = now_ms;
|
||||
lobby->activation_ms = now_ms + MAZE32_COUNTDOWN_DURATION_MS;
|
||||
lobby->revision++;
|
||||
return MAZE32_LOBBY_OK;
|
||||
}
|
||||
|
||||
int maze32_lobby_activate_if_due(maze32_lobby_t* lobby, uint64_t now_ms) {
|
||||
if (lobby == NULL || lobby->phase != MAZE32_LOBBY_COUNTDOWN || now_ms < lobby->activation_ms) {
|
||||
return 0;
|
||||
}
|
||||
lobby->phase = MAZE32_LOBBY_ACTIVE;
|
||||
lobby->revision++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
const maze32_session_t* maze32_lobby_session_for_connection(const maze32_lobby_t* lobby,
|
||||
int connection_id) {
|
||||
if (lobby == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
const maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role != MAZE32_ROLE_NONE && session->connected &&
|
||||
session->connection_id == connection_id) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
unsigned int maze32_lobby_connected_observers(const maze32_lobby_t* lobby) {
|
||||
unsigned int count = 0U;
|
||||
if (lobby == NULL) {
|
||||
return count;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
const maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role == MAZE32_ROLE_OBSERVER && session->connected) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int maze32_lobby_ready(const maze32_lobby_t* lobby) {
|
||||
if (lobby == NULL) {
|
||||
return 0;
|
||||
}
|
||||
for (unsigned int slot = 1U; slot <= MAZE32_PLAYER_LIMIT; ++slot) {
|
||||
int found = 0;
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
const maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role == MAZE32_ROLE_PLAYER && session->player_slot == slot &&
|
||||
session->connected) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int maze32_lobby_can_reconfigure(const maze32_lobby_t* lobby) {
|
||||
return lobby != NULL && lobby->phase == MAZE32_LOBBY_WAITING;
|
||||
}
|
||||
|
||||
const char* maze32_lobby_phase_name(maze32_lobby_phase_t phase) {
|
||||
switch (phase) {
|
||||
case MAZE32_LOBBY_WAITING:
|
||||
return "waiting";
|
||||
case MAZE32_LOBBY_COUNTDOWN:
|
||||
return "countdown";
|
||||
case MAZE32_LOBBY_ACTIVE:
|
||||
return "active";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
#include "maze32_protocol.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
const char* data;
|
||||
size_t length;
|
||||
size_t position;
|
||||
} json_cursor_t;
|
||||
|
||||
static void skip_space(json_cursor_t* cursor) {
|
||||
while (cursor->position < cursor->length) {
|
||||
char character = cursor->data[cursor->position];
|
||||
if (character != ' ' && character != '\t' && character != '\r' && character != '\n') {
|
||||
return;
|
||||
}
|
||||
cursor->position++;
|
||||
}
|
||||
}
|
||||
|
||||
static int consume(json_cursor_t* cursor, char expected) {
|
||||
skip_space(cursor);
|
||||
if (cursor->position >= cursor->length || cursor->data[cursor->position] != expected) {
|
||||
return 0;
|
||||
}
|
||||
cursor->position++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_string(json_cursor_t* cursor, char* output, size_t capacity) {
|
||||
skip_space(cursor);
|
||||
if (cursor->position >= cursor->length || cursor->data[cursor->position++] != '"') {
|
||||
return 0;
|
||||
}
|
||||
size_t output_length = 0U;
|
||||
while (cursor->position < cursor->length) {
|
||||
unsigned char character = (unsigned char)cursor->data[cursor->position++];
|
||||
if (character == '"') {
|
||||
if (output_length >= capacity) {
|
||||
return 0;
|
||||
}
|
||||
output[output_length] = '\0';
|
||||
return 1;
|
||||
}
|
||||
if (character < 0x20U) {
|
||||
return 0;
|
||||
}
|
||||
if (character == '\\') {
|
||||
if (cursor->position >= cursor->length) {
|
||||
return 0;
|
||||
}
|
||||
character = (unsigned char)cursor->data[cursor->position++];
|
||||
if (character == '"' || character == '\\' || character == '/') {
|
||||
/* The escaped byte is copied below. */
|
||||
} else if (character == 'b') {
|
||||
character = '\b';
|
||||
} else if (character == 'f') {
|
||||
character = '\f';
|
||||
} else if (character == 'n') {
|
||||
character = '\n';
|
||||
} else if (character == 'r') {
|
||||
character = '\r';
|
||||
} else if (character == 't') {
|
||||
character = '\t';
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (output_length + 1U >= capacity) {
|
||||
return 0;
|
||||
}
|
||||
output[output_length++] = (char)character;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_uint32(json_cursor_t* cursor, uint32_t* value) {
|
||||
skip_space(cursor);
|
||||
if (cursor->position >= cursor->length || cursor->data[cursor->position] < '0' ||
|
||||
cursor->data[cursor->position] > '9') {
|
||||
return 0;
|
||||
}
|
||||
uint64_t parsed = 0U;
|
||||
while (cursor->position < cursor->length && cursor->data[cursor->position] >= '0' &&
|
||||
cursor->data[cursor->position] <= '9') {
|
||||
parsed = parsed * 10U + (uint64_t)(cursor->data[cursor->position] - '0');
|
||||
if (parsed > UINT32_MAX) {
|
||||
return 0;
|
||||
}
|
||||
cursor->position++;
|
||||
}
|
||||
*value = (uint32_t)parsed;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int payload_field_allowed(maze32_message_type_t type, const char* key, char* value,
|
||||
maze32_client_message_t* message, unsigned int* fields) {
|
||||
if (type == MAZE32_MESSAGE_JOIN && strcmp(key, "role") == 0 && !(*fields & 1U)) {
|
||||
if (strcmp(value, "player") == 0) {
|
||||
message->role = MAZE32_ROLE_PLAYER;
|
||||
} else if (strcmp(value, "observer") == 0) {
|
||||
message->role = MAZE32_ROLE_OBSERVER;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
*fields |= 1U;
|
||||
return 1;
|
||||
}
|
||||
if (type == MAZE32_MESSAGE_JOIN && strcmp(key, "name") == 0 && !(*fields & 2U)) {
|
||||
if (strlen(value) > MAZE32_PLAYER_NAME_MAX_BYTES) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(message->name, value, strlen(value) + 1U);
|
||||
*fields |= 2U;
|
||||
return 1;
|
||||
}
|
||||
if (type == MAZE32_MESSAGE_RESTORE && strcmp(key, "token") == 0 && !(*fields & 1U)) {
|
||||
if (strlen(value) != MAZE32_SESSION_TOKEN_HEX_LENGTH) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(message->token, value, MAZE32_SESSION_TOKEN_HEX_LENGTH + 1U);
|
||||
*fields |= 1U;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_payload(json_cursor_t* cursor, maze32_client_message_t* message) {
|
||||
if (!consume(cursor, '{')) {
|
||||
return 0;
|
||||
}
|
||||
unsigned int fields = 0U;
|
||||
skip_space(cursor);
|
||||
if (cursor->position < cursor->length && cursor->data[cursor->position] == '}') {
|
||||
cursor->position++;
|
||||
} else {
|
||||
for (;;) {
|
||||
char key[16];
|
||||
char value[MAZE32_PLAYER_NAME_MAX_BYTES + 1U];
|
||||
if (!parse_string(cursor, key, sizeof(key)) || !consume(cursor, ':') ||
|
||||
!parse_string(cursor, value, sizeof(value)) ||
|
||||
!payload_field_allowed(message->type, key, value, message, &fields)) {
|
||||
return 0;
|
||||
}
|
||||
skip_space(cursor);
|
||||
if (consume(cursor, '}')) {
|
||||
break;
|
||||
}
|
||||
if (!consume(cursor, ',')) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message->type == MAZE32_MESSAGE_JOIN) {
|
||||
return message->role == MAZE32_ROLE_PLAYER ? fields == 3U : fields == 1U;
|
||||
}
|
||||
if (message->type == MAZE32_MESSAGE_RESTORE) {
|
||||
return fields == 1U;
|
||||
}
|
||||
return fields == 0U;
|
||||
}
|
||||
|
||||
static maze32_message_type_t message_type(const char* value) {
|
||||
if (strcmp(value, "join") == 0) {
|
||||
return MAZE32_MESSAGE_JOIN;
|
||||
}
|
||||
if (strcmp(value, "restore") == 0) {
|
||||
return MAZE32_MESSAGE_RESTORE;
|
||||
}
|
||||
if (strcmp(value, "start") == 0) {
|
||||
return MAZE32_MESSAGE_START;
|
||||
}
|
||||
if (strcmp(value, "heartbeat") == 0) {
|
||||
return MAZE32_MESSAGE_HEARTBEAT;
|
||||
}
|
||||
if (strcmp(value, "suggest_name") == 0) {
|
||||
return MAZE32_MESSAGE_SUGGEST_NAME;
|
||||
}
|
||||
return MAZE32_MESSAGE_INVALID;
|
||||
}
|
||||
|
||||
maze32_protocol_result_t maze32_protocol_parse(const char* data, size_t length,
|
||||
maze32_client_message_t* message) {
|
||||
if (data == NULL || message == NULL || length == 0U) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
if (length > MAZE32_PROTOCOL_MESSAGE_LIMIT) {
|
||||
return MAZE32_PROTOCOL_TOO_LARGE;
|
||||
}
|
||||
memset(message, 0, sizeof(*message));
|
||||
json_cursor_t cursor = {.data = data, .length = length, .position = 0U};
|
||||
if (!consume(&cursor, '{')) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
|
||||
unsigned int envelope_fields = 0U;
|
||||
uint32_t version = 0U;
|
||||
char type_value[24] = "";
|
||||
for (;;) {
|
||||
char key[24];
|
||||
if (!parse_string(&cursor, key, sizeof(key)) || !consume(&cursor, ':')) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
if (strcmp(key, "v") == 0 && !(envelope_fields & 1U)) {
|
||||
if (!parse_uint32(&cursor, &version)) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
envelope_fields |= 1U;
|
||||
} else if (strcmp(key, "type") == 0 && !(envelope_fields & 2U)) {
|
||||
if (!parse_string(&cursor, type_value, sizeof(type_value))) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
message->type = message_type(type_value);
|
||||
envelope_fields |= 2U;
|
||||
} else if (strcmp(key, "seq") == 0 && !(envelope_fields & 4U)) {
|
||||
if (!parse_uint32(&cursor, &message->sequence) || message->sequence == 0U) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
envelope_fields |= 4U;
|
||||
} else if (strcmp(key, "requestId") == 0 && !(envelope_fields & 8U)) {
|
||||
if (!parse_string(&cursor, message->request_id, sizeof(message->request_id))) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
envelope_fields |= 8U;
|
||||
} else if (strcmp(key, "payload") == 0 && !(envelope_fields & 16U)) {
|
||||
if (!(envelope_fields & 2U)) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
if (message->type == MAZE32_MESSAGE_INVALID) {
|
||||
return MAZE32_PROTOCOL_UNKNOWN_TYPE;
|
||||
}
|
||||
if (!parse_payload(&cursor, message)) {
|
||||
return MAZE32_PROTOCOL_INVALID_PAYLOAD;
|
||||
}
|
||||
envelope_fields |= 16U;
|
||||
} else {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
skip_space(&cursor);
|
||||
if (consume(&cursor, '}')) {
|
||||
break;
|
||||
}
|
||||
if (!consume(&cursor, ',')) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
}
|
||||
skip_space(&cursor);
|
||||
if (cursor.position != cursor.length ||
|
||||
(envelope_fields & (1U | 2U | 4U | 16U)) != (1U | 2U | 4U | 16U)) {
|
||||
return MAZE32_PROTOCOL_MALFORMED;
|
||||
}
|
||||
if (version != MAZE32_PROTOCOL_VERSION) {
|
||||
return MAZE32_PROTOCOL_UNSUPPORTED_VERSION;
|
||||
}
|
||||
if (message->type == MAZE32_MESSAGE_INVALID) {
|
||||
return MAZE32_PROTOCOL_UNKNOWN_TYPE;
|
||||
}
|
||||
return MAZE32_PROTOCOL_OK;
|
||||
}
|
||||
|
||||
const char* maze32_protocol_result_name(maze32_protocol_result_t result) {
|
||||
switch (result) {
|
||||
case MAZE32_PROTOCOL_OK:
|
||||
return "ok";
|
||||
case MAZE32_PROTOCOL_MALFORMED:
|
||||
return "malformed";
|
||||
case MAZE32_PROTOCOL_TOO_LARGE:
|
||||
return "too_large";
|
||||
case MAZE32_PROTOCOL_UNSUPPORTED_VERSION:
|
||||
return "unsupported_version";
|
||||
case MAZE32_PROTOCOL_UNKNOWN_TYPE:
|
||||
return "unknown_type";
|
||||
case MAZE32_PROTOCOL_INVALID_PAYLOAD:
|
||||
return "invalid_payload";
|
||||
default:
|
||||
return "unknown_error";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "maze32_web.c"
|
||||
SRCS "maze32_web.c" "maze32_gateway.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES esp_http_server maze32_platform
|
||||
REQUIRES esp_http_server esp_timer heap maze32_lobby maze32_platform
|
||||
)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#ifndef MAZE32_GATEWAY_H
|
||||
#define MAZE32_GATEWAY_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_timer.h"
|
||||
#include "maze32_lobby.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MAZE32_GATEWAY_CONNECTION_LIMIT MAZE32_SESSION_LIMIT
|
||||
#define MAZE32_GATEWAY_QUEUE_LIMIT 12U
|
||||
#define MAZE32_GATEWAY_OUTGOING_LIMIT 768U
|
||||
#define MAZE32_GATEWAY_RATE_LIMIT_PER_SECOND 20U
|
||||
|
||||
typedef struct {
|
||||
int active;
|
||||
int socket_fd;
|
||||
uint32_t last_client_sequence;
|
||||
uint64_t rate_window_started_ms;
|
||||
unsigned int messages_in_window;
|
||||
} maze32_gateway_connection_t;
|
||||
|
||||
typedef struct {
|
||||
int socket_fd;
|
||||
size_t length;
|
||||
char payload[MAZE32_GATEWAY_OUTGOING_LIMIT];
|
||||
} maze32_gateway_outgoing_t;
|
||||
|
||||
typedef struct {
|
||||
httpd_handle_t server;
|
||||
maze32_lobby_t lobby;
|
||||
maze32_gateway_connection_t connections[MAZE32_GATEWAY_CONNECTION_LIMIT];
|
||||
maze32_gateway_outgoing_t outgoing[MAZE32_GATEWAY_QUEUE_LIMIT];
|
||||
size_t outgoing_head;
|
||||
size_t outgoing_count;
|
||||
uint32_t server_sequence;
|
||||
unsigned int suggestion_index;
|
||||
esp_timer_handle_t countdown_timer;
|
||||
} maze32_gateway_t;
|
||||
|
||||
esp_err_t maze32_gateway_init(maze32_gateway_t* gateway);
|
||||
void maze32_gateway_set_server(maze32_gateway_t* gateway, httpd_handle_t server);
|
||||
esp_err_t maze32_gateway_register(httpd_handle_t server, maze32_gateway_t* gateway);
|
||||
void maze32_gateway_close_socket(httpd_handle_t server, int socket_fd);
|
||||
int maze32_gateway_can_reconfigure(const maze32_gateway_t* gateway);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -3,13 +3,15 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "maze32_gateway.h"
|
||||
#include "maze32_health.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
esp_err_t maze32_web_start(httpd_handle_t *server, maze32_health_t *health);
|
||||
esp_err_t maze32_web_start(httpd_handle_t* server, maze32_health_t* health,
|
||||
maze32_gateway_t* gateway);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -0,0 +1,663 @@
|
||||
#include "maze32_gateway.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h"
|
||||
#include "maze32_protocol.h"
|
||||
|
||||
static const char* TAG = "maze32_gateway";
|
||||
static const char* NAME_SUGGESTIONS[] = {
|
||||
"Байт-Беглец", "Кот-Навигатор", "Сэр Тупичок", "Пиксельный Ёж", "Лорд Лабиринт",
|
||||
};
|
||||
|
||||
static uint64_t now_ms(void) {
|
||||
return (uint64_t)(esp_timer_get_time() / 1000);
|
||||
}
|
||||
|
||||
static maze32_gateway_connection_t* find_connection(maze32_gateway_t* gateway, int socket_fd) {
|
||||
for (size_t index = 0U; index < MAZE32_GATEWAY_CONNECTION_LIMIT; ++index) {
|
||||
if (gateway->connections[index].active &&
|
||||
gateway->connections[index].socket_fd == socket_fd) {
|
||||
return &gateway->connections[index];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static maze32_gateway_connection_t* open_connection(maze32_gateway_t* gateway, int socket_fd) {
|
||||
maze32_gateway_connection_t* existing = find_connection(gateway, socket_fd);
|
||||
if (existing != NULL) {
|
||||
return existing;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_GATEWAY_CONNECTION_LIMIT; ++index) {
|
||||
maze32_gateway_connection_t* connection = &gateway->connections[index];
|
||||
if (!connection->active) {
|
||||
memset(connection, 0, sizeof(*connection));
|
||||
connection->active = 1;
|
||||
connection->socket_fd = socket_fd;
|
||||
connection->rate_window_started_ms = now_ms();
|
||||
unsigned int active_count = 0U;
|
||||
for (size_t count_index = 0U; count_index < MAZE32_GATEWAY_CONNECTION_LIMIT;
|
||||
++count_index) {
|
||||
if (gateway->connections[count_index].active) {
|
||||
active_count++;
|
||||
}
|
||||
}
|
||||
ESP_LOGI(TAG, "WebSocket connected: fd=%d, active=%u/%u", socket_fd, active_count,
|
||||
(unsigned int)MAZE32_GATEWAY_CONNECTION_LIMIT);
|
||||
if (active_count == MAZE32_GATEWAY_CONNECTION_LIMIT) {
|
||||
ESP_LOGI(TAG, "Six-client heap: free=%u bytes, largest=%u bytes",
|
||||
(unsigned int)heap_caps_get_free_size(MALLOC_CAP_8BIT),
|
||||
(unsigned int)heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int append_text(char* output, size_t capacity, size_t* length, const char* text) {
|
||||
size_t text_length = strlen(text);
|
||||
if (*length + text_length >= capacity) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(output + *length, text, text_length);
|
||||
*length += text_length;
|
||||
output[*length] = '\0';
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int append_json_string(char* output, size_t capacity, size_t* length, const char* value) {
|
||||
if (!append_text(output, capacity, length, "\"")) {
|
||||
return 0;
|
||||
}
|
||||
for (const unsigned char* cursor = (const unsigned char*)value; *cursor != '\0'; ++cursor) {
|
||||
const char* escape = NULL;
|
||||
if (*cursor == '"') {
|
||||
escape = "\\\"";
|
||||
} else if (*cursor == '\\') {
|
||||
escape = "\\\\";
|
||||
} else if (*cursor == '\n') {
|
||||
escape = "\\n";
|
||||
} else if (*cursor == '\r') {
|
||||
escape = "\\r";
|
||||
} else if (*cursor == '\t') {
|
||||
escape = "\\t";
|
||||
}
|
||||
if (escape != NULL) {
|
||||
if (!append_text(output, capacity, length, escape)) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
char byte[2] = {(char)*cursor, '\0'};
|
||||
if (!append_text(output, capacity, length, byte)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return append_text(output, capacity, length, "\"");
|
||||
}
|
||||
|
||||
static uint32_t next_server_sequence(maze32_gateway_t* gateway) {
|
||||
gateway->server_sequence++;
|
||||
if (gateway->server_sequence == 0U) {
|
||||
gateway->server_sequence = 1U;
|
||||
}
|
||||
return gateway->server_sequence;
|
||||
}
|
||||
|
||||
static int enqueue(maze32_gateway_t* gateway, int socket_fd, const char* payload) {
|
||||
size_t length = strlen(payload);
|
||||
if (gateway->outgoing_count >= MAZE32_GATEWAY_QUEUE_LIMIT ||
|
||||
length >= MAZE32_GATEWAY_OUTGOING_LIMIT) {
|
||||
ESP_LOGW(TAG, "Bounded outgoing queue rejected a message for fd=%d", socket_fd);
|
||||
return 0;
|
||||
}
|
||||
size_t index = (gateway->outgoing_head + gateway->outgoing_count) % MAZE32_GATEWAY_QUEUE_LIMIT;
|
||||
maze32_gateway_outgoing_t* entry = &gateway->outgoing[index];
|
||||
entry->socket_fd = socket_fd;
|
||||
entry->length = length;
|
||||
memcpy(entry->payload, payload, length + 1U);
|
||||
gateway->outgoing_count++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void drain_queue(maze32_gateway_t* gateway) {
|
||||
while (gateway->outgoing_count > 0U) {
|
||||
maze32_gateway_outgoing_t* entry = &gateway->outgoing[gateway->outgoing_head];
|
||||
httpd_ws_frame_t frame = {
|
||||
.final = true,
|
||||
.fragmented = false,
|
||||
.type = HTTPD_WS_TYPE_TEXT,
|
||||
.payload = (uint8_t*)entry->payload,
|
||||
.len = entry->length,
|
||||
};
|
||||
esp_err_t result = httpd_ws_send_frame_async(gateway->server, entry->socket_fd, &frame);
|
||||
if (result != ESP_OK) {
|
||||
ESP_LOGW(TAG, "WebSocket send failed for fd=%d: %s", entry->socket_fd,
|
||||
esp_err_to_name(result));
|
||||
}
|
||||
gateway->outgoing_head = (gateway->outgoing_head + 1U) % MAZE32_GATEWAY_QUEUE_LIMIT;
|
||||
gateway->outgoing_count--;
|
||||
}
|
||||
}
|
||||
|
||||
static void send_error(maze32_gateway_t* gateway, int socket_fd, const char* request_id,
|
||||
const char* code, const char* message) {
|
||||
char response[MAZE32_GATEWAY_OUTGOING_LIMIT] = "";
|
||||
size_t length = 0U;
|
||||
char prefix[128];
|
||||
int written = snprintf(prefix, sizeof(prefix), "{\"v\":1,\"type\":\"error\",\"seq\":%" PRIu32,
|
||||
next_server_sequence(gateway));
|
||||
if (written < 0 || (size_t)written >= sizeof(prefix) ||
|
||||
!append_text(response, sizeof(response), &length, prefix)) {
|
||||
return;
|
||||
}
|
||||
if (request_id != NULL && request_id[0] != '\0') {
|
||||
if (!append_text(response, sizeof(response), &length, ",\"requestId\":") ||
|
||||
!append_json_string(response, sizeof(response), &length, request_id)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!append_text(response, sizeof(response), &length, ",\"payload\":{\"code\":") ||
|
||||
!append_json_string(response, sizeof(response), &length, code) ||
|
||||
!append_text(response, sizeof(response), &length, ",\"message\":") ||
|
||||
!append_json_string(response, sizeof(response), &length, message) ||
|
||||
!append_text(response, sizeof(response), &length, "}}")) {
|
||||
return;
|
||||
}
|
||||
enqueue(gateway, socket_fd, response);
|
||||
}
|
||||
|
||||
static const maze32_session_t* player_for_slot(const maze32_lobby_t* lobby, unsigned int slot) {
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
const maze32_session_t* session = &lobby->sessions[index];
|
||||
if (session->role == MAZE32_ROLE_PLAYER && session->player_slot == slot) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int build_snapshot(maze32_gateway_t* gateway, char* response, size_t capacity) {
|
||||
uint32_t sequence = next_server_sequence(gateway);
|
||||
int written =
|
||||
snprintf(response, capacity,
|
||||
"{\"v\":1,\"type\":\"lobby\",\"seq\":%" PRIu32
|
||||
",\"payload\":{\"revision\":%" PRIu32 ",\"phase\":\"%s\","
|
||||
"\"ready\":%s,\"ownerSlot\":1,\"players\":[",
|
||||
sequence, gateway->lobby.revision, maze32_lobby_phase_name(gateway->lobby.phase),
|
||||
maze32_lobby_ready(&gateway->lobby) ? "true" : "false");
|
||||
if (written < 0 || (size_t)written >= capacity) {
|
||||
return 0;
|
||||
}
|
||||
size_t length = (size_t)written;
|
||||
for (unsigned int slot = 1U; slot <= MAZE32_PLAYER_LIMIT; ++slot) {
|
||||
const maze32_session_t* player = player_for_slot(&gateway->lobby, slot);
|
||||
char prefix[96];
|
||||
written = snprintf(prefix, sizeof(prefix),
|
||||
"%s{\"slot\":%u,\"name\":", slot == 1U ? "" : ",", slot);
|
||||
if (written < 0 || (size_t)written >= sizeof(prefix) ||
|
||||
!append_text(response, capacity, &length, prefix) ||
|
||||
!append_json_string(response, capacity, &length, player == NULL ? "" : player->name)) {
|
||||
return 0;
|
||||
}
|
||||
written = snprintf(prefix, sizeof(prefix), ",\"occupied\":%s,\"connected\":%s}",
|
||||
player == NULL ? "false" : "true",
|
||||
player != NULL && player->connected ? "true" : "false");
|
||||
if (written < 0 || (size_t)written >= sizeof(prefix) ||
|
||||
!append_text(response, capacity, &length, prefix)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
char suffix[128];
|
||||
written = snprintf(suffix, sizeof(suffix), "],\"observers\":%u,\"observerLimit\":%u}}",
|
||||
maze32_lobby_connected_observers(&gateway->lobby),
|
||||
(unsigned int)MAZE32_OBSERVER_LIMIT);
|
||||
return written >= 0 && (size_t)written < sizeof(suffix) &&
|
||||
append_text(response, capacity, &length, suffix);
|
||||
}
|
||||
|
||||
static void broadcast_snapshot(maze32_gateway_t* gateway) {
|
||||
char response[MAZE32_GATEWAY_OUTGOING_LIMIT];
|
||||
if (!build_snapshot(gateway, response, sizeof(response))) {
|
||||
ESP_LOGE(TAG, "Lobby snapshot exceeded its fixed buffer");
|
||||
return;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_GATEWAY_CONNECTION_LIMIT; ++index) {
|
||||
maze32_gateway_connection_t* connection = &gateway->connections[index];
|
||||
if (connection->active &&
|
||||
maze32_lobby_session_for_connection(&gateway->lobby, connection->socket_fd) != NULL) {
|
||||
enqueue(gateway, connection->socket_fd, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void send_session(maze32_gateway_t* gateway, int socket_fd, const maze32_session_t* session,
|
||||
const char* request_id) {
|
||||
char response[MAZE32_GATEWAY_OUTGOING_LIMIT] = "";
|
||||
size_t length = 0U;
|
||||
char prefix[160];
|
||||
int written = snprintf(prefix, sizeof(prefix), "{\"v\":1,\"type\":\"session\",\"seq\":%" PRIu32,
|
||||
next_server_sequence(gateway));
|
||||
if (written < 0 || (size_t)written >= sizeof(prefix) ||
|
||||
!append_text(response, sizeof(response), &length, prefix)) {
|
||||
return;
|
||||
}
|
||||
if (request_id[0] != '\0' &&
|
||||
(!append_text(response, sizeof(response), &length, ",\"requestId\":") ||
|
||||
!append_json_string(response, sizeof(response), &length, request_id))) {
|
||||
return;
|
||||
}
|
||||
written =
|
||||
snprintf(prefix, sizeof(prefix), ",\"payload\":{\"role\":\"%s\",\"slot\":%u,\"token\":",
|
||||
session->role == MAZE32_ROLE_PLAYER ? "player" : "observer", session->player_slot);
|
||||
if (written < 0 || (size_t)written >= sizeof(prefix) ||
|
||||
!append_text(response, sizeof(response), &length, prefix) ||
|
||||
!append_json_string(response, sizeof(response), &length, session->token) ||
|
||||
!append_text(response, sizeof(response), &length, "}}")) {
|
||||
return;
|
||||
}
|
||||
enqueue(gateway, socket_fd, response);
|
||||
}
|
||||
|
||||
static void generate_token(maze32_gateway_t* gateway, char* token) {
|
||||
static const char HEX[] = "0123456789abcdef";
|
||||
uint8_t bytes[MAZE32_SESSION_TOKEN_BYTES];
|
||||
for (;;) {
|
||||
esp_fill_random(bytes, sizeof(bytes));
|
||||
for (size_t index = 0U; index < sizeof(bytes); ++index) {
|
||||
token[index * 2U] = HEX[bytes[index] >> 4U];
|
||||
token[index * 2U + 1U] = HEX[bytes[index] & 0x0fU];
|
||||
}
|
||||
token[MAZE32_SESSION_TOKEN_HEX_LENGTH] = '\0';
|
||||
int collision = 0;
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
if (gateway->lobby.sessions[index].role != MAZE32_ROLE_NONE &&
|
||||
strcmp(gateway->lobby.sessions[index].token, token) == 0) {
|
||||
collision = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!collision) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void send_lobby_error(maze32_gateway_t* gateway, int socket_fd, const char* request_id,
|
||||
maze32_lobby_result_t result) {
|
||||
switch (result) {
|
||||
case MAZE32_LOBBY_INVALID_NAME:
|
||||
send_error(gateway, socket_fd, request_id, "invalid_name",
|
||||
"Имя должно быть длиной от 1 до 32 байт без пробелов по краям");
|
||||
break;
|
||||
case MAZE32_LOBBY_DUPLICATE_NAME:
|
||||
send_error(gateway, socket_fd, request_id, "duplicate_name", "Это имя уже занято");
|
||||
break;
|
||||
case MAZE32_LOBBY_ROLE_FULL:
|
||||
send_error(gateway, socket_fd, request_id, "role_full", "Свободных мест больше нет");
|
||||
break;
|
||||
case MAZE32_LOBBY_INVALID_TOKEN:
|
||||
send_error(gateway, socket_fd, request_id, "invalid_token", "Сессия не найдена");
|
||||
break;
|
||||
case MAZE32_LOBBY_SESSION_CONNECTED:
|
||||
send_error(gateway, socket_fd, request_id, "session_connected",
|
||||
"Эта сессия уже подключена");
|
||||
break;
|
||||
case MAZE32_LOBBY_NOT_OWNER:
|
||||
send_error(gateway, socket_fd, request_id, "not_owner",
|
||||
"Запустить игру может только Игрок 1");
|
||||
break;
|
||||
case MAZE32_LOBBY_NOT_READY:
|
||||
send_error(gateway, socket_fd, request_id, "not_ready",
|
||||
"Для старта нужны оба игрока онлайн");
|
||||
break;
|
||||
case MAZE32_LOBBY_INVALID_PHASE:
|
||||
send_error(gateway, socket_fd, request_id, "invalid_phase",
|
||||
"Лобби уже запустило обратный отсчёт");
|
||||
break;
|
||||
default:
|
||||
send_error(gateway, socket_fd, request_id, "request_rejected", "Команда отклонена");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void send_suggestion(maze32_gateway_t* gateway, int socket_fd, const char* request_id) {
|
||||
const char* suggestion = NULL;
|
||||
size_t suggestion_count = sizeof(NAME_SUGGESTIONS) / sizeof(NAME_SUGGESTIONS[0]);
|
||||
for (size_t attempt = 0U; attempt < suggestion_count; ++attempt) {
|
||||
size_t index = gateway->suggestion_index++ % suggestion_count;
|
||||
int occupied = 0;
|
||||
for (size_t session_index = 0U; session_index < MAZE32_SESSION_LIMIT; ++session_index) {
|
||||
const maze32_session_t* session = &gateway->lobby.sessions[session_index];
|
||||
if (session->role == MAZE32_ROLE_PLAYER &&
|
||||
strcmp(session->name, NAME_SUGGESTIONS[index]) == 0) {
|
||||
occupied = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!occupied) {
|
||||
suggestion = NAME_SUGGESTIONS[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (suggestion == NULL) {
|
||||
send_error(gateway, socket_fd, request_id, "no_suggestion",
|
||||
"Все смешные имена уже разобрали");
|
||||
return;
|
||||
}
|
||||
char response[MAZE32_GATEWAY_OUTGOING_LIMIT] = "";
|
||||
size_t length = 0U;
|
||||
char prefix[128];
|
||||
int written =
|
||||
snprintf(prefix, sizeof(prefix), "{\"v\":1,\"type\":\"name_suggestion\",\"seq\":%" PRIu32,
|
||||
next_server_sequence(gateway));
|
||||
if (written < 0 || (size_t)written >= sizeof(prefix) ||
|
||||
!append_text(response, sizeof(response), &length, prefix)) {
|
||||
return;
|
||||
}
|
||||
if (request_id[0] != '\0' &&
|
||||
(!append_text(response, sizeof(response), &length, ",\"requestId\":") ||
|
||||
!append_json_string(response, sizeof(response), &length, request_id))) {
|
||||
return;
|
||||
}
|
||||
if (append_text(response, sizeof(response), &length, ",\"payload\":{\"name\":") &&
|
||||
append_json_string(response, sizeof(response), &length, suggestion) &&
|
||||
append_text(response, sizeof(response), &length, "}}")) {
|
||||
enqueue(gateway, socket_fd, response);
|
||||
}
|
||||
}
|
||||
|
||||
static void broadcast_countdown(maze32_gateway_t* gateway) {
|
||||
char response[MAZE32_GATEWAY_OUTGOING_LIMIT];
|
||||
int written = snprintf(response, sizeof(response),
|
||||
"{\"v\":1,\"type\":\"countdown\",\"seq\":%" PRIu32
|
||||
",\"payload\":{\"steps\":[3,2,1],\"startedAtMs\":%" PRIu64
|
||||
",\"activationAtMs\":%" PRIu64 "}}",
|
||||
next_server_sequence(gateway), gateway->lobby.countdown_started_ms,
|
||||
gateway->lobby.activation_ms);
|
||||
if (written < 0 || (size_t)written >= sizeof(response)) {
|
||||
return;
|
||||
}
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
const maze32_session_t* session = &gateway->lobby.sessions[index];
|
||||
if (session->role == MAZE32_ROLE_PLAYER && session->connected) {
|
||||
enqueue(gateway, session->connection_id, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void countdown_finished_work(void* context) {
|
||||
maze32_gateway_t* gateway = context;
|
||||
if (!maze32_lobby_activate_if_due(&gateway->lobby, now_ms())) {
|
||||
return;
|
||||
}
|
||||
char response[MAZE32_GATEWAY_OUTGOING_LIMIT];
|
||||
int written = snprintf(response, sizeof(response),
|
||||
"{\"v\":1,\"type\":\"active\",\"seq\":%" PRIu32
|
||||
",\"payload\":{\"activatedAtMs\":%" PRIu64 "}}",
|
||||
next_server_sequence(gateway), gateway->lobby.activation_ms);
|
||||
if (written >= 0 && (size_t)written < sizeof(response)) {
|
||||
for (size_t index = 0U; index < MAZE32_SESSION_LIMIT; ++index) {
|
||||
const maze32_session_t* session = &gateway->lobby.sessions[index];
|
||||
if (session->role != MAZE32_ROLE_NONE && session->connected) {
|
||||
enqueue(gateway, session->connection_id, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
broadcast_snapshot(gateway);
|
||||
drain_queue(gateway);
|
||||
ESP_LOGI(TAG, "Lobby became active at server time %" PRIu64 " ms",
|
||||
gateway->lobby.activation_ms);
|
||||
}
|
||||
|
||||
static void countdown_timer_callback(void* context) {
|
||||
maze32_gateway_t* gateway = context;
|
||||
if (gateway->server != NULL) {
|
||||
esp_err_t result = httpd_queue_work(gateway->server, countdown_finished_work, gateway);
|
||||
if (result != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Could not queue countdown activation: %s", esp_err_to_name(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int rate_limit_allows(maze32_gateway_connection_t* connection, uint64_t current_ms) {
|
||||
if (current_ms - connection->rate_window_started_ms >= 1000U) {
|
||||
connection->rate_window_started_ms = current_ms;
|
||||
connection->messages_in_window = 0U;
|
||||
}
|
||||
connection->messages_in_window++;
|
||||
return connection->messages_in_window <= MAZE32_GATEWAY_RATE_LIMIT_PER_SECOND;
|
||||
}
|
||||
|
||||
static void handle_message(maze32_gateway_t* gateway, maze32_gateway_connection_t* connection,
|
||||
const maze32_client_message_t* message) {
|
||||
int socket_fd = connection->socket_fd;
|
||||
if (message->type == MAZE32_MESSAGE_SUGGEST_NAME) {
|
||||
send_suggestion(gateway, socket_fd, message->request_id);
|
||||
return;
|
||||
}
|
||||
if (message->type == MAZE32_MESSAGE_JOIN) {
|
||||
if (maze32_lobby_session_for_connection(&gateway->lobby, socket_fd) != NULL) {
|
||||
send_error(gateway, socket_fd, message->request_id, "already_joined",
|
||||
"Сессия уже выбрала роль");
|
||||
return;
|
||||
}
|
||||
char token[MAZE32_SESSION_TOKEN_HEX_LENGTH + 1U];
|
||||
generate_token(gateway, token);
|
||||
maze32_session_t* session = NULL;
|
||||
maze32_lobby_result_t result =
|
||||
message->role == MAZE32_ROLE_PLAYER
|
||||
? maze32_lobby_join_player(&gateway->lobby, socket_fd, message->name, token,
|
||||
&session)
|
||||
: maze32_lobby_join_observer(&gateway->lobby, socket_fd, token, &session);
|
||||
if (result != MAZE32_LOBBY_OK) {
|
||||
send_lobby_error(gateway, socket_fd, message->request_id, result);
|
||||
return;
|
||||
}
|
||||
send_session(gateway, socket_fd, session, message->request_id);
|
||||
broadcast_snapshot(gateway);
|
||||
return;
|
||||
}
|
||||
if (message->type == MAZE32_MESSAGE_RESTORE) {
|
||||
if (maze32_lobby_session_for_connection(&gateway->lobby, socket_fd) != NULL) {
|
||||
send_error(gateway, socket_fd, message->request_id, "already_joined",
|
||||
"Сессия уже восстановлена");
|
||||
return;
|
||||
}
|
||||
maze32_session_t* session = NULL;
|
||||
maze32_lobby_result_t result =
|
||||
maze32_lobby_restore(&gateway->lobby, socket_fd, message->token, &session);
|
||||
if (result != MAZE32_LOBBY_OK) {
|
||||
send_lobby_error(gateway, socket_fd, message->request_id, result);
|
||||
return;
|
||||
}
|
||||
send_session(gateway, socket_fd, session, message->request_id);
|
||||
broadcast_snapshot(gateway);
|
||||
return;
|
||||
}
|
||||
|
||||
const maze32_session_t* session =
|
||||
maze32_lobby_session_for_connection(&gateway->lobby, socket_fd);
|
||||
if (session == NULL) {
|
||||
send_error(gateway, socket_fd, message->request_id, "unauthorized",
|
||||
"Сначала выберите роль или восстановите сессию");
|
||||
return;
|
||||
}
|
||||
if (message->type == MAZE32_MESSAGE_START) {
|
||||
maze32_lobby_result_t result = maze32_lobby_start(&gateway->lobby, socket_fd, now_ms());
|
||||
if (result != MAZE32_LOBBY_OK) {
|
||||
send_lobby_error(gateway, socket_fd, message->request_id, result);
|
||||
return;
|
||||
}
|
||||
broadcast_snapshot(gateway);
|
||||
broadcast_countdown(gateway);
|
||||
esp_err_t timer_result =
|
||||
esp_timer_start_once(gateway->countdown_timer, MAZE32_COUNTDOWN_DURATION_MS * 1000U);
|
||||
if (timer_result != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Countdown timer could not start: %s", esp_err_to_name(timer_result));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message->type == MAZE32_MESSAGE_HEARTBEAT) {
|
||||
char response[MAZE32_GATEWAY_OUTGOING_LIMIT] = "";
|
||||
size_t length = 0U;
|
||||
char prefix[128];
|
||||
int written = snprintf(prefix, sizeof(prefix), "{\"v\":1,\"type\":\"ack\",\"seq\":%" PRIu32,
|
||||
next_server_sequence(gateway));
|
||||
if (written >= 0 && (size_t)written < sizeof(prefix) &&
|
||||
append_text(response, sizeof(response), &length, prefix) &&
|
||||
append_text(response, sizeof(response), &length, ",\"requestId\":") &&
|
||||
append_json_string(response, sizeof(response), &length, message->request_id) &&
|
||||
append_text(response, sizeof(response), &length,
|
||||
",\"payload\":{\"command\":\"heartbeat\"}}")) {
|
||||
enqueue(gateway, socket_fd, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t websocket_handler(httpd_req_t* request) {
|
||||
maze32_gateway_t* gateway = request->user_ctx;
|
||||
int socket_fd = httpd_req_to_sockfd(request);
|
||||
maze32_gateway_connection_t* connection = find_connection(gateway, socket_fd);
|
||||
if (connection == NULL) {
|
||||
connection = open_connection(gateway, socket_fd);
|
||||
if (connection == NULL) {
|
||||
ESP_LOGW(TAG, "WebSocket connection limit reached; rejecting fd=%d", socket_fd);
|
||||
shutdown(socket_fd, SHUT_RDWR);
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
httpd_ws_frame_t frame = {0};
|
||||
esp_err_t result = httpd_ws_recv_frame(request, &frame, 0U);
|
||||
if (result != ESP_OK) {
|
||||
return result;
|
||||
}
|
||||
if (frame.len > MAZE32_PROTOCOL_MESSAGE_LIMIT) {
|
||||
ESP_LOGW(TAG, "Oversized WebSocket frame rejected: fd=%d, length=%u", socket_fd,
|
||||
(unsigned int)frame.len);
|
||||
shutdown(socket_fd, SHUT_RDWR);
|
||||
return ESP_OK;
|
||||
}
|
||||
uint8_t payload[MAZE32_PROTOCOL_MESSAGE_LIMIT + 1U];
|
||||
frame.payload = payload;
|
||||
result = httpd_ws_recv_frame(request, &frame, MAZE32_PROTOCOL_MESSAGE_LIMIT);
|
||||
if (result != ESP_OK) {
|
||||
return result;
|
||||
}
|
||||
if (frame.type == HTTPD_WS_TYPE_CLOSE) {
|
||||
return ESP_OK;
|
||||
}
|
||||
if (frame.type != HTTPD_WS_TYPE_TEXT) {
|
||||
send_error(gateway, socket_fd, "", "text_required", "Нужен текстовый JSON-пакет");
|
||||
drain_queue(gateway);
|
||||
return ESP_OK;
|
||||
}
|
||||
payload[frame.len] = '\0';
|
||||
if (!rate_limit_allows(connection, now_ms())) {
|
||||
send_error(gateway, socket_fd, "", "rate_limited", "Слишком много команд");
|
||||
drain_queue(gateway);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
maze32_client_message_t message;
|
||||
maze32_protocol_result_t parse_result =
|
||||
maze32_protocol_parse((const char*)payload, frame.len, &message);
|
||||
if (parse_result != MAZE32_PROTOCOL_OK) {
|
||||
send_error(gateway, socket_fd, "", maze32_protocol_result_name(parse_result),
|
||||
"Пакет не прошёл проверку протокола");
|
||||
drain_queue(gateway);
|
||||
return ESP_OK;
|
||||
}
|
||||
if (message.sequence <= connection->last_client_sequence) {
|
||||
send_error(gateway, socket_fd, message.request_id, "stale_sequence",
|
||||
"Команда устарела или уже обработана");
|
||||
drain_queue(gateway);
|
||||
return ESP_OK;
|
||||
}
|
||||
connection->last_client_sequence = message.sequence;
|
||||
handle_message(gateway, connection, &message);
|
||||
drain_queue(gateway);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t websocket_pre_handshake(httpd_req_t* request) {
|
||||
maze32_gateway_t* gateway = request->user_ctx;
|
||||
int socket_fd = httpd_req_to_sockfd(request);
|
||||
if (open_connection(gateway, socket_fd) == NULL) {
|
||||
ESP_LOGW(TAG, "WebSocket connection limit reached; rejecting fd=%d", socket_fd);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t maze32_gateway_init(maze32_gateway_t* gateway) {
|
||||
if (gateway == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
memset(gateway, 0, sizeof(*gateway));
|
||||
maze32_lobby_init(&gateway->lobby);
|
||||
const esp_timer_create_args_t timer_arguments = {
|
||||
.callback = countdown_timer_callback,
|
||||
.arg = gateway,
|
||||
.dispatch_method = ESP_TIMER_TASK,
|
||||
.name = "lobby_countdown",
|
||||
.skip_unhandled_events = true,
|
||||
};
|
||||
return esp_timer_create(&timer_arguments, &gateway->countdown_timer);
|
||||
}
|
||||
|
||||
void maze32_gateway_set_server(maze32_gateway_t* gateway, httpd_handle_t server) {
|
||||
if (gateway != NULL) {
|
||||
gateway->server = server;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t maze32_gateway_register(httpd_handle_t server, maze32_gateway_t* gateway) {
|
||||
if (server == NULL || gateway == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
maze32_gateway_set_server(gateway, server);
|
||||
const httpd_uri_t websocket = {
|
||||
.uri = "/ws",
|
||||
.method = HTTP_GET,
|
||||
.handler = websocket_handler,
|
||||
.user_ctx = gateway,
|
||||
.is_websocket = true,
|
||||
.ws_pre_handshake_cb = websocket_pre_handshake,
|
||||
};
|
||||
esp_err_t result = httpd_register_uri_handler(server, &websocket);
|
||||
if (result == ESP_OK) {
|
||||
ESP_LOGI(TAG, "WebSocket gateway ready: connections=%u, message=%u, queue=%u",
|
||||
(unsigned int)MAZE32_GATEWAY_CONNECTION_LIMIT,
|
||||
(unsigned int)MAZE32_PROTOCOL_MESSAGE_LIMIT,
|
||||
(unsigned int)MAZE32_GATEWAY_QUEUE_LIMIT);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void maze32_gateway_close_socket(httpd_handle_t server, int socket_fd) {
|
||||
maze32_gateway_t* gateway = httpd_get_global_user_ctx(server);
|
||||
if (gateway != NULL) {
|
||||
maze32_gateway_connection_t* connection = find_connection(gateway, socket_fd);
|
||||
if (connection != NULL) {
|
||||
maze32_lobby_disconnect(&gateway->lobby, socket_fd);
|
||||
memset(connection, 0, sizeof(*connection));
|
||||
broadcast_snapshot(gateway);
|
||||
drain_queue(gateway);
|
||||
ESP_LOGI(TAG, "WebSocket disconnected: fd=%d", socket_fd);
|
||||
}
|
||||
}
|
||||
close(socket_fd);
|
||||
}
|
||||
|
||||
int maze32_gateway_can_reconfigure(const maze32_gateway_t* gateway) {
|
||||
return gateway != NULL && maze32_lobby_can_reconfigure(&gateway->lobby);
|
||||
}
|
||||
@@ -6,16 +6,19 @@
|
||||
|
||||
#define MAZE32_STATIC_CHUNK_SIZE 1024
|
||||
|
||||
static const char *TAG = "maze32_web";
|
||||
static const char *INDEX_PATH = "/littlefs/index.html.gz";
|
||||
static const char* TAG = "maze32_web";
|
||||
static const char* INDEX_PATH = "/littlefs/index.html.gz";
|
||||
|
||||
static esp_err_t send_index(httpd_req_t *request)
|
||||
{
|
||||
FILE *file = fopen(INDEX_PATH, "rb");
|
||||
static void preserve_static_context(void* context) {
|
||||
(void)context;
|
||||
}
|
||||
|
||||
static esp_err_t send_index(httpd_req_t* request) {
|
||||
FILE* file = fopen(INDEX_PATH, "rb");
|
||||
if (file == NULL) {
|
||||
ESP_LOGE(TAG, "Static asset is unavailable: %s", INDEX_PATH);
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Static application is unavailable");
|
||||
"Приложение временно недоступно");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -45,18 +48,17 @@ static esp_err_t send_index(httpd_req_t *request)
|
||||
return result;
|
||||
}
|
||||
|
||||
static esp_err_t send_health(httpd_req_t *request)
|
||||
{
|
||||
const maze32_health_t *health = request->user_ctx;
|
||||
static esp_err_t send_health(httpd_req_t* request) {
|
||||
const maze32_health_t* health = request->user_ctx;
|
||||
char response[192];
|
||||
int length = snprintf(response, sizeof(response),
|
||||
"{\"status\":\"%s\",\"healthy\":%s,\"version\":\"%s\",\"errorCode\":%d}",
|
||||
maze32_health_state_name(health->state),
|
||||
maze32_health_is_healthy(health) ? "true" : "false",
|
||||
health->version, health->error_code);
|
||||
maze32_health_is_healthy(health) ? "true" : "false", health->version,
|
||||
health->error_code);
|
||||
if (length < 0 || (size_t)length >= sizeof(response)) {
|
||||
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Health response exceeded its fixed buffer");
|
||||
"Диагностический ответ слишком большой");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
@@ -65,16 +67,21 @@ static esp_err_t send_health(httpd_req_t *request)
|
||||
return httpd_resp_send(request, response, length);
|
||||
}
|
||||
|
||||
esp_err_t maze32_web_start(httpd_handle_t *server, maze32_health_t *health)
|
||||
{
|
||||
if (server == NULL || health == NULL) {
|
||||
esp_err_t maze32_web_start(httpd_handle_t* server, maze32_health_t* health,
|
||||
maze32_gateway_t* gateway) {
|
||||
if (server == NULL || health == NULL || gateway == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
httpd_config_t configuration = HTTPD_DEFAULT_CONFIG();
|
||||
configuration.max_uri_handlers = 8;
|
||||
configuration.max_open_sockets = 7;
|
||||
configuration.lru_purge_enable = true;
|
||||
configuration.max_uri_handlers = 9;
|
||||
configuration.max_open_sockets = 9;
|
||||
configuration.lru_purge_enable = false;
|
||||
configuration.recv_wait_timeout = 5;
|
||||
configuration.send_wait_timeout = 1;
|
||||
configuration.global_user_ctx = gateway;
|
||||
configuration.global_user_ctx_free_fn = preserve_static_context;
|
||||
configuration.close_fn = maze32_gateway_close_socket;
|
||||
|
||||
esp_err_t result = httpd_start(server, &configuration);
|
||||
if (result != ESP_OK) {
|
||||
|
||||
Reference in New Issue
Block a user