| /* |
| * |
| * BlueZ - Bluetooth protocol stack for Linux |
| * |
| * Copyright (C) 2015,2016 Felipe F. Tonello <eu@felipetonello.com> |
| * Copyright (C) 2016 ROLI Ltd. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
| * |
| * Information about this plugin: |
| * |
| * This plugin implements the MIDI over Bluetooth Low-Energy (BLE-MIDI) 1.0 |
| * specification as published by MMA in November/2015. |
| * |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| #include <errno.h> |
| #include <alsa/asoundlib.h> |
| |
| #include "lib/bluetooth.h" |
| #include "lib/sdp.h" |
| #include "lib/uuid.h" |
| |
| #include "src/plugin.h" |
| #include "src/adapter.h" |
| #include "src/device.h" |
| #include "src/profile.h" |
| #include "src/service.h" |
| #include "src/shared/util.h" |
| #include "src/shared/att.h" |
| #include "src/shared/queue.h" |
| #include "src/shared/gatt-db.h" |
| #include "src/shared/gatt-client.h" |
| #include "src/shared/io.h" |
| #include "src/log.h" |
| #include "attrib/att.h" |
| |
| #include "libmidi.h" |
| |
| struct midi { |
| struct btd_device *dev; |
| struct gatt_db *db; |
| struct bt_gatt_client *client; |
| unsigned int io_cb_id; |
| struct io *io; |
| uint16_t midi_io_handle; |
| |
| /* ALSA handlers */ |
| snd_seq_t *seq_handle; |
| int seq_client_id; |
| int seq_port_id; |
| |
| /* MIDI parser*/ |
| struct midi_read_parser midi_in; |
| struct midi_write_parser midi_out; |
| }; |
| |
| static bool midi_write_cb(struct io *io, void *user_data) |
| { |
| struct midi *midi = user_data; |
| int err; |
| |
| void foreach_cb(const struct midi_write_parser *parser, void *user_data) { |
| struct midi *midi = user_data; |
| bt_gatt_client_write_without_response(midi->client, |
| midi->midi_io_handle, |
| false, |
| midi_write_data(parser), |
| midi_write_data_size(parser)); |
| }; |
| |
| do { |
| snd_seq_event_t *event = NULL; |
| |
| err = snd_seq_event_input(midi->seq_handle, &event); |
| |
| if (err < 0 || !event) |
| break; |
| |
| midi_read_ev(&midi->midi_out, event, foreach_cb, midi); |
| |
| } while (err > 0); |
| |
| if (midi_write_has_data(&midi->midi_out)) |
| bt_gatt_client_write_without_response(midi->client, |
| midi->midi_io_handle, |
| false, |
| midi_write_data(&midi->midi_out), |
| midi_write_data_size(&midi->midi_out)); |
| |
| midi_write_reset(&midi->midi_out); |
| |
| return true; |
| } |
| |
| static void midi_io_value_cb(uint16_t value_handle, const uint8_t *value, |
| uint16_t length, void *user_data) |
| { |
| struct midi *midi = user_data; |
| snd_seq_event_t ev; |
| unsigned int i = 0; |
| |
| if (length < 3) { |
| warn("MIDI I/O: Wrong packet format: length is %u bytes but it should " |
| "be at least 3 bytes", length); |
| return; |
| } |
| |
| snd_seq_ev_clear(&ev); |
| snd_seq_ev_set_source(&ev, midi->seq_port_id); |
| snd_seq_ev_set_subs(&ev); |
| snd_seq_ev_set_direct(&ev); |
| |
| midi_read_reset(&midi->midi_in); |
| |
| while (i < length) { |
| size_t count = midi_read_raw(&midi->midi_in, value + i, length - i, &ev); |
| |
| if (count == 0) |
| goto _err; |
| |
| if (ev.type != SND_SEQ_EVENT_NONE) |
| snd_seq_event_output_direct(midi->seq_handle, &ev); |
| |
| i += count; |
| } |
| |
| return; |
| |
| _err: |
| error("Wrong BLE-MIDI message"); |
| } |
| |
| static void midi_io_ccc_written_cb(uint16_t att_ecode, void *user_data) |
| { |
| if (att_ecode != 0) { |
| error("MIDI I/O: notifications not enabled %s", |
| att_ecode2str(att_ecode)); |
| return; |
| } |
| |
| DBG("MIDI I/O: notification enabled"); |
| } |
| |
| static void midi_io_initial_read_cb(bool success, uint8_t att_ecode, |
| const uint8_t *value, uint16_t length, |
| void *user_data) |
| { |
| struct midi *midi = user_data; |
| |
| if (!success) { |
| error("MIDI I/O: Failed to read initial request"); |
| return; |
| } |
| |
| /* request notify */ |
| midi->io_cb_id = |
| bt_gatt_client_register_notify(midi->client, |
| midi->midi_io_handle, |
| midi_io_ccc_written_cb, |
| midi_io_value_cb, |
| midi, |
| NULL); |
| } |
| |
| static void handle_midi_io(struct midi *midi, uint16_t value_handle) |
| { |
| DBG("MIDI I/O handle: 0x%04x", value_handle); |
| |
| midi->midi_io_handle = value_handle; |
| |
| /* |
| * The BLE-MIDI 1.0 spec specifies that The Central shall attempt to |
| * read the MIDI I/O characteristic of the Peripheral right after |
| * estrablhishing a connection with the accessory. |
| */ |
| if (!bt_gatt_client_read_value(midi->client, |
| value_handle, |
| midi_io_initial_read_cb, |
| midi, |
| NULL)) |
| DBG("MIDI I/O: Failed to send request to read initial value"); |
| } |
| |
| static void handle_characteristic(struct gatt_db_attribute *attr, |
| void *user_data) |
| { |
| struct midi *midi = user_data; |
| uint16_t value_handle; |
| bt_uuid_t uuid, midi_io_uuid; |
| |
| if (!gatt_db_attribute_get_char_data(attr, NULL, &value_handle, NULL, |
| NULL, &uuid)) { |
| error("Failed to obtain characteristic data"); |
| return; |
| } |
| |
| bt_string_to_uuid(&midi_io_uuid, MIDI_IO_UUID); |
| |
| if (bt_uuid_cmp(&midi_io_uuid, &uuid) == 0) |
| handle_midi_io(midi, value_handle); |
| else { |
| char uuid_str[MAX_LEN_UUID_STR]; |
| |
| bt_uuid_to_string(&uuid, uuid_str, sizeof(uuid_str)); |
| DBG("Unsupported characteristic: %s", uuid_str); |
| } |
| } |
| |
| static void foreach_midi_service(struct gatt_db_attribute *attr, |
| void *user_data) |
| { |
| struct midi *midi = user_data; |
| |
| gatt_db_service_foreach_char(attr, handle_characteristic, midi); |
| } |
| |
| static int midi_device_probe(struct btd_service *service) |
| { |
| struct btd_device *device = btd_service_get_device(service); |
| struct midi *midi; |
| char addr[18]; |
| |
| ba2str(device_get_address(device), addr); |
| DBG("MIDI GATT Driver profile probe (%s)", addr); |
| |
| /* Ignore, if we were probed for this device already */ |
| midi = btd_service_get_user_data(service); |
| if (midi) { |
| error("Profile probed twice for the same device!"); |
| return -EADDRINUSE; |
| } |
| |
| midi = g_new0(struct midi, 1); |
| if (!midi) |
| return -ENOMEM; |
| |
| midi->dev = btd_device_ref(device); |
| |
| btd_service_set_user_data(service, midi); |
| |
| return 0; |
| } |
| |
| static void midi_device_remove(struct btd_service *service) |
| { |
| struct btd_device *device = btd_service_get_device(service); |
| struct midi *midi; |
| char addr[18]; |
| |
| ba2str(device_get_address(device), addr); |
| DBG("MIDI GATT Driver profile remove (%s)", addr); |
| |
| midi = btd_service_get_user_data(service); |
| if (!midi) { |
| error("MIDI Service not handled by profile"); |
| return; |
| } |
| |
| btd_device_unref(midi->dev); |
| g_free(midi); |
| } |
| |
| static int midi_accept(struct btd_service *service) |
| { |
| struct btd_device *device = btd_service_get_device(service); |
| struct gatt_db *db = btd_device_get_gatt_db(device); |
| struct bt_gatt_client *client = btd_device_get_gatt_client(device); |
| bt_uuid_t midi_uuid; |
| struct pollfd pfd; |
| struct midi *midi; |
| char addr[18]; |
| char device_name[MAX_NAME_LENGTH + 11]; /* 11 = " Bluetooth\0"*/ |
| int err; |
| snd_seq_client_info_t *info; |
| |
| ba2str(device_get_address(device), addr); |
| DBG("MIDI GATT Driver profile accept (%s)", addr); |
| |
| midi = btd_service_get_user_data(service); |
| if (!midi) { |
| error("MIDI Service not handled by profile"); |
| return -ENODEV; |
| } |
| |
| /* Port Name */ |
| memset(device_name, 0, sizeof(device_name)); |
| if (device_name_known(device)) |
| device_get_name(device, device_name, sizeof(device_name)); |
| else |
| strncpy(device_name, addr, sizeof(addr)); |
| |
| /* ALSA Sequencer Client and Port Setup */ |
| err = snd_seq_open(&midi->seq_handle, "default", SND_SEQ_OPEN_DUPLEX, 0); |
| if (err < 0) { |
| error("Could not open ALSA Sequencer: %s (%d)", snd_strerror(err), err); |
| return err; |
| } |
| |
| err = snd_seq_nonblock(midi->seq_handle, SND_SEQ_NONBLOCK); |
| if (err < 0) { |
| error("Could not set nonblock mode: %s (%d)", snd_strerror(err), err); |
| goto _err_handle; |
| } |
| |
| err = snd_seq_set_client_name(midi->seq_handle, device_name); |
| if (err < 0) { |
| error("Could not configure ALSA client: %s (%d)", snd_strerror(err), err); |
| goto _err_handle; |
| } |
| |
| err = snd_seq_client_id(midi->seq_handle); |
| if (err < 0) { |
| error("Could retreive ALSA client: %s (%d)", snd_strerror(err), err); |
| goto _err_handle; |
| } |
| midi->seq_client_id = err; |
| |
| err = snd_seq_create_simple_port(midi->seq_handle, strcat(device_name, " Bluetooth"), |
| SND_SEQ_PORT_CAP_READ | |
| SND_SEQ_PORT_CAP_WRITE | |
| SND_SEQ_PORT_CAP_SUBS_READ | |
| SND_SEQ_PORT_CAP_SUBS_WRITE, |
| SND_SEQ_PORT_TYPE_MIDI_GENERIC | |
| SND_SEQ_PORT_TYPE_HARDWARE); |
| if (err < 0) { |
| error("Could not create ALSA port: %s (%d)", snd_strerror(err), err); |
| goto _err_handle; |
| } |
| midi->seq_port_id = err; |
| |
| snd_seq_client_info_alloca(&info); |
| err = snd_seq_get_client_info(midi->seq_handle, info); |
| if (err < 0) |
| goto _err_port; |
| |
| /* list of relevant sequencer events */ |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_NOTEOFF); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_NOTEON); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_KEYPRESS); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_CONTROLLER); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_PGMCHANGE); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_CHANPRESS); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_PITCHBEND); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_SYSEX); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_QFRAME); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_SONGPOS); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_SONGSEL); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_TUNE_REQUEST); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_CLOCK); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_START); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_CONTINUE); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_STOP); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_SENSING); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_RESET); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_CONTROL14); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_NONREGPARAM); |
| snd_seq_client_info_event_filter_add(info, SND_SEQ_EVENT_REGPARAM); |
| |
| err = snd_seq_set_client_info(midi->seq_handle, info); |
| if (err < 0) |
| goto _err_port; |
| |
| |
| /* Input file descriptors */ |
| snd_seq_poll_descriptors(midi->seq_handle, &pfd, 1, POLLIN); |
| |
| midi->io = io_new(pfd.fd); |
| if (!midi->io) { |
| error("Could not allocate I/O eventloop"); |
| goto _err_port; |
| } |
| |
| io_set_read_handler(midi->io, midi_write_cb, midi, NULL); |
| |
| midi->db = gatt_db_ref(db); |
| midi->client = bt_gatt_client_ref(client); |
| |
| err = midi_read_init(&midi->midi_in); |
| if (err < 0) { |
| error("Could not initialise MIDI input parser"); |
| goto _err_port; |
| } |
| |
| err = midi_write_init(&midi->midi_out, bt_gatt_client_get_mtu(midi->client) - 3); |
| if (err < 0) { |
| error("Could not initialise MIDI output parser"); |
| goto _err_midi; |
| } |
| |
| bt_string_to_uuid(&midi_uuid, MIDI_UUID); |
| gatt_db_foreach_service(db, &midi_uuid, foreach_midi_service, midi); |
| |
| btd_service_connecting_complete(service, 0); |
| |
| return 0; |
| |
| _err_midi: |
| midi_read_free(&midi->midi_in); |
| |
| _err_port: |
| snd_seq_delete_simple_port(midi->seq_handle, midi->seq_port_id); |
| |
| _err_handle: |
| snd_seq_close(midi->seq_handle); |
| midi->seq_handle = NULL; |
| |
| btd_service_connecting_complete(service, err); |
| |
| return err; |
| } |
| |
| static int midi_disconnect(struct btd_service *service) |
| { |
| struct btd_device *device = btd_service_get_device(service); |
| struct midi *midi; |
| char addr[18]; |
| |
| ba2str(device_get_address(device), addr); |
| DBG("MIDI GATT Driver profile disconnect (%s)", addr); |
| |
| midi = btd_service_get_user_data(service); |
| if (!midi) { |
| error("MIDI Service not handled by profile"); |
| return -ENODEV; |
| } |
| |
| midi_read_free(&midi->midi_in); |
| midi_write_free(&midi->midi_out); |
| io_destroy(midi->io); |
| snd_seq_delete_simple_port(midi->seq_handle, midi->seq_port_id); |
| midi->seq_port_id = 0; |
| snd_seq_close(midi->seq_handle); |
| midi->seq_handle = NULL; |
| |
| /* Clean-up any old client/db */ |
| bt_gatt_client_unregister_notify(midi->client, midi->io_cb_id); |
| bt_gatt_client_unref(midi->client); |
| gatt_db_unref(midi->db); |
| |
| btd_service_disconnecting_complete(service, 0); |
| |
| return 0; |
| } |
| |
| static struct btd_profile midi_profile = { |
| .name = "MIDI GATT Driver", |
| .remote_uuid = MIDI_UUID, |
| .priority = BTD_PROFILE_PRIORITY_HIGH, |
| .auto_connect = true, |
| |
| .device_probe = midi_device_probe, |
| .device_remove = midi_device_remove, |
| |
| .accept = midi_accept, |
| |
| .disconnect = midi_disconnect, |
| }; |
| |
| static int midi_init(void) |
| { |
| return btd_profile_register(&midi_profile); |
| } |
| |
| static void midi_exit(void) |
| { |
| btd_profile_unregister(&midi_profile); |
| } |
| |
| BLUETOOTH_PLUGIN_DEFINE(midi, VERSION, BLUETOOTH_PLUGIN_PRIORITY_HIGH, |
| midi_init, midi_exit); |