Browse Source

ble: Add Request, Response, Notification classes

ble
Andreas Berthoud 4 years ago
parent
commit
d63d56ddea
  1. 21
      README.md
  2. 7
      backend/__init__.py
  3. 178
      backend/command.py
  4. 31
      backend/command_endpoint.py
  5. 243
      backend/command_execution.py
  6. 273
      backend/commands.py
  7. 2
      backend/defaults/config.yml
  8. 4
      nucleo-wb55-ble/.vimrc
  9. 12
      nucleo-wb55-dongle-ble/Core/Src/main.c
  10. 37
      nucleo-wb55-dongle-ble/app/Command.cpp
  11. 19
      nucleo-wb55-dongle-ble/app/Command.hpp
  12. 61
      nucleo-wb55-dongle-ble/app/LedCommand.cpp
  13. 74
      nucleo-wb55-dongle-ble/app/LedCommand.hpp
  14. 22
      nucleo-wb55-dongle-ble/app/LogCommand.cpp
  15. 11
      nucleo-wb55-dongle-ble/app/LogCommand.hpp
  16. 34
      nucleo-wb55-dongle-ble/app/Notification.cpp
  17. 38
      nucleo-wb55-dongle-ble/app/Notification.hpp
  18. 19
      nucleo-wb55-dongle-ble/app/Request.cpp
  19. 30
      nucleo-wb55-dongle-ble/app/Request.hpp
  20. 15
      nucleo-wb55-dongle-ble/app/Response.cpp
  21. 23
      nucleo-wb55-dongle-ble/app/Response.hpp
  22. 12
      nucleo-wb55-dongle-ble/app/command_interpreter.c
  23. 1
      nucleo-wb55-dongle-ble/app/commands.h

21
README.md

@ -1,3 +1,11 @@
- [STM32WB55 boards](#stm32wb55-boards)
- [Board](#board)
- [Wireless stack update](#wireless-stack-update)
- [Dongle](#dongle)
- [BLE](#ble)
- [GAP](#gap)
- [GATT](#gatt)
- [Client](#client)
# STM32WB55 boards
## Board
@ -19,3 +27,16 @@
## Dongle
Just update FUS and download binary
## BLE
### GAP
GAP concepts: https://learn.adafruit.com/introduction-to-bluetooth-low-energy/gap
### GATT
GATT concepts: https://learn.adafruit.com/introduction-to-bluetooth-low-energy/gatt
### Client
[ble-client-seq.puml](ble-client-seq.puml)

7
backend/__init__.py

@ -3,13 +3,14 @@ from logging.config import dictConfig
from flask_api import FlaskAPI
from . import command
from . import command_endpoint
from . import command_execution
from . import container
def create_app() -> FlaskAPI:
app = FlaskAPI(__name__)
app.register_blueprint(command.bp)
app.register_blueprint(command_endpoint.bp)
dictConfig(
{
@ -34,6 +35,6 @@ def create_app() -> FlaskAPI:
if os.environ.get("WERKZEUG_RUN_MAIN") != "true":
# prevent from be called twice in debug mode
command.start_backgroup_process()
command_execution.start_backgroup_process()
return app

178
backend/command.py

@ -1,178 +0,0 @@
import atexit
import logging
import os
import time
from dataclasses import dataclass
from enum import Enum
from multiprocessing import Process
from multiprocessing import Queue
from struct import pack
from struct import unpack
from typing import Optional
from flask import Response
from flask import blueprints
from flask import request
from flask_api import status
from serial import Serial
from .container import get_container
from .container import initialize_container
bp = blueprints.Blueprint("command", __name__)
_process: Optional[Process] = None
_queue: Queue = Queue()
_logger = logging.getLogger(__name__)
@bp.route("/command", methods=["POST", "GET"])
def command():
logger = logging.getLogger("test")
command_id = request.args.get("command-id")
if _queue is not None and command_id is not None:
logger.info(f"put in queue: {command_id}")
_queue.put(command_id)
return Response(status=status.HTTP_200_OK)
def _end_running_process():
if _process is not None:
_process.kill()
def worker_process(
queue: Queue,
config,
# container: Container
):
logging.basicConfig(
level=logging.DEBUG,
format="[%(asctime)s] [%(name)-20s] [%(levelname)-8s] --- %(message)s",
)
logger = logging.getLogger("worker_process")
logger.setLevel(logging.INFO)
initialize_container(config)
container = get_container()
counter = 1
with container.serial() as serial:
while True:
logger.debug(f"Ping {counter} process_id={os.getpid()}")
counter += 1
time.sleep(0.01)
receive_and_log(
serial=serial,
header_size=container.config.header_size(),
)
if queue is not None:
while not queue.empty():
command_id = queue.get()
payload = "test value".encode()
length = len(payload)
data = pack(
">BHB" + "B" * length + "B",
int(command_id),
length,
0,
*list(payload),
0xFF,
)
serial.write(data)
class CommandId(Enum):
command_none = 0
command_log = 0x1
@dataclass
class LogCommand:
"""Command ID: command_log"""
level: int
message: str
HEADER_SIZE = 1 # log level
def __init__(
self,
data: bytes,
) -> None:
self._logger = logging.getLogger(self.__class__.__name__)
self.received_logger = logging.getLogger("stm32wb55")
self.received_logger.setLevel(logging.DEBUG)
self._logger.setLevel(logging.INFO)
level = int(data[0])
self._logger.debug(f"level: {level}")
message = data[self.HEADER_SIZE :]
self._logger.debug("Message: " + str(message))
self.level = level
self.message = message.decode()
def execute(self):
self.received_logger.log(level=self.level, msg=self.message)
def receive_and_log(
serial: Serial,
header_size: int,
):
logger = logging.getLogger("receive_and_log")
logger.setLevel(logging.INFO)
bytes_read = serial.read(serial.in_waiting)
while bytes_read:
logger.debug(f"bytes: {bytes_read.hex()}")
command_id_int, data_length, _ = unpack(">BHB", bytes_read[:header_size])
payload = bytes(bytes_read[header_size : header_size + data_length])
stop_byte = bytes_read[header_size + data_length]
try:
command_id = CommandId(command_id_int)
except ValueError:
logger.error(
f"invalid command {command_id_int} with payload {str(payload)}",
)
bytes_read = bytes_read[header_size + data_length + 1 :]
continue
if command_id == CommandId.command_log:
command = LogCommand(
data=payload,
)
command.execute()
else:
return
logger.debug(f"stop_byte: {stop_byte}")
assert stop_byte == 0xFF
bytes_read = bytes_read[header_size + data_length + 1 :]
def start_backgroup_process():
_logger.warning("start_backgroup_process called")
global _process
_process = Process(
target=worker_process,
args=(
_queue,
get_container().config(),
),
)
_process.start()
atexit.register(_end_running_process)

31
backend/command_endpoint.py

@ -0,0 +1,31 @@
from flask import Response
from flask import blueprints
from flask import request
from flask_api import status
from .command_execution import enqueue_command
from .commands import CommandId
from .commands import get_command_id_from_name
from .commands import get_request_class
bp = blueprints.Blueprint("command", __name__)
@bp.route("/command", methods=["POST", "GET"])
def command():
arguments = dict(request.args)
cmd = arguments.pop("cmd")
try:
command_id = CommandId(int(cmd))
except ValueError:
command_id = get_command_id_from_name(cmd)
try:
command = get_request_class(command_id=command_id)(**arguments)
except Exception:
return Response(status=status.HTTP_400_BAD_REQUEST)
enqueue_command(command)
return Response(status=status.HTTP_200_OK)

243
backend/command_execution.py

@ -0,0 +1,243 @@
import atexit
import logging
import time
from enum import Enum
from multiprocessing import Process
from multiprocessing import Queue
from struct import unpack
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
from serial import Serial
from . import commands
from .commands import Command
from .commands import CommandId
from .commands import Request
from .commands import Response
from .container import get_container
from .container import initialize_container
_logger = logging.getLogger(__file__)
_command_queue: Queue = Queue()
class State(Enum):
heart_beat = 0x0
executing_command = 0x1
executing_command_waiting_for_response = 0x2
receiving_command = 0x10
def worker_process(
queue: Queue,
config,
):
logging.basicConfig(
level=logging.DEBUG,
format="[%(asctime)s] [%(name)-20s] [%(levelname)-8s] --- %(message)s",
)
logger = logging.getLogger("Command Loop")
logger.setLevel(logging.INFO)
initialize_container(config)
container = get_container()
heartbeat_interval = container.config.heartbeat_interval()
serial_reconnection_wait_timeout = (
container.config.serial_reconnection_wait_timeout()
)
connected = False
logger.info("entering command loop...")
while True:
try:
with container.serial() as serial:
logger.info("connected with serial device")
connected = True
enter_fsm(
serial=serial,
command_queue=queue,
heartbeat_interval=heartbeat_interval,
)
except OSError:
if connected:
logger.warning("connection to serial device lost")
connected = False
time.sleep(serial_reconnection_wait_timeout)
logger.warning("reconnecting...")
def enter_fsm(
serial: Serial,
command_queue: Queue,
heartbeat_interval: float,
):
logger = logging.getLogger("FSM")
state = State.executing_command
current_command: Optional[Command] = None
responses_received: List[Response] = list()
time_at_beginning_waiting_for_response: float = 0.0
last_heart_beat_time: float = 0.0
while True:
if state == State.heart_beat:
if time.time() - heartbeat_interval > last_heart_beat_time:
command_queue.put(commands.HeartbeatRequest())
last_heart_beat_time = time.time()
state = State.executing_command
continue
elif state == State.executing_command:
current_command = dequeue_command(queue=command_queue)
if current_command is None:
state = State.receiving_command
continue
current_command.execute(serial=serial)
if isinstance(current_command, Request):
time_at_beginning_waiting_for_response = time.time()
state = State.executing_command_waiting_for_response
continue
elif state == State.executing_command_waiting_for_response:
if not isinstance(current_command, Request):
raise RuntimeError(
"entered state 'executing_command_waiting_for_response' but "
"current command does not expect a response.",
)
else:
request: Request = current_command
commands_, responses = receive(serial=serial)
responses_received.extend(responses)
for command in commands_:
command_queue.put(command)
while responses_received:
received_response: Response = responses_received.pop(0)
if request.response_identifier == received_response.identifier:
request.process_response(
response=received_response,
)
state = State.executing_command
break
else:
logger.warning(
f"received response with ID {received_response.identifier} "
"but expected response with ID "
f"{request.response_identifier}",
)
else:
if (
time.time() - request.timeout
> time_at_beginning_waiting_for_response
):
logger.error(
"Timeout while waiting for response with ID "
f"{request.response_identifier}",
)
current_command = None
state = State.executing_command
continue
elif state == State.receiving_command:
commands_, responses = receive(serial=serial)
responses_received.extend(responses)
for command in commands_:
command_queue.put(command)
state = State.heart_beat
continue
else:
raise RuntimeError(f"Invalid state: {state}")
def dequeue_command(
queue: Queue,
) -> Optional[Command]:
while not queue.empty():
return queue.get()
return None
def enqueue_command(command: Command):
_command_queue.put(command)
def receive(
serial: Serial,
) -> Tuple[Sequence[Command], Sequence[Response]]:
logger = logging.getLogger("receive_and_log")
logger.setLevel(logging.INFO)
commands_received: List[Command] = list()
responses_received: List[Response] = list()
header_size = 4
bytes_read = serial.read(serial.in_waiting)
while bytes_read:
logger.debug(f"bytes: {bytes_read.hex()}")
command_id_int, data_length, _ = unpack(">BHB", bytes_read[:header_size])
payload = bytes(bytes_read[header_size : header_size + data_length])
stop_byte = bytes_read[header_size + data_length]
try:
command_id = CommandId(command_id_int)
except ValueError:
logger.error(
f"invalid command {command_id_int} with payload {str(payload)}",
)
bytes_read = bytes_read[header_size + data_length + 1 :]
continue
logger.debug(f"stop_byte: {stop_byte}")
if stop_byte != 0xFF:
logger.error("Invalid stop byte")
bytes_read = None
else:
bytes_read = bytes_read[header_size + data_length + 1 :]
if command_id == CommandId.command_log:
command = commands.LogCommand(
data=payload,
)
commands_received.append(command)
elif command_id == CommandId.command_heartbeat_response:
responses_received.append(commands.HeartbeatResponse(payload))
elif command_id == CommandId.command_led_response:
responses_received.append(commands.LEDResponse(payload))
else:
raise RuntimeError
return commands_received, responses_received
_process: Optional[Process] = None
def _end_running_process():
if _process is not None:
_process.kill()
def start_backgroup_process():
_logger.warning("start_backgroup_process called")
global _process
_process = Process(
target=worker_process,
args=(
_command_queue,
get_container().config(),
),
)
_process.start()
atexit.register(_end_running_process)

273
backend/commands.py

@ -0,0 +1,273 @@
import abc
import logging
from dataclasses import dataclass
from enum import Enum
from random import randint
from struct import pack
from struct import unpack
from typing import Union
from serial import Serial
class CommandId(Enum):
command_none = 0
command_log = 0x1
# even numbers are requests with optional response with ID = request ID + 1
command_heartbeat_request = 0x2
command_heartbeat_response = 0x3
command_led_request = 0x4
command_led_response = 0x5
def get_command_id_from_name(name: str) -> CommandId:
return {
"log": CommandId.command_log,
"led": CommandId.command_led_request,
}[name]
def get_request_class(
command_id: CommandId,
):
return {
CommandId.command_log: LogCommand,
CommandId.command_heartbeat_request: HeartbeatRequest,
CommandId.command_led_request: LEDRequest,
}[command_id]
def get_response_class(
command_id: CommandId,
):
return {
CommandId.command_heartbeat_response: HeartbeatResponse,
CommandId.command_led_response: LEDResponse,
}[command_id]
class Response(abc.ABC):
identifier: int
def __init__(
self,
data: bytes,
) -> None:
self._logger = logging.getLogger(self.__class__.__name__)
self.identifier = unpack(">H", data[:2])[0]
self.unpack_payload(data[2:])
@abc.abstractmethod
def unpack_payload(
self,
data: bytes,
):
pass
class Command(abc.ABC):
def __init__(self) -> None:
self._logger = logging.getLogger(self.__class__.__name__)
@property
@abc.abstractmethod
def identifier(self) -> CommandId:
pass
@abc.abstractmethod
def execute(
self,
serial: Serial,
):
pass
def send_command(self, payload: bytes, serial: Serial):
length = len(payload)
data = pack(
">BHB" + "B" * length + "B",
int(self.identifier.value),
length,
0,
*list(payload),
0xFF,
)
serial.write(data)
class Request(Command):
def __init__(self) -> None:
super().__init__()
self.response_identifier = randint(0, pow(2, 16) - 1)
@property
@abc.abstractmethod
def timeout(self) -> float:
pass
@abc.abstractmethod
def process_response(
self,
response: Response,
):
pass
def send_command(self, payload: bytes, serial: Serial):
response_identifier_header = pack(">H", self.response_identifier)
super().send_command(
payload=response_identifier_header + payload,
serial=serial,
)
@dataclass
class LogCommand(Command):
"""Command ID: command_log"""
level: int
message: str
HEADER_SIZE = 1 # log level
def __init__(
self,
data: bytes,
) -> None:
super().__init__()
self.received_logger = logging.getLogger("stm32wb55")
self.received_logger.setLevel(logging.DEBUG)
self._logger.setLevel(logging.INFO)
level = int(data[0])
self._logger.debug(f"level: {level}")
message = data[self.HEADER_SIZE :]
self._logger.debug("Message: " + str(message))
self.level = level
self.message = message.decode()
@property
def identifier(self) -> CommandId:
return CommandId.command_log
def execute(
self,
serial: Serial,
):
self.received_logger.log(level=self.level, msg=self.message)
class HeartbeatResponse(Response):
def unpack_payload(
self,
data: bytes,
):
pass
class HeartbeatRequest(Request):
@property
def identifier(self) -> CommandId:
return CommandId.command_heartbeat_request
@property
def timeout(self) -> float:
return 0.1
def process_response(self, response: Response):
if not isinstance(response, HeartbeatResponse):
raise TypeError(f"{response} is not a {HeartbeatResponse}")
def execute(self, serial: Serial):
self.send_command(
payload=bytes(),
serial=serial,
)
class LEDResponse(Response):
was_successful = True
# def __init__(self) -> None:
# super().__init__()
def unpack_payload(
self,
data: bytes,
):
self.was_successful = bool(data[0])
if self.was_successful:
self._logger.debug("LED command was successful")
else:
self._logger.debug("LED command was not successful")
class LEDRequest(Request):
def __init__(
self,
id: Union[int, str],
command: Union[int, str],
) -> None:
"""
led_id
-------
0: green
1: red
2: blue
led_command
--------
0: off
1: on
2: toggle
"""
super().__init__()
try:
self.led_id = int(id)
except ValueError:
self.led_id = {
"green": 0,
"red": 1,
"blue": 2,
}[str(id)]
try:
self.led_command = int(command)
except ValueError:
self.led_command = {
"off": 0,
"on": 1,
"toggle": 2,
}[str(command)]
@property
def identifier(self) -> CommandId:
return CommandId.command_led_request
@property
def timeout(self) -> float:
return 0.1
def process_response(self, response: Response):
if not isinstance(response, LEDResponse):
raise TypeError(f"{response} is not a {LEDResponse}")
def execute(self, serial: Serial):
payload = pack(
">BB",
self.led_id,
self.led_command,
)
self.send_command(
payload=payload,
serial=serial,
)

2
backend/defaults/config.yml

@ -2,3 +2,5 @@
device_id: /dev/tty.usbmodem207E3283544E1
baudrate: 115200
header_size: 4
heartbeat_interval: 1
serial_reconnection_wait_timeout: 1

4
nucleo-wb55-ble/.vimrc

@ -7,3 +7,7 @@ augroup END
let &path.="Core/Inc,Drivers/STM32WBxx_HAL_Driver/Inc,Drivers/STM32WBxx_HAL_Driver/Inc/Legacy,Drivers/CMSIS/Device/ST/STM32WBxx/Include,Drivers/CMSIS/Include,STM32_WPAN/App,Utilities/lpm/tiny_lpm,Middlewares/ST/STM32_WPAN,Middlewares/ST/STM32_WPAN/interface/patterns/ble_thread,Middlewares/ST/STM32_WPAN/interface/patterns/ble_thread/tl,Middlewares/ST/STM32_WPAN/interface/patterns/ble_thread/shci,Middlewares/ST/STM32_WPAN/utilities,Middlewares/ST/STM32_WPAN/ble/core,Middlewares/ST/STM32_WPAN/ble/core/auto,Middlewares/ST/STM32_WPAN/ble/core/template,Middlewares/ST/STM32_WPAN/ble/svc/Inc,Middlewares/ST/STM32_WPAN/ble/svc/Src,Utilities/sequencer,Middlewares/ST/STM32_WPAN/ble,"
set tabstop=2
set softtabstop=2
set shiftwidth=2
set noexpandtab

12
nucleo-wb55-dongle-ble/Core/Src/main.c

@ -98,16 +98,16 @@ int main(void)
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_SET);
HAL_Delay(50);
HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_RESET);
// HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_SET);
// HAL_Delay(50);
// HAL_GPIO_WritePin(LED_GREEN_GPIO_Port, LED_GREEN_Pin, GPIO_PIN_RESET);
pop_and_execute_commands();
HAL_Delay(450);
counter_value++;
// HAL_Delay(450);
// counter_value++;
log_debug("Counter value is %d", 1, counter_value);
// log_debug("Counter value is %d", 1, counter_value);
// log_info("Counter value is %d", 1, counter_value);
// log_warning("Counter value is %d", 1, counter_value);
// log_error("Counter value is %d", 1, counter_value);

37
nucleo-wb55-dongle-ble/app/Command.cpp

@ -9,23 +9,10 @@
#include "Command.hpp"
#include "string.h"
#include "stm32wbxx_hal.h"
#include "commands.hpp"
Command::Command(CommandId id) : id(id) {
this->data[0] = id;
this->data[3] = 0x00; // reserved
this->payload_ptr = this->data + 4;
}
void Command::set_payload_length(uint16_t payload_length) {
this->payload_length = payload_length;
data[1] = (payload_length & 0xFF00) >> 8;
data[2] = payload_length & 0x00FF;
}
uint16_t Command::get_payload_length() {
return this->payload_length;
}
#include "HeartbeatCommand.hpp"
#include "LedCommand.hpp"
std::queue<Command*> command_queue;
@ -38,6 +25,7 @@ void pop_and_execute_commands() {
while (!command_queue.empty()) {
Command * command = command_queue.front();
bool was_successful = command->execute();
HAL_Delay(5); // this delay is required. Otherwise, the command were not sent somehow...
if (!was_successful) {
log_error("Execution of command with ID %n was not successful!", 1, command->id);
}
@ -45,3 +33,20 @@ void pop_and_execute_commands() {
command_queue.pop();
}
}
void handle_received_command(uint8_t command_id, uint8_t * payload_ptr, uint16_t size) {
switch (command_id)
{
case COMMAND_HEARTBEAT_REQUEST:
push_command(new HeartbeatRequest(payload_ptr, size));
break;
case COMMAND_LED_REQUEST:
push_command(new LedRequest(payload_ptr, size));
break;
default:
break;
}
}

19
nucleo-wb55-dongle-ble/app/Command.hpp

@ -11,10 +11,15 @@
#include <stdint.h>
extern "C" void pop_and_execute_commands();
extern "C" void handle_received_command(uint8_t command_id, uint8_t * payload_ptr, uint16_t length);
typedef enum : uint8_t {
COMMAND_NONE = 0,
COMMAND_LOG = 0x1,
COMMAND_HEARTBEAT_REQUEST = 0x2,
COMMAND_HEARTBEAT_RESPONSE = 0x3,
COMMAND_LED_REQUEST = 0x4,
COMMAND_LED_RESPONSE = 0x5,
} CommandId;
@ -22,22 +27,10 @@ class Command {
public:
CommandId id;
Command(CommandId command_id);
virtual ~Command() {};
virtual CommandId get_command_id() = 0;
virtual bool execute() = 0;
protected:
uint8_t data[512];
uint8_t * payload_ptr;
const int max_payload_length = 507;
void set_payload_length(uint16_t length);
uint16_t get_payload_length();
private:
uint16_t payload_length;
};
void push_command(Command * command);

61
nucleo-wb55-dongle-ble/app/LedCommand.cpp

@ -0,0 +1,61 @@
/*
* LedCommand.cpp
*
* Created on: Jul 16, 2021
* Author: Andreas Berthoud
*/
#include "LedCommand.hpp"
#include "commands.h"
#include "stm32wbxx_hal.h"
#include "main.h"
LedResponse::LedResponse(uint16_t response_identifier, bool was_successful)
: Response(response_identifier) {
(this->payload_ptr + this->get_payload_size())[0] = (uint8_t)was_successful;
this->add_to_payload_size(1);
}
LedRequest::LedRequest(uint8_t * payload_ptr, uint16_t size) : Request(payload_ptr, size) {
uint16_t expected_length = this->buffer_offset + 2;
if (expected_length != size) {
//log_error("LedCommand: received request with length %d, expected length %d", 2, size, expected_length);
this->has_error = true;
return;
}
uint8_t * data_ptr = payload_ptr + this->buffer_offset;
this->led_id = static_cast<LedID>(data_ptr[0]);
this->led_command = static_cast<LedCommand>(data_ptr[1]);
}
LedResponse * LedRequest::execute_request(uint16_t response_identifier) {
bool was_successful = true;
int led_pin_mapping[3] = {LED_GREEN_Pin, LED_RED_Pin, LED_BLUE_Pin};
GPIO_TypeDef* led_prio_port_mapping[3] = {LED_GREEN_GPIO_Port, LED_RED_GPIO_Port, LED_BLUE_GPIO_Port};
if (led_id >= led_id_max) {
//log_error("LedCommand: invalid LED ID %d", 1, led_id);
was_successful = false;
} else {
switch (this->led_command)
{
case led_off:
HAL_GPIO_WritePin(led_prio_port_mapping[led_id], led_pin_mapping[led_id], GPIO_PIN_RESET);
break;
case led_on:
HAL_GPIO_WritePin(led_prio_port_mapping[led_id], led_pin_mapping[led_id], GPIO_PIN_SET);
break;
case led_toggle:
HAL_GPIO_TogglePin(led_prio_port_mapping[led_id], led_pin_mapping[led_id]);
break;
default:
//log_error("LedCommand: invalid LED command %d", 1, this->led_id);
was_successful = false;
break;
}
}
return new LedResponse(response_identifier, was_successful);
}

74
nucleo-wb55-dongle-ble/app/LedCommand.hpp

@ -0,0 +1,74 @@
/*
* LedCommand.hpp
*
* Created on: Jul 16, 2021
* Author: Andreas Berthoud
*/
#ifndef LEDCOMMAND_HPP_
#define LEDCOMMAND_HPP_
#include "Request.hpp"
#include "Response.hpp"
class LedResponse : public Response {
public:
LedResponse(uint16_t response_identifier, bool was_successful);
CommandId get_command_id() override { return COMMAND_LED_RESPONSE; }
private:
bool was_successful = false;
};
class LedRequest : public Request {
public:
/**
* @brief Construct a new Led Request object
*
* @param payload_ptr
* HEADER | led_id | led_command |
* | 1 byte | 1 byte |
*
* led_id
* -------
* 0: green
* 1: red
* 2: blue
*
* led_command
* -----------
* 0: off
* 1: on
* 2: toggle
*
* @param size
*/
LedRequest(uint8_t * payload_ptr, uint16_t size);
enum LedID {
greed_led,
greed_red,
greed_blue,
led_id_max,
};
enum LedCommand {
led_off,
led_on,
led_toggle,
led_command_max,
};
LedResponse * execute_request(uint16_t response_identifier);
CommandId get_command_id() override { return COMMAND_LED_REQUEST; }
private:
bool has_error = false;
LedID led_id = greed_led;
LedCommand led_command = led_off;
};
#endif /* LEDCOMMAND_HPP_ */

22
nucleo-wb55-dongle-ble/app/LogCommand.cpp

@ -9,7 +9,6 @@
#include <string.h>
#include <string>
#include "usbd_cdc_if.h"
#include "LogCommand.hpp"
@ -41,15 +40,16 @@ void log_error(const char * format, int nargs, ...) {
va_end(args);
}
LogCommand::LogCommand(const char * format, va_list args, LoggingLevel logging_level) : Command(COMMAND_LOG) {
*this->payload_ptr = logging_level;
vsnprintf((char *)this->payload_ptr + 1, this->max_payload_length - 1, format, args);
this->set_payload_length(strlen((char *)this->payload_ptr) + 1); // strlen + log level
}
LogCommand::LogCommand(
const char * format,
va_list args,
LoggingLevel logging_level) : Notification() {
uint16_t max_payload_size = NOTIFICATION_COMMAND_FREE_BUFFER - this->get_payload_size();
this->payload_ptr[0] = logging_level;
bool LogCommand::execute() {
uint16_t size = this->get_payload_length() + 5; // number of bytes to be sent over USB
this->data[size-1] = 0xff; // set stop byte
uint8_t result = CDC_Transmit_FS(this->data, size);
return result == USBD_OK || result == USBD_BUSY;
char * message = (char *)(this->payload_ptr + 1);
vsnprintf(message, max_payload_size - 1, format, args);
this->add_to_payload_size(strlen(message) + 1);
}

11
nucleo-wb55-dongle-ble/app/LogCommand.hpp

@ -8,7 +8,7 @@
#ifndef LOGCOMMAND_H_
#define LOGCOMMAND_H_
#include "Command.hpp"
#include "Notification.hpp"
extern "C" void log_debug(const char * format, int nargs, ...);
extern "C" void log_info(const char * format, int nargs, ...);
@ -22,7 +22,7 @@ typedef enum : uint8_t{
LOG_LEVEL_ERROR = 40,
} LoggingLevel;
class LogCommand : public Command {
class LogCommand : public Notification {
public:
/*
@ -31,8 +31,11 @@ public:
LogCommand(const char * format, va_list args, LoggingLevel logging_level);
virtual ~LogCommand() {};
bool execute() override;
virtual CommandId get_command_id() override { return COMMAND_LOG; }
private:
uint16_t payload_size = 0;
};
#endif /* LOGCOMMAND_H_ */

34
nucleo-wb55-dongle-ble/app/Notification.cpp

@ -0,0 +1,34 @@
/*
* Notification.cpp
*
* Created on: Jul 14, 2021
* Author: Andreas Berthoud
*/
#include "usbd_cdc_if.h"
#include "Notification.hpp"
Notification::Notification() {
this->payload_ptr = this->data +4;
}
bool Notification::execute() {
uint16_t size = NOTIFICATION_COMMAND_TOTAL_OVERHEAD + this->payload_size;
this->data[0] = (uint8_t)this->get_command_id();
this->data[1] = (this->payload_size & 0xFF00) >> 8;
this->data[2] = this->payload_size & 0x00FF;
this->data[3] = 0x00; // reserved
this->data[size-1] = 0xff; // set stop byte
uint8_t result = CDC_Transmit_FS(this->data, size);
return result == USBD_OK || result == USBD_BUSY;
}
uint16_t Notification::get_payload_size() {
return this->payload_size;
}
void Notification::add_to_payload_size(uint16_t size) {
this->payload_size += size;
}

38
nucleo-wb55-dongle-ble/app/Notification.hpp

@ -0,0 +1,38 @@
/*
* Notification.hpp
*
* Created on: Jul 14, 2021
* Author: Andreas Berthoud
*/
#ifndef NOTIFICATION_HPP_
#define NOTIFICATION_HPP_
#include "Command.hpp"
#define BUFFER_SIZE (512)
#define NOTIFICATION_COMMAND_HEADER_SIZE (4)
#define NOTIFICATION_COMMAND_STOP_BYTE_SIZE (1)
#define NOTIFICATION_COMMAND_TOTAL_OVERHEAD (NOTIFICATION_COMMAND_HEADER_SIZE + NOTIFICATION_COMMAND_STOP_BYTE_SIZE)
#define NOTIFICATION_COMMAND_FREE_BUFFER (BUFFER_SIZE - NOTIFICATION_COMMAND_TOTAL_OVERHEAD)
class Notification: public Command {
public:
Notification();
virtual ~Notification() {};
bool execute() override;
uint16_t get_payload_size();
void add_to_payload_size(uint16_t size);
protected:
uint8_t * payload_ptr;
private:
uint16_t payload_size = 0;
uint8_t data[BUFFER_SIZE];
};
#endif /* NOTIFICATION_HPP_ */

19
nucleo-wb55-dongle-ble/app/Request.cpp

@ -0,0 +1,19 @@
/*
* Request.cpp
*
* Created on: Jul 14, 2021
* Author: Andreas Berthoud
*/
#include "Request.hpp"
Request::Request(uint8_t * payload_ptr, uint16_t size) : Command() {
this->response_identifier = payload_ptr[0] << 8 | payload_ptr[1];
}
bool Request::execute() {
Response * response = this->execute_request(this->response_identifier);
bool result = response->execute();
delete response;
return result;
}

30
nucleo-wb55-dongle-ble/app/Request.hpp

@ -0,0 +1,30 @@
/*
* Request.hpp
*
* Created on: Jul 14, 2021
* Author: Andreas Berthoud
*/
#ifndef REQUEST_HPP_
#define REQUEST_HPP_
#include "Command.hpp"
#include "Response.hpp"
class Request : public Command {
public:
Request(uint8_t * payload_ptr, uint16_t size);
bool execute() override;
virtual Response * execute_request(uint16_t response_identifier) = 0;
protected:
int buffer_offset = 2;
private:
uint16_t response_identifier;
};
#endif /* REQUEST_HPP_ */

15
nucleo-wb55-dongle-ble/app/Response.cpp

@ -0,0 +1,15 @@
/*
* Response.cpp
*
* Created on: Jul 14, 2021
* Author: Andreas Berthoud
*/
#include "Response.hpp"
Response::Response(uint16_t response_identifier)
: Notification(), response_identifier(response_identifier) {
this->payload_ptr[0] = (response_identifier & 0xff00) >> 8;
this->payload_ptr[1] = response_identifier & 0x00ff;
this->add_to_payload_size(2);
}

23
nucleo-wb55-dongle-ble/app/Response.hpp

@ -0,0 +1,23 @@
/*
* Response.hpp
*
* Created on: Jul 14, 2021
* Author: Andreas Berthoud
*/
#ifndef RESPONSE_HPP_
#define RESPONSE_HPP_
#include "Notification.hpp"
class Response : public Notification {
public:
Response(uint16_t response_identifier);
virtual ~Response() {};
private:
uint16_t response_identifier;
};
#endif /* RESPONSE_HPP_ */

12
nucleo-wb55-dongle-ble/app/command_interpreter.c

@ -10,9 +10,8 @@
void usb_receive(uint8_t *buf, uint32_t *len) {
CDC_Transmit_FS(buf, *len); // echo
if (*len < 5) {
log_error("received command which cannot be interpreted", 0);
return;
}
uint8_t command_id = buf[0];
@ -24,7 +23,10 @@ void usb_receive(uint8_t *buf, uint32_t *len) {
stop_byte = buf[4+ length];
}
log_debug("received command, id: 0x%x", 1, command_id);
log_debug("length: %n", 1, length);
log_debug("stop_byte: 0x%x", 1, stop_byte);
if (stop_byte != 0xFF) {
log_error("received command has invalid stop byte: 0x%x", 1, stop_byte);
} else {
handle_received_command(command_id, payload_ptr, length);
}
}

1
nucleo-wb55-dongle-ble/app/commands.h

@ -9,6 +9,7 @@
#define COMMANDS_H_
void pop_and_execute_commands();
void handle_received_command(uint8_t command_id, uint8_t * payload_ptr, uint16_t length);
void log_debug(const char * format, int nargs, ...);
void log_info(const char * format, int nargs, ...);

Loading…
Cancel
Save