diff --git a/firmware/components/protocol/CMakeLists.txt b/firmware/components/protocol/CMakeLists.txt new file mode 100644 index 0000000..31bcd7a --- /dev/null +++ b/firmware/components/protocol/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "protocol_interface.c" + INCLUDE_DIRS "." + REQUIRES alarms +) diff --git a/firmware/components/protocol/protocol_interface.c b/firmware/components/protocol/protocol_interface.c new file mode 100644 index 0000000..53f19dc --- /dev/null +++ b/firmware/components/protocol/protocol_interface.c @@ -0,0 +1,244 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Camden Dixie O'Brien + */ + +#include "protocol_interface.h" + +#include "esp_log.h" +#include "lwip/sockets.h" +#include + +#define TAG "Protocol interface" + +#define PORT 3498 + +static CommandHandler handlers[COMMAND_INSTRUCTION_COUNT]; + +static Message command; +static Message response; + +static char string_buffer[CONFIG_MESSAGE_STRING_BUFFER_SIZE]; +static char *string_buffer_free_ptr; + +static bool receive(int socket, unsigned n, char *p) +{ + int len; + do { + len = recv(socket, p, n, 0); + if (len < 0) { + ESP_LOGE(TAG, "Error reading from socket: %d", errno); + return false; + } else if (len == 0) { + ESP_LOGI(TAG, "Connection closed"); + return false; + } else { + n -= len; + p += len; + } + } while (n > 0); + return true; +} + +static bool receive_param(int socket, unsigned i) +{ + MessageParam *param = &command.params[i]; + + uint8_t type_buf; + if (!receive(socket, 1, &type_buf)) + return false; + // TODO handle invalid param type + param->type = (MessageParamType)type_buf; + + uint8_t len, alarm_buf[3]; + switch (param->type) { + case MESSAGE_PARAM_TYPE_U8: + if (!receive(socket, 1, (char *)¶m->u8)) + return false; + break; + case MESSAGE_PARAM_TYPE_U16: + if (!receive(socket, 2, (char *)¶m->u16)) + return false; + param->u16 = ntohs(param->u16); + break; + case MESSAGE_PARAM_TYPE_U32: + if (!receive(socket, 4, (char *)¶m->u32)) + return false; + param->u32 = ntohl(param->u32); + break; + case MESSAGE_PARAM_TYPE_U64: + if (!receive(socket, 8, (char *)¶m->u64)) + return false; + param->u64 = ntohll(param->u64); + break; + + case MESSAGE_PARAM_TYPE_STRING: + if (!receive(socket, 1, &len)) + return false; + char *buf = alloc_message_string(len); + // TODO handle out-of-memory + if (!receive(socket, len, buf)) + return false; + param->string = buf; + break; + + case MESSAGE_PARAM_TYPE_ALARM: + if (!receive(socket, 3, alarm_buf)) + return false; + param->alarm.time.hour = alarm_buf[0]; + param->alarm.time.minute = alarm_buf[1]; + for (unsigned i = 0; i < WEEK_DAY_COUNT; ++i) + param->alarm.days[i] = (alarm_buf[2] >> i) & 1; + break; + } +} + +static bool receive_command(int socket) +{ + char header_buf[2]; + if (!receive(socket, 2, header_buf)) + return false; + // TODO handle invalid instruction + command.instruction = (CommandInstruction)header_buf[0]; + command.param_count = (uint8_t)header_buf[1]; + + for (unsigned i = 0; i < command.param_count; ++i) { + if (!receive_param(socket, i)) + return false; + } + + return true; +} + +static void send_response(int socket) +{ +} + +static void drop_message_strings(void) +{ + string_buffer_free_ptr = string_buffer; +} + +static void handle_command(void) +{ + const CommandHandler handler = handlers[command.instruction]; + if (handler == NULL) { + ESP_LOGW( + TAG, "Unhandled command, instruction: %u", command.instruction); + return; + } + + const ResponseStatus status = handler(&command, &response); + if (status != RESPONSE_STATUS_OK) { + response.status = status; + response.param_count = 0; + } + + send_response(); + drop_message_strings(); +} + +void server_task(void *arg) +{ + int status; + + // Create listening socket + const int listen_socket = socket(AF_INET6, SOCK_STREAM, IPPROTO_IPV6); + if (listen_socket < 0) { + ESP_LOGE(TAG, "Error creating socket: %d", errno); + goto error_no_cleanup; + } + + // Configure listening socket + int opt = 1; + setsockopt(listen_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + // Bind socket to port (any address) + struct sockaddr_storage server_addr; + struct sockaddr_in6 *server_addr_ip6 + = (struct sockaddr_in6 *)&server_addr; + memset( + &server_addr_ip6->sin6_addr.un, 0, + sizeof(server_addr_ip6->sin6_addr.un)); + server_addr_ip6->sin6_family = AF_INET6; + server_addr_ip6->sin6_port = htons(PORT); + status = bind( + listen_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)); + if (status != 0) { + ESP_LOGE(TAG, "Error binding socket: %d", errno); + goto error_cleanup_socket; + } + + // Listen on socket + status = listen(listen_socket, 1); + if (status != 0) { + ESP_LOGE(TAG, "Error listening on socket: %d", errno); + goto error_cleanup_socket; + } + + while (1) { + struct sockaddr_storage client_addr; + socklen_t addr_len = sizeof(client_addr); + + // Accept incoming connection + int socket = accept( + listen_socket, (struct sockaddr *)&client_addr, &addr_len); + if (socket < 0) { + ESP_LOGE(TAG, "Error accepting connection: %d", errno); + continue; + } + + // Log client address, run command-response loop until + // connection closed. + const char *addr_str + = ((struct sockaddr_in6 *)&client_addr)->sin6_addr; + ESP_LOGI(TAG, "Accepted connection from %s", addr_str); + while (1) { + if (!receive_command()) + break; + handle_command(); + } + + // Cleanup connection + shutdown(socket, 0); + close(socket); + } + +error_cleanup_socket: + close(listen_socket); +error_no_cleanup: + vTaskDelete(NULL); +} + +void protocol_interface_init(void) +{ + memset(handlers, 0, sizeof(handlers)); + string_buffer_free = string_buffer; + memset(command, 0, sizeof(command)); + memset(response, 0, sizeof(response)); +} + +void set_command_handler( + CommandInstruction instruction, CommandHandler handler) +{ + if (instruction < COMMAND_INSTRUCTION_COUNT) { + if (handlers[instruction] != NULL) + ESP_LOGW(TAG, "Handler #%u overwritten", instruction); + handlers[instruction] = handler; + } else { + ESP_LOGE(TAG, "Invalid instruction passed to %s()", __func__); + } +} + +char *alloc_message_string(unsigned length) +{ + if (string_buffer_free_ptr + length + 1 + < string_buffer + CONFIG_MESSAGE_STRING_BUFFER_SIZE) { + char *s = string_buffer_free_ptr; + string_buffer_free_ptr += length + 1; + s[length] = '\0'; + return s; + } else { + return NULL; + } +} diff --git a/firmware/components/protocol/protocol_interface.h b/firmware/components/protocol/protocol_interface.h new file mode 100644 index 0000000..5a0ad47 --- /dev/null +++ b/firmware/components/protocol/protocol_interface.h @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Camden Dixie O'Brien + */ + +#ifndef PROTOCOL_INTERFACE_H +#define PROTOCOL_INTERFACE_H + +#include "protocol_messages.h" + +typedef ResponseStatus (*CommandHandler)( + const Message *command, Message *response_out); + +/** + * Initialize the protocol interface and start listening for commands. + */ +void protocol_interface_init(void); + +/** + * Set the handler for commands with a given intruction. + * + * The handler will be invoked when/if a command with the specified + * instruction is received. The response status should be returned, + * and upon success the response should be populated. Strings in the + * response must be allocated with alloc_message_string(); these will + * be freed by the interface when the response has been sent. + * + * Only a single handler can be set for a given instruction; if + * multiple are added then only the one added last will be used. + */ +void set_command_handler( + CommandInstruction instruction, CommandHandler handler); + +/** + * Allocate a buffer for a string with the given length (in bytes), + * returning a pointer to it or NULL if space has ran out. + * + * The actual space allocated will be length + 1 bytes, and the last + * byte in this buffer will be set to the null character. + */ +char *alloc_message_string(unsigned length); + +#endif diff --git a/firmware/components/protocol/protocol_messages.h b/firmware/components/protocol/protocol_messages.h new file mode 100644 index 0000000..10f8359 --- /dev/null +++ b/firmware/components/protocol/protocol_messages.h @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * Copyright (c) Camden Dixie O'Brien + */ + +#ifndef PROTOCOL_MESSAGES_H +#define PROTOCOL_MESSAGES_H + +#include "alarm_types.h" + +#include + +#define MESSAGE_MAX_PARAMS 16 + +typedef enum { + MESSAGE_PARAM_TYPE_U8 = 0, + MESSAGE_PARAM_TYPE_U16 = 1, + MESSAGE_PARAM_TYPE_U32 = 2, + MESSAGE_PARAM_TYPE_U64 = 3, + MESSAGE_PARAM_TYPE_STRING = 4, + MESSAGE_PARAM_TYPE_ALARM = 5, + + MESSAGE_PARAM_TYPE_COUNT, +} MessageParamType; + +typedef struct { + MessageParamType type; + union { + uint8_t u8; + uint16_t u16; + uint32_t u32; + uint64_t u64; + const char *string; + Alarm alarm; + }; +} MessageParam; + +typedef enum { + COMMAND_INSTRUCTION_GET_VERSION = 0, + COMMAND_INSTRUCTION_SET_TIME = 1, + COMMAND_INSTRUCTION_LIST_ALARMS = 2, + COMMAND_INSTRUCTION_ADD_ALARM = 3, + COMMAND_INSTRUCTION_REMOVE_ALARM = 4, + COMMAND_INSTRUCTION_LIST_SETTINGS = 5, + COMMAND_INSTRUCTION_SET_SETTING = 6, + + COMMAND_INSTRUCTION_COUNT, +} CommandInstruction; + +typedef enum { + RESPONSE_STATUS_OK = 0, + RESPONSE_STATUS_BUSY = 1, + RESPONSE_STATUS_INVALID_COMMAND = 2, + RESPONSE_STATUS_INTERNAL_ERROR = 3, + RESPONSE_STATUS_OUT_OF_SPACE = 4, + + RESPONSE_STATUS_COUNT, +} ResponseStatus; + +typedef struct { + union { + CommandInstruction instruction; + ResponseStatus status; + }; + uint8_t param_count; + MessageParam params[MESSAGE_MAX_PARAMS]; +} Message; + +#endif