From 9dcef306fd7ba21326d98111332e9900e91a6b1e Mon Sep 17 00:00:00 2001 From: Robert Ancell Date: Wed, 18 Aug 2021 12:20:44 +1200 Subject: [PATCH] Implement Linux support https://github.com/PhilipsHue/flutter_reactive_ble/issues/246 --- example/linux/.gitignore | 1 + example/linux/CMakeLists.txt | 116 ++++++ example/linux/flutter/CMakeLists.txt | 87 +++++ .../flutter/generated_plugin_registrant.cc | 11 + .../flutter/generated_plugin_registrant.h | 15 + example/linux/flutter/generated_plugins.cmake | 15 + example/linux/main.cc | 6 + example/linux/my_application.cc | 104 ++++++ example/linux/my_application.h | 18 + example/pubspec.lock | 62 +++- example/pubspec.yaml | 2 + .../lib/src/reactive_ble.dart | 10 +- packages/flutter_reactive_ble/pubspec.lock | 54 ++- packages/flutter_reactive_ble/pubspec.yaml | 2 + packages/reactive_ble_linux/CHANGELOG.md | 4 + packages/reactive_ble_linux/LICENSE | 24 ++ packages/reactive_ble_linux/README.md | 3 + .../lib/reactive_ble_linux.dart | 3 + .../lib/src/reactive_ble_linux_platform.dart | 333 ++++++++++++++++++ packages/reactive_ble_linux/pubspec.yaml | 18 + .../test/reactive_ble_platform_test.dart | 5 + 21 files changed, 875 insertions(+), 18 deletions(-) create mode 100644 example/linux/.gitignore create mode 100644 example/linux/CMakeLists.txt create mode 100644 example/linux/flutter/CMakeLists.txt create mode 100644 example/linux/flutter/generated_plugin_registrant.cc create mode 100644 example/linux/flutter/generated_plugin_registrant.h create mode 100644 example/linux/flutter/generated_plugins.cmake create mode 100644 example/linux/main.cc create mode 100644 example/linux/my_application.cc create mode 100644 example/linux/my_application.h create mode 100644 packages/reactive_ble_linux/CHANGELOG.md create mode 100644 packages/reactive_ble_linux/LICENSE create mode 100644 packages/reactive_ble_linux/README.md create mode 100644 packages/reactive_ble_linux/lib/reactive_ble_linux.dart create mode 100644 packages/reactive_ble_linux/lib/src/reactive_ble_linux_platform.dart create mode 100644 packages/reactive_ble_linux/pubspec.yaml create mode 100644 packages/reactive_ble_linux/test/reactive_ble_platform_test.dart diff --git a/example/linux/.gitignore b/example/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/example/linux/CMakeLists.txt b/example/linux/CMakeLists.txt new file mode 100644 index 00000000..ffdc8524 --- /dev/null +++ b/example/linux/CMakeLists.txt @@ -0,0 +1,116 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "example") +set(APPLICATION_ID "com.signify.hue.example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/example/linux/flutter/CMakeLists.txt b/example/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..33fd5801 --- /dev/null +++ b/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,87 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..e71a16d2 --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..51436ae8 --- /dev/null +++ b/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/example/linux/main.cc b/example/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/example/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc new file mode 100644 index 00000000..0ba8f430 --- /dev/null +++ b/example/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/example/linux/my_application.h b/example/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/example/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/example/pubspec.lock b/example/pubspec.lock index 1ebcd3db..3c79089e 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -29,6 +29,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + bluez: + dependency: transitive + description: + name: bluez + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.4" boolean_selector: dependency: transitive description: @@ -42,7 +49,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" build_config: dependency: transitive description: @@ -56,7 +63,7 @@ packages: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" build_resolvers: dependency: transitive description: @@ -91,7 +98,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.1.2" + version: "8.1.3" characters: dependency: transitive description: @@ -119,7 +126,7 @@ packages: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.3.3" + version: "0.3.5" clock: dependency: transitive description: @@ -161,7 +168,14 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" + dbus: + dependency: transitive + description: + name: dbus + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" dependency_validator: dependency: "direct dev" description: @@ -176,6 +190,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" file: dependency: transitive description: @@ -241,7 +262,7 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" graphs: dependency: transitive description: @@ -290,7 +311,7 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.3.0" lints: dependency: transitive description: @@ -325,7 +346,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" nested: dependency: transitive description: @@ -354,13 +375,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.11.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" pool: dependency: transitive description: @@ -396,6 +424,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + reactive_ble_linux: + dependency: "direct overridden" + description: + path: "../packages/reactive_ble_linux" + relative: true + source: path + version: "5.0.1" reactive_ble_mobile: dependency: "direct overridden" description: @@ -512,7 +547,7 @@ packages: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" web_socket_channel: dependency: transitive description: @@ -520,6 +555,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.1" yaml: dependency: transitive description: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ccdde200..cea24580 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -27,6 +27,8 @@ dev_dependencies: dependency_overrides: flutter_reactive_ble: path: ../packages/flutter_reactive_ble + reactive_ble_linux: + path: ../packages/reactive_ble_linux reactive_ble_mobile: path: ../packages/reactive_ble_mobile diff --git a/packages/flutter_reactive_ble/lib/src/reactive_ble.dart b/packages/flutter_reactive_ble/lib/src/reactive_ble.dart index 053a882b..9272707b 100644 --- a/packages/flutter_reactive_ble/lib/src/reactive_ble.dart +++ b/packages/flutter_reactive_ble/lib/src/reactive_ble.dart @@ -8,6 +8,7 @@ import 'package:flutter_reactive_ble/src/device_scanner.dart'; import 'package:flutter_reactive_ble/src/discovered_devices_registry.dart'; import 'package:flutter_reactive_ble/src/rx_ext/repeater.dart'; import 'package:meta/meta.dart'; +import 'package:reactive_ble_linux/reactive_ble_linux.dart'; import 'package:reactive_ble_mobile/reactive_ble_mobile.dart'; import 'package:reactive_ble_platform_interface/reactive_ble_platform_interface.dart'; @@ -104,8 +105,13 @@ class FlutterReactiveBle { print, ); - ReactiveBlePlatform.instance = - const ReactiveBleMobilePlatformFactory().create(); + if (Platform.isLinux) { + ReactiveBlePlatform.instance = + const ReactiveBleLinuxPlatformFactory().create(); + } else { + ReactiveBlePlatform.instance = + const ReactiveBleMobilePlatformFactory().create(); + } _blePlatform = ReactiveBlePlatform.instance; diff --git a/packages/flutter_reactive_ble/pubspec.lock b/packages/flutter_reactive_ble/pubspec.lock index 2b46a9e1..a2459157 100644 --- a/packages/flutter_reactive_ble/pubspec.lock +++ b/packages/flutter_reactive_ble/pubspec.lock @@ -28,7 +28,14 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" + bluez: + dependency: transitive + description: + name: bluez + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.4" boolean_selector: dependency: transitive description: @@ -98,7 +105,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -162,6 +169,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + dbus: + dependency: transitive + description: + name: dbus + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" fake_async: dependency: transitive description: @@ -169,6 +183,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" file: dependency: transitive description: @@ -290,7 +311,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" meta: dependency: "direct main" description: @@ -333,6 +354,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.11.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.0" plugin_platform_interface: dependency: transitive description: @@ -368,6 +396,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + reactive_ble_linux: + dependency: "direct main" + description: + path: "../reactive_ble_linux" + relative: true + source: path + version: "4.0.0" reactive_ble_mobile: dependency: "direct main" description: @@ -456,7 +491,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.3" timing: dependency: transitive description: @@ -477,7 +512,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" watcher: dependency: transitive description: @@ -492,6 +527,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.0" yaml: dependency: transitive description: @@ -500,5 +542,5 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.14.0 <3.0.0" flutter: ">=2.0.0" diff --git a/packages/flutter_reactive_ble/pubspec.yaml b/packages/flutter_reactive_ble/pubspec.yaml index a68f7f2d..071493b7 100644 --- a/packages/flutter_reactive_ble/pubspec.yaml +++ b/packages/flutter_reactive_ble/pubspec.yaml @@ -21,8 +21,10 @@ dependencies: sdk: flutter functional_data: ^1.0.0 meta: ^1.3.0 + reactive_ble_linux: ^5.0.1 reactive_ble_mobile: ^5.0.1 reactive_ble_platform_interface: ^5.0.1 + dev_dependencies: build_runner: ^2.1.2 flutter_lints: ^1.0.4 diff --git a/packages/reactive_ble_linux/CHANGELOG.md b/packages/reactive_ble_linux/CHANGELOG.md new file mode 100644 index 00000000..fb6c0d5a --- /dev/null +++ b/packages/reactive_ble_linux/CHANGELOG.md @@ -0,0 +1,4 @@ +## 4.0.0 + +* Initial Open Source release. + diff --git a/packages/reactive_ble_linux/LICENSE b/packages/reactive_ble_linux/LICENSE new file mode 100644 index 00000000..1b0489a1 --- /dev/null +++ b/packages/reactive_ble_linux/LICENSE @@ -0,0 +1,24 @@ +BSD license + +©2019 Signify Holding. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/packages/reactive_ble_linux/README.md b/packages/reactive_ble_linux/README.md new file mode 100644 index 00000000..8d8988b2 --- /dev/null +++ b/packages/reactive_ble_linux/README.md @@ -0,0 +1,3 @@ +# Reactive_ble_linux + +Official Linux implementation for the flutter_reactive_ble plugin. diff --git a/packages/reactive_ble_linux/lib/reactive_ble_linux.dart b/packages/reactive_ble_linux/lib/reactive_ble_linux.dart new file mode 100644 index 00000000..42ed2e75 --- /dev/null +++ b/packages/reactive_ble_linux/lib/reactive_ble_linux.dart @@ -0,0 +1,3 @@ +library reactive_ble_linux; + +export 'src/reactive_ble_linux_platform.dart'; diff --git a/packages/reactive_ble_linux/lib/src/reactive_ble_linux_platform.dart b/packages/reactive_ble_linux/lib/src/reactive_ble_linux_platform.dart new file mode 100644 index 00000000..6156c8bf --- /dev/null +++ b/packages/reactive_ble_linux/lib/src/reactive_ble_linux_platform.dart @@ -0,0 +1,333 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:bluez/bluez.dart'; +import 'package:reactive_ble_platform_interface/reactive_ble_platform_interface.dart'; + +class ReactiveBleLinuxPlatform extends ReactiveBlePlatform { + final _client = BlueZClient(); + StreamSubscription? _adapterAddedSubscription; + StreamSubscription? _adapterRemovedSubscription; + StreamSubscription? _deviceAddedSubscription; + StreamSubscription? _deviceRemovedSubscription; + final _scanController = StreamController.broadcast(); + final _bleStatusController = StreamController.broadcast(); + final _connectionUpdateController = + StreamController.broadcast(); + final _charValueUpdateController = + StreamController.broadcast(); + BleStatus? _lastStatus; + + ReactiveBleLinuxPlatform() { + _adapterAddedSubscription = _client.adapterAdded.listen(_adapterAdded); + _adapterRemovedSubscription = + _client.adapterRemoved.listen(_adapterRemoved); + _deviceAddedSubscription = _client.deviceAdded.listen(_deviceAdded); + _deviceRemovedSubscription = _client.deviceRemoved.listen(_deviceRemoved); + + // Ensure a new listener always gets the current status. + _scanController.onListen = () { + _client.devices.forEach(_sendDeviceState); + }; + _bleStatusController.onListen = () { + _lastStatus = null; + _checkStatus(); + }; + _connectionUpdateController.onListen = () { + _client.devices.forEach(_checkDeviceConnectionState); + }; + } + + @override + Stream get scanStream => _scanController.stream; + + @override + Stream get bleStatusStream => _bleStatusController.stream; + + @override + Stream get connectionUpdateStream => + _connectionUpdateController.stream; + + @override + Stream get charValueUpdateStream => + _charValueUpdateController.stream; + + @override + Future initialize() async { + await _client.connect(); + } + + @override + Future deinitialize() async { + await _adapterAddedSubscription?.cancel(); + await _adapterRemovedSubscription?.cancel(); + await _deviceAddedSubscription?.cancel(); + await _deviceRemovedSubscription?.cancel(); + await _client.close(); + } + + @override + Stream scanForDevices({ + required List withServices, + required ScanMode scanMode, + required bool requireLocationServicesEnabled, + }) { + final controller = StreamController() + ..onCancel = () { + for (final adapter in _client.adapters) { + adapter.stopDiscovery(); + } + }; + + for (final adapter in _client.adapters) { + adapter + ..setDiscoveryFilter( + uuids: withServices.map((uuid) => uuid.toString()).toList()) + ..startDiscovery(); + } + + // Indicate have received request. + controller + ..add(null) + ..close(); + + return controller.stream; + } + + @override + Stream connectToDevice( + String id, + Map>? servicesWithCharacteristicsToDiscover, + Duration? connectionTimeout, + ) { + final controller = StreamController(); + + controller.onListen = () { + final device = _getDeviceWithId(id); + if (device == null) { + controller.addError('No such device $id'); + } else { + device.connect(); + controller + ..add(null) + ..close(); + } + }; + + return controller.stream; + } + + @override + Future disconnectDevice(String deviceId) async { + final device = _getDeviceWithId(deviceId); + if (device == null) { + throw Exception('No such device $deviceId'); + } + await device.disconnect(); + } + + @override + Stream readCharacteristic(QualifiedCharacteristic characteristic) { + final controller = StreamController(); + + controller.onListen = () { + final c = _getCharacteristic(characteristic); + if (c == null) { + controller.addError('No such characteristic'); + } else { + c.readValue().then((value) { + _charValueUpdateController.add(CharacteristicValue( + characteristic: characteristic, result: Result.success(value))); + }).catchError((Object error) { + _charValueUpdateController.add(CharacteristicValue( + characteristic: characteristic, + result: Result.failure(GenericFailure( + code: CharacteristicValueUpdateError.unknown, + message: error.toString())))); + }); + + // Indicate have received request. + controller + ..add(null) + ..close(); + } + }; + + return controller.stream; + } + + @override + Future writeCharacteristicWithResponse( + QualifiedCharacteristic characteristic, + List value, + ) async { + final c = _getCharacteristic(characteristic); + if (c == null) { + throw Exception('No such characteristic'); + } + + await c.writeValue(value); + + return WriteCharacteristicInfo( + characteristic: characteristic, result: const Result.success(Unit())); + } + + @override + Future writeCharacteristicWithoutResponse( + QualifiedCharacteristic characteristic, + List value, + ) async { + final c = _getCharacteristic(characteristic); + if (c == null) { + throw Exception('No such characteristic'); + } + + await c.writeValue(value); + + return WriteCharacteristicInfo( + characteristic: characteristic, result: const Result.success(Unit())); + } + + @override + Future> discoverServices(String deviceId) async { + final device = _getDeviceWithId(deviceId); + if (device == null) { + throw Exception('No such device $deviceId'); + } + final services = []; + for (final service in device.gattServices) { + services.add(DiscoveredService( + serviceId: Uuid(service.uuid.value), + characteristicIds: + service.characteristics.map((c) => Uuid(c.uuid.value)).toList(), + characteristics: service.characteristics + .map((c) => DiscoveredCharacteristic( + characteristicId: Uuid(c.uuid.value), + serviceId: Uuid(service.uuid.value), + isReadable: + c.flags.contains(BlueZGattCharacteristicFlag.read), + isWritableWithResponse: + c.flags.contains(BlueZGattCharacteristicFlag.write), + isWritableWithoutResponse: c.flags.contains( + BlueZGattCharacteristicFlag.writeWithoutResponse), + isNotifiable: + c.flags.contains(BlueZGattCharacteristicFlag.notify), + isIndicatable: + c.flags.contains(BlueZGattCharacteristicFlag.indicate))) + .toList())); + } + return services; + } + + BlueZDevice? _getDeviceWithId(String id) { + for (final device in _client.devices) { + if (device.address == id) { + return device; + } + } + return null; + } + + BlueZGattCharacteristic? _getCharacteristic( + QualifiedCharacteristic characteristic) { + final device = _getDeviceWithId(characteristic.deviceId); + if (device == null) { + return null; + } + for (final service in device.gattServices) { + if (Uuid(service.uuid.value) != characteristic.serviceId) { + continue; + } + for (final c in service.characteristics) { + if (Uuid(c.uuid.value) == characteristic.characteristicId) { + return c; + } + } + } + + return null; + } + + void _adapterAdded(BlueZAdapter adapter) { + adapter.propertiesChanged + .listen((List properties) => _checkStatus()); + _checkStatus(); + } + + void _adapterRemoved(BlueZAdapter adapter) { + _checkStatus(); + } + + void _deviceAdded(BlueZDevice device) { + device.propertiesChanged + .listen((properties) => _deviceChanged(device, properties)); + _deviceChanged(device, []); + } + + void _deviceChanged(BlueZDevice device, List properties) { + _checkDeviceConnectionState(device); + _sendDeviceState(device); + } + + void _sendDeviceState(BlueZDevice device) { + /// id: address -> path? + final result = DiscoveredDevice( + id: device.address, + name: device.alias != '' ? device.alias : device.name, + serviceData: const {}, + manufacturerData: Uint8List(0), + rssi: device.rssi, + serviceUuids: device.uuids.map((uuid) => Uuid(uuid.value)).toList()); + _scanController.add(ScanResult(result: Result.success(result))); + } + + void _deviceRemoved(BlueZDevice device) {} + + // Check the status and update stream if it has changed. + void _checkStatus() { + final status = _getStatus(); + if (status != _lastStatus) { + _lastStatus = status; + _bleStatusController.add(status); + } + } + + // Get the current status. + BleStatus _getStatus() { + if (_client.adapters.isEmpty) { + return BleStatus.unsupported; + } + + for (final adapter in _client.adapters) { + if (adapter.powered) { + return BleStatus.ready; + } + } + + return BleStatus.poweredOff; + } + + void _checkDeviceConnectionState(BlueZDevice device) { + // FIXME: Only if changes? + _connectionUpdateController.add(ConnectionStateUpdate( + deviceId: device.address, + connectionState: _getDeviceConnectionState(device), + failure: null)); + } + + DeviceConnectionState _getDeviceConnectionState(BlueZDevice device) { + if (device.connected) { + // FIXME: May be disconnecting + return DeviceConnectionState.connected; + } else { + // FIXME: May be connecting + return DeviceConnectionState.disconnected; + } + } +} + +class ReactiveBleLinuxPlatformFactory { + const ReactiveBleLinuxPlatformFactory(); + + ReactiveBleLinuxPlatform create() => ReactiveBleLinuxPlatform(); +} diff --git a/packages/reactive_ble_linux/pubspec.yaml b/packages/reactive_ble_linux/pubspec.yaml new file mode 100644 index 00000000..f186986d --- /dev/null +++ b/packages/reactive_ble_linux/pubspec.yaml @@ -0,0 +1,18 @@ +name: reactive_ble_linux +description: Official Linux implementation for the flutter_reactive_ble plugin. +version: 5.0.1 +homepage: https://github.com/PhilipsHue/flutter_reactive_ble + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +dependencies: + bluez: ^0.7.3 + flutter: + sdk: flutter + reactive_ble_platform_interface: ^5.0.1 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/reactive_ble_linux/test/reactive_ble_platform_test.dart b/packages/reactive_ble_linux/test/reactive_ble_platform_test.dart new file mode 100644 index 00000000..1f508431 --- /dev/null +++ b/packages/reactive_ble_linux/test/reactive_ble_platform_test.dart @@ -0,0 +1,5 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); +}