Init. "BLE+SPP MIDI controller with cs-midi, encoders, and dual-transport routing"

This commit is contained in:
pszsh 2026-03-03 18:18:10 -08:00
parent 6e78d816b5
commit 1ae9b58eba
12 changed files with 578 additions and 29 deletions

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "lib/Control-Surface"] [submodule "lib/Control-Surface"]
path = lib/Control-Surface path = lib/Control-Surface
url = https://github.com/tttapa/Control-Surface.git url = https://github.com/tttapa/Control-Surface.git
[submodule "lib/cs-midi"]
path = lib/cs-midi
url = https://git.else-if.org/jess/cs-midi.git

View File

@ -15,18 +15,30 @@ add_compile_definitions(PICO_RP2350A=0)
pico_sdk_init() pico_sdk_init()
add_subdirectory(lib/cs-midi)
# cs_midi's BTstack sources need project-level btstack_config.h and lwipopts.h
target_include_directories(cs_midi PRIVATE ${CMAKE_CURRENT_LIST_DIR})
add_executable(fractional_looper add_executable(fractional_looper
main.c main.cpp
src/encoder.cpp
src/spp_midi.cpp
) )
target_include_directories(fractional_looper PRIVATE ${CMAKE_CURRENT_LIST_DIR}) target_include_directories(fractional_looper PRIVATE
${CMAKE_CURRENT_LIST_DIR}
${CMAKE_CURRENT_LIST_DIR}/src
)
target_link_libraries(fractional_looper target_link_libraries(fractional_looper
pico_stdlib pico_stdlib
pico_cyw43_arch_lwip_threadsafe_background pico_cyw43_arch_lwip_threadsafe_background
pico_btstack_ble pico_btstack_ble
pico_btstack_classic
pico_btstack_cyw43 pico_btstack_cyw43
hardware_adc hardware_adc
cs_midi
) )
pico_enable_stdio_usb(fractional_looper 1) pico_enable_stdio_usb(fractional_looper 1)

View File

@ -9,7 +9,16 @@ $(BUILD_DIR)/Makefile: CMakeLists.txt
@cd $(BUILD_DIR) && PICO_SDK_PATH=$$HOME/Staging/pico-sdk cmake .. @cd $(BUILD_DIR) && PICO_SDK_PATH=$$HOME/Staging/pico-sdk cmake ..
clean: clean:
@rm -f $(BUILD_DIR)/CMakeFiles/$(TARGET).dir/main.cpp.o
@rm -f $(BUILD_DIR)/CMakeFiles/$(TARGET).dir/src/*.o
@rm -f $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).uf2
@rm -f $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin
@rm -f $(BUILD_DIR)/$(TARGET).dis $(BUILD_DIR)/$(TARGET).map
@echo "Project objects cleaned (libraries + SDK preserved)"
distclean:
@rm -rf $(BUILD_DIR) @rm -rf $(BUILD_DIR)
@echo "Full clean (rebuild everything with make)"
flash: all flash: all
@echo "Waiting for RP2350 volume..." @echo "Waiting for RP2350 volume..."
@ -18,4 +27,4 @@ flash: all
@cp $(BUILD_DIR)/$(TARGET).uf2 /Volumes/RP2350/ @cp $(BUILD_DIR)/$(TARGET).uf2 /Volumes/RP2350/
@echo "Done." @echo "Done."
.PHONY: all clean flash .PHONY: all clean distclean flash

View File

@ -1,9 +1,16 @@
#ifndef _BTSTACK_CONFIG_H #ifndef _BTSTACK_CONFIG_H
#define _BTSTACK_CONFIG_H #define _BTSTACK_CONFIG_H
// BLE
#define ENABLE_LE_PERIPHERAL #define ENABLE_LE_PERIPHERAL
#define ENABLE_LE_CENTRAL #define ENABLE_LE_CENTRAL
#define ENABLE_L2CAP_LE_CREDIT_BASED_FLOW_CONTROL_MODE #define ENABLE_L2CAP_LE_CREDIT_BASED_FLOW_CONTROL_MODE
// Classic BT
#ifndef ENABLE_CLASSIC
#define ENABLE_CLASSIC
#endif
#define ENABLE_LOG_INFO
#define ENABLE_PRINTF_HEXDUMP #define ENABLE_PRINTF_HEXDUMP
#define HCI_OUTGOING_PRE_BUFFER_SIZE 4 #define HCI_OUTGOING_PRE_BUFFER_SIZE 4
@ -12,9 +19,9 @@
#define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2 #define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2
#define MAX_NR_GATT_CLIENTS 1 #define MAX_NR_GATT_CLIENTS 1
#define MAX_NR_HCI_CONNECTIONS 2 #define MAX_NR_HCI_CONNECTIONS 4
#define MAX_NR_L2CAP_CHANNELS 4 #define MAX_NR_L2CAP_CHANNELS 6
#define MAX_NR_L2CAP_SERVICES 3 #define MAX_NR_L2CAP_SERVICES 4
#define MAX_NR_SM_LOOKUP_ENTRIES 3 #define MAX_NR_SM_LOOKUP_ENTRIES 3
#define MAX_NR_WHITELIST_ENTRIES 1 #define MAX_NR_WHITELIST_ENTRIES 1
#define MAX_NR_LE_DEVICE_DB_ENTRIES 4 #define MAX_NR_LE_DEVICE_DB_ENTRIES 4
@ -28,6 +35,14 @@
#define HCI_HOST_SCO_PACKET_LEN 120 #define HCI_HOST_SCO_PACKET_LEN 120
#define HCI_HOST_SCO_PACKET_NUM 3 #define HCI_HOST_SCO_PACKET_NUM 3
// RFCOMM for SPP
#define MAX_NR_RFCOMM_MULTIPLEXERS 1
#define MAX_NR_RFCOMM_SERVICES 1
#define MAX_NR_RFCOMM_CHANNELS 1
// SDP
#define MAX_NR_SERVICE_RECORD_ITEMS 2
#define NVM_NUM_DEVICE_DB_ENTRIES 16 #define NVM_NUM_DEVICE_DB_ENTRIES 16
#define NVM_NUM_LINK_KEYS 16 #define NVM_NUM_LINK_KEYS 16

1
lib/cs-midi Submodule

@ -0,0 +1 @@
Subproject commit e215f7686c92a5588907164ac2c7e61258abc039

23
main.c
View File

@ -1,23 +0,0 @@
#include <stdio.h>
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
int main(void) {
stdio_init_all();
if (cyw43_arch_init()) {
printf("CYW43 init failed\n");
return 1;
}
printf("FractionalLooper: CYW43 initialized\n");
while (1) {
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 1);
sleep_ms(500);
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, 0);
sleep_ms(500);
}
return 0;
}

117
main.cpp Normal file
View File

@ -0,0 +1,117 @@
#include <cstdio>
#include "pico/stdlib.h"
#include "pico/cyw43_arch.h"
#include <cs_midi.h>
#include "encoder.h"
#include "spp_midi.h"
// Two CC sets, toggled by SW1 button.
// Set A: encoders CC 16-19, buttons CC 20-22 (SW2-SW4)
// Set B: encoders CC 24-27, buttons CC 28-30 (SW2-SW4)
static constexpr uint8_t SET_A_ENC_CC = 16;
static constexpr uint8_t SET_A_BTN_CC = 20;
static constexpr uint8_t SET_B_ENC_CC = 24;
static constexpr uint8_t SET_B_BTN_CC = 28;
static constexpr uint8_t TOGGLE_CC = 23;
static uint8_t enc_cc_base = SET_A_ENC_CC;
static uint8_t btn_cc_base = SET_A_BTN_CC;
static bool set_b_active = false;
// LED state
enum LedState { LED_IDLE_BLINK, LED_CONNECT_FLASH, LED_OFF };
static LedState led_state = LED_IDLE_BLINK;
static uint16_t led_counter = 0;
static uint8_t led_flash_count = 0;
static bool was_connected = false;
static void led_set(bool on) {
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, on);
}
int main() {
stdio_init_all();
if (cyw43_arch_init()) {
printf("CYW43 init failed\n");
return 1;
}
printf("FractionalLooper: CYW43 initialized\n");
cs::BluetoothMIDI_Interface ble;
ble.begin();
printf("BLE MIDI started\n");
SPPStreamMIDI_Interface spp;
spp.begin();
cs::BidirectionalMIDI_PipeFactory<2> pipes;
ble | pipes | spp;
encoders_init();
printf("Encoders initialized\n");
ble.setAsDefault();
while (true) {
bool connected = ble.isConnected() || spp.isConnected();
if (connected && !was_connected) {
led_state = LED_CONNECT_FLASH;
led_flash_count = 0;
led_counter = 0;
} else if (!connected && was_connected) {
led_state = LED_IDLE_BLINK;
led_counter = 0;
}
was_connected = connected;
switch (led_state) {
case LED_IDLE_BLINK:
led_set(led_counter < 500);
if (++led_counter >= 1000) led_counter = 0;
break;
case LED_CONNECT_FLASH:
led_set(led_counter < 50);
if (++led_counter >= 100) {
led_counter = 0;
if (++led_flash_count >= 6) {
led_state = LED_OFF;
led_set(false);
}
}
break;
case LED_OFF:
break;
}
for (int i = 0; i < NUM_ENCODERS; i++) {
int32_t delta = encoder_get_delta(i);
if (delta != 0) {
int32_t clamped = delta;
if (clamped > 63) clamped = 63;
if (clamped < -63) clamped = -63;
uint8_t val = (clamped > 0) ? (uint8_t)clamped
: (uint8_t)(128 + clamped);
ble.sendControlChange({enc_cc_base + i, cs::Channel_1}, val);
}
if (button_pressed(i)) {
if (i == 0) {
set_b_active = !set_b_active;
enc_cc_base = set_b_active ? SET_B_ENC_CC : SET_A_ENC_CC;
btn_cc_base = set_b_active ? SET_B_BTN_CC : SET_A_BTN_CC;
ble.sendControlChange({TOGGLE_CC, cs::Channel_1},
set_b_active ? 127 : 0);
} else {
ble.sendControlChange({btn_cc_base + (i - 1), cs::Channel_1}, 127);
}
}
}
ble.update();
spp.update();
sleep_ms(1);
}
}

140
src/encoder.cpp Normal file
View File

@ -0,0 +1,140 @@
#include "encoder.h"
#include "hardware/gpio.h"
#include "hardware/structs/sio.h"
#include "hardware/timer.h"
#include "pico/stdlib.h"
#include <atomic>
// Interrupt-driven quadrature decoder using the same state machine
// as Control Surface's AHEncoder. Fires on every edge of both A and B
// pins, reads both pins directly from SIO register, and updates
// position atomically.
//
// new_B new_A old_B old_A Result
// 0 0 0 1 +1
// 0 0 1 0 -1
// 0 0 1 1 +2
// 0 1 0 0 -1
// 0 1 1 0 -2
// 0 1 1 1 +1
// 1 0 0 0 +1
// 1 0 0 1 -2
// 1 0 1 1 -1
// 1 1 0 0 +2
// 1 1 0 1 -1
// 1 1 1 0 +1
static std::atomic<int32_t> enc_position[NUM_ENCODERS];
static uint8_t enc_state[NUM_ENCODERS];
static inline void encoder_update(int i) {
uint32_t gpio_all = sio_hw->gpio_in;
uint8_t s = enc_state[i] & 0b11;
if (gpio_all & (1u << ENC_PIN_A[i])) s |= 4;
if (gpio_all & (1u << ENC_PIN_B[i])) s |= 8;
enc_state[i] = (s >> 2);
switch (s) {
case 1: case 7: case 8: case 14:
enc_position[i].fetch_add(1, std::memory_order_relaxed); return;
case 2: case 4: case 11: case 13:
enc_position[i].fetch_add(-1, std::memory_order_relaxed); return;
case 3: case 12:
enc_position[i].fetch_add(2, std::memory_order_relaxed); return;
case 6: case 9:
enc_position[i].fetch_add(-2, std::memory_order_relaxed); return;
}
}
// Button state
static volatile bool btn_flags[NUM_ENCODERS];
static volatile uint32_t btn_debounce_time[NUM_ENCODERS];
#define DEBOUNCE_MS 25
// Single callback for all encoder edges and button presses
static void gpio_irq_callback(uint gpio, uint32_t events) {
for (int i = 0; i < NUM_ENCODERS; i++) {
if (gpio == ENC_PIN_A[i] || gpio == ENC_PIN_B[i]) {
encoder_update(i);
return;
}
}
if (events & GPIO_IRQ_EDGE_FALL) {
uint32_t now = to_ms_since_boot(get_absolute_time());
for (int i = 0; i < NUM_ENCODERS; i++) {
if (gpio == BTN_PINS[i]) {
if (now - btn_debounce_time[i] > DEBOUNCE_MS) {
btn_debounce_time[i] = now;
btn_flags[i] = true;
}
return;
}
}
}
}
void encoders_init() {
for (int i = 0; i < NUM_ENCODERS; i++) {
enc_position[i].store(0, std::memory_order_relaxed);
enc_state[i] = 0;
btn_flags[i] = false;
btn_debounce_time[i] = 0;
}
// Set up encoder pins with pull-ups and edge interrupts
for (int i = 0; i < NUM_ENCODERS; i++) {
gpio_init(ENC_PIN_A[i]);
gpio_set_dir(ENC_PIN_A[i], GPIO_IN);
gpio_pull_up(ENC_PIN_A[i]);
gpio_init(ENC_PIN_B[i]);
gpio_set_dir(ENC_PIN_B[i], GPIO_IN);
gpio_pull_up(ENC_PIN_B[i]);
}
// Set up button pins
for (int i = 0; i < NUM_ENCODERS; i++) {
gpio_init(BTN_PINS[i]);
gpio_set_dir(BTN_PINS[i], GPIO_IN);
gpio_pull_up(BTN_PINS[i]);
}
// Allow RC filters to settle
sleep_ms(2);
// Read initial encoder states
uint32_t gpio_all = sio_hw->gpio_in;
for (int i = 0; i < NUM_ENCODERS; i++) {
uint8_t s = 0;
if (gpio_all & (1u << ENC_PIN_A[i])) s |= 1;
if (gpio_all & (1u << ENC_PIN_B[i])) s |= 2;
enc_state[i] = s;
}
// Register single callback, enable IRQs on all encoder and button pins
gpio_set_irq_enabled_with_callback(ENC_PIN_A[0],
GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, gpio_irq_callback);
for (int i = 0; i < NUM_ENCODERS; i++) {
if (i > 0)
gpio_set_irq_enabled(ENC_PIN_A[i],
GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true);
gpio_set_irq_enabled(ENC_PIN_B[i],
GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true);
gpio_set_irq_enabled(BTN_PINS[i],
GPIO_IRQ_EDGE_FALL, true);
}
}
int32_t encoder_get_delta(int index) {
return enc_position[index].exchange(0, std::memory_order_relaxed);
}
bool button_pressed(int index) {
if (btn_flags[index]) {
btn_flags[index] = false;
return true;
}
return false;
}

19
src/encoder.h Normal file
View File

@ -0,0 +1,19 @@
#pragma once
#include <cstdint>
#define NUM_ENCODERS 4
// A/B swapped on encoders 0-2 to correct rotation direction.
// Encoder 0 rewired to GPIO 17/18. Encoder 3 left as-is (already correct).
static constexpr uint8_t ENC_PIN_A[NUM_ENCODERS] = {17, 5, 8, 9};
static constexpr uint8_t ENC_PIN_B[NUM_ENCODERS] = {18, 3, 6, 11};
static constexpr uint8_t BTN_PINS[NUM_ENCODERS] = {1, 4, 7, 10};
void encoders_init();
// Returns accumulated delta since last call. Positive = clockwise.
int32_t encoder_get_delta(int index);
// Returns true if button was pressed since last call (edge-triggered).
bool button_pressed(int index);

47
src/encoder.pio Normal file
View File

@ -0,0 +1,47 @@
.pio_version 0
; Quadrature encoder decoder for non-adjacent A/B pins.
;
; Pin layout per encoder: A = in_base+0, (switch = in_base+1), B = in_base+2.
; On each B transition, samples A and B into the ISR and pushes to RX FIFO.
; Software decodes direction from the state table.
;
; Set clock divider for ~10 kHz to filter mechanical bounce.
.program encoder
.wrap_target
wait 1 pin 2 ; wait for B (base+2) to go high
in pins, 3 ; sample A, switch, B (3 bits from base)
push noblock
wait 0 pin 2 ; wait for B to go low
in pins, 3 ; sample again
push noblock
.wrap
% c-sdk {
#include "hardware/clocks.h"
#include "hardware/gpio.h"
static inline void encoder_program_init(PIO pio, uint sm, uint offset,
uint pin_a) {
// Pin layout: A = pin_a, switch = pin_a+1, B = pin_a+2
uint pin_sw = pin_a + 1;
uint pin_b = pin_a + 2;
pio_gpio_init(pio, pin_a);
pio_gpio_init(pio, pin_b);
gpio_pull_up(pin_a);
gpio_pull_up(pin_b);
pio_sm_set_consecutive_pindirs(pio, sm, pin_a, 3, false);
pio_sm_config c = encoder_program_get_default_config(offset);
sm_config_set_in_pins(&c, pin_a);
sm_config_set_in_shift(&c, false, false, 32);
float div = (float)clock_get_hz(clk_sys) / 10000.0f;
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
pio_sm_set_enabled(pio, sm, true);
}
%}

160
src/spp_midi.cpp Normal file
View File

@ -0,0 +1,160 @@
#include "spp_midi.h"
#include <cstdio>
#include <cstring>
#include "btstack.h"
#define RFCOMM_SERVER_CHANNEL 1
static SPPStreamMIDI_Interface *spp_instance = nullptr;
static uint16_t rfcomm_cid = 0;
static uint16_t rfcomm_mtu = 0;
static uint8_t spp_service_buffer[150];
static btstack_packet_callback_registration_t hci_event_cb_reg;
// Ring buffer storage
volatile uint8_t SPPStreamMIDI_Interface::rx_buf[RX_BUF_SIZE];
volatile uint16_t SPPStreamMIDI_Interface::rx_head = 0;
volatile uint16_t SPPStreamMIDI_Interface::rx_tail = 0;
SPPStreamMIDI_Interface *SPPStreamMIDI_Interface::instance() {
return spp_instance;
}
void SPPStreamMIDI_Interface::push_rx_data(const uint8_t *data, uint16_t len) {
for (uint16_t i = 0; i < len; i++) {
uint16_t next = (rx_head + 1) % RX_BUF_SIZE;
if (next == rx_tail) break; // overflow — drop
rx_buf[rx_head] = data[i];
rx_head = next;
}
}
bool SPPStreamMIDI_Interface::RxPuller::pull(uint8_t &byte) {
if (rx_head == rx_tail) return false;
byte = rx_buf[rx_tail];
rx_tail = (rx_tail + 1) % RX_BUF_SIZE;
return true;
}
// BTstack packet handler for RFCOMM events
void spp_packet_handler(uint8_t packet_type, uint16_t channel,
uint8_t *packet, uint16_t size) {
(void)channel;
bd_addr_t event_addr;
switch (packet_type) {
case HCI_EVENT_PACKET:
switch (hci_event_packet_get_type(packet)) {
case HCI_EVENT_PIN_CODE_REQUEST:
hci_event_pin_code_request_get_bd_addr(packet, event_addr);
gap_pin_code_response(event_addr, "0000");
break;
case HCI_EVENT_USER_CONFIRMATION_REQUEST:
hci_event_user_confirmation_request_get_bd_addr(packet, event_addr);
gap_ssp_confirmation_response(event_addr);
break;
case RFCOMM_EVENT_INCOMING_CONNECTION:
rfcomm_cid = rfcomm_event_incoming_connection_get_rfcomm_cid(packet);
rfcomm_accept_connection(rfcomm_cid);
break;
case RFCOMM_EVENT_CHANNEL_OPENED:
if (rfcomm_event_channel_opened_get_status(packet)) {
printf("SPP: channel open failed 0x%02x\n",
rfcomm_event_channel_opened_get_status(packet));
rfcomm_cid = 0;
} else {
rfcomm_cid = rfcomm_event_channel_opened_get_rfcomm_cid(packet);
rfcomm_mtu = rfcomm_event_channel_opened_get_max_frame_size(packet);
if (spp_instance) spp_instance->connected = true;
printf("SPP: connected, cid 0x%02x mtu %u\n", rfcomm_cid, rfcomm_mtu);
}
break;
case RFCOMM_EVENT_CHANNEL_CLOSED:
if (spp_instance) spp_instance->connected = false;
printf("SPP: disconnected\n");
rfcomm_cid = 0;
gap_discoverable_control(1);
gap_connectable_control(1);
break;
default:
break;
}
break;
case RFCOMM_DATA_PACKET:
SPPStreamMIDI_Interface::push_rx_data(packet, size);
break;
default:
break;
}
}
void SPPStreamMIDI_Interface::begin() {
spp_instance = this;
// Classic BT init
l2cap_init();
rfcomm_init();
rfcomm_register_service(spp_packet_handler, RFCOMM_SERVER_CHANNEL, 0xffff);
sdp_init();
memset(spp_service_buffer, 0, sizeof(spp_service_buffer));
spp_create_sdp_record(spp_service_buffer,
sdp_create_service_record_handle(),
RFCOMM_SERVER_CHANNEL,
"FractionalLooper MIDI");
sdp_register_service(spp_service_buffer);
hci_event_cb_reg.callback = &spp_packet_handler;
hci_add_event_handler(&hci_event_cb_reg);
gap_set_local_name("FractionalLooper 00:00:00:00:00:00");
gap_ssp_set_io_capability(SSP_IO_CAPABILITY_DISPLAY_YES_NO);
gap_discoverable_control(1);
gap_connectable_control(1);
printf("SPP MIDI: initialized\n");
}
cs::MIDIReadEvent SPPStreamMIDI_Interface::read() {
RxPuller puller;
return parser.pull(puller);
}
void SPPStreamMIDI_Interface::send_bytes(const uint8_t *data, size_t len) {
if (rfcomm_cid == 0 || len == 0) return;
rfcomm_send(rfcomm_cid, (uint8_t *)data, (uint16_t)len);
}
void SPPStreamMIDI_Interface::sendChannelMessageImpl(cs::ChannelMessage msg) {
uint8_t buf[3] = {msg.header, msg.data1, msg.data2};
size_t len = msg.hasTwoDataBytes() ? 3 : 2;
send_bytes(buf, len);
}
void SPPStreamMIDI_Interface::sendSysCommonImpl(cs::SysCommonMessage msg) {
uint8_t buf[3] = {msg.header, msg.data1, msg.data2};
size_t len = msg.getNumberOfDataBytes() + 1;
send_bytes(buf, len);
}
void SPPStreamMIDI_Interface::sendSysExImpl(cs::SysExMessage msg) {
send_bytes(msg.data, msg.length);
}
void SPPStreamMIDI_Interface::sendRealTimeImpl(cs::RealTimeMessage msg) {
uint8_t buf[1] = {msg.message};
send_bytes(buf, 1);
}
void SPPStreamMIDI_Interface::sendNowImpl() {
// RFCOMM sends immediately, nothing to flush
}

49
src/spp_midi.h Normal file
View File

@ -0,0 +1,49 @@
#pragma once
#include <cs_midi.h>
#include <MIDI_Parsers/SerialMIDI_Parser.hpp>
// Classic Bluetooth SPP MIDI interface.
// Carries raw MIDI bytes over RFCOMM serial port.
class SPPStreamMIDI_Interface : public cs::MIDI_Interface {
public:
SPPStreamMIDI_Interface() = default;
void begin() override;
void update() override { cs::MIDI_Interface::updateIncoming(this); }
void handleStall() override { cs::MIDI_Interface::handleStall(this); }
cs::MIDIReadEvent read();
cs::ChannelMessage getChannelMessage() const { return parser.getChannelMessage(); }
cs::SysCommonMessage getSysCommonMessage() const { return parser.getSysCommonMessage(); }
cs::RealTimeMessage getRealTimeMessage() const { return parser.getRealTimeMessage(); }
cs::SysExMessage getSysExMessage() const { return parser.getSysExMessage(); }
static void push_rx_data(const uint8_t *data, uint16_t len);
static SPPStreamMIDI_Interface *instance();
bool isConnected() const { return connected; }
protected:
void sendChannelMessageImpl(cs::ChannelMessage msg) override;
void sendSysCommonImpl(cs::SysCommonMessage msg) override;
void sendSysExImpl(cs::SysExMessage msg) override;
void sendRealTimeImpl(cs::RealTimeMessage msg) override;
void sendNowImpl() override;
private:
cs::SerialMIDI_Parser parser;
struct RxPuller {
bool pull(uint8_t &byte);
};
static constexpr size_t RX_BUF_SIZE = 256;
static volatile uint8_t rx_buf[RX_BUF_SIZE];
static volatile uint16_t rx_head;
static volatile uint16_t rx_tail;
bool connected = false;
void send_bytes(const uint8_t *data, size_t len);
friend void spp_packet_handler(uint8_t, uint16_t, uint8_t *, uint16_t);
};