Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
138edf9c52 | ||
|
|
8fdc409a39 | ||
|
|
cf11785f44 | ||
|
|
e961496973 | ||
|
|
d2e2ea21df | ||
|
|
8c957d043a |
No files matched your search
@@ -8,7 +8,7 @@ Networked bedside clock utilising an ESP32-SOLO-1.
|
|||||||
- [x] 7 segment display
|
- [x] 7 segment display
|
||||||
- [x] WiFi networking
|
- [x] WiFi networking
|
||||||
- [x] Console for configuration
|
- [x] Console for configuration
|
||||||
- [ ] TCP API for configuration
|
- [ ] TCP control protocol
|
||||||
- [x] SNTP synchronisation
|
- [x] SNTP synchronisation
|
||||||
- [x] Snooze and dismiss buttons
|
- [x] Snooze and dismiss buttons
|
||||||
- [x] Multiple alarms
|
- [x] Multiple alarms
|
||||||
@@ -20,3 +20,83 @@ Networked bedside clock utilising an ESP32-SOLO-1.
|
|||||||
The firmware is a standard ESP-IDF v5.0 project; refer to [Espressif's
|
The firmware is a standard ESP-IDF v5.0 project; refer to [Espressif's
|
||||||
ESP-IDF
|
ESP-IDF
|
||||||
docs](https://docs.espressif.com/projects/esp-idf/en/release-v5.0/esp32/index.html).
|
docs](https://docs.espressif.com/projects/esp-idf/en/release-v5.0/esp32/index.html).
|
||||||
|
|
||||||
|
## TCP Control Protocol
|
||||||
|
|
||||||
|
Simple, binary, command / reponse protocol. Port number 3498.
|
||||||
|
|
||||||
|
Capabilities:
|
||||||
|
- Get firmware version
|
||||||
|
- Set time
|
||||||
|
- List, add and remove alarms
|
||||||
|
- List and modify settings
|
||||||
|
|
||||||
|
Clients should wait for a response before sending another command.
|
||||||
|
|
||||||
|
### Message Format
|
||||||
|
|
||||||
|
The command and response message formats are the same, except commands
|
||||||
|
have an instruction where responses have a status code:
|
||||||
|
|
||||||
|
| Offset (bytes) | Description | Length (bytes) |
|
||||||
|
|----------------|---------------------------|----------------|
|
||||||
|
| 0 | Instruction / status code | 1 |
|
||||||
|
| 1 | Parameter count | 1 |
|
||||||
|
| 2 | Parameters | *Variable* |
|
||||||
|
|
||||||
|
Parameters are structured as a sequence of type, value tuples:
|
||||||
|
|
||||||
|
| Offset | Description | Length (bytes) |
|
||||||
|
|--------|----------------|------------------------------|
|
||||||
|
| 0 | Type | 1 |
|
||||||
|
| 1 | Value | *Determined by length field* |
|
||||||
|
|
||||||
|
A maximum of 16 parameters is permitted.
|
||||||
|
|
||||||
|
### Instructions
|
||||||
|
|
||||||
|
| Instruction | Description | Command parameters | Response parameters |
|
||||||
|
|-------------|---------------|------------------------------|----------------------------------|
|
||||||
|
| 0 | Get version | None | Major (u8), Minor (u8) |
|
||||||
|
| 1 | Set time | UNIX Timestamp (u64) | None |
|
||||||
|
| 2 | List alarms | None | Sequence of ID (u8), Alarm pairs |
|
||||||
|
| 3 | Add alarm | Alarm (alarm) | Alarm ID (u8) |
|
||||||
|
| 4 | Remove alarm | Alarm ID (u8) | None |
|
||||||
|
| 5 | List settings | None | Sequence of key (string), value |
|
||||||
|
| 6 | Set setting | Key (string), value (string) | None |
|
||||||
|
|
||||||
|
### Status codes
|
||||||
|
|
||||||
|
| Code | Description | Parameters |
|
||||||
|
|------|-----------------|-------------------------|
|
||||||
|
| 0 | OK / Success | *Instruction-dependent* |
|
||||||
|
| 1 | Busy | None |
|
||||||
|
| 2 | Invalid command | None |
|
||||||
|
| 3 | Internal error | None |
|
||||||
|
| 4 | Out of memory | None |
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
| Code | Name | Length (bytes) | Structure |
|
||||||
|
|------|--------|----------------|------------------------------------------|
|
||||||
|
| 0 | U8 | 1 | |
|
||||||
|
| 1 | U16 | 2 | |
|
||||||
|
| 2 | U32 | 4 | |
|
||||||
|
| 3 | U64 | 8 | |
|
||||||
|
| 4 | String | - | Length (u8), UTF-8 |
|
||||||
|
| 5 | Alarm | 3 | Hour (u8), minute (u8), day bitmask (u8) |
|
||||||
|
|
||||||
|
All integers are big-endian.
|
||||||
|
|
||||||
|
#### Day Bitmask
|
||||||
|
|
||||||
|
| Bit (0 is least-significant) | Day |
|
||||||
|
|------------------------------|-----------|
|
||||||
|
| 0 | Monday |
|
||||||
|
| 1 | Tuesday |
|
||||||
|
| 2 | Wednesday |
|
||||||
|
| 3 | Thursday |
|
||||||
|
| 4 | Friday |
|
||||||
|
| 5 | Saturday |
|
||||||
|
| 6 | Sunday |
|
||||||
|
| 7 | *Unused* |
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -264,7 +264,7 @@ void alarms_init(void)
|
|||||||
"alarms",
|
"alarms",
|
||||||
"List, add and remove alarms\n\nIf a dayspec is specified, it "
|
"List, add and remove alarms\n\nIf a dayspec is specified, it "
|
||||||
"should be a sequence of 'x's and '-'s (for enabled or disabled "
|
"should be a sequence of 'x's and '-'s (for enabled or disabled "
|
||||||
"respectively). For example: \"*-*-*--\" means Mondays, Wednesdays "
|
"respectively). For example: \"x-x-x--\" means Mondays, Wednesdays "
|
||||||
"and Fridays only.",
|
"and Fridays only.",
|
||||||
"alarms OR alarms add <hh:mm> [dayspec] OR alarms remove <index> OR "
|
"alarms OR alarms add <hh:mm> [dayspec] OR alarms remove <index> OR "
|
||||||
"alarms <clear>",
|
"alarms <clear>",
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,5 @@
|
|||||||
|
idf_component_register(
|
||||||
|
SRCS "protocol_interface.c"
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES alarms
|
||||||
|
)
|
||||||
@@ -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 <string.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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 <stdint.h>
|
||||||
|
|
||||||
|
#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
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,407 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
# Copyright (c) Camden Dixie O'Brien
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import argparse
|
||||||
|
import enum
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class Weekday:
|
||||||
|
MONDAY = 0
|
||||||
|
TUESDAY = 1
|
||||||
|
WEDNESDAY = 2
|
||||||
|
THURSDAY = 3
|
||||||
|
FRIDAY = 4
|
||||||
|
SATURDAY = 5
|
||||||
|
SUNDAY = 6
|
||||||
|
|
||||||
|
|
||||||
|
class Alarm:
|
||||||
|
SERIALIZED_LENGTH = 3
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def deserialize(alarm_bytes):
|
||||||
|
if len(alarm_bytes) != Alarm.SERIALIZED_LENGTH:
|
||||||
|
raise RuntimeError("Wrong number of bytes for alarm")
|
||||||
|
hour = alarm_bytes[0]
|
||||||
|
minute = alarm_bytes[1]
|
||||||
|
days = Alarm._read_day_bitmask(alarm_bytes[2])
|
||||||
|
return Alarm(hour, minute, days)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _read_day_bitmask(bitmask):
|
||||||
|
days = set()
|
||||||
|
for i in range(7):
|
||||||
|
if (bitmask >> i & 1) != 0:
|
||||||
|
days.add(Weekday(i))
|
||||||
|
return days
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _make_day_bitmask(days):
|
||||||
|
bitmask = 0
|
||||||
|
for i in range(7):
|
||||||
|
if Weekday(i) in days:
|
||||||
|
bitmask |= 1 << i
|
||||||
|
return bitmask
|
||||||
|
|
||||||
|
def __init__(self, hour, minute, days):
|
||||||
|
self.hour = hour
|
||||||
|
self.minute = minute
|
||||||
|
self.days = days
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
day_bitmask = Alarm._make_day_bitmask(self.days)
|
||||||
|
return bytes([self.hour, self.minute, day_bitmask])
|
||||||
|
|
||||||
|
|
||||||
|
class Type(enum.Enum):
|
||||||
|
U8 = 0
|
||||||
|
U16 = 1
|
||||||
|
U32 = 2
|
||||||
|
U64 = 3
|
||||||
|
STRING = 4
|
||||||
|
ALARM = 5
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def length(type):
|
||||||
|
if type == Type.U8:
|
||||||
|
return 1
|
||||||
|
elif type == Type.U16:
|
||||||
|
return 2
|
||||||
|
elif type == Type.U32:
|
||||||
|
return 4
|
||||||
|
elif type == Type.U64:
|
||||||
|
return 8
|
||||||
|
elif type == Type.STRING:
|
||||||
|
return None
|
||||||
|
elif type == Type.ALARM:
|
||||||
|
return Alarm.SERIALIZED_LENGTH
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Invalid param type")
|
||||||
|
|
||||||
|
|
||||||
|
class Param:
|
||||||
|
@staticmethod
|
||||||
|
def deserialize(self, type, value_bytes):
|
||||||
|
return Param(type, Param._deserialize_value(type, value_bytes))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _deserialize_value(self, type, value_bytes):
|
||||||
|
if type == Type.U8:
|
||||||
|
return value_bytes[0]
|
||||||
|
elif type == Type.U16:
|
||||||
|
return struct.unpack('!H', value_bytes)[0]
|
||||||
|
elif type == Type.U32:
|
||||||
|
return struct.pack('!I', value_bytes)[0]
|
||||||
|
elif type == Type.U64:
|
||||||
|
return struct.pack('!Q', value_bytes)[0]
|
||||||
|
elif type == Type.STRING:
|
||||||
|
# Strings handled separately due to variable length
|
||||||
|
raise NotImplemented
|
||||||
|
elif type == Type.ALARM:
|
||||||
|
return Alarm.deserialize(value_bytes)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Invalid param type")
|
||||||
|
|
||||||
|
def __init__(self, type, value):
|
||||||
|
self.type = type
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
if self.type == Type.U8:
|
||||||
|
return bytes([self.value])
|
||||||
|
elif self.type == Type.U16:
|
||||||
|
return struct.pack('!H', self.value)
|
||||||
|
elif self.type == Type.U32:
|
||||||
|
return struct.pack('!I', self.value)
|
||||||
|
elif self.type == Type.U64:
|
||||||
|
return struct.pack('!Q', self.value)
|
||||||
|
elif self.type == Type.STRING:
|
||||||
|
return bytes([len(self.value)]) + self.value.encode('utf-8')
|
||||||
|
elif self.type == Type.ALARM:
|
||||||
|
return self.value.serialize()
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Invalid param type")
|
||||||
|
|
||||||
|
class Instruction(enum.Enum):
|
||||||
|
GET_VERSION = 0
|
||||||
|
SET_TIME = 1
|
||||||
|
LIST_ALARMS = 2
|
||||||
|
ADD_ALARM = 3
|
||||||
|
REMOVE_ALARM = 4
|
||||||
|
LIST_SETTINGS = 5
|
||||||
|
SET_SETTING = 6
|
||||||
|
|
||||||
|
|
||||||
|
class CommandFailed(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Command(abc.ABC):
|
||||||
|
def __init__(self, instruction, *params):
|
||||||
|
self.instruction = instruction
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
def serialize(self):
|
||||||
|
param_bytes = [param.serialize() for param in self.params]
|
||||||
|
return bytes([instruction]) + b''.join(param_bytes)
|
||||||
|
|
||||||
|
def run(self, connection):
|
||||||
|
response = connection.run(self)
|
||||||
|
if response.status != Status.OK:
|
||||||
|
raise CommandFailed()
|
||||||
|
return self.proc_response(response.params)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def proc_response(self, params):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GetVersion(Command):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(Instruction.GET_VERSION)
|
||||||
|
|
||||||
|
def proc_response(self, params):
|
||||||
|
return (params[0].value, params[1].value)
|
||||||
|
|
||||||
|
|
||||||
|
class SetTime(Command):
|
||||||
|
def __init__(self, timestamp):
|
||||||
|
super().__init__(Instruction.SET_TIME, Param(Type.U64, timestamp))
|
||||||
|
|
||||||
|
|
||||||
|
class ListAlarms(Command):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(Instruction.LIST_ALARMS)
|
||||||
|
|
||||||
|
def proc_response(self, params):
|
||||||
|
pairs = zip(params[::2], params[1::2])
|
||||||
|
return {key.value: value.value for key, value in pairs}
|
||||||
|
|
||||||
|
|
||||||
|
class AddAlarm(Command):
|
||||||
|
def __init__(self, hour, minute, days):
|
||||||
|
alarm = Alarm(hour, minute, days)
|
||||||
|
super().__init__(Instruction.ADD_ALARM, Param(Type.ALARM, alarm))
|
||||||
|
|
||||||
|
def proc_response(self, params):
|
||||||
|
return params[0].value
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveAlarm(Command):
|
||||||
|
def __init__(self, alarm_id):
|
||||||
|
super().__init__(Instruction.REMOVE_ALARM, Param(Type.U8, alarm_id))
|
||||||
|
|
||||||
|
|
||||||
|
class ListSettings(Command):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(Instruction.LIST_SETTINGS)
|
||||||
|
|
||||||
|
def proc_response(self, params):
|
||||||
|
pairs = zip(params[::2], params[1::2])
|
||||||
|
return {key.value: value.value for key, value in pairs}
|
||||||
|
|
||||||
|
|
||||||
|
class SetSetting(Command):
|
||||||
|
def __init__(self, key, value):
|
||||||
|
key_param = Param(Type.STRING, key)
|
||||||
|
value_param = Param(Type.STRING, value)
|
||||||
|
super().__init__(Instruction.SET_SETTING, key_param, value_param)
|
||||||
|
|
||||||
|
|
||||||
|
class Status(enum.Enum):
|
||||||
|
OK = 0
|
||||||
|
BUSY = 1
|
||||||
|
INVALID_COMMAND = 2
|
||||||
|
INTERNAL_ERROR = 3
|
||||||
|
OUT_OF_SPACE = 4
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def description(status):
|
||||||
|
if status == OK:
|
||||||
|
return "OK"
|
||||||
|
elif status == BUSY:
|
||||||
|
return "Busy"
|
||||||
|
elif status == INVALID_COMMAND:
|
||||||
|
return "Invalid command"
|
||||||
|
elif status == INTERNAL_ERROR:
|
||||||
|
return "Internal error"
|
||||||
|
elif status == OUT_OF_SPACE:
|
||||||
|
return "Device out of space"
|
||||||
|
else:
|
||||||
|
raise RuntimeError("Invalid status")
|
||||||
|
|
||||||
|
|
||||||
|
class Response:
|
||||||
|
def __init__(self, status, params):
|
||||||
|
self.status = status
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
|
||||||
|
class Connection:
|
||||||
|
PORT = 3498
|
||||||
|
|
||||||
|
def __init__(self, host):
|
||||||
|
self.socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||||
|
self.socket.connect((host, self.PORT))
|
||||||
|
|
||||||
|
def run(self, command):
|
||||||
|
self._send(command)
|
||||||
|
return self._receive_response()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.socket.close()
|
||||||
|
|
||||||
|
def _send(self, command):
|
||||||
|
command_bytes = command.serialize()
|
||||||
|
total_sent = 0
|
||||||
|
while total_sent < len(command_bytes):
|
||||||
|
sent = self.socket.send(command_bytes[total_sent:])
|
||||||
|
if sent == 0:
|
||||||
|
raise RuntimeError("Connection broken")
|
||||||
|
total_sent += sent
|
||||||
|
|
||||||
|
def _receive_response(self):
|
||||||
|
status, param_count = self._receive(2)
|
||||||
|
params = [self._receive_param() for _ in range(param_count)]
|
||||||
|
return Response(status, params)
|
||||||
|
|
||||||
|
def _receive_param(self):
|
||||||
|
type = self._receive(1)
|
||||||
|
if type == Type.STRING:
|
||||||
|
length = self._receive(1)
|
||||||
|
value = self._receive(length).decode('utf-8')
|
||||||
|
return Param(type, value)
|
||||||
|
else:
|
||||||
|
value_bytes = self._receive(Type.length(type))
|
||||||
|
return Param.deserialize(type, value_bytes)
|
||||||
|
|
||||||
|
def _receive(self, n):
|
||||||
|
chunks = []
|
||||||
|
remaining = n
|
||||||
|
while remaining > 0:
|
||||||
|
chunk = self.socket.recv(remaining)
|
||||||
|
if len(chunk) == 0:
|
||||||
|
raise RuntimeError("Connection broken")
|
||||||
|
remaining -= len(chunk)
|
||||||
|
chunks.append(chunk)
|
||||||
|
return b''.join(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def version_subcommand(connection, args):
|
||||||
|
major, minor = GetVersion().run(connection)
|
||||||
|
print(f"{major}.{minor}")
|
||||||
|
|
||||||
|
|
||||||
|
def sync_subcommand(connection, args):
|
||||||
|
timestamp = int(time.time())
|
||||||
|
SetTime(timestamp).run(connection)
|
||||||
|
|
||||||
|
|
||||||
|
def alarms_subcommand(connection, args):
|
||||||
|
def read_dayspec(dayspec):
|
||||||
|
days = set()
|
||||||
|
for i in range(7):
|
||||||
|
if dayspec[i] == 'x':
|
||||||
|
days.add(Weekday(i))
|
||||||
|
return days
|
||||||
|
|
||||||
|
def format_dayspec(days):
|
||||||
|
dayspec = ""
|
||||||
|
for i in range(7):
|
||||||
|
dayspec += 'x' if Weekday(i) in days else '-'
|
||||||
|
return dayspec
|
||||||
|
|
||||||
|
if args.action == None:
|
||||||
|
alarms = ListAlarms().run(connection)
|
||||||
|
for id, alarm in alarms.items():
|
||||||
|
dayspec = format_dayspec(alarm.days)
|
||||||
|
print(f"[{id:2}] {alarm.hour:02}:{alarm.minute:02} {dayspec}")
|
||||||
|
|
||||||
|
elif args.action == "add":
|
||||||
|
hour, minute = args.time.split(':')
|
||||||
|
days = read_dayspec(args.dayspec)
|
||||||
|
id = AddAlarm(int(hour), int(minute), days).run(connection)
|
||||||
|
print(f"{id}")
|
||||||
|
|
||||||
|
elif args.action == "rm":
|
||||||
|
RemoveAlarm(int(args.id)).run(connection)
|
||||||
|
|
||||||
|
|
||||||
|
def settings_subcommand(connection, args):
|
||||||
|
if args.action == None:
|
||||||
|
settings = ListSettings().run(connection)
|
||||||
|
for key, value in settings.items():
|
||||||
|
print(f"{key:15} {value}")
|
||||||
|
elif args.action == "set":
|
||||||
|
SetSetting(args.key, args.value).run(connection)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Control a bedside clock remotely")
|
||||||
|
parser.add_argument(
|
||||||
|
"--host", help="The hostname/IP of the clock",
|
||||||
|
default="bedside-clock")
|
||||||
|
subparsers = parser.add_subparsers(
|
||||||
|
help="Action to perform", dest="subcommand", required=True)
|
||||||
|
|
||||||
|
version_parser = subparsers.add_parser(
|
||||||
|
"version", help="Get the version of the clock",
|
||||||
|
description = "Get the version of the clock")
|
||||||
|
sync_parser = subparsers.add_parser(
|
||||||
|
"sync", help="Syncronise the clock to this machine",
|
||||||
|
description="Syncronise the clock to this machine")
|
||||||
|
alarms_parser = subparsers.add_parser(
|
||||||
|
"alarms", help="List and manage alarms",
|
||||||
|
description="List and manage alarms")
|
||||||
|
settings_parser = subparsers.add_parser(
|
||||||
|
"settings", help="List and manage settings",
|
||||||
|
description="List and manage settings")
|
||||||
|
|
||||||
|
alarms_subparsers = alarms_parser.add_subparsers(dest="action")
|
||||||
|
alarms_add_parser = alarms_subparsers.add_parser(
|
||||||
|
"add", help="Add an alarm", description="Add an alarm")
|
||||||
|
alarms_rm_parser = alarms_subparsers.add_parser(
|
||||||
|
"rm", help="Remove an alarm", description="Remove an alarm")
|
||||||
|
|
||||||
|
alarms_add_parser.add_argument(
|
||||||
|
"--workdays", const="xxxxx--", dest="dayspec", action="store_const")
|
||||||
|
alarms_add_parser.add_argument(
|
||||||
|
"--weekends", const="-----xx", dest="dayspec", action="store_const")
|
||||||
|
alarms_add_parser.add_argument(
|
||||||
|
"time", help="The time for the alarm (HH:MM)")
|
||||||
|
alarms_add_parser.add_argument(
|
||||||
|
"dayspec", help="""Days for the alarm to be active on.
|
||||||
|
|
||||||
|
Should be a sequence of 'x's and '-'s. For example,
|
||||||
|
\"x-x----\" means Mondays and Tuesdays only.""",
|
||||||
|
default="xxxxxxx")
|
||||||
|
|
||||||
|
alarms_rm_parser.add_argument(
|
||||||
|
"id", help="ID of the alarm to remove")
|
||||||
|
|
||||||
|
settings_subparsers = settings_parser.add_subparsers(dest="action")
|
||||||
|
settings_set_parser = settings_subparsers.add_parser(
|
||||||
|
"set", help="Set a setting", description="Set a setting")
|
||||||
|
|
||||||
|
settings_set_parser.add_argument(
|
||||||
|
"key", help="Key of the setting to set")
|
||||||
|
settings_set_parser.add_argument(
|
||||||
|
"value", help="Value of the setting to set")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
subcommand_map = {
|
||||||
|
"version": version_subcommand,
|
||||||
|
"sync": sync_subcommand,
|
||||||
|
"alarms": alarms_subcommand,
|
||||||
|
"settings": settings_subcommand,
|
||||||
|
}
|
||||||
|
connection = Connection(args.host)
|
||||||
|
subcommand_map[args.subcommand](connection, args)
|
||||||
|
connection.close()
|
||||||
Reference in new issue
Block a user