diff options
136 files changed, 15958 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..f9a6b2c --- /dev/null +++ b/.clang-format @@ -0,0 +1,167 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -2 +AlignAfterOpenBracket: DontAlign +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: InlineOnly +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: NonAssignment +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 100 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 6 +ContinuationIndentWidth: 6 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: true +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<ext/.*\.h>' + Priority: 2 + SortPriority: 0 + - Regex: '^<.*\.h>' + Priority: 1 + SortPriority: 0 + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + - Regex: '.*' + Priority: 3 + SortPriority: 0 +IncludeIsMainRegex: '([-_](test|unittest|manualtest))?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Right +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: true +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Auto +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 4 +UseCRLF: false +UseTab: Never +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c9507e --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +profiling-result +debian/aitt-dev.debhelper.log +debian/aitt.debhelper.log +debian/aitt +debian/aitt-dev +debian/aitt-plugins +debian/.debhelper/ +debian/aitt.substvars +debian/aitt-dev.substvars +debian/aitt-plugins.substvars +debian/files +debian/tmp/ +build +out +aitt_gcov.info +sam_cli.cfg + +# Built application files +*.aar +jacoco.exec + +# Gradle files +.gradle/ +.galaxy/ +build/ +*.jar + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# IntelliJ +*.iml +.idea/ + +# External native build folder +.cxx/ + +#Third-party dependencies +third_party/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c6fde39 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,96 @@ +CMAKE_MINIMUM_REQUIRED(VERSION 3.4.1) +SET(CMAKE_SKIP_BUILD_RPATH true) +PROJECT(aitt VERSION 0.0.1 LANGUAGES CXX) +SET_PROPERTY(GLOBAL PROPERTY GLOBAL_DEPENDS_DEBUG_MODE 0) +SET(CMAKE_POSITION_INDEPENDENT_CODE TRUE) +SET(CMAKE_CXX_STANDARD 17) +SET(CMAKE_CXX_STANDARD_REQUIRED ON) +SET(CMAKE_CXX_EXTENSIONS OFF) + +OPTION(VERSIONING "Specify Library Verion" ON) + +INCLUDE(GNUInstallDirs) +INCLUDE(FindPkgConfig) + +IF(PLATFORM STREQUAL "android") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -fdiagnostics-color") + ADD_DEFINITIONS(-DANDROID) + INCLUDE(${PROJECT_ROOT_DIR}/cmake/aitt_android_flatbuffers.cmake) + INCLUDE(${PROJECT_ROOT_DIR}/cmake/aitt_android_glib.cmake) + INCLUDE(${PROJECT_ROOT_DIR}/cmake/aitt_android_mosquitto.cmake) + SET(AITT_NEEDS_LIBRARIES ${GLIB_LIBRARIES} ${MOSQUITTO_LIBRARY} ${FLATBUFFERS_LIBRARY}) +ELSE(PLATFORM STREQUAL "android") + IF(PLATFORM STREQUAL "tizen") + ADD_DEFINITIONS(-DTIZEN) + ADD_DEFINITIONS(-DPLATFORM=${PLATFORM}) + SET(ADDITIONAL_OPT "-DTIZEN") + SET(PKGS dlog) + ENDIF(PLATFORM STREQUAL "tizen") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror -Wno-psabi -fdiagnostics-color -fvisibility=hidden") + PKG_CHECK_MODULES(AITT_NEEDS REQUIRED ${PKGS} libmosquitto flatbuffers glib-2.0) + INCLUDE_DIRECTORIES(${AITT_NEEDS_INCLUDE_DIRS}) + LINK_DIRECTORIES(${AITT_NEEDS_LIBRARY_DIRS}) +ENDIF(PLATFORM STREQUAL "android") + +FIND_PACKAGE(Threads REQUIRED) + +IF(LOG_STDOUT) + ADD_DEFINITIONS(-DLOG_STDOUT) +ENDIF(LOG_STDOUT) + +ADD_DEFINITIONS(-DLOG_TAG="AITT") + +IF(COVERAGE_TEST) + ADD_COMPILE_OPTIONS(--coverage) + ADD_LINK_OPTIONS(--coverage -Wl,--dynamic-list-data) +ENDIF(COVERAGE_TEST) + +SET(AITT_COMMON ${PROJECT_NAME}-common) + +INCLUDE_DIRECTORIES(include common) + +AUX_SOURCE_DIRECTORY(src AITT_SRC) + +ADD_LIBRARY(M_LOADER_OBJ OBJECT src/TransportModuleLoader.cc) +LIST(REMOVE_ITEM AITT_SRC src/TransportModuleLoader.cc) + +ADD_LIBRARY(${PROJECT_NAME} SHARED ${AITT_SRC} $<TARGET_OBJECTS:M_LOADER_OBJ>) +TARGET_LINK_LIBRARIES(${PROJECT_NAME} Threads::Threads ${CMAKE_DL_LIBS} ${AITT_COMMON}) +TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${AITT_NEEDS_LIBRARIES}) + +TARGET_COMPILE_OPTIONS(${PROJECT_NAME} PUBLIC ${AITT_NEEDS_CFLAGS_OTHER}) +IF(VERSIONING) + SET_TARGET_PROPERTIES(${PROJECT_NAME} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + ) +ENDIF(VERSIONING) +INSTALL(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +FILE(GLOB HEADERS include/*.h) +INSTALL(FILES ${HEADERS} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}) + +CONFIGURE_FILE(${PROJECT_NAME}.pc.in ${PROJECT_NAME}.pc @ONLY) +INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) +SET_DIRECTORY_PROPERTIES(PROPERTIES ADDITIONAL_CMAKE_CLEAN_FILES ${PROJECT_NAME}.pc) + +IF(BUILD_TESTING) + ENABLE_TESTING() # NOTE: Must comes first than unittest subdirectories + SET(AITT_TEST_BINDIR ${CMAKE_INSTALL_BINDIR}) + ADD_SUBDIRECTORY(tests) + ADD_SUBDIRECTORY(tools) +ENDIF(BUILD_TESTING) + +OPTION(WITH_TCP "Build TCP module?" ON) +IF(WITH_TCP) + ADD_SUBDIRECTORY(modules/tcp) +ENDIF() +IF(PLATFORM STREQUAL "tizen") + OPTION(WITH_WEBRTC "Build WebRtc module?" ON) + IF(WITH_WEBRTC) + ADD_DEFINITIONS(-DWITH_WEBRTC) + ADD_SUBDIRECTORY(modules/webrtc) + ENDIF() +ENDIF(PLATFORM STREQUAL "tizen") + +ADD_SUBDIRECTORY(common) diff --git a/LICENSE.APLv2 b/LICENSE.APLv2 new file mode 100644 index 0000000..a4da8fd --- /dev/null +++ b/LICENSE.APLv2 @@ -0,0 +1,203 @@ +Copyright (c) 2021-2022 Samsung Electronics Co., Ltd. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. @@ -0,0 +1,3 @@ +Copyright (c) 2021-2022 Samsung Electronics Co., Ltd. All rights reserved. +Except as noted, this software is licensed under Apache License, Version 2. +Please, see the LICENSE.APLv2 file for Apache license, version 2 terms and conditions. diff --git a/aitt.pc.in b/aitt.pc.in new file mode 100644 index 0000000..1cd7e88 --- /dev/null +++ b/aitt.pc.in @@ -0,0 +1,8 @@ +libdir=@CMAKE_INSTALL_FULL_LIBDIR@ +includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@/@PROJECT_NAME@ + +Name: @PROJECT_NAME@ +Description: Development files for the modualrized AI framework +Version: @PROJECT_VERSION@ +Libs: -L${libdir} -l@PROJECT_NAME@ +Cflags: -I${includedir} @ADDITIONAL_OPT@ diff --git a/android/aitt/.gitignore b/android/aitt/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/android/aitt/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android/aitt/CMakeLists.txt b/android/aitt/CMakeLists.txt new file mode 100644 index 0000000..4d1b82f --- /dev/null +++ b/android/aitt/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.4.1) +project("aitt-android" CXX) + +if(NOT DEFINED PROJECT_ROOT_DIR) + set(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../..) +endif(NOT DEFINED PROJECT_ROOT_DIR) + +find_library(ALOG NAMES log) +set(LOG_LIBRARIES ${ALOG}) + +add_subdirectory(${PROJECT_ROOT_DIR} ${CMAKE_BINARY_DIR}/aitt) + +include_directories(${PROJECT_ROOT_DIR}/include) + +set(ANDROID_SRC src/main/jni/aitt_jni.cc) + +add_library(${PROJECT_NAME} SHARED ${ANDROID_SRC}) + +target_link_libraries(${PROJECT_NAME} aitt ${LOG_LIBRARIES}) + +add_dependencies(${PROJECT_NAME} aitt) diff --git a/android/aitt/build.gradle b/android/aitt/build.gradle new file mode 100644 index 0000000..2fb82be --- /dev/null +++ b/android/aitt/build.gradle @@ -0,0 +1,164 @@ +plugins { + id 'com.android.library' + id "de.undercouch.download" version "5.0.1" +} + +def thirdPartyDir = new File ("${rootProject.projectDir}/third_party") + +def flatbuffersDir = new File("${thirdPartyDir}/flatbuffers-2.0.0") +def mosquittoDir = new File("${thirdPartyDir}/mosquitto-2.0.14") + +android { + compileSdkVersion 31 + ndkVersion "21.3.6528147" + defaultConfig { + minSdkVersion 28 + targetSdkVersion 31 + versionCode 1 + versionName '1.0' + project.archivesBaseName = "aitt" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + externalNativeBuild { + cmake { + arguments '-DLOG_STDOUT=ON' + arguments '-DCMAKE_VERBOSE_MAKEFILE=1' + arguments '-DCMAKE_INSTALL_PREFIX:PATH=/usr' + arguments '-DANDROID_STL=c++_shared' + arguments "-DANDROID_NDK_HOME=${System.env.ANDROID_NDK_ROOT}" + arguments "-DGSTREAMER_ROOT_ANDROID=${System.env.GSTREAMER_ROOT_ANDROID}" + arguments '-DBUILD_TESTING=OFF' + arguments '-DUSE_PREBUILT=OFF' + arguments '-DVERSIONING=OFF' + arguments '-DPLATFORM=android' + arguments '-DCOVERAGE=OFF' + abiFilters 'arm64-v8a', 'x86' + cppFlags "-std=c++17" + targets "aitt-android", "aitt-transport-tcp" + } + } + } + + externalNativeBuild { + cmake { + path file('./CMakeLists.txt') + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + packagingOptions { + jniLibs { + useLegacyPackaging = true + } + pickFirst 'lib/armeabi-v7a/libaitt.so' + } + libraryVariants.all { variant -> + variant.outputs.all { + outputFileName = "${archivesBaseName}-${defaultConfig.versionName}.aar" + } + } + + testOptions { + unitTests.returnDefaultValues = true + unitTests.includeAndroidResources = true + unitTests.all { + jacoco { + includeNoLocationClasses = true + excludes = ['jdk.internal.*'] + } + } + } +} + +dependencies { + compileOnly project(":android:flatbuffers") + compileOnly project(":android:mosquitto") + + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.flatbuffers:flatbuffers-java:2.0.0' + implementation fileTree(include: ['*.jar'], dir: 'modules/webrtc') + implementation project(path: ':android:modules:webrtc') + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:2.25.0' + testImplementation 'org.powermock:powermock-core:2.0.0-beta.5' + testImplementation 'org.powermock:powermock-module-junit4:2.0.0-beta.5' + testImplementation 'org.powermock:powermock-api-mockito2:2.0.0-beta.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} + +task downloadFlatBuffers(type: Download) { + doFirst { + println("Downloading FlatBuffers") + } + src "https://github.com/google/flatbuffers/archive/refs/tags/v2.0.0.zip" + dest new File(thirdPartyDir, "flatbuffers.zip") + onlyIfModified true +} + +task unzipFlatBuffers(type: Copy, dependsOn: downloadFlatBuffers) { + doFirst { + println("Unzipping FlatBuffers") + } + from zipTree(downloadFlatBuffers.dest) + into thirdPartyDir + onlyIf { !flatbuffersDir.exists() } +} + +task downloadMosquitto(type: Download) { + doFirst { + println("Downloading Mosquitto") + } + src "https://github.com/eclipse/mosquitto/archive/refs/tags/v2.0.14.zip" + dest new File(thirdPartyDir, "mosquitto-2.0.14.zip") + onlyIfModified true +} + +task unzipMosquitto(type: Copy, dependsOn: downloadMosquitto) { + doFirst { + println("Unzipping Mosquitto") + } + from zipTree(downloadMosquitto.dest) + into thirdPartyDir + onlyIf { !mosquittoDir.exists() } +} + +preBuild.dependsOn(unzipFlatBuffers) +preBuild.dependsOn(unzipMosquitto) +preBuild.dependsOn ":android:flatbuffers:build" +preBuild.dependsOn ":android:mosquitto:build" + +apply plugin: 'jacoco' + +task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { + + reports { + xml.enabled = true + html.enabled = true + } + + def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'] + def debugTree = fileTree(dir : "${buildDir}/intermediates/javac/debugUnitTest", excludes: fileFilter) + + def mainSrc = "${project.projectDir}/src/main/java" + + getSourceDirectories().setFrom(files([mainSrc])) + getClassDirectories().setFrom(files([debugTree])) + getExecutionData().setFrom(fileTree(dir: "$buildDir", includes: [ + "jacoco/testDebugUnitTest.exec", + "outputs/code-coverage/connected/*coverage.ec" + ])) +} + +task wrapper(type: Wrapper) { + gradleVersion = '4.1' +} + +build.dependsOn jacocoTestReport diff --git a/android/aitt/proguard-rules.pro b/android/aitt/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/android/aitt/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/aitt/src/main/AndroidManifest.xml b/android/aitt/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e51cebf --- /dev/null +++ b/android/aitt/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.samsung.android.aitt"> + <uses-permission android:name="android.permission.INTERNET" /> +</manifest> diff --git a/android/aitt/src/main/java/com/samsung/android/aitt/Aitt.java b/android/aitt/src/main/java/com/samsung/android/aitt/Aitt.java new file mode 100644 index 0000000..b335314 --- /dev/null +++ b/android/aitt/src/main/java/com/samsung/android/aitt/Aitt.java @@ -0,0 +1,687 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.samsung.android.aitt; + +import android.content.Context; +import android.util.Log; +import android.util.Pair; + +import androidx.annotation.Nullable; + +import com.google.flatbuffers.FlexBuffers; +import com.google.flatbuffers.FlexBuffersBuilder; +import com.samsung.android.modules.webrtc.WebRTC; +import com.samsung.android.modules.webrtc.WebRTCServer; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +/** + * Class creates a Java layer to operate between application and JNI layer for AITT C++ + * 1. Connect to MQTT broker + * 2. Subscribe to a topic with protocol and other params + * 3. Publish to a topic using protocol and other params + * 4. Unsubscribe to a topic + * 5. Invoke JNI api's which interact with aitt c++ + */ +public class Aitt { + private static final String TAG = "AITT_ANDROID"; + private static final String WILL_LEAVE_NETWORK = "disconnected"; + private static final String AITT_LOCALHOST = "127.0.0.1"; + private static final int AITT_PORT = 1883; + private static final String JAVA_SPECIFIC_DISCOVERY_TOPIC = "/java/aitt/discovery/"; + private static final String JOIN_NETWORK = "connected"; + private static final String RESPONSE_POSTFIX = "_AittRe_"; + private static final String INVALID_TOPIC = "Invalid topic"; + private static final String STATUS = "status"; + + /** + * Load aitt-android library + */ + static { + try { + System.loadLibrary("aitt-android"); + }catch (UnsatisfiedLinkError e){ + // only ignore exception in non-android env + if ("Dalvik".equals(System.getProperty("java.vm.name"))) throw e; + } + } + private HashMap<String, ArrayList<SubscribeCallback>> subscribeCallbacks = new HashMap<>(); + private HashMap<String, HostTable> publishTable = new HashMap<>(); + private HashMap<String, Pair<Protocol , Object>> subscribeMap = new HashMap<>(); + private HashMap<String, Long> aittSubId = new HashMap<String, Long>(); + private ConnectionCallback connectionCallback = null; + + private long instance = 0; + private String ip; + private Context appContext; + //ToDo - For now using sample app parameters, later fetch frameWidth & frameHeight from app + private Integer frameWidth = 640, frameHeight = 480; + + /** + * QoS levels to define the guarantee of delivery for a message + */ + public enum QoS { + AT_MOST_ONCE, // Fire and forget + AT_LEAST_ONCE, // Receiver is able to receive multiple times + EXACTLY_ONCE, // Receiver only receives exactly once + } + + /** + * List of protocols supported by AITT framework + */ + public enum Protocol { + MQTT(0x1 << 0), // Publish message through the MQTT + TCP(0x1 << 1), // Publish message to peers using the TCP + UDP(0x1 << 2), // Publish message to peers using the UDP + SRTP(0x1 << 3), // Publish message to peers using the SRTP + WEBRTC(0x1 << 4); // Publish message to peers using the WEBRTC + + private final int value; + + private Protocol(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static Protocol fromInt(long value) { + for (Protocol type : values()) { + if (type.getValue() == value) { + return type; + } + } + return null; + } + } + + /** + * HostTable to store String and PortTable instances + */ + private static class HostTable { + HashMap<String, PortTable> hostMap = new HashMap<>(); + } + + /** + * PortTable to store port with protocol, webRTC transportHandler object instance + */ + private static class PortTable { + HashMap<Integer, Pair<Protocol , Object>> portMap = new HashMap<>(); + } + + /** + * Interface to implement connection status callback + */ + public interface ConnectionCallback { + void onConnected(); + void onDisconnected(); + } + + /** + * Interface to implement callback method for subscribe call + */ + public interface SubscribeCallback { + void onMessageReceived(AittMessage message); + } + + /** + * Aitt constructor to create AITT object + * @param appContext context of the application invoking the constructor + * @param id Unique identifier for the Aitt instance + */ + public Aitt(Context appContext , String id) throws InstantiationException { + this(appContext , id, AITT_LOCALHOST, false); + } + + /** + * Aitt constructor to create AITT object + * @param appContext context of the application invoking the constructor + * @param id Unique identifier for the Aitt instance + * @param ip IP address of the device, on which application is running + * @param clearSession "clear" the current session when the client disconnects + */ + public Aitt(Context appContext, String id, String ip, boolean clearSession) throws InstantiationException { + if (appContext == null) { + throw new IllegalArgumentException("Invalid appContext"); + } + if (id == null || id.isEmpty()) { + throw new IllegalArgumentException("Invalid id"); + } + instance = initJNI(id, ip, clearSession); + if (instance == 0L) { + throw new InstantiationException("Failed to instantiate native instance"); + } + this.ip = ip; + this.appContext = appContext; + } + + /** + * Method to set connection status callback + * @param callback ConnectionCallback to which status should be updated + */ + public void setConnectionCallback(ConnectionCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("Invalid callback"); + } + connectionCallback = callback; + setConnectionCallbackJNI(instance); + } + + /** + * Method to connect to MQTT broker + * @param brokerIp Broker IP address to which, device has to connect + */ + public void connect(@Nullable String brokerIp) { + connect(brokerIp, AITT_PORT); + } + + /** + * Method to connect to MQTT broker + * @param brokerIp Broker IP address to which, device has to connect + * @param port Broker port number to which, device has to connect + */ + public void connect(@Nullable String brokerIp, int port) { + if (brokerIp == null || brokerIp.isEmpty()) { + brokerIp = AITT_LOCALHOST; + } + connectJNI(instance, brokerIp, port); + //Subscribe to java discovery topic + subscribeJNI(instance, JAVA_SPECIFIC_DISCOVERY_TOPIC, Protocol.MQTT.getValue(), QoS.EXACTLY_ONCE.ordinal()); + } + + /** + * Method to disconnect from MQTT broker + */ + public void disconnect() { + publishJNI(instance, JAVA_SPECIFIC_DISCOVERY_TOPIC, new byte[0], 0, Protocol.MQTT.getValue(), QoS.AT_LEAST_ONCE.ordinal(), true); + + disconnectJNI(instance); + try { + close(); + } catch (Exception e) { + Log.e(TAG, "Error during disconnect", e); + } + } + + /** + * Method to publish message to a specific topic + * @param topic String to which message needs to be published + * @param message Byte message that needs to be published + */ + public void publish(String topic, byte[] message) { + EnumSet<Protocol> protocolSet = EnumSet.of(Protocol.MQTT); + publish(topic, message, protocolSet, QoS.AT_MOST_ONCE, false); + } + + /** + * Method to publish message to a specific topic + * @param topic String to which message needs to be published + * @param message Byte message that needs to be published + * @param protocol Protocol to be used to publish message + * @param qos QoS at which the message should be delivered + * @param retain Boolean to decide whether or not the message should be retained by the broker + */ + public void publish(String topic, byte[] message, Protocol protocol, QoS qos, boolean retain) { + EnumSet<Protocol> protocolSet = EnumSet.of(protocol); + publish(topic, message, protocolSet, qos, retain); + } + + /** + * Method to publish message to a specific topic + * @param topic String to which message needs to be published + * @param message Byte message that needs to be published + * @param protocols Protocol to be used to publish message + * @param qos QoS at which the message should be delivered + * @param retain Boolean to decide whether or not the message should be retained by the broker + */ + public void publish(String topic, byte[] message, EnumSet<Protocol> protocols, QoS qos, boolean retain) { + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(INVALID_TOPIC); + } + if (protocols.isEmpty()) { + throw new IllegalArgumentException("Invalid protocols"); + } + if (protocols.contains(Protocol.WEBRTC)) { + try { + synchronized (this) { + if (!publishTable.containsKey(topic)) { + Log.e(TAG, "Invalid publish request over unsubscribed topic"); + return; + } + HostTable hostTable = publishTable.get(topic); + for (String hostIp : hostTable.hostMap.keySet()) { + PortTable portTable = hostTable.hostMap.get(hostIp); + for (Integer port : portTable.portMap.keySet()) { + Object transportHandler = portTable.portMap.get(port).second; + publishWebRTC(portTable, topic, transportHandler, hostIp, port, message); + } + } + } + } catch (Exception e) { + Log.e(TAG, "Error during publish", e); + } + } else { + int proto = protocolsToInt(protocols); + publishJNI(instance, topic, message, message.length, proto, qos.ordinal(), retain); + } + } + + /** + * Method used to identify data type for webRTC channel and transfer data + * @param portTable portTable has information about port and associated protocol with transport Handler object + * @param topic The topic to which data is published + * @param transportHandler WebRTC object instance + * @param ip IP address of the destination + * @param port Port number of the destination + * @param message Data to be tranferred over WebRTC + */ + private void publishWebRTC(PortTable portTable, String topic, Object transportHandler, String ip, int port, byte[] message) { + WebRTC.DataType dataType = topic.endsWith(RESPONSE_POSTFIX) ? WebRTC.DataType.MESSAGE : WebRTC.DataType.VIDEOFRAME; + WebRTC webrtcHandler; + if (transportHandler == null) { + webrtcHandler = new WebRTC(dataType, appContext); + transportHandler = webrtcHandler; + portTable.portMap.replace(port, new Pair<>(Protocol.WEBRTC, transportHandler)); + webrtcHandler.connect(ip, port); + } else { + webrtcHandler = (WebRTC) transportHandler; + } + if (dataType == WebRTC.DataType.MESSAGE) { + webrtcHandler.sendMessageData(message); + } else if (dataType == WebRTC.DataType.VIDEOFRAME) { + webrtcHandler.sendVideoData(message, frameWidth, frameHeight); + } + } + + /** + * Method to subscribe to a specific topic + * @param topic String to which applications can subscribe, to receive data + * @param callback Callback object specific to a subscribe call + */ + public void subscribe(String topic, SubscribeCallback callback) { + EnumSet<Protocol> protocolSet = EnumSet.of(Protocol.MQTT); + subscribe(topic, callback, protocolSet, QoS.AT_MOST_ONCE); + } + + /** + * Method to subscribe to a specific topic + * @param topic String to which applications can subscribe, to receive data + * @param callback Callback object specific to a subscribe call + * @param protocol Protocol supported by application, invoking subscribe + * @param qos QoS at which the message should be delivered + */ + public void subscribe(String topic, SubscribeCallback callback, Protocol protocol, QoS qos) { + EnumSet<Protocol> protocolSet = EnumSet.of(protocol); + subscribe(topic, callback, protocolSet, qos); + } + + /** + * Method to subscribe to a specific topic + * @param topic String to which applications can subscribe, to receive data + * @param callback Callback object specific to a subscribe call + * @param protocols Protocol supported by application, invoking subscribe + * @param qos QoS at which the message should be delivered + */ + public void subscribe(String topic, SubscribeCallback callback, EnumSet<Protocol> protocols, QoS qos) { + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(INVALID_TOPIC); + } + if (callback == null) { + throw new IllegalArgumentException("Invalid callback"); + } + if (protocols.isEmpty()) { + throw new IllegalArgumentException("Invalid protocols"); + } + try { + if (protocols.contains(Protocol.WEBRTC)) { + WebRTC.ReceiveDataCallback cb = frame -> { + AittMessage message = new AittMessage(frame); + message.setTopic(topic); + messageReceived(message); + }; + WebRTC.DataType dataType = topic.endsWith(RESPONSE_POSTFIX) ? WebRTC.DataType.MESSAGE : WebRTC.DataType.VIDEOFRAME; + WebRTCServer ws = new WebRTCServer(appContext, dataType, cb); + int serverPort = ws.start(); + if (serverPort < 0) { + throw new IllegalArgumentException("Failed to start webRTC server-socket"); + } + synchronized (this) { + subscribeMap.put(topic, new Pair(Protocol.WEBRTC, ws)); + } + byte[] data = wrapPublishData(topic, serverPort); + publishJNI(instance, JAVA_SPECIFIC_DISCOVERY_TOPIC, data, data.length, Protocol.MQTT.value, QoS.EXACTLY_ONCE.ordinal(), true); + } else { + int proto = protocolsToInt(protocols); + Long pObject = subscribeJNI(instance, topic, proto, qos.ordinal()); + synchronized (this) { + aittSubId.put(topic, pObject); + } + } + } catch (Exception e) { + Log.e(TAG, "Error during subscribe", e); + } + addCallBackToSubscribeMap(topic, callback); + } + + /** + * Method to wrap topic, device IP address, webRTC server instance port number for publishing + * @param topic Topic to which the application has subscribed to + * @param serverPort Port number of the WebRTC server instance + * @return Byte data wrapped, contains topic, device IP, webRTC server port number + */ + private byte[] wrapPublishData(String topic, int serverPort) { + FlexBuffersBuilder fbb = new FlexBuffersBuilder(ByteBuffer.allocate(512)); + { + int smap = fbb.startMap(); + fbb.putString(STATUS, JOIN_NETWORK); + fbb.putString("host", this.ip); + { + int smap1 = fbb.startMap(); + fbb.putInt("protocol", Protocol.WEBRTC.value); + fbb.putInt("port", serverPort); + fbb.endMap(topic, smap1); + } + fbb.endMap(null, smap); + } + ByteBuffer buffer = fbb.finish(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data, 0, data.length); + return data; + } + + /** + * Method to map subscribe callback instance to subscribing topic + * @param topic String to which application can subscribe + * @param callback Subscribe callback instance created during subscribe call + */ + private void addCallBackToSubscribeMap(String topic, SubscribeCallback callback) { + synchronized (this) { + try { + ArrayList<SubscribeCallback> cbList = subscribeCallbacks.get(topic); + + if (cbList != null) { + // check whether the list already contains same callback + if (!cbList.contains(callback)) { + cbList.add(callback); + } + } else { + cbList = new ArrayList<>(); + cbList.add(callback); + subscribeCallbacks.put(topic, cbList); + } + } catch (Exception e) { + Log.e(TAG, "Error during callback add", e); + } + } + } + + /** + * Method to unsubscribe to a topic, subscribed by application + * @param topic String topic to which application had subscribed + */ + public void unsubscribe(String topic) { + if (topic == null || topic.isEmpty()) { + throw new IllegalArgumentException(INVALID_TOPIC); + } + + boolean isRemoved = false; + try { + synchronized (this) { + if (subscribeMap.containsKey(topic) && subscribeMap.get(topic).first == Protocol.WEBRTC) { + WebRTCServer ws = (WebRTCServer) subscribeMap.get(topic).second; + ws.stop(); + subscribeMap.remove(topic); + isRemoved = true; + } + } + + if (!isRemoved) { + Long paittSubId = null; + synchronized (this) { + if (aittSubId.containsKey(topic)) { + paittSubId = aittSubId.get(topic); + } + } + if (paittSubId != null) { + unsubscribeJNI(instance, paittSubId); + } + } + + synchronized (this) { + subscribeCallbacks.remove(topic); + aittSubId.remove(topic); + } + } catch (Exception e) { + Log.e(TAG, "Error during unsubscribe", e); + } + } + + /** + * Method invoked from JNI layer to Java layer for MQTT connection status update + * @param status Status of the MQTT connection + */ + private void connectionStatusCallback(int status) { + if (status == 0) { + connectionCallback.onDisconnected(); + } else { + connectionCallback.onConnected(); + } + } + + /** + * Method invoked from JNI layer to Java layer for message exchange + * @param topic Topic to which message callback is called + * @param payload Byte data shared from JNI layer to Java layer + */ + private void messageCallback(String topic, byte[] payload) { + try { + if (topic.compareTo(JAVA_SPECIFIC_DISCOVERY_TOPIC) == 0) { + if (payload.length <= 0) { + Log.e(TAG, "Invalid payload, Ignore"); + return; + } + discoveryMessageCallback(payload); + } else { + AittMessage message = new AittMessage(payload); + message.setTopic(topic); + messageReceived(message); + } + } catch (Exception e) { + Log.e(TAG, "Error processing callback ", e); + } + } + + /** + * This API is called when MQTT subscribe callback is invoked at aitt C++. + * It has discovery information in a "payload" + * @param payload + * Flexbuffer discovery message expected + * { + * "status": "connected", + * "host": "127.0.0.1", + * "/customTopic/aitt/faceRecog": { + * "protocol": 1, + * "port": 108081, + * }, + * "/customTopic/aitt/ASR": { + * "protocol": 2, + * "port": 102020, + * }, + * + * ... + * + * "/customTopic/aitt/+": { + * "protocol": 3, + * "port": 20123, + * }, + * } + */ + + /** + * Method to receive discovery message with device, protocol and other details and update publish table + * @param payload Byte data having discovery related message + */ + private void discoveryMessageCallback(byte[] payload) { + try { + ByteBuffer buffer = ByteBuffer.wrap(payload); + FlexBuffers.Map map = FlexBuffers.getRoot(buffer).asMap(); + String host = map.get("host").asString(); + String status = map.get(STATUS).asString(); + if (status != null && status.compareTo(WILL_LEAVE_NETWORK) == 0) { + synchronized (this) { + for (Map.Entry<String, HostTable> entry : publishTable.entrySet()) { + HostTable hostTable = entry.getValue(); + if (hostTable != null) { + hostTable.hostMap.remove(host); + } + } + } + return; + } + + FlexBuffers.KeyVector topics = map.keys(); + for (int i = 0; i < topics.size(); i++) { + String _topic = topics.get(i).toString(); + if (_topic.compareTo("host") == 0 || _topic.compareTo(STATUS) == 0) { + continue; + } + + FlexBuffers.Map _map = map.get(_topic).asMap(); + int port = _map.get("port").asInt(); + long p = _map.get("protocol").asUInt(); + Protocol protocol = Protocol.fromInt(p); + updatePublishTable(_topic, host, port, protocol); + } + } catch (Exception e) { + Log.e(TAG, "Error during discovery callback processing", e); + } + } + + /** + * Method used to update Publish table of the application + * @param topic The topic to which, other parties have subscribed to + * @param host String which specifies a particular host + * @param port Port of the party which subscribed to given topic + * @param protocol protocol supported by the party which subscribed to given topic + */ + private void updatePublishTable(String topic, String host, int port, Protocol protocol) { + synchronized(this) { + if (!publishTable.containsKey(topic)) { + PortTable portTable = new PortTable(); + portTable.portMap.put(port, new Pair(protocol , null)); + HostTable hostTable = new HostTable(); + hostTable.hostMap.put(host, portTable); + publishTable.put(topic, hostTable); + return; + } + + HostTable hostTable = publishTable.get(topic); + if (!hostTable.hostMap.containsKey(host)) { + PortTable portTable = new PortTable(); + portTable.portMap.put(port, new Pair(protocol , null)); + hostTable.hostMap.put(host, portTable); + return; + } + + PortTable portTable = hostTable.hostMap.get(host); + if (portTable.portMap.containsKey(port)) { + portTable.portMap.replace(port, new Pair(protocol , null)); + return; + } + + portTable.portMap.put(port, new Pair(protocol , null)); + } + } + + /** + * Method that receives message from JNI layer for topics other than discovery topics + * @param message The data received from JNI layer to be sent to application layer + */ + private void messageReceived(AittMessage message) { + try { + String topic = message.getTopic(); + synchronized (this) { + ArrayList<SubscribeCallback> cbList = subscribeCallbacks.get(topic); + + if (cbList != null) { + for (int i = 0; i < cbList.size(); i++) { + cbList.get(i).onMessageReceived(message); + } + } + } + } catch (Exception e) { + Log.e(TAG, "Error during messageReceived", e); + } + } + + /** + * Method used to convert EnumSet protocol into int + * @param protocols List of protocols + * @return The protocol value + */ + private int protocolsToInt(EnumSet<Protocol> protocols) { + int proto = 0; + for (Protocol p : Protocol.values()) { + if (protocols.contains(p)) { + proto += p.getValue(); + } + } + return proto; + } + + /** + * Method to close all the callbacks and release resources + */ + public void close() { + synchronized (this) { + if(subscribeCallbacks!=null) { + subscribeCallbacks.clear(); + subscribeCallbacks = null; + } + if(aittSubId!=null) { + aittSubId.clear(); + aittSubId = null; + } + } + } + + /* native API's set */ + /* Native API to initialize JNI */ + private native long initJNI(String id, String ip, boolean clearSession); + + /* Native API for connecting to broker */ + private native void connectJNI(long instance, final String host, int port); + + /* Native API for disconnecting from broker */ + private native void disconnectJNI(long instance); + + /* Native API for setting connection callback */ + private native void setConnectionCallbackJNI(long instance); + + /* Native API for publishing to a topic */ + private native void publishJNI(long instance, final String topic, final byte[] data, long datalen, int protocol, int qos, boolean retain); + + /* Native API for subscribing to a topic */ + private native long subscribeJNI(long instance, final String topic, int protocol, int qos); + + /* Native API for unsubscribing a topic */ + private native void unsubscribeJNI(long instance, final long aittSubId); +} diff --git a/android/aitt/src/main/java/com/samsung/android/aitt/AittMessage.java b/android/aitt/src/main/java/com/samsung/android/aitt/AittMessage.java new file mode 100644 index 0000000..7ba570f --- /dev/null +++ b/android/aitt/src/main/java/com/samsung/android/aitt/AittMessage.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.samsung.android.aitt; + +public class AittMessage { + private String topic; + private String correlation; + private String replyTopic; + private int sequence; + private boolean endSequence; + private byte[] payload; + + /** + * AittMessage constructor for initializing + */ + public AittMessage() { + setPayload(new byte[]{}); + } + + /** + * AittMessage constructor with params for initializing + * @param payload The message data set as payload + */ + public AittMessage(byte[] payload) { + setPayload(payload); + } + + /** + * Method to get Topic assigned with AittMessage object + * @return The string topic + */ + public String getTopic() { + return topic; + } + + /** + * Method to set topic to AittMessage object + * @param topic String topic to be assigned to the object + */ + public void setTopic(String topic) { + this.topic = topic; + } + + /** + * Method to get Correlation + * @return the correlation + */ + public String getCorrelation() { + return correlation; + } + + /** + * Method to set correlation + * @param correlation correlation string to be set + */ + public void setCorrelation(String correlation) { + this.correlation = correlation; + } + + /** + * Method to get the reply topic + * @return the string of reply topic + */ + public String getReplyTopic() { + return replyTopic; + } + + /** + * Method to set reply topic + * @param replyTopic String that is set as reply topic + */ + public void setReplyTopic(String replyTopic) { + this.replyTopic = replyTopic; + } + + /** + * Method used to get sequence + * @return the sequence + */ + public int getSequence() { + return sequence; + } + + /** + * Method used to set sequence + * @param sequence the sequence value to be set + */ + public void setSequence(int sequence) { + this.sequence = sequence; + } + + /** + * Method used to increase the sequence by one + */ + public void increaseSequence() { + sequence = sequence+1; + } + + /** + * Method used to set endSequence + * @param endSequence boolean value to be set to end sequence + */ + public void setEndSequence(boolean endSequence) { + this.endSequence = endSequence; + } + + /** + * Method used to get if sequence is ended + * @return The state of sequence + */ + public boolean isEndSequence() { + return endSequence; + } + + /** + * Method used to retrieve payload + * @return The data in byte[] format + */ + public byte[] getPayload() { + return payload; + } + + /** + * Method used to clear the payload + */ + public void clearPayload() { + this.payload = new byte[]{}; + } + + /** + * Method used to set payload to AittMessage object + * @param payload the byte[] message/payload to be set + */ + public void setPayload(byte[] payload) { + if (payload == null) { + throw new NullPointerException(); + } + this.payload = payload; + } +} diff --git a/android/aitt/src/main/jni/aitt_jni.cc b/android/aitt/src/main/jni/aitt_jni.cc new file mode 100644 index 0000000..801b906 --- /dev/null +++ b/android/aitt/src/main/jni/aitt_jni.cc @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "aitt_jni.h" + +AittNativeInterface::CallbackContext AittNativeInterface::cbContext = { + .jvm = nullptr, + .messageCallbackMethodID = nullptr, +}; + +AittNativeInterface::AittNativeInterface(std::string &mqId, std::string &ip, bool clearSession) + : cbObject(nullptr), aitt(mqId, ip, clearSession) +{ +} + +AittNativeInterface::~AittNativeInterface(void) +{ + if (cbObject != nullptr) { + JNIEnv *env = nullptr; + bool attached = false; + int JNIStatus = cbContext.jvm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); + if (JNIStatus == JNI_EDETACHED) { + if (cbContext.jvm->AttachCurrentThread(&env, nullptr) != 0) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to attach current thread"); + } else { + attached = true; + } + } else if (JNIStatus == JNI_EVERSION) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Unsupported version"); + } + + if (env != nullptr) { + env->DeleteGlobalRef(cbObject); + cbObject = nullptr; + } + if (attached) { + cbContext.jvm->DetachCurrentThread(); + } + } +} + +std::string AittNativeInterface::GetStringUTF(JNIEnv *env, jstring str) +{ + if (env == nullptr) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Env is null"); + return nullptr; + } + const char *cstr = env->GetStringUTFChars(str, 0); + if (cstr == nullptr) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to get string UTF chars"); + return nullptr; + } + std::string _str(cstr); + env->ReleaseStringUTFChars(str, cstr); + return _str; +} + +void AittNativeInterface::Java_com_samsung_android_aitt_Aitt_connectJNI(JNIEnv *env, + jobject jniInterfaceObject, jlong handle, jstring host, jint port) +{ + if (env == nullptr || jniInterfaceObject == nullptr) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Env or Jobject is null"); + return; + } + AittNativeInterface *instance = reinterpret_cast<AittNativeInterface *>(handle); + std::string brokerIp = GetStringUTF(env, host); + if (brokerIp.empty()) { + return; + } + + int brokerPort = (int)port; + + try { + instance->aitt.Connect(brokerIp, brokerPort); + } catch (std::exception &e) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to connect"); + JNI_LOG(ANDROID_LOG_ERROR, TAG, e.what()); + } +} + +void AittNativeInterface::Java_com_samsung_android_aitt_Aitt_publishJNI(JNIEnv *env, + jobject jniInterfaceObject, jlong handle, jstring topic, jbyteArray data, jlong datalen, + jint protocol, jint qos, jboolean retain) +{ + if (env == nullptr || jniInterfaceObject == nullptr) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Env or jobject is null"); + return; + } + AittNativeInterface *instance = reinterpret_cast<AittNativeInterface *>(handle); + std::string customTopic = GetStringUTF(env, topic); + if (customTopic.empty()) { + return; + } + + int num_bytes = (int)datalen; + const char *cdata = (char *)env->GetByteArrayElements(data, 0); + if (cdata == nullptr) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to get byte array elements"); + return; + } + const void *_data = reinterpret_cast<const void *>(cdata); + + AittProtocol _protocol = static_cast<AittProtocol>(protocol); + AittQoS _qos = static_cast<AittQoS>(qos); + bool _retain = (bool)retain; + + try { + instance->aitt.Publish(customTopic, _data, num_bytes, _protocol, _qos, _retain); + } catch (std::exception &e) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to publish"); + JNI_LOG(ANDROID_LOG_ERROR, TAG, e.what()); + } + env->ReleaseByteArrayElements(data, reinterpret_cast<jbyte *>((char *)cdata), 0); +} + +void AittNativeInterface::Java_com_samsung_android_aitt_Aitt_disconnectJNI(JNIEnv *env, + jobject jniInterfaceObject, jlong handle) +{ + if (env == nullptr || jniInterfaceObject == nullptr) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Env or Jobject is null"); + return; + } + AittNativeInterface *instance = reinterpret_cast<AittNativeInterface *>(handle); + try { + instance->aitt.Disconnect(); + } catch (std::exception &e) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to disconnect"); + JNI_LOG(ANDROID_LOG_ERROR, TAG, e.what()); + } +} + +jlong AittNativeInterface::Java_com_samsung_android_aitt_Aitt_subscribeJNI(JNIEnv *env, + jobject jniInterfaceObject, jlong handle, jstring topic, jint protocol, jint qos) +{ + if (env == nullptr || jniInterfaceObject == nullptr) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Env or Jobject is null"); + return 0L; + } + AittNativeInterface *instance = reinterpret_cast<AittNativeInterface *>(handle); + std::string customTopic = GetStringUTF(env, topic); + if (customTopic.empty()) { + return 0L; + } + + AittProtocol _protocol = static_cast<AittProtocol>(protocol); + AittQoS _qos = static_cast<AittQoS>(qos); + + AittSubscribeID _id = nullptr; + try { + _id = instance->aitt.Subscribe( + customTopic, + [&](aitt::MSG *handle, const void *msg, const int szmsg, void *cbdata) -> void { + AittNativeInterface *instance = reinterpret_cast<AittNativeInterface *>(cbdata); + JNIEnv *env; + int JNIStatus = + cbContext.jvm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); + if (JNIStatus == JNI_EDETACHED) { + if (cbContext.jvm->AttachCurrentThread(&env, nullptr) != 0) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to attach current thread"); + return; + } + } else if (JNIStatus == JNI_EVERSION) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Unsupported version"); + return; + } + if (env != nullptr && instance->cbObject != nullptr) { + jstring _topic = env->NewStringUTF(handle->GetTopic().c_str()); + if (env->ExceptionCheck() == true) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to create new UTF string"); + cbContext.jvm->DetachCurrentThread(); + return; + } + + jbyteArray array = env->NewByteArray(szmsg); + auto _msg = reinterpret_cast<unsigned char *>(const_cast<void *>(msg)); + env->SetByteArrayRegion(array, 0, szmsg, reinterpret_cast<jbyte *>(_msg)); + if (env->ExceptionCheck() == true) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to set byte array"); + cbContext.jvm->DetachCurrentThread(); + return; + } + + env->CallVoidMethod(instance->cbObject, cbContext.messageCallbackMethodID, + _topic, array); + if (env->ExceptionCheck() == true) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to call void method"); + cbContext.jvm->DetachCurrentThread(); + return; + } + } + cbContext.jvm->DetachCurrentThread(); + }, + reinterpret_cast<void *>(instance), _protocol, _qos); + } catch (std::exception &e) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to subscribe"); + JNI_LOG(ANDROID_LOG_ERROR, TAG, e.what()); + } + return (jlong)_id; +} + +void AittNativeInterface::Java_com_samsung_android_aitt_Aitt_unsubscribeJNI(JNIEnv *env, + jobject jniInterfaceObject, jlong handle, jlong aittSubId) +{ + if (env == nullptr || jniInterfaceObject == nullptr) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Env or Jobject is null"); + return; + } + AittNativeInterface *instance = reinterpret_cast<AittNativeInterface *>(handle); + void *subId = reinterpret_cast<void *>(aittSubId); + try { + instance->aitt.Unsubscribe(static_cast<AittSubscribeID>(subId)); + } catch (std::exception &e) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to unsubscribe"); + JNI_LOG(ANDROID_LOG_ERROR, TAG, e.what()); + } +} + +void AittNativeInterface::Java_com_samsung_android_aitt_Aitt_setConnectionCallbackJNI(JNIEnv *env, + jobject jniInterfaceObject, jlong handle) +{ + if (env == nullptr || jniInterfaceObject == nullptr) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Env or Jobject is null"); + return; + } + AittNativeInterface *instance = reinterpret_cast<AittNativeInterface *>(handle); + + try { + instance->aitt.SetConnectionCallback( + [&](AITT &handle, int status, void *user_data) -> void { + AittNativeInterface *instance = reinterpret_cast<AittNativeInterface *>(user_data); + JNIEnv *env; + int JNIStatus = + cbContext.jvm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); + if (JNIStatus == JNI_EDETACHED) { + if (cbContext.jvm->AttachCurrentThread(&env, nullptr) != 0) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to attach current thread"); + return; + } + } else if (JNIStatus == JNI_EVERSION) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Unsupported version"); + return; + } + if (env != nullptr && instance->cbObject != nullptr) { + env->CallVoidMethod(instance->cbObject, cbContext.connectionCallbackMethodID, + (jint)status); + if (env->ExceptionCheck() == true) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to call void method"); + cbContext.jvm->DetachCurrentThread(); + return; + } + } + cbContext.jvm->DetachCurrentThread(); + }, + reinterpret_cast<void *>(instance)); + } catch (std::exception &e) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to set connection callback"); + JNI_LOG(ANDROID_LOG_ERROR, TAG, e.what()); + } +} + +jlong AittNativeInterface::Java_com_samsung_android_aitt_Aitt_initJNI(JNIEnv *env, + jobject jniInterfaceObject, jstring id, jstring ip, jboolean clearSession) +{ + if (env == nullptr || jniInterfaceObject == nullptr) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Env or Jobject is null"); + return JNI_ERR; + } + std::string mqId = GetStringUTF(env, id); + if (mqId.empty()) { + return 0L; + } + + std::string selfIp = GetStringUTF(env, ip); + if (selfIp.empty()) { + return 0L; + } + + bool _clearSession = clearSession; + + AittNativeInterface *instance; + try { + instance = new AittNativeInterface(mqId, selfIp, _clearSession); + } catch (std::exception &e) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Failed to create new instance"); + JNI_LOG(ANDROID_LOG_ERROR, TAG, e.what()); + return 0L; + } + + if (env->GetJavaVM(&cbContext.jvm) != JNI_OK) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Unable to get Java VM"); + delete instance; + instance = nullptr; + return 0L; + } + try { + instance->cbObject = env->NewGlobalRef(jniInterfaceObject); + + jclass callbackClass = env->FindClass("com/samsung/android/aitt/Aitt"); + cbContext.messageCallbackMethodID = + env->GetMethodID(callbackClass, "messageCallback", "(Ljava/lang/String;[B)V"); + cbContext.connectionCallbackMethodID = + env->GetMethodID(callbackClass, "connectionStatusCallback", "(I)V"); + env->DeleteLocalRef(callbackClass); + } catch (std::exception &e) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, e.what()); + delete instance; + instance = nullptr; + return 0L; + } + + return reinterpret_cast<long>(instance); +} + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) +{ + JNIEnv *env = nullptr; + if (vm->GetEnv((void **)(&env), JNI_VERSION_1_6) != JNI_OK) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "Not a valid JNI version"); + return JNI_ERR; + } + jclass klass = env->FindClass("com/samsung/android/aitt/Aitt"); + if (nullptr == klass) { + JNI_LOG(ANDROID_LOG_ERROR, TAG, "klass is null"); + return JNI_ERR; + } + static JNINativeMethod aitt_jni_methods[] = { + {"initJNI", "(Ljava/lang/String;Ljava/lang/String;Z)J", + reinterpret_cast<void *>( + AittNativeInterface::Java_com_samsung_android_aitt_Aitt_initJNI)}, + {"connectJNI", "(JLjava/lang/String;I)V", + reinterpret_cast<void *>( + AittNativeInterface::Java_com_samsung_android_aitt_Aitt_connectJNI)}, + {"subscribeJNI", "(JLjava/lang/String;II)J", + reinterpret_cast<void *>( + AittNativeInterface::Java_com_samsung_android_aitt_Aitt_subscribeJNI)}, + {"publishJNI", "(JLjava/lang/String;[BJIIZ)V", + reinterpret_cast<void *>( + AittNativeInterface::Java_com_samsung_android_aitt_Aitt_publishJNI)}, + {"unsubscribeJNI", "(JJ)V", + reinterpret_cast<void *>( + AittNativeInterface::Java_com_samsung_android_aitt_Aitt_unsubscribeJNI)}, + {"disconnectJNI", "(J)V", + reinterpret_cast<void *>( + AittNativeInterface::Java_com_samsung_android_aitt_Aitt_disconnectJNI)}, + {"setConnectionCallbackJNI", "(J)V", + reinterpret_cast<void *>( + AittNativeInterface::Java_com_samsung_android_aitt_Aitt_setConnectionCallbackJNI)}}; + if (env->RegisterNatives(klass, aitt_jni_methods, + sizeof(aitt_jni_methods) / sizeof(aitt_jni_methods[0]))) { + env->DeleteLocalRef(klass); + return JNI_ERR; + } + env->DeleteLocalRef(klass); + JNI_LOG(ANDROID_LOG_INFO, TAG, "JNI loaded successfully"); + return JNI_VERSION_1_6; +} diff --git a/android/aitt/src/main/jni/aitt_jni.h b/android/aitt/src/main/jni/aitt_jni.h new file mode 100644 index 0000000..69e3262 --- /dev/null +++ b/android/aitt/src/main/jni/aitt_jni.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <AITT.h> +#include <android/log.h> +#include <jni.h> +#include <string> + +#define TAG "AITT_ANDROID_JNI" +#define JNI_LOG(a, b, c) __android_log_write(a, b, c) + +using AITT = aitt::AITT; + +class AittNativeInterface { + private: + struct CallbackContext { + JavaVM *jvm; + jmethodID messageCallbackMethodID; + jmethodID connectionCallbackMethodID; + }; + + private: + AittNativeInterface(std::string &mqId, std::string &ip, bool clearSession); + virtual ~AittNativeInterface(void); + static std::string GetStringUTF(JNIEnv *env, jstring str); + + public: + static jlong Java_com_samsung_android_aitt_Aitt_initJNI(JNIEnv *env, jobject jniInterfaceObject, + jstring id, jstring ip, jboolean clearSession); + static void Java_com_samsung_android_aitt_Aitt_connectJNI(JNIEnv *env, jobject jniInterfaceObject, jlong handle, + jstring host, jint port); + static jlong Java_com_samsung_android_aitt_Aitt_subscribeJNI(JNIEnv *env, jobject jniInterfaceObject, jlong handle, + jstring topic, jint protocol, jint qos); + static void Java_com_samsung_android_aitt_Aitt_publishJNI(JNIEnv *env, jobject jniInterfaceObject, jlong handle, + jstring topic, jbyteArray data, jlong datalen, jint protocol, + jint qos, jboolean retain); + static void Java_com_samsung_android_aitt_Aitt_unsubscribeJNI(JNIEnv *env, jobject jniInterfaceObject, jlong handle, + jlong aittSubId); + static void Java_com_samsung_android_aitt_Aitt_disconnectJNI(JNIEnv *env, jobject jniInterfaceObject, jlong handle); + static void Java_com_samsung_android_aitt_Aitt_setConnectionCallbackJNI(JNIEnv *env, jobject jniInterfaceObject, jlong handle); + + private: + AITT aitt; + jobject cbObject; + static CallbackContext cbContext; +}; diff --git a/android/aitt/src/test/java/com/samsung/android/aitt/AittMessageUnitTest.java b/android/aitt/src/test/java/com/samsung/android/aitt/AittMessageUnitTest.java new file mode 100644 index 0000000..abbb0a8 --- /dev/null +++ b/android/aitt/src/test/java/com/samsung/android/aitt/AittMessageUnitTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.samsung.android.aitt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +public class AittMessageUnitTest { + + private final String topic = "aittMessage/topic"; + private final String correlation = "correlation"; + private final String replyTopic = "aittMessage/replyTopic"; + private final int sequence = 007; + private final boolean endSequence = true; + private final String message = "Aitt Message"; + + @Test + public void testAittMessageInitialize_P01(){ + AittMessage aittMessage = new AittMessage(); + assertNotNull("Not null AittMessage Object", aittMessage); + } + + @Test + public void testAittMessageInitializePayload_P02(){ + byte[] payload = message.getBytes(); + AittMessage aittMessage = new AittMessage(payload); + assertNotNull("Not null AittMessage Object", aittMessage); + } + + @Test(expected = NullPointerException.class) + public void testAittMessageInitializeInvalidPayload_N01() throws NullPointerException{ + byte[] payload = null; + AittMessage aittMessage = new AittMessage(payload); + assertNull("Null AittMessage Object", aittMessage); + } + + @Test + public void testTopic_P03(){ + byte[] payload = message.getBytes(); + AittMessage aittMessage = new AittMessage(payload); + aittMessage.setTopic(topic); + String newTopic = aittMessage.getTopic(); + assertEquals("Received topic and set topic are equal", topic, newTopic); + } + + @Test + public void testCorrelation_P04(){ + byte[] payload = message.getBytes(); + AittMessage aittMessage = new AittMessage(payload); + aittMessage.setCorrelation(correlation); + String newCorrelation = aittMessage.getCorrelation(); + assertEquals("Received correlation and set correlation are equal", correlation, newCorrelation); + } + + @Test + public void testReplyTopic_P05(){ + byte[] payload = message.getBytes(); + AittMessage aittMessage = new AittMessage(payload); + aittMessage.setReplyTopic(replyTopic); + String newReplyTopic = aittMessage.getReplyTopic(); + assertEquals("Received replyTopic and set replyTopic are equal", replyTopic, newReplyTopic); + } + + @Test + public void testSequence_P06(){ + byte[] payload = message.getBytes(); + AittMessage aittMessage = new AittMessage(payload); + aittMessage.setSequence(sequence); + aittMessage.increaseSequence(); + int newSequence = aittMessage.getSequence(); + assertEquals("Received sequence and set sequence are equal", sequence+1, newSequence); + } + + @Test + public void testEndSequence_P07(){ + byte[] payload = message.getBytes(); + AittMessage aittMessage = new AittMessage(payload); + aittMessage.setEndSequence(endSequence); + boolean bool = aittMessage.isEndSequence(); + assertEquals("Received endSequence and set endSequence are equal", endSequence, bool); + } + + @Test + public void testPayload_P08(){ + AittMessage aittMessage = new AittMessage(); + byte[] payload = message.getBytes(); + aittMessage.setPayload(payload); + byte[] newPayload = aittMessage.getPayload(); + assertEquals("Received payload and set payload are equal", payload, newPayload); + } + + @Test(expected = NullPointerException.class) + public void TestInvalidPayload_N02() throws NullPointerException { + AittMessage aittMessage = new AittMessage(); + byte[] payload = null; + aittMessage.setPayload(payload); + } + + @Test + public void testClearPayload_P09(){ + byte[] payload = message.getBytes(); + AittMessage aittMessage = new AittMessage(payload); + aittMessage.clearPayload(); + byte[] newPayload = aittMessage.getPayload(); + assertEquals("Received payload and expected payload are equal", 0, newPayload.length); + } +} diff --git a/android/aitt/src/test/java/com/samsung/android/aitt/AittUnitTest.java b/android/aitt/src/test/java/com/samsung/android/aitt/AittUnitTest.java new file mode 100644 index 0000000..a7d1789 --- /dev/null +++ b/android/aitt/src/test/java/com/samsung/android/aitt/AittUnitTest.java @@ -0,0 +1,795 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.samsung.android.aitt; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import android.content.Context; + +import com.google.flatbuffers.FlexBuffersBuilder; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.api.support.membermodification.MemberMatcher; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.EnumSet; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(Aitt.class) +public class AittUnitTest { + @Mock + private final Context appContext = mock(Context.class); + + private static final String JOIN_NETWORK = "connected"; + private static final String WILL_LEAVE_NETWORK = "disconnected"; + private static final String JAVA_SPECIFIC_DISCOVERY_TOPIC = "/java/aitt/discovery/"; + private static final String AITT_LOCALHOST = "127.0.0.1"; + private static final int DISCOVERY_MESSAGES_COUNT = 6; + private final String brokerIp = "192.168.0.1"; + private final int port = 1803; + private final String topic = "aitt/test"; + private final String message = "test message"; + private final String aittId = "aitt"; + + private Method messageCallbackMethod; + + @Before + public void initialize() { + try { + PowerMockito.replace(MemberMatcher.method(Aitt.class, "initJNI")).with(new InvocationHandler() { + @Override + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + return 1L; + } + }); + PowerMockito.replace(MemberMatcher.method(Aitt.class, "connectJNI")).with(new InvocationHandler() { + @Override + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + return null; + } + }); + PowerMockito.replace(MemberMatcher.method(Aitt.class, "disconnectJNI")).with(new InvocationHandler() { + @Override + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + return null; + } + }); + PowerMockito.replace(MemberMatcher.method(Aitt.class, "setConnectionCallbackJNI")).with(new InvocationHandler() { + @Override + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + return null; + } + }); + PowerMockito.replace(MemberMatcher.method(Aitt.class, "publishJNI")).with(new InvocationHandler() { + @Override + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + return null; + } + }); + PowerMockito.replace(MemberMatcher.method(Aitt.class, "subscribeJNI")).with(new InvocationHandler() { + @Override + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + return 1L; + } + }); + PowerMockito.replace(MemberMatcher.method(Aitt.class, "unsubscribeJNI")).with(new InvocationHandler() { + @Override + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + return null; + } + }); + + messageCallbackMethod = Aitt.class.getDeclaredMethod("messageCallback", String.class, byte[].class); + messageCallbackMethod.setAccessible(true); + } catch(Exception e) { + fail("Failed to mock Aitt " + e); + } + } + + private byte[] createDiscoveryMessage(int count) { + int start; + FlexBuffersBuilder builder = new FlexBuffersBuilder(ByteBuffer.allocate(512)); + start = builder.startMap(); + switch (count) { + case 1: + /* + * { + * "status": "connected", + * "host": "127.0.0.1", + * "aitt/topic1": { + * "protocol": TCP, + * "port": 1000, + * } + * } + */ + builder.putString("status", JOIN_NETWORK); + builder.putString("host", AITT_LOCALHOST); + int secondStart = builder.startMap(); + builder.putInt("port", 1000); + builder.putInt("protocol", Aitt.Protocol.TCP.getValue()); + builder.endMap("aitt/topic1", secondStart); + break; + case 2: + /* + * { + * "status": "connected", + * "host": "127.0.0.2", + * "aitt/topic1": { + * "protocol": MQTT, + * "port": 2000, + * } + * } + */ + builder.putString("status", JOIN_NETWORK); + builder.putString("host", "127.0.0.2"); + secondStart = builder.startMap(); + builder.putInt("port", 2000); + builder.putInt("protocol", Aitt.Protocol.MQTT.getValue()); + builder.endMap("aitt/topic1", secondStart); + break; + case 3: + /* + * { + * "status": "connected", + * "host": "127.0.0.1", + * "aitt/topic2": { + * "protocol": MQTT, + * "port": 2000, + * } + * } + */ + builder.putString("status", JOIN_NETWORK); + builder.putString("host",AITT_LOCALHOST); + secondStart = builder.startMap(); + builder.putInt("port", 2000); + builder.putInt("protocol", Aitt.Protocol.MQTT.getValue()); + builder.endMap("aitt/topic2", secondStart); + break; + case 4: + /* + * { + * "status": "connected", + * "host": "127.0.0.1", + * "aitt/topic2": { + * "protocol": TCP, + * "port": 4000, + * } + * } + */ + builder.putString("status", JOIN_NETWORK); + builder.putString("host",AITT_LOCALHOST); + secondStart = builder.startMap(); + builder.putInt("port", 4000); + builder.putInt("protocol", Aitt.Protocol.TCP.getValue()); + builder.endMap("aitt/topic2", secondStart); + break; + case 5: + /* + * { + * "status": "connected", + * "host": "127.0.0.1", + * "aitt/topic2": { + * "protocol": WEBRTC, + * "port": 2000, + * } + * } + */ + builder.putString("status", JOIN_NETWORK); + builder.putString("host",AITT_LOCALHOST); + secondStart = builder.startMap(); + builder.putInt("port", 2000); + builder.putInt("protocol", Aitt.Protocol.WEBRTC.getValue()); + builder.endMap("aitt/topic2", secondStart); + break; + case 6: + /* + * { + * "status": "disconnected", + * "host": "127.0.0.1", + * "aitt/topic1": { + * "protocol": TCP, + * "port": 1000, + * } + * } + */ + builder.putString("status", WILL_LEAVE_NETWORK); + builder.putString("host",AITT_LOCALHOST); + secondStart = builder.startMap(); + builder.putInt("port", 1000); + builder.putInt("protocol", Aitt.Protocol.TCP.getValue()); + builder.endMap("aitt/topic1", secondStart); + break; + default: + return null; + } + builder.endMap(null, start); + ByteBuffer bb = builder.finish(); + byte[] array = new byte[bb.remaining()]; + bb.get(array,0,array.length); + return array; + } + + @Test + public void testAittConstructor_P(){ + String id = "aitt"; + try { + Aitt aitt = new Aitt(appContext, id); + assertNotNull("Aitt Instance not null", aitt); + } catch(Exception e) { + fail("Failed testInitialize " + e); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testInitializeInvalidId_N() { + String _id = ""; + try { + Aitt aitt = new Aitt(appContext, _id); + aitt.close(); + } catch(InstantiationException e) { + fail("Error during testInitializeInvalidId" + e); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testInitializeInvalidContext_N() { + String _id = ""; + try { + Aitt aitt = new Aitt(null, _id); + aitt.close(); + } catch(InstantiationException e) { + fail("Error during testInitializeInvalidContext" + e); + } + } + + @Test(expected = InstantiationException.class) + public void testConstructorFail_N() throws InstantiationException { + try{ + PowerMockito.replace(MemberMatcher.method(Aitt.class, "initJNI")).with(new InvocationHandler() { + @Override + public Object invoke(Object o, Method method, Object[] objects) throws Throwable { + return 0L; + } + }); + } catch(Exception e) { + fail("Failed to replace method" + e); + } + String id = "aitt"; + Aitt aitt = new Aitt(appContext,id); + aitt.close(); + } + + @Test + public void testConnect_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + aitt.close(); + } catch(Exception e) { + fail("Failed testConnect " + e); + } + } + + @Test + public void testConnectWithoutIP_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(null); + + aitt.close(); + } catch(Exception e) { + fail("Failed testConnectWithoutIP " + e); + } + } + + @Test + public void testDisconnect_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testDisconnect " + e); + } + } + + @Test + public void testPublishMqtt_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + byte[] payload = message.getBytes(); + aitt.publish(topic, payload); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testPublishMqtt " + e); + } + } + + @Test + public void testPublishWebRTC_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + byte[] payload = message.getBytes(); + aitt.publish(topic, payload, Aitt.Protocol.WEBRTC, Aitt.QoS.AT_MOST_ONCE, false); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testPublishWebRTC " + e); + } + } + + @Test + public void testPublishInvalidTopic_N(){ + try { + Aitt aitt = new Aitt(appContext, aittId); + aitt.connect(brokerIp, port); + String _topic = ""; + byte[] payload = message.getBytes(); + + assertThrows(IllegalArgumentException.class, () -> { + aitt.publish(_topic, payload); + }); + + aitt.disconnect(); + } catch(Exception e){ + fail("Failed testPublishInvalidTopic" + e); + } + } + + @Test + public void testPublishAnyProtocol_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + byte[] payload = message.getBytes(); + aitt.publish(topic, payload, Aitt.Protocol.TCP, Aitt.QoS.AT_LEAST_ONCE, false); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testPublishAnyProtocol " + e); + } + } + + @Test + public void testPublishProtocolSet_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + byte[] payload = message.getBytes(); + EnumSet<Aitt.Protocol> protocols = EnumSet.of(Aitt.Protocol.MQTT, Aitt.Protocol.TCP); + aitt.publish(topic, payload, protocols, Aitt.QoS.AT_MOST_ONCE, false); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testPublishProtocolSet " + e); + } + } + + @Test + public void testPublishInvalidProtocol_N(){ + try{ + Aitt aitt = new Aitt(appContext, aittId); + aitt.connect(brokerIp,port); + byte[] payload = message.getBytes(); + EnumSet<Aitt.Protocol> protocols = EnumSet.noneOf(Aitt.Protocol.class); + + assertThrows(IllegalArgumentException.class, () -> { + aitt.publish(topic, payload, protocols, Aitt.QoS.AT_MOST_ONCE, false); + }); + + aitt.disconnect(); + } catch(Exception e){ + fail("Failed testPublishInvalidProtocol" + e); + } + } + + @Test + public void testSubscribeMqtt_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + aitt.subscribe(topic, new Aitt.SubscribeCallback() { + @Override + public void onMessageReceived(AittMessage message) { + String _topic = message.getTopic(); + byte[] payload = message.getPayload(); + } + }); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testSubscribeMqtt " + e); + } + } + + @Test + public void testSubscribeWebRTC_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + aitt.subscribe(topic, new Aitt.SubscribeCallback() { + @Override + public void onMessageReceived(AittMessage message) { + String _topic = message.getTopic(); + byte[] payload = message.getPayload(); + } + }, + Aitt.Protocol.WEBRTC, Aitt.QoS.AT_MOST_ONCE); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testSubscribeWebRTC " + e); + } + } + + + @Test + public void testSubscribeInvalidTopic_N() { + + try{ + Aitt aitt = new Aitt(appContext, aittId); + aitt.connect(brokerIp, port); + + String _topic = ""; + + assertThrows(IllegalArgumentException.class, () -> { + aitt.subscribe(_topic, new Aitt.SubscribeCallback() { + @Override + public void onMessageReceived(AittMessage message) { + } + }); + }); + + aitt.disconnect(); + } catch(Exception e){ + fail("Failed testSubscribeInvalidTopic " + e); + } + } + + @Test + public void testSubscribeInvalidCallback_N() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + aitt.connect(brokerIp, port); + + String _topic = "topic"; + + assertThrows(IllegalArgumentException.class, () -> { + aitt.subscribe(_topic, null); + }); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testSubscribeInvalidCallback " + e); + } + } + + @Test + public void testSubscribeAnyProtocol_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + aitt.subscribe(topic, new Aitt.SubscribeCallback() { + @Override + public void onMessageReceived(AittMessage message) { + String _topic = message.getTopic(); + byte[] payload = message.getPayload(); + } + }, + Aitt.Protocol.UDP, Aitt.QoS.AT_MOST_ONCE); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testSubscribeAnyProtocol " + e); + } + } + + @Test + public void testSubscribeInvalidProtocol_N() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + aitt.connect(brokerIp, port); + EnumSet<Aitt.Protocol> protocols = EnumSet.noneOf(Aitt.Protocol.class); + + assertThrows(IllegalArgumentException.class, () -> { + aitt.subscribe(topic, new Aitt.SubscribeCallback() { + @Override + public void onMessageReceived(AittMessage message) { + String _topic = message.getTopic(); + byte[] payload = message.getPayload(); + } + }, + protocols, Aitt.QoS.AT_MOST_ONCE); + }); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testSubscribeAnyProtocol " + e); + } + } + + @Test + public void testSubscribeProtocolSet_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + EnumSet<Aitt.Protocol> protocols = EnumSet.of(Aitt.Protocol.MQTT, Aitt.Protocol.TCP); + aitt.subscribe(topic, new Aitt.SubscribeCallback() { + @Override + public void onMessageReceived(AittMessage message) { + String _topic = message.getTopic(); + byte[] payload = message.getPayload(); + } + }, + protocols, Aitt.QoS.EXACTLY_ONCE); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testSubscribeProtocolSet " + e); + } + } + + @Test + public void testUnsubscribe_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + aitt.subscribe(topic, new Aitt.SubscribeCallback() { + @Override + public void onMessageReceived(AittMessage message) { + } + }); + + aitt.unsubscribe(topic); + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testUnsubscribe " + e); + } + } + + @Test + public void testUnsubscribeInvalidTopic_N() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + aitt.connect(brokerIp, port); + String _topic = ""; + + assertThrows(IllegalArgumentException.class, () -> { + aitt.unsubscribe(_topic); + }); + + aitt.disconnect(); + } catch(Exception e){ + fail("Failed testUnsubscribeInvalidTopic " + e); + } + } + + @Test + public void testSetConnectionCallback_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.setConnectionCallback(new Aitt.ConnectionCallback() { + @Override + public void onConnected() {} + + @Override + public void onDisconnected() {} + }); + aitt.connect(brokerIp, port); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testSetConnectionCallback " + e); + } + } + + @Test + public void testSetConnectionCallbackInvalidCallback_N() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertThrows(IllegalArgumentException.class, () -> { + aitt.setConnectionCallback(null); + }); + + aitt.connect(brokerIp, port); + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testSetConnectionCallbackInvalidCallback " + e); + } + } + + @Test + public void testSubscribeMultipleCallbacks_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + Aitt.SubscribeCallback callback1 = message -> {}; + + Aitt.SubscribeCallback callback2 = message -> {}; + + aitt.subscribe(topic, callback1); + aitt.subscribe(topic, callback2); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testSubscribeMultipleCallbacks " + e); + } + } + + // The test covers different cases of updating the publish table + @Test + public void testDiscoveryMessageCallbackConnected_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + int counter = 1; + while (counter < DISCOVERY_MESSAGES_COUNT) { + byte[] discoveryMessage = createDiscoveryMessage(counter); + messageCallbackMethod.invoke(aitt, JAVA_SPECIFIC_DISCOVERY_TOPIC, (Object) discoveryMessage); + counter++; + } + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testDiscoveryMessageCallback " + e); + } + } + + @Test + public void testDiscoveryMessageCallbackDisconnected_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + int counter = 1; + byte[] discoveryMessage = createDiscoveryMessage(counter); + messageCallbackMethod.invoke(aitt, JAVA_SPECIFIC_DISCOVERY_TOPIC, (Object) discoveryMessage); + + counter = 6; + byte[] disconnectMessage = createDiscoveryMessage(counter); + messageCallbackMethod.invoke(aitt, JAVA_SPECIFIC_DISCOVERY_TOPIC, (Object) disconnectMessage); + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testDiscoveryMessageCallback " + e); + } + } + + @Test + public void testDiscoveryMessageCallbackEmptyPayload_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + + assertNotNull("Aitt Instance not null", aitt); + aitt.connect(brokerIp, port); + + byte[] discoveryMessage = new byte[0]; + messageCallbackMethod.invoke(aitt, JAVA_SPECIFIC_DISCOVERY_TOPIC, (Object) discoveryMessage); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testDiscoveryMessageCallbackEmptyPayload " + e); + } + } + + @Test + public void testSubscribeCallbackVerifyTopic_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + aitt.connect(brokerIp, port); + + aitt.subscribe(topic, new Aitt.SubscribeCallback() { + @Override + public void onMessageReceived(AittMessage aittMessage) { + String recvTopic = aittMessage.getTopic(); + assertEquals("Received topic and subscribed topic are equal", recvTopic, topic); + } + }); + + messageCallbackMethod.invoke(aitt, topic, message.getBytes(StandardCharsets.UTF_8)); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testSubscribeCallback " + e); + } + } + + @Test + public void testSubscribeCallbackVerifyPayload_P() { + try { + Aitt aitt = new Aitt(appContext, aittId); + aitt.connect(brokerIp, port); + + aitt.subscribe(topic, new Aitt.SubscribeCallback() { + @Override + public void onMessageReceived(AittMessage aittMessage) { + String recvMessage = new String(aittMessage.getPayload(), StandardCharsets.UTF_8); + assertEquals("Received message and sent message are equal", message, recvMessage); + } + }); + + messageCallbackMethod.invoke(aitt, topic, message.getBytes(StandardCharsets.UTF_8)); + + aitt.disconnect(); + } catch(Exception e) { + fail("Failed testSubscribeCallback " + e); + } + } +} diff --git a/android/aitt/src/test/resources/robolectric.properties b/android/aitt/src/test/resources/robolectric.properties new file mode 100644 index 0000000..932b01b --- /dev/null +++ b/android/aitt/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=28 diff --git a/android/flatbuffers/build.gradle b/android/flatbuffers/build.gradle new file mode 100644 index 0000000..ccd0ce7 --- /dev/null +++ b/android/flatbuffers/build.gradle @@ -0,0 +1,79 @@ +plugins { + id 'com.android.library' + id "de.undercouch.download" version "5.0.1" +} + +def thirdPartyDir = new File ("${rootProject.projectDir}/third_party") + +def flatbuffersDir = new File("${thirdPartyDir}/flatbuffers-2.0.0") + +android { + compileSdkVersion 31 + ndkVersion "21.3.6528147" + defaultConfig { + minSdkVersion 28 + targetSdkVersion 31 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + externalNativeBuild { + cmake { + arguments '-DCMAKE_VERBOSE_MAKEFILE=1' + arguments '-DCMAKE_INSTALL_PREFIX:PATH=/usr' + arguments '-DANDROID_STL=c++_shared' + arguments "-DANDROID_NDK_HOME=${System.env.ANDROID_NDK_ROOT}" + arguments '-DFLATBUFFERS_BUILD_TESTS=OFF' + arguments '-DFLATBUFFERS_BUILD_FLATC=OFF' + arguments '-DFLATBUFFERS_BUILD_FLATHASH=OFF' + arguments '-DFLATBUFFERS_BUILD_CPP17=ON' + arguments '-DFLATBUFFERS_INSTALL=OFF' + cppFlags "-std=c++17" + abiFilters 'arm64-v8a', 'x86' + targets "flatbuffers" + } + } + } + buildFeatures { + prefab false + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + externalNativeBuild { + cmake { + path "${flatbuffersDir}/CMakeLists.txt" + } + } +} + +dependencies { + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} + +task downloadFlatBuffers(type: Download) { + doFirst { + println("Downloading FlatBuffers") + } + src "https://github.com/google/flatbuffers/archive/refs/tags/v2.0.0.zip" + dest new File(thirdPartyDir, "flatbuffers.zip") + onlyIfModified true +} + +task unzipFlatBuffers(type: Copy, dependsOn: downloadFlatBuffers) { + doFirst { + println("Unzipping FlatBuffers") + } + from zipTree(downloadFlatBuffers.dest) + into thirdPartyDir + onlyIf { !flatbuffersDir.exists() } +} + +task wrapper(type: Wrapper) { + gradleVersion = '4.1' +} + +preBuild.dependsOn(unzipFlatBuffers) diff --git a/android/flatbuffers/proguard-rules.pro b/android/flatbuffers/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/android/flatbuffers/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/flatbuffers/src/main/AndroidManifest.xml b/android/flatbuffers/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f4119ba --- /dev/null +++ b/android/flatbuffers/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.samsung.android.flatbuffers"> +</manifest> diff --git a/android/modules/webrtc/.gitignore b/android/modules/webrtc/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/android/modules/webrtc/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android/modules/webrtc/build.gradle b/android/modules/webrtc/build.gradle new file mode 100644 index 0000000..9764253 --- /dev/null +++ b/android/modules/webrtc/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'com.android.library' +} + +android { + compileSdkVersion 31 + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 31 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.android.material:material:1.5.0' + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' + implementation 'org.webrtc:google-webrtc:1.0.32006' +} + +task wrapper(type: Wrapper) { + gradleVersion = '4.1' +} diff --git a/android/modules/webrtc/proguard-rules.pro b/android/modules/webrtc/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/android/modules/webrtc/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/modules/webrtc/src/main/AndroidManifest.xml b/android/modules/webrtc/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7b7ae3f --- /dev/null +++ b/android/modules/webrtc/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.samsung.android.modules.webrtc"> + + <uses-feature android:glEsVersion="0x00020000" android:required="true" /> + <uses-permission android:name="android.permission.INTERNET" /> + +</manifest> diff --git a/android/modules/webrtc/src/main/java/com/samsung/android/modules/webrtc/WebRTC.java b/android/modules/webrtc/src/main/java/com/samsung/android/modules/webrtc/WebRTC.java new file mode 100644 index 0000000..ebf6c6e --- /dev/null +++ b/android/modules/webrtc/src/main/java/com/samsung/android/modules/webrtc/WebRTC.java @@ -0,0 +1,698 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.samsung.android.modules.webrtc; + +import android.content.Context; +import android.os.SystemClock; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; +import org.webrtc.CapturerObserver; +import org.webrtc.DataChannel; +import org.webrtc.DefaultVideoDecoderFactory; +import org.webrtc.DefaultVideoEncoderFactory; +import org.webrtc.EglBase; +import org.webrtc.IceCandidate; +import org.webrtc.MediaConstraints; +import org.webrtc.MediaStream; +import org.webrtc.MediaStreamTrack; +import org.webrtc.NV21Buffer; +import org.webrtc.PeerConnection; +import org.webrtc.PeerConnectionFactory; +import org.webrtc.RtpReceiver; +import org.webrtc.SdpObserver; +import org.webrtc.SessionDescription; +import org.webrtc.SurfaceTextureHelper; +import org.webrtc.VideoCapturer; +import org.webrtc.VideoDecoderFactory; +import org.webrtc.VideoEncoderFactory; +import org.webrtc.VideoFrame; +import org.webrtc.VideoSink; +import org.webrtc.VideoSource; +import org.webrtc.VideoTrack; +import static org.webrtc.SessionDescription.Type.ANSWER; +import static org.webrtc.SessionDescription.Type.OFFER; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +/** + * WebRTC class to implement webRTC functionalities + */ +public class WebRTC { + private static final String TAG = "WebRTC"; + public static final String VIDEO_TRACK_ID = "ARDAMSv0"; + private static final String CANDIDATE = "candidate"; + private java.net.Socket socket; + private boolean isInitiator; + private boolean isChannelReady; + private boolean isStarted; + private boolean isReciever; + private PeerConnection peerConnection; + private PeerConnectionFactory factory; + private VideoTrack videoTrackFromSource; + private ObjectOutputStream outStream; + private ObjectInputStream inputStream; + private SDPThread sdpThread; + private Context appContext; + private DataChannel localDataChannel; + private FrameVideoCapturer videoCapturer; + private ReceiveDataCallback dataCallback; + private String recieverIP; + private Integer recieverPort; + + /** + * WebRTC channels supported - Media channel, data channel + */ + public enum DataType{ + MESSAGE, + VIDEOFRAME, + } + + /** + * WebRTC constructor to create webRTC instance + * @param dataType To decide webRTC channel type + * @param appContext Application context creating webRTC instance + */ + public WebRTC(DataType dataType , Context appContext) { + this.appContext = appContext; + this.isReciever = false; + } + + /** + * WebRTC constructor to create webRTC instance + * @param dataType To decide webRTC channel type + * @param appContext Application context creating webRTC instance + * @param socket Java server socket for webrtc signalling + */ + WebRTC(DataType dataType , Context appContext , Socket socket) { + Log.d(TAG , "InWebRTC Constructor"); + this.appContext = appContext; + this.socket = socket; + this.isReciever = true; + } + + /** + * To create data call-back mechanism + * @param cb aitt callback registered to receive a webrtc data + */ + public void registerDataCallback(ReceiveDataCallback cb){ + this.dataCallback = cb; + } + + /** + * Method to disconnect the connection from peer + */ + public void disconnect() { + if (sdpThread != null) { + sdpThread.stop(); + } + + if (socket != null) { + new Thread(() -> { + try { + sendMessage(false, "bye"); + socket.close(); + if (outStream != null) { + outStream.close(); + } + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + Log.e(TAG, "Error during disconnect", e); + } + }).start(); + } + } + + /** + * Method to establish a socket connection with peer node + */ + public void connect() { + initialize(); + } + + /** + * Method to establish communication with peer node + * @param recieverIP IP Address of the destination(peer) node + * @param recieverPort Port number of the destination(peer) node + */ + public void connect(String recieverIP , Integer recieverPort){ + this.recieverIP = recieverIP; + this.recieverPort = recieverPort; + initialize(); + } + + /** + * Method to initialize webRTC APIs while establishing connection + */ + private void initialize(){ + initializePeerConnectionFactory(); + initializePeerConnections(); + if(!isReciever){ + createVideoTrack(); + addVideoTrack(); + } + isInitiator = isReciever; + + sdpThread = new SDPThread(); + new Thread(sdpThread).start(); + } + + /** + * Method to create webRTC offer for sdp negotiation + */ + private void doCall() { + MediaConstraints sdpMediaConstraints = new MediaConstraints(); + sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true")); + + peerConnection.createOffer(new SimpleSdpObserver() { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + Log.d(TAG, "onCreateSuccess: "); + peerConnection.setLocalDescription(new SimpleSdpObserver(), sessionDescription); + JSONObject message = new JSONObject(); + try { + message.put("type", "offer"); + message.put("sdp", sessionDescription.description); + sendMessage(true , message); + } catch (JSONException | IOException e) { + Log.e(TAG, "Error during create offer", e); + } + } + }, sdpMediaConstraints); + } + + /** + * Method to send signalling messages over socket connection + * @param isJSON Boolean to check if message is JSON + * @param message Data to be sent over webRTC connection + * @throws IOException Throws IOException if writing to outStream fails + */ + private void sendMessage(boolean isJSON, Object message) throws IOException { + Log.d(TAG, message.toString()); + if (outStream != null) { + if (isJSON) { + outStream.writeObject(new Packet((JSONObject) message)); + } else { + outStream.writeObject(new Packet((String) message)); + } + } + } + + /** + * Class to create proxy video sink + */ + private static class ProxyVideoSink implements VideoSink { + + private ReceiveDataCallback dataCallback; + + /** + * ProxyVideoSink constructor to create its instance + * @param dataCb DataCall back to be set to self-object + */ + ProxyVideoSink(ReceiveDataCallback dataCb){ + this.dataCallback = dataCb; + } + + /** + * Method to send data through data call back + * @param frame VideoFrame to be transferred using media channel + */ + @Override + synchronized public void onFrame(VideoFrame frame) { + byte[] rawFrame = createNV21Data(frame.getBuffer().toI420()); + dataCallback.pushData(rawFrame); + } + + /** + * Method used to convert VideoFrame to NV21 data format + * @param i420Buffer VideoFrame in I420 buffer format + * @return the video frame in NV21 data format + */ + public byte[] createNV21Data(VideoFrame.I420Buffer i420Buffer) { + final int width = i420Buffer.getWidth(); + final int height = i420Buffer.getHeight(); + final int chromaStride = width; + final int chromaWidth = (width + 1) / 2; + final int chromaHeight = (height + 1) / 2; + final int ySize = width * height; + final ByteBuffer nv21Buffer = ByteBuffer.allocateDirect(ySize + chromaStride * chromaHeight); + final byte[] nv21Data = nv21Buffer.array(); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + final byte yValue = i420Buffer.getDataY().get(y * i420Buffer.getStrideY() + x); + nv21Data[y * width + x] = yValue; + } + } + for (int y = 0; y < chromaHeight; ++y) { + for (int x = 0; x < chromaWidth; ++x) { + final byte uValue = i420Buffer.getDataU().get(y * i420Buffer.getStrideU() + x); + final byte vValue = i420Buffer.getDataV().get(y * i420Buffer.getStrideV() + x); + nv21Data[ySize + y * chromaStride + 2 * x + 0] = vValue; + nv21Data[ySize + y * chromaStride + 2 * x + 1] = uValue; + } + } + return nv21Data; + } + } + + /** + * Method to initialize peer connection factory + */ + private void initializePeerConnectionFactory() { + EglBase mRootEglBase; + mRootEglBase = EglBase.create(); + VideoEncoderFactory encoderFactory = new DefaultVideoEncoderFactory(mRootEglBase.getEglBaseContext(), true /* enableIntelVp8Encoder */, true); + VideoDecoderFactory decoderFactory = new DefaultVideoDecoderFactory(mRootEglBase.getEglBaseContext()); + + PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(appContext).setEnableInternalTracer(true).createInitializationOptions()); + PeerConnectionFactory.Builder builder = PeerConnectionFactory.builder().setVideoEncoderFactory(encoderFactory).setVideoDecoderFactory(decoderFactory); + builder.setOptions(null); + factory = builder.createPeerConnectionFactory(); + } + + /** + * Method to create video track + */ + private void createVideoTrack(){ + videoCapturer = new FrameVideoCapturer(); + VideoSource videoSource = factory.createVideoSource(false); + videoCapturer.initialize(null , null ,videoSource.getCapturerObserver()); + videoTrackFromSource = factory.createVideoTrack(VIDEO_TRACK_ID, videoSource); + videoTrackFromSource.setEnabled(true); + } + + /** + * Method to initialize peer connections + */ + private void initializePeerConnections() { + peerConnection = createPeerConnection(factory); + if (peerConnection != null) { + localDataChannel = peerConnection.createDataChannel("sendDataChannel", new DataChannel.Init()); + } + } + + /** + * Method to add video track + */ + private void addVideoTrack() { + MediaStream mediaStream = factory.createLocalMediaStream("ARDAMS"); + mediaStream.addTrack(videoTrackFromSource); + if(peerConnection!=null){ + peerConnection.addStream(mediaStream); + } + } + + /** + * Method to create peer connection + * @param factory Peer connection factory object + * @return return factory object + */ + private PeerConnection createPeerConnection(PeerConnectionFactory factory) { + PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(new ArrayList<>()); + MediaConstraints pcConstraints = new MediaConstraints(); + + PeerConnection.Observer pcObserver = new PeerConnection.Observer() { + @Override + public void onSignalingChange(PeerConnection.SignalingState signalingState) { + Log.d(TAG, "onSignalingChange: "); + } + + @Override + public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { + Log.d(TAG, "onIceConnectionChange: "); + } + + @Override + public void onIceConnectionReceivingChange(boolean b) { + Log.d(TAG, "onIceConnectionReceivingChange: "); + } + + @Override + public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { + Log.d(TAG, "onIceGatheringChange: "); + } + + @Override + public void onIceCandidate(IceCandidate iceCandidate) { + Log.d(TAG, "onIceCandidate: "); + JSONObject message = new JSONObject(); + try { + message.put("type", CANDIDATE); + message.put("label", iceCandidate.sdpMLineIndex); + message.put("id", iceCandidate.sdpMid); + message.put(CANDIDATE, iceCandidate.sdp); + Log.d(TAG, "onIceCandidate: sending candidate " + message); + sendMessage(true , message); + } catch (JSONException | IOException e) { + Log.e(TAG, "Error during onIceCandidate", e); + } + } + + @Override + public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { + Log.d(TAG, "onIceCandidatesRemoved: "); + } + + @Override + public void onAddStream(MediaStream mediaStream) { + Log.d(TAG, "onAddStream: " + mediaStream.videoTracks.size()); + VideoTrack remoteVideoTrack = mediaStream.videoTracks.get(0); + remoteVideoTrack.setEnabled(true); + } + + @Override + public void onRemoveStream(MediaStream mediaStream) { + Log.d(TAG, "onRemoveStream: "); + } + + @Override + public void onDataChannel(DataChannel dataChannel) { + Log.d(TAG, "onDataChannel: "); + dataChannel.registerObserver(new DataChannel.Observer() { + @Override + public void onBufferedAmountChange(long l) { + //Keep this callback for future usage + Log.d(TAG, "onBufferedAmountChange:"); + } + + @Override + public void onStateChange() { + Log.d(TAG, "onStateChange: remote data channel state: " + dataChannel.state().toString()); + } + + @Override + public void onMessage(DataChannel.Buffer buffer) { + Log.d(TAG, "onMessage: got message"); + dataCallback.pushData(readIncomingMessage(buffer.data)); + } + }); + } + + @Override + public void onRenegotiationNeeded() { + Log.d(TAG, "onRenegotiationNeeded: "); + } + + @Override + public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) { + MediaStreamTrack track = rtpReceiver.track(); + if (track instanceof VideoTrack && isReciever) { + Log.i(TAG, "onAddVideoTrack"); + VideoTrack remoteVideoTrack = (VideoTrack) track; + remoteVideoTrack.setEnabled(true); + ProxyVideoSink videoSink = new ProxyVideoSink(dataCallback); + remoteVideoTrack.addSink(videoSink); + } + } + }; + return factory.createPeerConnection(rtcConfig, pcConstraints, pcObserver); + } + + /** + * Method used to send video data + * @param frame Video frame in byte format + * @param width width of the video frame + * @param height height of the video frame + */ + public void sendVideoData(byte[] frame , int width , int height){ + videoCapturer.send(frame , width , height); + } + + /** + * Method to send message data + * @param message message to be sent in byte format + */ + public void sendMessageData(byte[] message) { + ByteBuffer data = ByteBuffer.wrap(message); + localDataChannel.send(new DataChannel.Buffer(data, false)); + } + + /** + * Interface to create data call back mechanism + */ + public interface ReceiveDataCallback{ + void pushData(byte[] frame); + } + + /** + * Class packet to create a packet + */ + private static class Packet implements Serializable { + boolean isString; + String obj; + Packet(String s){ + isString = true; + obj = s; + } + + Packet(JSONObject json){ + isString = false; + obj = json.toString(); + } + } + + /** + * Method to read incoming message and convert it to byte format + * @param buffer Message incoming in Byte buffer format + * @return returns byteBuffer message in byte format + */ + private byte[] readIncomingMessage(ByteBuffer buffer) { + byte[] bytes; + if (buffer.hasArray()) { + bytes = buffer.array(); + } else { + bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + } + return bytes; + } + + /** + * Class to implement SDP observer + */ + private static class SimpleSdpObserver implements SdpObserver { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + //Required for future reference + } + + @Override + public void onSetSuccess() { + Log.d(TAG, "onSetSuccess:"); + } + + @Override + public void onCreateFailure(String s) { + Log.d(TAG, "onCreateFailure: Reason = " + s); + } + + @Override + public void onSetFailure(String s) { + Log.d(TAG, "onSetFailure: Reason = " + s); + } + } + + /** + * Class to implement Frame video capturer + */ + private static class FrameVideoCapturer implements VideoCapturer { + private CapturerObserver capturerObserver; + + void send(byte[] frame, int width, int height) { + long timestampNS = TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime()); + NV21Buffer buffer = new NV21Buffer(frame, width, height, null); + VideoFrame videoFrame = new VideoFrame(buffer, 0, timestampNS); + this.capturerObserver.onFrameCaptured(videoFrame); + videoFrame.release(); + } + + @Override + public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context context, CapturerObserver capturerObserver) { + this.capturerObserver = capturerObserver; + } + + public void startCapture(int width, int height, int framerate) { + //Required for future reference + } + + public void stopCapture() throws InterruptedException { + //Required for future reference + } + + public void changeCaptureFormat(int width, int height, int framerate) { + //Required for future reference + } + + public void dispose() { + //Required for future reference + } + + public boolean isScreencast() { + return false; + } + } + + /** + * Class to implement SDP thread + */ + private class SDPThread implements Runnable { + private volatile boolean isRunning = true; + + @Override + public void run() { + isChannelReady = true; + + createSocket(); + invokeSendMessage(); + + while (isRunning) { + try { + Packet recvPacketNew = (Packet) inputStream.readObject(); + if (recvPacketNew != null) { + if (recvPacketNew.isString) { + String message = recvPacketNew.obj; + checkPacketMessage(message); + } else { + JSONObject message = new JSONObject(recvPacketNew.obj); + Log.d(TAG, "connectToSignallingServer: got message " + message); + decodeMessage(message); + } + } + } catch (ClassNotFoundException | JSONException | IOException e) { + isRunning = false; + Log.e(TAG, "Error during JSON read", e); + } + } + } + + /** + * Method to decode message + * @param message Message received in JSON object format + */ + private void decodeMessage(JSONObject message) { + try { + if (message.getString("type").equals("offer")) { + Log.d(TAG, "connectToSignallingServer: received an offer " + isInitiator + " " + isStarted); + invokeMaybeStart(); + peerConnection.setRemoteDescription(new SimpleSdpObserver(), new SessionDescription(OFFER, message.getString("sdp"))); + doAnswer(); + } else if (message.getString("type").equals("answer") && isStarted) { + peerConnection.setRemoteDescription(new SimpleSdpObserver(), new SessionDescription(ANSWER, message.getString("sdp"))); + } else if (message.getString("type").equals(CANDIDATE) && isStarted) { + Log.d(TAG, "connectToSignallingServer: receiving candidates"); + IceCandidate candidate = new IceCandidate(message.getString("id"), message.getInt("label"), message.getString(CANDIDATE)); + peerConnection.addIceCandidate(candidate); + } + } catch (JSONException e) { + Log.e(TAG, "Error during message decoding", e); + } + } + + /** + * Method to create SDP answer for a given SDP offer + */ + private void doAnswer() { + peerConnection.createAnswer(new SimpleSdpObserver() { + @Override + public void onCreateSuccess(SessionDescription sessionDescription) { + peerConnection.setLocalDescription(new SimpleSdpObserver(), sessionDescription); + JSONObject message = new JSONObject(); + try { + message.put("type", "answer"); + message.put("sdp", sessionDescription.description); + sendMessage(true, message); + } catch (JSONException | IOException e) { + Log.e(TAG, "Error during sdp answer", e); + } + } + }, new MediaConstraints()); + } + + /** + * Method used to create a socket for SDP negotiation + */ + private void createSocket(){ + try { + if(!isReciever){ + socket = new Socket(recieverIP, recieverPort); + } + outStream = new ObjectOutputStream(socket.getOutputStream()); + inputStream = new ObjectInputStream(socket.getInputStream()); + } catch (Exception e) { + Log.e(TAG, "Error during create socket", e); + } + } + + /** + * Method to invoke Signalling handshake message + */ + private void invokeSendMessage(){ + try { + sendMessage(false , "got user media"); + } catch (Exception e) { + Log.e(TAG, "Error during invoke send message", e); + } + } + + /** + * Method to check if the message in received packet is "got user media" + */ + private void checkPacketMessage(String message){ + if (message.equals("got user media")) { + maybeStart(); + } + } + + /** + * Method to invoke MaybeStart() + */ + private void invokeMaybeStart(){ + if (!isInitiator && !isStarted) { + maybeStart(); + } + } + + /** + * Method to begin SDP negotiation by sending SDP offer to peer + */ + private void maybeStart() { + Log.d(TAG, "maybeStart: " + isStarted + " " + isChannelReady); + if (!isStarted && isChannelReady) { + isStarted = true; + if (isInitiator) { + doCall(); + } + } + } + + /** + * Method to stop thread + */ + public void stop() { + isRunning = false; + } + } +} diff --git a/android/modules/webrtc/src/main/java/com/samsung/android/modules/webrtc/WebRTCServer.java b/android/modules/webrtc/src/main/java/com/samsung/android/modules/webrtc/WebRTCServer.java new file mode 100644 index 0000000..6e93ed7 --- /dev/null +++ b/android/modules/webrtc/src/main/java/com/samsung/android/modules/webrtc/WebRTCServer.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.samsung.android.modules.webrtc; + +import android.content.Context; +import android.util.Log; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; + +/** + * Class to implement WebRTC server related functionalities + */ +public class WebRTCServer { + private static final String TAG = "WebRTCServer"; + private WebRTC.DataType dataType; + private ServerSocket serverSocket = null; + private Context appContext; + private WebRTC.ReceiveDataCallback dataCallback; + private List<WebRTC> connectionList = new ArrayList<>(); + private ServerThread serverThread = null; + private Thread thread = null; + + /** + * WebRTCServer constructor to create its instance + * @param appContext Application context of the app creating WebRTCServer instance + * @param dataType Datatype to create webRTC channel - Media channel or data channel + * @param dataCallback Data callback object to create call back mechanism + */ + public WebRTCServer(Context appContext, WebRTC.DataType dataType, WebRTC.ReceiveDataCallback dataCallback){ + this.appContext = appContext; + this.dataType = dataType; + this.dataCallback = dataCallback; + } + + /** + * Method to start WebRTCServer instance + * @return Returns Port number on success and -1 on failure + */ + public int start(){ + try { + serverSocket = new ServerSocket(0); + } catch (IOException e) { + Log.e(TAG, "Error during start", e); + return -1; + } + serverThread = new ServerThread(); + thread = new Thread(serverThread); + thread.start(); + return serverSocket.getLocalPort(); + } + + /** + * Method to stop running WebRTC server instance + */ + public void stop(){ + if (serverThread != null) { + serverThread.stop(); + } + try { + if (serverSocket != null) { + serverSocket.close(); + } + } catch (IOException e) { + Log.e(TAG, "Error during stop", e); + } + for(WebRTC web : connectionList){ + web.disconnect(); + } + } + + /** + * Class to implement a server thread + */ + private class ServerThread implements Runnable{ + private volatile boolean isRunning = true; + + @Override + public void run() { + while(isRunning){ + try { + Socket socket = serverSocket.accept(); + WebRTC web = new WebRTC(dataType , appContext , socket); + web.connect(); + web.registerDataCallback(dataCallback); + connectionList.add(web); + } catch (IOException e) { + isRunning = false; + Log.e(TAG, "Error during run", e); + } + } + } + + public void stop() { + isRunning = false; + } + } +} diff --git a/android/modules/webrtc/src/test/java/com/samsung/android/modules/webrtc/ExampleUnitTest.java b/android/modules/webrtc/src/test/java/com/samsung/android/modules/webrtc/ExampleUnitTest.java new file mode 100644 index 0000000..1536b3d --- /dev/null +++ b/android/modules/webrtc/src/test/java/com/samsung/android/modules/webrtc/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.samsung.android.modules.webrtc; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +}
\ No newline at end of file diff --git a/android/mosquitto/build.gradle b/android/mosquitto/build.gradle new file mode 100644 index 0000000..0b6001a --- /dev/null +++ b/android/mosquitto/build.gradle @@ -0,0 +1,80 @@ +plugins { + id 'com.android.library' + id "de.undercouch.download" version "5.0.1" +} + +def thirdPartyDir = new File ("${rootProject.projectDir}/third_party") + +def mosquittoDir = new File("${thirdPartyDir}/mosquitto-2.0.14") + +android { + compileSdkVersion 31 + ndkVersion "21.3.6528147" + defaultConfig { + minSdkVersion 28 + targetSdkVersion 31 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + externalNativeBuild { + cmake { + arguments '-DCMAKE_VERBOSE_MAKEFILE=1' + arguments '-DCMAKE_INSTALL_PREFIX:PATH=/usr' + arguments '-DANDROID_STL=c++_shared' + arguments "-DANDROID_NDK_HOME=${System.env.ANDROID_NDK_ROOT}" + arguments '-DWITH_STATIC_LIBRARIES=ON' + arguments '-DWITH_TLS=OFF' + arguments '-DWITH_TLS_PSK=OFF' + arguments '-DWITH_CJSON=OFF' + arguments '-DWITH_APPS=OFF' + arguments '-DDOCUMENTATION=OFF' + cppFlags "-std=c++17" + abiFilters 'arm64-v8a', 'x86' + targets "libmosquitto", "mosquittopp", "libmosquitto_static", "mosquittopp_static" + } + } + } + buildFeatures { + prefab false + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + externalNativeBuild { + cmake { + path "${mosquittoDir}/CMakeLists.txt" + } + } +} + +dependencies { + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} + +task downloadMosquitto(type: Download) { + doFirst { + println("Downloading Mosquitto") + } + src "https://github.com/eclipse/mosquitto/archive/refs/tags/v2.0.14.zip" + dest new File(thirdPartyDir, "mosquitto-2.0.14.zip") + onlyIfModified true +} + +task unzipMosquitto(type: Copy, dependsOn: downloadMosquitto) { + doFirst { + println("Unzipping Mosquitto") + } + from zipTree(downloadMosquitto.dest) + into thirdPartyDir + onlyIf { !mosquittoDir.exists() } +} + +task wrapper(type: Wrapper) { + gradleVersion = '4.1' +} + +preBuild.dependsOn(unzipMosquitto) diff --git a/android/mosquitto/src/main/AndroidManifest.xml b/android/mosquitto/src/main/AndroidManifest.xml new file mode 100644 index 0000000..458e804 --- /dev/null +++ b/android/mosquitto/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.samsung.android.mosquitto"> +</manifest> diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..aa2ce81 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,4 @@ +include ':aitt' +include ':flatbuffers' +include ':mosquitto' +include ':modules:webrtc' diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..a8ae137 --- /dev/null +++ b/build.gradle @@ -0,0 +1,25 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:4.2.0" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/cmake/aitt_android_flatbuffers.cmake b/cmake/aitt_android_flatbuffers.cmake new file mode 100644 index 0000000..048d7ec --- /dev/null +++ b/cmake/aitt_android_flatbuffers.cmake @@ -0,0 +1,14 @@ +if(CMAKE_VERSION VERSION_LESS "3.10.0") + if(DEFINED AITT_ANDROID_FLATBUFFERS) + return() + endif() + set(AITT_ANDROID_FLATBUFFERS TRUE) +else(CMAKE_VERSION VERSION_LESS "3.10.0") + include_guard(GLOBAL) +endif(CMAKE_VERSION VERSION_LESS "3.10.0") + +include_directories(${PROJECT_ROOT_DIR}/third_party/flatbuffers-2.0.0/include) + +link_directories(${PROJECT_ROOT_DIR}/android/flatbuffers/.cxx/cmake/debug/${ANDROID_ABI}) + +set(FLATBUFFERS_LIBRARY ${PROJECT_ROOT_DIR}/android/flatbuffers/.cxx/cmake/debug/${ANDROID_ABI}/libflatbuffers.a) diff --git a/cmake/aitt_android_glib.cmake b/cmake/aitt_android_glib.cmake new file mode 100644 index 0000000..44c6618 --- /dev/null +++ b/cmake/aitt_android_glib.cmake @@ -0,0 +1,27 @@ +if(CMAKE_VERSION VERSION_LESS "3.10.0") + if(DEFINED AITT_ANDROID_GLIB) + return() + endif() + set(AITT_ANDROID_GLIB TRUE) +else(CMAKE_VERSION VERSION_LESS "3.10.0") + include_guard(GLOBAL) +endif(CMAKE_VERSION VERSION_LESS "3.10.0") + +if(ANDROID_ABI STREQUAL "arm64-v8a") + set(GSTREAMER_ABI arm64) +elseif(ANDROID_ABI STREQUAL "armeabi-v7a") + set(GSTREAMER_ABI armv7) +else(ANDROID_ABI STREQUAL "armeabi-v7a") + set(GSTREAMER_ABI ${ANDROID_ABI}) +endif(ANDROID_ABI STREQUAL "arm64-v8a") + +include_directories( + ${GSTREAMER_ROOT_ANDROID}/${GSTREAMER_ABI}/include/glib-2.0 + ${GSTREAMER_ROOT_ANDROID}/${GSTREAMER_ABI}/lib/glib-2.0/include +) + +link_directories(${GSTREAMER_ROOT_ANDROID}/${GSTREAMER_ABI}/lib) + +set(GLIB_LIBRARIES ${GSTREAMER_ROOT_ANDROID}/${GSTREAMER_ABI}/lib/libglib-2.0.a + ${GSTREAMER_ROOT_ANDROID}/${GSTREAMER_ABI}/lib/libiconv.a + ${GSTREAMER_ROOT_ANDROID}/${GSTREAMER_ABI}/lib/libintl.a) diff --git a/cmake/aitt_android_mosquitto.cmake b/cmake/aitt_android_mosquitto.cmake new file mode 100644 index 0000000..4428534 --- /dev/null +++ b/cmake/aitt_android_mosquitto.cmake @@ -0,0 +1,14 @@ +if(CMAKE_VERSION VERSION_LESS "3.10.0") + if(DEFINED AITT_ANDROID_MOSQUITTO) + return() + endif() + set(AITT_ANDROID_MOSQUITTO TRUE) +else(CMAKE_VERSION VERSION_LESS "3.10.0") + include_guard(GLOBAL) +endif(CMAKE_VERSION VERSION_LESS "3.10.0") + +include_directories(${PROJECT_ROOT_DIR}/third_party/mosquitto-2.0.14/include) + +link_directories(${PROJECT_ROOT_DIR}/android/mosquitto/.cxx/cmake/debug/${ANDROID_ABI}/lib) + +set(MOSQUITTO_LIBRARY ${PROJECT_ROOT_DIR}/android/mosquitto/.cxx/cmake/debug/${ANDROID_ABI}/lib/libmosquitto_static.a) diff --git a/common/AITTEx.cc b/common/AITTEx.cc new file mode 100755 index 0000000..b99d305 --- /dev/null +++ b/common/AITTEx.cc @@ -0,0 +1,59 @@ +/*
+ * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "AITTEx.h"
+
+using namespace aitt;
+
+AITTEx::AITTEx(ErrCode code) : err_code(code)
+{
+ err_msg = getErrString();
+}
+
+AITTEx::AITTEx(ErrCode code, const std::string& msg) : err_code(code)
+{
+ err_msg = getErrString() + " : " + msg;
+}
+
+AITTEx::ErrCode AITTEx::getErrCode()
+{
+ return err_code;
+}
+
+std::string AITTEx::getErrString() const
+{
+ switch (err_code) {
+ case INVALID_ARG:
+ return "Invalid Argument";
+ case NO_MEMORY:
+ return "Memory allocation failure";
+ case OPERATION_FAILED:
+ return "Operation failure";
+ case SYSTEM_ERR:
+ return "System failure";
+ case MQTT_ERR:
+ return "MQTT failure";
+ case NO_DATA:
+ return "No data found";
+ default:
+ return "Unknown Error";
+ }
+}
+
+const char* AITTEx::what() const throw()
+{
+ return err_msg.c_str();
+}
+
diff --git a/common/AITTEx.h b/common/AITTEx.h new file mode 100755 index 0000000..8018c7f --- /dev/null +++ b/common/AITTEx.h @@ -0,0 +1,49 @@ +/*
+ * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <exception>
+#include <string>
+#include <vector>
+
+namespace aitt {
+
+class AITTEx : public std::exception {
+ public:
+ typedef enum {
+ INVALID_ARG,
+ NO_MEMORY,
+ OPERATION_FAILED,
+ SYSTEM_ERR,
+ MQTT_ERR,
+ NO_DATA
+ } ErrCode;
+
+ AITTEx(ErrCode err_code);
+ AITTEx(ErrCode err_code, const std::string& custom_err_msg);
+
+ ErrCode getErrCode();
+ virtual const char* what() const throw() override;
+
+ private:
+ ErrCode err_code;
+ std::string err_msg;
+
+ std::string getErrString() const;
+};
+
+} // namespace aitt
+
diff --git a/common/AittDiscovery.cc b/common/AittDiscovery.cc new file mode 100644 index 0000000..8f383c0 --- /dev/null +++ b/common/AittDiscovery.cc @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "AittDiscovery.h" + +#include <flatbuffers/flexbuffers.h> + +#include <atomic> + +#include "AITTEx.h" +#include "aitt_internal.h" + +namespace aitt { + +AittDiscovery::AittDiscovery(const std::string &id) + : id_(id), discovery_mq(id + "d", true), callback_handle(nullptr) +{ +} + +void AittDiscovery::Start(const std::string &host, int port, const std::string &username, + const std::string &password) +{ + RET_IF(callback_handle); + + discovery_mq.SetWillInfo(DISCOVERY_TOPIC_BASE + id_, nullptr, 0, AITT_QOS_EXACTLY_ONCE, true); + discovery_mq.Connect(host, port, username, password); + + callback_handle = discovery_mq.Subscribe(DISCOVERY_TOPIC_BASE + "+", DiscoveryMessageCallback, + static_cast<void *>(this), AITT_QOS_EXACTLY_ONCE); +} + +void AittDiscovery::Stop() +{ + discovery_mq.Publish(DISCOVERY_TOPIC_BASE + id_, nullptr, 0, AITT_QOS_EXACTLY_ONCE, true); + discovery_mq.Unsubscribe(callback_handle); + callback_handle = nullptr; + discovery_mq.Disconnect(); +} + +void AittDiscovery::UpdateDiscoveryMsg(AittProtocol protocol, const void *msg, size_t length) +{ + auto it = discovery_map.find(protocol); + if (it == discovery_map.end()) + discovery_map.emplace(protocol, DiscoveryBlob(msg, length)); + else + it->second = DiscoveryBlob(msg, length); + + PublishDiscoveryMsg(); +} + +int AittDiscovery::AddDiscoveryCB(AittProtocol protocol, const DiscoveryCallback &cb) +{ + static std::atomic_int id(0); + id++; + callbacks.emplace(id, std::make_pair(protocol, cb)); + + return id; +} + +void AittDiscovery::RemoveDiscoveryCB(int callback_id) +{ + auto it = callbacks.find(callback_id); + if (it == callbacks.end()) { + ERR("Unknown callback_id(%d)", callback_id); + throw AITTEx(AITTEx::INVALID_ARG); + } + callbacks.erase(it); +} + +void AittDiscovery::DiscoveryMessageCallback(MSG *mq, const std::string &topic, const void *msg, + const int szmsg, void *user_data) +{ + RET_IF(user_data == nullptr); + + AittDiscovery *discovery = static_cast<AittDiscovery *>(user_data); + + DBG("Called(id = %s, msg = %p:%d)", discovery->id_.c_str(), msg, szmsg); + + size_t end = topic.find("/", DISCOVERY_TOPIC_BASE.length()); + std::string clientId = topic.substr(DISCOVERY_TOPIC_BASE.length(), end); + if (clientId.empty()) { + ERR("ClientId is empty"); + return; + } + + if (msg == nullptr) { + for (const auto &node : discovery->callbacks) { + std::pair<AittProtocol, DiscoveryCallback> cb_info = node.second; + cb_info.second(clientId, WILL_LEAVE_NETWORK, nullptr, 0); + } + return; + } + + auto map = flexbuffers::GetRoot(static_cast<const uint8_t *>(msg), szmsg).AsMap(); + std::string status = map["status"].AsString().c_str(); + + auto keys = map.Keys(); + for (size_t idx = 0; idx < keys.size(); ++idx) { + std::string key = keys[idx].AsString().str(); + + if (!key.compare("status")) + continue; + + auto blob = map[key].AsBlob(); + for (const auto &node : discovery->callbacks) { + std::pair<AittProtocol, DiscoveryCallback> cb_info = node.second; + if (cb_info.first == discovery->GetProtocol(key)) { + cb_info.second(clientId, status, blob.data(), blob.size()); + } + } + } +} + +void AittDiscovery::PublishDiscoveryMsg() +{ + flexbuffers::Builder fbb; + + fbb.Map([this, &fbb]() { + fbb.String("status", JOIN_NETWORK); + + for (const std::pair<const AittProtocol, const DiscoveryBlob &> &node : discovery_map) { + fbb.Key(GetProtocolStr(node.first)); + fbb.Blob(node.second.data.get(), node.second.len); + } + }); + + fbb.Finish(); + + auto buf = fbb.GetBuffer(); + discovery_mq.Publish(DISCOVERY_TOPIC_BASE + id_, buf.data(), buf.size(), AITT_QOS_EXACTLY_ONCE, + true); +} + +const char *AittDiscovery::GetProtocolStr(AittProtocol protocol) +{ + switch (protocol) { + case AITT_TYPE_MQTT: + return "mqtt"; + case AITT_TYPE_TCP: + return "tcp"; + case AITT_TYPE_WEBRTC: + return "webrtc"; + default: + ERR("Unknown protocol(%d)", protocol); + } + + return nullptr; +} + +AittProtocol AittDiscovery::GetProtocol(const std::string &protocol_str) +{ + if (STR_EQ == protocol_str.compare(GetProtocolStr(AITT_TYPE_MQTT))) + return AITT_TYPE_MQTT; + + if (STR_EQ == protocol_str.compare(GetProtocolStr(AITT_TYPE_TCP))) + return AITT_TYPE_TCP; + + if (STR_EQ == protocol_str.compare(GetProtocolStr(AITT_TYPE_WEBRTC))) + return AITT_TYPE_WEBRTC; + + return AITT_TYPE_UNKNOWN; +} + +AittDiscovery::DiscoveryBlob::DiscoveryBlob(const void *msg, size_t length) + : len(length), data(new char[len]) +{ + memcpy(data.get(), msg, length); +} + +AittDiscovery::DiscoveryBlob::~DiscoveryBlob() +{ +} + +AittDiscovery::DiscoveryBlob::DiscoveryBlob(const DiscoveryBlob &a) : len(a.len), data(a.data) +{ +} + +AittDiscovery::DiscoveryBlob &AittDiscovery::DiscoveryBlob::operator=(const DiscoveryBlob &src) +{ + len = src.len; + data = src.data; + return *this; +} + +} // namespace aitt diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt new file mode 100644 index 0000000..53eadd9 --- /dev/null +++ b/common/CMakeLists.txt @@ -0,0 +1,12 @@ +FILE(GLOB COMMON_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/*.cc) + +ADD_LIBRARY(${AITT_COMMON} SHARED ${COMMON_SRCS}) +TARGET_LINK_LIBRARIES(${AITT_COMMON} ${AITT_NEEDS_LIBRARIES} Threads::Threads) +TARGET_COMPILE_OPTIONS(${AITT_COMMON} PUBLIC ${AITT_NEEDS_CFLAGS_OTHER} "-fvisibility=default") +IF(VERSIONING) + SET_TARGET_PROPERTIES(${AITT_COMMON} PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + ) +ENDIF(VERSIONING) +INSTALL(TARGETS ${AITT_COMMON} DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/common/MQ.cc b/common/MQ.cc new file mode 100644 index 0000000..2be4518 --- /dev/null +++ b/common/MQ.cc @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "MQ.h" + +#include <mqtt_protocol.h> +#include <sys/types.h> +#include <unistd.h> + +#include <algorithm> +#include <cerrno> +#include <stdexcept> +#include <thread> + +#include "AITTEx.h" +#include "AittTypes.h" +#include "aitt_internal.h" + +namespace aitt { + +const std::string MQ::REPLY_SEQUENCE_NUM_KEY = "sequenceNum"; +const std::string MQ::REPLY_IS_END_SEQUENCE_KEY = "isEndSequence"; +thread_local bool MQ::in_callback = false; + +MQ::MQ(const std::string &id, bool clear_session) + : handle(nullptr), + keep_alive(60), + subscribers_iterating(false), + subscriber_iterator_updated(false), + connect_cb(nullptr) +{ + do { + int ret = mosquitto_lib_init(); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_lib_init() Fail(%s)", mosquitto_strerror(ret)); + break; + } + + handle = mosquitto_new(id.c_str(), clear_session, this); + if (handle == nullptr) { + ERR("mosquitto_new(%s, %d) Fail", id.c_str(), clear_session); + break; + } + + ret = mosquitto_int_option(handle, MOSQ_OPT_PROTOCOL_VERSION, MQTT_PROTOCOL_V5); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_int_option() Fail(%s)", mosquitto_strerror(ret)); + break; + } + + mosquitto_message_v5_callback_set(handle, MessageCallback); + + ret = mosquitto_loop_start(handle); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_loop_start() Fail(%s)", mosquitto_strerror(ret)); + break; + } + + return; + } while (0); + + mosquitto_destroy(handle); + mosquitto_lib_cleanup(); + throw AITTEx(AITTEx::MQTT_ERR, std::string("MQ Constructor Error")); +} + +MQ::~MQ(void) +{ + int ret; + INFO("Destructor"); + + if (mq_connect_thread.joinable()) + mq_connect_thread.join(); + + ret = mosquitto_loop_stop(handle, true); + if (ret != MOSQ_ERR_SUCCESS) + ERR("mosquitto_loop_stop() Fail(%s)", mosquitto_strerror(ret)); + + callback_lock.lock(); + connect_cb = nullptr; + subscribers.clear(); + callback_lock.unlock(); + + mosquitto_destroy(handle); + + ret = mosquitto_lib_cleanup(); + if (ret != MOSQ_ERR_SUCCESS) + ERR("mosquitto_lib_cleanup() Fail(%s)", mosquitto_strerror(ret)); +} + +void MQ::SetConnectionCallback(const MQConnectionCallback &cb) +{ + std::lock_guard<std::recursive_mutex> lock_from_here(callback_lock); + connect_cb = cb; + + if (mq_connect_thread.joinable()) + mq_connect_thread.join(); + + if (in_callback) { + // When it's called in the cb, it's blocked by lock. That's why it uses thread. + mq_connect_thread = std::thread([&]() { SetConnectionCallbackReal(cb ? true : false); }); + } else { + SetConnectionCallbackReal(cb ? true : false); + } +} + +void MQ::SetConnectionCallbackReal(bool is_set) +{ + if (is_set) { + mosquitto_connect_v5_callback_set(handle, ConnectCallback); + mosquitto_disconnect_v5_callback_set(handle, DisconnectCallback); + } else { + mosquitto_connect_v5_callback_set(handle, nullptr); + mosquitto_disconnect_v5_callback_set(handle, nullptr); + } +} + +void MQ::ConnectCallback(struct mosquitto *mosq, void *obj, int rc, int flag, + const mosquitto_property *props) +{ + RET_IF(obj == nullptr); + MQ *mq = static_cast<MQ *>(obj); + + INFO("Connected : rc(%d), flag(%d)", rc, flag); + + std::lock_guard<std::recursive_mutex> lock_from_here(mq->callback_lock); + in_callback = true; + if (mq->connect_cb) + mq->connect_cb(AITT_CONNECTED); + in_callback = false; +} + +void MQ::DisconnectCallback(struct mosquitto *mosq, void *obj, int rc, + const mosquitto_property *props) +{ + RET_IF(obj == nullptr); + MQ *mq = static_cast<MQ *>(obj); + + INFO("Disconnected : rc(%d)", rc); + + std::lock_guard<std::recursive_mutex> lock_from_here(mq->callback_lock); + in_callback = true; + if (mq->connect_cb) + mq->connect_cb(AITT_DISCONNECTED); + in_callback = false; +} + +void MQ::Connect(const std::string &host, int port, const std::string &username, + const std::string &password) +{ + int ret; + + if (username.empty() == false) { + ret = mosquitto_username_pw_set(handle, username.c_str(), password.c_str()); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_username_pw_set(%s, %s) Fail(%s)", username.c_str(), password.c_str(), + mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } + } + + ret = mosquitto_connect(handle, host.c_str(), port, keep_alive); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_connect(%s, %d) Fail(%s)", host.c_str(), port, mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } +} + +void MQ::SetWillInfo(const std::string &topic, const void *msg, size_t szmsg, int qos, bool retain) +{ + int ret = mosquitto_will_set(handle, topic.c_str(), szmsg, msg, qos, retain); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_will_set(%s) Fail(%s)", topic.c_str(), mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } +} + +void MQ::Disconnect(void) +{ + int ret = mosquitto_disconnect(handle); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_disconnect() Fail(%s)", mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } + + mosquitto_will_clear(handle); +} + +void MQ::MessageCallback(mosquitto *handle, void *obj, const mosquitto_message *msg, + const mosquitto_property *props) +{ + RET_IF(obj == nullptr); + MQ *mq = static_cast<MQ *>(obj); + + std::lock_guard<std::recursive_mutex> auto_lock(mq->callback_lock); + mq->subscribers_iterating = true; + mq->subscriber_iterator = mq->subscribers.begin(); + while (mq->subscriber_iterator != mq->subscribers.end()) { + auto subscribe_data = *(mq->subscriber_iterator); + if (nullptr == subscribe_data) + ERR("end() is not valid because elements were added."); + + bool result = CompareTopic(subscribe_data->topic.c_str(), msg->topic); + if (result) + mq->InvokeCallback(msg, props); + + if (!mq->subscriber_iterator_updated) + mq->subscriber_iterator++; + else + mq->subscriber_iterator_updated = false; + } + mq->subscribers_iterating = false; + mq->subscribers.insert(mq->subscribers.end(), mq->new_subscribers.begin(), + mq->new_subscribers.end()); + mq->new_subscribers.clear(); +} + +void MQ::InvokeCallback(const mosquitto_message *msg, const mosquitto_property *props) +{ + MSG mq_msg; + mq_msg.SetTopic(msg->topic); + if (props) { + const mosquitto_property *prop; + + char *response_topic = nullptr; + prop = mosquitto_property_read_string(props, MQTT_PROP_RESPONSE_TOPIC, &response_topic, + false); + if (prop) { + mq_msg.SetResponseTopic(response_topic); + free(response_topic); + } + + void *correlation = nullptr; + uint16_t correlation_size = 0; + prop = mosquitto_property_read_binary(props, MQTT_PROP_CORRELATION_DATA, &correlation, + &correlation_size, false); + if (prop == nullptr || correlation == nullptr) + ERR("No Correlation Data"); + + mq_msg.SetCorrelation(std::string((char *)correlation, correlation_size)); + if (correlation) + free(correlation); + + char *name = nullptr; + char *value = nullptr; + prop = mosquitto_property_read_string_pair(props, MQTT_PROP_USER_PROPERTY, &name, &value, + false); + while (prop) { + if (REPLY_SEQUENCE_NUM_KEY == name) { + mq_msg.SetSequence(std::stoi(value)); + } else if (REPLY_IS_END_SEQUENCE_KEY == name) { + mq_msg.SetEndSequence(std::stoi(value) == 1); + } else { + ERR("Unsupported property(%s, %s)", name, value); + } + free(name); + free(value); + + prop = mosquitto_property_read_string_pair(prop, MQTT_PROP_USER_PROPERTY, &name, &value, + true); + } + } + in_callback = true; + SubscribeData *cb_info = *subscriber_iterator; + cb_info->cb(&mq_msg, msg->topic, msg->payload, msg->payloadlen, cb_info->user_data); + in_callback = false; +} + +void MQ::Publish(const std::string &topic, const void *data, const size_t datalen, int qos, + bool retain) +{ + int mid = -1; + int ret = mosquitto_publish(handle, &mid, topic.c_str(), datalen, data, qos, retain); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_publish(%s) Fail(%s)", topic.c_str(), mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } +} + +void MQ::PublishWithReply(const std::string &topic, const void *data, const size_t datalen, int qos, + bool retain, const std::string &reply_topic, const std::string &correlation) +{ + int ret; + int mid = -1; + mosquitto_property *props = nullptr; + + ret = mosquitto_property_add_string(&props, MQTT_PROP_RESPONSE_TOPIC, reply_topic.c_str()); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_property_add_string(response-topic) Fail(%s)", mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } + + ret = mosquitto_property_add_binary(&props, MQTT_PROP_CORRELATION_DATA, correlation.c_str(), + correlation.size()); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_property_add_binary(correlation) Fail(%s)", mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } + ret = mosquitto_publish_v5(handle, &mid, topic.c_str(), datalen, data, qos, retain, props); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_publish_v5(%s) Fail(%s)", topic.c_str(), mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } +} + +void MQ::SendReply(MSG *msg, const void *data, const size_t datalen, int qos, bool retain) +{ + RET_IF(msg == nullptr); + + int ret; + int mId = -1; + mosquitto_property *props = nullptr; + + ret = mosquitto_property_add_binary(&props, MQTT_PROP_CORRELATION_DATA, + msg->GetCorrelation().c_str(), msg->GetCorrelation().size()); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_property_add_binary(correlation) Fail(%s)", mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } + + ret = mosquitto_property_add_string_pair(&props, MQTT_PROP_USER_PROPERTY, + REPLY_SEQUENCE_NUM_KEY.c_str(), std::to_string(msg->GetSequence()).c_str()); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_property_add_string_pair(squenceNum) Fail(%s)", mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } + + ret = mosquitto_property_add_string_pair(&props, MQTT_PROP_USER_PROPERTY, + REPLY_IS_END_SEQUENCE_KEY.c_str(), std::to_string(msg->IsEndSequence()).c_str()); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_property_add_string_pair(IsEndSequence) Fail(%s)", mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } + + ret = mosquitto_publish_v5(handle, &mId, msg->GetResponseTopic().c_str(), datalen, data, qos, + retain, props); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_publish_v5(%s) Fail(%s)", msg->GetResponseTopic().c_str(), + mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } +} + +void *MQ::Subscribe(const std::string &topic, const SubscribeCallback &cb, void *user_data, int qos) +{ + int mid = -1; + int ret = mosquitto_subscribe(handle, &mid, topic.c_str(), qos); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_subscribe(%s) Fail(%s)", topic.c_str(), mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } + + std::lock_guard<std::recursive_mutex> lock_from_here(callback_lock); + SubscribeData *data = new SubscribeData(topic, cb, user_data); + if (subscribers_iterating) + new_subscribers.push_back(data); + else + subscribers.push_back(data); + + return static_cast<void *>(data); +} + +void *MQ::Unsubscribe(void *sub_handle) +{ + std::lock_guard<std::recursive_mutex> auto_lock(callback_lock); + auto it = std::find(subscribers.begin(), subscribers.end(), + static_cast<SubscribeData *>(sub_handle)); + + if (it == subscribers.end()) { + ERR("No Subscription(%p)", sub_handle); + throw AITTEx(AITTEx::NO_DATA); + } + + SubscribeData *data = static_cast<SubscribeData *>(sub_handle); + + if (subscriber_iterator == it) { + subscriber_iterator = subscribers.erase(it); + subscriber_iterator_updated = true; + } else { + subscribers.erase(it); + } + + void *user_data = data->user_data; + std::string topic = data->topic; + delete data; + + int mid = -1; + int ret = mosquitto_unsubscribe(handle, &mid, topic.c_str()); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_unsubscribe(%s) Fail(%d)", topic.c_str(), ret); + throw AITTEx(AITTEx::MQTT_ERR); + } + + return user_data; +} + +bool MQ::CompareTopic(const std::string &left, const std::string &right) +{ + bool result = false; + int ret = mosquitto_topic_matches_sub(left.c_str(), right.c_str(), &result); + if (ret != MOSQ_ERR_SUCCESS) { + ERR("mosquitto_topic_matches_sub(%s, %s) Fail(%s)", left.c_str(), right.c_str(), + mosquitto_strerror(ret)); + throw AITTEx(AITTEx::MQTT_ERR); + } + return result; +} + +MQ::SubscribeData::SubscribeData(const std::string &in_topic, const SubscribeCallback &in_cb, + void *in_user_data) + : topic(in_topic), cb(in_cb), user_data(in_user_data) +{ +} + +} // namespace aitt diff --git a/common/MQ.h b/common/MQ.h new file mode 100644 index 0000000..3da6097 --- /dev/null +++ b/common/MQ.h @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <mosquitto.h> + +#include <functional> +#include <map> +#include <mutex> +#include <string> +#include <thread> +#include <utility> +#include <vector> + +#include "MSG.h" + +#define MQTT_LOCALHOST "127.0.0.1" +#define MQTT_PORT 1883 + +namespace aitt { + +class MQ { + public: + using SubscribeCallback = std::function<void(MSG *msg, const std::string &topic, + const void *data, const size_t datalen, void *user_data)>; + using MQConnectionCallback = std::function<void(int)>; + + explicit MQ(const std::string &id, bool clear_session = false); + virtual ~MQ(void); + + static bool CompareTopic(const std::string &left, const std::string &right); + + void SetConnectionCallback(const MQConnectionCallback &cb); + void Connect(const std::string &host, int port, const std::string &username, + const std::string &password); + void SetWillInfo(const std::string &topic, const void *msg, size_t szmsg, int qos, bool retain); + void Disconnect(void); + void Publish(const std::string &topic, const void *data, const size_t datalen, int qos = 0, + bool retain = false); + void PublishWithReply(const std::string &topic, const void *data, const size_t datalen, int qos, + bool retain, const std::string &reply_topic, const std::string &correlation); + void SendReply(MSG *msg, const void *data, const size_t datalen, int qos, bool retain); + void *Subscribe(const std::string &topic, const SubscribeCallback &cb, + void *user_data = nullptr, int qos = 0); + void *Unsubscribe(void *handle); + + private: + struct SubscribeData { + SubscribeData(const std::string &topic, const SubscribeCallback &cb, void *user_data); + std::string topic; + SubscribeCallback cb; + void *user_data; + }; + + static void ConnectCallback(mosquitto *mosq, void *obj, int rc, int flag, + const mosquitto_property *props); + static void DisconnectCallback(mosquitto *mosq, void *obj, int rc, + const mosquitto_property *props); + static void MessageCallback(mosquitto *, void *, const mosquitto_message *, + const mosquitto_property *); + void InvokeCallback(const mosquitto_message *msg, const mosquitto_property *props); + void SetConnectionCallbackReal(bool is_set); + + static const std::string REPLY_SEQUENCE_NUM_KEY; + static const std::string REPLY_IS_END_SEQUENCE_KEY; + thread_local static bool in_callback; + + mosquitto *handle; + const int keep_alive; + std::vector<SubscribeData *> subscribers; + bool subscribers_iterating; + std::vector<SubscribeData *> new_subscribers; + std::vector<SubscribeData *>::iterator subscriber_iterator; + bool subscriber_iterator_updated; + std::recursive_mutex callback_lock; + MQConnectionCallback connect_cb; + std::thread mq_connect_thread; +}; + +} // namespace aitt diff --git a/common/MSG.cc b/common/MSG.cc new file mode 100644 index 0000000..46eb8e3 --- /dev/null +++ b/common/MSG.cc @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "MSG.h" + +namespace aitt { +MSG::MSG() : sequence(0), end_sequence(true), id_(nullptr), protocols_(AITT_TYPE_MQTT) +{ +} + +void MSG::SetID(AittSubscribeID id) +{ + id_ = id; +} + +AittSubscribeID MSG::GetID() +{ + return id_; +} + +void MSG::SetTopic(const std::string& topic) +{ + topic_ = topic; +} + +const std::string& MSG::GetTopic() +{ + return topic_; +} + +void MSG::SetCorrelation(const std::string& correlation) +{ + correlation_ = correlation; +} + +const std::string& MSG::GetCorrelation() +{ + return correlation_; +} + +void MSG::SetResponseTopic(const std::string& replyTopic) +{ + reply_topic_ = replyTopic; +} + +const std::string& MSG::GetResponseTopic() +{ + return reply_topic_; +} + +void MSG::SetSequence(int num) +{ + sequence = num; +} + +void MSG::IncreaseSequence() +{ + sequence++; +} + +int MSG::GetSequence() +{ + return sequence; +} + +void MSG::SetEndSequence(bool end) +{ + end_sequence = end; +} + +bool MSG::IsEndSequence() +{ + return end_sequence; +} + +void MSG::SetProtocols(AittProtocol protocols) +{ + protocols_ = protocols; +} + +AittProtocol MSG::GetProtocols() +{ + return protocols_; +} + +} // namespace aitt diff --git a/common/MainLoopHandler.cc b/common/MainLoopHandler.cc new file mode 100644 index 0000000..747a6bc --- /dev/null +++ b/common/MainLoopHandler.cc @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "MainLoopHandler.h" + +#include <glib.h> + +#include "aitt_internal.h" + +namespace aitt { + +MainLoopHandler::MainLoopHandler() +{ + GMainContext *ctx = g_main_context_new(); + if (ctx == nullptr) + throw std::runtime_error("Failed to create a context"); + + loop = g_main_loop_new(ctx, FALSE); + if (loop == nullptr) { + g_main_context_unref(ctx); + throw std::runtime_error("Failed to create a loop"); + } + g_main_context_unref(ctx); +} + +MainLoopHandler::~MainLoopHandler() +{ + g_main_loop_unref(loop); +} + +void MainLoopHandler::Run() +{ + g_main_loop_run(loop); +} + +bool MainLoopHandler::Quit() +{ + if (g_main_loop_is_running(loop) == FALSE) { + ERR("main loop is not running"); + return false; + } + + g_main_loop_quit(loop); + return true; +} + +void MainLoopHandler::AddWatch(int fd, const mainLoopCB &cb, MainLoopData *user_data) +{ + MainLoopCbData *cb_data = new MainLoopCbData(); + GMainContext *ctx = g_main_loop_get_context(loop); + cb_data->ctx = ctx; + cb_data->cb = cb; + cb_data->data = user_data; + cb_data->fd = fd; + + GIOChannel *channel = g_io_channel_unix_new(fd); + GSource *source = g_io_create_watch(channel, (GIOCondition)(G_IO_IN | G_IO_HUP | G_IO_ERR)); + g_source_set_callback(source, (GSourceFunc)EventHandler, cb_data, DestroyNotify); + + g_source_attach(source, ctx); + + g_source_unref(source); + + callback_table_lock.lock(); + callback_table.insert(CallbackMap::value_type(fd, std::make_pair(source, cb_data))); + callback_table_lock.unlock(); +} + +MainLoopHandler::MainLoopData *MainLoopHandler::RemoveWatch(int fd) +{ + GSource *source; + MainLoopData *user_data = nullptr; + + { + std::lock_guard<std::mutex> autoLock(callback_table_lock); + auto it = callback_table.find(fd); + if (it == callback_table.end()) + return user_data; + source = it->second.first; + user_data = it->second.second->data; + callback_table.erase(it); + } + + g_source_destroy(source); + return user_data; +} + +unsigned int MainLoopHandler::AddTimeout(int interval, const mainLoopCB &cb, MainLoopData *data) +{ + MainLoopCbData *cb_data = new MainLoopCbData(); + GMainContext *ctx = g_main_loop_get_context(loop); + cb_data->ctx = ctx; + cb_data->cb = cb; + cb_data->data = data; + + GSource *source = g_timeout_source_new(interval); + g_source_set_callback(source, IdlerHandler, cb_data, DestroyNotify); + unsigned int id = g_source_attach(source, cb_data->ctx); + g_source_unref(source); + + return id; +} + +void MainLoopHandler::RemoveTimeout(unsigned int id) +{ + GSource *source; + source = g_main_context_find_source_by_id(g_main_loop_get_context(loop), id); + if (source) + g_source_destroy(source); +} + +void MainLoopHandler::AddIdle(MainLoopHandler *handle, const mainLoopCB &cb, + MainLoopData *user_data) +{ + RET_IF(handle == nullptr); + + MainLoopCbData *cb_data = new MainLoopCbData(); + cb_data->cb = cb; + cb_data->data = user_data; + cb_data->ctx = g_main_loop_get_context(handle->loop); + + AddIdle(cb_data, DestroyNotify); +} + +void MainLoopHandler::AddIdle(MainLoopCbData *cb_data, GDestroyNotify destroy) +{ + RET_IF(cb_data->ctx == nullptr); + + GSource *source = g_idle_source_new(); + g_source_set_priority(source, G_PRIORITY_HIGH); + g_source_set_callback(source, IdlerHandler, cb_data, destroy); + g_source_attach(source, cb_data->ctx); + g_source_unref(source); +} + +gboolean MainLoopHandler::IdlerHandler(gpointer user_data) +{ + RETV_IF(user_data == nullptr, FALSE); + + MainLoopCbData *cb_data = static_cast<MainLoopCbData *>(user_data); + + cb_data->cb(cb_data->result, cb_data->fd, cb_data->data); + + return FALSE; +} + +gboolean MainLoopHandler::EventHandler(GIOChannel *src, GIOCondition condition, gpointer user_data) +{ + RETV_IF(user_data == nullptr, FALSE); + + int ret = TRUE; + MainLoopCbData *cb_data = static_cast<MainLoopCbData *>(user_data); + + if ((G_IO_HUP | G_IO_ERR) & condition) { + ERR("Connection Error(%d)", condition); + cb_data->result = (G_IO_HUP & condition) ? HANGUP : ERROR; + ret = FALSE; + } + + cb_data->cb(cb_data->result, cb_data->fd, cb_data->data); + + return ret; +} + +void MainLoopHandler::DestroyNotify(gpointer data) +{ + MainLoopCbData *cb_data = static_cast<MainLoopCbData *>(data); + delete cb_data; +} + +MainLoopHandler::MainLoopCbData::MainLoopCbData() : result(OK), fd(-1) +{ +} + +} // namespace aitt diff --git a/common/MainLoopHandler.h b/common/MainLoopHandler.h new file mode 100644 index 0000000..42737b1 --- /dev/null +++ b/common/MainLoopHandler.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <AittTypes.h> +#include <glib.h> + +#include <functional> +#include <map> +#include <mutex> + +namespace aitt { + +class MainLoopHandler { + public: + enum MainLoopResult { + OK, + ERROR, + REMOVED, + HANGUP, + }; + struct MainLoopData { + virtual ~MainLoopData() = default; + }; + using mainLoopCB = std::function<void(MainLoopResult result, int fd, MainLoopData *data)>; + + MainLoopHandler(); + ~MainLoopHandler(); + + static void AddIdle(MainLoopHandler *handle, const mainLoopCB &cb, MainLoopData *user_data); + + void Run(); + bool Quit(); + void AddWatch(int fd, const mainLoopCB &cb, MainLoopData *user_data); + MainLoopData *RemoveWatch(int fd); + unsigned int AddTimeout(int interval, const mainLoopCB &cb, MainLoopData *user_data); + void RemoveTimeout(unsigned int id); + + private: + struct MainLoopCbData { + MainLoopCbData(); + mainLoopCB cb; + MainLoopData *data; + MainLoopResult result; + int fd; + GMainContext *ctx; + }; + using CallbackMap = std::map<int, std::pair<GSource *, MainLoopCbData *>>; + + static void AddIdle(MainLoopCbData *, GDestroyNotify); + static gboolean IdlerHandler(gpointer user_data); + static gboolean EventHandler(GIOChannel *src, GIOCondition cond, gpointer user_data); + static void DestroyNotify(gpointer data); + + GMainLoop *loop; + CallbackMap callback_table; + std::mutex callback_table_lock; +}; +} // namespace aitt diff --git a/common/aitt_internal.h b/common/aitt_internal.h new file mode 100644 index 0000000..feef42b --- /dev/null +++ b/common/aitt_internal.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <libgen.h> +#include <sys/syscall.h> +#include <unistd.h> + +#include <cassert> +#include <cerrno> +#include <cstring> + +#include "aitt_internal_definitions.h" +#include "aitt_platform.h" + +#if defined(SYS_gettid) +#define GETTID() syscall(SYS_gettid) +#else // SYS_gettid +#define GETTID() 0lu +#endif // SYS_gettid + +#define AITT_ERRMSG_LEN 80 + +#if (_POSIX_C_SOURCE >= 200112L) && !_GNU_SOURCE +#define AITT_STRERROR_R(errno, buf, buflen) \ + do { \ + int ret = strerror_r(errno, buf, buflen); \ + if (ret != 0) { \ + assert(ret == 0 && "strerror_r failed"); \ + } \ + } while (0) +#else +#define AITT_STRERROR_R(errno, buf, buflen) \ + do { \ + const char *errstr = strerror_r(errno, buf, buflen); \ + if (errstr == nullptr) { \ + assert(errstr != nullptr && "strerror_r failed"); \ + } \ + } while (0) +#endif + +#ifdef _LOG_WITH_TIMESTAMP +#include "aitt_internal_profiling.h" +#else +#define DBG(fmt, ...) PLATFORM_LOGD("[%lu] " fmt, GETTID(), ##__VA_ARGS__) + +#define INFO(fmt, ...) PLATFORM_LOGI("[%lu] " fmt, GETTID(), ##__VA_ARGS__) +#define ERR(fmt, ...) PLATFORM_LOGE("[%lu] \033[31m" fmt "\033[0m", GETTID(), ##__VA_ARGS__) +#define ERR_CODE(_aitt_errno, fmt, ...) \ + do { \ + char errMsg[AITT_ERRMSG_LEN] = {'\0'}; \ + int _errno = (_aitt_errno); \ + AITT_STRERROR_R(_errno, errMsg, sizeof(errMsg)); \ + PLATFORM_LOGE("[%lu] (%d:%s) \033[31m" fmt "\033[0m", GETTID(), _errno, errMsg, \ + ##__VA_ARGS__); \ + } while (0) +#endif + +#define RET_IF(expr) \ + do { \ + if (expr) { \ + ERR("(%s)", #expr); \ + return; \ + } \ + } while (0) + +#define RETV_IF(expr, val) \ + do { \ + if (expr) { \ + ERR("(%s)", #expr); \ + return (val); \ + } \ + } while (0) + +#define RETVM_IF(expr, val, fmt, arg...) \ + do { \ + if (expr) { \ + ERR(fmt, ##arg); \ + return (val); \ + } \ + } while (0) diff --git a/common/aitt_internal_definitions.h b/common/aitt_internal_definitions.h new file mode 100644 index 0000000..10cde41 --- /dev/null +++ b/common/aitt_internal_definitions.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#ifdef API +#undef API +#endif +#define API __attribute__((visibility("default"))) + +#define STR_EQ 0 + +#define AITT_MANAGED_TOPIC_PREFIX "/v1/custom/aitt/" +#define DISCOVERY_TOPIC_BASE std::string(AITT_MANAGED_TOPIC_PREFIX "discovery/") +#define RESPONSE_POSTFIX "_AittRe_" + +// Specification MQTT-4.7.3-3 +#define AITT_TOPIC_NAME_MAX 65535 + +// Specification MQTT-1.5.5 +#define AITT_PAYLOAD_MAX 268435455 diff --git a/common/aitt_internal_profiling.h b/common/aitt_internal_profiling.h new file mode 100644 index 0000000..11fcd47 --- /dev/null +++ b/common/aitt_internal_profiling.h @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <sys/time.h> + +// Increase this if you need more space for profiling +#define AITT_PROFILE_ID_MAX 8 + +struct __aitt__tls__ { + struct timeval last_timestamp; + struct timeval profile_timestamp[AITT_PROFILE_ID_MAX]; + char profile_idx; // Max to 255 + char initialized; // Max to 255, but we only use 0 or 1 +}; + +extern __thread struct __aitt__tls__ __aitt; + +#define InitAITT() \ + do { \ + if (__aitt.initialized == 0) { \ + __aitt.initialized = 1; \ + if (gettimeofday(&__aitt.last_timestamp, NULL) < 0) { \ + assert(!"gettimeofday failed"); \ + } \ + } \ + } while (0) + +#define PROFILE_MARK() \ + do { \ + if (__aitt.profile_idx < AITT_PROFILE_ID_MAX) { \ + if (gettimeofday(__aitt.profile_timestamp + __aitt.profile_idx, NULL) < 0) { \ + assert(!"gettimeofday failed"); \ + } \ + ++__aitt.profile_idx; \ + } else { \ + ERR("Unable to mark a profile point: %d\n", __aitt.profile_idx); \ + } \ + } while (0) + +#define PROFILE_ESTIMATE(tres) \ + do { \ + struct timeval tv; \ + struct timeval res; \ + if (gettimeofday(&tv, NULL) < 0) { \ + assert(!"gettimeofday failed"); \ + } \ + --__aitt.profile_idx; \ + timersub(&tv, __aitt.profile_timestamp + __aitt.profile_idx, &res); \ + (tres) = static_cast<double>(res.tv_sec) + static_cast<double>(res.tv_usec) / 1000000.0f; \ + } while (0) + +#define PROFILE_RESET(count) \ + do { \ + __aitt.profile_idx -= (count); \ + } while (0) + +#define DBG(fmt, ...) \ + do { \ + struct timeval tv; \ + struct timeval res; \ + InitAITT(); \ + if (gettimeofday(&tv, NULL) < 0) { \ + assert(!"gettimeofday failed"); \ + } \ + timersub(&tv, &__aitt.last_timestamp, &res); \ + PLATFORM_LOGD("[%lu] %lu.%.6lu(%lu.%.6lu) " fmt, GETTID(), tv.tv_sec, tv.tv_usec, \ + res.tv_sec, res.tv_usec, ##__VA_ARGS__); \ + __aitt.last_timestamp.tv_sec = tv.tv_sec; \ + __aitt.last_timestamp.tv_usec = tv.tv_usec; \ + } while (0) + +#define INFO(fmt, ...) \ + do { \ + struct timeval tv; \ + struct timeval res; \ + InitAITT(); \ + if (gettimeofday(&tv, NULL) < 0) { \ + assert(!"gettimeofday failed"); \ + } \ + timersub(&tv, &__aitt.last_timestamp, &res); \ + PLATFORM_LOGI("[%lu] %lu.%.6lu(%lu.%.6lu) " fmt, GETTID(), tv.tv_sec, tv.tv_usec, \ + res.tv_sec, res.tv_usec, ##__VA_ARGS__); \ + __aitt.last_timestamp.tv_sec = tv.tv_sec; \ + __aitt.last_timestamp.tv_usec = tv.tv_usec; \ + } while (0) + +#define ERR(fmt, ...) \ + do { \ + struct timeval tv; \ + struct timeval res; \ + InitAITT(); \ + if (gettimeofday(&tv, NULL) < 0) { \ + assert(!"gettimeofday failed"); \ + } \ + timersub(&tv, &__aitt.last_timestamp, &res); \ + PLATFORM_LOGE("[%lu] %lu.%.6lu(%lu.%.6lu) \033[31m" fmt "\033[0m", GETTID(), tv.tv_sec, \ + tv.tv_usec, res.tv_sec, res.tv_usec, ##__VA_ARGS__); \ + __aitt.last_timestamp.tv_sec = tv.tv_sec; \ + __aitt.last_timestamp.tv_usec = tv.tv_usec; \ + } while (0) + +#define ERR_CODE(_aitt_errno, fmt, ...) \ + do { \ + struct timeval tv; \ + struct timeval res; \ + char errMsg[AITT_ERRMSG_LEN] = {'\0'}; \ + int _errno = (_aitt_errno); \ + \ + AITT_STRERROR_R(_errno, errMsg, sizeof(errMsg)); \ + \ + InitAITT(); \ + if (gettimeofday(&tv, NULL) < 0) { \ + assert(!"gettimeofday failed"); \ + } \ + timersub(&tv, &__aitt.last_timestamp, &res); \ + PLATFORM_LOGE("[%lu] %lu.%.6lu(%lu.%.6lu) (%d:%s) \033[31m" fmt "\033[0m", GETTID(), \ + tv.tv_sec, tv.tv_usec, res.tv_sec, res.tv_usec, _errno, errMsg, ##__VA_ARGS__); \ + __aitt.last_timestamp.tv_sec = tv.tv_sec; \ + __aitt.last_timestamp.tv_usec = tv.tv_usec; \ + } while (0) diff --git a/common/aitt_platform.h b/common/aitt_platform.h new file mode 100644 index 0000000..dd13d4d --- /dev/null +++ b/common/aitt_platform.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#ifndef LOG_TAG +#define LOG_TAG "AITT" +#endif + +#define LOG_RED "\033[31m" +#define LOG_GREEN "\033[32m" +#define LOG_BROWN "\033[33m" +#define LOG_BLUE "\033[34m" +#define LOG_END "\033[0m" + +#if defined(PLATFORM) && !defined(LOG_STDOUT) + +#define STR(x) #x +#define PURE(x) x +#define PLATFORM_HEADER(x) STR(x) +#include PLATFORM_HEADER(PLATFORM PURE(/) PURE(aitt_platform.h)) + +#else // PLATFORM + +#include <libgen.h> +#include <stdio.h> + +#define __FILENAME__ basename((char *)(__FILE__)) +#define PLATFORM_LOGD(fmt, ...) \ + fprintf(stdout, LOG_BROWN "[%s]%s(%s:%d)" LOG_END fmt "\n", LOG_TAG, __func__, __FILENAME__, \ + __LINE__, ##__VA_ARGS__) +#define PLATFORM_LOGI(fmt, ...) \ + fprintf(stdout, LOG_GREEN "[%s]%s(%s:%d)" LOG_END fmt "\n", LOG_TAG, __func__, __FILENAME__, \ + __LINE__, ##__VA_ARGS__) +#define PLATFORM_LOGE(fmt, ...) \ + fprintf(stderr, LOG_RED "[%s]%s(%s:%d)" LOG_END fmt "\n", LOG_TAG, __func__, __FILENAME__, \ + __LINE__, ##__VA_ARGS__) + +#endif // PLATFORM diff --git a/common/tizen/aitt_platform.h b/common/tizen/aitt_platform.h new file mode 100644 index 0000000..2a898a5 --- /dev/null +++ b/common/tizen/aitt_platform.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <dlog/dlog.h> + +#define PLATFORM_LOGD(fmt, ...) LOGD(fmt, ##__VA_ARGS__) +#define PLATFORM_LOGI(fmt, ...) LOGI(fmt, ##__VA_ARGS__) +#define PLATFORM_LOGE(fmt, ...) LOGE(fmt, ##__VA_ARGS__) diff --git a/debian/aitt-dev.install b/debian/aitt-dev.install new file mode 100644 index 0000000..7354e42 --- /dev/null +++ b/debian/aitt-dev.install @@ -0,0 +1,2 @@ +/usr/include/aitt/*.h +/usr/lib/*/pkgconfig/*.pc diff --git a/debian/aitt-plugins.install b/debian/aitt-plugins.install new file mode 100644 index 0000000..38722db --- /dev/null +++ b/debian/aitt-plugins.install @@ -0,0 +1 @@ +/usr/lib/*/libaitt-transport*.so* diff --git a/debian/aitt.install b/debian/aitt.install new file mode 100644 index 0000000..ccdd217 --- /dev/null +++ b/debian/aitt.install @@ -0,0 +1,2 @@ +/usr/lib/*/libaitt*.so* +/usr/bin/* diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..5dc0313 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +aitt (0.0.1) unstable; urgency=medium + + * Initialize the debian packaging scripts + + -- Sungjae Park <nicesj.park@samsung.com> Tue, 23 Nov 2021 14:27:00 +0900 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +9 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..c4c06ab --- /dev/null +++ b/debian/control @@ -0,0 +1,30 @@ +Source: aitt +Section: libs +Priority: optional +Maintainer: Semun Lee <semun.lee@samsung.com> +Build-Depends: gcc-9 | gcc-8 | gcc-7 | gcc-6 | gcc-5 (>=5.4), + cmake, debhelper (>=9), libmosquitto-dev, lcov, libgmock-dev, libflatbuffers-dev, libglib2.0-dev +Standards-Version: 0.0.1 + +Package: aitt +Architecture: any +Multi-Arch: same +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: AI Telemetry Transport based on MQTT + AITT is a Framework which transfers data of AI service. + It makes distributed AI Inference possible. + +Package: aitt-plugins +Architecture: any +Multi-Arch: same +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: Plugin Libraries for AITT P2P transport + The aitt-plugins package contains basic plugin libraries for AITT P2P transport. + +Package: aitt-dev +Architecture: any +Multi-Arch: same +Depends: aitt +Description: AITT development package + The aitt-dev package contains libraries and header files for + developing programs that use %{name} diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..deb2e6f --- /dev/null +++ b/debian/copyright @@ -0,0 +1,3 @@ +Files: * +License: Apache-2.0 +Copyright(C) Samsung Electonics 2021 diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..cbfdc90 --- /dev/null +++ b/debian/rules @@ -0,0 +1,71 @@ +#!/usr/bin/make -f +# See debhelper(7) (uncomment to enable) +# output every command that modifies files on the build system. +#export DH_VERBOSE = 1 + +# see FEATURE AREAS in dpkg-buildflags(1) +#export DEB_BUILD_MAINT_OPTIONS = hardening=+all + +# see ENVIRONMENT in dpkg-buildflags(1) +# package maintainers to append CFLAGS +#export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic +# package maintainers to append LDFLAGS +#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed + +ROOT_DIR:=$(shell pwd) +export DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH) +export DEB_HOST_ARCH ?= $(shell dpkg-architecture -qDEB_HOST_ARCH) +export BUILDDIR=build +export AITT_SOURCE_ROOT_PATH=$(ROOT_DIR) +export AITT_BUILD_ROOT_PATH=${AITT_SOURCE_ROOT_PATH}/${BUILDDIR} +export COMMIT_ID=$(shell git rev-parse --short HEAD) + +export TEST ?= 1 +export COVERAGE ?= 0 + +%: + dh $@ --parallel --buildsystem=cmake + +.PHONY: override_dh_auto_clean +override_dh_auto_clean: + rm -rf ${AITT_BUILD_ROOT_PATH} + +.PHONY: override_dh_auto_configure +override_dh_auto_configure: + mkdir -p ${AITT_BUILD_ROOT_PATH}; \ + cd ${AITT_BUILD_ROOT_PATH}; \ + cmake .. \ + -DCMAKE_INSTALL_PREFIX:PATH=/usr \ + -DCMAKE_VERBOSE_MAKEFILE:BOOL=OFF \ + -DBUILD_TESTING:BOOL=${TEST} \ + -DCOVERAGE_TEST:BOOL=${COVERAGE}; \ + cd - + +.PHONY: override_dh_auto_build +override_dh_auto_build: + make -C ${AITT_BUILD_ROOT_PATH} + +.PHONY: override_dh_shlibdeps +override_dh_shlibdeps: + dh_shlibdeps --dpkg-shlibdeps-params=--ignore-missing-info + +.PHONY: override_dh_auto_test +override_dh_auto_test: + cd ${AITT_BUILD_ROOT_PATH}; \ + ctest --output-on-failure --timeout 100 + + if [ ${TEST} -ne 0 -a ${COVERAGE} -ne 0 ]; then \ + lcov -c --ignore-errors graph --no-external -b . -d . -o aitt_gcov.info; \ + genhtml aitt_gcov.info -o out --legend --show-details; \ + fi +.PHONY: override_dh_link +override_dh_link: + +.PHONY: override_dh_auto_install +override_dh_auto_install: + DESTDIR=$(CURDIR)/debian/tmp make -C ${AITT_BUILD_ROOT_PATH} install + +.PHONY: override_dh_install +override_dh_install: + dh_install --sourcedir=debian/tmp + dh_missing --fail-missing diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..d5e8af3 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c6f3907 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Feb 24 14:17:33 IST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/include/AITT.h b/include/AITT.h new file mode 100644 index 0000000..aeeacd8 --- /dev/null +++ b/include/AITT.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <AittTypes.h> +#include <MSG.h> + +#include <functional> +#include <memory> +#include <string> + +#define AITT_LOCALHOST "127.0.0.1" +#define AITT_PORT 1883 + +namespace aitt { + +class API AITT { + public: + using SubscribeCallback = + std::function<void(MSG *msg, const void *data, const size_t datalen, void *user_data)>; + using ConnectionCallback = std::function<void(AITT &, int, void *user_data)>; + + explicit AITT(const std::string &id, const std::string &ip_addr, bool clear_session = false); + virtual ~AITT(void); + + void SetWillInfo(const std::string &topic, const void *data, const size_t datalen, AittQoS qos, + bool retain); + void SetConnectionCallback(ConnectionCallback cb, void *user_data = nullptr); + void Connect(const std::string &host = AITT_LOCALHOST, int port = AITT_PORT, + const std::string &username = std::string(), const std::string &password = std::string()); + void Disconnect(void); + + void Publish(const std::string &topic, const void *data, const size_t datalen, + AittProtocol protocols = AITT_TYPE_MQTT, AittQoS qos = AITT_QOS_AT_MOST_ONCE, + bool retain = false); + int PublishWithReply(const std::string &topic, const void *data, const size_t datalen, + AittProtocol protocol, AittQoS qos, bool retain, const SubscribeCallback &cb, + void *cbdata, const std::string &correlation); + + int PublishWithReplySync(const std::string &topic, const void *data, const size_t datalen, + AittProtocol protocol, AittQoS qos, bool retain, const SubscribeCallback &cb, + void *cbdata, const std::string &correlation, int timeout_ms = 0); + + AittSubscribeID Subscribe(const std::string &topic, const SubscribeCallback &cb, + void *cbdata = nullptr, AittProtocol protocol = AITT_TYPE_MQTT, + AittQoS qos = AITT_QOS_AT_MOST_ONCE); + void *Unsubscribe(AittSubscribeID handle); + + void SendReply(MSG *msg, const void *data, const size_t datalen, bool end = true); + + // NOTE: + // Provide utility functions to developers who only be able to access the AITT class + static bool CompareTopic(const std::string &left, const std::string &right); + + private: + class Impl; + std::unique_ptr<Impl> pImpl; +}; + +} // namespace aitt diff --git a/include/AittDiscovery.h b/include/AittDiscovery.h new file mode 100644 index 0000000..f7e3f46 --- /dev/null +++ b/include/AittDiscovery.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "MQ.h" + +namespace aitt { + +class AittDiscovery { + public: + static constexpr const char *WILL_LEAVE_NETWORK = "disconnected"; + static constexpr const char *JOIN_NETWORK = "connected"; + + using DiscoveryCallback = std::function<void(const std::string &clientId, + const std::string &status, const void *msg, const int szmsg)>; + + explicit AittDiscovery(const std::string &id); + void Start(const std::string &host, int port, const std::string &username, + const std::string &password); + void Stop(); + void UpdateDiscoveryMsg(AittProtocol protocol, const void *msg, size_t length); + int AddDiscoveryCB(AittProtocol protocol, const DiscoveryCallback &cb); + void RemoveDiscoveryCB(int callback_id); + + private: + struct DiscoveryBlob { + explicit DiscoveryBlob(const void *msg, size_t length); + ~DiscoveryBlob(); + DiscoveryBlob(const DiscoveryBlob &src); + DiscoveryBlob &operator=(const DiscoveryBlob &src); + + size_t len; + std::shared_ptr<char> data; + }; + + static void DiscoveryMessageCallback(MSG *mq, const std::string &topic, const void *msg, + const int szmsg, void *user_data); + void PublishDiscoveryMsg(); + const char *GetProtocolStr(AittProtocol protocol); + AittProtocol GetProtocol(const std::string &protocol_str); + + std::string id_; + MQ discovery_mq; + void *callback_handle; + std::map<AittProtocol, DiscoveryBlob> discovery_map; + std::map<int, std::pair<AittProtocol, DiscoveryCallback>> callbacks; +}; + +// Discovery Message (flexbuffers) +// map { +// "status": "connected", +// "tcp": Blob Data for tcp Module, +// "webrtc": Blob Data for tcp Module, +// } + +} // namespace aitt diff --git a/include/AittTransport.h b/include/AittTransport.h new file mode 100644 index 0000000..6523b2e --- /dev/null +++ b/include/AittTransport.h @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <AittDiscovery.h> +#include <AittTypes.h> + +#include <functional> +#include <string> + +namespace aitt { + +class AittTransport { + public: + typedef void *(*ModuleEntry)(const char *ip, AittDiscovery &discovery); + using SubscribeCallback = std::function<void(const std::string &topic, const void *msg, + const size_t szmsg, void *cbdata, const std::string &correlation)>; + + static constexpr const char *const MODULE_ENTRY_NAME = "aitt_module_entry"; + + explicit AittTransport(AittDiscovery &discovery) : discovery(discovery) {} + virtual ~AittTransport(void) = default; + + virtual void Publish(const std::string &topic, const void *data, const size_t datalen, + const std::string &correlation, AittQoS qos = AITT_QOS_AT_MOST_ONCE, + bool retain = false) = 0; + + virtual void Publish(const std::string &topic, const void *data, const size_t datalen, + AittQoS qos = AITT_QOS_AT_MOST_ONCE, bool retain = false) = 0; + + virtual void *Subscribe(const std::string &topic, const SubscribeCallback &cb, + void *cbdata = nullptr, AittQoS qos = AITT_QOS_AT_MOST_ONCE) = 0; + virtual void *Subscribe(const std::string &topic, const SubscribeCallback &cb, const void *data, + const size_t datalen, void *cbdata = nullptr, AittQoS qos = AITT_QOS_AT_MOST_ONCE) = 0; + + virtual void *Unsubscribe(void *handle) = 0; + + protected: + aitt::AittDiscovery &discovery; +}; + +} // namespace aitt diff --git a/include/AittTypes.h b/include/AittTypes.h new file mode 100644 index 0000000..1770922 --- /dev/null +++ b/include/AittTypes.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#define API __attribute__((visibility("default"))) + +typedef void* AittSubscribeID; + +enum AittProtocol { + AITT_TYPE_UNKNOWN = 0, + AITT_TYPE_MQTT = (0x1 << 0), // Publish message through the MQTT + AITT_TYPE_TCP = (0x1 << 1), // Publish message to peers using the TCP + AITT_TYPE_WEBRTC = (0x1 << 2), // Publish message to peers using the WEBRTC +}; + +// AittQoS only works with the AITT_TYPE_MQTT +enum AittQoS { + AITT_QOS_AT_MOST_ONCE = 0, // Fire and forget + AITT_QOS_AT_LEAST_ONCE = 1, // Receiver is able to receive multiple times + AITT_QOS_EXACTLY_ONCE = 2, // Receiver only receives exactly once +}; + +enum AittConnectionState { + AITT_DISCONNECTED = 0, // The connection is disconnected. + AITT_CONNECTED = 1, // A connection was successfully established to the mqtt broker. +}; + +#ifdef TIZEN +#include <tizen.h> +#define TIZEN_ERROR_AITT -0x04020000 +#else +#include <errno.h> + +#define TIZEN_ERROR_NONE 0 +#define TIZEN_ERROR_INVALID_PARAMETER -EINVAL +#define TIZEN_ERROR_PERMISSION_DENIED -EACCES +#define TIZEN_ERROR_OUT_OF_MEMORY -ENOMEM +#define TIZEN_ERROR_TIMED_OUT (-1073741824LL + 1) +#define TIZEN_ERROR_NOT_SUPPORTED (-1073741824LL + 2) +#define TIZEN_ERROR_AITT -0x04020000 +#endif + +enum AittError { + AITT_ERROR_NONE = TIZEN_ERROR_NONE, /**< On Success */ + AITT_ERROR_INVALID_PARAMETER = TIZEN_ERROR_INVALID_PARAMETER, /**< Invalid parameter */ + AITT_ERROR_PERMISSION_DENIED = TIZEN_ERROR_PERMISSION_DENIED, /**< Permission denied */ + AITT_ERROR_OUT_OF_MEMORY = TIZEN_ERROR_OUT_OF_MEMORY, /**< Out of memory */ + AITT_ERROR_TIMED_OUT = TIZEN_ERROR_TIMED_OUT, /**< Time out */ + AITT_ERROR_NOT_SUPPORTED = TIZEN_ERROR_NOT_SUPPORTED, /**< Not supported */ + AITT_ERROR_UNKNOWN = TIZEN_ERROR_AITT | 0x01, /**< Unknown Error */ + AITT_ERROR_SYSTEM = TIZEN_ERROR_AITT | 0x02, /**< System errors */ + AITT_ERROR_NOT_READY = TIZEN_ERROR_AITT | 0x03, /**< System errors */ +}; diff --git a/include/MSG.h b/include/MSG.h new file mode 100644 index 0000000..85ecc76 --- /dev/null +++ b/include/MSG.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <string> + +#include <AittTypes.h> + +namespace aitt { +class API MSG { + public: + MSG(); + + void SetID(AittSubscribeID id); + AittSubscribeID GetID(); + void SetTopic(const std::string &topic); + const std::string &GetTopic(); + void SetCorrelation(const std::string &correlation); + const std::string &GetCorrelation(); + void SetResponseTopic(const std::string &reply_topic); + const std::string &GetResponseTopic(); + void SetSequence(int num); + void IncreaseSequence(); + int GetSequence(); + void SetEndSequence(bool end); + bool IsEndSequence(); + void SetProtocols(AittProtocol protocols); + AittProtocol GetProtocols(); + + protected: + std::string topic_; + std::string correlation_; + std::string reply_topic_; + int sequence; + bool end_sequence; + AittSubscribeID id_; + AittProtocol protocols_; +}; +} // namespace aitt diff --git a/include/aitt_c.h b/include/aitt_c.h new file mode 100644 index 0000000..24ffce3 --- /dev/null +++ b/include/aitt_c.h @@ -0,0 +1,366 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <stddef.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#include <AittTypes.h> +/** + * @addtogroup CAPI_AITT_MODULE + * @{ + */ + +/** + * @brief @a aitt_h is an opaque data structure to represent AITT service handle. + * @since_tizen 7.0 + */ +typedef struct aitt_handle *aitt_h; + +/** + * @brief @a aitt_msg_h is an opaque data structure to represent AITT message handle. + * @since_tizen 7.0 + * @see aitt_sub_fn + * @see aitt_msg_get_topic() + */ +typedef void *aitt_msg_h; + +/** + * @brief @a aitt_sub_h is an opaque data structure to represent AITT subscribe ID. + * @since_tizen 7.0 + * @see aitt_subscribe(), aitt_unsubscribe() + */ +typedef AittSubscribeID aitt_sub_h; + +/** + * @brief Enumeration for protocol. + * @since_tizen 7.0 + */ +typedef enum AittProtocol aitt_protocol_e; + +/** + * @brief Enumeration for MQTT QoS. + * It only works with the AITT_TYPE_MQTT + * @since_tizen 7.0 + */ +typedef enum AittQoS aitt_qos_e; + +/** + * @brief Enumeration for AITT error code. + * @since_tizen 7.0 + */ +typedef enum AittError aitt_error_e; + +/** + * @brief Specify the type of function passed to aitt_subscribe(). + * @details When the aitt get message, it is called, immediately. + * @since_tizen 7.0 + * @param[in] msg_handle aitt message handle. The handle has topic name and so on. @c aitt_msg_h + * @param[in] msg pointer to the data received + * @param[in] msg_len the size of the @c msg (bytes) + * @param[in] user_data The user data to pass to the function + * + * @pre The callback must be registered using aitt_subscribe(), and aitt_subscribe_full() + * + * @see aitt_subscribe() + * @see aitt_subscribe_full() + */ +typedef void ( + *aitt_sub_fn)(aitt_msg_h msg_handle, const void *msg, size_t msg_len, void *user_data); + +/** + * @brief Create a new AITT service instance. + * @detail If id is NULL or empty string, id will be generated automatically. + * If my_ip is NULL or empty string, my_ip will be set as 127.0.0.1. + * @since_tizen 7.0 + * @privlevel public + * @param[in] id Unique identifier in local network + * @param[in] my_ip Own device ip address for connecting by others + * @return @c handle of AITT service + * otherwise NULL value on failure + * @see aitt_destroy() + */ +aitt_h aitt_new(const char *id, const char *my_ip); + +/** + * @brief Enumeration for option. + * @since_tizen 7.0 + */ +typedef enum { + AITT_OPT_UNKNOWN, /**< Unknown */ +} aitt_option_e; + +/** + * @brief Set the contents of a @c handle related with @c option to @c value + * @detail The @c value can be NULL for removing the content + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] option value of @a aitt_option_e. + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + * + * @see aitt_get_option() + */ +int aitt_set_option(aitt_h handle, aitt_option_e option, const char *value); + +/** + * @brief Returns the string value of a @c handle related with @c option + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] option value of @a aitt_option_e. + * @return @c value related with @c option + * otherwise NULL value + * + * @see aitt_set_option() + */ +const char *aitt_get_option(aitt_h handle, aitt_option_e option); + +/** + * @brief Configure will information for a aitt instance. + * @detail By default, clients do not have a will. This must be called before calling aitt_connect() + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] topic the topic on which to publish the will. + * @param[in] msg pointer to the data to send. + * @param[in] msg_len the size of the @c msg (bytes). Valid values are between 0 and 268,435,455. + * @param[in] qos integer value 0, 1 or 2 indicating the Quality of Service. + * @param[in] retain set to true to make the will a retained message. + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + * + * @see aitt_connect() + */ +int aitt_will_set(aitt_h handle, const char *topic, const void *msg, const size_t msg_len, + aitt_qos_e qos, bool retain); + +/** + * @brief Release memory of the AITT service instance. + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service; + * @see aitt_new() + */ +void aitt_destroy(aitt_h handle); + +/** + * @brief Connect to mqtt broker. + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] broker_ip IP address of the broker to connect to + * @param[in] port the network port to connect to. Usually 1883. + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + */ +int aitt_connect(aitt_h handle, const char *broker_ip, int port); + +/** + * @brief Connect to mqtt broker as aitt_connect(), but takes username and password. + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] broker_ip IP address of the broker to connect to + * @param[in] port the network port to connect to. Usually 1883. + * @param[in] username the username to send as a string, or NULL to disable authentication + * @param[in] password the password to send as a string + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + */ +int aitt_connect_full(aitt_h handle, const char *broker_ip, int port, const char *username, + const char *password); + +/** + * @brief Disconnect from the broker. + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + */ +int aitt_disconnect(aitt_h handle); + +/** + * @brief Publish a message on a given topic using MQTT, QoS0(At most once). + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] topic null terminated string of the topic to publish to. + * @param[in] msg pointer to the data to send. + * @param[in] msg_len the size of the @c msg (bytes). Valid values are between 0 and 268,435,455. + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + */ +int aitt_publish(aitt_h handle, const char *topic, const void *msg, const size_t msg_len); + +/** + * @brief Publish a message on a given topic as aitt_publish(), but takes protocols and qos. + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] topic null terminated string of the topic to publish to. + * @param[in] msg pointer to the data to send. + * @param[in] msg_len the size of the @c msg (bytes). Valid values are between 0 and 268,435,455. + * @param[in] protocols value of @a aitt_protocol_e. The value can be bitwise-or'd. + * @param[in] qos integer value 0, 1 or 2 indicating the Quality of Service. + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + */ +int aitt_publish_full(aitt_h handle, const char *topic, const void *msg, const size_t msg_len, + int protocols, aitt_qos_e qos); + +/** + * @brief Publish a message on a given topic as aitt_publish_full(), + * but takes reply topic and callback for the reply. + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] topic null terminated string of the topic to publish to. + * @param[in] msg pointer to the data to send. + * @param[in] msg_len the size of the @c msg (bytes). Valid values are between 0 and 268,435,455. + * @param[in] protocols value of @a aitt_protocol_e. The value can be bitwise-or'd. + * @param[in] qos integer value 0, 1 or 2 indicating the Quality of Service. + * @param[in] correlation value indicating the Correlation. + * @param[in] cb The callback function to invoke + * @param[in] user_data The user data to pass to the function + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + */ +int aitt_publish_with_reply(aitt_h handle, const char *topic, const void *msg, const size_t msg_len, + aitt_protocol_e protocols, aitt_qos_e qos, const char *correlation, aitt_sub_fn cb, + void *user_data); + +/** + * @brief Send reply message to regarding topic. + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] msg_handle Handle of published message(to reply). + * @param[in] reply pointer to the data to send. + * @param[in] reply_len the size of the @c reply (bytes). + * Valid values are between 0 and 268,435,455. + * @param[in] end boolean value indicating the reply message is end or not. + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + */ +int aitt_send_reply(aitt_h handle, aitt_msg_h msg_handle, const void *reply, const size_t reply_len, + bool end); + +/** + * @brief Get topic name from @c handle + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle aitt message handle + * @return topic name on success otherwise NULL value on failure + */ +const char *aitt_msg_get_topic(aitt_msg_h handle); + +/** + * @brief Subscribe to a topic on MQTT with QoS0(at most once). + * @details Sets a function to be called when the aitt get messages related with @c topic + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] topic null terminated string of the topic to subscribe to. + * @param[in] cb The callback function to invoke + * @param[in] user_data The user data to pass to the function + * @param[out] sub_handle Handle of subscribed topic + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + */ +int aitt_subscribe(aitt_h handle, const char *topic, aitt_sub_fn cb, void *user_data, + aitt_sub_h *sub_handle); + +/** + * @brief Subscribe to a topic, but takes protocols and qos. + * @details Sets a function to be called when the aitt get messages related with @c topic + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] topic null terminated string of the topic to subscribe to. + * @param[in] cb The callback function to invoke + * @param[in] user_data The user data to pass to the function + * @param[in] protocols value of @a aitt_protocol_e. + * @param[in] qos integer value 0, 1 or 2 indicating the Quality of Service. + * @param[out] sub_handle Handle of subscribed topic + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + */ +int aitt_subscribe_full(aitt_h handle, const char *topic, aitt_sub_fn cb, void *user_data, + aitt_protocol_e protocols, aitt_qos_e qos, aitt_sub_h *sub_handle); + +/** + * @brief Unsubscribe from a topic. + * @details Removes the subscription of changes with given ID. + * @since_tizen 7.0 + * @privlevel public + * @param[in] handle Handle of AITT service + * @param[in] sub_handle Handle of subscribed topic + * @return @c 0 on success + * otherwise a negative error value + * @retval #AITT_ERROR_NONE Success + * @retval #AITT_ERROR_INVALID_PARAMETER Invalid parameter + * @retval #AITT_ERROR_SYSTEM System errors + * + * @see aitt_subscribe(), aitt_subscribe_full() + */ +int aitt_unsubscribe(aitt_h handle, aitt_sub_h sub_handle); + +/** + * @} + */ + +#ifdef __cplusplus +} +#endif diff --git a/mock/MQMockTest.h b/mock/MQMockTest.h new file mode 100644 index 0000000..307640b --- /dev/null +++ b/mock/MQMockTest.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <gmock/gmock.h> + +#include "MQTTMock.h" + +class MQMockTest : public ::testing::Test { + protected: + void SetUp() override { mqttMock = new MQTTMock; } + + void TearDown() override + { + delete mqttMock; + mqttMock = nullptr; + } + + public: + static MQTTMock &GetMock(void) { return *mqttMock; } + + private: + static MQTTMock *mqttMock; +}; diff --git a/mock/MQTTMock.h b/mock/MQTTMock.h new file mode 100644 index 0000000..10d484e --- /dev/null +++ b/mock/MQTTMock.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <gmock/gmock.h> + +class MQTTMock { + public: + MQTTMock(void) = default; + virtual ~MQTTMock(void) = default; + + MOCK_METHOD0(mosquitto_lib_init, int(void)); + MOCK_METHOD0(mosquitto_lib_cleanup, int(void)); + MOCK_METHOD3(mosquitto_new, struct mosquitto *(const char *id, bool clean_session, void *obj)); + MOCK_METHOD3(mosquitto_int_option, int(struct mosquitto *mosq, int option, int value)); + MOCK_METHOD1(mosquitto_destroy, void(struct mosquitto *mosq)); + MOCK_METHOD3(mosquitto_username_pw_set, + int(struct mosquitto *mosq, const char *username, const char *password)); + MOCK_METHOD6(mosquitto_will_set, int(struct mosquitto *mosq, const char *topic, int payloadlen, + const void *payload, int qos, bool retain)); + MOCK_METHOD1(mosquitto_will_clear, int(struct mosquitto *mosq)); + MOCK_METHOD4(mosquitto_connect, + int(struct mosquitto *mosq, const char *host, int port, int keepalive)); + MOCK_METHOD1(mosquitto_disconnect, int(struct mosquitto *mosq)); + MOCK_METHOD7(mosquitto_publish, + int(struct mosquitto *mosq, int *mid, const char *topic, int payloadlen, + const void *payload, int qos, bool retain)); + MOCK_METHOD4(mosquitto_subscribe, + int(struct mosquitto *mosq, int *mid, const char *sub, int qos)); + MOCK_METHOD3(mosquitto_unsubscribe, int(struct mosquitto *mosq, int *mid, const char *sub)); + MOCK_METHOD1(mosquitto_loop_start, int(struct mosquitto *mosq)); + MOCK_METHOD2(mosquitto_loop_stop, int(struct mosquitto *mosq, bool force)); + MOCK_METHOD2(mosquitto_message_v5_callback_set, + void(struct mosquitto *mosq, + void (*on_message)(struct mosquitto *, void *, const struct mosquitto_message *, + const struct mqtt5__property *))); +}; diff --git a/mock/mosquitto.cc b/mock/mosquitto.cc new file mode 100644 index 0000000..5ea4f1b --- /dev/null +++ b/mock/mosquitto.cc @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include <cstring> + +#include "MQMockTest.h" +#include "MQTTMock.h" + +MQTTMock *MQMockTest::mqttMock = nullptr; + +extern "C" { + +int mosquitto_lib_init(void) +{ + return MQMockTest::GetMock().mosquitto_lib_init(); +} + +int mosquitto_lib_cleanup(void) +{ + return MQMockTest::GetMock().mosquitto_lib_cleanup(); +} + +struct mosquitto *mosquitto_new(const char *id, bool clean_session, void *obj) +{ + return MQMockTest::GetMock().mosquitto_new(id, clean_session, obj); +} + +int mosquitto_int_option(struct mosquitto *mosq, int option, int value) +{ + return MQMockTest::GetMock().mosquitto_int_option(mosq, option, value); +} + +void mosquitto_destroy(struct mosquitto *mosq) +{ + return MQMockTest::GetMock().mosquitto_destroy(mosq); +} + +int mosquitto_username_pw_set(struct mosquitto *mosq, const char *username, const char *password) +{ + return MQMockTest::GetMock().mosquitto_username_pw_set(mosq, username, password); +} + +int mosquitto_will_set(struct mosquitto *mosq, const char *topic, int payloadlen, + const void *payload, int qos, bool retain) +{ + return MQMockTest::GetMock().mosquitto_will_set(mosq, topic, payloadlen, payload, qos, retain); +} + +int mosquitto_will_clear(struct mosquitto *mosq) +{ + return MQMockTest::GetMock().mosquitto_will_clear(mosq); +} + +int mosquitto_connect(struct mosquitto *mosq, const char *host, int port, int keepalive) +{ + return MQMockTest::GetMock().mosquitto_connect(mosq, host, port, keepalive); +} + +int mosquitto_disconnect(struct mosquitto *mosq) +{ + return MQMockTest::GetMock().mosquitto_disconnect(mosq); +} + +int mosquitto_publish(struct mosquitto *mosq, int *mid, const char *topic, int payloadlen, + const void *payload, int qos, bool retain) +{ + return MQMockTest::GetMock().mosquitto_publish(mosq, mid, topic, payloadlen, payload, qos, + retain); +} + +int mosquitto_subscribe(struct mosquitto *mosq, int *mid, const char *sub, int qos) +{ + return MQMockTest::GetMock().mosquitto_subscribe(mosq, mid, sub, qos); +} + +int mosquitto_unsubscribe(struct mosquitto *mosq, int *mid, const char *sub) +{ + return MQMockTest::GetMock().mosquitto_unsubscribe(mosq, mid, sub); +} + +int mosquitto_loop_start(struct mosquitto *mosq) +{ + return MQMockTest::GetMock().mosquitto_loop_start(mosq); +} + +int mosquitto_loop_stop(struct mosquitto *mosq, bool force) +{ + return MQMockTest::GetMock().mosquitto_loop_stop(mosq, force); +} + +void mosquitto_message_v5_callback_set(struct mosquitto *mosq, + void (*on_message)(struct mosquitto *, void *, const struct mosquitto_message *, + const struct mqtt5__property *)) +{ + return MQMockTest::GetMock().mosquitto_message_v5_callback_set(mosq, on_message); +} + +} // extern "C" diff --git a/modules/main.cc b/modules/main.cc new file mode 100644 index 0000000..86e9c36 --- /dev/null +++ b/modules/main.cc @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include <assert.h> + +#include "Module.h" +#include "aitt_internal_definitions.h" + +extern "C" { + +// Function name Should be same with aitt::AittTransport::MODULE_ENTRY_NAME +API void *aitt_module_entry(const char *ip, AittDiscovery &discovery) +{ + assert(!strcmp(__func__, aitt::AittTransport::MODULE_ENTRY_NAME) + && "Entry point name is not matched"); + + std::string ip_address(ip); + Module *module = new Module(ip_address, discovery); + + AittTransport *tModule = dynamic_cast<AittTransport *>(module); + // NOTE: + // validate that the module creates valid object (which inherits AittTransport) + assert(tModule && "Transport Module is not created"); + + return tModule; +} + +} // extern "C" diff --git a/modules/tcp/CMakeLists.txt b/modules/tcp/CMakeLists.txt new file mode 100644 index 0000000..edac2fd --- /dev/null +++ b/modules/tcp/CMakeLists.txt @@ -0,0 +1,14 @@ +SET(AITT_TCP aitt-transport-tcp) + +INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}) + +ADD_LIBRARY(TCP_OBJ OBJECT TCP.cc TCPServer.cc) +ADD_LIBRARY(${AITT_TCP} SHARED ../main.cc Module.cc $<TARGET_OBJECTS:TCP_OBJ>) +TARGET_LINK_LIBRARIES(${AITT_TCP} ${AITT_TCP_NEEDS_LIBRARIES} Threads::Threads ${AITT_COMMON}) + +INSTALL(TARGETS ${AITT_TCP} DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +IF(BUILD_TESTING) + ADD_SUBDIRECTORY(samples) + ADD_SUBDIRECTORY(tests) +ENDIF(BUILD_TESTING) diff --git a/modules/tcp/Module.cc b/modules/tcp/Module.cc new file mode 100644 index 0000000..bc50d7d --- /dev/null +++ b/modules/tcp/Module.cc @@ -0,0 +1,513 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "Module.h" + +#include <MQ.h> +#include <flatbuffers/flexbuffers.h> +#include <unistd.h> + +#include "aitt_internal.h" + +/* + * P2P Data Packet Definition + * TopicLength: 4 bytes + * TopicString: $TopicLength + */ + +Module::Module(const std::string &ip, AittDiscovery &discovery) : AittTransport(discovery), ip(ip) +{ + aittThread = std::thread(&Module::ThreadMain, this); + + discovery_cb = discovery.AddDiscoveryCB(AITT_TYPE_TCP, + std::bind(&Module::DiscoveryMessageCallback, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); + DBG("Discovery Callback : %p, %d", this, discovery_cb); +} + +Module::~Module(void) +{ + discovery.RemoveDiscoveryCB(discovery_cb); + + while (main_loop.Quit() == false) { + // wait when called before the thread has completely created. + usleep(1000); + } + + if (aittThread.joinable()) + aittThread.join(); +} + +void Module::ThreadMain(void) +{ + pthread_setname_np(pthread_self(), "TCPWorkerLoop"); + main_loop.Run(); +} + +void Module::Publish(const std::string &topic, const void *data, const size_t datalen, + const std::string &correlation, AittQoS qos, bool retain) +{ + // NOTE: + // Iterate discovered service table + // PublishMap + // map { + // "/customTopic/faceRecog": map { + // "$clientId": map { + // 11234: $handle, + // + // ... + // + // 21234: nullptr, + // }, + // }, + // } + std::lock_guard<std::mutex> auto_lock_publish(publishTableLock); + for (PublishMap::iterator it = publishTable.begin(); it != publishTable.end(); ++it) { + // NOTE: + // Find entries that have matched with the given topic + if (!aitt::MQ::CompareTopic(it->first, topic)) + continue; + + // NOTE: + // Iterate all hosts + for (HostMap::iterator hostIt = it->second.begin(); hostIt != it->second.end(); ++hostIt) { + // Iterate all ports, + // the current implementation only be able to have the ZERO or a SINGLE entry + // hostIt->first // clientId + for (PortMap::iterator portIt = hostIt->second.begin(); portIt != hostIt->second.end(); + ++portIt) { + // portIt->first // port + // portIt->second // handle + if (!portIt->second) { + std::string host; + { + ClientMap::iterator clientIt; + std::lock_guard<std::mutex> auto_lock_client(clientTableLock); + + clientIt = clientTable.find(hostIt->first); + if (clientIt != clientTable.end()) + host = clientIt->second; + + // NOTE: + // otherwise, it is a critical error + // The broken clientTable or subscribeTable + } + + std::unique_ptr<TCP> client(std::make_unique<TCP>(host, portIt->first)); + + // TODO: + // If the client gets disconnected, + // This channel entry must be cleared + // In order to do that, + // There should be an observer to monitor + // each connections and manipulate + // the discovered service table + portIt->second = std::move(client); + } + + if (!portIt->second) { + ERR("Failed to create a new client instance"); + continue; + } + + SendTopic(topic, portIt); + SendPayload(datalen, portIt, data); + } + } // connectionEntries + } // publishTable +} + +void Module::SendTopic(const std::string &topic, Module::PortMap::iterator &portIt) +{ + uint32_t topicLen = topic.length(); + size_t szData = sizeof(topicLen); + portIt->second->Send(static_cast<void *>(&topicLen), szData); + szData = topicLen; + portIt->second->Send(static_cast<const void *>(topic.c_str()), szData); +} + +void Module::SendPayload(const size_t &datalen, Module::PortMap::iterator &portIt, const void *data) +{ + uint32_t sendsize = datalen; + size_t szsize = sizeof(sendsize); + + try { + if (0 == datalen) { + // distinguish between connection problems and zero-size messages + INFO("Send zero-size Message"); + sendsize = UINT32_MAX; + } + portIt->second->Send(static_cast<void *>(&sendsize), szsize); + + int msgSize = datalen; + while (0 < msgSize) { + size_t sentSize = msgSize; + char *dataIdx = (char *)data + (sendsize - msgSize); + portIt->second->Send(dataIdx, sentSize); + if (sentSize > 0) { + msgSize -= sentSize; + } + } + } catch (std::exception &e) { + ERR("An exception(%s) occurs during Send().", e.what()); + } +} + +void Module::Publish(const std::string &topic, const void *data, const size_t datalen, AittQoS qos, + bool retain) +{ + Publish(topic, data, datalen, std::string(), qos, retain); +} + +void *Module::Subscribe(const std::string &topic, const AittTransport::SubscribeCallback &cb, + void *cbdata, AittQoS qos) +{ + std::unique_ptr<TCP::Server> tcpServer; + + unsigned short port = 0; + tcpServer = std::make_unique<TCP::Server>("0.0.0.0", port); + TCPServerData *listen_info = new TCPServerData; + listen_info->impl = this; + listen_info->cb = cb; + listen_info->cbdata = cbdata; + listen_info->topic = topic; + auto handle = tcpServer->GetHandle(); + + main_loop.AddWatch(handle, AcceptConnection, listen_info); + + // 서비스 테이블에 토픽을 키워드로 프로토콜을 등록한다. + { + std::lock_guard<std::mutex> autoLock(subscribeTableLock); + subscribeTable.insert(SubscribeMap::value_type(topic, std::move(tcpServer))); + UpdateDiscoveryMsg(); + } + + return reinterpret_cast<void *>(handle); +} + +void *Module::Subscribe(const std::string &topic, const AittTransport::SubscribeCallback &cb, + const void *data, const size_t datalen, void *cbdata, AittQoS qos) +{ + return nullptr; +} + +void *Module::Unsubscribe(void *handlePtr) +{ + int handle = static_cast<int>(reinterpret_cast<intptr_t>(handlePtr)); + TCPServerData *listen_info = dynamic_cast<TCPServerData *>(main_loop.RemoveWatch(handle)); + if (!listen_info) + return nullptr; + + { + std::lock_guard<std::mutex> autoLock(subscribeTableLock); + auto it = subscribeTable.find(listen_info->topic); + if (it == subscribeTable.end()) + throw std::runtime_error("Service is not registered: " + listen_info->topic); + + subscribeTable.erase(it); + + UpdateDiscoveryMsg(); + } + + void *cbdata = listen_info->cbdata; + listen_info->client_lock.lock(); + for (auto fd : listen_info->client_list) { + TCPData *connect_info = dynamic_cast<TCPData *>(main_loop.RemoveWatch(fd)); + delete connect_info; + } + listen_info->client_list.clear(); + listen_info->client_lock.unlock(); + delete listen_info; + + return cbdata; +} + +void Module::DiscoveryMessageCallback(const std::string &clientId, const std::string &status, + const void *msg, const int szmsg) +{ + // NOTE: + // Iterate discovered service table + // PublishMap + // map { + // "/customTopic/faceRecog": map { + // "clientId.uniq.abcd.123": map { + // 11234: pair { + // "protocol": 1, + // "handle": nullptr, + // }, + // + // ... + // + // 21234: pair { + // "protocol": 2, + // "handle": nullptr, + // } + // }, + // }, + // } + + if (!status.compare(AittDiscovery::WILL_LEAVE_NETWORK)) { + { + std::lock_guard<std::mutex> autoLock(clientTableLock); + // Delete from the { clientId : Host } mapping table + clientTable.erase(clientId); + } + + { + // NOTE: + // Iterate all topics in the publishTable holds discovered client information + std::lock_guard<std::mutex> autoLock(publishTableLock); + for (auto it = publishTable.begin(); it != publishTable.end(); ++it) + it->second.erase(clientId); + } + return; + } + + // serviceMessage (flexbuffers) + // map { + // "host": "192.168.1.11", + // "$topic": port, + // } + auto map = flexbuffers::GetRoot(static_cast<const uint8_t *>(msg), szmsg).AsMap(); + std::string host = map["host"].AsString().c_str(); + + // NOTE: + // Update the clientTable + { + std::lock_guard<std::mutex> autoLock(clientTableLock); + auto clientIt = clientTable.find(clientId); + if (clientIt == clientTable.end()) + clientTable.insert(ClientMap::value_type(clientId, host)); + else if (clientIt->second.compare(host)) + clientIt->second = host; + } + + auto topics = map.Keys(); + for (size_t idx = 0; idx < topics.size(); ++idx) { + std::string topic = topics[idx].AsString().c_str(); + + if (!topic.compare("host")) + continue; + + auto port = map[topic].AsUInt16(); + + { + std::lock_guard<std::mutex> autoLock(publishTableLock); + UpdatePublishTable(topic, clientId, port); + } + } +} + +void Module::UpdateDiscoveryMsg() +{ + flexbuffers::Builder fbb; + // flexbuffers + // { + // "host": "127.0.0.1", + // "/customTopic/aitt/faceRecog": $port, + // "/customTopic/aitt/ASR": 102020, + // + // ... + // + // "/customTopic/aitt/+": 20123, + // } + fbb.Map([this, &fbb]() { + fbb.String("host", ip); + + // SubscribeTable + // map { + // "/customTopic/mytopic": $serverHandle, + // ... + // } + for (auto it = subscribeTable.begin(); it != subscribeTable.end(); ++it) { + if (it->second) + fbb.UInt(it->first.c_str(), it->second->GetPort()); + else + fbb.UInt(it->first.c_str(), 0); // this is an error case + } + }); + fbb.Finish(); + + auto buf = fbb.GetBuffer(); + discovery.UpdateDiscoveryMsg(AITT_TYPE_TCP, buf.data(), buf.size()); +} + +void Module::ReceiveData(MainLoopHandler::MainLoopResult result, int handle, + MainLoopHandler::MainLoopData *user_data) +{ + TCPData *connect_info = dynamic_cast<TCPData *>(user_data); + RET_IF(connect_info == nullptr); + TCPServerData *parent_info = connect_info->parent; + RET_IF(parent_info == nullptr); + Module *impl = parent_info->impl; + RET_IF(impl == nullptr); + + if (result == MainLoopHandler::HANGUP) { + ERR("Disconnected"); + return impl->HandleClientDisconnect(handle); + } + + uint32_t szmsg = 0; + size_t szdata = sizeof(szmsg); + char *msg = nullptr; + std::string topic; + + try { + topic = impl->GetTopicName(connect_info); + if (topic.empty()) { + ERR("Unknown Topic"); + return impl->HandleClientDisconnect(handle); + } + + connect_info->client->Recv(static_cast<void *>(&szmsg), szdata); + if (szmsg == 0) { + ERR("Disconnected"); + return impl->HandleClientDisconnect(handle); + } + + if (UINT32_MAX == szmsg) { + // distinguish between connection problems and zero-size messages + INFO("Got zero-size Message"); + szmsg = 0; + } + + msg = static_cast<char *>(malloc(szmsg)); + int msgSize = szmsg; + while (0 < msgSize) { + size_t receivedSize = msgSize; + connect_info->client->Recv(static_cast<void *>(msg + (szmsg - msgSize)), receivedSize); + if (receivedSize > 0) { + msgSize -= receivedSize; + } + } + } catch (std::exception &e) { + ERR("An exception(%s) occurs during Recv()", e.what()); + } + + std::string correlation; + // TODO: + // Correlation data (string) should be filled + + parent_info->cb(topic, msg, szmsg, parent_info->cbdata, correlation); + free(msg); +} + +void Module::HandleClientDisconnect(int handle) +{ + TCPData *connect_info = dynamic_cast<TCPData *>(main_loop.RemoveWatch(handle)); + if (connect_info == nullptr) { + ERR("No watch data"); + return; + } + connect_info->parent->client_lock.lock(); + auto it = std::find(connect_info->parent->client_list.begin(), + connect_info->parent->client_list.end(), handle); + connect_info->parent->client_list.erase(it); + connect_info->parent->client_lock.unlock(); + + delete connect_info; +} + +std::string Module::GetTopicName(Module::TCPData *connect_info) +{ + uint32_t topic_len = 0; + size_t data_size = sizeof(topic_len); + connect_info->client->Recv(static_cast<void *>(&topic_len), data_size); + + if (AITT_TOPIC_NAME_MAX < topic_len) { + ERR("Invalid topic name length(%d)", topic_len); + return std::string(); + } + + char data[topic_len]; + data_size = topic_len; + connect_info->client->Recv(data, data_size); + if (data_size != topic_len) + ERR("Recv() Fail"); + + return std::string(data, data_size); +} + +void Module::AcceptConnection(MainLoopHandler::MainLoopResult result, int handle, + MainLoopHandler::MainLoopData *user_data) +{ + // TODO: + // Update the discovery map + std::unique_ptr<TCP> client; + + TCPServerData *listen_info = dynamic_cast<TCPServerData *>(user_data); + Module *impl = listen_info->impl; + { + std::lock_guard<std::mutex> autoLock(impl->subscribeTableLock); + + auto clientIt = impl->subscribeTable.find(listen_info->topic); + if (clientIt == impl->subscribeTable.end()) + return; + + client = clientIt->second->AcceptPeer(); + } + + if (client == nullptr) { + ERR("Unable to accept a peer"); // NOTE: FATAL ERROR + return; + } + + int cHandle = client->GetHandle(); + listen_info->client_list.push_back(cHandle); + + TCPData *ecd = new TCPData; + ecd->parent = listen_info; + ecd->client = std::move(client); + + impl->main_loop.AddWatch(cHandle, ReceiveData, ecd); +} + +void Module::UpdatePublishTable(const std::string &topic, const std::string &clientId, + unsigned short port) +{ + auto topicIt = publishTable.find(topic); + if (topicIt == publishTable.end()) { + PortMap portMap; + portMap.insert(PortMap::value_type(port, nullptr)); + HostMap hostMap; + hostMap.insert(HostMap::value_type(clientId, std::move(portMap))); + publishTable.insert(PublishMap::value_type(topic, std::move(hostMap))); + return; + } + + auto hostIt = topicIt->second.find(clientId); + if (hostIt == topicIt->second.end()) { + PortMap portMap; + portMap.insert(PortMap::value_type(port, nullptr)); + topicIt->second.insert(HostMap::value_type(clientId, std::move(portMap))); + return; + } + + // NOTE: + // The current implementation only has a single port entry + // therefore, if the hostIt is not empty, there is the previous connection + if (!hostIt->second.empty()) { + auto portIt = hostIt->second.begin(); + + if (portIt->first == port) + return; // nothing changed. keep the current handle + + // otherwise, delete the connection handle + // to make a new connection with the new port + hostIt->second.clear(); + } + + hostIt->second.insert(PortMap::value_type(port, nullptr)); +} diff --git a/modules/tcp/Module.h b/modules/tcp/Module.h new file mode 100644 index 0000000..4011980 --- /dev/null +++ b/modules/tcp/Module.h @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <AittTransport.h> +#include <MainLoopHandler.h> + +#include <map> +#include <memory> +#include <mutex> +#include <string> +#include <thread> + +#include "TCPServer.h" + +using AittTransport = aitt::AittTransport; +using MainLoopHandler = aitt::MainLoopHandler; +using AittDiscovery = aitt::AittDiscovery; + +class Module : public AittTransport { + public: + explicit Module(const std::string &ip, AittDiscovery &discovery); + virtual ~Module(void); + + void Publish(const std::string &topic, const void *data, const size_t datalen, + const std::string &correlation, AittQoS qos = AITT_QOS_AT_MOST_ONCE, + bool retain = false) override; + + void Publish(const std::string &topic, const void *data, const size_t datalen, + AittQoS qos = AITT_QOS_AT_MOST_ONCE, bool retain = false) override; + + void *Subscribe(const std::string &topic, const SubscribeCallback &cb, void *cbdata = nullptr, + AittQoS qos = AITT_QOS_AT_MOST_ONCE) override; + + void *Subscribe(const std::string &topic, const SubscribeCallback &cb, const void *data, + const size_t datalen, void *cbdata = nullptr, + AittQoS qos = AITT_QOS_AT_MOST_ONCE) override; + void *Unsubscribe(void *handle) override; + + private: + struct TCPServerData : public MainLoopHandler::MainLoopData { + Module *impl; + SubscribeCallback cb; + void *cbdata; + std::string topic; + std::vector<int> client_list; + std::mutex client_lock; + }; + + struct TCPData : public MainLoopHandler::MainLoopData { + TCPServerData *parent; + std::unique_ptr<TCP> client; + }; + + // SubscribeTable + // map { + // "/customTopic/mytopic": $serverHandle, + // ... + // } + using SubscribeMap = std::map<std::string, std::unique_ptr<TCP::Server>>; + + // ClientTable + // map { + // $clientId: $host, + // "client.uniqId.123": "192.168.1.11" + // ... + // } + using ClientMap = std::map<std::string /* id */, std::string /* host */>; + + // NOTE: + // There could be multiple clientIds for the single host + // If several applications are run on the same device, each applicaion will get unique client + // Ids therefore we have to keep in mind that the clientId is not 1:1 matched for the IPAddress. + + // PublishTable + // map { + // "/customTopic/faceRecog": map { + // $clientId: map { + // 11234: $clientHandle, + // + // ... + // + // 21234: $clientHandle, + // }, + // }, + // } + // + // NOTE: + // TCP handle should be the unique_ptr, so if we delete the entry from the map, + // the handle must be released automatically + // in order to make the handle "unique_ptr", it should be a class object not the "void *" + using PortMap = std::map<unsigned short /* port */, std::unique_ptr<TCP>>; + using HostMap = std::map<std::string /* clientId */, PortMap>; + using PublishMap = std::map<std::string /* topic */, HostMap>; + + static void AcceptConnection(MainLoopHandler::MainLoopResult result, int handle, + MainLoopHandler::MainLoopData *watchData); + void DiscoveryMessageCallback(const std::string &clientId, const std::string &status, + const void *msg, const int szmsg); + void UpdateDiscoveryMsg(); + static void ReceiveData(MainLoopHandler::MainLoopResult result, int handle, + MainLoopHandler::MainLoopData *watchData); + void HandleClientDisconnect(int handle); + std::string GetTopicName(TCPData *connect_info); + void ThreadMain(void); + void SendPayload(const size_t &datalen, Module::PortMap::iterator &portIt, const void *data); + void SendTopic(const std::string &topic, Module::PortMap::iterator &portIt); + + void UpdatePublishTable(const std::string &topic, const std::string &host, unsigned short port); + + MainLoopHandler main_loop; + std::thread aittThread; + std::string ip; + int discovery_cb; + + PublishMap publishTable; + std::mutex publishTableLock; + SubscribeMap subscribeTable; + std::mutex subscribeTableLock; + ClientMap clientTable; + std::mutex clientTableLock; +}; diff --git a/modules/tcp/TCP.cc b/modules/tcp/TCP.cc new file mode 100644 index 0000000..3b6751e --- /dev/null +++ b/modules/tcp/TCP.cc @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "TCP.h" + +#include <arpa/inet.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <unistd.h> + +#include <cstdlib> +#include <cstring> +#include <stdexcept> + +#include "aitt_internal.h" + +TCP::TCP(const std::string &host, unsigned short port) : handle(-1), addrlen(0), addr(nullptr) +{ + int ret = 0; + + do { + if (port == 0) { + ret = EINVAL; + break; + } + + handle = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (handle < 0) { + ERR("socket() Fail()"); + break; + } + + addrlen = sizeof(sockaddr_in); + addr = static_cast<sockaddr *>(calloc(1, addrlen)); + if (!addr) { + ERR("calloc() Fail()"); + break; + } + + sockaddr_in *inet_addr = reinterpret_cast<sockaddr_in *>(addr); + if (!inet_pton(AF_INET, host.c_str(), &inet_addr->sin_addr)) { + ret = EINVAL; + break; + } + + inet_addr->sin_port = htons(port); + inet_addr->sin_family = AF_INET; + + ret = connect(handle, addr, addrlen); + if (ret < 0) { + ERR("connect() Fail(%s, %d)", host.c_str(), port); + break; + } + + SetupOptions(); + return; + } while (0); + + if (ret <= 0) + ret = errno; + + free(addr); + if (handle >= 0 && close(handle) < 0) + ERR_CODE(errno, "close"); + throw std::runtime_error(strerror(ret)); +} + +TCP::TCP(int handle, sockaddr *addr, socklen_t szAddr) : handle(handle), addrlen(szAddr), addr(addr) +{ + SetupOptions(); +} + +TCP::~TCP(void) +{ + if (handle < 0) + return; + + free(addr); + if (close(handle) < 0) + ERR_CODE(errno, "close"); +} + +void TCP::SetupOptions(void) +{ + int on = 1; + + int ret = setsockopt(handle, IPPROTO_IP, TCP_NODELAY, &on, sizeof(on)); + if (ret < 0) { + ERR_CODE(errno, "delay option setting failed"); + } +} + +void TCP::Send(const void *data, size_t &szData) +{ + int ret = send(handle, data, szData, 0); + if (ret < 0) { + ERR("Fail to send data, handle = %d, size = %zu", handle, szData); + throw std::runtime_error(strerror(errno)); + } + + szData = ret; +} + +void TCP::Recv(void *data, size_t &szData) +{ + int ret = recv(handle, data, szData, 0); + if (ret < 0) { + ERR("Fail to recv data, handle = %d, size = %zu", handle, szData); + throw std::runtime_error(strerror(errno)); + } + + szData = ret; +} + +int TCP::GetHandle(void) +{ + return handle; +} + +void TCP::GetPeerInfo(std::string &host, unsigned short &port) +{ + char address[INET_ADDRSTRLEN] = { + 0, + }; + + if (!inet_ntop(AF_INET, &reinterpret_cast<sockaddr_in *>(this->addr)->sin_addr, address, + sizeof(address))) + throw std::runtime_error(strerror(errno)); + + port = ntohs(reinterpret_cast<sockaddr_in *>(this->addr)->sin_port); + host = address; +} + +unsigned short TCP::GetPort(void) +{ + sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + + if (getsockname(handle, reinterpret_cast<sockaddr *>(&addr), &addrlen) < 0) + throw std::runtime_error(strerror(errno)); + + return ntohs(addr.sin_port); +} diff --git a/modules/tcp/TCP.h b/modules/tcp/TCP.h new file mode 100644 index 0000000..535819c --- /dev/null +++ b/modules/tcp/TCP.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <sys/socket.h> +#include <sys/types.h> /* See NOTES */ + +#include <string> + +class TCP { + public: + class Server; + + TCP(const std::string &host, unsigned short port); + virtual ~TCP(void); + + void Send(const void *data, size_t &szData); + void Recv(void *data, size_t &szData); + int GetHandle(void); + unsigned short GetPort(void); + void GetPeerInfo(std::string &host, unsigned short &port); + + private: + TCP(int handle, sockaddr *addr, socklen_t addrlen); + void SetupOptions(void); + + int handle; + socklen_t addrlen; + sockaddr *addr; +}; diff --git a/modules/tcp/TCPServer.cc b/modules/tcp/TCPServer.cc new file mode 100644 index 0000000..55f8511 --- /dev/null +++ b/modules/tcp/TCPServer.cc @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "TCPServer.h" + +#include <arpa/inet.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <unistd.h> + +#include <cstdlib> +#include <stdexcept> + +#include "aitt_internal.h" + +#define BACKLOG 10 // Accept only 10 simultaneously connections by default + +TCP::Server::Server(const std::string &host, unsigned short &port) + : handle(-1), addr(nullptr), addrlen(0) +{ + int ret = 0; + + do { + handle = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (handle < 0) + break; + + addrlen = sizeof(sockaddr_in); + addr = static_cast<sockaddr *>(calloc(1, sizeof(sockaddr_in))); + if (!addr) + break; + + sockaddr_in *inet_addr = reinterpret_cast<sockaddr_in *>(addr); + if (!inet_pton(AF_INET, host.c_str(), &inet_addr->sin_addr)) { + ret = EINVAL; + break; + } + + inet_addr->sin_port = htons(port); + inet_addr->sin_family = AF_INET; + + int on = 1; + ret = setsockopt(handle, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); + if (ret < 0) + break; + + ret = bind(handle, addr, addrlen); + if (ret < 0) + break; + + if (!port) { + if (getsockname(handle, addr, &addrlen) < 0) + break; + port = ntohs(inet_addr->sin_port); + } + + ret = listen(handle, BACKLOG); + if (ret < 0) + break; + + return; + } while (0); + + if (ret <= 0) + ret = errno; + + free(addr); + + if (handle >= 0 && close(handle) < 0) + ERR_CODE(errno, "close"); + + throw std::runtime_error(strerror(ret)); +} + +TCP::Server::~Server(void) +{ + if (handle < 0) + return; + + free(addr); + if (close(handle) < 0) + ERR_CODE(errno, "close"); +} + +std::unique_ptr<TCP> TCP::Server::AcceptPeer(void) +{ + sockaddr *peerAddr; + socklen_t szAddr = sizeof(sockaddr_in); + int peerHandle; + + peerAddr = static_cast<sockaddr *>(calloc(1, szAddr)); + if (!peerAddr) + throw std::runtime_error(strerror(errno)); + + peerHandle = accept(handle, peerAddr, &szAddr); + if (peerHandle < 0) { + free(peerAddr); + throw std::runtime_error(strerror(errno)); + } + + return std::unique_ptr<TCP>(new TCP(peerHandle, peerAddr, szAddr)); +} + +int TCP::Server::GetHandle(void) +{ + return handle; +} + +unsigned short TCP::Server::GetPort(void) +{ + sockaddr_in addr; + socklen_t addrlen = sizeof(addr); + + if (getsockname(handle, reinterpret_cast<sockaddr *>(&addr), &addrlen) < 0) + throw std::runtime_error(strerror(errno)); + + return ntohs(addr.sin_port); +} diff --git a/modules/tcp/TCPServer.h b/modules/tcp/TCPServer.h new file mode 100644 index 0000000..3c82bc6 --- /dev/null +++ b/modules/tcp/TCPServer.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <memory> +#include <string> + +#include "TCP.h" + +class TCP::Server { + public: + Server(const std::string &host, unsigned short &port); + virtual ~Server(void); + + std::unique_ptr<TCP> AcceptPeer(void); + + int GetHandle(void); + unsigned short GetPort(void); + + private: + int handle; + sockaddr *addr; + socklen_t addrlen; +}; diff --git a/modules/tcp/samples/CMakeLists.txt b/modules/tcp/samples/CMakeLists.txt new file mode 100644 index 0000000..8fd1b4b --- /dev/null +++ b/modules/tcp/samples/CMakeLists.txt @@ -0,0 +1,3 @@ +ADD_EXECUTABLE("aitt_tcp_test" tcp_test.cc $<TARGET_OBJECTS:TCP_OBJ>) +TARGET_LINK_LIBRARIES("aitt_tcp_test" ${PROJECT_NAME} Threads::Threads ${AITT_NEEDS_LIBRARIES}) +INSTALL(TARGETS "aitt_tcp_test" DESTINATION ${AITT_TEST_BINDIR}) diff --git a/modules/tcp/samples/tcp_test.cc b/modules/tcp/samples/tcp_test.cc new file mode 100644 index 0000000..d319e27 --- /dev/null +++ b/modules/tcp/samples/tcp_test.cc @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include <TCP.h> +#include <TCPServer.h> +#include <getopt.h> +#include <glib.h> + +#include <functional> +#include <iostream> +#include <memory> +#include <string> + +//#define _LOG_WITH_TIMESTAMP +#include "aitt_internal.h" +#ifdef _LOG_WITH_TIMESTAMP +__thread __aitt__tls__ __aitt; +#endif + +#define HELLO_STRING "hello" +#define BYE_STRING "bye" +#define SEND_INTERVAL 1000 + +class AittTcpSample { + public: + AittTcpSample(const std::string &host, unsigned short &port) + : server(std::make_unique<TCP::Server>(host, port)) + { + } + virtual ~AittTcpSample(void) {} + + std::unique_ptr<TCP::Server> server; +}; + +int main(int argc, char *argv[]) +{ + const option opts[] = { + { + .name = "server", + .has_arg = 0, + .flag = nullptr, + .val = 's', + }, + { + .name = "host", + .has_arg = 1, + .flag = nullptr, + .val = 'h', + }, + { + .name = "port", + .has_arg = 1, + .flag = nullptr, + .val = 'p', + }, + }; + int c; + int idx; + bool isServer = false; + std::string host = "127.0.0.1"; + unsigned short port = 0; + + while ((c = getopt_long(argc, argv, "sh:up:", opts, &idx)) != -1) { + switch (c) { + case 's': + isServer = true; + break; + case 'h': + host = optarg; + break; + case 'p': + port = std::stoi(optarg); + break; + default: + break; + } + } + + INFO("Host[%s] port[%u]", host.c_str(), port); + + struct EventData { + GSource source_; + GPollFD fd; + AittTcpSample *sample; + }; + + guint timeoutId = 0; + GSource *src = nullptr; + EventData *ed = nullptr; + + GMainLoop *mainLoop = g_main_loop_new(nullptr, FALSE); + if (!mainLoop) { + ERR("Failed to create a main loop"); + return 1; + } + + // Handling the server/client events + if (isServer) { + GSourceFuncs srcs = { + [](GSource *src, gint *timeout) -> gboolean { + *timeout = 1; + return FALSE; + }, + [](GSource *src) -> gboolean { + EventData *ed = reinterpret_cast<EventData *>(src); + RETV_IF(ed == nullptr, FALSE); + + if ((ed->fd.revents & G_IO_IN) == G_IO_IN) + return TRUE; + if ((ed->fd.revents & G_IO_ERR) == G_IO_ERR) + return TRUE; + + return FALSE; + }, + [](GSource *src, GSourceFunc callback, gpointer user_data) -> gboolean { + EventData *ed = reinterpret_cast<EventData *>(src); + RETV_IF(ed == nullptr, FALSE); + + if ((ed->fd.revents & G_IO_ERR) == G_IO_ERR) { + ERR("Error!"); + return FALSE; + } + + std::unique_ptr<TCP> peer = ed->sample->server->AcceptPeer(); + + INFO("Assigned port: %u, %u", ed->sample->server->GetPort(), peer->GetPort()); + std::string peerHost; + unsigned short peerPort = 0; + peer->GetPeerInfo(peerHost, peerPort); + INFO("Peer Info: %s %u", peerHost.c_str(), peerPort); + + char buffer[10]; + void *ptr = static_cast<void *>(buffer); + size_t szData = sizeof(HELLO_STRING); + peer->Recv(ptr, szData); + INFO("Gots[%s]", buffer); + + szData = sizeof(BYE_STRING); + peer->Send(BYE_STRING, szData); + INFO("Reply to client[%s]", BYE_STRING); + + return TRUE; + }, + nullptr, + }; + + src = g_source_new(&srcs, sizeof(EventData)); + if (!src) { + g_main_loop_unref(mainLoop); + ERR("g_source_new failed"); + return 1; + } + + ed = reinterpret_cast<EventData *>(src); + + try { + ed->sample = new AittTcpSample(host, port); + } catch (std::exception &e) { + ERR("new: %s", e.what()); + g_source_unref(src); + g_main_loop_unref(mainLoop); + return 1; + } + + INFO("host: %s, port: %u", host.c_str(), port); + + ed->fd.fd = ed->sample->server->GetHandle(); + ed->fd.events = G_IO_IN | G_IO_ERR; + g_source_add_poll(src, &ed->fd); + guint id = g_source_attach(src, g_main_loop_get_context(mainLoop)); + g_source_unref(src); + if (id == 0) { + delete ed->sample; + g_source_destroy(src); + g_main_loop_unref(mainLoop); + return 1; + } + } else { + static struct Main { + const std::string &host; + unsigned short port; + } main_data = { + .host = host, + .port = port, + }; + // Now the server is ready. + // Let's create a new client and communicate with the server within every + // SEND_INTERTVAL + timeoutId = g_timeout_add( + SEND_INTERVAL, + [](gpointer data) -> gboolean { + Main *ctx = static_cast<Main *>(data); + std::unique_ptr<TCP> client(std::make_unique<TCP>(ctx->host, ctx->port)); + + INFO("Assigned client port: %u", client->GetPort()); + + INFO("Send[%s]", HELLO_STRING); + size_t szBuffer = sizeof(HELLO_STRING); + client->Send(HELLO_STRING, szBuffer); + + char buffer[10]; + void *ptr = static_cast<void *>(buffer); + szBuffer = sizeof(BYE_STRING); + client->Recv(ptr, szBuffer); + INFO("Replied with[%s]", buffer); + + // Send oneshot message, and disconnect from the server + return TRUE; + }, + &main_data); + } + + g_main_loop_run(mainLoop); + + if (src) { + delete ed->sample; + g_source_destroy(src); + } + if (timeoutId) + g_source_remove(timeoutId); + g_main_loop_unref(mainLoop); + return 0; +} diff --git a/modules/tcp/tests/CMakeLists.txt b/modules/tcp/tests/CMakeLists.txt new file mode 100644 index 0000000..bf1adf1 --- /dev/null +++ b/modules/tcp/tests/CMakeLists.txt @@ -0,0 +1,19 @@ +PKG_CHECK_MODULES(UT_NEEDS REQUIRED gmock_main) +INCLUDE_DIRECTORIES(${UT_NEEDS_INCLUDE_DIRS}) +LINK_DIRECTORIES(${UT_NEEDS_LIBRARY_DIRS}) + +SET(AITT_TCP_UT ${PROJECT_NAME}_tcp_ut) + +SET(AITT_TCP_UT_SRC TCP_test.cc TCPServer_test.cc) + +ADD_EXECUTABLE(${AITT_TCP_UT} ${AITT_TCP_UT_SRC} $<TARGET_OBJECTS:TCP_OBJ>) +TARGET_LINK_LIBRARIES(${AITT_TCP_UT} ${UT_NEEDS_LIBRARIES} Threads::Threads ${AITT_NEEDS_LIBRARIES}) +INSTALL(TARGETS ${AITT_TCP_UT} DESTINATION ${AITT_TEST_BINDIR}) + +ADD_TEST( + NAME + ${AITT_TCP_UT} + COMMAND + ${CMAKE_COMMAND} -E env + ${CMAKE_CURRENT_BINARY_DIR}/${AITT_TCP_UT} --gtest_filter=*_Anytime +) diff --git a/modules/tcp/tests/TCPServer_test.cc b/modules/tcp/tests/TCPServer_test.cc new file mode 100644 index 0000000..e8b48b1 --- /dev/null +++ b/modules/tcp/tests/TCPServer_test.cc @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "../TCPServer.h" + +#include <gtest/gtest.h> + +#include <condition_variable> +#include <cstring> +#include <memory> +#include <mutex> +#include <thread> + +#define TEST_SERVER_ADDRESS "127.0.0.1" +#define TEST_SERVER_INVALID_ADDRESS "287.0.0.1" +#define TEST_SERVER_PORT 8123 +#define TEST_SERVER_AVAILABLE_PORT 0 + +TEST(TCPServer, Positive_Create_Anytime) +{ + unsigned short port = TEST_SERVER_PORT; + std::unique_ptr<TCP::Server> tcp(std::make_unique<TCP::Server>(TEST_SERVER_ADDRESS, port)); + ASSERT_NE(tcp, nullptr); +} + +TEST(TCPServer, Negative_Create_Anytime) +{ + try { + unsigned short port = TEST_SERVER_PORT; + + std::unique_ptr<TCP::Server> tcp( + std::make_unique<TCP::Server>(TEST_SERVER_INVALID_ADDRESS, port)); + ASSERT_EQ(tcp, nullptr); + } catch (std::exception &e) { + ASSERT_STREQ(e.what(), strerror(EINVAL)); + } +} + +TEST(TCPServer, Positive_Create_AutoPort_Anytime) +{ + unsigned short port = TEST_SERVER_AVAILABLE_PORT; + std::unique_ptr<TCP::Server> tcp(std::make_unique<TCP::Server>(TEST_SERVER_ADDRESS, port)); + ASSERT_NE(tcp, nullptr); + ASSERT_NE(port, 0); +} + +TEST(TCPServer, Positive_GetPort_Anytime) +{ + unsigned short port = TEST_SERVER_PORT; + std::unique_ptr<TCP::Server> tcp(std::make_unique<TCP::Server>(TEST_SERVER_ADDRESS, port)); + ASSERT_NE(tcp, nullptr); + ASSERT_EQ(tcp->GetPort(), TEST_SERVER_PORT); +} + +TEST(TCPServer, Positive_GetHandle_Anytime) +{ + unsigned short port = TEST_SERVER_PORT; + std::unique_ptr<TCP::Server> tcp(std::make_unique<TCP::Server>(TEST_SERVER_ADDRESS, port)); + ASSERT_NE(tcp, nullptr); + ASSERT_GE(tcp->GetHandle(), 0); +} + +TEST(TCPServer, Positive_GetPort_AutoPort_Anytime) +{ + unsigned short port = TEST_SERVER_AVAILABLE_PORT; + std::unique_ptr<TCP::Server> tcp(std::make_unique<TCP::Server>(TEST_SERVER_ADDRESS, port)); + ASSERT_NE(tcp, nullptr); + ASSERT_EQ(tcp->GetPort(), port); +} + +TEST(TCPServer, Positive_AcceptPeer_Anytime) +{ + std::mutex m; + std::condition_variable ready_cv; + std::condition_variable connected_cv; + bool ready = false; + bool connected = false; + + unsigned short serverPort = TEST_SERVER_PORT; + std::thread serverThread( + [serverPort, &m, &ready, &connected, &ready_cv, &connected_cv](void) mutable -> void { + std::unique_ptr<TCP::Server> tcp( + std::make_unique<TCP::Server>(TEST_SERVER_ADDRESS, serverPort)); + { + std::lock_guard<std::mutex> lk(m); + ready = true; + } + ready_cv.notify_one(); + + std::unique_ptr<TCP> peer = tcp->AcceptPeer(); + { + std::lock_guard<std::mutex> lk(m); + connected = !!peer; + } + connected_cv.notify_one(); + }); + + { + std::unique_lock<std::mutex> lk(m); + ready_cv.wait(lk, [&ready] { return ready; }); + std::unique_ptr<TCP> tcp(std::make_unique<TCP>(TEST_SERVER_ADDRESS, serverPort)); + connected_cv.wait(lk, [&connected] { return connected; }); + } + + serverThread.join(); + + ASSERT_EQ(ready, true); + ASSERT_EQ(connected, true); +} diff --git a/modules/tcp/tests/TCP_test.cc b/modules/tcp/tests/TCP_test.cc new file mode 100644 index 0000000..604bd23 --- /dev/null +++ b/modules/tcp/tests/TCP_test.cc @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include <gtest/gtest.h> + +#include <condition_variable> +#include <cstring> +#include <functional> +#include <memory> +#include <mutex> +#include <thread> + +#include "TCPServer.h" + +#define TEST_SERVER_ADDRESS "127.0.0.1" +#define TEST_SERVER_INVALID_ADDRESS "287.0.0.1" +#define TEST_SERVER_PORT 8123 +#define TEST_SERVER_AVAILABLE_PORT 0 +#define TEST_BUFFER_SIZE 256 +#define TEST_BUFFER_HELLO "Hello World" +#define TEST_BUFFER_BYE "Good Bye" + +class TCPTest : public testing::Test { + protected: + void SetUp() override + { + ready = false; + serverPort = TEST_SERVER_PORT; + customTest = [](void) {}; + + clientThread = std::thread([this](void) mutable -> void { + std::unique_lock<std::mutex> lk(m); + ready_cv.wait(lk, [this] { return ready; }); + client = std::make_unique<TCP>(TEST_SERVER_ADDRESS, serverPort); + + customTest(); + }); + } + + void RunServer(void) + { + tcp = std::make_unique<TCP::Server>(TEST_SERVER_ADDRESS, serverPort); + { + std::lock_guard<std::mutex> lk(m); + ready = true; + } + ready_cv.notify_one(); + + peer = tcp->AcceptPeer(); + } + + void TearDown() override { clientThread.join(); } + + protected: + std::mutex m; + std::condition_variable ready_cv; + bool ready; + unsigned short serverPort; + std::thread clientThread; + std::unique_ptr<TCP::Server> tcp; + std::unique_ptr<TCP> peer; + std::unique_ptr<TCP> client; + std::function<void(void)> customTest; +}; + +TEST(TCP, Negative_Create_InvalidPort_Anytime) +{ + try { + std::unique_ptr<TCP> tcp( + std::make_unique<TCP>(TEST_SERVER_ADDRESS, TEST_SERVER_AVAILABLE_PORT)); + ASSERT_EQ(tcp, nullptr); + } catch (std::exception &e) { + ASSERT_STREQ(e.what(), strerror(EINVAL)); + } +} + +TEST(TCP, Negative_Create_InvalidAddress_Anytime) +{ + try { + std::unique_ptr<TCP> tcp( + std::make_unique<TCP>(TEST_SERVER_INVALID_ADDRESS, TEST_SERVER_PORT)); + ASSERT_EQ(tcp, nullptr); + } catch (std::exception &e) { + ASSERT_STREQ(e.what(), strerror(EINVAL)); + } +} + +TEST_F(TCPTest, Positive_GetPeerInfo_Anytime) +{ + std::string peerHost; + unsigned short peerPort = 0; + + RunServer(); + + peer->GetPeerInfo(peerHost, peerPort); + ASSERT_STREQ(peerHost.c_str(), TEST_SERVER_ADDRESS); + ASSERT_GT(peerPort, 0); +} + +TEST_F(TCPTest, Positive_GetHandle_Anytime) +{ + RunServer(); + int handle = peer->GetHandle(); + ASSERT_GE(handle, 0); +} + +TEST_F(TCPTest, Positive_GetPort_Anytime) +{ + RunServer(); + unsigned short port = peer->GetPort(); + ASSERT_GT(port, 0); +} + +TEST_F(TCPTest, Positive_SendRecv_Anytime) +{ + char helloBuffer[TEST_BUFFER_SIZE]; + char byeBuffer[TEST_BUFFER_SIZE]; + + customTest = [this, &helloBuffer](void) mutable -> void { + size_t szData = sizeof(helloBuffer); + client->Recv(static_cast<void *>(helloBuffer), szData); + + szData = sizeof(TEST_BUFFER_BYE); + client->Send(TEST_BUFFER_BYE, szData); + }; + + RunServer(); + + size_t szMsg = sizeof(TEST_BUFFER_HELLO); + peer->Send(TEST_BUFFER_HELLO, szMsg); + + szMsg = sizeof(byeBuffer); + peer->Recv(static_cast<void *>(byeBuffer), szMsg); + + ASSERT_STREQ(helloBuffer, TEST_BUFFER_HELLO); + ASSERT_STREQ(byeBuffer, TEST_BUFFER_BYE); +} diff --git a/modules/webrtc/CMakeLists.txt b/modules/webrtc/CMakeLists.txt new file mode 100644 index 0000000..9452b2b --- /dev/null +++ b/modules/webrtc/CMakeLists.txt @@ -0,0 +1,24 @@ +SET(AITT_WEBRTC aitt-transport-webrtc) + +INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}) + +PKG_CHECK_MODULES(AITT_WEBRTC_NEEDS REQUIRED + capi-media-camera + capi-media-webrtc + json-glib-1.0 +) +INCLUDE_DIRECTORIES(${AITT_WEBRTC_NEEDS_INCLUDE_DIRS}) +LINK_DIRECTORIES(${AITT_WEBRTC_NEEDS_LIBRARY_DIRS}) + +FILE(GLOB AITT_WEBRTC_SRC *.cc) +list(REMOVE_ITEM AITT_WEBRTC_SRC ${CMAKE_CURRENT_SOURCE_DIR}/Module.cc) +ADD_LIBRARY(WEBRTC_OBJ OBJECT ${AITT_WEBRTC_SRC}) +ADD_LIBRARY(${AITT_WEBRTC} SHARED $<TARGET_OBJECTS:WEBRTC_OBJ> ../main.cc Module.cc) +TARGET_LINK_LIBRARIES(${AITT_WEBRTC} ${AITT_WEBRTC_NEEDS_LIBRARIES} ${AITT_COMMON}) +TARGET_COMPILE_OPTIONS(${AITT_WEBRTC} PUBLIC ${AITT_WEBRTC_NEEDS_CFLAGS_OTHER}) + +INSTALL(TARGETS ${AITT_WEBRTC} DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +IF(BUILD_TESTING) + ADD_SUBDIRECTORY(tests) +ENDIF(BUILD_TESTING) diff --git a/modules/webrtc/CameraHandler.cc b/modules/webrtc/CameraHandler.cc new file mode 100644 index 0000000..c3fc8ec --- /dev/null +++ b/modules/webrtc/CameraHandler.cc @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "CameraHandler.h" + +#include "aitt_internal.h" + +#define RETURN_DEFINED_NAME_AS_STRING(defined_constant) \ + case defined_constant: \ + return #defined_constant; + +CameraHandler::~CameraHandler(void) +{ + if (handle_) { + camera_state_e state = CAMERA_STATE_NONE; + + int ret = camera_get_state(handle_, &state); + if (ret != CAMERA_ERROR_NONE) { + ERR("camera_get_state() Fail(%s)", ErrorToString(ret)); + } + + if (state == CAMERA_STATE_PREVIEW) { + INFO("CameraHandler preview is not stopped (stop)"); + ret = camera_stop_preview(handle_); + if (ret != CAMERA_ERROR_NONE) { + ERR("camera_stop_preview() Fail(%s)", ErrorToString(ret)); + } + } + } + + if (handle_) + camera_destroy(handle_); +} + +int CameraHandler::Init(const MediaPacketPreviewCallback &preview_cb, void *user_data) +{ + int ret = camera_create(CAMERA_DEVICE_CAMERA0, &handle_); + if (ret != CAMERA_ERROR_NONE) { + ERR("camera_create() Fail(%s)", ErrorToString(ret)); + return -1; + } + SettingCamera(preview_cb, user_data); + + return 0; +} + +void CameraHandler::SettingCamera(const MediaPacketPreviewCallback &preview_cb, void *user_data) +{ + int ret = camera_set_media_packet_preview_cb(handle_, CameraPreviewCB, this); + if (ret != CAMERA_ERROR_NONE) { + ERR("camera_set_media_packet_preview_cb() Fail(%s)", ErrorToString(ret)); + return; + } + media_packet_preview_cb_ = preview_cb; + user_data_ = user_data; +} + +void CameraHandler::Deinit(void) +{ + if (!handle_) { + ERR("Handler is nullptr"); + return; + } + + is_started_ = false; + media_packet_preview_cb_ = nullptr; + user_data_ = nullptr; +} + +int CameraHandler::StartPreview(void) +{ + camera_state_e state; + int ret = camera_get_state(handle_, &state); + if (ret != CAMERA_ERROR_NONE) { + ERR("camera_get_state() Fail(%s)", ErrorToString(ret)); + return -1; + } + + if (state == CAMERA_STATE_PREVIEW) { + INFO("Preview is already started"); + is_started_ = true; + return 0; + } + + ret = camera_start_preview(handle_); + if (ret != CAMERA_ERROR_NONE) { + ERR("camera_start_preview() Fail(%s)", ErrorToString(ret)); + return -1; + } + + is_started_ = true; + + return 0; +} + +int CameraHandler::StopPreview(void) +{ + RETV_IF(handle_ == nullptr, -1); + is_started_ = false; + + return 0; +} + +void CameraHandler::CameraPreviewCB(media_packet_h media_packet, void *user_data) +{ + auto camera_handler = static_cast<CameraHandler *>(user_data); + if (!camera_handler) { + ERR("Invalid user_data"); + return; + } + + if (!camera_handler->is_started_) { + ERR("Preveiw is not started yet"); + return; + } + + if (!camera_handler->media_packet_preview_cb_) { + ERR("Preveiw cb is not set"); + return; + } + + camera_handler->media_packet_preview_cb_(media_packet, camera_handler->user_data_); +} + +const char *CameraHandler::ErrorToString(const int error) +{ + switch (error) { + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_NONE) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_INVALID_PARAMETER) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_INVALID_STATE) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_OUT_OF_MEMORY) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_DEVICE) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_INVALID_OPERATION) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_SECURITY_RESTRICTED) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_DEVICE_BUSY) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_DEVICE_NOT_FOUND) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_ESD) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_PERMISSION_DENIED) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_NOT_SUPPORTED) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_RESOURCE_CONFLICT) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_ERROR_SERVICE_DISCONNECTED) + } + + return "Unknown error"; +} + +const char *CameraHandler::StateToString(const camera_state_e state) +{ + switch (state) { + RETURN_DEFINED_NAME_AS_STRING(CAMERA_STATE_NONE) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_STATE_CREATED) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_STATE_PREVIEW) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_STATE_CAPTURING) + RETURN_DEFINED_NAME_AS_STRING(CAMERA_STATE_CAPTURED) + } + + return "Unknown state"; +} diff --git a/modules/webrtc/CameraHandler.h b/modules/webrtc/CameraHandler.h new file mode 100644 index 0000000..5c44828 --- /dev/null +++ b/modules/webrtc/CameraHandler.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <camera.h> + +#include <functional> + +class CameraHandler { + public: + using MediaPacketPreviewCallback = std::function<void(media_packet_h, void *)>; + + ~CameraHandler(); + int Init(const MediaPacketPreviewCallback &preview_cb, void *user_data); + void Deinit(void); + int StartPreview(void); + int StopPreview(void); + + static const char *ErrorToString(const int error); + static const char *StateToString(const camera_state_e state); + + private: + void SettingCamera(const MediaPacketPreviewCallback &preview_cb, void *user_data); + static void CameraPreviewCB(media_packet_h media_packet, void *user_data); + + camera_h handle_; + bool is_started_; + MediaPacketPreviewCallback media_packet_preview_cb_; + void *user_data_; +}; diff --git a/modules/webrtc/Config.h b/modules/webrtc/Config.h new file mode 100644 index 0000000..63dbd4b --- /dev/null +++ b/modules/webrtc/Config.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> + +class Config { + public: + Config() : disable_ssl_(false), broker_port_(0), user_data_length_(0) {}; + Config(const std::string &id, const std::string &broker_ip, int broker_port, + const std::string &room_id, const std::string &source_id = std::string("")) + : local_id_(id), + room_id_(room_id), + source_id_(source_id), + disable_ssl_(false), + broker_ip_(broker_ip), + broker_port_(broker_port), + user_data_length_(0){}; + std::string GetLocalId(void) const { return local_id_; }; + void SetLocalId(const std::string &local_id) { local_id_ = local_id; }; + std::string GetRoomId(void) const { return room_id_; }; + void SetRoomId(const std::string &room_id) { room_id_ = room_id; }; + std::string GetSourceId(void) const { return source_id_; }; + void SetSourceId(const std::string &source_id) { source_id_ = source_id; }; + void SetSignalingServerUrl(const std::string &signaling_server_url) + { + signaling_server_url_ = signaling_server_url; + }; + std::string GetSignalingServerUrl(void) const { return signaling_server_url_; }; + void SetDisableSSl(bool disable_ssl) { disable_ssl_ = disable_ssl; }; + bool GetDisableSSl(void) const { return disable_ssl_; }; + std::string GetBrokerIp(void) const { return broker_ip_; }; + void SetBrokerIp(const std::string &broker_ip) { broker_ip_ = broker_ip; }; + int GetBrokerPort(void) const { return broker_port_; }; + void SetBrokerPort(int port) { broker_port_ = port; }; + unsigned int GetUserDataLength(void) const { return user_data_length_; }; + void SetUserDataLength(unsigned int user_data_length) { user_data_length_ = user_data_length; }; + + private: + std::string local_id_; + std::string room_id_; + std::string source_id_; + std::string signaling_server_url_; + bool disable_ssl_; + std::string broker_ip_; + int broker_port_; + unsigned int user_data_length_; +}; diff --git a/modules/webrtc/IfaceServer.h b/modules/webrtc/IfaceServer.h new file mode 100644 index 0000000..ad6d36d --- /dev/null +++ b/modules/webrtc/IfaceServer.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <functional> +#include <vector> + +class IfaceServer { + public: + enum class ConnectionState { + Disconnected, + Connecting, + Connected, + Registering, + Registered, + }; + + virtual ~IfaceServer(){}; + virtual void SetConnectionStateChangedCb( + std::function<void(ConnectionState)> connection_state_changed_cb) = 0; + virtual void UnsetConnectionStateChangedCb(void) = 0; + virtual int Connect(void) = 0; + virtual int Disconnect(void) = 0; + virtual bool IsConnected(void) = 0; + virtual int SendMessage(const std::string &peer_id, const std::string &message) = 0; +}; diff --git a/modules/webrtc/Module.cc b/modules/webrtc/Module.cc new file mode 100644 index 0000000..3c9e4f8 --- /dev/null +++ b/modules/webrtc/Module.cc @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Module.h" + +#include <flatbuffers/flexbuffers.h> + +#include "Config.h" +#include "aitt_internal.h" + +Module::Module(const std::string &ip, AittDiscovery &discovery) : AittTransport(discovery) +{ +} + +Module::~Module(void) +{ +} + +void Module::Publish(const std::string &topic, const void *data, const size_t datalen, + const std::string &correlation, AittQoS qos, bool retain) +{ + // TODO +} + +void Module::Publish(const std::string &topic, const void *data, const size_t datalen, AittQoS qos, + bool retain) +{ + std::lock_guard<std::mutex> publish_table_lock(publish_table_lock_); + + auto config = BuildConfigFromFb(data, datalen); + if (config.GetUserDataLength()) { + publish_table_[topic] = + std::make_shared<PublishStream>(topic, BuildConfigFromFb(data, datalen)); + + publish_table_[topic]->Start(); + } else { + auto publish_table_itr = publish_table_.find(topic); + if (publish_table_itr == publish_table_.end()) { + ERR("%s not found", topic.c_str()); + return; + } + auto publish_stream = publish_table_itr->second; + publish_stream->Stop(); + publish_table_.erase(publish_table_itr); + } +} + +void *Module::Subscribe(const std::string &topic, const AittTransport::SubscribeCallback &cb, + void *cbdata, AittQoS qos) +{ + return nullptr; +} + +void *Module::Subscribe(const std::string &topic, const AittTransport::SubscribeCallback &cb, + const void *data, const size_t datalen, void *cbdata, AittQoS qos) +{ + std::lock_guard<std::mutex> subscribe_table_lock(subscribe_table_lock_); + + subscribe_table_[topic] = + std::make_shared<SubscribeStream>(topic, BuildConfigFromFb(data, datalen)); + + subscribe_table_[topic]->Start(qos == AITT_QOS_EXACTLY_ONCE, cbdata); + + return subscribe_table_[topic].get(); +} + +Config Module::BuildConfigFromFb(const void *data, const size_t data_size) +{ + Config config; + auto webrtc_configs = + flexbuffers::GetRoot(static_cast<const uint8_t *>(data), data_size).AsMap(); + auto webrtc_config_keys = webrtc_configs.Keys(); + for (size_t idx = 0; idx < webrtc_config_keys.size(); ++idx) { + std::string key = webrtc_config_keys[idx].AsString().c_str(); + + if (key.compare("Id") == 0) + config.SetLocalId(webrtc_configs[key].AsString().c_str()); + else if (key.compare("RoomId") == 0) + config.SetRoomId(webrtc_configs[key].AsString().c_str()); + else if (key.compare("SourceId") == 0) + config.SetSourceId(webrtc_configs[key].AsString().c_str()); + else if (key.compare("BrokerIp") == 0) + config.SetBrokerIp(webrtc_configs[key].AsString().c_str()); + else if (key.compare("BrokerPort") == 0) + config.SetBrokerPort(webrtc_configs[key].AsInt32()); + else if (key.compare("UserDataLength") == 0) + config.SetUserDataLength(webrtc_configs[key].AsUInt32()); + else { + printf("Not supported key name: %s\n", key.c_str()); + } + } + + return config; +} + +void *Module::Unsubscribe(void *handlePtr) +{ + void *ret = nullptr; + std::string topic; + std::lock_guard<std::mutex> subscribe_table_lock(subscribe_table_lock_); + for (auto itr = subscribe_table_.begin(); itr != subscribe_table_.end(); ++itr) { + if (itr->second.get() == handlePtr) { + auto topic = itr->first; + break; + } + } + + if (topic.size() != 0) { + ret = subscribe_table_[topic]->Stop(); + subscribe_table_.erase(topic); + } + + return ret; +} diff --git a/modules/webrtc/Module.h b/modules/webrtc/Module.h new file mode 100644 index 0000000..ca31eb8 --- /dev/null +++ b/modules/webrtc/Module.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <AittTransport.h> +#include <MainLoopHandler.h> + +#include <map> +#include <memory> +#include <mutex> +#include <set> +#include <string> +#include <thread> + +#include "PublishStream.h" +#include "SubscribeStream.h" + +using AittTransport = aitt::AittTransport; +using MainLoopHandler = aitt::MainLoopHandler; +using AittDiscovery = aitt::AittDiscovery; + +class Module : public AittTransport { + public: + explicit Module(const std::string &ip, AittDiscovery &discovery); + virtual ~Module(void); + + // TODO: How about regarding topic as service name? + void Publish(const std::string &topic, const void *data, const size_t datalen, + const std::string &correlation, AittQoS qos = AITT_QOS_AT_MOST_ONCE, + bool retain = false) override; + + void Publish(const std::string &topic, const void *data, const size_t datalen, + AittQoS qos = AITT_QOS_AT_MOST_ONCE, bool retain = false) override; + + // TODO: How about regarding topic as service name? + void *Subscribe(const std::string &topic, const AittTransport::SubscribeCallback &cb, + void *cbdata = nullptr, AittQoS qos = AITT_QOS_AT_MOST_ONCE) override; + + void *Subscribe(const std::string &topic, const AittTransport::SubscribeCallback &cb, + const void *data, const size_t datalen, void *cbdata = nullptr, + AittQoS qos = AITT_QOS_AT_MOST_ONCE) override; + + void *Unsubscribe(void *handle) override; + + private: + Config BuildConfigFromFb(const void *data, const size_t data_size); + + std::map<std::string, std::shared_ptr<PublishStream>> publish_table_; + std::mutex publish_table_lock_; + std::map<std::string, std::shared_ptr<SubscribeStream>> subscribe_table_; + std::mutex subscribe_table_lock_; +}; diff --git a/modules/webrtc/MqttServer.cc b/modules/webrtc/MqttServer.cc new file mode 100644 index 0000000..70a07ed --- /dev/null +++ b/modules/webrtc/MqttServer.cc @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MqttServer.h" + +#include "aitt_internal.h" + +#define MQTT_HANDLER_MSG_QOS 1 +#define MQTT_HANDLER_MGMT_QOS 2 + +MqttServer::MqttServer(const Config &config) : mq(config.GetLocalId(), true) +{ + broker_ip_ = config.GetBrokerIp(); + broker_port_ = config.GetBrokerPort(); + id_ = config.GetLocalId(); + room_id_ = config.GetRoomId(); + source_id_ = config.GetSourceId(); + is_publisher_ = (id_ == source_id_); + + DBG("ID[%s] BROKER IP[%s] BROKER PORT [%d] ROOM[%s] %s", id_.c_str(), broker_ip_.c_str(), + broker_port_, room_id_.c_str(), is_publisher_ ? "Publisher" : "Subscriber"); + + mq.SetConnectionCallback(std::bind(&MqttServer::ConnectCallBack, this, std::placeholders::_1)); +} + +MqttServer::~MqttServer() +{ + // Prevent to call below callbacks after destructoring + connection_state_changed_cb_ = nullptr; + room_message_arrived_cb_ = nullptr; +} + +void MqttServer::SetConnectionState(ConnectionState state) +{ + connection_state_ = state; + if (connection_state_changed_cb_) + connection_state_changed_cb_(state); +} + +void MqttServer::ConnectCallBack(int status) +{ + if (status == AITT_CONNECTED) + OnConnect(); + else + OnDisconnect(); +} + +void MqttServer::OnConnect() +{ + INFO("Connected to signalling server"); + + // Sometimes it seems that broker is silently disconnected/reconnected + if (GetConnectionState() != ConnectionState::Connecting) { + ERR("Invalid status"); + return; + } + + SetConnectionState(ConnectionState::Connected); + SetConnectionState(ConnectionState::Registering); + try { + RegisterWithServer(); + } catch (const std::runtime_error &e) { + ERR("%s", e.what()); + SetConnectionState(ConnectionState::Connected); + } +} + +void MqttServer::OnDisconnect() +{ + INFO("mosquitto disconnected"); + + SetConnectionState(ConnectionState::Disconnected); + // TODO +} + +void MqttServer::RegisterWithServer(void) +{ + if (connection_state_ != IfaceServer::ConnectionState::Registering) { + ERR("Invaild status(%d)", (int)connection_state_); + throw std::runtime_error("Invalid status"); + } + + // Notify Who is source? + std::string source_topic = room_id_ + std::string("/source"); + if (is_publisher_) { + mq.Publish(source_topic, id_.c_str(), id_.size(), AITT_QOS_EXACTLY_ONCE, true); + SetConnectionState(ConnectionState::Registered); + } else { + mq.Subscribe(source_topic, + std::bind(&MqttServer::HandleSourceTopic, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5), + nullptr, AITT_QOS_EXACTLY_ONCE); + } +} + +void MqttServer::HandleSourceTopic(aitt::MSG *msg, const std::string &topic, const void *data, + const size_t datalen, void *user_data) +{ + INFO("Source topic"); + if (connection_state_ != IfaceServer::ConnectionState::Registering) { + ERR("Invaild status(%d)", (int)connection_state_); + return; + } + + if (is_publisher_) { + ERR("Ignore"); + } else { + std::string message(static_cast<const char *>(data), datalen); + INFO("Set source ID %s", message.c_str()); + SetSourceId(message); + SetConnectionState(ConnectionState::Registered); + } +} + +bool MqttServer::IsConnected(void) +{ + INFO("%s", __func__); + + return connection_state_ == IfaceServer::ConnectionState::Registered; +} + +int MqttServer::Connect(void) +{ + std::string will_message = std::string("ROOM_PEER_LEFT ") + id_; + mq.SetWillInfo(room_id_, will_message.c_str(), will_message.size(), AITT_QOS_EXACTLY_ONCE, + false); + + SetConnectionState(ConnectionState::Connecting); + mq.Connect(broker_ip_, broker_port_, std::string(), std::string()); + + return 0; +} + +int MqttServer::Disconnect(void) +{ + if (is_publisher_) { + INFO("remove retained"); + std::string source_topic = room_id_ + std::string("/source"); + mq.Publish(source_topic, nullptr, 0, AITT_QOS_AT_LEAST_ONCE, true); + } + + std::string left_message = std::string("ROOM_PEER_LEFT ") + id_; + mq.Publish(room_id_, left_message.c_str(), left_message.size(), AITT_QOS_AT_LEAST_ONCE, false); + + mq.Disconnect(); + + room_id_ = std::string(""); + + SetConnectionState(ConnectionState::Disconnected); + return 0; +} + +int MqttServer::SendMessage(const std::string &peer_id, const std::string &msg) +{ + if (room_id_.empty()) { + ERR("Invaild status"); + return -1; + } + if (peer_id.size() == 0 || msg.size() == 0) { + ERR("Invalid parameter"); + return -1; + } + + std::string receiver_topic = room_id_ + std::string("/") + peer_id; + std::string server_formatted_msg = "ROOM_PEER_MSG " + id_ + " " + msg; + mq.Publish(receiver_topic, server_formatted_msg.c_str(), server_formatted_msg.size(), + AITT_QOS_AT_LEAST_ONCE); + + return 0; +} + +std::string MqttServer::GetConnectionStateStr(ConnectionState state) +{ + std::string state_str; + switch (state) { + case IfaceServer::ConnectionState::Disconnected: { + state_str = std::string("Disconnected"); + break; + } + case IfaceServer::ConnectionState::Connecting: { + state_str = std::string("Connecting"); + break; + } + case IfaceServer::ConnectionState::Connected: { + state_str = std::string("Connected"); + break; + } + case IfaceServer::ConnectionState::Registering: { + state_str = std::string("Registering"); + break; + } + case IfaceServer::ConnectionState::Registered: { + state_str = std::string("Registered"); + break; + } + } + + return state_str; +} + +void MqttServer::JoinRoom(const std::string &room_id) +{ + if (room_id.empty() || room_id != room_id_) { + ERR("Invaild room id"); + throw std::runtime_error(std::string("Invalid room_id")); + } + + // Subscribe PEER_JOIN PEER_LEFT + mq.Subscribe(room_id_, + std::bind(&MqttServer::HandleRoomTopic, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5), + nullptr, AITT_QOS_EXACTLY_ONCE); + + // Subscribe PEER_MSG + std::string receiving_topic = room_id + std::string("/") + id_; + mq.Subscribe(receiving_topic, + std::bind(&MqttServer::HandleMessageTopic, this, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5), + nullptr, AITT_QOS_AT_LEAST_ONCE); + + INFO("Subscribe room topics"); + + if (!is_publisher_) { + std::string join_message = std::string("ROOM_PEER_JOINED ") + id_; + mq.Publish(room_id_, join_message.c_str(), join_message.size(), AITT_QOS_EXACTLY_ONCE); + } +} + +void MqttServer::HandleRoomTopic(aitt::MSG *msg, const std::string &topic, const void *data, + const size_t datalen, void *user_data) +{ + std::string message(static_cast<const char *>(data), datalen); + INFO("Room topic(%s, %s)", topic.c_str(), message.c_str()); + + std::string peer_id; + if (message.compare(0, 16, "ROOM_PEER_JOINED") == 0) { + peer_id = message.substr(17, std::string::npos); + } else if (message.compare(0, 14, "ROOM_PEER_LEFT") == 0) { + peer_id = message.substr(15, std::string::npos); + } else { + ERR("Invalid type of Room message %s", message.c_str()); + return; + } + + if (peer_id == id_) { + ERR("ignore"); + return; + } + + if (is_publisher_) { + if (room_message_arrived_cb_) + room_message_arrived_cb_(message); + } else { + // TODO: ADHOC, will handle this by room + if (peer_id != source_id_) { + ERR("peer(%s) is Not source(%s)", peer_id.c_str(), source_id_.c_str()); + return; + } + + if (room_message_arrived_cb_) + room_message_arrived_cb_(message); + } +} + +void MqttServer::HandleMessageTopic(aitt::MSG *msg, const std::string &topic, const void *data, + const size_t datalen, void *user_data) +{ + INFO("Message topic"); + std::string message(static_cast<const char *>(data), datalen); + + if (room_message_arrived_cb_) + room_message_arrived_cb_(message); +} diff --git a/modules/webrtc/MqttServer.h b/modules/webrtc/MqttServer.h new file mode 100644 index 0000000..7f93192 --- /dev/null +++ b/modules/webrtc/MqttServer.h @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <MQ.h> + +#include "Config.h" +#include "IfaceServer.h" + +class MqttServer : public IfaceServer { + public: + explicit MqttServer(const Config &config); + virtual ~MqttServer(); + + void SetConnectionStateChangedCb( + std::function<void(ConnectionState)> connection_state_changed_cb) override + { + connection_state_changed_cb_ = connection_state_changed_cb; + }; + void UnsetConnectionStateChangedCb(void) override { connection_state_changed_cb_ = nullptr; }; + + bool IsConnected(void) override; + int Connect(void) override; + int Disconnect(void) override; + int SendMessage(const std::string &peer_id, const std::string &msg) override; + + static std::string GetConnectionStateStr(ConnectionState state); + void RegisterWithServer(void); + void JoinRoom(const std::string &room_id); + void SetConnectionState(ConnectionState state); + ConnectionState GetConnectionState(void) const { return connection_state_; }; + std::string GetId(void) const { return id_; }; + std::string GetSourceId(void) const { return source_id_; }; + void SetSourceId(const std::string &source_id) { source_id_ = source_id; }; + + void SetRoomMessageArrivedCb(std::function<void(const std::string &)> room_message_arrived_cb) + { + room_message_arrived_cb_ = room_message_arrived_cb; + }; + void UnsetRoomMessageArrivedCb(void) { room_message_arrived_cb_ = nullptr; } + + private: + static void MessageCallback(mosquitto *handle, void *mqtt_server, const mosquitto_message *msg, + const mosquitto_property *props); + void OnConnect(); + void OnDisconnect(); + void ConnectCallBack(int status); + void HandleRoomTopic(aitt::MSG *msg, const std::string &topic, const void *data, + const size_t datalen, void *user_data); + void HandleSourceTopic(aitt::MSG *msg, const std::string &topic, const void *data, + const size_t datalen, void *user_data); + void HandleMessageTopic(aitt::MSG *msg, const std::string &topic, const void *data, + const size_t datalen, void *user_data); + + std::string broker_ip_; + int broker_port_; + std::string id_; + std::string room_id_; + std::string source_id_; + bool is_publisher_; + aitt::MQ mq; + + ConnectionState connection_state_; + std::function<void(ConnectionState)> connection_state_changed_cb_; + std::function<void(const std::string &)> room_message_arrived_cb_; +}; diff --git a/modules/webrtc/PublishStream.cc b/modules/webrtc/PublishStream.cc new file mode 100644 index 0000000..f93ecea --- /dev/null +++ b/modules/webrtc/PublishStream.cc @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "PublishStream.h" + +#include <sys/time.h> + +#include "WebRtcEventHandler.h" +#include "aitt_internal.h" + +PublishStream::~PublishStream() +{ + // TODO: clear resources +} + +void PublishStream::Start(void) +{ + PrepareStream(); + SetSignalingServerCallbacks(); + SetRoomCallbacks(); +} + +void PublishStream::PrepareStream(void) +{ + std::lock_guard<std::mutex> prepared_stream_lock(prepared_stream_lock_); + prepared_stream_ = std::make_shared<WebRtcStream>(); + prepared_stream_->Create(true, false); + prepared_stream_->AttachCameraSource(); + auto on_stream_state_changed_prepared_cb = + std::bind(OnStreamStateChangedPrepared, std::placeholders::_1, std::ref(*this)); + prepared_stream_->GetEventHandler().SetOnStateChangedCb(on_stream_state_changed_prepared_cb); + prepared_stream_->Start(); +} + +void PublishStream::OnStreamStateChangedPrepared(WebRtcState::Stream state, PublishStream &stream) +{ + ERR("%s", __func__); + if (state == WebRtcState::Stream::NEGOTIATING) { + auto on_offer_created_prepared_cb = + std::bind(OnOfferCreatedPrepared, std::placeholders::_1, std::ref(stream)); + stream.prepared_stream_->CreateOfferAsync(on_offer_created_prepared_cb); + } +} + +void PublishStream::OnOfferCreatedPrepared(std::string sdp, PublishStream &stream) +{ + ERR("%s", __func__); + + stream.prepared_stream_->SetPreparedLocalDescription(sdp); + stream.prepared_stream_->SetLocalDescription(sdp); + try { + stream.server_->Connect(); + } catch (const std::exception &e) { + ERR("Failed to start Publish stream %s", e.what()); + } +} + +void PublishStream::SetSignalingServerCallbacks(void) +{ + auto on_signaling_server_connection_state_changed = + std::bind(OnSignalingServerConnectionStateChanged, std::placeholders::_1, + std::ref(*room_), std::ref(*server_)); + + server_->SetConnectionStateChangedCb(on_signaling_server_connection_state_changed); + + auto on_room_message_arrived = + std::bind(OnRoomMessageArrived, std::placeholders::_1, std::ref(*room_)); + + server_->SetRoomMessageArrivedCb(on_room_message_arrived); +} + +void PublishStream::OnSignalingServerConnectionStateChanged(IfaceServer::ConnectionState state, + WebRtcRoom &room, MqttServer &server) +{ + DBG("current state [%s]", MqttServer::GetConnectionStateStr(state).c_str()); + + if (state == IfaceServer::ConnectionState::Disconnected) { + ; // TODO: what to do when server is disconnected? + } else if (state == IfaceServer::ConnectionState::Registered) { + server.JoinRoom(room.getId()); + } +} + +void PublishStream::OnRoomMessageArrived(const std::string &message, WebRtcRoom &room) +{ + room.handleMessage(message); +} + +void PublishStream::SetRoomCallbacks() +{ + auto on_room_joined = std::bind(OnRoomJoined, std::ref(*this)); + + room_->SetRoomJoinedCb(on_room_joined); + + auto on_peer_joined = std::bind(OnPeerJoined, std::placeholders::_1, std::ref(*this)); + room_->SetPeerJoinedCb(on_peer_joined); + + auto on_peer_left = std::bind(OnPeerLeft, std::placeholders::_1, std::ref(*room_)); + room_->SetPeerLeftCb(on_peer_left); +} + +void PublishStream::OnRoomJoined(PublishStream &publish_stream) +{ + // TODO: Notify Room Joined? + DBG("%s on %p", __func__, &publish_stream); +} + +void PublishStream::OnPeerJoined(const std::string &peer_id, PublishStream &publish_stream) +{ + DBG("%s [%s]", __func__, peer_id.c_str()); + if (!publish_stream.room_->AddPeer(peer_id)) { + ERR("Failed to add peer"); + return; + } + + try { + WebRtcPeer &peer = publish_stream.room_->GetPeer(peer_id); + + std::unique_lock<std::mutex> prepared_stream_lock(publish_stream.prepared_stream_lock_); + auto prepared_stream = publish_stream.prepared_stream_; + publish_stream.prepared_stream_ = nullptr; + prepared_stream_lock.unlock(); + + try { + peer.SetWebRtcStream(prepared_stream); + publish_stream.SetWebRtcStreamCallbacks(peer); + publish_stream.server_->SendMessage(peer.getId(), + peer.GetWebRtcStream()->GetPreparedLocalDescription()); + prepared_stream->SetPreparedLocalDescription(""); + } catch (std::exception &e) { + ERR("Failed to start stream for peer %s", e.what()); + } + // TODO why we can't prepare more sources? + + } catch (std::exception &e) { + ERR("Wired %s", e.what()); + } +} + +void PublishStream::SetWebRtcStreamCallbacks(WebRtcPeer &peer) +{ + // TODO: set more webrtc callbacks + WebRtcEventHandler event_handlers; + auto on_stream_state_changed_cb = std::bind(OnStreamStateChanged, std::placeholders::_1, + std::ref(peer), std::ref(*server_)); + event_handlers.SetOnStateChangedCb(on_stream_state_changed_cb); + + auto on_signaling_state_notify_cb = std::bind(OnSignalingStateNotify, std::placeholders::_1, + std::ref(peer), std::ref(*server_)); + event_handlers.SetOnSignalingStateNotifyCb(on_signaling_state_notify_cb); + + auto on_ice_connection_state_notify = std::bind(OnIceConnectionStateNotify, + std::placeholders::_1, std::ref(peer), std::ref(*server_)); + event_handlers.SetOnIceConnectionStateNotifyCb(on_ice_connection_state_notify); + + peer.GetWebRtcStream()->SetEventHandler(event_handlers); +} + +void PublishStream::OnStreamStateChanged(WebRtcState::Stream state, WebRtcPeer &peer, + MqttServer &server) +{ + ERR("%s for %s", __func__, peer.getId().c_str()); +} + +void PublishStream::OnSignalingStateNotify(WebRtcState::Signaling state, WebRtcPeer &peer, + MqttServer &server) +{ + ERR("Singaling State: %s", WebRtcState::SignalingToStr(state).c_str()); + if (state == WebRtcState::Signaling::STABLE) { + auto ice_candidates = peer.GetWebRtcStream()->GetIceCandidates(); + for (const auto &candidate : ice_candidates) + server.SendMessage(peer.getId(), candidate); + } +} + +void PublishStream::OnIceConnectionStateNotify(WebRtcState::IceConnection state, WebRtcPeer &peer, + MqttServer &server) +{ + ERR("IceConnection State: %s", WebRtcState::IceConnectionToStr(state).c_str()); +} + +void PublishStream::OnPeerLeft(const std::string &peer_id, WebRtcRoom &room) +{ + DBG("%s [%s]", __func__, peer_id.c_str()); + if (!room.RemovePeer(peer_id)) + ERR("Failed to remove peer"); +} + +void PublishStream::Stop(void) +{ + try { + server_->Disconnect(); + } catch (const std::exception &e) { + ERR("Failed to disconnect server %s", e.what()); + } + + room_->ClearPeers(); +} diff --git a/modules/webrtc/PublishStream.h b/modules/webrtc/PublishStream.h new file mode 100644 index 0000000..1805528 --- /dev/null +++ b/modules/webrtc/PublishStream.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <memory> +#include <mutex> +#include <string> + +#include "Config.h" +#include "MqttServer.h" +#include "WebRtcRoom.h" +#include "WebRtcStream.h" + +class PublishStream { + // TODO: Notify & get status + public: + PublishStream() = delete; + PublishStream(const std::string &topic, const Config &config) + : topic_(topic), + config_(config), + server_(std::make_shared<MqttServer>(config)), + room_(std::make_shared<WebRtcRoom>(config.GetRoomId())), + prepared_stream_(nullptr){}; + ~PublishStream(); + + void Start(void); + void Stop(void); + void SetSignalingServerCallbacks(void); + void SetRoomCallbacks(void); + void SetWebRtcStreamCallbacks(WebRtcPeer &peer); + void PrepareStream(void); + + private: + static void OnStreamStateChangedPrepared(WebRtcState::Stream state, PublishStream &stream); + static void OnOfferCreatedPrepared(std::string sdp, PublishStream &stream); + static void OnSignalingServerConnectionStateChanged(IfaceServer::ConnectionState state, + WebRtcRoom &room, MqttServer &server); + static void OnRoomMessageArrived(const std::string &message, WebRtcRoom &room); + static void OnRoomJoined(PublishStream &publish_stream); + static void OnPeerJoined(const std::string &peer_id, PublishStream &publish_stream); + static void OnPeerLeft(const std::string &peer_id, WebRtcRoom &room); + static void OnStreamStateChanged(WebRtcState::Stream state, WebRtcPeer &peer, + MqttServer &server); + + static void OnSignalingStateNotify(WebRtcState::Signaling state, WebRtcPeer &peer, + MqttServer &server); + static void OnIceConnectionStateNotify(WebRtcState::IceConnection state, WebRtcPeer &peer, + MqttServer &server); + + private: + std::string topic_; + Config config_; + std::shared_ptr<MqttServer> server_; + std::shared_ptr<WebRtcRoom> room_; + std::mutex prepared_stream_lock_; + std::shared_ptr<WebRtcStream> prepared_stream_; +}; diff --git a/modules/webrtc/SubscribeStream.cc b/modules/webrtc/SubscribeStream.cc new file mode 100644 index 0000000..841cfa6 --- /dev/null +++ b/modules/webrtc/SubscribeStream.cc @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "SubscribeStream.h" + +#include "WebRtcEventHandler.h" +#include "aitt_internal.h" + +SubscribeStream::~SubscribeStream() +{ + // TODO Clear resources +} + +void SubscribeStream::Start(bool need_display, void *display_object) +{ + display_object_ = display_object; + is_track_added_ = need_display; + SetSignalingServerCallbacks(); + SetRoomCallbacks(); + try { + server_->Connect(); + } catch (const std::exception &e) { + ERR("Failed to start Subscribe stream %s", e.what()); + } +} + +void SubscribeStream::SetSignalingServerCallbacks(void) +{ + auto on_signaling_server_connection_state_changed = + std::bind(OnSignalingServerConnectionStateChanged, std::placeholders::_1, + std::ref(*room_), std::ref(*server_)); + + server_->SetConnectionStateChangedCb(on_signaling_server_connection_state_changed); + + auto on_room_message_arrived = + std::bind(OnRoomMessageArrived, std::placeholders::_1, std::ref(*room_)); + + server_->SetRoomMessageArrivedCb(on_room_message_arrived); +} + +void SubscribeStream::OnSignalingServerConnectionStateChanged(IfaceServer::ConnectionState state, + WebRtcRoom &room, MqttServer &server) +{ + // TODO VD doesn't show DBG level log + ERR("current state [%s]", MqttServer::GetConnectionStateStr(state).c_str()); + + if (state == IfaceServer::ConnectionState::Disconnected) { + ; // TODO: what to do when server is disconnected? + } else if (state == IfaceServer::ConnectionState::Registered) { + if (server.GetSourceId().size() != 0) + room.SetSourceId(server.GetSourceId()); + server.JoinRoom(room.getId()); + } +} + +void SubscribeStream::OnRoomMessageArrived(const std::string &message, WebRtcRoom &room) +{ + room.handleMessage(message); +} + +void SubscribeStream::SetRoomCallbacks(void) +{ + auto on_room_joined = std::bind(OnRoomJoined, std::ref(*this)); + + room_->SetRoomJoinedCb(on_room_joined); + + auto on_peer_joined = std::bind(OnPeerJoined, std::placeholders::_1, std::ref(*this)); + room_->SetPeerJoinedCb(on_peer_joined); + + auto on_peer_left = std::bind(OnPeerLeft, std::placeholders::_1, std::ref(*room_)); + room_->SetPeerLeftCb(on_peer_left); +} + +void SubscribeStream::OnRoomJoined(SubscribeStream &subscribe_stream) +{ + // TODO: Notify Room Joined? + ERR("%s on %p", __func__, &subscribe_stream); +} + +void SubscribeStream::OnPeerJoined(const std::string &peer_id, SubscribeStream &subscribe_stream) +{ + ERR("%s [%s]", __func__, peer_id.c_str()); + + if (peer_id.compare(subscribe_stream.room_->GetSourceId()) != 0) { + ERR("is not matched to source ID, ignored"); + return; + } + + if (!subscribe_stream.room_->AddPeer(peer_id)) { + ERR("Failed to add peer"); + return; + } + + try { + WebRtcPeer &peer = subscribe_stream.room_->GetPeer(peer_id); + + auto webrtc_subscribe_stream = peer.GetWebRtcStream(); + webrtc_subscribe_stream->Create(false, subscribe_stream.is_track_added_); + webrtc_subscribe_stream->Start(); + subscribe_stream.SetWebRtcStreamCallbacks(peer); + } catch (std::out_of_range &e) { + ERR("Wired %s", e.what()); + } +} + +void SubscribeStream::SetWebRtcStreamCallbacks(WebRtcPeer &peer) +{ + WebRtcEventHandler event_handlers; + + auto on_signaling_state_notify = std::bind(OnSignalingStateNotify, std::placeholders::_1, + std::ref(peer), std::ref(*server_)); + event_handlers.SetOnSignalingStateNotifyCb(on_signaling_state_notify); + + auto on_ice_connection_state_notify = std::bind(OnIceConnectionStateNotify, + std::placeholders::_1, std::ref(peer), std::ref(*server_)); + event_handlers.SetOnIceConnectionStateNotifyCb(on_ice_connection_state_notify); + + auto on_encoded_frame = std::bind(OnEncodedFrame, std::ref(peer)); + event_handlers.SetOnEncodedFrameCb(on_encoded_frame); + + auto on_track_added = + std::bind(OnTrackAdded, std::placeholders::_1, display_object_, std::ref(peer)); + event_handlers.SetOnTrakAddedCb(on_track_added); + + peer.GetWebRtcStream()->SetEventHandler(event_handlers); +} + +void SubscribeStream::OnSignalingStateNotify(WebRtcState::Signaling state, WebRtcPeer &peer, + MqttServer &server) +{ + ERR("Singaling State: %s", WebRtcState::SignalingToStr(state).c_str()); + if (state == WebRtcState::Signaling::HAVE_REMOTE_OFFER) { + auto on_answer_created_cb = + std::bind(OnAnswerCreated, std::placeholders::_1, std::ref(peer), std::ref(server)); + peer.GetWebRtcStream()->CreateAnswerAsync(on_answer_created_cb); + } +} + +void SubscribeStream::OnIceConnectionStateNotify(WebRtcState::IceConnection state, WebRtcPeer &peer, + MqttServer &server) +{ + ERR("IceConnection State: %s", WebRtcState::IceConnectionToStr(state).c_str()); + if (state == WebRtcState::IceConnection::CHECKING) { + auto ice_candidates = peer.GetWebRtcStream()->GetIceCandidates(); + for (const auto &candidate : ice_candidates) + server.SendMessage(peer.getId(), candidate); + } +} + +void SubscribeStream::OnAnswerCreated(std::string sdp, WebRtcPeer &peer, MqttServer &server) +{ + server.SendMessage(peer.getId(), sdp); + peer.GetWebRtcStream()->SetLocalDescription(sdp); +} + +void SubscribeStream::OnEncodedFrame(WebRtcPeer &peer) +{ + // TODO +} + +void SubscribeStream::OnTrackAdded(unsigned int id, void *display_object, WebRtcPeer &peer) +{ + peer.GetWebRtcStream()->SetDisplayObject(id, display_object); +} + +void SubscribeStream::OnPeerLeft(const std::string &peer_id, WebRtcRoom &room) +{ + /*TODO + ERR("%s [%s]", __func__, peer_id.c_str()); + if (peer_id.compare(room.getSourceId()) != 0) { + ERR("is not matched to source ID, ignored"); + return; + } + */ + if (!room.RemovePeer(peer_id)) + ERR("Failed to remove peer"); +} + +void *SubscribeStream::Stop(void) +{ + try { + server_->Disconnect(); + } catch (const std::exception &e) { + ERR("Failed to disconnect server %s", e.what()); + } + + room_->ClearPeers(); + + return display_object_; +} diff --git a/modules/webrtc/SubscribeStream.h b/modules/webrtc/SubscribeStream.h new file mode 100644 index 0000000..c8853f6 --- /dev/null +++ b/modules/webrtc/SubscribeStream.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <memory> +#include <mutex> + +#include "Config.h" +#include "MqttServer.h" +#include "WebRtcRoom.h" +#include "WebRtcStream.h" + +class SubscribeStream { + public: + SubscribeStream() = delete; + SubscribeStream(const std::string &topic, const Config &config) + : topic_(topic), + config_(config), + server_(std::make_shared<MqttServer>(config)), + room_(std::make_shared<WebRtcRoom>(config.GetRoomId())), + is_track_added_(false), + display_object_(nullptr){}; + ~SubscribeStream(); + + // TODO what will be final form of callback + void Start(bool need_display, void *display_object); + void *Stop(void); + void SetSignalingServerCallbacks(void); + void SetRoomCallbacks(void); + void SetWebRtcStreamCallbacks(WebRtcPeer &peer); + + private: + static void OnSignalingServerConnectionStateChanged(IfaceServer::ConnectionState state, + WebRtcRoom &room, MqttServer &server); + static void OnRoomMessageArrived(const std::string &message, WebRtcRoom &room); + static void OnRoomJoined(SubscribeStream &subscribe_stream); + static void OnPeerJoined(const std::string &peer_id, SubscribeStream &subscribe_stream); + static void OnPeerLeft(const std::string &peer_id, WebRtcRoom &room); + static void OnSignalingStateNotify(WebRtcState::Signaling state, WebRtcPeer &peer, + MqttServer &server); + static void OnIceConnectionStateNotify(WebRtcState::IceConnection state, WebRtcPeer &peer, + MqttServer &server); + static void OnAnswerCreated(std::string sdp, WebRtcPeer &peer, MqttServer &server); + static void OnEncodedFrame(WebRtcPeer &peer); + static void OnTrackAdded(unsigned int id, void *dispaly_object, WebRtcPeer &peer); + + private: + std::string topic_; + Config config_; + std::shared_ptr<MqttServer> server_; + std::shared_ptr<WebRtcRoom> room_; + bool is_track_added_; + void *display_object_; +}; diff --git a/modules/webrtc/WebRtcEventHandler.h b/modules/webrtc/WebRtcEventHandler.h new file mode 100644 index 0000000..c922672 --- /dev/null +++ b/modules/webrtc/WebRtcEventHandler.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <functional> +#include <string> + +#include "WebRtcState.h" + +class WebRtcEventHandler { + public: + // TODO Add error and state callbacks + void SetOnStateChangedCb(std::function<void(WebRtcState::Stream)> on_state_changed_cb) + { + on_state_changed_cb_ = on_state_changed_cb; + }; + void CallOnStateChangedCb(WebRtcState::Stream state) const + { + if (on_state_changed_cb_) + on_state_changed_cb_(state); + }; + void UnsetOnStateChangedCb(void) { on_state_changed_cb_ = nullptr; }; + + void SetOnSignalingStateNotifyCb( + std::function<void(WebRtcState::Signaling)> on_signaling_state_notify_cb) + { + on_signaling_state_notify_cb_ = on_signaling_state_notify_cb; + }; + void CallOnSignalingStateNotifyCb(WebRtcState::Signaling state) const + { + if (on_signaling_state_notify_cb_) + on_signaling_state_notify_cb_(state); + }; + void UnsetOnSignalingStateNotifyCb(void) { on_signaling_state_notify_cb_ = nullptr; }; + + void SetOnIceConnectionStateNotifyCb(std::function<void(WebRtcState::IceConnection)> on_ice_connection_state_notify_cb) + { + on_ice_connection_state_notify_cb_ = on_ice_connection_state_notify_cb; + }; + void CallOnIceConnectionStateNotifyCb(WebRtcState::IceConnection state) const + { + if (on_ice_connection_state_notify_cb_) + on_ice_connection_state_notify_cb_(state); + }; + void UnsetOnIceConnectionStateNotifyeCb(void) { on_ice_connection_state_notify_cb_ = nullptr; }; + + void SetOnEncodedFrameCb(std::function<void(void)> on_encoded_frame_cb) + { + on_encoded_frame_cb_ = on_encoded_frame_cb; + }; + void CallOnEncodedFrameCb(void) const + { + if (on_encoded_frame_cb_) + on_encoded_frame_cb_(); + }; + void UnsetEncodedFrameCb(void) { on_encoded_frame_cb_ = nullptr; }; + + void SetOnTrakAddedCb(std::function<void(unsigned int id)> on_track_added_cb) + { + on_track_added_cb_ = on_track_added_cb; + }; + void CallOnTrakAddedCb(unsigned int id) const + { + if (on_track_added_cb_) + on_track_added_cb_(id); + }; + void UnsetTrackAddedCb(void) { on_track_added_cb_ = nullptr; }; + + private: + std::function<void(void)> on_negotiation_needed_cb_; + std::function<void(WebRtcState::Stream)> on_state_changed_cb_; + std::function<void(WebRtcState::Signaling)> on_signaling_state_notify_cb_; + std::function<void(WebRtcState::IceConnection)> on_ice_connection_state_notify_cb_; + std::function<void(void)> on_encoded_frame_cb_; + std::function<void(unsigned int id)> on_track_added_cb_; +}; diff --git a/modules/webrtc/WebRtcMessage.cc b/modules/webrtc/WebRtcMessage.cc new file mode 100644 index 0000000..3d9af79 --- /dev/null +++ b/modules/webrtc/WebRtcMessage.cc @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <json-glib/json-glib.h> + +#include "aitt_internal.h" + +#include "WebRtcMessage.h" + +WebRtcMessage::Type WebRtcMessage::getMessageType(const std::string &message) +{ + WebRtcMessage::Type type = WebRtcMessage::Type::UNKNOWN; + JsonParser *parser = json_parser_new(); + if (!json_parser_load_from_data(parser, message.c_str(), -1, NULL)) { + DBG("Unknown message '%s', ignoring", message.c_str()); + g_object_unref(parser); + return type; + } + + JsonNode *root = json_parser_get_root(parser); + if (!JSON_NODE_HOLDS_OBJECT(root)) { + DBG("Unknown json message '%s', ignoring", message.c_str()); + g_object_unref(parser); + return type; + } + + JsonObject *object = json_node_get_object(root); + /* Check type of JSON message */ + + if (json_object_has_member(object, "sdp")) { + type = WebRtcMessage::Type::SDP; + } else if (json_object_has_member(object, "ice")) { + type = WebRtcMessage::Type::ICE; + } else { + DBG("%s:UNKNOWN", __func__); + } + + g_object_unref(parser); + return type; +} diff --git a/modules/webrtc/WebRtcMessage.h b/modules/webrtc/WebRtcMessage.h new file mode 100644 index 0000000..6057a22 --- /dev/null +++ b/modules/webrtc/WebRtcMessage.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> + +class WebRtcMessage { + public: + enum class Type { + SDP, + ICE, + UNKNOWN, + }; + static WebRtcMessage::Type getMessageType(const std::string &message); +}; diff --git a/modules/webrtc/WebRtcPeer.cc b/modules/webrtc/WebRtcPeer.cc new file mode 100644 index 0000000..119f6e4 --- /dev/null +++ b/modules/webrtc/WebRtcPeer.cc @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WebRtcPeer.h" + +#include "WebRtcMessage.h" +#include "aitt_internal.h" + +WebRtcPeer::WebRtcPeer(const std::string &peer_id) + : local_id_(peer_id), webrtc_stream_(std::make_shared<WebRtcStream>()) +{ + DBG("%s", __func__); +} + +WebRtcPeer::~WebRtcPeer() +{ + webrtc_stream_ = nullptr; + DBG("%s removed", local_id_.c_str()); +} + +std::shared_ptr<WebRtcStream> WebRtcPeer::GetWebRtcStream(void) const +{ + return webrtc_stream_; +} + +void WebRtcPeer::SetWebRtcStream(std::shared_ptr<WebRtcStream> webrtc_stream) +{ + webrtc_stream_ = webrtc_stream; +} + +std::string WebRtcPeer::getId(void) const +{ + return local_id_; +} + +void WebRtcPeer::HandleMessage(const std::string &message) +{ + WebRtcMessage::Type type = WebRtcMessage::getMessageType(message); + if (type == WebRtcMessage::Type::SDP) + webrtc_stream_->SetRemoteDescription(message); + else if (type == WebRtcMessage::Type::ICE) + webrtc_stream_->AddIceCandidateFromMessage(message); + else + DBG("%s can't handle %s", __func__, message.c_str()); +} diff --git a/modules/webrtc/WebRtcPeer.h b/modules/webrtc/WebRtcPeer.h new file mode 100644 index 0000000..1ccb4e9 --- /dev/null +++ b/modules/webrtc/WebRtcPeer.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <memory> +#include <string> + +#include "WebRtcStream.h" + +class WebRtcPeer { + public: + explicit WebRtcPeer(const std::string &peer_id); + ~WebRtcPeer(); + + std::shared_ptr<WebRtcStream> GetWebRtcStream(void) const; + void SetWebRtcStream(std::shared_ptr<WebRtcStream> webrtc_stream); + std::string getId(void) const; + void HandleMessage(const std::string &message); + + private: + std::string local_id_; + std::shared_ptr<WebRtcStream> webrtc_stream_; +}; diff --git a/modules/webrtc/WebRtcRoom.cc b/modules/webrtc/WebRtcRoom.cc new file mode 100644 index 0000000..781b72b --- /dev/null +++ b/modules/webrtc/WebRtcRoom.cc @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WebRtcRoom.h" + +#include <sstream> +#include <stdexcept> + +#include "aitt_internal.h" + +WebRtcRoom::~WebRtcRoom() +{ + //TODO How about removing handling webrtc_stream part from Room? + for (auto pair : peers_) { + auto peer = pair.second; + auto webrtc_stream = peer->GetWebRtcStream(); + webrtc_stream->Stop(); + webrtc_stream->Destroy(); + } +} + +static std::vector<std::string> split(const std::string &line, char delimiter) +{ + std::vector<std::string> result; + std::stringstream input(line); + + std::string buffer; + while (getline(input, buffer, delimiter)) { + result.push_back(buffer); + } + + return result; +} + +void WebRtcRoom::handleMessage(const std::string &msg) +{ + if (msg.compare("ROOM_OK ") == 0) + CallRoomJoinedCb(); + else if (msg.compare(0, 8, "ROOM_OK ") == 0) { + CallRoomJoinedCb(); + HandleRoomJoinedWithPeerList(msg.substr(8, std::string::npos)); + } else if (msg.compare(0, 16, "ROOM_PEER_JOINED") == 0) { + CallPeerJoinedCb(msg.substr(17, std::string::npos)); + } else if (msg.compare(0, 14, "ROOM_PEER_LEFT") == 0) { + CallPeerLeftCb(msg.substr(15, std::string::npos)); + } else if (msg.compare(0, 13, "ROOM_PEER_MSG") == 0) { + HandlePeerMessage(msg.substr(14, std::string::npos)); + } else { + DBG("Not defined"); + } + + return; +} + +void WebRtcRoom::HandleRoomJoinedWithPeerList(const std::string &peer_list) +{ + auto peer_ids = split(peer_list, ' '); + for (const auto &peer_id : peer_ids) { + CallPeerJoinedCb(peer_id); + } +} + +void WebRtcRoom::HandlePeerMessage(const std::string &msg) +{ + std::size_t pos = msg.find(' '); + if (pos == std::string::npos) { + DBG("%s can't handle %s", __func__, msg.c_str()); + return; + } + + auto peer_id = msg.substr(0, pos); + auto itr = peers_.find(peer_id); + if (itr == peers_.end()) { + ERR("%s is not in peer list", peer_id.c_str()); + //Opening backdoor here for source. What'll be crisis for this? + CallPeerJoinedCb(peer_id); + itr = peers_.find(peer_id); + RET_IF(itr == peers_.end()); + } + + itr->second->HandleMessage(msg.substr(pos + 1, std::string::npos)); +} + +bool WebRtcRoom::AddPeer(const std::string &peer_id) +{ + auto peer = std::make_shared<WebRtcPeer>(peer_id); + auto ret = peers_.insert(std::make_pair(peer_id, peer)); + + return ret.second; +} + +bool WebRtcRoom::RemovePeer(const std::string &peer_id) +{ + auto itr = peers_.find(peer_id); + if (itr == peers_.end()) { + DBG("There's no such peer"); + return false; + } + auto peer = itr->second; + + //TODO How about removing handling webrtc_stream part from Room? + auto webrtc_stream = peer->GetWebRtcStream(); + webrtc_stream->Stop(); + webrtc_stream->Destroy(); + + return peers_.erase(peer_id) == 1; +} + +WebRtcPeer &WebRtcRoom::GetPeer(const std::string &peer_id) +{ + auto itr = peers_.find(peer_id); + if (itr == peers_.end()) + throw std::out_of_range("There's no such peer"); + + return *itr->second; +} + +void WebRtcRoom::ClearPeers(void) +{ + //TODO How about removing handling webrtc_stream part from Room? + for (auto pair : peers_) { + auto peer = pair.second; + auto webrtc_stream = peer->GetWebRtcStream(); + webrtc_stream->Stop(); + webrtc_stream->Destroy(); + } + peers_.clear(); +} diff --git a/modules/webrtc/WebRtcRoom.h b/modules/webrtc/WebRtcRoom.h new file mode 100644 index 0000000..fabeb1e --- /dev/null +++ b/modules/webrtc/WebRtcRoom.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <functional> +#include <map> +#include <memory> +#include <string> + +#include "WebRtcPeer.h" + +class WebRtcRoom { + public: + enum class State { + JOINNING, + JOINED, + }; + WebRtcRoom() = delete; + WebRtcRoom(const std::string &room_id) : id_(room_id){}; + ~WebRtcRoom(); + void setRoomState(State current) { state_ = current; } + State getRoomState(void) const { return state_; }; + void handleMessage(const std::string &msg); + bool AddPeer(const std::string &peer_id); + bool RemovePeer(const std::string &peer_id); + void ClearPeers(void); + // You need to handle out_of_range exception if there's no matching peer; + WebRtcPeer &GetPeer(const std::string &peer_id); + std::string getId(void) const { return id_; }; + void SetSourceId(const std::string &source_id) { source_id_ = source_id; }; + std::string GetSourceId(void) const { return source_id_; }; + + void SetRoomJoinedCb(std::function<void(void)> on_room_joined_cb) + { + on_room_joined_cb_ = on_room_joined_cb; + }; + void CallRoomJoinedCb(void) const + { + if (on_room_joined_cb_) + on_room_joined_cb_(); + }; + void UnsetRoomJoinedCb(void) { on_room_joined_cb_ = nullptr; }; + void SetPeerJoinedCb(std::function<void(const std::string &peer_id)> on_peer_joined_cb) + { + on_peer_joined_cb_ = on_peer_joined_cb; + }; + void CallPeerJoinedCb(const std::string &peer_id) const + { + if (on_peer_joined_cb_) + on_peer_joined_cb_(peer_id); + }; + void UnsetPeerJoinedCb(void) { on_peer_joined_cb_ = nullptr; }; + void SetPeerLeftCb(std::function<void(const std::string &peer_id)> on_peer_left_cb) + { + on_peer_left_cb_ = on_peer_left_cb; + }; + void CallPeerLeftCb(const std::string &peer_id) const + { + if (on_peer_left_cb_) + on_peer_left_cb_(peer_id); + }; + void UnsetPeerLeftCb(void) { on_peer_left_cb_ = nullptr; }; + + private: + void HandleRoomJoinedWithPeerList(const std::string &peer_list); + void HandlePeerMessage(const std::string &msg); + + private: + std::string id_; + std::string source_id_; + std::map<std::string, std::shared_ptr<WebRtcPeer>> peers_; + State state_; + std::function<void(void)> on_room_joined_cb_; + std::function<void(const std::string &peer_id)> on_peer_joined_cb_; + std::function<void(const std::string &peer_id)> on_peer_left_cb_; +}; diff --git a/modules/webrtc/WebRtcState.cc b/modules/webrtc/WebRtcState.cc new file mode 100644 index 0000000..437460d --- /dev/null +++ b/modules/webrtc/WebRtcState.cc @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WebRtcState.h" + +WebRtcState::Stream WebRtcState::ToStreamState(webrtc_state_e state) +{ + switch (state) { + case WEBRTC_STATE_IDLE: { + return Stream::IDLE; + } + case WEBRTC_STATE_NEGOTIATING: { + return Stream::NEGOTIATING; + } + case WEBRTC_STATE_PLAYING: { + return Stream::PLAYING; + } + } + return Stream::IDLE; +} + +std::string WebRtcState::StreamToStr(WebRtcState::Stream state) +{ + switch (state) { + case Stream::IDLE: { + return std::string("IDLE"); + } + case Stream::NEGOTIATING: { + return std::string("NEGOTIATING"); + } + case Stream::PLAYING: { + return std::string("PLAYING"); + } + } + return std::string(""); +} + +WebRtcState::Signaling WebRtcState::ToSignalingState(webrtc_signaling_state_e state) +{ + switch (state) { + case WEBRTC_SIGNALING_STATE_STABLE: { + return Signaling::STABLE; + } + case WEBRTC_SIGNALING_STATE_HAVE_LOCAL_OFFER: { + return Signaling::HAVE_LOCAL_OFFER; + } + case WEBRTC_SIGNALING_STATE_HAVE_REMOTE_OFFER: { + return Signaling::HAVE_REMOTE_OFFER; + } + case WEBRTC_SIGNALING_STATE_HAVE_LOCAL_PRANSWER: { + return Signaling::HAVE_LOCAL_PRANSWER; + } + case WEBRTC_SIGNALING_STATE_HAVE_REMOTE_PRANSWER: { + return Signaling::HAVE_REMOTE_PRANSWER; + } + case WEBRTC_SIGNALING_STATE_CLOSED: { + return Signaling::CLOSED; + } + } + return Signaling::STABLE; +} + +std::string WebRtcState::SignalingToStr(WebRtcState::Signaling state) +{ + switch (state) { + case (WebRtcState::Signaling::STABLE): { + return std::string("STABLE"); + } + case (WebRtcState::Signaling::CLOSED): { + return std::string("CLOSED"); + } + case (WebRtcState::Signaling::HAVE_LOCAL_OFFER): { + return std::string("HAVE_LOCAL_OFFER"); + } + case (WebRtcState::Signaling::HAVE_REMOTE_OFFER): { + return std::string("HAVE_REMOTE_OFFER"); + } + case (WebRtcState::Signaling::HAVE_LOCAL_PRANSWER): { + return std::string("HAVE_LOCAL_PRANSWER"); + } + case (WebRtcState::Signaling::HAVE_REMOTE_PRANSWER): { + return std::string("HAVE_REMOTE_PRANSWER"); + } + } + return std::string(""); +} + +WebRtcState::IceGathering WebRtcState::ToIceGatheringState(webrtc_ice_gathering_state_e state) +{ + switch (state) { + case WEBRTC_ICE_GATHERING_STATE_COMPLETE: { + return IceGathering::COMPLETE; + } + case WEBRTC_ICE_GATHERING_STATE_GATHERING: { + return IceGathering::GATHERING; + } + case WEBRTC_ICE_GATHERING_STATE_NEW: { + return IceGathering::NEW; + } + } + return IceGathering::NEW; +} + +std::string WebRtcState::IceGatheringToStr(WebRtcState::IceGathering state) +{ + switch (state) { + case (WebRtcState::IceGathering::NEW): { + return std::string("NEW"); + } + case (WebRtcState::IceGathering::GATHERING): { + return std::string("GATHERING"); + } + case (WebRtcState::IceGathering::COMPLETE): { + return std::string("COMPLETE"); + } + } + return std::string(""); +} + +WebRtcState::IceConnection WebRtcState::ToIceConnectionState(webrtc_ice_connection_state_e state) +{ + switch (state) { + case WEBRTC_ICE_CONNECTION_STATE_CHECKING: { + return IceConnection::CHECKING; + } + case WEBRTC_ICE_CONNECTION_STATE_CLOSED: { + return IceConnection::CLOSED; + } + case WEBRTC_ICE_CONNECTION_STATE_COMPLETED: { + return IceConnection::COMPLETED; + } + case WEBRTC_ICE_CONNECTION_STATE_CONNECTED: { + return IceConnection::CONNECTED; + } + case WEBRTC_ICE_CONNECTION_STATE_DISCONNECTED: { + return IceConnection::DISCONNECTED; + } + case WEBRTC_ICE_CONNECTION_STATE_FAILED: { + return IceConnection::FAILED; + } + case WEBRTC_ICE_CONNECTION_STATE_NEW: { + return IceConnection::NEW; + } + } + return IceConnection::NEW; +} + +std::string WebRtcState::IceConnectionToStr(WebRtcState::IceConnection state) +{ + switch (state) { + case (WebRtcState::IceConnection::NEW): { + return std::string("NEW"); + } + case (WebRtcState::IceConnection::CHECKING): { + return std::string("CHECKING"); + } + case (WebRtcState::IceConnection::CONNECTED): { + return std::string("CONNECTED"); + } + case (WebRtcState::IceConnection::COMPLETED): { + return std::string("COMPLETED"); + } + case (WebRtcState::IceConnection::FAILED): { + return std::string("FAILED"); + } + case (WebRtcState::IceConnection::DISCONNECTED): { + return std::string("DISCONNECTED"); + } + case (WebRtcState::IceConnection::CLOSED): { + return std::string("CLOSED"); + } + } + return std::string(""); +} diff --git a/modules/webrtc/WebRtcState.h b/modules/webrtc/WebRtcState.h new file mode 100644 index 0000000..c6ad8d0 --- /dev/null +++ b/modules/webrtc/WebRtcState.h @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <functional> +#include <string> + +#include <webrtc.h> + +class WebRtcState { + public: + enum class Stream { + IDLE, + NEGOTIATING, + PLAYING, + }; + + enum class PeerConnection { + NEW, + CONNECTING, + CONNECTED, + DISCONNECTED, + FAILED, + CLOSED, + }; + + enum class Signaling { + STABLE, + CLOSED, + HAVE_LOCAL_OFFER, + HAVE_REMOTE_OFFER, + HAVE_LOCAL_PRANSWER, + HAVE_REMOTE_PRANSWER, + }; + + enum class IceGathering { + NEW, + GATHERING, + COMPLETE, + }; + + enum class IceConnection { + NEW, + CHECKING, + CONNECTED, + COMPLETED, + FAILED, + DISCONNECTED, + CLOSED, + }; + + public: + static Stream ToStreamState(webrtc_state_e state); + static std::string StreamToStr(WebRtcState::Stream state); + static Signaling ToSignalingState(webrtc_signaling_state_e state); + static std::string SignalingToStr(WebRtcState::Signaling state); + static IceGathering ToIceGatheringState(webrtc_ice_gathering_state_e state); + static std::string IceGatheringToStr(WebRtcState::IceGathering state); + static IceConnection ToIceConnectionState(webrtc_ice_connection_state_e state); + static std::string IceConnectionToStr(WebRtcState::IceConnection state); +}; diff --git a/modules/webrtc/WebRtcStream.cc b/modules/webrtc/WebRtcStream.cc new file mode 100644 index 0000000..ef717b0 --- /dev/null +++ b/modules/webrtc/WebRtcStream.cc @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WebRtcStream.h" + +#include "aitt_internal.h" + +WebRtcStream::~WebRtcStream() +{ + Destroy(); + DBG("%s", __func__); +} + +bool WebRtcStream::Create(bool is_source, bool need_display) +{ + if (webrtc_handle_) { + ERR("Already created %p", webrtc_handle_); + return false; + } + + auto ret = webrtc_create(&webrtc_handle_); + if (ret != WEBRTC_ERROR_NONE) { + ERR("Failed to create webrtc handle"); + return false; + } + AttachSignals(is_source, need_display); + + return true; +} + +void WebRtcStream::Destroy(void) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return; + } + auto stop_ret = webrtc_stop(webrtc_handle_); + if (stop_ret != WEBRTC_ERROR_NONE) + ERR("Failed to stop webrtc handle"); + + auto ret = webrtc_destroy(webrtc_handle_); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to destroy webrtc handle"); + webrtc_handle_ = nullptr; +} + +bool WebRtcStream::Start(void) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return false; + } + if (camera_handler_) + camera_handler_->StartPreview(); + + auto ret = webrtc_start(webrtc_handle_); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to start webrtc handle"); + + return ret == WEBRTC_ERROR_NONE; +} + +bool WebRtcStream::Stop(void) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return false; + } + if (camera_handler_) + camera_handler_->StopPreview(); + + auto ret = webrtc_stop(webrtc_handle_); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to stop webrtc handle"); + + return ret == WEBRTC_ERROR_NONE; +} + +bool WebRtcStream::AttachCameraSource(void) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return false; + } + + if (source_id_) { + ERR("source already attached"); + return false; + } + + auto ret = + webrtc_add_media_source(webrtc_handle_, WEBRTC_MEDIA_SOURCE_TYPE_CAMERA, &source_id_); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to add media source"); + + return ret == WEBRTC_ERROR_NONE; +} + +bool WebRtcStream::AttachCameraPreviewSource(void) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return false; + } + + if (source_id_) { + ERR("source already attached"); + return false; + } + + camera_handler_ = std::make_unique<CameraHandler>(); + camera_handler_->Init(OnMediaPacketPreview, this); + + auto ret = webrtc_add_media_source(webrtc_handle_, WEBRTC_MEDIA_SOURCE_TYPE_MEDIA_PACKET, + &source_id_); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to add media source"); + + return ret == WEBRTC_ERROR_NONE; +} + +void WebRtcStream::OnMediaPacketPreview(media_packet_h media_packet, void *user_data) +{ + ERR("%s", __func__); + auto webrtc_stream = static_cast<WebRtcStream *>(user_data); + RET_IF(webrtc_stream == nullptr); + + if (webrtc_stream->is_source_overflow_) { + return; + } + if (webrtc_media_packet_source_push_packet(webrtc_stream->webrtc_handle_, + webrtc_stream->source_id_, media_packet) + != WEBRTC_ERROR_NONE) { + media_packet_destroy(media_packet); + } +} + +bool WebRtcStream::DetachCameraSource(void) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return false; + } + + if (!source_id_) { + ERR("Camera source is not attached"); + return false; + } + + camera_handler_ = nullptr; + + auto ret = webrtc_remove_media_source(webrtc_handle_, source_id_); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to remove media source"); + + return ret == WEBRTC_ERROR_NONE; +} + +void WebRtcStream::SetDisplayObject(unsigned int id, void *object) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return; + } + + if (!object) { + ERR("Object is not specified"); + return; + } + + webrtc_set_display(webrtc_handle_, id, WEBRTC_DISPLAY_TYPE_EVAS, object); +} + +bool WebRtcStream::CreateOfferAsync(std::function<void(std::string)> on_created_cb) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return false; + } + on_offer_created_cb_ = on_created_cb; + auto ret = webrtc_create_offer_async(webrtc_handle_, NULL, OnOfferCreated, this); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to create offer async"); + + return ret == WEBRTC_ERROR_NONE; +} + +void WebRtcStream::OnOfferCreated(webrtc_h webrtc, const char *description, void *user_data) +{ + RET_IF(!user_data); + + WebRtcStream *webrtc_stream = static_cast<WebRtcStream *>(user_data); + + if (webrtc_stream->on_offer_created_cb_) + webrtc_stream->on_offer_created_cb_(std::string(description)); +} + +bool WebRtcStream::CreateAnswerAsync(std::function<void(std::string)> on_created_cb) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return false; + } + on_answer_created_cb_ = on_created_cb; + auto ret = webrtc_create_answer_async(webrtc_handle_, NULL, OnAnswerCreated, this); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to create answer async"); + + return ret == WEBRTC_ERROR_NONE; +} + +void WebRtcStream::OnAnswerCreated(webrtc_h webrtc, const char *description, void *user_data) +{ + if (!user_data) + return; + + WebRtcStream *webrtc_stream = static_cast<WebRtcStream *>(user_data); + if (webrtc_stream->on_answer_created_cb_) + webrtc_stream->on_answer_created_cb_(std::string(description)); +} + +bool WebRtcStream::SetLocalDescription(const std::string &description) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return false; + } + auto ret = webrtc_set_local_description(webrtc_handle_, description.c_str()); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to set local description"); + + return ret == WEBRTC_ERROR_NONE; +} + +bool WebRtcStream::SetRemoteDescription(const std::string &description) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return false; + } + + webrtc_state_e state; + auto get_state_ret = webrtc_get_state(webrtc_handle_, &state); + if (get_state_ret != WEBRTC_ERROR_NONE) { + ERR("Failed to get state"); + return false; + } + + if (state != WEBRTC_STATE_NEGOTIATING) { + remote_description_ = description; + ERR("Invalid state, will be registred at NEGOTIATING state"); + return true; + } + + auto ret = webrtc_set_remote_description(webrtc_handle_, description.c_str()); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to set remote description"); + + return ret == WEBRTC_ERROR_NONE; +} + +bool WebRtcStream::AddIceCandidateFromMessage(const std::string &ice_message) +{ + ERR("%s", __func__); + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return false; + } + auto ret = webrtc_add_ice_candidate(webrtc_handle_, ice_message.c_str()); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to set add ice candidate"); + + return ret == WEBRTC_ERROR_NONE; +} + +void WebRtcStream::AttachSignals(bool is_source, bool need_display) +{ + if (!webrtc_handle_) { + ERR("WebRTC handle is not created"); + return; + } + + int ret = WEBRTC_ERROR_NONE; + // TODO: ADHOC TV profile doesn't show DBG level log + ret = webrtc_set_error_cb(webrtc_handle_, OnError, this); + DBG("webrtc_set_error_cb %s", ret == WEBRTC_ERROR_NONE ? "Succeeded" : "failed"); + ret = webrtc_set_state_changed_cb(webrtc_handle_, OnStateChanged, this); + DBG("webrtc_set_state_changed_cb %s", ret == WEBRTC_ERROR_NONE ? "Succeeded" : "failed"); + ret = webrtc_set_signaling_state_change_cb(webrtc_handle_, OnSignalingStateChanged, this); + DBG("webrtc_set_signaling_state_change_cb %s", + ret == WEBRTC_ERROR_NONE ? "Succeeded" : "failed"); + ret = webrtc_set_ice_connection_state_change_cb(webrtc_handle_, OnIceConnectionStateChanged, + this); + DBG("webrtc_set_ice_connection_state_change_cb %s", + ret == WEBRTC_ERROR_NONE ? "Succeeded" : "failed"); + ret = webrtc_set_ice_candidate_cb(webrtc_handle_, OnIceCandiate, this); + DBG("webrtc_set_ice_candidate_cb %s", ret == WEBRTC_ERROR_NONE ? "Succeeded" : "failed"); + + if (!is_source && !need_display) { + ret = webrtc_set_encoded_video_frame_cb(webrtc_handle_, OnEncodedFrame, this); + ERR("webrtc_set_encoded_video_frame_cb %s", + ret == WEBRTC_ERROR_NONE ? "Succeeded" : "failed"); + } + + if (!is_source && need_display) { + ret = webrtc_set_track_added_cb(webrtc_handle_, OnTrackAdded, this); + ERR("webrtc_set_track_added_cb %s", ret == WEBRTC_ERROR_NONE ? "Succeeded" : "failed"); + } + + ret = webrtc_media_packet_source_set_buffer_state_changed_cb(webrtc_handle_, source_id_, + OnMediaPacketBufferStateChanged, this); + DBG("webrtc_media_packet_source_set_buffer_state_changed_cb %s", + ret == WEBRTC_ERROR_NONE ? "Succeeded" : "failed"); + + return; +} + +void WebRtcStream::OnError(webrtc_h webrtc, webrtc_error_e error, webrtc_state_e state, + void *user_data) +{ + // TODO + ERR("%s", __func__); +} + +void WebRtcStream::OnStateChanged(webrtc_h webrtc, webrtc_state_e previous, webrtc_state_e current, + void *user_data) +{ + ERR("%s", __func__); + auto webrtc_stream = static_cast<WebRtcStream *>(user_data); + RET_IF(webrtc_stream == nullptr); + + if (current == WEBRTC_STATE_NEGOTIATING && webrtc_stream->remote_description_.size() != 0) { + ERR("received remote description exists"); + auto ret = webrtc_set_remote_description(webrtc_stream->webrtc_handle_, + webrtc_stream->remote_description_.c_str()); + if (ret != WEBRTC_ERROR_NONE) + ERR("Failed to set remote description"); + webrtc_stream->remote_description_ = std::string(); + } + webrtc_stream->GetEventHandler().CallOnStateChangedCb(WebRtcState::ToStreamState(current)); +} + +void WebRtcStream::OnSignalingStateChanged(webrtc_h webrtc, webrtc_signaling_state_e state, + void *user_data) +{ + ERR("%s", __func__); + auto webrtc_stream = static_cast<WebRtcStream *>(user_data); + RET_IF(webrtc_stream == nullptr); + webrtc_stream->GetEventHandler().CallOnSignalingStateNotifyCb( + WebRtcState::ToSignalingState(state)); +} + +void WebRtcStream::OnIceConnectionStateChanged(webrtc_h webrtc, webrtc_ice_connection_state_e state, + void *user_data) +{ + ERR("%s %d", __func__, state); + auto webrtc_stream = static_cast<WebRtcStream *>(user_data); + RET_IF(webrtc_stream == nullptr); + + webrtc_stream->GetEventHandler().CallOnIceConnectionStateNotifyCb( + WebRtcState::ToIceConnectionState(state)); +} + +void WebRtcStream::OnIceCandiate(webrtc_h webrtc, const char *candidate, void *user_data) +{ + ERR("%s", __func__); + auto webrtc_stream = static_cast<WebRtcStream *>(user_data); + webrtc_stream->ice_candidates_.push_back(candidate); +} + +void WebRtcStream::OnEncodedFrame(webrtc_h webrtc, webrtc_media_type_e type, unsigned int track_id, + media_packet_h packet, void *user_data) +{ + ERR("%s", __func__); + // TODO +} + +void WebRtcStream::OnTrackAdded(webrtc_h webrtc, webrtc_media_type_e type, unsigned int id, + void *user_data) +{ + // type AUDIO(0), VIDEO(1) + INFO("Added Track : id(%d), type(%s)", id, type ? "Video" : "Audio"); + + ERR("%s", __func__); + auto webrtc_stream = static_cast<WebRtcStream *>(user_data); + RET_IF(webrtc_stream == nullptr); + + if (type == WEBRTC_MEDIA_TYPE_VIDEO) + webrtc_stream->GetEventHandler().CallOnTrakAddedCb(id); +} + +void WebRtcStream::OnMediaPacketBufferStateChanged(unsigned int source_id, + webrtc_media_packet_source_buffer_state_e state, void *user_data) +{ + ERR("%s", __func__); + auto webrtc_stream = static_cast<WebRtcStream *>(user_data); + RET_IF(webrtc_stream == nullptr); + + webrtc_stream->is_source_overflow_ = + (state == WEBRTC_MEDIA_PACKET_SOURCE_BUFFER_STATE_OVERFLOW); +} diff --git a/modules/webrtc/WebRtcStream.h b/modules/webrtc/WebRtcStream.h new file mode 100644 index 0000000..755c1ae --- /dev/null +++ b/modules/webrtc/WebRtcStream.h @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <functional> +#include <list> +#include <memory> +#include <mutex> +#include <string> + +// TODO: webrtc.h is very heavy header file. +// I think we need to decide whether to include this or not +#include <webrtc.h> + +#include "CameraHandler.h" +#include "WebRtcEventHandler.h" + +class WebRtcStream { + public: + ~WebRtcStream(); + bool Create(bool is_source, bool need_display); + void Destroy(void); + bool Start(void); + bool Stop(void); + bool AttachCameraSource(void); + bool AttachCameraPreviewSource(void); + static void OnMediaPacketPreview(media_packet_h media_packet, void *user_data); + bool DetachCameraSource(void); + void SetDisplayObject(unsigned int id, void *object); + void AttachSignals(bool is_source, bool need_display); + // Cautions : Event handler is not a pointer. So, change event_handle after Set Event handler + // doesn't affect event handler which is included int WebRtcStream + void SetEventHandler(WebRtcEventHandler event_handler) { event_handler_ = event_handler; }; + WebRtcEventHandler &GetEventHandler(void) { return event_handler_; }; + + bool CreateOfferAsync(std::function<void(std::string)> on_created_cb); + void CallOnOfferCreatedCb(std::string offer) + { + if (on_offer_created_cb_) + on_offer_created_cb_(offer); + } + bool CreateAnswerAsync(std::function<void(std::string)> on_created_cb); + void CallOnAnswerCreatedCb(std::string answer) + { + if (on_answer_created_cb_) + on_answer_created_cb_(answer); + } + void SetPreparedLocalDescription(const std::string &description) + { + local_description_ = description; + }; + std::string GetPreparedLocalDescription(void) const { return local_description_; }; + + bool SetLocalDescription(const std::string &description); + bool SetRemoteDescription(const std::string &description); + + bool AddIceCandidateFromMessage(const std::string &ice_message); + const std::vector<std::string> &GetIceCandidates() const { return ice_candidates_; }; + + std::string GetRemoteDescription(void) const { return remote_description_; }; + + private: + static void OnOfferCreated(webrtc_h webrtc, const char *description, void *user_data); + static void OnAnswerCreated(webrtc_h webrtc, const char *description, void *user_data); + static void OnError(webrtc_h webrtc, webrtc_error_e error, webrtc_state_e state, + void *user_data); + static void OnStateChanged(webrtc_h webrtc, webrtc_state_e previous, webrtc_state_e current, + void *user_data); + static void OnSignalingStateChanged(webrtc_h webrtc, webrtc_signaling_state_e state, + void *user_data); + static void OnIceConnectionStateChanged(webrtc_h webrtc, webrtc_ice_connection_state_e state, + void *user_data); + static void OnIceCandiate(webrtc_h webrtc, const char *candidate, void *user_data); + static void OnEncodedFrame(webrtc_h webrtc, webrtc_media_type_e type, unsigned int track_id, + media_packet_h packet, void *user_data); + static void OnTrackAdded(webrtc_h webrtc, webrtc_media_type_e type, unsigned int id, + void *user_data); + static void OnMediaPacketBufferStateChanged(unsigned int source_id, + webrtc_media_packet_source_buffer_state_e state, void *user_data); + + private: + webrtc_h webrtc_handle_; + std::shared_ptr<CameraHandler> camera_handler_; + // DO we need to make is_source_overflow_ as atomic? + bool is_source_overflow_; + unsigned int source_id_; + std::string local_description_; + std::string remote_description_; + std::vector<std::string> ice_candidates_; + std::function<void(std::string)> on_offer_created_cb_; + std::function<void(std::string)> on_answer_created_cb_; + WebRtcEventHandler event_handler_; +}; diff --git a/modules/webrtc/tests/CMakeLists.txt b/modules/webrtc/tests/CMakeLists.txt new file mode 100644 index 0000000..a1dd90f --- /dev/null +++ b/modules/webrtc/tests/CMakeLists.txt @@ -0,0 +1,21 @@ +INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/..) + +PKG_CHECK_MODULES(UT_NEEDS REQUIRED gmock_main) +INCLUDE_DIRECTORIES(${UT_NEEDS_INCLUDE_DIRS}) +LINK_DIRECTORIES(${UT_NEEDS_LIBRARY_DIRS}) + +SET(AITT_WEBRTC_UT ${PROJECT_NAME}_webrtc_ut) +SET(AITT_WEBRTC_UT_SRC WEBRTC_test.cc) + +ADD_EXECUTABLE(${AITT_WEBRTC_UT} ${AITT_WEBRTC_UT_SRC} $<TARGET_OBJECTS:WEBRTC_OBJ>) +TARGET_LINK_LIBRARIES(${AITT_WEBRTC_UT} ${UT_NEEDS_LIBRARIES} ${AITT_WEBRTC_NEEDS_LIBRARIES} ${AITT_COMMON}) +INSTALL(TARGETS ${AITT_WEBRTC_UT} DESTINATION ${AITT_TEST_BINDIR}) + +ADD_TEST( + NAME + ${AITT_WEBRTC_UT} + COMMAND + ${CMAKE_COMMAND} -E env + LD_LIBRARY_PATH=../../../common:$ENV{LD_LIBRARY_PATH} + ${CMAKE_CURRENT_BINARY_DIR}/${AITT_WEBRTC_UT} --gtest_filter=*_Anytime +) diff --git a/modules/webrtc/tests/MockPublishStream.h b/modules/webrtc/tests/MockPublishStream.h new file mode 100644 index 0000000..ced285e --- /dev/null +++ b/modules/webrtc/tests/MockPublishStream.h @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <memory> +#include <string> + +#include "Config.h" +#include "MqttServer.h" +#include "WebRtcRoom.h" + +class MockPublishStream { + // TODO: Notify & get status + public: + MockPublishStream() = delete; + MockPublishStream(const std::string &topic, const Config &config) + : topic_(topic), config_(config), server_(std::make_shared<MqttServer>(config)) + { + config.SetSourceId(config.GetLocalId()); + config.SetRoomId(config::MQTT_ROOM_PREFIX() + topic); + room_ = std::make_shared<WebRtcRoom>(config.GetRoomId()); + }; + ~MockPublishStream(); + + void Start(void) + { + SetSignalingServerCallbacks(); + SetRoomCallbacks(); + server_->Connect(); + } + void Stop(void){ + // TODO + }; + + private: + void SetSignalingServerCallbacks(void) + { + auto on_signaling_server_connection_state_changed = + std::bind(OnSignalingServerConnectionStateChanged, std::placeholders::_1, + std::ref(*room_), std::ref(*server_)); + + server_->SetConnectionStateChangedCb(on_signaling_server_connection_state_changed); + + auto on_room_message_arrived = + std::bind(OnRoomMessageArrived, std::placeholders::_1, std::ref(*room_)); + + server_->SetRoomMessageArrivedCb(on_room_message_arrived); + }; + + void SetRoomCallbacks(void) + { + auto on_peer_joined = + std::bind(OnPeerJoined, std::placeholders::_1, std::ref(*server_), std::ref(*room_)); + room_->SetPeerJoinedCb(on_peer_joined); + + auto on_peer_left = std::bind(OnPeerLeft, std::placeholders::_1, std::ref(*room_)); + room_->SetPeerLeftCb(on_peer_left); + }; + + static void OnSignalingServerConnectionStateChanged(IfaceServer::ConnectionState state, + WebRtcRoom &room, MqttServer &server) + { + DBG("current state [%s]", SignalingServer::GetConnectionStateStr(state).c_str()); + + if (state == IfaceServer::ConnectionState::Registered) + server.JoinRoom(room.getId()); + }; + + static void OnRoomMessageArrived(const std::string &message, WebRtcRoom &room) + { + room.handleMessage(message); + }; + + static void OnPeerJoined(const std::string &peer_id, MqttServer &server, WebRtcRoom &room) + { + DBG("%s [%s]", __func__, peer_id.c_str()); + }; + + static void OnPeerLeft(const std::string &peer_id, WebRtcRoom &room) + { + DBG("%s [%s]", __func__, peer_id.c_str()); + }; + + private: + std::string topic_; + config config_; + std::shared_ptr<MqttServer> server_; + std::shared_ptr<WebRtcRoom> room_; +}; diff --git a/modules/webrtc/tests/MockSubscribeStream.h b/modules/webrtc/tests/MockSubscribeStream.h new file mode 100644 index 0000000..ea2736d --- /dev/null +++ b/modules/webrtc/tests/MockSubscribeStream.h @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <memory> + +#include "Config.h" +#include "MqttServer.h" + +class MockSubscribeStream { + public: + MockSubscribeStream() = delete; + MockSubscribeStream(const std::string &topic, const Config &config) + : topic_(topic), config_(config), server_(std::make_shared<SignalingServer>(config)) + { + config.SetRoomId(config::MQTT_ROOM_PREFIX() + topic); + room_ = std::make_shared<WebRtcRoom>(config.GetRoomId()); + }; + ~MockSubscribeStream(){}; + + void Start(void) + { + SetSignalingServerCallbacks(); + SetRoomCallbacks(); + server_->Connect(); + }; + + void Stop(void){ + //TODO + }; + + private: + void SetSignalingServerCallbacks(void) + { + auto on_signaling_server_connection_state_changed = + std::bind(OnSignalingServerConnectionStateChanged, std::placeholders::_1, + std::ref(*room_), std::ref(*server_)); + + server_->SetConnectionStateChangedCb(on_signaling_server_connection_state_changed); + + auto on_room_message_arrived = + std::bind(OnRoomMessageArrived, std::placeholders::_1, std::ref(*room_)); + + server_->SetRoomMessageArrivedCb(on_room_message_arrived); + }; + + void SetRoomCallbacks(void) + { + auto on_peer_joined = + std::bind(OnPeerJoined, std::placeholders::_1, std::ref(*server_), std::ref(*room_)); + room_->SetPeerJoinedCb(on_peer_joined); + + auto on_peer_left = std::bind(OnPeerLeft, std::placeholders::_1, std::ref(*room_)); + room_->SetPeerLeftCb(on_peer_left); + }; + + static void OnSignalingServerConnectionStateChanged(IfaceServer::ConnectionState state, + WebRtcRoom &room, MqttServer &server) + { + DBG("current state [%s]", SignalingServer::GetConnectionStateStr(state).c_str()); + + if (state == IfaceServer::ConnectionState::Registered) + server.JoinRoom(room.getId()); + }; + + static void OnRoomMessageArrived(const std::string &message, WebRtcRoom &room) + { + room.handleMessage(message); + }; + + static void OnPeerJoined(const std::string &peer_id, MqttServer &server, WebRtcRoom &room) + { + DBG("%s [%s]", __func__, peer_id.c_str()); + }; + + static void OnPeerLeft(const std::string &peer_id, WebRtcRoom &room) + { + DBG("%s [%s]", __func__, peer_id.c_str()); + }; + + private: + std::string topic_; + Config config_; + std::shared_ptr<MqttServer> server_; + std::shared_ptr<WebRtcRoom> room_; +}; diff --git a/modules/webrtc/tests/WEBRTC_test.cc b/modules/webrtc/tests/WEBRTC_test.cc new file mode 100644 index 0000000..5ee42c1 --- /dev/null +++ b/modules/webrtc/tests/WEBRTC_test.cc @@ -0,0 +1,598 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <glib.h> +#include <gtest/gtest.h> + +#include <chrono> +#include <set> +#include <thread> + +#include "AITTEx.h" +#include "Config.h" +#include "MqttServer.h" +#include "aitt_internal.h" + +#define DEFAULT_BROKER_IP "127.0.0.1" +#define DEFAULT_BROKER_PORT 1883 + +#define DEFAULT_WEBRTC_SRC_ID "webrtc_src" +#define DEFAULT_FIRST_SINK_ID "webrtc_first_sink" +#define DEFAULT_SECOND_SINK_ID "webrtc_second_sink" +#define DEFAULT_ROOM_ID AITT_MANAGED_TOPIC_PREFIX "webrtc/room/Room.webrtc" + +class MqttServerTest : public testing::Test { + protected: + void SetUp() override + { + webrtc_src_config_ = Config(DEFAULT_WEBRTC_SRC_ID, DEFAULT_BROKER_IP, DEFAULT_BROKER_PORT, + DEFAULT_ROOM_ID, DEFAULT_WEBRTC_SRC_ID); + webrtc_first_sink_config_ = Config(DEFAULT_FIRST_SINK_ID, DEFAULT_BROKER_IP, + DEFAULT_BROKER_PORT, DEFAULT_ROOM_ID); + webrtc_second_sink_config_ = Config(DEFAULT_SECOND_SINK_ID, DEFAULT_BROKER_IP, + DEFAULT_BROKER_PORT, DEFAULT_ROOM_ID); + + loop_ = g_main_loop_new(nullptr, FALSE); + } + + void TearDown() override { g_main_loop_unref(loop_); } + + protected: + Config webrtc_src_config_; + Config webrtc_first_sink_config_; + Config webrtc_second_sink_config_; + GMainLoop *loop_; +}; +static void onConnectionStateChanged(IfaceServer::ConnectionState state, MqttServer &server, + GMainLoop *loop) +{ + if (state == IfaceServer::ConnectionState::Registered) { + EXPECT_EQ(server.IsConnected(), true) << "should return Connected"; + g_main_loop_quit(loop); + } +} + +TEST_F(MqttServerTest, Positive_Connect_Anytime) +{ + try { + MqttServer server(webrtc_src_config_); + EXPECT_EQ(server.IsConnected(), false) << "Should return not connected"; + + auto on_connection_state_changed = + std::bind(onConnectionStateChanged, std::placeholders::_1, std::ref(server), loop_); + server.SetConnectionStateChangedCb(on_connection_state_changed); + + server.Connect(); + + g_main_loop_run(loop_); + + server.UnsetConnectionStateChangedCb(); + server.Disconnect(); + } catch (...) { + FAIL() << "Expected No throw"; + } +} +static int Positive_Connect_Src_Sinks_Anytime_connect_count; +static void onConnectionStateChangedPositive_Connect_Src_Sinks_Anytime( + IfaceServer::ConnectionState state, MqttServer &server, GMainLoop *loop) +{ + if (state == IfaceServer::ConnectionState::Registered) { + EXPECT_EQ(server.IsConnected(), true) << "should return Connected"; + ++Positive_Connect_Src_Sinks_Anytime_connect_count; + if (Positive_Connect_Src_Sinks_Anytime_connect_count == 3) { + g_main_loop_quit(loop); + } + } +} + +TEST_F(MqttServerTest, Positive_Connect_Src_Sinks_Anytime) +{ + try { + Positive_Connect_Src_Sinks_Anytime_connect_count = 0; + MqttServer src_server(webrtc_src_config_); + EXPECT_EQ(src_server.IsConnected(), false) << "Should return not connected"; + + auto on_src_connection_state_changed = + std::bind(onConnectionStateChangedPositive_Connect_Src_Sinks_Anytime, + std::placeholders::_1, std::ref(src_server), loop_); + src_server.SetConnectionStateChangedCb(on_src_connection_state_changed); + + src_server.Connect(); + + MqttServer first_sink_server(webrtc_first_sink_config_); + EXPECT_EQ(first_sink_server.IsConnected(), false) << "Should return not connected"; + + auto on_first_sink_connection_state_changed = + std::bind(onConnectionStateChangedPositive_Connect_Src_Sinks_Anytime, + std::placeholders::_1, std::ref(first_sink_server), loop_); + first_sink_server.SetConnectionStateChangedCb(on_first_sink_connection_state_changed); + + first_sink_server.Connect(); + + MqttServer second_sink_server(webrtc_second_sink_config_); + EXPECT_EQ(second_sink_server.IsConnected(), false) << "Should return not connected"; + + auto on_second_sink_connection_state_changed = + std::bind(onConnectionStateChangedPositive_Connect_Src_Sinks_Anytime, + std::placeholders::_1, std::ref(second_sink_server), loop_); + second_sink_server.SetConnectionStateChangedCb(on_second_sink_connection_state_changed); + + second_sink_server.Connect(); + + g_main_loop_run(loop_); + + src_server.UnsetConnectionStateChangedCb(); + first_sink_server.UnsetConnectionStateChangedCb(); + second_sink_server.UnsetConnectionStateChangedCb(); + src_server.Disconnect(); + first_sink_server.Disconnect(); + second_sink_server.Disconnect(); + } catch (...) { + FAIL() << "Expected No throw"; + } +} + +TEST_F(MqttServerTest, Negative_Disconnect_Anytime) +{ + EXPECT_THROW( + { + try { + MqttServer server(webrtc_src_config_); + EXPECT_EQ(server.IsConnected(), false) << "Should return not connected"; + + server.Disconnect(); + + g_main_loop_run(loop_); + } catch (const aitt::AITTEx &e) { + // and this tests that it has the correct message + throw; + } + }, + aitt::AITTEx); +} + +TEST_F(MqttServerTest, Positive_Disconnect_Anytime) +{ + try { + MqttServer server(webrtc_src_config_); + EXPECT_EQ(server.IsConnected(), false); + + auto on_connection_state_changed = + std::bind(onConnectionStateChanged, std::placeholders::_1, std::ref(server), loop_); + server.SetConnectionStateChangedCb(on_connection_state_changed); + + server.Connect(); + + g_main_loop_run(loop_); + + server.UnsetConnectionStateChangedCb(); + server.Disconnect(); + + EXPECT_EQ(server.IsConnected(), false) << "Should return not connected"; + } catch (...) { + FAIL() << "Expected No throw"; + } +} + +TEST_F(MqttServerTest, Negative_Register_Anytime) +{ + EXPECT_THROW( + { + try { + MqttServer server(webrtc_src_config_); + EXPECT_EQ(server.IsConnected(), false) << "Should return not connected"; + + server.RegisterWithServer(); + } catch (const std::runtime_error &e) { + // and this tests that it has the correct message + throw; + } + }, + std::runtime_error); +} + +TEST_F(MqttServerTest, Negative_JoinRoom_Invalid_Parameter_Anytime) +{ + EXPECT_THROW( + { + try { + MqttServer server(webrtc_src_config_); + EXPECT_EQ(server.IsConnected(), false) << "Should return not connected"; + + server.JoinRoom(std::string("InvalidRoomId")); + + } catch (const std::runtime_error &e) { + // and this tests that it has the correct message + throw; + } + }, + std::runtime_error); +} + +static void joinRoomOnRegisteredQuit(IfaceServer::ConnectionState state, MqttServer &server, + GMainLoop *loop) +{ + if (state != IfaceServer::ConnectionState::Registered) { + return; + } + + EXPECT_EQ(server.IsConnected(), true) << "should return Connected"; + try { + server.JoinRoom(DEFAULT_ROOM_ID); + g_main_loop_quit(loop); + } catch (...) { + FAIL() << "Expected No throw"; + } +} + +TEST_F(MqttServerTest, Positive_JoinRoom_Anytime) +{ + try { + MqttServer server(webrtc_src_config_); + EXPECT_EQ(server.IsConnected(), false) << "Should return not connected"; + + auto join_room_on_registered = + std::bind(joinRoomOnRegisteredQuit, std::placeholders::_1, std::ref(server), loop_); + server.SetConnectionStateChangedCb(join_room_on_registered); + + server.Connect(); + + g_main_loop_run(loop_); + + server.UnsetConnectionStateChangedCb(); + server.Disconnect(); + } catch (...) { + FAIL() << "Expected No throw"; + } +} + +static void joinRoomOnRegistered(IfaceServer::ConnectionState state, MqttServer &server) +{ + if (state != IfaceServer::ConnectionState::Registered) { + return; + } + + EXPECT_EQ(server.IsConnected(), true) << "should return Connected"; + try { + server.JoinRoom(DEFAULT_ROOM_ID); + } catch (...) { + FAIL() << "Expected No throw"; + } +} + +static void onSrcMessage(const std::string &msg, MqttServer &server, GMainLoop *loop) +{ + if (msg.compare(0, 16, "ROOM_PEER_JOINED") == 0) { + std::string peer_id = msg.substr(17, std::string::npos); + EXPECT_EQ(peer_id.compare(std::string(DEFAULT_FIRST_SINK_ID)), 0) + << "Not expected peer" << peer_id; + + } else if (msg.compare(0, 14, "ROOM_PEER_LEFT") == 0) { + std::string peer_id = msg.substr(15, std::string::npos); + EXPECT_EQ(peer_id.compare(std::string(DEFAULT_FIRST_SINK_ID)), 0) + << "Not expected peer" << peer_id; + g_main_loop_quit(loop); + } else { + FAIL() << "Invalid type of Room message " << msg; + } +} + +static void onSinkMessage(const std::string &msg, MqttServer &server, GMainLoop *loop) +{ + if (msg.compare(0, 16, "ROOM_PEER_JOINED") == 0) { + std::string peer_id = msg.substr(17, std::string::npos); + EXPECT_EQ(peer_id.compare(std::string(DEFAULT_WEBRTC_SRC_ID)), 0) + << "Not expected peer" << peer_id; + server.Disconnect(); + } else { + FAIL() << "Invalid type of Room message " << msg; + } +} + +TEST_F(MqttServerTest, Positive_src_sink) +{ + try { + MqttServer src_server(webrtc_src_config_); + auto join_room_on_registered_src = + std::bind(joinRoomOnRegistered, std::placeholders::_1, std::ref(src_server)); + src_server.SetConnectionStateChangedCb(join_room_on_registered_src); + + auto on_src_message = + std::bind(onSrcMessage, std::placeholders::_1, std::ref(src_server), loop_); + src_server.SetRoomMessageArrivedCb(on_src_message); + src_server.Connect(); + + MqttServer sink_server(webrtc_first_sink_config_); + auto join_room_on_registered_sink = + std::bind(joinRoomOnRegistered, std::placeholders::_1, std::ref(sink_server)); + sink_server.SetConnectionStateChangedCb(join_room_on_registered_sink); + + auto on_sink_message = + std::bind(onSinkMessage, std::placeholders::_1, std::ref(sink_server), loop_); + sink_server.SetRoomMessageArrivedCb(on_sink_message); + + sink_server.Connect(); + + g_main_loop_run(loop_); + + src_server.UnsetConnectionStateChangedCb(); + sink_server.UnsetConnectionStateChangedCb(); + src_server.Disconnect(); + } catch (...) { + FAIL() << "Expected No throw"; + } +} + +TEST_F(MqttServerTest, Positive_sink_src) +{ + try { + MqttServer sink_server(webrtc_first_sink_config_); + auto join_room_on_registered_sink = + std::bind(joinRoomOnRegistered, std::placeholders::_1, std::ref(sink_server)); + sink_server.SetConnectionStateChangedCb(join_room_on_registered_sink); + + auto on_sink_message = + std::bind(onSinkMessage, std::placeholders::_1, std::ref(sink_server), loop_); + sink_server.SetRoomMessageArrivedCb(on_sink_message); + + sink_server.Connect(); + + MqttServer src_server(webrtc_src_config_); + auto join_room_on_registered_src = + std::bind(joinRoomOnRegistered, std::placeholders::_1, std::ref(src_server)); + src_server.SetConnectionStateChangedCb(join_room_on_registered_src); + + auto on_src_message = + std::bind(onSrcMessage, std::placeholders::_1, std::ref(src_server), loop_); + src_server.SetRoomMessageArrivedCb(on_src_message); + src_server.Connect(); + + g_main_loop_run(loop_); + + src_server.UnsetConnectionStateChangedCb(); + sink_server.UnsetConnectionStateChangedCb(); + src_server.Disconnect(); + } catch (...) { + FAIL() << "Expected No throw"; + } +} + +static void onSrcMessageDisconnect(const std::string &msg, MqttServer &server, GMainLoop *loop) +{ + if (msg.compare(0, 16, "ROOM_PEER_JOINED") == 0) { + std::string peer_id = msg.substr(17, std::string::npos); + EXPECT_EQ(peer_id.compare(std::string(DEFAULT_FIRST_SINK_ID)), 0) + << "Not expected peer" << peer_id; + server.Disconnect(); + + } else { + FAIL() << "Invalid type of Room message " << msg; + } +} + +static void onSinkMessageDisconnect(const std::string &msg, MqttServer &server, GMainLoop *loop) +{ + if (msg.compare(0, 16, "ROOM_PEER_JOINED") == 0) { + std::string peer_id = msg.substr(17, std::string::npos); + EXPECT_EQ(peer_id.compare(std::string(DEFAULT_WEBRTC_SRC_ID)), 0) + << "Not expected peer" << peer_id; + } else if (msg.compare(0, 14, "ROOM_PEER_LEFT") == 0) { + std::string peer_id = msg.substr(15, std::string::npos); + EXPECT_EQ(peer_id.compare(std::string(DEFAULT_WEBRTC_SRC_ID)), 0) + << "Not expected peer" << peer_id; + g_main_loop_quit(loop); + } else { + FAIL() << "Invalid type of Room message " << msg; + } +} + +TEST_F(MqttServerTest, Positive_src_sink_disconnect_src_first_Anytime) +{ + try { + MqttServer src_server(webrtc_src_config_); + auto join_room_on_registered_src = + std::bind(joinRoomOnRegistered, std::placeholders::_1, std::ref(src_server)); + src_server.SetConnectionStateChangedCb(join_room_on_registered_src); + + auto on_src_message = + std::bind(onSrcMessageDisconnect, std::placeholders::_1, std::ref(src_server), loop_); + src_server.SetRoomMessageArrivedCb(on_src_message); + src_server.Connect(); + + MqttServer sink_server(webrtc_first_sink_config_); + auto join_room_on_registered_sink = + std::bind(joinRoomOnRegistered, std::placeholders::_1, std::ref(sink_server)); + sink_server.SetConnectionStateChangedCb(join_room_on_registered_sink); + + auto on_sink_message = std::bind(onSinkMessageDisconnect, std::placeholders::_1, + std::ref(sink_server), loop_); + sink_server.SetRoomMessageArrivedCb(on_sink_message); + + sink_server.Connect(); + + g_main_loop_run(loop_); + + src_server.UnsetConnectionStateChangedCb(); + sink_server.UnsetConnectionStateChangedCb(); + sink_server.Disconnect(); + } catch (...) { + FAIL() << "Expected No throw"; + } +} + +TEST_F(MqttServerTest, Positive_sink_src_disconnect_src_first_Anytime) +{ + try { + MqttServer sink_server(webrtc_first_sink_config_); + auto join_room_on_registered_sink = + std::bind(joinRoomOnRegistered, std::placeholders::_1, std::ref(sink_server)); + sink_server.SetConnectionStateChangedCb(join_room_on_registered_sink); + + auto on_sink_message = std::bind(onSinkMessageDisconnect, std::placeholders::_1, + std::ref(sink_server), loop_); + sink_server.SetRoomMessageArrivedCb(on_sink_message); + + sink_server.Connect(); + + MqttServer src_server(webrtc_src_config_); + auto join_room_on_registered_src = + std::bind(joinRoomOnRegistered, std::placeholders::_1, std::ref(src_server)); + src_server.SetConnectionStateChangedCb(join_room_on_registered_src); + + auto on_src_message = + std::bind(onSrcMessageDisconnect, std::placeholders::_1, std::ref(src_server), loop_); + src_server.SetRoomMessageArrivedCb(on_src_message); + src_server.Connect(); + + g_main_loop_run(loop_); + + src_server.UnsetConnectionStateChangedCb(); + sink_server.UnsetConnectionStateChangedCb(); + sink_server.Disconnect(); + } catch (...) { + FAIL() << "Expected No throw"; + } +} + +static int handled_sink; +static int expected_sink; + +std::set<std::string> sink_set; + +static void onSrcMessageThreeWay(const std::string &msg, MqttServer &server, GMainLoop *loop) +{ + if (msg.compare(0, 16, "ROOM_PEER_JOINED") == 0) { + auto peer_id = msg.substr(17, std::string::npos); + sink_set.insert(peer_id); + server.SendMessage(peer_id, "Three"); + + } else if (msg.compare(0, 14, "ROOM_PEER_LEFT") == 0) { + auto peer_id = msg.substr(15, std::string::npos); + + if (sink_set.find(peer_id) != sink_set.end()) + sink_set.erase(peer_id); + + if (sink_set.size() == 0 && handled_sink == expected_sink) + g_main_loop_quit(loop); + + } else if (msg.compare(0, 13, "ROOM_PEER_MSG") == 0) { + auto peer_msg = msg.substr(14, std::string::npos); + std::size_t pos = peer_msg.find(' '); + if (pos == std::string::npos) + FAIL() << "Invalid type of peer message" << msg; + + auto peer_id = peer_msg.substr(0, pos); + auto received_msg = peer_msg.substr(pos + 1, std::string::npos); + + if (received_msg.compare("Way") == 0) { + server.SendMessage(peer_id, "HandShake"); + ++handled_sink; + } else + FAIL() << "Can't understand message" << received_msg; + + } else { + FAIL() << "Invalid type of Room message " << msg; + } +} + +static void onSinkMessageThreeWay(const std::string &msg, MqttServer &server) +{ + if (msg.compare(0, 16, "ROOM_PEER_JOINED") == 0) { + auto peer_id = msg.substr(17, std::string::npos); + + EXPECT_EQ(peer_id.compare(std::string(DEFAULT_WEBRTC_SRC_ID)), 0) + << "Not expected peer" << peer_id; + + } else if (msg.compare(0, 14, "ROOM_PEER_LEFT") == 0) { + auto peer_id = msg.substr(15, std::string::npos); + + EXPECT_EQ(peer_id.compare(std::string(DEFAULT_WEBRTC_SRC_ID)), 0) + << "Not expected peer" << peer_id; + + server.Disconnect(); + + } else if (msg.compare(0, 13, "ROOM_PEER_MSG") == 0) { + auto peer_msg = msg.substr(14, std::string::npos); + std::size_t pos = peer_msg.find(' '); + if (pos == std::string::npos) + FAIL() << "Invalid type of peer message" << msg; + + auto peer_id = peer_msg.substr(0, pos); + auto received_msg = peer_msg.substr(pos + 1, std::string::npos); + + EXPECT_EQ(peer_id.compare(std::string(DEFAULT_WEBRTC_SRC_ID)), 0) + << "Not expected peer " << peer_id; + + if (received_msg.compare("Three") == 0) + server.SendMessage(peer_id, "Way"); + else if (received_msg.compare("HandShake") == 0) + server.Disconnect(); + else + FAIL() << "Can't understand message" << received_msg; + } else { + FAIL() << "Invalid type of Room message " << msg; + } +} + +TEST_F(MqttServerTest, Positive_SendMessageThreeWay_Src_Sinks1_Anytime) +{ + try { + handled_sink = 0; + expected_sink = 2; + MqttServer src_server(webrtc_src_config_); + + auto join_room_on_registered_src = + std::bind(joinRoomOnRegistered, std::placeholders::_1, std::ref(src_server)); + src_server.SetConnectionStateChangedCb(join_room_on_registered_src); + + auto on_src_message = + std::bind(onSrcMessageThreeWay, std::placeholders::_1, std::ref(src_server), loop_); + src_server.SetRoomMessageArrivedCb(on_src_message); + src_server.Connect(); + + MqttServer first_sink_server(webrtc_first_sink_config_); + + auto join_room_on_registered_first_sink = + std::bind(joinRoomOnRegistered, std::placeholders::_1, std::ref(first_sink_server)); + first_sink_server.SetConnectionStateChangedCb(join_room_on_registered_first_sink); + + auto on_first_sink_message = + std::bind(onSinkMessageThreeWay, std::placeholders::_1, std::ref(first_sink_server)); + first_sink_server.SetRoomMessageArrivedCb(on_first_sink_message); + first_sink_server.Connect(); + + MqttServer second_sink_server(webrtc_second_sink_config_); + + auto join_room_on_registered_second_sink = + std::bind(joinRoomOnRegistered, std::placeholders::_1, std::ref(second_sink_server)); + second_sink_server.SetConnectionStateChangedCb(join_room_on_registered_second_sink); + + auto on_second_sink_message = + std::bind(onSinkMessageThreeWay, std::placeholders::_1, std::ref(second_sink_server)); + second_sink_server.SetRoomMessageArrivedCb(on_second_sink_message); + + second_sink_server.Connect(); + + g_main_loop_run(loop_); + + src_server.UnsetConnectionStateChangedCb(); + first_sink_server.UnsetConnectionStateChangedCb(); + second_sink_server.UnsetConnectionStateChangedCb(); + src_server.Disconnect(); + } catch (...) { + FAIL() << "Expected No throw"; + } +} diff --git a/packaging/aitt.manifest b/packaging/aitt.manifest new file mode 100644 index 0000000..a76fdba --- /dev/null +++ b/packaging/aitt.manifest @@ -0,0 +1,5 @@ +<manifest> + <request> + <domain name="_" /> + </request> +</manifest> diff --git a/packaging/aitt.spec b/packaging/aitt.spec new file mode 100644 index 0000000..50aaa78 --- /dev/null +++ b/packaging/aitt.spec @@ -0,0 +1,104 @@ +Name: aitt +Version: 0.0.1 +Release: 0 +Summary: AI Telemetry Transport based on MQTT + +Group: Machine Learning / ML Framework +License: Apache-2.0 +Source0: %{name}-%{version}.tar.gz +Source1001: %{name}.manifest + +%{!?stdoutlog: %global stdoutlog 0} +%{!?test: %global test 1} +%{!?gcov: %global gcov 0} + +BuildRequires: cmake +BuildRequires: pkgconfig(dlog) +BuildRequires: pkgconfig(flatbuffers) +BuildRequires: pkgconfig(glib-2.0) +BuildRequires: pkgconfig(libmosquitto) +BuildRequires: pkgconfig(gmock_main) +BuildRequires: pkgconfig(capi-media-tool) +BuildRequires: pkgconfig(capi-media-sound-manager) +BuildRequires: pkgconfig(bundle) +BuildRequires: elementary-tizen +BuildRequires: pkgconfig(capi-media-webrtc) +BuildRequires: pkgconfig(capi-media-camera) +BuildRequires: pkgconfig(json-glib-1.0) +%if 0%{gcov} +BuildRequires: lcov +%endif + +%description +AITT is a Framework which transfers data of AI service. +It makes distributed AI Inference possible. + +%package plugins +Summary: Plugin Libraries for AITT P2P transport +Group: Machine Learning / ML Framework +Requires: %{name} = %{version} + +%description plugins +The %{name}-plugins package contains basic plugin libraries for AITT P2P transport. + +%package devel +Summary: AITT development package +Group: Development/Libraries +Requires: %{name} = %{version}-%{release} + +%description devel +The %{name}-devel package contains libraries and header files for +developing programs that use %{name}. + +%prep +%setup -q +cp %{SOURCE1001} . + +%build +%cmake . \ + -DLOG_STDOUT:BOOL=%{stdoutlog} \ + -DPLATFORM="tizen" \ + -DVERSIONING:BOOL=OFF \ + -DWITH_WEBRTC:BOOL=ON \ + -DCMAKE_INSTALL_PREFIX:PATH=%{_prefix} \ + -DCMAKE_VERBOSE_MAKEFILE=OFF \ + -DBUILD_TESTING:BOOL=%{test} \ + -DCOVERAGE_TEST:BOOL=%{gcov} + +%__make %{?_smp_mflags} + +%install +%make_install + +%check +ctest --output-on-failure --timeout 30 || true + +%if 0%{test} && 0%{gcov} +# Extract coverage information +lcov -c --ignore-errors graph --no-external -b . -d . -o %{name}_gcov.info +genhtml %{name}_gcov.info -o out --legend --show-details +%endif + +%post -p /sbin/ldconfig + +%postun -p /sbin/ldconfig + +%files +%manifest %{name}.manifest +%if 0%{test} +%{_bindir}/* +%endif +%{_libdir}/lib%{name}*.so* +%license LICENSE.APLv2 + +%files plugins +%manifest %{name}.manifest +%{_libdir}/lib%{name}-transport*.so* +%license LICENSE.APLv2 + +%files devel +%{_includedir}/* +%{_libdir}/pkgconfig/*.pc + +%clean +rm -rf %{buildroot} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..a8e5154 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,4 @@ +include ':android:aitt' +include ':android:flatbuffers' +include ':android:mosquitto' +include ':android:modules:webrtc' diff --git a/src/AITT.cc b/src/AITT.cc new file mode 100644 index 0000000..c50332d --- /dev/null +++ b/src/AITT.cc @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include <memory> +#include <random> + +#include "AITTImpl.h" +#include "aitt_internal.h" + +namespace aitt { + +AITT::AITT(const std::string &id, const std::string &ip_addr, bool clear_session) +{ + std::string valid_id = id; + std::string valid_ip = ip_addr; + + if (id.empty()) { + const char character_set[] = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + std::mt19937 random_gen{std::random_device{}()}; + std::uniform_int_distribution<std::string::size_type> gen(0, 61); + char name[16]; + for (size_t i = 0; i < sizeof(name); i++) { + name[i] = character_set[gen(random_gen)]; + } + valid_id = "aitt-" + std::string(name, sizeof(name) - 1); + DBG("Generated name = %s", valid_id.c_str()); + } + + if (ip_addr.empty()) + valid_ip = "127.0.0.1"; + + pImpl = std::make_unique<AITT::Impl>(*this, valid_id, valid_ip, clear_session); +} + +AITT::~AITT(void) +{ +} + +void AITT::SetWillInfo(const std::string &topic, const void *data, const size_t datalen, + AittQoS qos, bool retain) +{ + return pImpl->SetWillInfo(topic, data, datalen, qos, retain); +} + +void AITT::SetConnectionCallback(ConnectionCallback cb, void *user_data) +{ + return pImpl->SetConnectionCallback(cb, user_data); +} + +void AITT::Connect(const std::string &host, int port, const std::string &username, + const std::string &password) +{ + return pImpl->Connect(host, port, username, password); +} + +void AITT::Disconnect(void) +{ + return pImpl->Disconnect(); +} + +void AITT::Publish(const std::string &topic, const void *data, const size_t datalen, + AittProtocol protocols, AittQoS qos, bool retain) +{ + if (AITT_PAYLOAD_MAX < datalen) { + ERR("Invalid Size(%zu)", datalen); + throw std::runtime_error("Invalid Size"); + } + + return pImpl->Publish(topic, data, datalen, protocols, qos, retain); +} + +int AITT::PublishWithReply(const std::string &topic, const void *data, const size_t datalen, + AittProtocol protocol, AittQoS qos, bool retain, const SubscribeCallback &cb, void *cbdata, + const std::string &correlation) +{ + if (AITT_PAYLOAD_MAX < datalen) { + ERR("Invalid Size(%zu)", datalen); + throw std::runtime_error("Invalid Size"); + } + + return pImpl->PublishWithReply(topic, data, datalen, protocol, qos, retain, cb, cbdata, + correlation); +} + +int AITT::PublishWithReplySync(const std::string &topic, const void *data, const size_t datalen, + AittProtocol protocol, AittQoS qos, bool retain, const SubscribeCallback &cb, void *cbdata, + const std::string &correlation, int timeout_ms) +{ + if (AITT_PAYLOAD_MAX < datalen) { + ERR("Invalid Size(%zu)", datalen); + throw std::runtime_error("Invalid Size"); + } + + return pImpl->PublishWithReplySync(topic, data, datalen, protocol, qos, retain, cb, cbdata, + correlation, timeout_ms); +} + +AittSubscribeID AITT::Subscribe(const std::string &topic, const SubscribeCallback &cb, void *cbdata, + AittProtocol protocols, AittQoS qos) +{ + return pImpl->Subscribe(topic, cb, cbdata, protocols, qos); +} + +void *AITT::Unsubscribe(AittSubscribeID handle) +{ + return pImpl->Unsubscribe(handle); +} + +void AITT::SendReply(MSG *msg, const void *data, size_t datalen, bool end) +{ + if (AITT_PAYLOAD_MAX < datalen) { + ERR("Invalid Size(%zu)", datalen); + throw std::runtime_error("Invalid Size"); + } + + return pImpl->SendReply(msg, data, datalen, end); +} + +bool AITT::CompareTopic(const std::string &left, const std::string &right) +{ + return MQ::CompareTopic(left, right); +} + +} // namespace aitt diff --git a/src/AITTImpl.cc b/src/AITTImpl.cc new file mode 100644 index 0000000..b851625 --- /dev/null +++ b/src/AITTImpl.cc @@ -0,0 +1,437 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "AITTImpl.h" + +#include <flatbuffers/flexbuffers.h> + +#include <cerrno> +#include <cstring> +#include <functional> +#include <memory> +#include <stdexcept> + +#include "aitt_internal.h" + +#define WEBRTC_ROOM_ID_PREFIX std::string(AITT_MANAGED_TOPIC_PREFIX "webrtc/room/Room.webrtc") +#define WEBRTC_ID_POSTFIX std::string("_for_webrtc") + +namespace aitt { + +AITT::Impl::Impl(AITT &parent, const std::string &id, const std::string &ipAddr, bool clearSession) + : public_api(parent), + id_(id), + mq(id, clearSession), + discovery(id), + reply_id(0), + modules(ipAddr) +{ + // TODO: + // Validate ipAddr + + aittThread = std::thread(&AITT::Impl::ThreadMain, this); +} + +AITT::Impl::~Impl(void) +{ + if (false == mqtt_broker_ip_.empty()) + Disconnect(); + + while (main_loop.Quit() == false) { + // wait when called before the thread has completely created. + usleep(1000); // 1millisecond + } + + if (aittThread.joinable()) + aittThread.join(); +} + +void AITT::Impl::ThreadMain(void) +{ + pthread_setname_np(pthread_self(), "AITTWorkerLoop"); + main_loop.Run(); +} + +void AITT::Impl::SetWillInfo(const std::string &topic, const void *data, const size_t datalen, + AittQoS qos, bool retain) +{ + mq.SetWillInfo(topic, data, datalen, qos, retain); +} + +void AITT::Impl::SetConnectionCallback(ConnectionCallback cb, void *user_data) +{ + if (cb) + mq.SetConnectionCallback( + std::bind(&Impl::ConnectionCB, this, cb, user_data, std::placeholders::_1)); + else + mq.SetConnectionCallback(nullptr); +} + +void AITT::Impl::ConnectionCB(ConnectionCallback cb, void *user_data, int status) +{ + RET_IF(cb == nullptr); + + cb(public_api, status, user_data); +} + +void AITT::Impl::Connect(const std::string &host, int port, const std::string &username, + const std::string &password) +{ + modules.Init(discovery); + + discovery.Start(host, port, username, password); + mq.Connect(host, port, username, password); + + mqtt_broker_ip_ = host; + mqtt_broker_port_ = port; +} + +void AITT::Impl::Disconnect(void) +{ + UnsubscribeAll(); + + mqtt_broker_ip_.clear(); + mqtt_broker_port_ = -1; + + mq.Disconnect(); + discovery.Stop(); +} + +void AITT::Impl::UnsubscribeAll() +{ + std::unique_lock<std::mutex> lock(subscribed_list_mutex_); + + for (auto subscribe_info : subscribed_list) { + switch (subscribe_info->first) { + case AITT_TYPE_MQTT: + mq.Unsubscribe(subscribe_info->second); + break; + + case AITT_TYPE_TCP: + case AITT_TYPE_WEBRTC: + modules.GetInstance(subscribe_info->first)->Unsubscribe(subscribe_info->second); + break; + + default: + ERR("Unknown AittProtocol(%d)", subscribe_info->first); + break; + } + + delete subscribe_info; + } + subscribed_list.clear(); +} + +void AITT::Impl::ConfigureTransportModule(const std::string &key, const std::string &value, + AittProtocol protocols) +{ +} + +void AITT::Impl::Publish(const std::string &topic, const void *data, const size_t datalen, + AittProtocol protocols, AittQoS qos, bool retain) +{ + if ((protocols & AITT_TYPE_MQTT) == AITT_TYPE_MQTT) + mq.Publish(topic, data, datalen, qos, retain); + + // NOTE: + // Invoke the publish method of the specified transport module + if ((protocols & AITT_TYPE_TCP) == AITT_TYPE_TCP) { + auto tcpModule = modules.GetInstance(AITT_TYPE_TCP); + tcpModule->Publish(topic, data, datalen, qos, retain); + } + if ((protocols & AITT_TYPE_WEBRTC) == AITT_TYPE_WEBRTC) { + PublishWebRtc(topic, data, datalen, qos, retain); + } +} + +void AITT::Impl::PublishWebRtc(const std::string &topic, const void *data, const size_t datalen, + AittQoS qos, bool retain) +{ + auto webrtcModule = modules.GetInstance(AITT_TYPE_WEBRTC); + flexbuffers::Builder fbb; + fbb.Map([=, &fbb]() { + fbb.String("Id", id_ + WEBRTC_ID_POSTFIX); + fbb.String("BrokerIp", mqtt_broker_ip_); + fbb.Int("BrokerPort", mqtt_broker_port_); + fbb.String("RoomId", WEBRTC_ROOM_ID_PREFIX + topic); + fbb.String("SourceId", id_ + WEBRTC_ID_POSTFIX); + // TODO pass user data to WEBRTC module + fbb.UInt("UserDataLength", datalen); + }); + fbb.Finish(); + auto buf = fbb.GetBuffer(); + webrtcModule->Publish(topic, buf.data(), buf.size(), qos, retain); +} + +AittSubscribeID AITT::Impl::Subscribe(const std::string &topic, const AITT::SubscribeCallback &cb, + void *user_data, AittProtocol protocol, AittQoS qos) +{ + SubscribeInfo *info = new SubscribeInfo(); + info->first = protocol; + + void *subscribe_handle; + switch (protocol) { + case AITT_TYPE_MQTT: + subscribe_handle = SubscribeMQ(info, &main_loop, topic, cb, user_data, qos); + break; + case AITT_TYPE_TCP: + subscribe_handle = SubscribeTCP(info, topic, cb, user_data, qos); + break; + case AITT_TYPE_WEBRTC: + subscribe_handle = SubscribeWebRtc(info, topic, cb, user_data, qos); + break; + default: + ERR("Unknown AittProtocol(%d)", protocol); + delete info; + throw std::runtime_error("Unknown AittProtocol"); + } + info->second = subscribe_handle; + { + std::unique_lock<std::mutex> lock(subscribed_list_mutex_); + subscribed_list.push_back(info); + } + + INFO("Subscribe topic(%s) : %p", topic.c_str(), info); + return reinterpret_cast<AittSubscribeID>(info); +} + +AittSubscribeID AITT::Impl::SubscribeMQ(SubscribeInfo *handle, MainLoopHandler *loop_handle, + const std::string &topic, const SubscribeCallback &cb, void *user_data, AittQoS qos) +{ + return mq.Subscribe( + topic, + [this, handle, loop_handle, cb](MSG *msg, const std::string &topic, const void *data, + const size_t datalen, void *mq_user_data) { + void *delivery = malloc(datalen); + if (delivery) + memcpy(delivery, data, datalen); + + msg->SetID(handle); + auto idler_cb = + std::bind(&Impl::DetachedCB, this, cb, *msg, delivery, datalen, mq_user_data, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + MainLoopHandler::AddIdle(loop_handle, idler_cb, nullptr); + }, + user_data, qos); +} + +void AITT::Impl::DetachedCB(SubscribeCallback cb, MSG msg, void *data, const size_t datalen, + void *user_data, MainLoopHandler::MainLoopResult result, int fd, + MainLoopHandler::MainLoopData *loop_data) +{ + RET_IF(cb == nullptr); + + cb(&msg, data, datalen, user_data); + + free(data); +} + +void *AITT::Impl::Unsubscribe(AittSubscribeID subscribe_id) +{ + INFO("[%s] %p", __func__, subscribe_id); + SubscribeInfo *info = reinterpret_cast<SubscribeInfo *>(subscribe_id); + + std::unique_lock<std::mutex> lock(subscribed_list_mutex_); + + auto it = std::find(subscribed_list.begin(), subscribed_list.end(), info); + if (it == subscribed_list.end()) { + ERR("Unknown subscribe_id(%p)", subscribe_id); + throw std::runtime_error("subscribe_id"); + } + + void *user_data = nullptr; + SubscribeInfo *found_info = *it; + switch (found_info->first) { + case AITT_TYPE_MQTT: + user_data = mq.Unsubscribe(found_info->second); + break; + case AITT_TYPE_TCP: { + auto tcpModule = modules.GetInstance(AITT_TYPE_TCP); + user_data = tcpModule->Unsubscribe(found_info->second); + break; + } + case AITT_TYPE_WEBRTC: { + auto webrtcModule = modules.GetInstance(AITT_TYPE_WEBRTC); + user_data = webrtcModule->Unsubscribe(found_info->second); + break; + } + default: + ERR("Unknown AittProtocol(%d)", found_info->first); + break; + } + + subscribed_list.erase(it); + delete info; + + return user_data; +} + +int AITT::Impl::PublishWithReply(const std::string &topic, const void *data, const size_t datalen, + AittProtocol protocol, AittQoS qos, bool retain, const SubscribeCallback &cb, void *user_data, + const std::string &correlation) +{ + std::string replyTopic = topic + RESPONSE_POSTFIX + std::to_string(reply_id++); + + if (protocol != AITT_TYPE_MQTT) + return -1; // not yet support + + Subscribe( + replyTopic, + [this, cb](MSG *sub_msg, const void *sub_data, const size_t sub_datalen, + void *sub_cbdata) { + if (sub_msg->IsEndSequence()) { + try { + Unsubscribe(sub_msg->GetID()); + } catch (std::runtime_error &e) { + ERR("Unsubscribe() Fail(%s)", e.what()); + } + } + cb(sub_msg, sub_data, sub_datalen, sub_cbdata); + }, + user_data, protocol, qos); + + mq.PublishWithReply(topic, data, datalen, qos, false, replyTopic, correlation); + return 0; +} + +int AITT::Impl::PublishWithReplySync(const std::string &topic, const void *data, + const size_t datalen, AittProtocol protocol, AittQoS qos, bool retain, + const SubscribeCallback &cb, void *user_data, const std::string &correlation, int timeout_ms) +{ + std::string replyTopic = topic + RESPONSE_POSTFIX + std::to_string(reply_id++); + + if (protocol != AITT_TYPE_MQTT) + return -1; // not yet support + + SubscribeInfo *info = new SubscribeInfo(); + info->first = protocol; + + void *subscribe_handle; + MainLoopHandler sync_loop; + unsigned int timeout_id = 0; + bool is_timeout = false; + + subscribe_handle = SubscribeMQ( + info, &sync_loop, replyTopic, + [&](MSG *sub_msg, const void *sub_data, const size_t sub_datalen, void *sub_cbdata) { + if (sub_msg->IsEndSequence()) { + try { + Unsubscribe(sub_msg->GetID()); + } catch (std::runtime_error &e) { + ERR("Unsubscribe() Fail(%s)", e.what()); + } + sync_loop.Quit(); + } else { + if (timeout_id) { + sync_loop.RemoveTimeout(timeout_id); + HandleTimeout(timeout_ms, timeout_id, sync_loop, is_timeout); + } + } + cb(sub_msg, sub_data, sub_datalen, sub_cbdata); + }, + user_data, qos); + info->second = subscribe_handle; + { + std::unique_lock<std::mutex> lock(subscribed_list_mutex_); + subscribed_list.push_back(info); + } + + mq.PublishWithReply(topic, data, datalen, qos, false, replyTopic, correlation); + if (timeout_ms) + HandleTimeout(timeout_ms, timeout_id, sync_loop, is_timeout); + + sync_loop.Run(); + + if (is_timeout) + return AITT_ERROR_TIMED_OUT; + return 0; +} + +void AITT::Impl::HandleTimeout(int timeout_ms, unsigned int &timeout_id, + aitt::MainLoopHandler &sync_loop, bool &is_timeout) +{ + timeout_id = sync_loop.AddTimeout( + timeout_ms, + [&, timeout_ms](MainLoopHandler::MainLoopResult result, int fd, + MainLoopHandler::MainLoopData *data) { + ERR("PublishWithReplySync() timeout(%d)", timeout_ms); + sync_loop.Quit(); + is_timeout = true; + }, + nullptr); +} + +void AITT::Impl::SendReply(MSG *msg, const void *data, const int datalen, bool end) +{ + RET_IF(msg == nullptr); + + if ((msg->GetProtocols() & AITT_TYPE_MQTT) != AITT_TYPE_MQTT) + return; // not yet support + + if (end == false || msg->GetSequence()) + msg->IncreaseSequence(); + msg->SetEndSequence(end); + + mq.SendReply(msg, data, datalen, AITT_QOS_AT_MOST_ONCE, false); +} + +void *AITT::Impl::SubscribeTCP(SubscribeInfo *handle, const std::string &topic, + const SubscribeCallback &cb, void *user_data, AittQoS qos) +{ + auto tcpModule = modules.GetInstance(AITT_TYPE_TCP); + return tcpModule->Subscribe( + topic, + [handle, cb](const std::string &topic, const void *data, const size_t datalen, + void *user_data, const std::string &correlation) -> void { + MSG msg; + msg.SetID(handle); + msg.SetTopic(topic); + msg.SetCorrelation(correlation); + msg.SetProtocols(AITT_TYPE_TCP); + + return cb(&msg, data, datalen, user_data); + }, + user_data, qos); +} + +void *AITT::Impl::SubscribeWebRtc(SubscribeInfo *handle, const std::string &topic, + const SubscribeCallback &cb, void *user_data, AittQoS qos) +{ + auto webrtc_module = modules.GetInstance(AITT_TYPE_WEBRTC); + flexbuffers::Builder fbb; + fbb.Map([=, &fbb]() { + fbb.String("Id", id_ + WEBRTC_ID_POSTFIX); + fbb.String("BrokerIp", mqtt_broker_ip_); + fbb.String("RoomId", WEBRTC_ROOM_ID_PREFIX + topic); + fbb.Int("BrokerPort", mqtt_broker_port_); + }); + fbb.Finish(); + auto buf = fbb.GetBuffer(); + + return webrtc_module->Subscribe( + topic, + [handle, cb](const std::string &topic, const void *data, const size_t datalen, + void *user_data, const std::string &correlation) -> void { + MSG msg; + msg.SetID(handle); + msg.SetTopic(topic); + msg.SetCorrelation(correlation); + msg.SetProtocols(AITT_TYPE_WEBRTC); + + return cb(&msg, data, datalen, user_data); + }, + buf.data(), buf.size(), user_data, qos); +} +} // namespace aitt diff --git a/src/AITTImpl.h b/src/AITTImpl.h new file mode 100644 index 0000000..08bec4a --- /dev/null +++ b/src/AITTImpl.h @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <flatbuffers/flexbuffers.h> + +#include <map> +#include <memory> +#include <mutex> +#include <string> +#include <thread> +#include <utility> + +#include "AITT.h" +#include "AittDiscovery.h" +#include "MQ.h" +#include "MainLoopHandler.h" +#include "TransportModuleLoader.h" + +namespace aitt { + +class AITT::Impl { + public: + Impl(AITT &parent, const std::string &id, const std::string &ipAddr, bool clearSession); + virtual ~Impl(void); + + void SetWillInfo(const std::string &topic, const void *data, const size_t datalen, AittQoS qos, + bool retain); + void SetConnectionCallback(ConnectionCallback cb, void *user_data); + void Connect(const std::string &host, int port, const std::string &username, + const std::string &password); + void Disconnect(void); + + void ConfigureTransportModule(const std::string &key, const std::string &value, + AittProtocol protocols); + + void Publish(const std::string &topic, const void *data, const size_t datalen, + AittProtocol protocols, AittQoS qos, bool retain); + + int PublishWithReply(const std::string &topic, const void *data, const size_t datalen, + AittProtocol protocol, AittQoS qos, bool retain, const AITT::SubscribeCallback &cb, + void *cbdata, const std::string &correlation); + + int PublishWithReplySync(const std::string &topic, const void *data, const size_t datalen, + AittProtocol protocol, AittQoS qos, bool retain, const SubscribeCallback &cb, + void *cbdata, const std::string &correlation, int timeout_ms); + + AittSubscribeID Subscribe(const std::string &topic, const AITT::SubscribeCallback &cb, + void *cbdata, AittProtocol protocols, AittQoS qos); + + void *Unsubscribe(AittSubscribeID handle); + + void SendReply(MSG *msg, const void *data, const int datalen, bool end); + + private: + using Blob = std::pair<const void *, int>; + using SubscribeInfo = std::pair<AittProtocol, void *>; + + void ConnectionCB(ConnectionCallback cb, void *user_data, int status); + AittSubscribeID SubscribeMQ(SubscribeInfo *info, MainLoopHandler *loop_handle, + const std::string &topic, const SubscribeCallback &cb, void *cbdata, AittQoS qos); + void DetachedCB(SubscribeCallback cb, MSG mq_msg, void *data, const size_t datalen, + void *cbdata, MainLoopHandler::MainLoopResult result, int fd, + MainLoopHandler::MainLoopData *loop_data); + void *SubscribeTCP(SubscribeInfo *, const std::string &topic, const SubscribeCallback &cb, + void *cbdata, AittQoS qos); + void *SubscribeWebRtc(SubscribeInfo *, const std::string &topic, const SubscribeCallback &cb, + void *cbdata, AittQoS qos); + void HandleTimeout(int timeout_ms, unsigned int &timeout_id, aitt::MainLoopHandler &sync_loop, + bool &is_timeout); + void PublishWebRtc(const std::string &topic, const void *data, const size_t datalen, + AittQoS qos, bool retain); + void UnsubscribeAll(); + + AITT &public_api; + std::string id_; + std::string mqtt_broker_ip_; + int mqtt_broker_port_; + MQ mq; + AittDiscovery discovery; + unsigned short reply_id; + TransportModuleLoader modules; + MainLoopHandler main_loop; + void ThreadMain(void); + std::thread aittThread; + std::vector<SubscribeInfo *> subscribed_list; + std::mutex subscribed_list_mutex_; +}; + +} // namespace aitt diff --git a/src/TransportModuleLoader.cc b/src/TransportModuleLoader.cc new file mode 100644 index 0000000..a953ac9 --- /dev/null +++ b/src/TransportModuleLoader.cc @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "TransportModuleLoader.h" + +#include <dlfcn.h> + +#include "AITTEx.h" +#include "aitt_internal.h" + +namespace aitt { + +TransportModuleLoader::TransportModuleLoader(const std::string &ip) : ip(ip) +{ +} + +std::string TransportModuleLoader::GetModuleFilename(AittProtocol protocol) +{ + // TODO: + // We are able to generate the module name by a particular syntax, + // It could be introduced later when we have several modules. + if (protocol == AITT_TYPE_TCP) + return "libaitt-transport-tcp.so"; + if (protocol == AITT_TYPE_WEBRTC) + return "libaitt-transport-webrtc.so"; + + return std::string(); +} + +int TransportModuleLoader::LoadModule(AittProtocol protocol, AittDiscovery &discovery) +{ + std::string filename = GetModuleFilename(protocol); + + Handler handle(dlopen(filename.c_str(), RTLD_LAZY | RTLD_LOCAL), + [](const void *handle) -> void { + if (dlclose(const_cast<void *>(handle))) + ERR("dlclose: %s", dlerror()); + }); + if (handle == nullptr) { + ERR("dlopen: %s", dlerror()); + return -1; + } + + AittTransport::ModuleEntry get_instance_fn = reinterpret_cast<AittTransport::ModuleEntry>( + dlsym(handle.get(), AittTransport::MODULE_ENTRY_NAME)); + if (get_instance_fn == nullptr) { + ERR("dlsym: %s", dlerror()); + return -1; + } + + std::shared_ptr<AittTransport> instance( + static_cast<AittTransport *>(get_instance_fn(ip.c_str(), discovery)), + [](const AittTransport *instance) -> void { delete instance; }); + if (instance == nullptr) { + ERR("Failed to create a new instance"); + return -1; + } + + module_table.emplace(protocol, std::make_pair(std::move(handle), instance)); + + return 0; +} + +void TransportModuleLoader::Init(AittDiscovery &discovery) +{ + std::lock_guard<std::mutex> lock_from_here(module_lock); + if (LoadModule(AITT_TYPE_TCP, discovery) < 0) { + ERR("LoadModule(AITT_TYPE_TCP) Fail"); + } + +#ifdef WITH_WEBRTC + if (LoadModule(AITT_TYPE_WEBRTC, discovery) < 0) { + ERR("LoadModule(AITT_TYPE_WEBRTC) Fail"); + } +#endif // WITH_WEBRTC +} + +std::shared_ptr<AittTransport> TransportModuleLoader::GetInstance(AittProtocol protocol) +{ + std::lock_guard<std::mutex> lock_from_here(module_lock); + + auto item = module_table.find(protocol); + if (item == module_table.end()) { + ERR("Not Initialized"); + // throw AITTEx(AITTEx::NO_DATA, "Not Initialized"); + return nullptr; + } + + return item->second.second; +} + +} // namespace aitt diff --git a/src/TransportModuleLoader.h b/src/TransportModuleLoader.h new file mode 100644 index 0000000..576c97e --- /dev/null +++ b/src/TransportModuleLoader.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <AITT.h> +#include <AittTransport.h> + +#include <map> +#include <memory> +#include <mutex> +#include <string> + +#include "TransportModuleLoader.h" + +namespace aitt { + +class TransportModuleLoader { + public: + explicit TransportModuleLoader(const std::string &ip); + virtual ~TransportModuleLoader() = default; + + void Init(AittDiscovery &discovery); + std::shared_ptr<AittTransport> GetInstance(AittProtocol protocol); + + private: + using Handler = std::unique_ptr<void, void (*)(const void *)>; + using ModuleMap = std::map<AittProtocol, std::pair<Handler, std::shared_ptr<AittTransport>>>; + + std::string GetModuleFilename(AittProtocol protocol); + int LoadModule(AittProtocol protocol, AittDiscovery &discovery); + + ModuleMap module_table; + std::mutex module_lock; + std::string ip; +}; + +} // namespace aitt diff --git a/src/aitt_c.cc b/src/aitt_c.cc new file mode 100644 index 0000000..1135353 --- /dev/null +++ b/src/aitt_c.cc @@ -0,0 +1,282 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "aitt_c.h" + +#include <arpa/inet.h> +#include <stdlib.h> + +#include "AITT.h" +#include "aitt_internal.h" + +using namespace aitt; + +struct aitt_handle { + aitt_handle() : aitt(nullptr) {} + AITT *aitt; + bool connected; +}; + +API aitt_h aitt_new(const char *id, const char *my_ip) +{ + aitt_h handle = nullptr; + try { + std::string valid_id; + std::string valid_ip; + + if (id) + valid_id = id; + + if (my_ip) + valid_ip = my_ip; + + DBG("id(%s), ip(%s)", valid_id.c_str(), valid_ip.c_str()); + + handle = new aitt_handle(); + handle->aitt = new AITT(valid_id, valid_ip, true); + handle->connected = false; + } catch (std::exception &e) { + ERR("new() Fail(%s)", e.what()); + return nullptr; + } + + return handle; +} + +API int aitt_set_option(aitt_h handle, aitt_option_e option, const char *value) +{ + RETV_IF(handle == nullptr, AITT_ERROR_INVALID_PARAMETER); + + switch (option) { + case AITT_OPT_UNKNOWN: + try { + // something to do + } catch (std::exception &e) { + ERR("string() Fail(%s)", e.what()); + return AITT_ERROR_SYSTEM; + } + break; + default: + ERR("Unknown option(%d)", option); + return AITT_ERROR_INVALID_PARAMETER; + } + + return AITT_ERROR_NONE; +} + +API const char *aitt_get_option(aitt_h handle, aitt_option_e option) +{ + RETV_IF(handle == nullptr, nullptr); + + switch (option) { + case AITT_OPT_UNKNOWN: + return "Unknown"; + default: + ERR("Unknown option(%d)", option); + } + + return nullptr; +} + +API int aitt_will_set(aitt_h handle, const char *topic, const void *msg, const size_t msg_len, + aitt_qos_e qos, bool retain) +{ + RETV_IF(handle == nullptr, AITT_ERROR_INVALID_PARAMETER); + + try { + handle->aitt->SetWillInfo(topic, msg, msg_len, qos, retain); + } catch (std::exception &e) { + ERR("SetWillInfo(%s, %zu) Fail(%s)", topic, msg_len, e.what()); + return AITT_ERROR_SYSTEM; + } + return AITT_ERROR_NONE; +} + +API void aitt_destroy(aitt_h handle) +{ + if (handle == nullptr) { + ERR("handle is NULL"); + return; + } + + try { + delete handle->aitt; + delete handle; + } catch (std::exception &e) { + ERR("delete() Fail(%s)", e.what()); + } +} + +static bool is_valid_ip(const char *ip) +{ + RETV_IF(ip == nullptr, false); + + struct sockaddr_in sa; + if (inet_pton(AF_INET, ip, &(sa.sin_addr)) <= 0) + return false; + + return true; +} + +API int aitt_connect(aitt_h handle, const char *broker_ip, int port) +{ + return aitt_connect_full(handle, broker_ip, port, NULL, NULL); +} + +API int aitt_connect_full(aitt_h handle, const char *broker_ip, int port, const char *username, + const char *password) +{ + RETV_IF(handle == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETVM_IF(is_valid_ip(broker_ip) == false, AITT_ERROR_INVALID_PARAMETER, "Invalid IP(%s)", + broker_ip); + + try { + handle->aitt->Connect(broker_ip, port, username ? username : std::string(), + password ? password : std::string()); + } catch (std::exception &e) { + ERR("Connect(%s, %d) Fail(%s)", broker_ip, port, e.what()); + return AITT_ERROR_SYSTEM; + } + + handle->connected = true; + return AITT_ERROR_NONE; +} + +API int aitt_disconnect(aitt_h handle) +{ + RETV_IF(handle == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(handle->aitt == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(handle->connected == false, AITT_ERROR_NOT_READY); + + try { + handle->aitt->Disconnect(); + } catch (std::exception &e) { + ERR("Disconnect() Fail(%s)", e.what()); + return AITT_ERROR_SYSTEM; + } + return AITT_ERROR_NONE; +} + +API int aitt_publish(aitt_h handle, const char *topic, const void *msg, const size_t msg_len) +{ + return aitt_publish_full(handle, topic, msg, msg_len, AITT_TYPE_MQTT, AITT_QOS_AT_MOST_ONCE); +} + +API int aitt_publish_full(aitt_h handle, const char *topic, const void *msg, const size_t msg_len, + int protocols, aitt_qos_e qos) +{ + RETV_IF(handle == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(handle->aitt == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(handle->connected == false, AITT_ERROR_NOT_READY); + RETV_IF(topic == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(msg == nullptr, AITT_ERROR_INVALID_PARAMETER); + + try { + handle->aitt->Publish(topic, msg, msg_len, AITT_TYPE_MQTT); + } catch (std::exception &e) { + ERR("Publish(topic:%s, msg_len:%zu) Fail(%s)", topic, msg_len, e.what()); + return AITT_ERROR_SYSTEM; + } + + return AITT_ERROR_NONE; +} + +API int aitt_publish_with_reply(aitt_h handle, const char *topic, const void *msg, + const size_t msg_len, aitt_protocol_e protocols, aitt_qos_e qos, const char *correlation, + aitt_sub_fn cb, void *user_data) +{ + RETV_IF(handle == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(handle->aitt == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(handle->connected == false, AITT_ERROR_NOT_READY); + RETV_IF(topic == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(msg == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(cb == nullptr, AITT_ERROR_INVALID_PARAMETER); + + try { + // TODO: handle protocols, qos + handle->aitt->PublishWithReply(topic, msg, msg_len, protocols, AITT_QOS_AT_MOST_ONCE, false, + cb, user_data, std::string(correlation)); + } catch (std::exception &e) { + ERR("PublishWithReply(%s) Fail(%s)", topic, e.what()); + return AITT_ERROR_SYSTEM; + } + return AITT_ERROR_NONE; +} + +API int aitt_send_reply(aitt_h handle, aitt_msg_h msg_handle, const void *reply, + const size_t reply_len, bool end) +{ + try { + aitt::MSG *msg = static_cast<aitt::MSG *>(msg_handle); + + handle->aitt->SendReply(msg, reply, reply_len, end); + } catch (std::exception &e) { + ERR("SendReply Fail(%s)", e.what()); + return AITT_ERROR_SYSTEM; + } + return AITT_ERROR_NONE; +} + +API int aitt_subscribe(aitt_h handle, const char *topic, aitt_sub_fn cb, void *user_data, + aitt_sub_h *sub_handle) +{ + return aitt_subscribe_full(handle, topic, cb, user_data, AITT_TYPE_MQTT, AITT_QOS_AT_MOST_ONCE, + sub_handle); +} + +API int aitt_subscribe_full(aitt_h handle, const char *topic, aitt_sub_fn cb, void *user_data, + aitt_protocol_e protocol, aitt_qos_e qos, aitt_sub_h *sub_handle) +{ + RETV_IF(handle == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(handle->aitt == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(handle->connected == false, AITT_ERROR_NOT_READY); + RETV_IF(topic == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(cb == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(sub_handle == nullptr, AITT_ERROR_INVALID_PARAMETER); + + try { + // TODO: handle protocols, qos + *sub_handle = + handle->aitt->Subscribe(topic, cb, user_data, static_cast<AittProtocol>(protocol)); + } catch (std::exception &e) { + ERR("Subscribe(%s) Fail(%s)", topic, e.what()); + return AITT_ERROR_SYSTEM; + } + return AITT_ERROR_NONE; +} + +API int aitt_unsubscribe(aitt_h handle, aitt_sub_h sub_handle) +{ + RETV_IF(handle == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(handle->aitt == nullptr, AITT_ERROR_INVALID_PARAMETER); + RETV_IF(handle->connected == false, AITT_ERROR_NOT_READY); + RETV_IF(sub_handle == nullptr, AITT_ERROR_INVALID_PARAMETER); + + try { + handle->aitt->Unsubscribe(static_cast<AittSubscribeID>(sub_handle)); + } catch (std::exception &e) { + ERR("Unsubscribe(%p) Fail(%s)", sub_handle, e.what()); + return AITT_ERROR_SYSTEM; + } + return AITT_ERROR_NONE; +} + +API const char *aitt_msg_get_topic(aitt_msg_h handle) +{ + RETV_IF(handle == nullptr, nullptr); + + MSG *msg = reinterpret_cast<MSG *>(handle); + return msg->GetTopic().c_str(); +} diff --git a/tests/AITT_TCP_test.cc b/tests/AITT_TCP_test.cc new file mode 100644 index 0000000..bd28993 --- /dev/null +++ b/tests/AITT_TCP_test.cc @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include <glib.h> +#include <gtest/gtest.h> + +#include <thread> + +#include "AITT.h" +#include "aitt_internal.h" +#include "aitt_tests.h" + +using AITT = aitt::AITT; + +class AITTTCPTest : public testing::Test, public AittTests { + protected: + void SetUp() override { Init(); } + void TearDown() override { Deinit(); } +}; + +TEST_F(AITTTCPTest, TCP_Wildcards1_Anytime) +{ + try { + char dump_msg[204800]; + + AITT aitt(clientId, LOCAL_IP); + aitt.Connect(); + + aitt.Subscribe( + "test/#", + [&](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void { + AITTTCPTest *test = static_cast<AITTTCPTest *>(cbdata); + INFO("Got Message(Topic:%s, size:%zu)", handle->GetTopic().c_str(), szmsg); + static int cnt = 0; + ++cnt; + if (cnt == 3) + test->ToggleReady(); + }, + static_cast<void *>(this), AITT_TYPE_TCP); + + // Wait a few seconds until the AITT client gets a server list (discover devices) + DBG("Sleep %d secs", SLEEP_MS); + sleep(SLEEP_MS); + + aitt.Publish("test/step1/value1", dump_msg, 12, AITT_TYPE_TCP); + aitt.Publish("test/step2/value1", dump_msg, 1600, AITT_TYPE_TCP); + aitt.Publish("test/step2/value1", dump_msg, 1600, AITT_TYPE_TCP); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + + ASSERT_TRUE(ready); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTCPTest, TCP_Wildcards2_Anytime) +{ + try { + char dump_msg[204800]; + + AITT aitt(clientId, LOCAL_IP); + aitt.Connect(); + + aitt.Subscribe( + "test/+", + [&](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void { + AITTTCPTest *test = static_cast<AITTTCPTest *>(cbdata); + INFO("Got Message(Topic:%s, size:%zu)", handle->GetTopic().c_str(), szmsg); + static int cnt = 0; + ++cnt; + + std::stringstream ss; + ss << "test/value" << cnt; + EXPECT_EQ(ss.str(), handle->GetTopic()); + + if (cnt == 3) + test->ToggleReady(); + }, + static_cast<void *>(this), AITT_TYPE_TCP); + + // Wait a few seconds until the AITT client gets a server list (discover devices) + DBG("Sleep %d secs", SLEEP_MS); + sleep(SLEEP_MS); + + aitt.Publish("test/value1", dump_msg, 12, AITT_TYPE_TCP); + aitt.Publish("test/value2", dump_msg, 1600, AITT_TYPE_TCP); + aitt.Publish("test/value3", dump_msg, 1600, AITT_TYPE_TCP); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + + ASSERT_TRUE(ready); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} diff --git a/tests/AITT_manualtest.cc b/tests/AITT_manualtest.cc new file mode 100644 index 0000000..cea0fcf --- /dev/null +++ b/tests/AITT_manualtest.cc @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "AITT.h" + +#include <glib.h> +#include <gtest/gtest.h> + +#include "aitt_internal.h" +#include "aitt_tests.h" + +using AITT = aitt::AITT; + +class AITTManualTest : public testing::Test, public AittTests { + protected: + void SetUp() override { Init(); } + void TearDown() override { Deinit(); } +}; + +TEST_F(AITTManualTest, WillSet_P) +{ + try { + AITT aitt("", LOCAL_IP, true); + aitt.Connect(); + aitt.Subscribe( + "test/AITT_will", + [](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void { + AITTManualTest *test = static_cast<AITTManualTest *>(cbdata); + test->ToggleReady(); + DBG("Subscribe invoked: %s %zu", static_cast<const char *>(msg), szmsg); + }, + static_cast<void *>(this)); + + int pid = fork(); + if (pid == 0) { + AITT aitt_will("test_will_AITT", LOCAL_IP, true); + aitt_will.SetWillInfo("test/AITT_will", TEST_MSG, sizeof(TEST_MSG), + AITT_QOS_AT_LEAST_ONCE, false); + aitt_will.Connect(); + sleep(2); + // Do not call aitt_will.Disconnect() + } else { + sleep(1); + kill(pid, SIGKILL); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + IterateEventLoop(); + + ASSERT_TRUE(ready); + } + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST(AITT_MANUAL, Connect_with_ID_P) +{ + try { + AITT aitt("", LOCAL_IP); + aitt.Connect(LOCAL_IP, 1883, "testID", "testPasswd"); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} diff --git a/tests/AITT_test.cc b/tests/AITT_test.cc new file mode 100644 index 0000000..1dcd8c6 --- /dev/null +++ b/tests/AITT_test.cc @@ -0,0 +1,556 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "AITT.h" + +#include <glib.h> +#include <gtest/gtest.h> +#include <sys/random.h> + +#include <thread> + +#include "aitt_internal.h" +#include "aitt_tests.h" + +using AITT = aitt::AITT; + +class AITTTest : public testing::Test, public AittTests { + protected: + void SetUp() override { Init(); } + void TearDown() override { Deinit(); } + + void pubsub_template(const char *test_msg, AittProtocol protocol) + { + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + aitt.Subscribe( + testTopic, + [](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void { + AITTTest *test = static_cast<AITTTest *>(cbdata); + test->ToggleReady(); + DBG("Subscribe invoked: %s %zu", static_cast<const char *>(msg), szmsg); + }, + static_cast<void *>(this), protocol); + + // Wait a few seconds until the AITT client gets a server list (discover devices) + DBG("Sleep %d secs", SLEEP_MS); + sleep(SLEEP_MS); + + DBG("Publish(%s) : %s(%zu)", testTopic.c_str(), test_msg, strlen(test_msg)); + aitt.Publish(testTopic, test_msg, strlen(test_msg), protocol); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + + ASSERT_TRUE(ready); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } + } +}; + +TEST_F(AITTTest, Positive_Create_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, SetConnectionCallback_P_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.SetConnectionCallback( + [&](AITT &handle, int status, void *user_data) { + AITTTest *test = static_cast<AITTTest *>(user_data); + + if (test->ready2) { + EXPECT_EQ(status, AITT_DISCONNECTED); + test->ToggleReady(); + } else { + EXPECT_EQ(status, AITT_CONNECTED); + test->ToggleReady2(); + handle.Disconnect(); + } + }, + this); + aitt.Connect(); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + ASSERT_TRUE(ready); + ASSERT_TRUE(ready2); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, UnsetConnectionCallback_P_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.SetConnectionCallback( + [&](AITT &handle, int status, void *user_data) { + AITTTest *test = static_cast<AITTTest *>(user_data); + + if (test->ready) { + FAIL() << "Should not be called"; + } else { + EXPECT_EQ(status, AITT_CONNECTED); + test->ToggleReady(); + handle.SetConnectionCallback(nullptr, nullptr); + handle.Disconnect(); + } + }, + this); + aitt.Connect(); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + sleep(1); + ASSERT_FALSE(ready2); + ASSERT_TRUE(ready); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positive_Connect_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positive_Disconnect_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + aitt.Disconnect(); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positive_Connect_twice_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + aitt.Disconnect(); + aitt.Connect(); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positive_Publish_MQTT_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + aitt.Publish(testTopic, TEST_MSG, sizeof(TEST_MSG)); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positive_Publish_TCP_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + aitt.Publish(testTopic, TEST_MSG, sizeof(TEST_MSG), AITT_TYPE_TCP); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positive_Publish_Multiple_Protocols_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + aitt.Publish(testTopic, TEST_MSG, sizeof(TEST_MSG), + (AittProtocol)(AITT_TYPE_MQTT | AITT_TYPE_TCP)); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positive_Subscribe_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + aitt.Subscribe( + testTopic, + [](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void {}, + nullptr, AITT_TYPE_TCP); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positive_Unsubscribe_MQTT_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + subscribeHandle = aitt.Subscribe( + testTopic, + [](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void {}, + nullptr, AITT_TYPE_MQTT); + DBG(">>> Handle: %p", reinterpret_cast<void *>(subscribeHandle)); + aitt.Unsubscribe(subscribeHandle); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positive_Unsubscribe_TCP_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + subscribeHandle = aitt.Subscribe( + testTopic, + [](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void {}, + nullptr, AITT_TYPE_TCP); + DBG("Subscribe handle: %p", reinterpret_cast<void *>(subscribeHandle)); + aitt.Unsubscribe(subscribeHandle); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positve_PublishSubscribe_MQTT_Anytime) +{ + pubsub_template(TEST_MSG, AITT_TYPE_MQTT); +} + +TEST_F(AITTTest, Positve_Publish_0_MQTT_Anytime) +{ + pubsub_template("", AITT_TYPE_MQTT); +} + +TEST_F(AITTTest, Positve_Unsubscribe_in_Subscribe_MQTT_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + subscribeHandle = aitt.Subscribe( + testTopic, + [&](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void { + AITTTest *test = static_cast<AITTTest *>(cbdata); + DBG("Subscribe invoked: %s %zu", static_cast<const char *>(msg), szmsg); + + static int cnt = 0; + ++cnt; + if (cnt == 2) + FAIL() << "Should not be called"; + + aitt.Unsubscribe(test->subscribeHandle); + DBG("Ready flag is toggled"); + test->ToggleReady(); + }, + static_cast<void *>(this)); + + DBG("Publish message to %s (%s)", testTopic.c_str(), TEST_MSG); + aitt.Publish(testTopic, TEST_MSG, sizeof(TEST_MSG)); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + + ASSERT_TRUE(ready); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positve_Subscribe_in_Subscribe_MQTT_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + subscribeHandle = aitt.Subscribe( + testTopic, + [&](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void { + DBG("Subscribe invoked: %s %zu", static_cast<const char *>(msg), szmsg); + + static int cnt = 0; + ++cnt; + if (cnt == 2) + FAIL() << "Should not be called"; + + aitt.Subscribe( + "topic1InCallback", + [](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) {}, + cbdata); + + aitt.Subscribe( + "topic2InCallback", + [](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) {}, + cbdata); + DBG("Ready flag is toggled"); + g_timeout_add( + 100, + [](gpointer data) -> gboolean { + AITTTest *test = static_cast<AITTTest *>(data); + test->ToggleReady(); + return G_SOURCE_REMOVE; + }, + cbdata); + }, + static_cast<void *>(this)); + + DBG("Publish message to %s (%s)", testTopic.c_str(), TEST_MSG); + aitt.Publish(testTopic, TEST_MSG, sizeof(TEST_MSG)); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + + ASSERT_TRUE(ready); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positve_PublishSubscribe_TCP_Anytime) +{ + pubsub_template(TEST_MSG, AITT_TYPE_TCP); +} + +TEST_F(AITTTest, Positve_Publish_0_TCP_Anytime) +{ + pubsub_template("", AITT_TYPE_TCP); +} + +TEST_F(AITTTest, Positve_PublishSubscribe_Multiple_Protocols_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + aitt.Subscribe( + testTopic, + [&](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void { + AITTTest *test = static_cast<AITTTest *>(cbdata); + DBG("Subscribe invoked: %s %zu", static_cast<const char *>(msg), szmsg); + test->ToggleReady(); + }, + static_cast<void *>(this), AITT_TYPE_TCP); + + aitt.Subscribe( + testTopic, + [&](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void { + AITTTest *test = static_cast<AITTTest *>(cbdata); + DBG("Subscribe invoked: %s %zu", static_cast<const char *>(msg), szmsg); + test->ToggleReady2(); + }, + static_cast<void *>(this), AITT_TYPE_MQTT); + + // Wait a few seconds to the AITT client gets server list (discover devices) + DBG("Sleep %d secs", SLEEP_MS); + sleep(SLEEP_MS); + + DBG("Publish message to %s (%s) / %zu", testTopic.c_str(), TEST_MSG, sizeof(TEST_MSG)); + aitt.Publish(testTopic, TEST_MSG, sizeof(TEST_MSG), + (AittProtocol)(AITT_TYPE_MQTT | AITT_TYPE_TCP)); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + + ASSERT_TRUE(ready); + ASSERT_TRUE(ready2); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positve_PublishSubscribe_twice_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP); + aitt.Connect(); + aitt.Subscribe( + testTopic, + [&](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void { + AITTTest *test = static_cast<AITTTest *>(cbdata); + // NOTE: + // Subscribe callback will be invoked 2 times + static int cnt = 0; + ++cnt; + if (cnt == 2) + test->ToggleReady(); + DBG("Subscribe callback called: %d", cnt); + }, + static_cast<void *>(this), AITT_TYPE_TCP); + + // Wait a few seconds to the AITT client gets server list (discover devices) + sleep(SLEEP_MS); + + // NOTE: + // Select target peers and send the data through the specified protocol - TCP + aitt.Publish(testTopic, TEST_MSG, sizeof(TEST_MSG), AITT_TYPE_TCP); + + // NOTE: + // Publish message through the specified protocol - TCP + aitt.Publish(testTopic, TEST_MSG2, sizeof(TEST_MSG2), AITT_TYPE_TCP); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + + ASSERT_TRUE(ready); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, Positive_Subscribe_Retained_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP); + aitt.Connect(); + aitt.Subscribe( + testTopic, + [&](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void { + AITTTest *test = static_cast<AITTTest *>(cbdata); + static int cnt = 0; + ++cnt; + if (cnt == 1) + test->ToggleReady(); + DBG("Subscribe callback called: %d", cnt); + }, + static_cast<void *>(this), AITT_TYPE_TCP); + + // Wait a few seconds to the AITT client gets server list (discover devices) + sleep(SLEEP_MS); + + // NOTE: + // Publish a message with the retained flag + // This message will not be delivered, subscriber subscribes TCP protocol + aitt.Publish(testTopic, TEST_MSG, sizeof(TEST_MSG), AITT_TYPE_MQTT, AITT_QOS_AT_MOST_ONCE, + true); + + aitt.Publish(testTopic, TEST_MSG2, sizeof(TEST_MSG2), AITT_TYPE_TCP); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + + aitt.Publish(testTopic, nullptr, 0, AITT_TYPE_MQTT, AITT_QOS_AT_LEAST_ONCE, true); + + ASSERT_TRUE(ready); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, TCP_Publish_Disconnect_Anytime) +{ + try { + char dump_msg[204800] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + int dump_msg_size = getrandom(dump_msg, sizeof(dump_msg), 0); + if (-1 == dump_msg_size) { + ERR("getrandom() Fail(%d)", errno); + dump_msg_size = 62; + } + + AITT aitt(clientId, LOCAL_IP); + AITT aitt_retry("retry_test", LOCAL_IP); + aitt.Connect(); + aitt_retry.Connect(); + + aitt.Subscribe( + "test/stress1", + [&](aitt::MSG *handle, const void *msg, const size_t szmsg, void *cbdata) -> void { + AITTTest *test = static_cast<AITTTest *>(cbdata); + static int cnt = 0; + ++cnt; + if (szmsg == 0 && cnt != 12) { + FAIL() << "Unexpected value" << cnt; + } + if (cnt == 10) + test->ToggleReady(); + if (cnt == 11) + test->ToggleReady(); + }, + static_cast<void *>(this), AITT_TYPE_TCP); + + { + AITT aitt1("stress_test1", LOCAL_IP); + aitt1.Connect(); + + // Wait a few seconds to the AITT client gets server list (discover devices) + sleep(SLEEP_MS); + + for (int i = 0; i < 10; i++) { + INFO("size = %d", dump_msg_size); + aitt1.Publish("test/stress1", dump_msg, dump_msg_size, AITT_TYPE_TCP, + AITT_QOS_AT_MOST_ONCE, true); + } + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + } + DBG("aitt1 client Finish"); + + // Here, It's automatically checked Unexpected callback(szmsg = 0) + // when publisher is disconnected. + + ASSERT_TRUE(ready); + ready = false; + + aitt_retry.Publish("test/stress1", dump_msg, dump_msg_size, AITT_TYPE_TCP, + AITT_QOS_AT_MOST_ONCE, true); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + + ASSERT_TRUE(ready); + + aitt_retry.Publish("test/stress1", nullptr, 0, AITT_TYPE_TCP, AITT_QOS_AT_LEAST_ONCE); + // Check auto release of aitt. It sould be no Segmentation fault + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(AITTTest, WillSet_N_Anytime) +{ + EXPECT_THROW( + { + AITT aitt_will("", LOCAL_IP, true); + aitt_will.SetWillInfo("+", "will msg", 8, AITT_QOS_AT_MOST_ONCE, false); + aitt_will.Connect(); + aitt_will.Disconnect(); + }, + std::exception); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..307765d --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,62 @@ +SET(AITT_UT ${PROJECT_NAME}_ut) + +ADD_DEFINITIONS(-DLOG_STDOUT) + +PKG_CHECK_MODULES(UT_NEEDS REQUIRED gmock_main) +INCLUDE_DIRECTORIES(${UT_NEEDS_INCLUDE_DIRS}) +LINK_DIRECTORIES(${UT_NEEDS_LIBRARY_DIRS}) + +########################################################################### +SET(AITT_UT_SRC AITT_test.cc RequestResponse_test.cc MainLoopHandler_test.cc aitt_c_test.cc AITT_TCP_test.cc MQ_test.cc) +ADD_EXECUTABLE(${AITT_UT} ${AITT_UT_SRC}) +TARGET_LINK_LIBRARIES(${AITT_UT} Threads::Threads ${UT_NEEDS_LIBRARIES} ${PROJECT_NAME}) + +INSTALL(TARGETS ${AITT_UT} DESTINATION ${AITT_TEST_BINDIR}) + +ADD_TEST( + NAME + ${AITT_UT} + COMMAND + ${CMAKE_COMMAND} -E env + LD_LIBRARY_PATH=../modules/tcp/:../:../common/:$ENV{LD_LIBRARY_PATH} + ${CMAKE_CURRENT_BINARY_DIR}/${AITT_UT} --gtest_filter=*_Anytime +) + +########################################################################### +FILE(GLOB AITT_MANUAL_SRC *_manualtest.cc) +ADD_EXECUTABLE(${AITT_UT}_manual ${AITT_MANUAL_SRC}) +TARGET_LINK_LIBRARIES(${AITT_UT}_manual Threads::Threads ${UT_NEEDS_LIBRARIES} ${PROJECT_NAME}) + +INSTALL(TARGETS ${AITT_UT}_manual DESTINATION ${AITT_TEST_BINDIR}) + +########################################################################### +AUX_SOURCE_DIRECTORY(../mock MOCK_SRC) +ADD_EXECUTABLE(${AITT_UT}_mq MQ_mocktest.cc ${MOCK_SRC}) +TARGET_LINK_LIBRARIES(${AITT_UT}_mq ${UT_NEEDS_LIBRARIES} Threads::Threads ${AITT_NEEDS_LIBRARIES} ${AITT_COMMON}) +TARGET_INCLUDE_DIRECTORIES(${AITT_UT}_mq PRIVATE ../src ../mock) +INSTALL(TARGETS ${AITT_UT}_mq DESTINATION ${AITT_TEST_BINDIR}) + +ADD_TEST( + NAME + ${AITT_UT}_mq + COMMAND + ${CMAKE_COMMAND} -E env + LD_LIBRARY_PATH=../common/:$ENV{LD_LIBRARY_PATH} + ${CMAKE_CURRENT_BINARY_DIR}/${AITT_UT}_mq --gtest_filter=*_Anytime +) + +########################################################################### +ADD_EXECUTABLE(${AITT_UT}_module TransportModuleLoader_test.cc $<TARGET_OBJECTS:M_LOADER_OBJ>) +TARGET_LINK_LIBRARIES(${AITT_UT}_module ${UT_NEEDS_LIBRARIES} ${AITT_NEEDS_LIBRARIES} ${CMAKE_DL_LIBS} ${AITT_COMMON}) +TARGET_INCLUDE_DIRECTORIES(${AITT_UT}_module PRIVATE ../src) + +INSTALL(TARGETS ${AITT_UT}_module DESTINATION ${AITT_TEST_BINDIR}) + +ADD_TEST( + NAME + ${AITT_UT}_module + COMMAND + ${CMAKE_COMMAND} -E env + LD_LIBRARY_PATH=../modules/tcp/:../:../common/:$ENV{LD_LIBRARY_PATH} + ${CMAKE_CURRENT_BINARY_DIR}/${AITT_UT}_module --gtest_filter=*_Anytime +) diff --git a/tests/MQ_mocktest.cc b/tests/MQ_mocktest.cc new file mode 100644 index 0000000..06fcc7d --- /dev/null +++ b/tests/MQ_mocktest.cc @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2021-2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include <condition_variable> +#include <mutex> + +#include "AittTypes.h" +#include "MQ.h" +#include "MQMockTest.h" +#include "MQTTMock.h" + +using ::testing::Return; + +#define TEST_TOPIC "Test/Topic" +#define TEST_PAYLOAD "The last will is ..." +#define TEST_CLIENT_ID "testClient" +#define TEST_PORT 8123 +#define TEST_HOST "localhost" +#define TEST_HANDLE reinterpret_cast<mosquitto *>(0xbeefbeef) + +TEST_F(MQMockTest, Negative_Create_lib_init_Anytime) +{ + EXPECT_CALL(GetMock(), mosquitto_lib_init()).WillOnce(Return(MOSQ_ERR_NOT_SUPPORTED)); + EXPECT_CALL(GetMock(), mosquitto_destroy(nullptr)).WillOnce(Return()); + EXPECT_CALL(GetMock(), mosquitto_lib_cleanup()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + + try { + aitt::MQ mq(TEST_CLIENT_ID, true); + FAIL() << "lib_init must be failed"; + } catch (std::exception &e) { + ASSERT_STREQ(e.what(), "MQTT failure : MQ Constructor Error"); + } +} + +TEST_F(MQMockTest, Negative_Create_new_Anytime) +{ + EXPECT_CALL(GetMock(), mosquitto_lib_init()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_new(testing::StrEq(TEST_CLIENT_ID), true, testing::_)) + .WillOnce(Return(nullptr)); + EXPECT_CALL(GetMock(), mosquitto_destroy(nullptr)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_lib_cleanup()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + + try { + aitt::MQ mq(TEST_CLIENT_ID, true); + FAIL() << "lib_init must be failed"; + } catch (std::exception &e) { + ASSERT_STREQ(e.what(), "MQTT failure : MQ Constructor Error"); + } +} + +TEST_F(MQMockTest, Positive_Publish_Anytime) +{ + EXPECT_CALL(GetMock(), mosquitto_lib_init()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_new(testing::StrEq(TEST_CLIENT_ID), true, testing::_)) + .WillOnce(Return(TEST_HANDLE)); + EXPECT_CALL(GetMock(), mosquitto_message_v5_callback_set(TEST_HANDLE, testing::_)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_loop_start(TEST_HANDLE)).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_connect(TEST_HANDLE, testing::StrEq(TEST_HOST), TEST_PORT, 60)) + .WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_publish(TEST_HANDLE, testing::_, testing::StrEq(TEST_TOPIC), + sizeof(TEST_PAYLOAD), TEST_PAYLOAD, AITT_QOS_AT_MOST_ONCE, false)) + .WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_destroy(TEST_HANDLE)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_lib_cleanup()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + + try { + aitt::MQ mq(TEST_CLIENT_ID, true); + mq.Connect(TEST_HOST, TEST_PORT, "", ""); + mq.Publish(TEST_TOPIC, TEST_PAYLOAD, sizeof(TEST_PAYLOAD)); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(MQMockTest, Positive_Subscribe_Anytime) +{ + EXPECT_CALL(GetMock(), mosquitto_lib_init()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_new(testing::StrEq(TEST_CLIENT_ID), true, testing::_)) + .WillOnce(Return(TEST_HANDLE)); + EXPECT_CALL(GetMock(), mosquitto_message_v5_callback_set(TEST_HANDLE, testing::_)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_loop_start(TEST_HANDLE)).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_connect(TEST_HANDLE, testing::StrEq(TEST_HOST), TEST_PORT, 60)) + .WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_subscribe(TEST_HANDLE, testing::_, testing::StrEq(TEST_TOPIC), + AITT_QOS_AT_MOST_ONCE)) + .WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_destroy(TEST_HANDLE)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_lib_cleanup()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + + try { + aitt::MQ mq(TEST_CLIENT_ID, true); + mq.Connect(TEST_HOST, TEST_PORT, "", ""); + mq.Subscribe( + TEST_TOPIC, + [](aitt::MSG *info, const std::string &topic, const void *msg, const int szmsg, + const void *cbdata) -> void {}, + nullptr, AITT_QOS_AT_MOST_ONCE); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(MQMockTest, Positive_Unsubscribe_Anytime) +{ + EXPECT_CALL(GetMock(), mosquitto_lib_init()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_new(testing::StrEq(TEST_CLIENT_ID), true, testing::_)) + .WillOnce(Return(TEST_HANDLE)); + EXPECT_CALL(GetMock(), mosquitto_message_v5_callback_set(TEST_HANDLE, testing::_)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_loop_start(TEST_HANDLE)).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_connect(TEST_HANDLE, testing::StrEq(TEST_HOST), TEST_PORT, 60)) + .WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), + mosquitto_subscribe(TEST_HANDLE, testing::_, testing::StrEq(TEST_TOPIC), 0)) + .WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), + mosquitto_unsubscribe(TEST_HANDLE, testing::_, testing::StrEq(TEST_TOPIC))) + .WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_destroy(TEST_HANDLE)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_lib_cleanup()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + + try { + aitt::MQ mq(TEST_CLIENT_ID, true); + mq.Connect(TEST_HOST, TEST_PORT, "", ""); + void *handle = mq.Subscribe( + TEST_TOPIC, + [](aitt::MSG *info, const std::string &topic, const void *msg, const int szmsg, + const void *cbdata) -> void {}, + nullptr, AITT_QOS_AT_MOST_ONCE); + mq.Unsubscribe(handle); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} + +TEST_F(MQMockTest, Positive_Create_Anytime) +{ + EXPECT_CALL(GetMock(), mosquitto_lib_init()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_new(testing::StrEq(TEST_CLIENT_ID), true, testing::_)) + .WillOnce(Return(TEST_HANDLE)); + EXPECT_CALL(GetMock(), + mosquitto_int_option(TEST_HANDLE, MOSQ_OPT_PROTOCOL_VERSION, MQTT_PROTOCOL_V5)) + .Times(1); + EXPECT_CALL(GetMock(), mosquitto_message_v5_callback_set(TEST_HANDLE, testing::_)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_destroy(TEST_HANDLE)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_lib_cleanup()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + + try { + aitt::MQ mq(TEST_CLIENT_ID, true); + } catch (std::exception &e) { + FAIL() << "Unexpected exception occurred"; + } +} + +TEST_F(MQMockTest, Negative_Connect_will_set_Anytime) +{ + EXPECT_CALL(GetMock(), mosquitto_lib_init()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_new(testing::StrEq(TEST_CLIENT_ID), true, testing::_)) + .WillOnce(Return(TEST_HANDLE)); + EXPECT_CALL(GetMock(), mosquitto_message_v5_callback_set(TEST_HANDLE, testing::_)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_will_set(TEST_HANDLE, testing::StrEq("lastWill"), + sizeof(TEST_PAYLOAD), TEST_PAYLOAD, AITT_QOS_AT_MOST_ONCE, true)) + .WillOnce(Return(MOSQ_ERR_NOMEM)); + EXPECT_CALL(GetMock(), mosquitto_destroy(TEST_HANDLE)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_lib_cleanup()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + try { + aitt::MQ mq(TEST_CLIENT_ID, true); + mq.SetWillInfo("lastWill", TEST_PAYLOAD, sizeof(TEST_PAYLOAD), AITT_QOS_AT_MOST_ONCE, true); + mq.Connect(TEST_HOST, TEST_PORT, "", ""); + FAIL() << "Connect() must be failed"; + } catch (std::exception &e) { + ASSERT_STREQ(e.what(), "MQTT failure"); + } +} + +TEST_F(MQMockTest, Positive_Connect_Anytime) +{ + EXPECT_CALL(GetMock(), mosquitto_lib_init()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_new(testing::StrEq(TEST_CLIENT_ID), true, testing::_)) + .WillOnce(Return(TEST_HANDLE)); + EXPECT_CALL(GetMock(), mosquitto_message_v5_callback_set(TEST_HANDLE, testing::_)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_connect(TEST_HANDLE, testing::StrEq(TEST_HOST), TEST_PORT, 60)) + .WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_destroy(TEST_HANDLE)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_lib_cleanup()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + try { + aitt::MQ mq(TEST_CLIENT_ID, true); + mq.Connect(TEST_HOST, TEST_PORT, "", ""); + } catch (std::exception &e) { + FAIL() << "Unepxected exception: " << e.what(); + } +} + +TEST_F(MQMockTest, Positive_Connect_User_Anytime) +{ + std::string username = "test"; + std::string password = "test"; + EXPECT_CALL(GetMock(), mosquitto_lib_init()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_new(testing::StrEq(TEST_CLIENT_ID), true, testing::_)) + .WillOnce(Return(TEST_HANDLE)); + EXPECT_CALL(GetMock(), mosquitto_message_v5_callback_set(TEST_HANDLE, testing::_)).Times(1); + EXPECT_CALL(GetMock(), + mosquitto_username_pw_set(TEST_HANDLE, username.c_str(), password.c_str())) + .Times(1); + EXPECT_CALL(GetMock(), mosquitto_connect(TEST_HANDLE, testing::StrEq(TEST_HOST), TEST_PORT, 60)) + .WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_destroy(TEST_HANDLE)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_lib_cleanup()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + try { + aitt::MQ mq(TEST_CLIENT_ID, true); + mq.Connect(TEST_HOST, TEST_PORT, username, password); + } catch (std::exception &e) { + FAIL() << "Unepxected exception: " << e.what(); + } +} + +TEST_F(MQMockTest, Positive_Disconnect_Anytime) +{ + EXPECT_CALL(GetMock(), mosquitto_lib_init()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_new(testing::StrEq(TEST_CLIENT_ID), true, testing::_)) + .WillOnce(Return(TEST_HANDLE)); + EXPECT_CALL(GetMock(), mosquitto_message_v5_callback_set(TEST_HANDLE, testing::_)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_disconnect(testing::_)).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_will_clear(TEST_HANDLE)).WillOnce(Return(MOSQ_ERR_SUCCESS)); + EXPECT_CALL(GetMock(), mosquitto_destroy(TEST_HANDLE)).Times(1); + EXPECT_CALL(GetMock(), mosquitto_lib_cleanup()).WillOnce(Return(MOSQ_ERR_SUCCESS)); + try { + aitt::MQ mq(TEST_CLIENT_ID, true); + mq.Disconnect(); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} diff --git a/tests/MQ_test.cc b/tests/MQ_test.cc new file mode 100644 index 0000000..4ff554b --- /dev/null +++ b/tests/MQ_test.cc @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "MQ.h" + +#include <gtest/gtest.h> + +#include "aitt_internal.h" +#include "aitt_tests.h" + +using MQ = aitt::MQ; + +class MQTest : public testing::Test, public AittTests { + protected: + void SetUp() override { Init(); } + void TearDown() override { Deinit(); } +}; + +TEST_F(MQTest, Positve_Subscribe_in_Subscribe_Anytime) +{ + try { + MQ mq("MQ_TEST_ID"); + mq.Connect(LOCAL_IP, 1883, "", ""); + mq.Subscribe( + "MQ_TEST_TOPIC1", + [&](aitt::MSG *handle, const std::string &topic, const void *data, + const size_t datalen, void *user_data) { + DBG("Subscribe invoked: %s %zu", static_cast<const char *>(data), datalen); + + mq.Subscribe( + "topic1InCallback", + [](aitt::MSG *handle, const std::string &topic, const void *msg, + const size_t szmsg, void *cbdata) {}, + user_data); + + mq.Subscribe( + "topic2InCallback", + [](aitt::MSG *handle, const std::string &topic, const void *msg, + const size_t szmsg, void *cbdata) {}, + user_data); + g_timeout_add( + 100, + [](gpointer cbdata) -> gboolean { + MQTest *test = static_cast<MQTest *>(cbdata); + test->ToggleReady(); + return G_SOURCE_REMOVE; + }, + user_data); + }, + static_cast<void *>(this)); + + DBG("Publish message to %s (%s)", "MQ_TEST_TOPIC1", TEST_MSG); + mq.Publish("MQ_TEST_TOPIC1", TEST_MSG, sizeof(TEST_MSG)); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + + IterateEventLoop(); + + ASSERT_TRUE(ready); + } catch (std::exception &e) { + FAIL() << "Unexpected exception: " << e.what(); + } +} diff --git a/tests/MainLoopHandler_test.cc b/tests/MainLoopHandler_test.cc new file mode 100644 index 0000000..6aab8a4 --- /dev/null +++ b/tests/MainLoopHandler_test.cc @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "MainLoopHandler.h" + +#include <fcntl.h> +#include <gtest/gtest.h> +#include <sys/socket.h> +#include <sys/un.h> + +#include <cstdlib> +#include <thread> + +#include "aitt_internal.h" + +class MainLoopTest : public testing::Test { + protected: + void SetUp() override + { + bzero(&addr, sizeof(addr)); + + server_fd = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_NE(server_fd, -1); + + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, std::to_string(std::rand() / 1e10).c_str(), + sizeof(addr.sun_path) - 1); + + int ret = bind(server_fd, (struct sockaddr *)&addr, SUN_LEN(&addr)); + ASSERT_NE(ret, -1); + + listen(server_fd, 1); + my_thread = std::thread(&MainLoopTest::eventWriter, this); + } + + void TearDown() override + { + my_thread.join(); + close(server_fd); + } + + int server_fd; + struct sockaddr_un addr; + std::thread my_thread; + + private: + void eventWriter() + { + int ret; + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_NE(fd, -1); + + ret = connect(fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)); + ASSERT_NE(ret, -1); + + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + ret = write(fd, "1", 1); + ASSERT_NE(ret, -1); + + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + close(fd); + } +}; + +using aitt::MainLoopHandler; + +TEST_F(MainLoopTest, Normal_Anytime) +{ + MainLoopHandler handler; + bool ret = false; + + handler.AddWatch( + server_fd, + [&](MainLoopHandler::MainLoopResult result, int fd, MainLoopHandler::MainLoopData *data) { + int client_fd = accept(server_fd, 0, 0); + EXPECT_NE(client_fd, -1); + handler.AddWatch( + client_fd, + [&](MainLoopHandler::MainLoopResult result, int fd, + MainLoopHandler::MainLoopData *data) { + EXPECT_EQ(result, MainLoopHandler::OK); + char buf[2] = {0}; + EXPECT_EQ(read(fd, buf, 1), 1); + EXPECT_STREQ(buf, "1"); + handler.Quit(); + ret = true; + }, + nullptr); + }, + nullptr); + handler.Run(); + + EXPECT_TRUE(ret); +} + +TEST_F(MainLoopTest, HANGUP_Anytime) +{ + MainLoopHandler handler; + bool ret = false; + + handler.AddWatch( + server_fd, + [&](MainLoopHandler::MainLoopResult result, int fd, MainLoopHandler::MainLoopData *data) { + int client_fd = accept(server_fd, 0, 0); + EXPECT_NE(client_fd, -1); + handler.AddWatch( + client_fd, + [&](MainLoopHandler::MainLoopResult result, int fd, + MainLoopHandler::MainLoopData *data) { + if (result == MainLoopHandler::OK) { + char buf[2] = {0}; + EXPECT_EQ(read(fd, buf, 1), 1); + EXPECT_STREQ(buf, "1"); + return; + } + + EXPECT_EQ(result, MainLoopHandler::HANGUP); + handler.Quit(); + ret = true; + }, + nullptr); + }, + nullptr); + + handler.Run(); + + EXPECT_TRUE(ret); +} + +TEST_F(MainLoopTest, removeWatch_Anytime) +{ + MainLoopHandler handler; + MainLoopHandler::MainLoopData test_data; + + handler.AddWatch( + server_fd, + [&](MainLoopHandler::MainLoopResult result, int fd, MainLoopHandler::MainLoopData *data) { + FAIL() << "It's removed"; + }, + &test_data); + MainLoopHandler::MainLoopData *check_data = handler.RemoveWatch(server_fd); + + EXPECT_TRUE(&test_data == check_data); +} + +TEST_F(MainLoopTest, UserData_Anytime) +{ + MainLoopHandler handler; + bool ret = false; + + MainLoopHandler::MainLoopData test_data; + + handler.AddWatch( + server_fd, + [&](MainLoopHandler::MainLoopResult result, int fd, MainLoopHandler::MainLoopData *data) { + EXPECT_EQ(data, &test_data); + handler.Quit(); + ret = true; + }, + &test_data); + + handler.Run(); + + EXPECT_TRUE(ret); +} + +TEST_F(MainLoopTest, AddIdle_Anytime) +{ + bool ret = false; + MainLoopHandler handler; + MainLoopHandler::MainLoopData test_data; + + handler.AddIdle( + &handler, + [&](MainLoopHandler::MainLoopResult result, int fd, MainLoopHandler::MainLoopData *data) { + EXPECT_EQ(data, &test_data); + handler.Quit(); + ret = true; + }, + &test_data); + + handler.Run(); + + EXPECT_TRUE(ret); +} + +TEST_F(MainLoopTest, AddTimeout_Anytime) +{ + bool ret = false; + int interval = 1000; + MainLoopHandler handler; + struct timespec ts_start, ts_end; + MainLoopHandler::MainLoopData test_data; + + clock_gettime(CLOCK_MONOTONIC, &ts_start); + + handler.AddTimeout( + interval, + [&](MainLoopHandler::MainLoopResult result, int fd, MainLoopHandler::MainLoopData *data) { + EXPECT_EQ(data, &test_data); + clock_gettime(CLOCK_MONOTONIC, &ts_end); + double diff = 1000.0 * ts_end.tv_sec + 1e-6 * ts_end.tv_nsec + - (1000.0 * ts_start.tv_sec + 1e-6 * ts_start.tv_nsec); + EXPECT_GE(diff, interval); + handler.Quit(); + ret = true; + }, + &test_data); + + handler.Run(); + + EXPECT_TRUE(ret); +} diff --git a/tests/RequestResponse_test.cc b/tests/RequestResponse_test.cc new file mode 100644 index 0000000..d723e3b --- /dev/null +++ b/tests/RequestResponse_test.cc @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include <glib.h> +#include <gtest/gtest.h> + +#include <iostream> + +#include "AITT.h" +#include "aitt_internal.h" +#include "aitt_tests.h" + +using AITT = aitt::AITT; + +class AITTRRTest : public testing::Test, public AittTests { + public: + void PublishSyncInCallback(aitt::AITT *aitt, bool *reply1_ok, bool *reply2_ok, aitt::MSG *msg, + const void *data, const size_t datalen, void *cbdata) + { + aitt->PublishWithReplySync( + rr_topic.c_str(), message.c_str(), message.size(), AITT_TYPE_MQTT, + AITT_QOS_AT_MOST_ONCE, false, + [&](aitt::MSG *msg, const void *data, const size_t datalen, void *cbdata) { + CheckReply(msg, data, datalen); + *reply1_ok = true; + }, + nullptr, correlation); + + CheckReply(msg, data, datalen); + *reply2_ok = true; + + ToggleReady(); + } + + void CheckReplyCallback(bool toggle, bool *reply_ok, aitt::MSG *msg, const void *data, + const size_t datalen, void *cbdata) + { + CheckReply(msg, data, datalen); + *reply_ok = true; + if (toggle) + ToggleReady(); + } + + protected: + void SetUp() override { Init(); } + void TearDown() override { Deinit(); } + + void CheckReply(aitt::MSG *msg, const void *data, const size_t datalen) + { + std::string received_data((const char *)data, datalen); + EXPECT_EQ(msg->GetCorrelation(), correlation); + EXPECT_EQ(received_data, reply); + EXPECT_EQ(msg->IsEndSequence(), true); + } + + void CheckSubscribe(aitt::MSG *msg, const void *data, const size_t datalen) + { + std::string received_data((const char *)data, datalen); + EXPECT_TRUE(msg->GetTopic() == rr_topic); + EXPECT_TRUE(msg->GetCorrelation() == correlation); + EXPECT_FALSE(msg->GetResponseTopic().empty()); + EXPECT_EQ(received_data, message); + } + + void Call2Times(bool first_sync, bool second_sync) + { + bool sub_ok; + bool reply_ok[2]; + sub_ok = reply_ok[0] = reply_ok[1] = false; + + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + + aitt.Subscribe(rr_topic.c_str(), + [&](aitt::MSG *msg, const void *data, const size_t datalen, void *cbdata) { + CheckSubscribe(msg, data, datalen); + aitt.SendReply(msg, reply.c_str(), reply.size()); + sub_ok = true; + }); + + using namespace std::placeholders; + for (int i = 0; i < 2; i++) { + if ((i == 0 && first_sync) || (i == 1 && second_sync)) { + INFO("PublishWithReplySync() Call"); + aitt.PublishWithReplySync(rr_topic.c_str(), message.c_str(), message.size(), + AITT_TYPE_MQTT, AITT_QOS_AT_MOST_ONCE, false, + std::bind(&AITTRRTest::CheckReplyCallback, this, (i == 1), &reply_ok[i], _1, + _2, _3, _4), + nullptr, correlation); + } else { + INFO("PublishWithReply() Call"); + aitt.PublishWithReply(rr_topic.c_str(), message.c_str(), message.size(), + AITT_TYPE_MQTT, AITT_QOS_AT_MOST_ONCE, false, + std::bind(&AITTRRTest::CheckReplyCallback, this, (i == 1), &reply_ok[i], _1, + _2, _3, _4), + nullptr, correlation); + } + } + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + IterateEventLoop(); + + EXPECT_TRUE(sub_ok); + EXPECT_TRUE(reply_ok[0]); + EXPECT_TRUE(reply_ok[1]); + } + + void SyncCallInCallback(bool sync) + { + bool sub_ok, reply1_ok, reply2_ok; + sub_ok = reply1_ok = reply2_ok = false; + + AITT sub_aitt(clientId + "sub", LOCAL_IP, true); + INFO("Constructor Success"); + + sub_aitt.Connect(); + INFO("Connected"); + + sub_aitt.Subscribe(rr_topic.c_str(), + [&](aitt::MSG *msg, const void *data, const size_t datalen, void *cbdata) { + CheckSubscribe(msg, data, datalen); + sub_aitt.SendReply(msg, reply.c_str(), reply.size()); + sub_ok = true; + }); + + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + + using namespace std::placeholders; + auto replyCB = std::bind(&AITTRRTest::PublishSyncInCallback, GetHandle(), &aitt, &reply1_ok, + &reply2_ok, _1, _2, _3, _4); + + if (sync) { + aitt.PublishWithReplySync(rr_topic.c_str(), message.c_str(), message.size(), + AITT_TYPE_MQTT, AITT_QOS_AT_MOST_ONCE, false, replyCB, nullptr, correlation); + } else { + aitt.PublishWithReply(rr_topic.c_str(), message.c_str(), message.size(), AITT_TYPE_MQTT, + AITT_QOS_AT_MOST_ONCE, false, replyCB, nullptr, correlation); + } + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + IterateEventLoop(); + + EXPECT_TRUE(sub_ok); + EXPECT_TRUE(reply1_ok); + EXPECT_TRUE(reply2_ok); + } + + AITTRRTest *GetHandle() { return this; } + + const std::string rr_topic = "test/rr_topic"; + const std::string message = "Hello world"; + const std::string correlation = "0001"; + const std::string reply = "Nice to meet you, RequestResponse"; +}; + +TEST_F(AITTRRTest, RequestResponse_P_Anytime) +{ + bool sub_ok, reply_ok; + sub_ok = reply_ok = false; + + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + + aitt.Subscribe(rr_topic.c_str(), + [&](aitt::MSG *msg, const void *data, const size_t datalen, void *cbdata) { + CheckSubscribe(msg, data, datalen); + aitt.SendReply(msg, reply.c_str(), reply.size()); + sub_ok = true; + }); + + aitt.PublishWithReply(rr_topic.c_str(), message.c_str(), message.size(), AITT_TYPE_MQTT, + AITT_QOS_AT_MOST_ONCE, false, + std::bind(&AITTRRTest::CheckReplyCallback, GetHandle(), true, &reply_ok, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4), + nullptr, correlation); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + IterateEventLoop(); + + EXPECT_TRUE(sub_ok); + EXPECT_TRUE(reply_ok); + } catch (std::exception &e) { + FAIL() << e.what(); + } +} + +TEST_F(AITTRRTest, RequestResponse_asymmetry_Anytime) +{ + std::string reply1 = "1st data"; + std::string reply2 = "2nd data"; + std::string reply3 = "final data"; + + bool sub_ok, reply_ok; + sub_ok = reply_ok = false; + + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + + aitt.Subscribe(rr_topic.c_str(), + [&](aitt::MSG *msg, const void *data, const size_t datalen, void *cbdata) { + CheckSubscribe(msg, data, datalen); + + aitt.SendReply(msg, reply1.c_str(), reply1.size(), false); + aitt.SendReply(msg, reply2.c_str(), reply2.size(), false); + aitt.SendReply(msg, reply3.c_str(), reply3.size(), true); + + sub_ok = true; + }); + + aitt.PublishWithReply( + rr_topic.c_str(), message.c_str(), message.size(), AITT_TYPE_MQTT, + AITT_QOS_AT_MOST_ONCE, false, + [&](aitt::MSG *msg, const void *data, const size_t datalen, void *cbdata) { + std::string reply((const char *)data, datalen); + + EXPECT_EQ(msg->GetCorrelation(), correlation); + switch (msg->GetSequence()) { + case 1: + EXPECT_EQ(reply, reply1); + break; + case 2: + EXPECT_EQ(reply, reply2); + break; + case 3: + EXPECT_EQ(reply, reply3); + EXPECT_TRUE(msg->IsEndSequence()); + reply_ok = true; + ToggleReady(); + break; + default: + FAIL() << "Unknown sequence" << msg->GetSequence(); + } + }, + nullptr, correlation); + + g_timeout_add(10, AittTests::ReadyCheck, static_cast<AittTests *>(this)); + IterateEventLoop(); + + EXPECT_TRUE(sub_ok); + EXPECT_TRUE(reply_ok); + + } catch (std::exception &e) { + FAIL() << e.what(); + } +} + +TEST_F(AITTRRTest, RequestResponse_2times_Anytime) +{ + try { + bool first_sync = false; + bool second_sync = false; + Call2Times(first_sync, second_sync); + } catch (std::exception &e) { + FAIL() << e.what(); + } +} + +TEST_F(AITTRRTest, RequestResponse_sync_P_Anytime) +{ + bool sub_ok, reply1_ok; + sub_ok = reply1_ok = false; + + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + + aitt.Subscribe(rr_topic.c_str(), + [&](aitt::MSG *msg, const void *data, const size_t datalen, void *cbdata) { + CheckSubscribe(msg, data, datalen); + aitt.SendReply(msg, reply.c_str(), reply.size()); + sub_ok = true; + }); + + aitt.PublishWithReplySync(rr_topic.c_str(), message.c_str(), message.size(), AITT_TYPE_MQTT, + AITT_QOS_AT_MOST_ONCE, false, + std::bind(&AITTRRTest::CheckReplyCallback, GetHandle(), false, &reply1_ok, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, + std::placeholders::_4), + nullptr, correlation); + + EXPECT_TRUE(sub_ok); + EXPECT_TRUE(reply1_ok); + } catch (std::exception &e) { + FAIL() << e.what(); + } +} + +TEST_F(AITTRRTest, RequestResponse_sync_async_P_Anytime) +{ + try { + bool first_sync = true; + bool second_sync = false; + Call2Times(first_sync, second_sync); + } catch (std::exception &e) { + FAIL() << e.what(); + } +} + +TEST_F(AITTRRTest, RequestResponse_sync_in_async_P_Anytime) +{ + try { + bool sync_callback = false; + SyncCallInCallback(sync_callback); + } catch (std::exception &e) { + FAIL() << e.what(); + } +} + +TEST_F(AITTRRTest, RequestResponse_sync_in_sync_P_Anytime) +{ + try { + bool sync_callback = true; + SyncCallInCallback(sync_callback); + } catch (std::exception &e) { + FAIL() << e.what(); + } +} + +TEST_F(AITTRRTest, RequestResponse_timeout_P_Anytime) +{ + try { + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + + int ret = aitt.PublishWithReplySync( + rr_topic.c_str(), message.c_str(), message.size(), AITT_TYPE_MQTT, + AITT_QOS_AT_MOST_ONCE, false, + [&](aitt::MSG *msg, const void *data, const size_t datalen, void *cbdata) { + FAIL() << "Should not be called"; + }, + nullptr, correlation, 1); + + EXPECT_EQ(ret, AITT_ERROR_TIMED_OUT); + } catch (std::exception &e) { + FAIL() << e.what(); + } +} + +TEST_F(AITTRRTest, RequestResponse_timeout_restart_P_Anytime) +{ + bool sub_ok, reply_ok; + sub_ok = reply_ok = false; + + try { + AITT sub_aitt(clientId + "sub", LOCAL_IP, true); + sub_aitt.Connect(); + sub_aitt.Subscribe(rr_topic.c_str(), + [&](aitt::MSG *msg, const void *data, const size_t datalen, void *cbdata) { + INFO("Subscribe Callback is called"); + CheckSubscribe(msg, data, datalen); + sub_aitt.SendReply(msg, reply.c_str(), reply.size(), false); + sub_ok = true; + }); + + AITT aitt(clientId, LOCAL_IP, true); + aitt.Connect(); + + int ret = aitt.PublishWithReplySync( + rr_topic.c_str(), message.c_str(), message.size(), AITT_TYPE_MQTT, + AITT_QOS_AT_MOST_ONCE, false, + [&](aitt::MSG *msg, const void *data, const size_t datalen, void *cbdata) { + INFO("Reply Callback is called"); + static int invalid = 0; + if (invalid) + FAIL() << "Should not be called"; + else + reply_ok = true; + invalid++; + }, + nullptr, correlation, 500); + + EXPECT_TRUE(sub_ok == reply_ok); + EXPECT_EQ(ret, AITT_ERROR_TIMED_OUT); + } catch (std::exception &e) { + FAIL() << e.what(); + } +} diff --git a/tests/TransportModuleLoader_test.cc b/tests/TransportModuleLoader_test.cc new file mode 100644 index 0000000..57e69b1 --- /dev/null +++ b/tests/TransportModuleLoader_test.cc @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "TransportModuleLoader.h" + +#include <AITT.h> +#include <gtest/gtest.h> + +#include "AittTransport.h" +#include "aitt_internal.h" + +class TransportModuleLoaderTest : public testing::Test { + public: + TransportModuleLoaderTest(void) : discovery("test"), loader("127.0.0.1") + { + loader.Init(discovery); + } + + protected: + void SetUp() override {} + void TearDown() override {} + + aitt::AittDiscovery discovery; + aitt::TransportModuleLoader loader; +}; + +TEST_F(TransportModuleLoaderTest, Positive_GetInstance_Anytime) +{ + std::shared_ptr<aitt::AittTransport> module = loader.GetInstance(AITT_TYPE_TCP); + ASSERT_NE(module, nullptr); +} + +TEST_F(TransportModuleLoaderTest, Negative_GetInstance_Anytime) +{ + std::shared_ptr<aitt::AittTransport> module = loader.GetInstance(AITT_TYPE_MQTT); + ASSERT_EQ(module, nullptr); +} diff --git a/tests/aitt_c_manualtest.cc b/tests/aitt_c_manualtest.cc new file mode 100644 index 0000000..6085ea5 --- /dev/null +++ b/tests/aitt_c_manualtest.cc @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "aitt_c.h" + +#include <glib.h> +#include <gtest/gtest.h> + +#include "aitt_tests.h" + +TEST(AITT_C_MANUAL, will_set_P) +{ + int ret; + aitt_h handle = aitt_new("test14", LOCAL_IP); + ASSERT_NE(handle, nullptr); + + ret = aitt_connect(handle, LOCAL_IP, 1883); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + static bool sub_called = false; + GMainLoop *loop = g_main_loop_new(nullptr, FALSE); + aitt_sub_h sub_handle = nullptr; + ret = aitt_subscribe( + handle, "test/topic_will", + [](aitt_msg_h msg_handle, const void *msg, size_t msg_len, void *user_data) { + std::string received_data((const char *)msg, msg_len); + EXPECT_STREQ(received_data.c_str(), TEST_C_MSG); + EXPECT_STREQ(aitt_msg_get_topic(msg_handle), TEST_C_TOPIC); + sub_called = true; + }, + loop, &sub_handle); + ASSERT_EQ(ret, AITT_ERROR_NONE); + EXPECT_TRUE(sub_handle != nullptr); + + int pid = fork(); + if (pid == 0) { + aitt_h handle_will = aitt_new("test_will", LOCAL_IP); + ASSERT_NE(handle_will, nullptr); + + ret = aitt_will_set(handle_will, "test/topic_will", TEST_C_MSG, strlen(TEST_C_MSG), + AITT_QOS_AT_LEAST_ONCE, false); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + ret = aitt_connect(handle_will, LOCAL_IP, 1883); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + // Do not call below + // aitt_disconnect(handle_will) + // aitt _destroy(handle_will); + } else { + sleep(1); + kill(pid, SIGKILL); + + g_timeout_add( + 10, + [](gpointer data) -> gboolean { + if (sub_called) { + GMainLoop *loop = static_cast<GMainLoop *>(data); + g_main_loop_quit(loop); + return FALSE; + } + return TRUE; + }, + loop); + + g_main_loop_run(loop); + g_main_loop_unref(loop); + + ret = aitt_disconnect(handle); + EXPECT_EQ(ret, AITT_ERROR_NONE); + + aitt_destroy(handle); + } +} + +// Set user/passwd in mosquitto.conf before testing +TEST(AITT_C_MANUAL, connect_id_passwd_P) +{ + aitt_h handle = aitt_new("test15", LOCAL_IP); + ASSERT_NE(handle, nullptr); + + int ret = aitt_connect_full(handle, LOCAL_IP, 1883, "testID", "testPasswd"); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + ret = aitt_disconnect(handle); + EXPECT_EQ(ret, AITT_ERROR_NONE); + + aitt_destroy(handle); +} diff --git a/tests/aitt_c_test.cc b/tests/aitt_c_test.cc new file mode 100644 index 0000000..a83f88d --- /dev/null +++ b/tests/aitt_c_test.cc @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "aitt_c.h" + +#include <glib.h> +#include <gtest/gtest.h> + +#include "aitt_internal.h" +#include "aitt_tests.h" + +TEST(AITT_C_INTERFACE, new_P_Anytime) +{ + aitt_h handle = aitt_new("test1", LOCAL_IP); + EXPECT_TRUE(handle != nullptr); + aitt_destroy(handle); + + handle = aitt_new(nullptr, nullptr); + EXPECT_TRUE(handle != nullptr); + aitt_destroy(handle); + + handle = aitt_new("", ""); + EXPECT_TRUE(handle != nullptr); + aitt_destroy(handle); +} + +TEST(AITT_C_INTERFACE, destroy_P_Anytime) +{ + aitt_h handle = aitt_new("test2", LOCAL_IP); + ASSERT_NE(handle, nullptr); + + aitt_destroy(handle); + aitt_destroy(nullptr); +} + +// TODO:: Not yet Support +/* +TEST(AITT_C_INTERFACE, option_P_Anytime) +{ + aitt_h handle = aitt_new("test3"); + ASSERT_NE(handle, nullptr); + + int ret = aitt_set_option(handle, AITT_OPT_MY_IP, LOCAL_IP); + EXPECT_EQ(ret, AITT_ERROR_NONE); + EXPECT_STREQ(LOCAL_IP, aitt_get_option(handle, AITT_OPT_MY_IP)); + + ret = aitt_set_option(handle, AITT_OPT_MY_IP, NULL); + EXPECT_EQ(ret, AITT_ERROR_NONE); + EXPECT_EQ(NULL, aitt_get_option(handle, AITT_OPT_MY_IP)); + + aitt_destroy(handle); +} + +TEST(AITT_C_INTERFACE, option_N_Anytime) +{ + aitt_h handle = aitt_new("test4"); + ASSERT_NE(handle, nullptr); + + int ret = aitt_set_option(handle, AITT_OPT_UNKNOWN, LOCAL_IP); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + ret = aitt_set_option(nullptr, AITT_OPT_MY_IP, LOCAL_IP); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + aitt_destroy(handle); +} +*/ + +TEST(AITT_C_INTERFACE, connect_disconnect_P_Anytime) +{ + aitt_h handle = aitt_new("test5", LOCAL_IP); + ASSERT_NE(handle, nullptr); + + int ret = aitt_connect(handle, LOCAL_IP, 1883); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + ret = aitt_disconnect(handle); + EXPECT_EQ(ret, AITT_ERROR_NONE); + + aitt_destroy(handle); +} + +TEST(AITT_C_INTERFACE, connect_N_Anytime) +{ + aitt_h handle = aitt_new("test6", LOCAL_IP); + ASSERT_NE(handle, nullptr); + + aitt_h invalid_handle = nullptr; + int ret = aitt_connect(invalid_handle, LOCAL_IP, 1883); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + const char *invalid_ip = "1.2.3"; + ret = aitt_connect(handle, invalid_ip, 1883); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + invalid_ip = ""; + ret = aitt_connect(handle, invalid_ip, 1883); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + int invalid_port = -1; + ret = aitt_connect(handle, LOCAL_IP, invalid_port); + EXPECT_EQ(ret, AITT_ERROR_SYSTEM); + + aitt_destroy(handle); +} + +TEST(AITT_C_INTERFACE, disconnect_N_Anytime) +{ + int ret = aitt_disconnect(nullptr); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + aitt_h handle = aitt_new("test7", LOCAL_IP); + ASSERT_NE(handle, nullptr); + + ret = aitt_disconnect(handle); + EXPECT_EQ(ret, AITT_ERROR_NOT_READY); + + aitt_destroy(handle); +} + +TEST(AITT_C_INTERFACE, pub_sub_P_Anytime) +{ + aitt_h handle = aitt_new("test8", LOCAL_IP); + ASSERT_NE(handle, nullptr); + + int ret = aitt_connect(handle, LOCAL_IP, 1883); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + GMainLoop *loop = g_main_loop_new(nullptr, FALSE); + aitt_sub_h sub_handle = nullptr; + ret = aitt_subscribe( + handle, TEST_C_TOPIC, + [](aitt_msg_h msg_handle, const void *msg, size_t msg_len, void *user_data) { + GMainLoop *loop = static_cast<GMainLoop *>(user_data); + std::string received_data((const char *)msg, msg_len); + EXPECT_STREQ(received_data.c_str(), TEST_C_MSG); + EXPECT_STREQ(aitt_msg_get_topic(msg_handle), TEST_C_TOPIC); + g_main_loop_quit(loop); + }, + loop, &sub_handle); + ASSERT_EQ(ret, AITT_ERROR_NONE); + EXPECT_TRUE(sub_handle != nullptr); + + ret = aitt_publish(handle, TEST_C_TOPIC, TEST_C_MSG, strlen(TEST_C_MSG)); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + g_main_loop_run(loop); + g_main_loop_unref(loop); + + ret = aitt_disconnect(handle); + EXPECT_EQ(ret, AITT_ERROR_NONE); + + aitt_destroy(handle); +} + +TEST(AITT_C_INTERFACE, pub_N_Anytime) +{ + aitt_h handle = aitt_new("test9", LOCAL_IP); + ASSERT_NE(handle, nullptr); + + int ret = aitt_connect(handle, nullptr, 1883); + EXPECT_NE(ret, AITT_ERROR_NONE); + + ret = aitt_publish(handle, TEST_C_TOPIC, TEST_C_MSG, strlen(TEST_C_MSG)); + EXPECT_EQ(ret, AITT_ERROR_NOT_READY); + + ret = aitt_publish(nullptr, TEST_C_TOPIC, TEST_C_MSG, strlen(TEST_C_MSG)); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + ret = aitt_connect(handle, LOCAL_IP, 1883); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + ret = aitt_publish(handle, nullptr, TEST_C_MSG, strlen(TEST_C_MSG)); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + ret = aitt_publish(handle, TEST_C_TOPIC, nullptr, strlen(TEST_C_MSG)); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + aitt_disconnect(handle); + aitt_destroy(handle); +} + +TEST(AITT_C_INTERFACE, sub_N_Anytime) +{ + aitt_h handle = aitt_new("test10", LOCAL_IP); + aitt_sub_h sub_handle = nullptr; + ASSERT_NE(handle, nullptr); + + int ret = aitt_connect(handle, nullptr, 1883); + EXPECT_NE(ret, AITT_ERROR_NONE); + + ret = aitt_subscribe( + handle, TEST_C_TOPIC, [](aitt_msg_h, const void *, size_t, void *) {}, nullptr, + &sub_handle); + EXPECT_EQ(ret, AITT_ERROR_NOT_READY); + + ret = aitt_subscribe( + nullptr, TEST_C_TOPIC, [](aitt_msg_h, const void *, size_t, void *) {}, nullptr, + &sub_handle); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + ret = aitt_connect(handle, LOCAL_IP, 1883); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + ret = aitt_subscribe( + handle, nullptr, [](aitt_msg_h, const void *, size_t, void *) {}, nullptr, &sub_handle); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + ret = aitt_subscribe(handle, TEST_C_TOPIC, nullptr, nullptr, &sub_handle); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + aitt_disconnect(handle); + aitt_destroy(handle); +} + +#define reply_msg "hello reply message" +#define test_correlation "0001" + +TEST(AITT_C_INTERFACE, pub_with_reply_send_reply_P_Anytime) +{ + aitt_h handle = aitt_new("test11", LOCAL_IP); + ASSERT_NE(handle, nullptr); + + int ret = aitt_connect(handle, LOCAL_IP, 1883); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + GMainLoop *loop = g_main_loop_new(nullptr, FALSE); + aitt_sub_h sub_handle = nullptr; + ret = aitt_subscribe( + handle, TEST_C_TOPIC, + [](aitt_msg_h msg_handle, const void *msg, size_t msg_len, void *user_data) { + aitt_h handle = static_cast<aitt_h>(user_data); + std::string received_data((const char *)msg, msg_len); + EXPECT_STREQ(received_data.c_str(), TEST_C_MSG); + EXPECT_STREQ(aitt_msg_get_topic(msg_handle), TEST_C_TOPIC); + aitt_send_reply(handle, msg_handle, reply_msg, sizeof(reply_msg), true); + }, + handle, &sub_handle); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + ret = aitt_publish_with_reply( + handle, TEST_C_TOPIC, TEST_C_MSG, strlen(TEST_C_MSG), AITT_TYPE_MQTT, + AITT_QOS_AT_MOST_ONCE, test_correlation, + [](aitt_msg_h msg_handle, const void *msg, size_t msg_len, void *user_data) { + GMainLoop *loop = static_cast<GMainLoop *>(user_data); + std::string received_data((const char *)msg, msg_len); + EXPECT_STREQ(received_data.c_str(), reply_msg); + g_main_loop_quit(loop); + }, + loop); + + g_main_loop_run(loop); + g_main_loop_unref(loop); + + ret = aitt_disconnect(handle); + EXPECT_EQ(ret, AITT_ERROR_NONE); + + aitt_destroy(handle); +} + +TEST(AITT_C_INTERFACE, pub_with_reply_N_Anytime) +{ + aitt_h handle = aitt_new("test12", LOCAL_IP); + ASSERT_NE(handle, nullptr); + + int ret = aitt_connect(handle, LOCAL_IP, 1883); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + ret = aitt_publish_with_reply( + nullptr, TEST_C_TOPIC, TEST_C_MSG, strlen(TEST_C_MSG), AITT_TYPE_MQTT, + AITT_QOS_AT_MOST_ONCE, test_correlation, + [](aitt_msg_h msg_handle, const void *msg, size_t msg_len, void *user_data) {}, nullptr); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + ret = aitt_publish_with_reply( + handle, nullptr, TEST_C_MSG, strlen(TEST_C_MSG), AITT_TYPE_MQTT, AITT_QOS_AT_MOST_ONCE, + test_correlation, + [](aitt_msg_h msg_handle, const void *msg, size_t msg_len, void *user_data) {}, nullptr); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + ret = aitt_publish_with_reply( + handle, TEST_C_TOPIC, nullptr, 0, AITT_TYPE_MQTT, AITT_QOS_AT_MOST_ONCE, test_correlation, + [](aitt_msg_h msg_handle, const void *msg, size_t msg_len, void *user_data) {}, nullptr); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + ret = aitt_publish_with_reply(handle, TEST_C_TOPIC, TEST_C_MSG, strlen(TEST_C_MSG), + AITT_TYPE_MQTT, AITT_QOS_AT_MOST_ONCE, test_correlation, nullptr, nullptr); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); + + aitt_destroy(handle); +} + +TEST(AITT_C_INTERFACE, sub_unsub_P_Anytime) +{ + aitt_h handle = aitt_new("test13", LOCAL_IP); + ASSERT_NE(handle, nullptr); + + int ret = aitt_connect(handle, LOCAL_IP, 1883); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + static unsigned int sub_call_count = 0; + GMainLoop *loop = g_main_loop_new(nullptr, FALSE); + static aitt_sub_h sub_handle = nullptr; + ret = aitt_subscribe( + handle, TEST_C_TOPIC, + [](aitt_msg_h msg_handle, const void *msg, size_t msg_len, void *user_data) { + sub_call_count++; + }, + nullptr, &sub_handle); + ASSERT_EQ(ret, AITT_ERROR_NONE); + EXPECT_TRUE(sub_handle != nullptr); + + ret = aitt_publish(handle, TEST_C_TOPIC, TEST_C_MSG, strlen(TEST_C_MSG)); + ASSERT_EQ(ret, AITT_ERROR_NONE); + + g_timeout_add( + 1000, + [](gpointer data) -> gboolean { + aitt_h handle = static_cast<aitt_h>(data); + int ret = aitt_unsubscribe(handle, sub_handle); + EXPECT_EQ(ret, AITT_ERROR_NONE); + sub_handle = nullptr; + + ret = aitt_publish(handle, TEST_C_TOPIC, TEST_C_MSG, strlen(TEST_C_MSG)); + EXPECT_EQ(ret, AITT_ERROR_NONE); + return FALSE; + }, + handle); + + g_timeout_add( + 2000, + [](gpointer data) -> gboolean { + GMainLoop *loop = static_cast<GMainLoop *>(data); + EXPECT_EQ(sub_call_count, 1); + + if (sub_call_count == 1) { + g_main_loop_quit(loop); + return FALSE; + } + + return TRUE; + }, + loop); + + g_main_loop_run(loop); + g_main_loop_unref(loop); + + ret = aitt_disconnect(handle); + EXPECT_EQ(ret, AITT_ERROR_NONE); + + aitt_destroy(handle); +} + +TEST(AITT_C_INTERFACE, will_set_N_Anytime) +{ + int ret = aitt_will_set(nullptr, "test/will_topic", "test", 4, AITT_QOS_AT_MOST_ONCE, false); + EXPECT_EQ(ret, AITT_ERROR_INVALID_PARAMETER); +} diff --git a/tests/aitt_tests.h b/tests/aitt_tests.h new file mode 100644 index 0000000..f740007 --- /dev/null +++ b/tests/aitt_tests.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <glib.h> +#include <sys/time.h> + +#include "aitt_internal.h" + +#define LOCAL_IP "127.0.0.1" +#define TEST_C_TOPIC "test/topic_c" +#define TEST_C_MSG "test123456789" + +#define TEST_MSG "This is aitt test message" +#define TEST_MSG2 "This message is going to be delivered through a specified AittProtocol" +#define SLEEP_MS 1 + +class AittTests { + public: + void Init() + { + ready = false; + ready2 = false; + + timeval tv; + char buffer[256]; + gettimeofday(&tv, nullptr); + snprintf(buffer, sizeof(buffer), "UniqueID.%lX%lX", tv.tv_sec, tv.tv_usec); + clientId = buffer; + snprintf(buffer, sizeof(buffer), "TestTopic.%lX%lX", tv.tv_sec, tv.tv_usec); + testTopic = buffer; + mainLoop = g_main_loop_new(nullptr, FALSE); + } + + void Deinit() { g_main_loop_unref(mainLoop); } + + void ToggleReady() { ready = true; } + void ToggleReady2() { ready2 = true; } + static gboolean ReadyCheck(gpointer data) + { + AittTests *test = static_cast<AittTests *>(data); + + if (test->ready) { + g_main_loop_quit(test->mainLoop); + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; + } + + void IterateEventLoop(void) + { + g_main_loop_run(mainLoop); + DBG("Go forward"); + } + + void *subscribeHandle; + bool ready; + bool ready2; + + GMainLoop *mainLoop; + std::string clientId; + std::string testTopic; +}; diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt new file mode 100644 index 0000000..d8fe56d --- /dev/null +++ b/tools/CMakeLists.txt @@ -0,0 +1,7 @@ +SET(AITT_DISCOVERY_TOOL ${PROJECT_NAME}_discovery_viewer) + +########################################################################### +ADD_EXECUTABLE(${AITT_DISCOVERY_TOOL} discovery_viewer.cc FlexbufPrinter.cc) +TARGET_LINK_LIBRARIES(${AITT_DISCOVERY_TOOL} ${AITT_NEEDS_LIBRARIES}) + +INSTALL(TARGETS ${AITT_DISCOVERY_TOOL} DESTINATION ${AITT_TEST_BINDIR}) diff --git a/tools/FlexbufPrinter.cc b/tools/FlexbufPrinter.cc new file mode 100644 index 0000000..339a9cd --- /dev/null +++ b/tools/FlexbufPrinter.cc @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "FlexbufPrinter.h" + +#include <flatbuffers/flexbuffers.h> + +#include <iostream> + +#include "aitt_internal.h" + +FlexbufPrinter::FlexbufPrinter() : tab(0) +{ +} + +void FlexbufPrinter::PrettyPrint(const std::string &name, const uint8_t *data, int datalen) +{ + std::cout << name << std::endl; + + auto root = flexbuffers::GetRoot(data, datalen); + PrettyParsing(root, false); +} + +std::string FlexbufPrinter::PrettyTab(bool ignore) +{ + if (ignore) + return std::string(); + + std::stringstream ss; + for (int i = 0; i < tab; i++) + ss << '\t'; + + return ss.str(); +} + +void FlexbufPrinter::PrettyMap(const flexbuffers::Reference &data, bool inline_value) +{ + std::cout << PrettyTab(inline_value) << "{" << std::endl; + tab++; + + auto map = data.AsMap(); + auto keys = map.Keys(); + for (size_t i = 0; i < keys.size(); i++) { + std::cout << PrettyTab(false) << keys[i].AsKey() << " : "; + PrettyParsing(map[keys[i].AsKey()], true); + } + + tab--; + std::cout << PrettyTab(false) << "}" << std::endl; +} + +void FlexbufPrinter::PrettyVector(const flexbuffers::Reference &data, bool inline_value) +{ + auto vec = data.AsVector(); + std::cout << PrettyTab(inline_value) << "[" << std::endl; + tab++; + + for (size_t i = 0; i < vec.size(); i++) + PrettyParsing(vec[i], false); + + tab--; + std::cout << PrettyTab(false) << "]" << std::endl; +} + +void FlexbufPrinter::PrettyBlob(const flexbuffers::Reference &data, bool inline_value) +{ + auto blob = data.AsBlob(); + auto root = flexbuffers::GetRoot(static_cast<const uint8_t *>(blob.data()), blob.size()); + + PrettyParsing(root, true); +} + +void FlexbufPrinter::PrettyParsing(const flexbuffers::Reference &data, bool inline_value) +{ + using namespace flexbuffers; + switch (data.GetType()) { + case FBT_NULL: + ERR("Unknown Type : case FBT_NULL"); + break; + case FBT_KEY: + ERR("Unknown Type : case FBT_KEY"); + break; + case FBT_MAP: + PrettyMap(data, inline_value); + break; + case FBT_BLOB: + PrettyBlob(data, inline_value); + break; + case FBT_VECTOR: + PrettyVector(data, inline_value); + break; + case FBT_INT: + case FBT_INDIRECT_INT: + case FBT_UINT: + case FBT_INDIRECT_UINT: + case FBT_FLOAT: + case FBT_INDIRECT_FLOAT: + case FBT_STRING: + case FBT_VECTOR_INT: + case FBT_VECTOR_UINT: + case FBT_VECTOR_FLOAT: + case FBT_VECTOR_KEY: + case FBT_VECTOR_STRING_DEPRECATED: + case FBT_VECTOR_INT2: + case FBT_VECTOR_UINT2: + case FBT_VECTOR_FLOAT2: + case FBT_VECTOR_INT3: + case FBT_VECTOR_UINT3: + case FBT_VECTOR_FLOAT3: + case FBT_VECTOR_INT4: + case FBT_VECTOR_UINT4: + case FBT_VECTOR_FLOAT4: + case FBT_VECTOR_BOOL: + case FBT_BOOL: + std::cout << PrettyTab(inline_value) << data.ToString() << std::endl; + break; + default: + ERR("Unknown Type(%d)", data.GetType()); + break; + } +} diff --git a/tools/FlexbufPrinter.h b/tools/FlexbufPrinter.h new file mode 100644 index 0000000..46bfe51 --- /dev/null +++ b/tools/FlexbufPrinter.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include <flatbuffers/flexbuffers.h> + +class FlexbufPrinter { + public: + FlexbufPrinter(); + + void PrettyPrint(const std::string &name, const uint8_t *data, int datalen); + + private: + std::string PrettyTab(bool ignore); + void PrettyMap(const flexbuffers::Reference &data, bool inline_value); + void PrettyVector(const flexbuffers::Reference &data, bool inline_value); + void PrettyBlob(const flexbuffers::Reference &data, bool inline_value); + void PrettyParsing(const flexbuffers::Reference &data, bool inline_value); + + int tab; +}; diff --git a/tools/discovery_viewer.cc b/tools/discovery_viewer.cc new file mode 100644 index 0000000..a3c2c76 --- /dev/null +++ b/tools/discovery_viewer.cc @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2022 Samsung Electronics Co., Ltd All Rights Reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include <argp.h> +#include <flatbuffers/flexbuffers.h> +#include <mosquitto.h> + +#include "FlexbufPrinter.h" +#include "aitt_internal.h" + +struct ParsingData { + ParsingData() : clean(false), broker_ip("127.0.0.1") {} + bool clean; + std::string broker_ip; +}; +const char *argp_program_version = "aitt-discovery-viewer 1.0"; + +static char argp_doc[] = + "AITT Discovery topic Viewer" + "\v" + "Tizen: <http://www.tizen.org>"; + +/* The options we understand. */ +static struct argp_option options[] = {{"broker", 'b', "IP", 0, "broker ip address"}, + {"clean", 'c', 0, OPTION_ARG_OPTIONAL, "clean discovery topic"}, {0}}; + +static error_t parse_opt(int key, char *arg, struct argp_state *state) +{ + struct ParsingData *args = (struct ParsingData *)state->input; + + switch (key) { + case 'b': + args->broker_ip = arg; + break; + case 'c': + args->clean = true; + break; + case ARGP_KEY_NO_ARGS: + // argp_usage(state); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} + +class MQTTHandler { + public: + MQTTHandler() + { + int ret = mosquitto_lib_init(); + if (ret != MOSQ_ERR_SUCCESS) + ERR("mosquitto_lib_init() Fail(%s)", mosquitto_strerror(ret)); + + std::string id = std::to_string(rand() % 100); + handle = mosquitto_new(id.c_str(), true, nullptr); + if (handle == nullptr) { + ERR("mosquitto_new() Fail"); + mosquitto_lib_cleanup(); + return; + } + mosquitto_int_option(handle, MOSQ_OPT_PROTOCOL_VERSION, MQTT_PROTOCOL_V5); + + mosquitto_message_v5_callback_set(handle, + [](mosquitto *handle, void *user_data, const mosquitto_message *msg, + const mosquitto_property *props) { + std::string topic = msg->topic; + size_t end = topic.find("/", DISCOVERY_TOPIC_BASE.length()); + std::string clientId = topic.substr(DISCOVERY_TOPIC_BASE.length(), end); + FlexbufPrinter printer; + if (msg->payloadlen) + printer.PrettyPrint(clientId, static_cast<const uint8_t *>(msg->payload), + msg->payloadlen); + }); + + ret = mosquitto_loop_start(handle); + if (ret != MOSQ_ERR_SUCCESS) + ERR("mosquitto_loop_start() Fail(%s)", mosquitto_strerror(ret)); + } + + void Connect(const std::string &broker_ip) + { + int ret = mosquitto_connect(handle, broker_ip.c_str(), 1883, 60); + if (ret != MOSQ_ERR_SUCCESS) + ERR("mosquitto_connect() Fail(%s)", mosquitto_strerror(ret)); + + int mid = 0; + ret = mosquitto_subscribe(handle, &mid, (DISCOVERY_TOPIC_BASE + "+").c_str(), 0); + if (ret != MOSQ_ERR_SUCCESS) + ERR("mosquitto_subscribe() Fail(%s)", mosquitto_strerror(ret)); + } + + private: + mosquitto *handle; +}; + +int main(int argc, char **argv) +{ + struct ParsingData arg; + struct argp argp_conf = {options, parse_opt, 0, argp_doc}; + + argp_parse(&argp_conf, argc, argv, 0, 0, &arg); + + if (arg.clean) { + ERR("Not Supported"); + return -1; + } + + MQTTHandler mqtt; + mqtt.Connect(arg.broker_ip); + + while (1) { + sleep(10); + } + + return 0; +} |