├── .clang-format ├── .clang-tidy ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake ├── clang-tidy.cmake ├── clear_variable.cmake ├── gcc_analyze.cmake └── sanitizer.cmake ├── src ├── CMakeLists.txt ├── bt │ ├── BLEDevice.cpp │ ├── BLEHelper.cpp │ ├── ByteEncDecoder.cpp │ └── CMakeLists.txt ├── date │ └── CMakeLists.txt ├── include │ ├── CMakeLists.txt │ ├── bt │ │ ├── BLEDevice.hpp │ │ ├── BLEHelper.hpp │ │ └── ByteEncDecoder.hpp │ ├── date │ │ └── date.hpp │ ├── io │ │ └── csv.hpp │ ├── jutta_bt_proto │ │ ├── CoffeeMaker.hpp │ │ ├── CoffeeMakerLoader.hpp │ │ └── Utils.hpp │ └── logger │ │ └── Logger.hpp ├── io │ └── CMakeLists.txt ├── jutta_bt_proto │ ├── CMakeLists.txt │ ├── CoffeeMaker.cpp │ ├── CoffeeMakerLoader.cpp │ └── Utils.cpp ├── logger │ ├── CMakeLists.txt │ └── Logger.cpp ├── resources │ ├── CMakeLists.txt │ └── extract_apk.sh └── test_exec │ ├── CMakeLists.txt │ ├── test_exec.cpp │ └── test_scanner.cpp └── tests ├── CMakeLists.txt └── Tests.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | AccessModifierOffset: -3 4 | AlignAfterOpenBracket: Align 5 | AlignConsecutiveAssignments: false 6 | AlignConsecutiveDeclarations: false 7 | AlignEscapedNewlines: Left 8 | AlignOperands: false 9 | AlignTrailingComments: false 10 | AllowAllParametersOfDeclarationOnNextLine: true 11 | AllowShortBlocksOnASingleLine: true 12 | AllowShortCaseLabelsOnASingleLine: true 13 | AllowShortFunctionsOnASingleLine: All 14 | AllowShortIfStatementsOnASingleLine: true 15 | AllowShortLoopsOnASingleLine: true 16 | AlwaysBreakAfterDefinitionReturnType: None 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: false 19 | AlwaysBreakTemplateDeclarations: MultiLine 20 | BinPackArguments: true 21 | BinPackParameters: true 22 | BraceWrapping: 23 | AfterClass: false 24 | AfterControlStatement: false 25 | AfterEnum: false 26 | AfterFunction: false 27 | AfterNamespace: false 28 | AfterObjCDeclaration: false 29 | AfterStruct: false 30 | AfterUnion: false 31 | AfterExternBlock: false 32 | BeforeCatch: false 33 | BeforeElse: false 34 | IndentBraces: false 35 | SplitEmptyFunction: true 36 | SplitEmptyRecord: true 37 | SplitEmptyNamespace: true 38 | BreakBeforeBinaryOperators: None 39 | BreakBeforeBraces: Attach 40 | BreakBeforeInheritanceComma: false 41 | BreakInheritanceList: BeforeColon 42 | BreakBeforeTernaryOperators: false 43 | BreakConstructorInitializersBeforeComma: false 44 | BreakConstructorInitializers: BeforeColon 45 | BreakAfterJavaFieldAnnotations: false 46 | BreakStringLiterals: true 47 | ColumnLimit: 0 48 | CommentPragmas: '(LCOV|unreachable)' 49 | CompactNamespaces: true 50 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 51 | ConstructorInitializerIndentWidth: 4 52 | ContinuationIndentWidth: 4 53 | Cpp11BracedListStyle: true 54 | DerivePointerAlignment: false 55 | DisableFormat: false 56 | ExperimentalAutoDetectBinPacking: true 57 | FixNamespaceComments: true 58 | ForEachMacros: 59 | - foreach 60 | - Q_FOREACH 61 | - BOOST_FOREACH 62 | IncludeBlocks: Preserve 63 | IncludeCategories: 64 | - Regex: '^"' 65 | Priority: 1 66 | - Regex: '^' 67 | Priority: 2 68 | - Regex: '^<.*\.h>' 69 | Priority: 4 70 | - Regex: '^<' 71 | Priority: 3 72 | - Regex: '.\*' 73 | Priority: 5 74 | IncludeIsMainRegex: '(Test)?$' 75 | IndentCaseLabels: true 76 | IndentPPDirectives: None 77 | IndentWidth: 4 78 | IndentWrappedFunctionNames: false 79 | JavaScriptQuotes: Leave 80 | JavaScriptWrapImports: true 81 | KeepEmptyLinesAtTheStartOfBlocks: false 82 | MacroBlockBegin: PROXY_BEGIN 83 | MacroBlockEnd: PROXY_END 84 | MaxEmptyLinesToKeep: 1 85 | NamespaceIndentation: None 86 | ObjCBinPackProtocolList: Auto 87 | ObjCBlockIndentWidth: 2 88 | ObjCSpaceAfterProperty: false 89 | ObjCSpaceBeforeProtocolList: true 90 | PenaltyBreakAssignment: 2 91 | PenaltyBreakBeforeFirstCallParameter: 50 92 | PenaltyBreakComment: 50 93 | PenaltyBreakFirstLessLess: 50 94 | PenaltyBreakString: 50 95 | PenaltyBreakTemplateDeclaration: 10 96 | PenaltyExcessCharacter: 1000000 97 | PenaltyReturnTypeOnItsOwnLine: 50 98 | PointerAlignment: Left 99 | ReflowComments: false 100 | SortIncludes: true 101 | SortUsingDeclarations: true 102 | SpaceAfterCStyleCast: true 103 | SpaceAfterTemplateKeyword: true 104 | SpaceBeforeAssignmentOperators: true 105 | SpaceBeforeCpp11BracedList: false 106 | SpaceBeforeCtorInitializerColon: true 107 | SpaceBeforeInheritanceColon: true 108 | SpaceBeforeParens: ControlStatements 109 | SpaceBeforeRangeBasedForLoopColon: true 110 | SpaceInEmptyParentheses: false 111 | SpacesBeforeTrailingComments: 2 112 | SpacesInAngles: false 113 | SpacesInContainerLiterals: false 114 | SpacesInCStyleCastParentheses: false 115 | SpacesInParentheses: false 116 | SpacesInSquareBrackets: false 117 | Standard: Cpp11 118 | TabWidth: 4 119 | UseTab: Never 120 | ... 121 | 122 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: '*, 3 | -cppcoreguidelines-pro-type-static-cast-downcast, 4 | -fuchsia-default-arguments-calls, 5 | -fuchsia-default-arguments, 6 | -fuchsia-default-arguments-declarations, 7 | -fuchsia-overloaded-operator, 8 | -fuchsia-statically-constructed-objects, 9 | -hicpp-use-auto, 10 | -modernize-use-auto, 11 | -modernize-use-trailing-return-type, 12 | -readability-implicit-bool-conversion, 13 | -readability-const-return-type, 14 | -google-runtime-references, 15 | -misc-non-private-member-variables-in-classes, 16 | -cppcoreguidelines-pro-bounds-array-to-pointer-decay, 17 | -hicpp-no-array-decay, 18 | -llvm-include-order, 19 | -cppcoreguidelines-non-private-member-variables-in-classes, 20 | -cppcoreguidelines-pro-type-vararg, 21 | -hicpp-vararg, 22 | -cppcoreguidelines-avoid-magic-numbers, 23 | -readability-magic-numbers, 24 | -cppcoreguidelines-owning-memory, 25 | -llvmlibc-implementation-in-namespace, 26 | -llvmlibc-callee-namespace, 27 | -llvmlibc-restrict-system-libc-headers, 28 | -fuchsia-multiple-inheritance, 29 | -hicpp-signed-bitwise, 30 | -misc-no-recursion, 31 | -google-readability-todo, 32 | -cppcoreguidelines-pro-bounds-constant-array-index, 33 | -bugprone-easily-swappable-parameters, 34 | -altera-id-dependent-backward-branch, 35 | -altera-unroll-loops, 36 | -readability-identifier-length 37 | ' 38 | WarningsAsErrors: '*' 39 | HeaderFilterRegex: 'src/*.hpp' 40 | FormatStyle: file 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Misc 35 | logs/ 36 | build/ 37 | 38 | # VScode clangd stuff 39 | .clangd/ 40 | compile_commands.json 41 | .cache/ 42 | .vscode/ 43 | src/resources/* 44 | !src/resources/extract_apk.sh 45 | !src/resources/CMakeLists.txt -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | set(PROJECT_DESCRIPTION "An unofficial Bluetooth protocol library implementation to control JURA coffee makers.") 4 | 5 | project("Jutta Bluteooth Protocol Library" 6 | VERSION 0.0.0 7 | DESCRIPTION "${PROJECT_DESCRIPTION}" 8 | HOMEPAGE_URL "https://github.com/Jutta-Proto/protocol-bt-cpp") 9 | set(VERSION_NAME "dev") 10 | 11 | set(CMAKE_CXX_STANDARD 20) 12 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 13 | 14 | macro(jutta_bt_proto_option OPTION_NAME OPTION_TEXT OPTION_DEFAULT) 15 | option(${OPTION_NAME} ${OPTION_TEXT} ${OPTION_DEFAULT}) 16 | if(DEFINED ENV{${OPTION_NAME}}) 17 | # Allow setting the option through an environment variable 18 | set(${OPTION_NAME} $ENV{${OPTION_NAME}}) 19 | endif() 20 | if(${OPTION_NAME}) 21 | add_definitions(-D${OPTION_NAME}) 22 | endif() 23 | message(STATUS " ${OPTION_NAME}: ${${OPTION_NAME}}") 24 | endmacro() 25 | 26 | message(STATUS "C++ Jutta Bluetooth Protocol Library Options") 27 | message(STATUS "=======================================================") 28 | jutta_bt_proto_option(JUTTA_BT_PROTO_BUILD_TEST_EXEC "Set to ON to build test executable." OFF) 29 | jutta_bt_proto_option(JUTTA_BT_PROTO_BUILD_TESTS "Set to ON to build tests." OFF) 30 | jutta_bt_proto_option(JUTTA_BT_PROTO_STATIC_ANALYZE "Set to ON to enable the GCC 10 static analysis. If enabled, JUTTA_BT_PROTO_ENABLE_LINTING has to be disabled." OFF) 31 | jutta_bt_proto_option(JUTTA_BT_PROTO_ENABLE_LINTING "Set to ON to enable clang linting. If enabled, JUTTA_BT_PROTO_STATIC_ANALYZE has to be disabled." OFF) 32 | message(STATUS "=======================================================") 33 | 34 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_BINARY_DIR}) 35 | list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) 36 | list(APPEND CMAKE_PREFIX_PATH ${CMAKE_CURRENT_BINARY_DIR}) 37 | 38 | include(sanitizer) 39 | include(gcc_analyze) 40 | include(clear_variable) 41 | include(FetchContent) 42 | 43 | if(${JUTTA_BT_PROTO_ENABLE_LINTING}) 44 | message(STATUS "Enabling linting") 45 | include(clang-tidy) 46 | else() 47 | message(STATUS "Linting is disabled") 48 | endif() 49 | 50 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic") 51 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") 52 | if(NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake") 53 | message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan") 54 | file(DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake-conan/0.18.1/conan.cmake" 55 | "${CMAKE_BINARY_DIR}/conan.cmake" 56 | TLS_VERIFY ON) 57 | endif() 58 | 59 | include(${CMAKE_BINARY_DIR}/conan.cmake) 60 | 61 | set(CONAN_CONFIGS"Release;Debug;RelWithDebInfo") 62 | if(NOT CMAKE_BUILD_TYPE IN_LIST CONAN_CONFIGS) 63 | set(CONAN_BUILD_TYPE "Debug") 64 | else() 65 | set(CONAN_BUILD_TYPE ${CMAKE_BUILD_TYPE}) 66 | endif() 67 | 68 | conan_cmake_configure(REQUIRES catch2/2.13.8 69 | spdlog/1.10.0 70 | tinyxml2/9.0.0 71 | GENERATORS cmake_find_package 72 | BUILD missing) 73 | conan_cmake_autodetect(settings) 74 | conan_cmake_install(PATH_OR_REFERENCE . 75 | BUILD missing 76 | REMOTE conancenter 77 | SETTINGS ${settings}) 78 | 79 | find_package(spdlog REQUIRED) 80 | find_package(tinyxml2 REQUIRED) 81 | 82 | # Disable linting for fetch content projects 83 | clear_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) 84 | 85 | set(GATTLIB_BUILD_DOCS OFF) 86 | FetchContent_Declare(gattlib 87 | GIT_REPOSITORY https://github.com/labapart/gattlib.git 88 | GIT_TAG bb90b55ddec9c1817729a507ca096c2a514d8dc3) # State on master from the 03.12.2021 89 | FetchContent_MakeAvailable(gattlib) 90 | 91 | FetchContent_Declare(eventpp 92 | GIT_REPOSITORY https://github.com/wqking/eventpp.git 93 | GIT_TAG cf1ba5689d51d9aafeedfc28788f08690d8b0f40) # State on master from the 18.01.2022 94 | FetchContent_MakeAvailable(eventpp) 95 | 96 | restore_variable(DESTINATION CMAKE_CXX_CLANG_TIDY BACKUP CMAKE_CXX_CLANG_TIDY_BKP) 97 | 98 | include_directories(${CMAKE_SOURCE_DIR}/src) 99 | 100 | add_subdirectory(src) 101 | 102 | # Testing 103 | if(${JUTTA_BT_PROTO_BUILD_TESTS}) 104 | message(STATUS "Testing is enabled") 105 | enable_testing() 106 | add_subdirectory(tests) 107 | else() 108 | message(STATUS "Testing is disabled") 109 | endif() 110 | 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JURA Bluetooth Protocol 2 | `C++` JURA Bluetooth protocol implementation for controlling a JURA coffee maker over a Bluetooth connection. 3 | 4 | For a device to be able to connect to an JURA coffee maker via Bluetooth, usually a [Smart Control](https://uk.jura.com/en/homeproducts/accessories/SmartConnect-Main-72167) dongle is required. 5 | 6 | ![Smart Control dongle](https://user-images.githubusercontent.com/11741404/135412711-23fd1946-77bc-45db-8795-f30da4421578.png) 7 | 8 | Most of this was done by [Reverse Engineering](#reverse-engineering) the Android APK. 9 | 10 | ## Table of Contents 11 | 1. [Protocol](#protocol) 12 | 2. [Bluetooth Characteristics](#bluetooth-characteristics) 13 | 3. [Brewing Coffee](#brewing-coffee) 14 | 4. [Building](#building) 15 | 5. [Reverse Engineering](#reverse-engineering) 16 | 6. [License and Copyright Notice](#license-and-copyright-notice) 17 | 18 | ## Protocol 19 | 20 | ### General 21 | There are several steps of obfuscation being done by the JURA coffee maker to prevent others from reading the bare protocol or sending arbitrary commands to it. 22 | 23 | #### Connecting to an JURA coffee maker 24 | To connect to a JURA coffee maker via Bluetooth, a [Smart Control](https://uk.jura.com/en/homeproducts/accessories/SmartConnect-Main-72167) dongle is required. 25 | This dongle has to be plugged into the coffee maker. 26 | Once this has been done, we can connect to the `TT214H BlueFrog` device via Bluetooth. 27 | 28 | #### Obtaining a key 29 | Once connected, we have to obtain the key used for decoding and encoding the data to be sent. 30 | This is done by analyzing the `advertisement data` or, more concretely, the `manufacturer data` found when scanning for devices. 31 | Here the `manufacturer data` is structured as follows: 32 | ``` 33 | 0 1 2 3 34 | 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 35 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 36 | | key | bfMajVer | bfMinVer | unused | 37 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 38 | | articleNumber | machineNumber | 39 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 40 | | serialNumber | machineProdDate | 41 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 42 | | machineProdDateUCHI | unused | statusBits | 43 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 44 | 45 | Optional extended data starting at byte 28: 46 | 0 1 2 3 47 | 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 48 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 49 | | | 50 | + bfVerStr + 51 | | | 52 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 53 | | | 54 | + + 55 | | | 56 | + coffeeMachineVerStr + 57 | | | 58 | + + 59 | | | 60 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 61 | | | lastConnectedTabledID | 62 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 63 | | | 64 | +-+-+-+-+-+-+-+-+ 65 | ``` 66 | 67 | * `key`: 8 Bit. Starts at byte 0. The key is used for decoding and encoding data. 68 | * `bfMajVer`: 8 Bit. Starts at byte 1. BlueFrog major version number. 69 | * `bfMinVer`: 8 Bit. Starts at byte 2. BlueFrog minor version number. 70 | * `articleNumber`: 16 Bit. Starts at byte 4. Article number in little-endian. 71 | * `machineNumber`: 16 Bit. Starts at byte 6. Machine number in little-endian. 72 | * `serialNumber`: 16 Bit. Starts at byte 8. Serial number in little-endian. 73 | * `machineProdDate`: 16 Bit. Starts at byte 10. Machine production date in a special format. 74 | * `machineProdDateUCHI`: 16 Bit. Starts at byte 12. **Probably** the steam plate production date in a special format. 75 | * `statusBits`: 8 Bit. Starts at byte 15. Some initial status bits: 4 - supports incasso, 6 - supports master pin, 7 - supports reset 76 | * `bfVerStr`: 8 Byte. Starts at byte 27 BlueFrog ASCII version string (optional). 77 | * `coffeeMachineVerStr`: 17 Byte. Starts at byte 35. Coffee maker ASCII version string (optional). 78 | * `lastConnectedTabledID`: 32 Bit. Starts at byte 51. An int representing the last connection ID. 79 | 80 | #### Parsing dates 81 | The following is used to parse the `machineProdDate` and `machineProdDateUCHI` dates. 82 | ```c++ 83 | void to_ymd(const std::vector& data, size_t offset) { 84 | uint16_t date = to_uint16_t_little_endian(data, offset); // Convert two bytes (little-endian) to an unsigned short 85 | uint16_t year = ((date & 0xFE00) >> 9) + 1990; 86 | uint16_t month = (date & 0x1E0) >> 5; 87 | uint16_t day = date & 1F; 88 | } 89 | ``` 90 | 91 | #### Decoding data 92 | Once we have obtained the `key` described above, we can start decoding read data from the Bluetooth characteristics. 93 | Have a look at [this](https://github.com/Jutta-Proto/protocol-bt-cpp/blob/0adb1ea802df13aac03262033706755f431f93b6/src/bt/ByteEncDecoder.cpp#L33-L47) implementation of the `encDecBytes`, which takes our obtained key and some data to decode or encode. 94 | Once decoded successfully, the first byte of the resulting data has to be the `key`, otherwise the decoding failed. 95 | 96 | #### Encoding data 97 | When encoding data that should be written to Bluetooth characteristics, we again need the `key` we have obtained described above. 98 | First we have to make sure we set the first byte of your data to the `key` and then feed it to [`encDecBytes`](https://github.com/Jutta-Proto/protocol-bt-cpp/blob/0adb1ea802df13aac03262033706755f431f93b6/src/bt/ByteEncDecoder.cpp#L33-L47), as we have done for decoding. 99 | 100 | ### Heartbeat 101 | The coffee maker stays initially connected for 20 seconds. After that, it disconnects. 102 | To prevent this, we have to send at least every 10 seconds a heartbeat to it. 103 | The heartbeat is `0x007F80` encoded and then sent to the `P Mode` Characteristic `5a401529-ab2e-2548-c435-08c300000710`. 104 | For example, if the key is `0x2A`, the encoded data sent should be `0x77656d` (without the `0x` ;) ). 105 | Keep in mind, we have to set byte zero of our data that should be encoded to the key: `0x007F80` -> `0x2A7F80` -> `0x77656d` 106 | 107 | ## Bluetooth Characteristics 108 | 109 | ## Overview 110 | Here is an overview of all the known characteristics and services exposed by the coffee maker and some additional information in case we have found out how to use them. 111 | 112 | | Name | Services | Notes | 113 | | --- | --- | --- | 114 | | Default | `5a401523-ab2e-2548-c435-08c300000710` | Default service containing all relevant characteristics. | 115 | | UART | `5a401623-ab2e-2548-c435-08c300000710` | Contains a TX and RX UART characteristic. | 116 | 117 | | Name | Characteristic | Encoded | 118 | | --- | --- | --- | 119 | | About Machine | `5A401531-AB2E-2548-C435-08C300000710` | `false` | 120 | | Machine Status | `5a401524-ab2e-2548-c435-08c300000710` | `true` | 121 | | Barista Mode | `5a401530-ab2e-2548-c435-08c300000710` | `true` | 122 | | Product Progress | `5a401527-ab2e-2548-c435-08c300000710` | `true` | 123 | | P Mode | `5a401529-ab2e-2548-c435-08c300000710` | `true` | 124 | | P Mode Read | `5a401538-ab2e-2548-c435-08c300000710` | `UNKNOWN` | 125 | | Start Product | `5a401525-ab2e-2548-c435-08c300000710` | `true` | 126 | | Statistics Command | `5A401533-ab2e-2548-c435-08c300000710` | `true` | 127 | | Statistics Data | `5A401534-ab2e-2548-c435-08c300000710` | `UNKNOWN` | 128 | | Update Product Statistics | `5a401528-ab2e-2548-c435-08c300000710` | `UNKNOWN` | 129 | | UART TX | `5a401624-ab2e-2548-c435-08c300000710` | `true` | 130 | | UART RX | `5a401625-ab2e-2548-c435-08c300000710` | `true` | 131 | 132 | ### About Machine 133 | * `5A401531-AB2E-2548-C435-08C300000710` 134 | * Encoded: `false` 135 | 136 | This characteristic can only be read and provides general information about the coffee maker, like the `bfVerStr`` (8 byte, starts at byte 27) and the `coffeeMachineVerStr` (17 byte, starts at byte 35). 137 | 138 | ### Machine Status 139 | * `5a401524-ab2e-2548-c435-08c300000710` 140 | * Encoded: `true` 141 | 142 | When reading from this characteristic, the received data has to be decoded. Once decoded, the first byte has to be the `key` used for decoding. Otherwise, something went wrong. 143 | Starting from byte 1, the data represents status bits for the coffee maker. 144 | For example, bit 0 is set in case the water tray is missing and bit 1 of the first byte in case there is not enough water. 145 | For an exact mapping of bits to their action, we need the machine files found, for example, inside the Android app. 146 | More about this here: [Reverse Engineering](#reverse-engineering) 147 | 148 | ### P Mode 149 | * `5a401529-ab2e-2548-c435-08c300000710` 150 | * Encoded: `true` 151 | To begin with: I don't know what the "P" stands for. 152 | Used for sending the heartbeat to the coffee maker to prevent it from disconnecting. 153 | 154 | ### Start Product 155 | * `5a401525-ab2e-2548-c435-08c300000710` 156 | * Encoded: `true` 157 | Used to start preparing products. 158 | How to brew coffee can be found here: [Brewing Coffee](#brewing-coffee) 159 | 160 | ### Barista Mode 161 | * `5a401530-ab2e-2548-c435-08c300000710` 162 | * Encoded: `true` 163 | 164 | #### Writing 165 | Used for locking and unlocking the coffee maker screen and all its buttons. 166 | This could be used in a way where users have to authenticate first via some external service (e.g., an RFID or NFC card). 167 | The coffee maker would always be locked and the only way to create a cup of coffee was by sending commands via Bluetooth. 168 | 169 | **Locking** 170 | 171 | Write `0x0001` to this characteristic to lock the coffee maker. 172 | Encode this message like all other messages, but **do not** override the first byte at the end with the key. 173 | 174 | Here is an example: 175 | 176 | `0x0001` gets encoded to `0x77E0` using the key `0x2A` and then sent to the characteristic. 177 | 178 | **Unlocking** 179 | 180 | Write `0x0000` to this characteristic to lock the coffee maker. 181 | Encode this message like all other messages, but **do not** override the first byte at the end with the key. 182 | 183 | Here is an example: 184 | 185 | `0x0000` gets encoded to `0x77E1` using the key `0x2A` and then sent to the characteristic. 186 | 187 | ### UART TX 188 | * `5a401624-ab2e-2548-c435-08c300000710` 189 | * Encoded: `UNKNOWN` 190 | 191 | Probably exposes a raw TX interface for interacting directly with the coffee maker. 192 | 193 | ### UART RX 194 | * `5a401625-ab2e-2548-c435-08c300000710` 195 | * Encoded: `UNKNOWN` 196 | 197 | Probably exposes a raw RX interface for interacting directly with the coffee maker. 198 | 199 | ### Statistics Command 200 | * `5A401533-ab2e-2548-c435-08c300000710` 201 | * Encoded: `true` 202 | 203 | #### Writing 204 | Allows requesting statistics like product counts and maintenance data from the coffee maker. 205 | A command sent to this characteristic is built as follows and consists of the following 5 bytes (hexadecimal): 206 | ``` 207 | 00 0001 FFFF 208 | ``` 209 | 210 | * `0x00` The first by has to be set to 0. 211 | * `0x0001` To request the overall product statistics. To get the daily counter use `0x0010`. 212 | * `0xFFFF` Defines the products we want to retrieve statistics for. All bits set to one forces data for all products. Selecting only specific products is done as follows: 213 | 214 | ```c++ 215 | void get_prod_stat_bits() const { 216 | std::array bArr{0}; 217 | 218 | for (const Product& p : joe->products) { 219 | size_t code = p.code_to_size_t(); 220 | 221 | code /= 4; 222 | size_t arrOffset = code / 8; 223 | assert(arrOffset < bArr.size()); 224 | bArr[arrOffset] = (1 << (code % 8)) | (bArr[arrOffset] & 0xFF); 225 | } 226 | 227 | // The resulting bytes are now inside bArr. 228 | } 229 | ``` 230 | 231 | #### Reading 232 | Once data has been written to the characteristic, we can read from it after a delay of 1200 ms. 233 | The command indicates success when the value read does not start with `0x0E`. 234 | For example: 235 | ``` 236 | 0x0EA2A2A2 -> We wrote an invalid command to the characteristic. 237 | 0xA200A2A2 -> Success, we now can read all statistics from the "Statistics Data" characteristic. 238 | 0x4200A2A2 -> Success, we now can read all statistics from the "Statistics Data" characteristic. 239 | ``` 240 | 241 | ### Statistics Data 242 | * `5A401534-ab2e-2548-c435-08c300000710` 243 | * Encoded: `true` 244 | 245 | This characteristic contains the statistics requested by sending a request to the `Statistics Command` characteristic. 246 | Such a response could look as follows (hex): 247 | ``` 248 | 00014E00000000002700009800000A00FFFF00000300FFFF00000900FFFF00FFFF00FFFF00FFFF00006700FFFF00FFFF00FFFF00000000000200000000FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00FFFF00000000000000000000000000FFFF00FFFF00FFFF00FFFF00000000000000000000000000FFFF00FFFF00FFFF00FFFF00000000000000000000000000FFFF00FFFF00FFFF00FFFF0000000000000000000000000000000000000000000000000000000000000000 249 | ``` 250 | 251 | It is split into multiple parts. Each part consists of six hex chars (3 bytes). 252 | ``` 253 | 00014E 000000 000027 000098 00000A 00FFFF 000003 00FFFF 000009 00FFFF... 254 | ``` 255 | Each block describes the counter for a different product, except the first block. 256 | The first block (`0x00014E` in this case) represents the total product count. 257 | In this example, the coffee maker has produced `0x14E` (or 334 in decimal) products. 258 | 259 | The offset of a product statistic is calculated by the product code found inside the machine file for the coffee maker. 260 | For example, inside the `EF532V2.xml` file, we find the following information: ` 351 | License 352 | 353 | ``` 354 | Boost Software License - Version 1.0 - August 17th, 2003 355 | 356 | Permission is hereby granted, free of charge, to any person or organization 357 | obtaining a copy of the software and accompanying documentation covered by 358 | this license (the "Software") to use, reproduce, display, distribute, 359 | execute, and transmit the Software, and to prepare derivative works of the 360 | Software, and to permit third-parties to whom the Software is furnished to 361 | do so, all subject to the following: 362 | 363 | The copyright notices in the Software and this entire statement, including 364 | the above license grant, this restriction and the following disclaimer, 365 | must be included in all copies of the Software, in whole or in part, and 366 | all derivative works of the Software, unless such copies or derivative 367 | works are solely in the form of machine-executable object code generated by 368 | a source language processor. 369 | 370 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 371 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 372 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 373 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 374 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 375 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 376 | DEALINGS IN THE SOFTWARE. 377 | ``` 378 | 379 | 380 | ### spdlog (1.10.0) 381 | Very fast, header-only/compiled, C++ logging library. 382 | Source: https://github.com/gabime/spdlog 383 | 384 |
385 | License 386 | 387 | ``` 388 | The MIT License (MIT) 389 | 390 | Copyright (c) 2016 Gabi Melman. 391 | 392 | Permission is hereby granted, free of charge, to any person obtaining a copy 393 | of this software and associated documentation files (the "Software"), to deal 394 | in the Software without restriction, including without limitation the rights 395 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 396 | copies of the Software, and to permit persons to whom the Software is 397 | furnished to do so, subject to the following conditions: 398 | 399 | The above copyright notice and this permission notice shall be included in 400 | all copies or substantial portions of the Software. 401 | 402 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 403 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 404 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 405 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 406 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 407 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 408 | THE SOFTWARE. 409 | 410 | -- NOTE: Third party dependency used by this software -- 411 | This software depends on the fmt lib (MIT License), 412 | and users must comply to its license: https://github.com/fmtlib/fmt/blob/master/LICENSE.rst 413 | ``` 414 |
415 | 416 | ### Fast C++ CSV Parser 417 | Small, easy-to-use and fast header-only library for reading comma separated value (CSV) files. 418 | Source: https://github.com/ben-strasser/fast-cpp-csv-parser 419 | 420 |
421 | License 422 | 423 | ``` 424 | Copyright (c) 2015, ben-strasser 425 | All rights reserved. 426 | 427 | Redistribution and use in source and binary forms, with or without 428 | modification, are permitted provided that the following conditions are met: 429 | 430 | * Redistributions of source code must retain the above copyright notice, this 431 | list of conditions and the following disclaimer. 432 | 433 | * Redistributions in binary form must reproduce the above copyright notice, 434 | this list of conditions and the following disclaimer in the documentation 435 | and/or other materials provided with the distribution. 436 | 437 | * Neither the name of fast-cpp-csv-parser nor the names of its 438 | contributors may be used to endorse or promote products derived from 439 | this software without specific prior written permission. 440 | 441 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 442 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 443 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 444 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 445 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 446 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 447 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 448 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 449 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 450 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 451 | ``` 452 |
453 | 454 | ### TinyXML-2 (9.0.0) 455 | TinyXML-2 is a simple, small, efficient, C++ XML parser that can be easily integrated into other programs. 456 | Source: https://github.com/leethomason/tinyxml2 457 | 458 |
459 | License 460 | 461 | ``` 462 | This software is provided 'as-is', without any express or implied 463 | warranty. In no event will the authors be held liable for any 464 | damages arising from the use of this software. 465 | 466 | Permission is granted to anyone to use this software for any 467 | purpose, including commercial applications, and to alter it and 468 | redistribute it freely, subject to the following restrictions: 469 | 470 | 1. The origin of this software must not be misrepresented; you must 471 | not claim that you wrote the original software. If you use this 472 | software in a product, an acknowledgment in the product documentation 473 | would be appreciated but is not required. 474 | 475 | 2. Altered source versions must be plainly marked as such, and 476 | must not be misrepresented as being the original software. 477 | 478 | 3. This notice may not be removed or altered from any source 479 | distribution. 480 | ``` 481 |
482 | 483 | ### Date (3.0.1) 484 | A date and time library based on the C++11/14/17 header. 485 | Source: https://github.com/HowardHinnant/date 486 | 487 |
488 | License 489 | 490 | ``` 491 | The source code in this project is released using the MIT License. There is no 492 | global license for the project because each file is licensed individually with 493 | different author names and/or dates. 494 | 495 | If you contribute to this project, please add your name to the license of each 496 | file you modify. If you have already contributed to this project and forgot to 497 | add your name to the license, please feel free to submit a new P/R to add your 498 | name to the license in each file you modified. 499 | 500 | For convenience, here is a copy of the MIT license found in each file except 501 | without author names or dates: 502 | 503 | The MIT License (MIT) 504 | 505 | Permission is hereby granted, free of charge, to any person obtaining a copy 506 | of this software and associated documentation files (the "Software"), to deal 507 | in the Software without restriction, including without limitation the rights 508 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 509 | copies of the Software, and to permit persons to whom the Software is 510 | furnished to do so, subject to the following conditions: 511 | 512 | The above copyright notice and this permission notice shall be included in all 513 | copies or substantial portions of the Software. 514 | 515 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 516 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 517 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 518 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 519 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 520 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 521 | SOFTWARE. 522 | ``` 523 |
524 | 525 | ### eventpp (master cf1ba5689d51d9aafeedfc28788f08690d8b0f40) 526 | A event library for callbacks, event dispatcher, and event queue. 527 | Source: https://github.com/wqking/eventpp 528 | 529 |
530 | License 531 | 532 | ``` 533 | eventpp library 534 | 535 | Copyright (C) 2018 Wang Qi (wqking) 536 | 537 | Licensed under the Apache License, Version 2.0 (the "License"); 538 | you may not use this file except in compliance with the License. 539 | You may obtain a copy of the License at 540 | 541 | http://www.apache.org/licenses/LICENSE-2.0 542 | 543 | Unless required by applicable law or agreed to in writing, software 544 | distributed under the License is distributed on an "AS IS" BASIS, 545 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 546 | See the License for the specific language governing permissions and 547 | limitations under the License. 548 | ``` 549 |
550 | 551 | ### gattlib (master bb90b55ddec9c1817729a507ca096c2a514d8dc3) 552 | A library used to access Generic Attribute Profile (GATT) protocol of BLE (Bluetooth Low Energy) devices. 553 | Source: https://github.com/labapart/gattlib 554 | -------------------------------------------------------------------------------- /cmake/clang-tidy.cmake: -------------------------------------------------------------------------------- 1 | if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "Windows") 2 | find_program(CLANG_TIDY_EXECUTABLE NAMES clang-tidy-7 clang-tidy) 3 | mark_as_advanced(CLANG_TIDY_EXECUTABLE) 4 | 5 | if (${CLANG_TIDY_EXECUTABLE}) 6 | message(WARNING "Clang-tidy not found") 7 | else() 8 | message(STATUS "Enabling clang-tidy") 9 | set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXECUTABLE};-warnings-as-errors=*") 10 | endif() 11 | else() 12 | message(STATUS "Clang-tidy is not supporten when building for windows") 13 | endif() 14 | -------------------------------------------------------------------------------- /cmake/clear_variable.cmake: -------------------------------------------------------------------------------- 1 | macro(clear_variable) 2 | cmake_parse_arguments(CLEAR_VAR "" "DESTINATION;BACKUP;REPLACE" "" ${ARGN}) 3 | set(${CLEAR_VAR_BACKUP} ${${CLEAR_VAR_DESTINATION}}) 4 | set(${CLEAR_VAR_DESTINATION} ${CLEAR_VAR_REPLACE}) 5 | endmacro() 6 | 7 | macro(restore_variable) 8 | cmake_parse_arguments(CLEAR_VAR "" "DESTINATION;BACKUP" "" ${ARGN}) 9 | set(${CLEAR_VAR_DESTINATION} ${${CLEAR_VAR_BACKUP}}) 10 | unset(${CLEAR_VAR_BACKUP}) 11 | endmacro() 12 | -------------------------------------------------------------------------------- /cmake/gcc_analyze.cmake: -------------------------------------------------------------------------------- 1 | include(CheckCXXCompilerFlag) 2 | 3 | if(JUTTA_BT_PROTO_STATIC_ANALYZE) 4 | if(JUTTA_BT_PROTO_ENABLE_LINTING) 5 | message(FATAL_ERROR "Linting and the GCC static analysis can not be enabled at the same time! Disable either JUTTA_BT_PROTO_STATIC_ANALYZE or JUTTA_BT_PROTO_ENABLE_LINTING.") 6 | endif() 7 | if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 8 | check_cxx_compiler_flag("-fanalyzer" HAS_GCC_STATIC_ANALYZER) 9 | if(HAS_GCC_STATIC_ANALYZER) 10 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fanalyzer") 11 | set(CMAKE_C_FLAGS "${CMAKE_CXX_FLAGS} -fanalyzer") 12 | message(STATUS "GCC static analysis successfully enabled.") 13 | else() 14 | message(FATAL_ERROR "Failed to enable GCC static analysis since the compiler does not support it.") 15 | endif() 16 | else() 17 | message(FATAL_ERROR "Failed to enable GCC static analysis since the compiler does not seam to be GCC (GNU).") 18 | endif() 19 | endif() 20 | -------------------------------------------------------------------------------- /cmake/sanitizer.cmake: -------------------------------------------------------------------------------- 1 | include(CheckCXXCompilerFlag) 2 | include(CheckCXXSourceRuns) 3 | 4 | set(ALLOWED_BUILD_TYPES Debug Release RelWithDebInfo MinSizeRel) 5 | set(ALLSAN_FLAGS "") 6 | 7 | # ThreadSanitizer 8 | set(THREAD_SAN_FLAGS "-fsanitize=thread") 9 | set(PREV_FLAG ${CMAKE_REQUIRED_FLAGS}) 10 | set(CMAKE_REQUIRED_FLAGS "${THREAD_SAN_FLAGS}") 11 | check_cxx_source_runs("int main() { return 0; }" THREAD_SANITIZER_AVAILABLE) 12 | set(CMAKE_REQUIRED_FLAGS ${PREV_FLAG}) 13 | if(THREAD_SANITIZER_AVAILABLE) 14 | list(APPEND ALLOWED_BUILD_TYPES ThreadSan) 15 | # Do not add Thread Sanitizer to all Sanitizers because it is incompatible with other Sanitizers 16 | endif() 17 | set(CMAKE_C_FLAGS_THREADSAN "${CMAKE_C_FLAGS_DEBUG} ${THREAD_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C compiler during Thread Sanitizer builds." FORCE) 18 | set(CMAKE_CXX_FLAGS_THREADSAN "${CMAKE_CXX_FLAGS_DEBUG} ${THREAD_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C++ compiler during Thread Sanitizer builds." FORCE) 19 | 20 | # AddressSanitizer 21 | set(ADDR_SAN_FLAGS "-fsanitize=address") 22 | set(PREV_FLAG ${CMAKE_REQUIRED_FLAGS}) 23 | set(CMAKE_REQUIRED_FLAGS "${ADDR_SAN_FLAGS}") 24 | check_cxx_source_runs("int main() { return 0; }" ADDRESS_SANITIZER_AVAILABLE) 25 | set(CMAKE_REQUIRED_FLAGS ${PREV_FLAG}) 26 | if(ADDRESS_SANITIZER_AVAILABLE) 27 | list(APPEND ALLOWED_BUILD_TYPES AddrSan) 28 | set(ALLSAN_FLAGS "${ALLSAN_FLAGS} ${ADDR_SAN_FLAGS}") 29 | endif() 30 | set(CMAKE_C_FLAGS_ADDRSAN "${CMAKE_C_FLAGS_DEBUG} ${ADDR_SAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C compiler during AddressSanitizer builds." FORCE) 31 | set(CMAKE_CXX_FLAGS_ADDRSAN "${CMAKE_CXX_FLAGS_DEBUG} ${ADDR_SAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C++ compiler during AddressSanitizer builds." FORCE) 32 | 33 | # LeakSanitizer 34 | set(LEAK_SAN_FLAGS "-fsanitize=leak") 35 | check_cxx_compiler_flag(${LEAK_SAN_FLAGS} LEAK_SANITIZER_AVAILABLE) 36 | if(LEAK_SANITIZER_AVAILABLE) 37 | list(APPEND ALLOWED_BUILD_TYPES LeakSan) 38 | set(ALLSAN_FLAGS "${ALLSAN_FLAGS} ${LEAK_SAN_FLAGS}") 39 | endif() 40 | set(CMAKE_C_FLAGS_LEAKSAN "${CMAKE_C_FLAGS_DEBUG} ${LEAK_SAN_FLAGS} -fno-omit-frame-pointer" CACHE INTERNAL "Flags used by the C compiler during LeakSanitizer builds." FORCE) 41 | set(CMAKE_CXX_FLAGS_LEAKSAN "${CMAKE_CXX_FLAGS_DEBUG} ${LEAK_SAN_FLAGS} -fno-omit-frame-pointer" CACHE INTERNAL "Flags used by the C++ compiler during LeakSanitizer builds." FORCE) 42 | 43 | # UndefinedBehaviour 44 | set(UDEF_SAN_FLAGS "-fsanitize=undefined") 45 | check_cxx_compiler_flag(${UDEF_SAN_FLAGS} UNDEFINED_BEHAVIOUR_SANITIZER_AVAILABLE) 46 | if(UNDEFINED_BEHAVIOUR_SANITIZER_AVAILABLE) 47 | list(APPEND ALLOWED_BUILD_TYPES UdefSan) 48 | set(ALLSAN_FLAGS "${ALLSAN_FLAGS} ${UDEF_SAN_FLAGS}") 49 | endif() 50 | set(CMAKE_C_FLAGS_UDEFSAN "${CMAKE_C_FLAGS_DEBUG} ${UDEF_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C compiler during Undefined_BehaviourSanitizer builds." FORCE) 51 | set(CMAKE_CXX_FLAGS_UDEFSAN "${CMAKE_CXX_FLAGS_DEBUG} ${UDEF_SAN_FLAGS}" CACHE INTERNAL "Flags used by the C++ compiler during Undefined_BehaviourSanitizer builds." FORCE) 52 | 53 | # AllSanetizer 54 | if(NOT ALLSAN_FLAGS STREQUAL "") 55 | set(PREV_FLAG ${CMAKE_REQUIRED_FLAGS}) 56 | set(CMAKE_REQUIRED_FLAGS "${ALLSAN_FLAGS}") 57 | check_cxx_source_runs("int main() { return 0; }" ALL_SANITIZERS_AVAILABLE) 58 | set(CMAKE_REQUIRED_FLAGS ${PREV_FLAG}) 59 | if(ALL_SANITIZERS_AVAILABLE) 60 | list(APPEND ALLOWED_BUILD_TYPES AllSan) 61 | endif() 62 | endif() 63 | 64 | set(CMAKE_C_FLAGS_ALLSAN "${CMAKE_C_FLAGS_DEBUG} ${ALLSAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C compiler during All Possible Sanetizer builds." FORCE) 65 | set(CMAKE_CXX_FLAGS_ALLSAN "${CMAKE_CXX_FLAGS_DEBUG} ${ALLSAN_FLAGS} -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE INTERNAL "Flags used by the C++ compiler during All Possible Sanetizer builds." FORCE) 66 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | add_subdirectory(io) 4 | add_subdirectory(date) 5 | add_subdirectory(bt) 6 | add_subdirectory(jutta_bt_proto) 7 | add_subdirectory(logger) 8 | add_subdirectory(include) 9 | add_subdirectory(test_exec) 10 | add_subdirectory(resources) 11 | -------------------------------------------------------------------------------- /src/bt/BLEDevice.cpp: -------------------------------------------------------------------------------- 1 | #include // Include first since we have some structs forward declared 2 | 3 | #include "bt/BLEDevice.hpp" 4 | #include "bt/ByteEncDecoder.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | //--------------------------------------------------------------------------- 21 | namespace bt { 22 | //--------------------------------------------------------------------------- 23 | BLEDevice::BLEDevice(std::string&& name, std::string&& addr, OnCharacteristicReadFunc onCharacteristicRead, OnConnectedFunc onConnected, OnDisconnectedFunc onDisconnected, OnCharacteristicNotificationFunc onCharacteristicNotification) : name(std::move(name)), 24 | addr(std::move(addr)), 25 | onCharacteristicRead(std::move(onCharacteristicRead)), 26 | onConnected(std::move(onConnected)), 27 | onDisconnected(std::move(onDisconnected)), 28 | onCharacteristicNotification(std::move(onCharacteristicNotification)) {} 29 | 30 | const std::vector BLEDevice::to_vec(const uint8_t* data, size_t len) { 31 | const uint8_t* dataBuf = static_cast(data); 32 | std::vector result; 33 | result.resize(len); 34 | for (size_t i = 0; i < len; i++) { 35 | // NOLINTNEXTLINE (cppcoreguidelines-pro-bounds-pointer-arithmetic) 36 | result[i] = static_cast(dataBuf[i]); 37 | } 38 | return result; 39 | } 40 | 41 | const std::vector BLEDevice::to_vec(const void* data, size_t len) { 42 | const uint8_t* dataBuf = static_cast(data); 43 | return to_vec(dataBuf, len); 44 | } 45 | 46 | const std::vector BLEDevice::get_mam_data() { 47 | gattlib_advertisement_data_t* adData = nullptr; 48 | size_t adDataCount = 0; 49 | uint16_t manId = 0; 50 | uint8_t* manData = nullptr; 51 | size_t manDataCount = 0; 52 | if (gattlib_get_advertisement_data(connection, &adData, &adDataCount, &manId, &manData, &manDataCount) != GATTLIB_SUCCESS) { 53 | SPDLOG_ERROR("BLE device advertisement data analysis failed."); 54 | return std::vector(); 55 | } 56 | return to_vec(manData, manDataCount); 57 | } 58 | 59 | bool BLEDevice::connect() { 60 | assert(!connection); 61 | assert(!connected); 62 | connection = gattlib_connect(nullptr, addr.c_str(), GATTLIB_CONNECTION_OPTIONS_LEGACY_DEFAULT); 63 | if (!connection) { 64 | return false; 65 | } 66 | 67 | int result = gattlib_discover_primary(connection, &services, &serviceCount); 68 | if (result != GATTLIB_SUCCESS) { 69 | SPDLOG_ERROR("BLE device GATT discovery failed with error code {}.", result); 70 | result = gattlib_disconnect(connection); 71 | if (result != GATTLIB_SUCCESS) { 72 | SPDLOG_ERROR("BLE device disconnect failed with error code {}.", result); 73 | } 74 | connection = nullptr; 75 | return false; 76 | } 77 | 78 | if (serviceCount <= 0) { 79 | SPDLOG_ERROR("BLE device GATT discovery failed with no ({}) services found.", serviceCount); 80 | result = gattlib_disconnect(connection); 81 | if (result != GATTLIB_SUCCESS) { 82 | SPDLOG_ERROR("BLE device disconnect failed with error code {}.", result); 83 | } 84 | connection = nullptr; 85 | return false; 86 | } 87 | 88 | SPDLOG_DEBUG("Discovered {} services.", serviceCount); 89 | SPDLOG_DEBUG("BLEDevice connected."); 90 | gattlib_register_on_disconnect(connection, &BLEDevice::on_disconnected, this); 91 | gattlib_register_notification(connection, &BLEDevice::on_notification, this); 92 | connected = true; 93 | onConnected(); 94 | return true; 95 | } 96 | 97 | void BLEDevice::disconnect() { 98 | if (connection) { 99 | gattlib_disconnect(connection); 100 | } 101 | } 102 | 103 | bool BLEDevice::is_connected() const { 104 | return connected; 105 | } 106 | 107 | void BLEDevice::read_characteristic(const uuid_t& characteristic) { 108 | if (!connected) { 109 | SPDLOG_WARN("Skipping read. Not connected."); 110 | return; 111 | } 112 | 113 | uuid_t uuid = characteristic; 114 | std::array uuidStr{}; 115 | gattlib_uuid_to_string(&uuid, uuidStr.data(), uuidStr.size()); 116 | 117 | void* buffer = nullptr; 118 | size_t bufLen = 0; 119 | int result = gattlib_read_char_by_uuid(connection, &uuid, &buffer, &bufLen); 120 | if (result != GATTLIB_SUCCESS) { 121 | SPDLOG_WARN("Failed to read characteristic '{}' with error code {}.", uuidStr.data(), result); 122 | return; 123 | } 124 | // Convert to a vector: 125 | const std::vector data = to_vec(buffer, bufLen); 126 | // NOLINTNEXTLINE (cppcoreguidelines-pro-bounds-pointer-arithmetic) 127 | onCharacteristicRead(data, characteristic); 128 | SPDLOG_TRACE("Read {} bytes from '{}'.", bufLen, uuidStr.data()); 129 | } 130 | 131 | void BLEDevice::read_characteristics() { 132 | std::array uuidStr{}; 133 | for (int i = 0; i < serviceCount; i++) { 134 | // NOLINTNEXTLINE (cppcoreguidelines-pro-bounds-pointer-arithmetic) 135 | gattlib_uuid_to_string(&services[i].uuid, uuidStr.data(), uuidStr.size()); 136 | SPDLOG_DEBUG("Found service with UUID: {}", uuidStr.data()); 137 | } 138 | 139 | int characteristics_count = 0; 140 | gattlib_characteristic_t* characteristics{nullptr}; 141 | gattlib_discover_char(connection, &characteristics, &characteristics_count); 142 | for (int i = 0; i < characteristics_count; i++) { 143 | // NOLINTNEXTLINE (cppcoreguidelines-pro-bounds-pointer-arithmetic) 144 | gattlib_uuid_to_string(&characteristics[i].uuid, uuidStr.data(), uuidStr.size()); 145 | SPDLOG_DEBUG("Found characteristic with UUID: {}", uuidStr.data()); 146 | } 147 | } 148 | 149 | bool BLEDevice::write(const uuid_t& characteristic, const std::vector& data) { 150 | if (!connected) { 151 | SPDLOG_WARN("Skipping write. Not connected."); 152 | return false; 153 | } 154 | uuid_t uuid = characteristic; 155 | std::array uuidStr{}; 156 | gattlib_uuid_to_string(&uuid, uuidStr.data(), uuidStr.size()); 157 | int result = gattlib_write_char_by_uuid(connection, &uuid, data.data(), data.size()); 158 | if (result == GATTLIB_SUCCESS) { 159 | SPDLOG_TRACE("Wrote {} byte to characteristic '{}'.", data.size(), uuidStr.data()); 160 | return true; 161 | } 162 | SPDLOG_ERROR("Failed to write to characteristic '{}' with error code {}!", uuidStr.data(), result); 163 | return false; 164 | } 165 | 166 | bool BLEDevice::subscribe(const uuid_t& characteristic) { 167 | const uuid_t uuid = characteristic; 168 | std::array uuidStr{}; 169 | gattlib_uuid_to_string(&uuid, uuidStr.data(), uuidStr.size()); 170 | int result = gattlib_notification_start(connection, &uuid); 171 | if (result == GATTLIB_SUCCESS) { 172 | SPDLOG_DEBUG("Subscribed to characteristic '{}'.", uuidStr.data()); 173 | return true; 174 | } 175 | SPDLOG_ERROR("Failed to subscribe to characteristic '{}' with error code {}!", uuidStr.data(), result); 176 | return false; 177 | } 178 | 179 | void BLEDevice::on_disconnected(void* arg) { 180 | BLEDevice* device = static_cast(arg); 181 | if (device->connected) { 182 | device->connected = false; 183 | device->connection = nullptr; 184 | device->onDisconnected(); 185 | SPDLOG_DEBUG("BLEDevice disconnected."); 186 | } 187 | } 188 | 189 | void BLEDevice::on_notification(const uuid_t* uuid, const uint8_t* data, size_t len, void* arg) { 190 | BLEDevice* device = static_cast(arg); 191 | const std::vector dataVec = BLEDevice::to_vec(data, len); 192 | device->onCharacteristicNotification(dataVec, *uuid); 193 | } 194 | //--------------------------------------------------------------------------- 195 | } // namespace bt 196 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/bt/BLEHelper.cpp: -------------------------------------------------------------------------------- 1 | #include "bt/BLEHelper.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | //--------------------------------------------------------------------------- 12 | namespace bt { 13 | //--------------------------------------------------------------------------- 14 | void on_device_discovered(void* adapter, const char* addr, const char* name, void* userData) { 15 | ScanArgs* args = static_cast(userData); 16 | args->m.lock(); 17 | if (name && std::regex_match(name, args->nameRegex)) { 18 | args->success = true; 19 | args->name = name; 20 | args->addr = addr; 21 | gattlib_adapter_scan_disable(adapter); 22 | SPDLOG_INFO("Coffee maker found!"); 23 | args->doneMutex.unlock(); 24 | } 25 | args->m.unlock(); 26 | if (name) { 27 | SPDLOG_DEBUG("FOUND: {}", name); 28 | } 29 | } 30 | 31 | std::shared_ptr scan_for_device(const std::string& regexStr, const bool* canceled) { 32 | SPDLOG_DEBUG("Scanning for devices..."); 33 | void* adapter = nullptr; 34 | int result = gattlib_adapter_open(nullptr, &adapter); 35 | if (result != GATTLIB_SUCCESS) { 36 | SPDLOG_ERROR("Failed to open Bluetooth adapter with error code {}.", result); 37 | return nullptr; 38 | } 39 | 40 | std::shared_ptr args = std::make_shared(); 41 | args->nameRegex = std::regex(regexStr); 42 | args->doneMutex.lock(); 43 | 44 | size_t timeoutSeconds = 0; 45 | if (gattlib_adapter_scan_enable(adapter, &on_device_discovered, timeoutSeconds, args.get())) { 46 | SPDLOG_ERROR("Bluetooth scan failed."); 47 | gattlib_adapter_close(adapter); 48 | return nullptr; 49 | } 50 | while (true) { 51 | if (args->doneMutex.try_lock()) { 52 | args->doneMutex.unlock(); 53 | break; 54 | } 55 | if (*canceled) { 56 | SPDLOG_DEBUG("Stopping scann..."); 57 | gattlib_adapter_scan_disable(adapter); 58 | // Wait for the scann to finish: 59 | args->doneMutex.lock(); 60 | args->doneMutex.unlock(); 61 | break; 62 | } 63 | std::this_thread::sleep_for(std::chrono::milliseconds(500)); 64 | } 65 | gattlib_adapter_close(adapter); 66 | SPDLOG_INFO("Scan stoped"); 67 | if (args->success) { 68 | return args; 69 | } 70 | return nullptr; 71 | } 72 | //--------------------------------------------------------------------------- 73 | } // namespace bt 74 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/bt/ByteEncDecoder.cpp: -------------------------------------------------------------------------------- 1 | #include "bt/ByteEncDecoder.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | //--------------------------------------------------------------------------- 10 | namespace bt { 11 | //--------------------------------------------------------------------------- 12 | const std::array numbers1 = {14, 4, 3, 2, 1, 13, 8, 11, 6, 15, 12, 7, 10, 5, 0, 9}; 13 | const std::array numbers2 = {10, 6, 13, 12, 14, 11, 1, 9, 15, 7, 0, 5, 3, 2, 4, 8}; 14 | 15 | uint8_t mod256(int i) { 16 | while (i > 255) { 17 | i -= 256; 18 | } 19 | while (i < 0) { 20 | i += 256; 21 | } 22 | return static_cast(i); 23 | } 24 | 25 | uint8_t shuffle(int dataNibble, int nibbleCount, int keyLeftNibbel, int keyRightNibbel) { 26 | uint8_t i5 = mod256(nibbleCount >> 4); 27 | uint8_t tmp1 = numbers1[mod256(dataNibble + nibbleCount + keyLeftNibbel) % 16]; 28 | uint8_t tmp2 = numbers2[mod256(tmp1 + keyRightNibbel + i5 - nibbleCount - keyLeftNibbel) % 16]; 29 | uint8_t tmp3 = numbers1[mod256(tmp2 + keyLeftNibbel + nibbleCount - keyRightNibbel - i5) % 16]; 30 | return mod256(tmp3 - nibbleCount - keyLeftNibbel) % 16; 31 | } 32 | 33 | std::vector encDecBytes(const std::vector& data, uint8_t key) { 34 | std::vector result; 35 | result.resize(data.size()); 36 | uint8_t keyLeftNibbel = key >> 4; 37 | uint8_t keyRightNibbel = key & 15; 38 | int nibbelCount = 0; 39 | for (size_t offset = 0; offset < data.size(); offset++) { 40 | uint8_t d = data[offset]; 41 | uint8_t dataLeftNibbel = d >> 4; 42 | uint8_t dataRightNibbel = d & 15; 43 | uint8_t resultLeftNibbel = shuffle(dataLeftNibbel, nibbelCount++, keyLeftNibbel, keyRightNibbel); 44 | uint8_t resultRightNibbel = shuffle(dataRightNibbel, nibbelCount++, keyLeftNibbel, keyRightNibbel); 45 | result[offset] = (resultLeftNibbel << 4) | resultRightNibbel; 46 | } 47 | return result; 48 | } 49 | 50 | //--------------------------------------------------------------------------- 51 | } // namespace bt 52 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/bt/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | add_library(bt SHARED BLEHelper.cpp 4 | BLEDevice.cpp 5 | ByteEncDecoder.cpp) 6 | target_link_libraries(bt PRIVATE logger gattlib) 7 | 8 | install(TARGETS bt) 9 | -------------------------------------------------------------------------------- /src/date/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | add_library(date INTERFACE) 4 | 5 | install(TARGETS date) 6 | -------------------------------------------------------------------------------- /src/include/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | target_include_directories(io INTERFACE 4 | $ 5 | $) 6 | 7 | target_sources(io INTERFACE 8 | # Header files (useful in IDEs) 9 | io/csv.hpp) 10 | 11 | target_sources(date INTERFACE 12 | # Header files (useful in IDEs) 13 | date/date.hpp) 14 | 15 | target_include_directories(bt PUBLIC 16 | $ 17 | $) 18 | 19 | target_sources(bt PRIVATE 20 | # Header files (useful in IDEs) 21 | bt/BLEDevice.hpp 22 | bt/BLEHelper.hpp 23 | bt/ByteEncDecoder.hpp) 24 | 25 | target_include_directories(jutta_bt_proto PUBLIC 26 | $ 27 | $) 28 | 29 | target_sources(jutta_bt_proto PRIVATE 30 | # Header files (useful in IDEs) 31 | jutta_bt_proto/CoffeeMaker.hpp 32 | jutta_bt_proto/Utils.hpp 33 | jutta_bt_proto/CoffeeMakerLoader.hpp) 34 | 35 | target_include_directories(logger PUBLIC 36 | $ 37 | $) 38 | 39 | target_sources(logger PRIVATE 40 | # Header files (useful in IDEs) 41 | logger/Logger.hpp) 42 | 43 | install(DIRECTORY bt DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 44 | install(DIRECTORY jutta_bt_proto DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 45 | install(DIRECTORY logger DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) -------------------------------------------------------------------------------- /src/include/bt/BLEDevice.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #ifndef __GATTLIB_H__ 13 | using gatt_connection_t = struct _gatt_connection_t; 14 | using gattlib_primary_service_t = struct gattlib_primary_service_t; 15 | #endif 16 | 17 | //--------------------------------------------------------------------------- 18 | namespace bt { 19 | //--------------------------------------------------------------------------- 20 | class BLEDevice { 21 | public: 22 | using OnCharacteristicReadFunc = std::function&, const uuid_t&)>; 23 | using OnCharacteristicNotificationFunc = std::function&, const uuid_t&)>; 24 | using OnConnectedFunc = std::function; 25 | using OnDisconnectedFunc = std::function; 26 | 27 | private: 28 | const std::string name; 29 | const std::string addr; 30 | 31 | OnCharacteristicReadFunc onCharacteristicRead; 32 | OnConnectedFunc onConnected; 33 | OnDisconnectedFunc onDisconnected; 34 | OnCharacteristicNotificationFunc onCharacteristicNotification; 35 | 36 | gatt_connection_t* connection{nullptr}; 37 | int serviceCount{0}; 38 | gattlib_primary_service_t* services{nullptr}; 39 | 40 | bool connected{false}; 41 | 42 | public: 43 | BLEDevice(std::string&& name, std::string&& addr, OnCharacteristicReadFunc onCharacteristicRead, OnConnectedFunc onConnected, OnDisconnectedFunc onDisconnected, OnCharacteristicNotificationFunc onCharacteristicNotification); 44 | BLEDevice(BLEDevice&&) = default; 45 | BLEDevice(const BLEDevice&) = default; 46 | BLEDevice& operator=(BLEDevice&&) = delete; 47 | BLEDevice& operator=(const BLEDevice&) = delete; 48 | ~BLEDevice() = default; 49 | 50 | bool connect(); 51 | void disconnect(); 52 | [[nodiscard]] bool is_connected() const; 53 | const std::vector get_mam_data(); 54 | void read_characteristics(); 55 | void read_characteristic(const uuid_t& characteristic); 56 | bool write(const uuid_t& characteristic, const std::vector& data); 57 | bool subscribe(const uuid_t& characteristic); 58 | 59 | private: 60 | static const std::vector to_vec(const void* data, size_t len); 61 | static const std::vector to_vec(const uint8_t* data, size_t len); 62 | 63 | static void on_disconnected(void* arg); 64 | static void on_notification(const uuid_t* uuid, const uint8_t* data, size_t len, void* arg); 65 | }; 66 | //--------------------------------------------------------------------------- 67 | } // namespace bt 68 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/include/bt/BLEHelper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | //--------------------------------------------------------------------------- 10 | namespace bt { 11 | //--------------------------------------------------------------------------- 12 | struct ScanArgs { 13 | std::mutex doneMutex; 14 | std::mutex m; 15 | std::string name; 16 | std::regex nameRegex; 17 | bool success{false}; 18 | std::string addr; 19 | } __attribute__((aligned(128))); 20 | 21 | std::shared_ptr scan_for_device(const std::string& regexStr, const bool* canceled); 22 | //--------------------------------------------------------------------------- 23 | } // namespace bt 24 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/include/bt/ByteEncDecoder.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | //--------------------------------------------------------------------------- 8 | namespace bt { 9 | //--------------------------------------------------------------------------- 10 | /** 11 | * Encodes or decodes the given data with the given key. 12 | * When you are decoding data, the first byte of the result should be key if everything went well. 13 | * This function is reversible. 14 | * encDecBytes(encDecBytes(data)) == data 15 | **/ 16 | std::vector encDecBytes(const std::vector& data, uint8_t key); 17 | //--------------------------------------------------------------------------- 18 | } // namespace bt 19 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/include/io/csv.hpp: -------------------------------------------------------------------------------- 1 | // Copyright: (2012-2015) Ben Strasser 2 | // License: BSD-3 3 | // 4 | // All rights reserved. 5 | // 6 | // Redistribution and use in source and binary forms, with or without 7 | // modification, are permitted provided that the following conditions are met: 8 | // 9 | // 1. Redistributions of source code must retain the above copyright notice, 10 | // this list of conditions and the following disclaimer. 11 | // 12 | // 2. Redistributions in binary form must reproduce the above copyright notice, 13 | // this list of conditions and the following disclaimer in the documentation 14 | // and/or other materials provided with the distribution. 15 | // 16 | // 3. Neither the name of the copyright holder nor the names of its contributors 17 | // may be used to endorse or promote products derived from this software 18 | // without specific prior written permission. 19 | // 20 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | // POSSIBILITY OF SUCH DAMAGE. 31 | 32 | // NOLINTBEGIN 33 | #ifndef CSV_H 34 | #define CSV_H 35 | 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #ifndef CSV_IO_NO_THREAD 44 | #include 45 | #include 46 | #include 47 | #endif 48 | #include 49 | #include 50 | #include 51 | #include 52 | 53 | namespace io { 54 | //////////////////////////////////////////////////////////////////////////// 55 | // LineReader // 56 | //////////////////////////////////////////////////////////////////////////// 57 | 58 | namespace error { 59 | struct base : std::exception { 60 | virtual void format_error_message() const = 0; 61 | 62 | const char* what() const noexcept override { 63 | format_error_message(); 64 | return error_message_buffer; 65 | } 66 | 67 | mutable char error_message_buffer[512]; 68 | }; 69 | 70 | const int max_file_name_length = 255; 71 | 72 | struct with_file_name { 73 | with_file_name() { 74 | std::memset(file_name, 0, sizeof(file_name)); 75 | } 76 | 77 | void set_file_name(const char* file_name) { 78 | if (file_name != nullptr) { 79 | // This call to strncpy has parenthesis around it 80 | // to silence the GCC -Wstringop-truncation warning 81 | (strncpy(this->file_name, file_name, sizeof(this->file_name))); 82 | this->file_name[sizeof(this->file_name) - 1] = '\0'; 83 | } else { 84 | this->file_name[0] = '\0'; 85 | } 86 | } 87 | 88 | char file_name[max_file_name_length + 1]; 89 | }; 90 | 91 | struct with_file_line { 92 | with_file_line() { 93 | file_line = -1; 94 | } 95 | 96 | void set_file_line(int file_line) { 97 | this->file_line = file_line; 98 | } 99 | 100 | int file_line; 101 | }; 102 | 103 | struct with_errno { 104 | with_errno() { 105 | errno_value = 0; 106 | } 107 | 108 | void set_errno(int errno_value) { 109 | this->errno_value = errno_value; 110 | } 111 | 112 | int errno_value; 113 | }; 114 | 115 | struct can_not_open_file : base, 116 | with_file_name, 117 | with_errno { 118 | void format_error_message() const override { 119 | if (errno_value != 0) 120 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 121 | "Can not open file \"%s\" because \"%s\".", file_name, std::strerror(errno_value)); 122 | else 123 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 124 | "Can not open file \"%s\".", file_name); 125 | } 126 | }; 127 | 128 | struct line_length_limit_exceeded : base, 129 | with_file_name, 130 | with_file_line { 131 | void format_error_message() const override { 132 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 133 | "Line number %d in file \"%s\" exceeds the maximum length of 2^24-1.", file_line, file_name); 134 | } 135 | }; 136 | } // namespace error 137 | 138 | class ByteSourceBase { 139 | public: 140 | virtual int read(char* buffer, int size) = 0; 141 | virtual ~ByteSourceBase() {} 142 | }; 143 | 144 | namespace detail { 145 | 146 | class OwningStdIOByteSourceBase : public ByteSourceBase { 147 | public: 148 | explicit OwningStdIOByteSourceBase(FILE* file) : file(file) { 149 | // Tell the std library that we want to do the buffering ourself. 150 | std::setvbuf(file, 0, _IONBF, 0); 151 | } 152 | 153 | int read(char* buffer, int size) { 154 | return std::fread(buffer, 1, size, file); 155 | } 156 | 157 | ~OwningStdIOByteSourceBase() { 158 | std::fclose(file); 159 | } 160 | 161 | private: 162 | FILE* file; 163 | }; 164 | 165 | class NonOwningIStreamByteSource : public ByteSourceBase { 166 | public: 167 | explicit NonOwningIStreamByteSource(std::istream& in) : in(in) {} 168 | 169 | int read(char* buffer, int size) { 170 | in.read(buffer, size); 171 | return in.gcount(); 172 | } 173 | 174 | ~NonOwningIStreamByteSource() {} 175 | 176 | private: 177 | std::istream& in; 178 | }; 179 | 180 | class NonOwningStringByteSource : public ByteSourceBase { 181 | public: 182 | NonOwningStringByteSource(const char* str, long long size) : str(str), remaining_byte_count(size) {} 183 | 184 | int read(char* buffer, int desired_byte_count) { 185 | int to_copy_byte_count = desired_byte_count; 186 | if (remaining_byte_count < to_copy_byte_count) 187 | to_copy_byte_count = remaining_byte_count; 188 | std::memcpy(buffer, str, to_copy_byte_count); 189 | remaining_byte_count -= to_copy_byte_count; 190 | str += to_copy_byte_count; 191 | return to_copy_byte_count; 192 | } 193 | 194 | ~NonOwningStringByteSource() {} 195 | 196 | private: 197 | const char* str; 198 | long long remaining_byte_count; 199 | }; 200 | 201 | #ifndef CSV_IO_NO_THREAD 202 | class AsynchronousReader { 203 | public: 204 | void init(std::unique_ptr arg_byte_source) { 205 | std::unique_lock guard(lock); 206 | byte_source = std::move(arg_byte_source); 207 | desired_byte_count = -1; 208 | termination_requested = false; 209 | worker = std::thread( 210 | [&] { 211 | std::unique_lock guard(lock); 212 | try { 213 | for (;;) { 214 | read_requested_condition.wait( 215 | guard, 216 | [&] { 217 | return desired_byte_count != -1 || termination_requested; 218 | }); 219 | if (termination_requested) 220 | return; 221 | 222 | read_byte_count = byte_source->read(buffer, desired_byte_count); 223 | desired_byte_count = -1; 224 | if (read_byte_count == 0) 225 | break; 226 | read_finished_condition.notify_one(); 227 | } 228 | } catch (...) { 229 | read_error = std::current_exception(); 230 | } 231 | read_finished_condition.notify_one(); 232 | }); 233 | } 234 | 235 | bool is_valid() const { 236 | return byte_source != nullptr; 237 | } 238 | 239 | void start_read(char* arg_buffer, int arg_desired_byte_count) { 240 | std::unique_lock guard(lock); 241 | buffer = arg_buffer; 242 | desired_byte_count = arg_desired_byte_count; 243 | read_byte_count = -1; 244 | read_requested_condition.notify_one(); 245 | } 246 | 247 | int finish_read() { 248 | std::unique_lock guard(lock); 249 | read_finished_condition.wait( 250 | guard, 251 | [&] { 252 | return read_byte_count != -1 || read_error; 253 | }); 254 | if (read_error) 255 | std::rethrow_exception(read_error); 256 | else 257 | return read_byte_count; 258 | } 259 | 260 | ~AsynchronousReader() { 261 | if (byte_source != nullptr) { 262 | { 263 | std::unique_lock guard(lock); 264 | termination_requested = true; 265 | } 266 | read_requested_condition.notify_one(); 267 | worker.join(); 268 | } 269 | } 270 | 271 | private: 272 | std::unique_ptr byte_source; 273 | 274 | std::thread worker; 275 | 276 | bool termination_requested; 277 | std::exception_ptr read_error; 278 | char* buffer; 279 | int desired_byte_count; 280 | int read_byte_count; 281 | 282 | std::mutex lock; 283 | std::condition_variable read_finished_condition; 284 | std::condition_variable read_requested_condition; 285 | }; 286 | #endif 287 | 288 | class SynchronousReader { 289 | public: 290 | void init(std::unique_ptr arg_byte_source) { 291 | byte_source = std::move(arg_byte_source); 292 | } 293 | 294 | bool is_valid() const { 295 | return byte_source != nullptr; 296 | } 297 | 298 | void start_read(char* arg_buffer, int arg_desired_byte_count) { 299 | buffer = arg_buffer; 300 | desired_byte_count = arg_desired_byte_count; 301 | } 302 | 303 | int finish_read() { 304 | return byte_source->read(buffer, desired_byte_count); 305 | } 306 | 307 | private: 308 | std::unique_ptr byte_source; 309 | char* buffer; 310 | int desired_byte_count; 311 | }; 312 | } // namespace detail 313 | 314 | class LineReader { 315 | private: 316 | static const int block_len = 1 << 20; 317 | std::unique_ptr buffer; // must be constructed before (and thus destructed after) the reader! 318 | #ifdef CSV_IO_NO_THREAD 319 | detail::SynchronousReader reader; 320 | #else 321 | detail::AsynchronousReader reader; 322 | #endif 323 | int data_begin; 324 | int data_end; 325 | 326 | char file_name[error::max_file_name_length + 1]; 327 | unsigned file_line; 328 | 329 | static std::unique_ptr open_file(const char* file_name) { 330 | // We open the file in binary mode as it makes no difference under *nix 331 | // and under Windows we handle \r\n newlines ourself. 332 | FILE* file = std::fopen(file_name, "rb"); 333 | if (file == 0) { 334 | int x = errno; // store errno as soon as possible, doing it after constructor call can fail. 335 | error::can_not_open_file err; 336 | err.set_errno(x); 337 | err.set_file_name(file_name); 338 | throw err; 339 | } 340 | return std::unique_ptr(new detail::OwningStdIOByteSourceBase(file)); 341 | } 342 | 343 | void init(std::unique_ptr byte_source) { 344 | file_line = 0; 345 | 346 | buffer = std::unique_ptr(new char[3 * block_len]); 347 | data_begin = 0; 348 | data_end = byte_source->read(buffer.get(), 2 * block_len); 349 | 350 | // Ignore UTF-8 BOM 351 | if (data_end >= 3 && buffer[0] == '\xEF' && buffer[1] == '\xBB' && buffer[2] == '\xBF') 352 | data_begin = 3; 353 | 354 | if (data_end == 2 * block_len) { 355 | reader.init(std::move(byte_source)); 356 | reader.start_read(buffer.get() + 2 * block_len, block_len); 357 | } 358 | } 359 | 360 | public: 361 | LineReader() = delete; 362 | LineReader(const LineReader&) = delete; 363 | LineReader& operator=(const LineReader&) = delete; 364 | 365 | explicit LineReader(const char* file_name) { 366 | set_file_name(file_name); 367 | init(open_file(file_name)); 368 | } 369 | 370 | explicit LineReader(const std::string& file_name) { 371 | set_file_name(file_name.c_str()); 372 | init(open_file(file_name.c_str())); 373 | } 374 | 375 | LineReader(const char* file_name, std::unique_ptr byte_source) { 376 | set_file_name(file_name); 377 | init(std::move(byte_source)); 378 | } 379 | 380 | LineReader(const std::string& file_name, std::unique_ptr byte_source) { 381 | set_file_name(file_name.c_str()); 382 | init(std::move(byte_source)); 383 | } 384 | 385 | LineReader(const char* file_name, const char* data_begin, const char* data_end) { 386 | set_file_name(file_name); 387 | init(std::unique_ptr(new detail::NonOwningStringByteSource(data_begin, data_end - data_begin))); 388 | } 389 | 390 | LineReader(const std::string& file_name, const char* data_begin, const char* data_end) { 391 | set_file_name(file_name.c_str()); 392 | init(std::unique_ptr(new detail::NonOwningStringByteSource(data_begin, data_end - data_begin))); 393 | } 394 | 395 | LineReader(const char* file_name, FILE* file) { 396 | set_file_name(file_name); 397 | init(std::unique_ptr(new detail::OwningStdIOByteSourceBase(file))); 398 | } 399 | 400 | LineReader(const std::string& file_name, FILE* file) { 401 | set_file_name(file_name.c_str()); 402 | init(std::unique_ptr(new detail::OwningStdIOByteSourceBase(file))); 403 | } 404 | 405 | LineReader(const char* file_name, std::istream& in) { 406 | set_file_name(file_name); 407 | init(std::unique_ptr(new detail::NonOwningIStreamByteSource(in))); 408 | } 409 | 410 | LineReader(const std::string& file_name, std::istream& in) { 411 | set_file_name(file_name.c_str()); 412 | init(std::unique_ptr(new detail::NonOwningIStreamByteSource(in))); 413 | } 414 | 415 | void set_file_name(const std::string& file_name) { 416 | set_file_name(file_name.c_str()); 417 | } 418 | 419 | void set_file_name(const char* file_name) { 420 | if (file_name != nullptr) { 421 | strncpy(this->file_name, file_name, sizeof(this->file_name)); 422 | this->file_name[sizeof(this->file_name) - 1] = '\0'; 423 | } else { 424 | this->file_name[0] = '\0'; 425 | } 426 | } 427 | 428 | const char* get_truncated_file_name() const { 429 | return file_name; 430 | } 431 | 432 | void set_file_line(unsigned file_line) { 433 | this->file_line = file_line; 434 | } 435 | 436 | unsigned get_file_line() const { 437 | return file_line; 438 | } 439 | 440 | char* next_line() { 441 | if (data_begin == data_end) 442 | return nullptr; 443 | 444 | ++file_line; 445 | 446 | assert(data_begin < data_end); 447 | assert(data_end <= block_len * 2); 448 | 449 | if (data_begin >= block_len) { 450 | std::memcpy(buffer.get(), buffer.get() + block_len, block_len); 451 | data_begin -= block_len; 452 | data_end -= block_len; 453 | if (reader.is_valid()) { 454 | data_end += reader.finish_read(); 455 | std::memcpy(buffer.get() + block_len, buffer.get() + 2 * block_len, block_len); 456 | reader.start_read(buffer.get() + 2 * block_len, block_len); 457 | } 458 | } 459 | 460 | int line_end = data_begin; 461 | while (line_end != data_end && buffer[line_end] != '\n') { 462 | ++line_end; 463 | } 464 | 465 | if (line_end - data_begin + 1 > block_len) { 466 | error::line_length_limit_exceeded err; 467 | err.set_file_name(file_name); 468 | err.set_file_line(file_line); 469 | throw err; 470 | } 471 | 472 | if (line_end != data_end && buffer[line_end] == '\n') { 473 | buffer[line_end] = '\0'; 474 | } else { 475 | // some files are missing the newline at the end of the 476 | // last line 477 | ++data_end; 478 | buffer[line_end] = '\0'; 479 | } 480 | 481 | // handle windows \r\n-line breaks 482 | if (line_end != data_begin && buffer[line_end - 1] == '\r') 483 | buffer[line_end - 1] = '\0'; 484 | 485 | char* ret = buffer.get() + data_begin; 486 | data_begin = line_end + 1; 487 | return ret; 488 | } 489 | }; 490 | 491 | //////////////////////////////////////////////////////////////////////////// 492 | // CSV // 493 | //////////////////////////////////////////////////////////////////////////// 494 | 495 | namespace error { 496 | const int max_column_name_length = 63; 497 | struct with_column_name { 498 | with_column_name() { 499 | std::memset(column_name, 0, max_column_name_length + 1); 500 | } 501 | 502 | void set_column_name(const char* column_name) { 503 | if (column_name != nullptr) { 504 | std::strncpy(this->column_name, column_name, max_column_name_length); 505 | this->column_name[max_column_name_length] = '\0'; 506 | } else { 507 | this->column_name[0] = '\0'; 508 | } 509 | } 510 | 511 | char column_name[max_column_name_length + 1]; 512 | }; 513 | 514 | const int max_column_content_length = 63; 515 | 516 | struct with_column_content { 517 | with_column_content() { 518 | std::memset(column_content, 0, max_column_content_length + 1); 519 | } 520 | 521 | void set_column_content(const char* column_content) { 522 | if (column_content != nullptr) { 523 | std::strncpy(this->column_content, column_content, max_column_content_length); 524 | this->column_content[max_column_content_length] = '\0'; 525 | } else { 526 | this->column_content[0] = '\0'; 527 | } 528 | } 529 | 530 | char column_content[max_column_content_length + 1]; 531 | }; 532 | 533 | struct extra_column_in_header : base, 534 | with_file_name, 535 | with_column_name { 536 | void format_error_message() const override { 537 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 538 | R"(Extra column "%s" in header of file "%s".)", column_name, file_name); 539 | } 540 | }; 541 | 542 | struct missing_column_in_header : base, 543 | with_file_name, 544 | with_column_name { 545 | void format_error_message() const override { 546 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 547 | R"(Missing column "%s" in header of file "%s".)", column_name, file_name); 548 | } 549 | }; 550 | 551 | struct duplicated_column_in_header : base, 552 | with_file_name, 553 | with_column_name { 554 | void format_error_message() const override { 555 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 556 | R"(Duplicated column "%s" in header of file "%s".)", column_name, file_name); 557 | } 558 | }; 559 | 560 | struct header_missing : base, 561 | with_file_name { 562 | void format_error_message() const override { 563 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 564 | "Header missing in file \"%s\".", file_name); 565 | } 566 | }; 567 | 568 | struct too_few_columns : base, 569 | with_file_name, 570 | with_file_line { 571 | void format_error_message() const override { 572 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 573 | "Too few columns in line %d in file \"%s\".", file_line, file_name); 574 | } 575 | }; 576 | 577 | struct too_many_columns : base, 578 | with_file_name, 579 | with_file_line { 580 | void format_error_message() const override { 581 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 582 | "Too many columns in line %d in file \"%s\".", file_line, file_name); 583 | } 584 | }; 585 | 586 | struct escaped_string_not_closed : base, 587 | with_file_name, 588 | with_file_line { 589 | void format_error_message() const override { 590 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 591 | "Escaped string was not closed in line %d in file \"%s\".", file_line, file_name); 592 | } 593 | }; 594 | 595 | struct integer_must_be_positive : base, 596 | with_file_name, 597 | with_file_line, 598 | with_column_name, 599 | with_column_content { 600 | void format_error_message() const override { 601 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 602 | R"(The integer "%s" must be positive or 0 in column "%s" in file "%s" in line "%d".)", column_content, column_name, file_name, file_line); 603 | } 604 | }; 605 | 606 | struct no_digit : base, 607 | with_file_name, 608 | with_file_line, 609 | with_column_name, 610 | with_column_content { 611 | void format_error_message() const override { 612 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 613 | R"(The integer "%s" contains an invalid digit in column "%s" in file "%s" in line "%d".)", column_content, column_name, file_name, file_line); 614 | } 615 | }; 616 | 617 | struct integer_overflow : base, 618 | with_file_name, 619 | with_file_line, 620 | with_column_name, 621 | with_column_content { 622 | void format_error_message() const override { 623 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 624 | R"(The integer "%s" overflows in column "%s" in file "%s" in line "%d".)", column_content, column_name, file_name, file_line); 625 | } 626 | }; 627 | 628 | struct integer_underflow : base, 629 | with_file_name, 630 | with_file_line, 631 | with_column_name, 632 | with_column_content { 633 | void format_error_message() const override { 634 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 635 | R"(The integer "%s" underflows in column "%s" in file "%s" in line "%d".)", column_content, column_name, file_name, file_line); 636 | } 637 | }; 638 | 639 | struct invalid_single_character : base, 640 | with_file_name, 641 | with_file_line, 642 | with_column_name, 643 | with_column_content { 644 | void format_error_message() const override { 645 | std::snprintf(error_message_buffer, sizeof(error_message_buffer), 646 | R"(The content "%s" of column "%s" in file "%s" in line "%d" is not a single character.)", column_content, column_name, file_name, file_line); 647 | } 648 | }; 649 | } // namespace error 650 | 651 | using ignore_column = unsigned int; 652 | static const ignore_column ignore_no_column = 0; 653 | static const ignore_column ignore_extra_column = 1; 654 | static const ignore_column ignore_missing_column = 2; 655 | 656 | template 657 | struct trim_chars { 658 | private: 659 | constexpr static bool is_trim_char(char) { 660 | return false; 661 | } 662 | 663 | template 664 | constexpr static bool is_trim_char(char c, char trim_char, OtherTrimChars... other_trim_chars) { 665 | return c == trim_char || is_trim_char(c, other_trim_chars...); 666 | } 667 | 668 | public: 669 | static void trim(char*& str_begin, char*& str_end) { 670 | while (str_begin != str_end && is_trim_char(*str_begin, trim_char_list...)) 671 | ++str_begin; 672 | while (str_begin != str_end && is_trim_char(*(str_end - 1), trim_char_list...)) 673 | --str_end; 674 | *str_end = '\0'; 675 | } 676 | }; 677 | 678 | struct no_comment { 679 | static bool is_comment(const char*) { 680 | return false; 681 | } 682 | }; 683 | 684 | template 685 | struct single_line_comment { 686 | private: 687 | constexpr static bool is_comment_start_char(char) { 688 | return false; 689 | } 690 | 691 | template 692 | constexpr static bool is_comment_start_char(char c, char comment_start_char, OtherCommentStartChars... other_comment_start_chars) { 693 | return c == comment_start_char || is_comment_start_char(c, other_comment_start_chars...); 694 | } 695 | 696 | public: 697 | static bool is_comment(const char* line) { 698 | return is_comment_start_char(*line, comment_start_char_list...); 699 | } 700 | }; 701 | 702 | struct empty_line_comment { 703 | static bool is_comment(const char* line) { 704 | if (*line == '\0') 705 | return true; 706 | while (*line == ' ' || *line == '\t') { 707 | ++line; 708 | if (*line == 0) 709 | return true; 710 | } 711 | return false; 712 | } 713 | }; 714 | 715 | template 716 | struct single_and_empty_line_comment { 717 | static bool is_comment(const char* line) { 718 | return single_line_comment::is_comment(line) || empty_line_comment::is_comment(line); 719 | } 720 | }; 721 | 722 | template 723 | struct no_quote_escape { 724 | static const char* find_next_column_end(const char* col_begin) { 725 | while (*col_begin != sep && *col_begin != '\0') 726 | ++col_begin; 727 | return col_begin; 728 | } 729 | 730 | static void unescape(char*&, char*&) { 731 | } 732 | }; 733 | 734 | template 735 | struct double_quote_escape { 736 | static const char* find_next_column_end(const char* col_begin) { 737 | while (*col_begin != sep && *col_begin != '\0') 738 | if (*col_begin != quote) 739 | ++col_begin; 740 | else { 741 | do { 742 | ++col_begin; 743 | while (*col_begin != quote) { 744 | if (*col_begin == '\0') 745 | throw error::escaped_string_not_closed(); 746 | ++col_begin; 747 | } 748 | ++col_begin; 749 | } while (*col_begin == quote); 750 | } 751 | return col_begin; 752 | } 753 | 754 | static void unescape(char*& col_begin, char*& col_end) { 755 | if (col_end - col_begin >= 2) { 756 | if (*col_begin == quote && *(col_end - 1) == quote) { 757 | ++col_begin; 758 | --col_end; 759 | char* out = col_begin; 760 | for (char* in = col_begin; in != col_end; ++in) { 761 | if (*in == quote && (in + 1) != col_end && *(in + 1) == quote) { 762 | ++in; 763 | } 764 | *out = *in; 765 | ++out; 766 | } 767 | col_end = out; 768 | *col_end = '\0'; 769 | } 770 | } 771 | } 772 | }; 773 | 774 | struct throw_on_overflow { 775 | template 776 | static void on_overflow(T&) { 777 | throw error::integer_overflow(); 778 | } 779 | 780 | template 781 | static void on_underflow(T&) { 782 | throw error::integer_underflow(); 783 | } 784 | }; 785 | 786 | struct ignore_overflow { 787 | template 788 | static void on_overflow(T&) {} 789 | 790 | template 791 | static void on_underflow(T&) {} 792 | }; 793 | 794 | struct set_to_max_on_overflow { 795 | template 796 | static void on_overflow(T& x) { 797 | // using (std::numeric_limits::max) instead of std::numeric_limits::max 798 | // to make code including windows.h with its max macro happy 799 | x = (std::numeric_limits::max)(); 800 | } 801 | 802 | template 803 | static void on_underflow(T& x) { 804 | x = (std::numeric_limits::min)(); 805 | } 806 | }; 807 | 808 | namespace detail { 809 | template 810 | void chop_next_column( 811 | char*& line, char*& col_begin, char*& col_end) { 812 | assert(line != nullptr); 813 | 814 | col_begin = line; 815 | // the col_begin + (... - col_begin) removes the constness 816 | col_end = col_begin + (quote_policy::find_next_column_end(col_begin) - col_begin); 817 | 818 | if (*col_end == '\0') { 819 | line = nullptr; 820 | } else { 821 | *col_end = '\0'; 822 | line = col_end + 1; 823 | } 824 | } 825 | 826 | template 827 | void parse_line( 828 | char* line, 829 | char** sorted_col, 830 | const std::vector& col_order) { 831 | for (int i : col_order) { 832 | if (line == nullptr) 833 | throw ::io::error::too_few_columns(); 834 | char *col_begin, *col_end; 835 | chop_next_column(line, col_begin, col_end); 836 | 837 | if (i != -1) { 838 | trim_policy::trim(col_begin, col_end); 839 | quote_policy::unescape(col_begin, col_end); 840 | 841 | sorted_col[i] = col_begin; 842 | } 843 | } 844 | if (line != nullptr) 845 | throw ::io::error::too_many_columns(); 846 | } 847 | 848 | template 849 | void parse_header_line( 850 | char* line, 851 | std::vector& col_order, 852 | const std::string* col_name, 853 | ignore_column ignore_policy) { 854 | col_order.clear(); 855 | 856 | bool found[column_count]; 857 | std::fill(found, found + column_count, false); 858 | while (line) { 859 | char *col_begin, *col_end; 860 | chop_next_column(line, col_begin, col_end); 861 | 862 | trim_policy::trim(col_begin, col_end); 863 | quote_policy::unescape(col_begin, col_end); 864 | 865 | for (unsigned i = 0; i < column_count; ++i) 866 | if (col_begin == col_name[i]) { 867 | if (found[i]) { 868 | error::duplicated_column_in_header err; 869 | err.set_column_name(col_begin); 870 | throw err; 871 | } 872 | found[i] = true; 873 | col_order.push_back(i); 874 | col_begin = 0; 875 | break; 876 | } 877 | if (col_begin) { 878 | if (ignore_policy & ::io::ignore_extra_column) 879 | col_order.push_back(-1); 880 | else { 881 | error::extra_column_in_header err; 882 | err.set_column_name(col_begin); 883 | throw err; 884 | } 885 | } 886 | } 887 | if (!(ignore_policy & ::io::ignore_missing_column)) { 888 | for (unsigned i = 0; i < column_count; ++i) { 889 | if (!found[i]) { 890 | error::missing_column_in_header err; 891 | err.set_column_name(col_name[i].c_str()); 892 | throw err; 893 | } 894 | } 895 | } 896 | } 897 | 898 | template 899 | void parse(char* col, char& x) { 900 | if (!*col) 901 | throw error::invalid_single_character(); 902 | x = *col; 903 | ++col; 904 | if (*col) 905 | throw error::invalid_single_character(); 906 | } 907 | 908 | template 909 | void parse(char* col, std::string& x) { 910 | x = col; 911 | } 912 | 913 | template 914 | void parse(char* col, const char*& x) { 915 | x = col; 916 | } 917 | 918 | template 919 | void parse(char* col, char*& x) { 920 | x = col; 921 | } 922 | 923 | template 924 | void parse_unsigned_integer(const char* col, T& x) { 925 | x = 0; 926 | while (*col != '\0') { 927 | if ('0' <= *col && *col <= '9') { 928 | T y = *col - '0'; 929 | if (x > ((std::numeric_limits::max)() - y) / 10) { 930 | overflow_policy::on_overflow(x); 931 | return; 932 | } 933 | x = 10 * x + y; 934 | } else 935 | throw error::no_digit(); 936 | ++col; 937 | } 938 | } 939 | 940 | template 941 | void parse(char* col, unsigned char& x) { parse_unsigned_integer(col, x); } 942 | template 943 | void parse(char* col, unsigned short& x) { parse_unsigned_integer(col, x); } 944 | template 945 | void parse(char* col, unsigned int& x) { parse_unsigned_integer(col, x); } 946 | template 947 | void parse(char* col, unsigned long& x) { parse_unsigned_integer(col, x); } 948 | template 949 | void parse(char* col, unsigned long long& x) { parse_unsigned_integer(col, x); } 950 | 951 | template 952 | void parse_signed_integer(const char* col, T& x) { 953 | if (*col == '-') { 954 | ++col; 955 | 956 | x = 0; 957 | while (*col != '\0') { 958 | if ('0' <= *col && *col <= '9') { 959 | T y = *col - '0'; 960 | if (x < ((std::numeric_limits::min)() + y) / 10) { 961 | overflow_policy::on_underflow(x); 962 | return; 963 | } 964 | x = 10 * x - y; 965 | } else 966 | throw error::no_digit(); 967 | ++col; 968 | } 969 | return; 970 | } else if (*col == '+') 971 | ++col; 972 | parse_unsigned_integer(col, x); 973 | } 974 | 975 | template 976 | void parse(char* col, signed char& x) { parse_signed_integer(col, x); } 977 | template 978 | void parse(char* col, signed short& x) { parse_signed_integer(col, x); } 979 | template 980 | void parse(char* col, signed int& x) { parse_signed_integer(col, x); } 981 | template 982 | void parse(char* col, signed long& x) { parse_signed_integer(col, x); } 983 | template 984 | void parse(char* col, signed long long& x) { parse_signed_integer(col, x); } 985 | 986 | template 987 | void parse_float(const char* col, T& x) { 988 | bool is_neg = false; 989 | if (*col == '-') { 990 | is_neg = true; 991 | ++col; 992 | } else if (*col == '+') 993 | ++col; 994 | 995 | x = 0; 996 | while ('0' <= *col && *col <= '9') { 997 | int y = *col - '0'; 998 | x *= 10; 999 | x += y; 1000 | ++col; 1001 | } 1002 | 1003 | if (*col == '.' || *col == ',') { 1004 | ++col; 1005 | T pos = 1; 1006 | while ('0' <= *col && *col <= '9') { 1007 | pos /= 10; 1008 | int y = *col - '0'; 1009 | ++col; 1010 | x += y * pos; 1011 | } 1012 | } 1013 | 1014 | if (*col == 'e' || *col == 'E') { 1015 | ++col; 1016 | int e; 1017 | 1018 | parse_signed_integer(col, e); 1019 | 1020 | if (e != 0) { 1021 | T base; 1022 | if (e < 0) { 1023 | base = T(0.1); 1024 | e = -e; 1025 | } else { 1026 | base = T(10); 1027 | } 1028 | 1029 | while (e != 1) { 1030 | if ((e & 1) == 0) { 1031 | base = base * base; 1032 | e >>= 1; 1033 | } else { 1034 | x *= base; 1035 | --e; 1036 | } 1037 | } 1038 | x *= base; 1039 | } 1040 | } else { 1041 | if (*col != '\0') 1042 | throw error::no_digit(); 1043 | } 1044 | 1045 | if (is_neg) 1046 | x = -x; 1047 | } 1048 | 1049 | template 1050 | void parse(char* col, float& x) { parse_float(col, x); } 1051 | template 1052 | void parse(char* col, double& x) { parse_float(col, x); } 1053 | template 1054 | void parse(char* col, long double& x) { parse_float(col, x); } 1055 | 1056 | template 1057 | void parse(char* col, T& x) { 1058 | // Mute unused variable compiler warning 1059 | (void) col; 1060 | (void) x; 1061 | // GCC evalutes "false" when reading the template and 1062 | // "sizeof(T)!=sizeof(T)" only when instantiating it. This is why 1063 | // this strange construct is used. 1064 | static_assert(sizeof(T) != sizeof(T), 1065 | "Can not parse this type. Only buildin integrals, floats, char, char*, const char* and std::string are supported"); 1066 | } 1067 | 1068 | } // namespace detail 1069 | 1070 | template , 1072 | class quote_policy = no_quote_escape<','>, 1073 | class overflow_policy = throw_on_overflow, 1074 | class comment_policy = no_comment> 1075 | class CSVReader { 1076 | private: 1077 | LineReader in; 1078 | 1079 | char* row[column_count]{}; 1080 | std::string column_names[column_count]; 1081 | 1082 | std::vector col_order; 1083 | 1084 | template 1085 | void set_column_names(std::string s, ColNames... cols) { 1086 | column_names[column_count - sizeof...(ColNames) - 1] = std::move(s); 1087 | set_column_names(std::forward(cols)...); 1088 | } 1089 | 1090 | void set_column_names() {} 1091 | 1092 | public: 1093 | CSVReader() = delete; 1094 | CSVReader(const CSVReader&) = delete; 1095 | CSVReader& operator=(const CSVReader&); 1096 | 1097 | template 1098 | explicit CSVReader(Args&&... args) : in(std::forward(args)...) { 1099 | std::fill(row, row + column_count, nullptr); 1100 | col_order.resize(column_count); 1101 | for (unsigned i = 0; i < column_count; ++i) { 1102 | col_order[i] = static_cast(i); 1103 | } 1104 | for (unsigned i = 1; i <= column_count; ++i) { 1105 | column_names[i - 1] = "col" + std::to_string(i); 1106 | } 1107 | } 1108 | 1109 | char* next_line() { 1110 | return in.next_line(); 1111 | } 1112 | 1113 | template 1114 | void read_header(ignore_column ignore_policy, ColNames... cols) { 1115 | static_assert(sizeof...(ColNames) >= column_count, "not enough column names specified"); 1116 | static_assert(sizeof...(ColNames) <= column_count, "too many column names specified"); 1117 | try { 1118 | set_column_names(std::forward(cols)...); 1119 | 1120 | char* line = nullptr; 1121 | do { 1122 | line = in.next_line(); 1123 | if (!line) { 1124 | throw error::header_missing(); 1125 | } 1126 | } while (comment_policy::is_comment(line)); 1127 | 1128 | detail::parse_header_line(line, col_order, column_names, ignore_policy); 1129 | } catch (error::with_file_name& err) { 1130 | err.set_file_name(in.get_truncated_file_name()); 1131 | throw; 1132 | } 1133 | } 1134 | 1135 | template 1136 | void set_header(ColNames... cols) { 1137 | static_assert(sizeof...(ColNames) >= column_count, 1138 | "not enough column names specified"); 1139 | static_assert(sizeof...(ColNames) <= column_count, 1140 | "too many column names specified"); 1141 | set_column_names(std::forward(cols)...); 1142 | std::fill(row, row + column_count, nullptr); 1143 | col_order.resize(column_count); 1144 | for (unsigned i = 0; i < column_count; ++i) { 1145 | col_order[i] = static_cast(i); 1146 | } 1147 | } 1148 | 1149 | [[nodiscard]] bool has_column(const std::string& name) const { 1150 | return col_order.end() != std::find(col_order.begin(), col_order.end(), std::find(std::begin(column_names), std::end(column_names), name) - std::begin(column_names)); 1151 | } 1152 | 1153 | void set_file_name(const std::string& file_name) { 1154 | in.set_file_name(file_name); 1155 | } 1156 | 1157 | void set_file_name(const char* file_name) { 1158 | in.set_file_name(file_name); 1159 | } 1160 | 1161 | [[nodiscard]] const char* get_truncated_file_name() const { 1162 | return in.get_truncated_file_name(); 1163 | } 1164 | 1165 | void set_file_line(unsigned file_line) { 1166 | in.set_file_line(file_line); 1167 | } 1168 | 1169 | [[nodiscard]] unsigned get_file_line() const { 1170 | return in.get_file_line(); 1171 | } 1172 | 1173 | private: 1174 | void parse_helper(std::size_t) {} 1175 | 1176 | template 1177 | void parse_helper(std::size_t r, T& t, ColType&... cols) { 1178 | if (row[r]) { 1179 | try { 1180 | try { 1181 | ::io::detail::parse(row[r], t); 1182 | } catch (error::with_column_content& err) { 1183 | err.set_column_content(row[r]); 1184 | throw; 1185 | } 1186 | } catch (error::with_column_name& err) { 1187 | err.set_column_name(column_names[r].c_str()); 1188 | throw; 1189 | } 1190 | } 1191 | parse_helper(r + 1, cols...); 1192 | } 1193 | 1194 | public: 1195 | template 1196 | bool read_row(ColType&... cols) { 1197 | static_assert(sizeof...(ColType) >= column_count, 1198 | "not enough columns specified"); 1199 | static_assert(sizeof...(ColType) <= column_count, 1200 | "too many columns specified"); 1201 | try { 1202 | try { 1203 | char* line = nullptr; 1204 | do { 1205 | line = in.next_line(); 1206 | if (!line) { 1207 | return false; 1208 | } 1209 | } while (comment_policy::is_comment(line)); 1210 | 1211 | detail::parse_line(line, row, col_order); 1212 | 1213 | parse_helper(0, cols...); 1214 | } catch (error::with_file_name& err) { 1215 | err.set_file_name(in.get_truncated_file_name()); 1216 | throw; 1217 | } 1218 | } catch (error::with_file_line& err) { 1219 | err.set_file_line(in.get_file_line()); 1220 | throw; 1221 | } 1222 | 1223 | return true; 1224 | } 1225 | }; 1226 | } // namespace io 1227 | #endif 1228 | // NOLINTEND -------------------------------------------------------------------------------- /src/include/jutta_bt_proto/CoffeeMaker.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "bt/BLEDevice.hpp" 4 | #include "date/date.hpp" 5 | #include "jutta_bt_proto/CoffeeMakerLoader.hpp" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | //--------------------------------------------------------------------------- 18 | namespace jutta_bt_proto { 19 | //--------------------------------------------------------------------------- 20 | struct RelevantUUIDs { 21 | uuid_t DEFAULT_SERVICE_UUID{}; 22 | uuid_t ABOUT_MACHINE_CHARACTERISTIC_UUID{}; 23 | uuid_t MACHINE_STATUS_CHARACTERISTIC_UUID{}; 24 | uuid_t BARISTA_MODE_CHARACTERISTIC_UUID{}; 25 | uuid_t PRODUCT_PROGRESS_CHARACTERISTIC_UUID{}; 26 | uuid_t P_MODE_CHARACTERISTIC_UUID{}; 27 | uuid_t P_MODE_READ_CHARACTERISTIC_UUID{}; 28 | uuid_t START_PRODUCT_CHARACTERISTIC_UUID{}; 29 | uuid_t STATISTICS_COMMAND_CHARACTERISTIC_UUID{}; 30 | uuid_t STATISTICS_DATA_CHARACTERISTIC_UUID{}; 31 | uuid_t UPDATE_PRODUCT_CHARACTERISTIC_UUID{}; 32 | 33 | uuid_t UART_SERVICE_UUID{}; 34 | uuid_t UART_TX_CHARACTERISTIC_UUID{}; 35 | uuid_t UART_RX_CHARACTERISTIC_UUID{}; 36 | 37 | RelevantUUIDs() noexcept; 38 | 39 | private: 40 | static void to_uuid(const std::string& s, uuid_t* uuid); 41 | } __attribute__((aligned(128))); 42 | 43 | enum CoffeeMakerState { 44 | DISCONNECTED, 45 | CONNECTING, 46 | CONNECTED, 47 | DISCONNECTING 48 | }; 49 | 50 | enum StatParseMode : uint16_t { 51 | /** 52 | * Triggers the Joe::productStatisticCountersChangedEventHandler event handler. 53 | * It contains a pointer to Joe containing the products with their individual counters. 54 | **/ 55 | PRODUCT_COUNTERS = 1, 56 | /** 57 | * Triggers the Joe::maintenanceCountersChangedEventHandler event handler. 58 | **/ 59 | MAINTENANCE_COUNTER = 4, 60 | /** 61 | * Triggers the Joe::maintenancePercentagesChangedEventHandler event handler. 62 | **/ 63 | MAINTENANCE_PERCENT = 8 64 | }; 65 | 66 | struct ManufacturerData { 67 | uint8_t key{0}; 68 | uint8_t bfMajVer{0}; 69 | uint8_t bfMinVer{0}; 70 | uint16_t articleNumber{0}; 71 | uint16_t machineNumber{0}; 72 | uint16_t serialNumber{0}; 73 | date::year_month_day machineProdDate{}; 74 | date::year_month_day machineProdDateUCHI{}; 75 | uint8_t unusedSecond{0}; 76 | uint8_t statusBits{0}; 77 | } __attribute__((aligned(32))); 78 | 79 | struct AboutData { 80 | std::string blueFrogVersion{}; 81 | std::string coffeeMachineVersion{}; 82 | } __attribute__((aligned(64))); 83 | 84 | class CoffeeMaker { 85 | public: 86 | static const RelevantUUIDs RELEVANT_UUIDS; 87 | 88 | // Event handler: 89 | eventpp::CallbackList stateChangedEventHandler; 90 | eventpp::CallbackList manDataChangedEventHandler; 91 | eventpp::CallbackList aboutDataChangedEventHandler; 92 | eventpp::CallbackList&)> joeChangedEventHandler; 93 | 94 | private: 95 | bt::BLEDevice bleDevice; 96 | CoffeeMakerState state{CoffeeMakerState::DISCONNECTED}; 97 | std::optional heartbeatThread{std::nullopt}; 98 | 99 | const std::unordered_map machines; 100 | 101 | std::shared_ptr joe{nullptr}; 102 | ManufacturerData manData{}; 103 | AboutData aboutData{}; 104 | std::vector alerts{}; 105 | 106 | StatParseMode statParserMode{}; 107 | bool statDataReady{false}; 108 | 109 | public: 110 | explicit CoffeeMaker(std::string&& name, std::string&& addr); 111 | CoffeeMaker(CoffeeMaker&&) = default; 112 | CoffeeMaker(const CoffeeMaker&) = delete; 113 | CoffeeMaker& operator=(CoffeeMaker&&) = delete; 114 | CoffeeMaker& operator=(const CoffeeMaker&) = delete; 115 | ~CoffeeMaker() = default; 116 | 117 | /** 118 | * Connects to the bluetooth device and returns true on success. 119 | **/ 120 | bool connect(); 121 | /** 122 | * Gracefully disconnects from the coffee maker. 123 | **/ 124 | void disconnect(); 125 | /** 126 | * Returns the CoffeeMakerState indicating the current connection state. 127 | **/ 128 | [[nodiscard]] CoffeeMakerState get_state() const; 129 | [[nodiscard]] const std::shared_ptr& get_joe() const; 130 | [[nodiscard]] const ManufacturerData& get_man_data() const; 131 | [[nodiscard]] const AboutData& get_about_data() const; 132 | [[nodiscard]] const std::vector& get_alerts() const; 133 | /** 134 | * Performs a graceful shutdown with rinsing. 135 | **/ 136 | void shutdown(); 137 | /** 138 | * Requests the current status of the coffee maker. 139 | **/ 140 | void request_status(); 141 | /** 142 | * Requests the current product progress. 143 | **/ 144 | void request_progress(); 145 | /** 146 | * Requests the about coffee maker info. 147 | **/ 148 | void request_about_info(); 149 | /** 150 | * Heartbeat that should be send at least once every ten seconds, so the coffee maker stays connected. 151 | **/ 152 | void stay_in_ble(); 153 | /** 154 | * Reads from the RX characteristic. 155 | **/ 156 | void read_rx(); 157 | /** 158 | * Sends the given data to the TX characteristic. 159 | * Before sending, the data will be encoded. 160 | **/ 161 | void write_tx(const std::vector& data); 162 | /** 163 | * Sends the given string to the TX characteristic. 164 | * Before sending, the data will be encoded. 165 | **/ 166 | void write_tx(const std::string& s); 167 | void request_coffee(); 168 | void request_coffee(const Product& product); 169 | /** 170 | * Requests product or maintenance statistics. 171 | * On success the appropriate event gets triggered inside Joe. 172 | **/ 173 | void request_statistics(StatParseMode mode); 174 | 175 | /** 176 | * Locks the coffee maker screen and disables all button input until unlock() is called. 177 | **/ 178 | void lock(); 179 | /** 180 | * Unlocks the coffee maker screen again after you have locked it with lock(). 181 | **/ 182 | void unlock(); 183 | 184 | private: 185 | void set_state(CoffeeMakerState state); 186 | /** 187 | * Analyzes the manufacturer specific data from the advertisement send by the coffee maker. 188 | * Extracts the encryption key, machine number, serial number, ... 189 | **/ 190 | void analyze_man_data(); 191 | 192 | void parse_man_data(const std::vector& data); 193 | void parse_about_data(const std::vector& data); 194 | static void parse_product_progress(const std::vector& data, uint8_t key); 195 | void parse_machine_status(const std::vector& data, uint8_t key); 196 | static void parse_rx(const std::vector& data, uint8_t key); 197 | static std::string parse_version(const std::vector& data, size_t from, size_t to); 198 | void parse_statistics_command(const std::vector& data, uint8_t key); 199 | void parse_statistics_data(const std::vector& data, uint8_t key); 200 | void parse_maintainence_counter_data(const std::vector& data); 201 | void parse_maintainence_percent_data(const std::vector& data); 202 | void parse_product_counter_data(const std::vector& data); 203 | 204 | static size_t get_stat_val(const std::vector& data, size_t offset, size_t bytesPerVal); 205 | void append_prod_stat_bits(std::vector data) const; 206 | /** 207 | * Converts the given data to an uint16_t from little-endian. 208 | **/ 209 | static uint16_t to_uint16_t_little_endian(const std::vector& data, size_t offset); 210 | /** 211 | * Parses the given data as a date::year_month_day object. 212 | **/ 213 | static date::year_month_day to_ymd(const std::vector& data, size_t offset); 214 | /** 215 | * Writes the given data to the given characteristic. 216 | * Allows you to specify wether the data should be encoded and the key inside the data should be overriden. 217 | * Usually you only want to set encode to true. 218 | **/ 219 | bool write(const uuid_t& characteristic, const std::vector& data, bool encode, bool overrideKey); 220 | /** 221 | * Event handler that gets triggered when a characteristic got read. 222 | * data: The data read which might be encoded and has to be decoded. 223 | **/ 224 | void on_characteristic_read(const std::vector& data, const uuid_t& uuid); 225 | /** 226 | * Event handler that gets triggered when the coffee maker is connected. 227 | **/ 228 | void on_connected(); 229 | /** 230 | * Event handler that gets triggered when the coffee maker is disconnected. 231 | **/ 232 | void on_disconnected(); 233 | /** 234 | * Handels the periodic sending of the heartbeat to the coffee maker. 235 | * Should be the entry point of a new thread. 236 | **/ 237 | void heartbeat_run(); 238 | static std::vector build_stats_cmd(StatParseMode mode); 239 | }; 240 | //--------------------------------------------------------------------------- 241 | } // namespace jutta_bt_proto 242 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/include/jutta_bt_proto/CoffeeMakerLoader.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | //--------------------------------------------------------------------------- 14 | namespace jutta_bt_proto { 15 | //--------------------------------------------------------------------------- 16 | struct Machine { 17 | size_t articleNumber; 18 | std::string name; 19 | std::string fileName; 20 | uint8_t version; 21 | 22 | Machine(size_t articleNumber, std::string&& name, std::string&& fileName, uint8_t version) : articleNumber(articleNumber), 23 | name(std::move(name)), 24 | fileName(std::move(fileName)), 25 | version(version){}; 26 | } __attribute__((aligned(128))); 27 | 28 | struct Item { 29 | std::string name; 30 | std::string value; 31 | 32 | Item(std::string&& name, std::string&& value) : name(std::move(name)), 33 | value(std::move(value)) {} 34 | } __attribute__((aligned(64))); 35 | 36 | struct ItemsOption { 37 | std::string argument; 38 | std::string defaultValue; 39 | std::vector items; 40 | 41 | ItemsOption(std::string&& argument, std::string&& defaultValue, std::vector&& items) : argument(std::move(argument)), 42 | defaultValue(std::move(defaultValue)), 43 | items(std::move(items)) {} 44 | 45 | void to_bt_command(std::vector& command) const; 46 | } __attribute__((aligned(128))); 47 | 48 | struct MinMaxOption { 49 | std::string argument; 50 | uint8_t value; 51 | uint8_t min; 52 | uint8_t max; 53 | uint8_t step; 54 | 55 | MinMaxOption(std::string&& argument, uint8_t value, uint8_t min, uint8_t max, uint8_t step) : argument(std::move(argument)), 56 | value(value), 57 | min(min), 58 | max(max), 59 | step(step) {} 60 | 61 | void to_bt_command(std::vector& command) const; 62 | } __attribute__((aligned(64))); 63 | 64 | struct Product { 65 | std::string name; 66 | std::string code; 67 | 68 | std::optional strength; 69 | std::optional temperature; 70 | std::optional waterAmount; 71 | std::optional milkFoamAmount; 72 | 73 | size_t statCounter{0}; 74 | 75 | Product(std::string&& name, std::string&& code, std::optional&& strength, std::optional&& temperature, std::optional&& waterAmount, std::optional milkFoamAmount) : name(std::move(name)), 76 | code(std::move(code)), 77 | strength(std::move(strength)), 78 | temperature(std::move(temperature)), 79 | waterAmount(std::move(waterAmount)), 80 | milkFoamAmount(std::move(milkFoamAmount)) {} 81 | 82 | [[nodiscard]] std::string to_bt_command() const; 83 | [[nodiscard]] size_t code_to_size_t() const; 84 | } __attribute__((aligned(128))); 85 | 86 | struct Alert { 87 | size_t bit; 88 | std::string name; 89 | std::string type; 90 | 91 | Alert(size_t bit, std::string&& name, std::string&& type) : bit(bit), 92 | name(std::move(name)), 93 | type(std::move(type)) {} 94 | } __attribute__((aligned(128))); 95 | 96 | struct MaintenanceCounter { 97 | std::string name; 98 | uint16_t count; 99 | 100 | MaintenanceCounter(std::string&& name, uint16_t count) : name(std::move(name)), 101 | count(count) {} 102 | } __attribute__((aligned(64))); 103 | 104 | struct MaintenancePercentage { 105 | std::string name; 106 | uint8_t percent; 107 | 108 | MaintenancePercentage(std::string&& name, uint8_t percent) : name(std::move(name)), 109 | percent(percent) {} 110 | } __attribute__((aligned(64))); 111 | 112 | struct Joe { 113 | std::string dated; 114 | const Machine* machine; 115 | std::vector products; 116 | std::vector alerts; 117 | std::vector maintenanceCounters; 118 | std::vector maintenancePercentages; 119 | 120 | size_t statTotalCount{0}; 121 | 122 | // Events: 123 | eventpp::CallbackList&)> alertsChangedEventHandler; 124 | eventpp::CallbackList&)> productStatisticCountersChangedEventHandler; 125 | eventpp::CallbackList&)> maintenanceCountersChangedEventHandler; 126 | eventpp::CallbackList&)> maintenancePercentagesChangedEventHandler; 127 | 128 | Joe(std::string&& dated, const Machine* machine, std::vector&& products, std::vector&& alerts, std::vector&& maintenanceCounters, std::vector&& maintenancePercentages) : dated(std::move(dated)), 129 | machine(machine), 130 | products(std::move(products)), 131 | alerts(std::move(alerts)), 132 | maintenanceCounters(std::move(maintenanceCounters)), 133 | maintenancePercentages(std::move(maintenancePercentages)) {} 134 | } __attribute__((aligned(128))); 135 | 136 | std::unordered_map load_machines(const std::filesystem::path& path); 137 | std::shared_ptr load_joe(const Machine* machine); 138 | //--------------------------------------------------------------------------- 139 | } // namespace jutta_bt_proto 140 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/include/jutta_bt_proto/Utils.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | //--------------------------------------------------------------------------- 9 | namespace jutta_bt_proto { 10 | //--------------------------------------------------------------------------- 11 | std::string to_hex_string(const std::vector& data); 12 | std::vector from_hex_string(const std::string& hex); 13 | //--------------------------------------------------------------------------- 14 | } // namespace jutta_bt_proto 15 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/include/logger/Logger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace logger { 7 | const std::filesystem::path log_folder("logs"); 8 | // Setup the logger, note the loglevel can not be set below the CMAKE log level (To change this use -DLOG_LEVEL=...) 9 | void setup_logger(const spdlog::level::level_enum level); 10 | void set_log_level(const spdlog::level::level_enum level); 11 | void deactivate_logger(); 12 | } // namespace logger 13 | -------------------------------------------------------------------------------- /src/io/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | add_library(io INTERFACE) 4 | 5 | install(TARGETS io) 6 | -------------------------------------------------------------------------------- /src/jutta_bt_proto/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | add_library(jutta_bt_proto SHARED CoffeeMaker.cpp 4 | Utils.cpp 5 | CoffeeMakerLoader.cpp) 6 | 7 | target_link_libraries(jutta_bt_proto PUBLIC bt date eventpp 8 | PRIVATE logger tinyxml2::tinyxml2 gattlib) 9 | 10 | # Set version for shared libraries. 11 | set_target_properties(jutta_bt_proto PROPERTIES 12 | VERSION ${${PROJECT_NAME}_VERSION} 13 | SOVERSION ${${PROJECT_NAME}_VERSION_MAJOR}) 14 | 15 | install(TARGETS jutta_bt_proto) 16 | -------------------------------------------------------------------------------- /src/jutta_bt_proto/CoffeeMaker.cpp: -------------------------------------------------------------------------------- 1 | #include // Include first since we have some structs forward declared 2 | 3 | #include "bt/ByteEncDecoder.hpp" 4 | #include "date/date.hpp" 5 | #include "jutta_bt_proto/CoffeeMaker.hpp" 6 | #include "jutta_bt_proto/CoffeeMakerLoader.hpp" 7 | #include "jutta_bt_proto/Utils.hpp" 8 | #include "logger/Logger.hpp" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | //--------------------------------------------------------------------------- 22 | namespace jutta_bt_proto { 23 | //--------------------------------------------------------------------------- 24 | RelevantUUIDs::RelevantUUIDs() noexcept { 25 | try { 26 | to_uuid("5a401523-ab2e-2548-c435-08c300000710", &DEFAULT_SERVICE_UUID); 27 | to_uuid("5A401531-AB2E-2548-C435-08C300000710", &ABOUT_MACHINE_CHARACTERISTIC_UUID); 28 | to_uuid("5a401524-ab2e-2548-c435-08c300000710", &MACHINE_STATUS_CHARACTERISTIC_UUID); 29 | to_uuid("5a401530-ab2e-2548-c435-08c300000710", &BARISTA_MODE_CHARACTERISTIC_UUID); 30 | to_uuid("5a401527-ab2e-2548-c435-08c300000710", &PRODUCT_PROGRESS_CHARACTERISTIC_UUID); 31 | to_uuid("5a401529-ab2e-2548-c435-08c300000710", &P_MODE_CHARACTERISTIC_UUID); 32 | to_uuid("5a401538-ab2e-2548-c435-08c300000710", &P_MODE_READ_CHARACTERISTIC_UUID); 33 | to_uuid("5a401525-ab2e-2548-c435-08c300000710", &START_PRODUCT_CHARACTERISTIC_UUID); 34 | to_uuid("5A401533-ab2e-2548-c435-08c300000710", &STATISTICS_COMMAND_CHARACTERISTIC_UUID); 35 | to_uuid("5A401534-ab2e-2548-c435-08c300000710", &STATISTICS_DATA_CHARACTERISTIC_UUID); 36 | to_uuid("5a401528-ab2e-2548-c435-08c300000710", &UPDATE_PRODUCT_CHARACTERISTIC_UUID); 37 | 38 | to_uuid("5a401623-ab2e-2548-c435-08c300000710", &UART_SERVICE_UUID); 39 | to_uuid("5a401624-ab2e-2548-c435-08c300000710", &UART_RX_CHARACTERISTIC_UUID); 40 | to_uuid("5a401625-ab2e-2548-c435-08c300000710", &UART_TX_CHARACTERISTIC_UUID); 41 | } catch (const std::exception& e) { 42 | SPDLOG_ERROR("Loading UUIDS failed with: {}", e.what()); 43 | std::terminate(); 44 | } 45 | } 46 | 47 | void RelevantUUIDs::to_uuid(const std::string& s, uuid_t* uuid) { 48 | gattlib_string_to_uuid(s.data(), s.size(), uuid); 49 | } 50 | 51 | const RelevantUUIDs CoffeeMaker::RELEVANT_UUIDS{}; 52 | 53 | CoffeeMaker::CoffeeMaker(std::string&& name, std::string&& addr) : bleDevice( 54 | std::move(name), 55 | std::move(addr), 56 | [this](const std::vector& data, const uuid_t& uuid) { this->on_characteristic_read(data, uuid); }, 57 | [this]() { this->on_connected(); }, 58 | [this]() { this->on_disconnected(); }, 59 | [this](const std::vector& data, const uuid_t& uuid) { this->on_characteristic_read(data, uuid); }), 60 | machines(load_machines("machinefiles/JOE_MACHINES.TXT")) {} 61 | 62 | std::string CoffeeMaker::parse_version(const std::vector& data, size_t from, size_t to) { 63 | std::string result; 64 | for (size_t i = from; i <= to; i++) { 65 | if (data[i]) { 66 | result += static_cast(data[i]); 67 | } 68 | } 69 | return result; 70 | } 71 | 72 | void CoffeeMaker::parse_about_data(const std::vector& data) { 73 | std::string blueFrogVersion = parse_version(data, 27, 34); 74 | std::string coffeeMachineVersion = parse_version(data, 35, 50); 75 | if (blueFrogVersion != aboutData.blueFrogVersion || coffeeMachineVersion != aboutData.coffeeMachineVersion) { 76 | aboutData.blueFrogVersion = std::move(blueFrogVersion); 77 | aboutData.coffeeMachineVersion = std::move(coffeeMachineVersion); 78 | SPDLOG_DEBUG("Found new about data. BlueFrog Version: {} Coffee Makers Version: {}", aboutData.blueFrogVersion, aboutData.coffeeMachineVersion); 79 | 80 | // Invoke the about data event handler: 81 | if (aboutDataChangedEventHandler) { 82 | aboutDataChangedEventHandler(aboutData); 83 | } 84 | } 85 | } 86 | 87 | void CoffeeMaker::parse_machine_status(const std::vector& data, uint8_t key) { 88 | if (!joe) { 89 | return; 90 | } 91 | 92 | std::vector newAlerts; 93 | std::vector alertVec = bt::encDecBytes(data, key); 94 | for (size_t i = 0; i < (alertVec.size() - 1) << 3; i++) { 95 | size_t offsetAbs = (i >> 3) + 1; 96 | size_t offsetByte = 7 - (i & 0b111); 97 | if ((alertVec[offsetAbs] >> offsetByte) & 0b1) { 98 | for (const Alert& alert : joe->alerts) { 99 | if (alert.bit == i) { 100 | newAlerts.push_back(&alert); 101 | } 102 | } 103 | } 104 | } 105 | 106 | if (alerts != newAlerts) { 107 | alerts.clear(); 108 | alerts.insert(alerts.end(), newAlerts.begin(), newAlerts.end()); 109 | 110 | // Invoke the alerts event handler: 111 | if (joe->alertsChangedEventHandler) { 112 | joe->alertsChangedEventHandler(alerts); 113 | } 114 | } 115 | } 116 | 117 | void CoffeeMaker::parse_product_progress(const std::vector& data, uint8_t key) { 118 | std::vector actData = bt::encDecBytes(data, key); 119 | } 120 | 121 | void CoffeeMaker::analyze_man_data() { 122 | parse_man_data(bleDevice.get_mam_data()); 123 | } 124 | 125 | void CoffeeMaker::parse_man_data(const std::vector& data) { 126 | manData.key = data[0]; 127 | manData.bfMajVer = data[1]; 128 | manData.bfMinVer = data[2]; 129 | manData.articleNumber = to_uint16_t_little_endian(data, 4); 130 | manData.machineNumber = to_uint16_t_little_endian(data, 6); 131 | manData.serialNumber = to_uint16_t_little_endian(data, 8); 132 | manData.machineProdDate = to_ymd(data, 10); 133 | manData.machineProdDateUCHI = to_ymd(data, 12); 134 | manData.unusedSecond = data[14]; 135 | manData.statusBits = data[15]; 136 | 137 | // Invoke the manufacturer data event handler: 138 | if (manDataChangedEventHandler) { 139 | manDataChangedEventHandler(manData); 140 | } 141 | 142 | // Load machine: 143 | if (!machines.contains(manData.articleNumber)) { 144 | SPDLOG_ERROR("Coffee maker with article number '{}' not supported with the given machine files.", manData.articleNumber); 145 | // NOLINTNEXTLINE(concurrency-mt-unsafe) 146 | exit(-1); 147 | } 148 | const Machine* machine = &(machines.at(manData.articleNumber)); 149 | joe = load_joe(machine); 150 | alerts.clear(); 151 | SPDLOG_INFO("Found machine '{}' Version: {} with {} products.", machine->name, machine->version, joe->products.size()); 152 | 153 | // Invoke the JOE event handler: 154 | if (joeChangedEventHandler) { 155 | joeChangedEventHandler(joe); 156 | } 157 | } 158 | 159 | void CoffeeMaker::parse_rx(const std::vector& data, uint8_t key) { 160 | std::vector actData = bt::encDecBytes(data, key); 161 | SPDLOG_INFO("Read from RX (dec hex): {}", to_hex_string(actData)); 162 | SPDLOG_INFO("Read from RX (dec str): {}", std::string(actData.begin(), actData.end())); 163 | } 164 | 165 | /** 166 | * Parses the statistics command response and prints an error in case the response indicates an unsuccessful action. 167 | **/ 168 | void CoffeeMaker::parse_statistics_command(const std::vector& data, uint8_t key) { 169 | std::vector actData = bt::encDecBytes(data, key); 170 | // In case the received data starts with '0x0E', the statistics command has been successful. 171 | statDataReady = actData.size() > 1 && actData[0] == 0x0E; 172 | 173 | if (statDataReady) { 174 | SPDLOG_DEBUG("Successful statistics command: {}", to_hex_string(actData)); 175 | } else { 176 | SPDLOG_DEBUG("Statistics data not ready yet."); 177 | } 178 | SPDLOG_TRACE("Statistics data received: {}", to_hex_string(actData)); 179 | } 180 | 181 | size_t CoffeeMaker::get_stat_val(const std::vector& data, size_t offset, size_t bytesPerVal) { 182 | const size_t valueOffset = offset * bytesPerVal; 183 | if (data.size() < valueOffset + bytesPerVal) { 184 | return 0; 185 | } 186 | assert(data.size() >= valueOffset + bytesPerVal); 187 | 188 | size_t result = 0; 189 | for (size_t i = 0; i < bytesPerVal; i++) { 190 | result <<= 8; 191 | result |= data[valueOffset + i]; 192 | } 193 | return result; 194 | } 195 | 196 | void CoffeeMaker::parse_statistics_data(const std::vector& data, uint8_t key) { 197 | std::vector actData = bt::encDecBytes(data, key); 198 | SPDLOG_DEBUG("Read statistics data: {}", to_hex_string(actData)); 199 | 200 | switch (statParserMode) { 201 | case StatParseMode::MAINTENANCE_COUNTER: 202 | parse_maintainence_counter_data(actData); 203 | break; 204 | 205 | case StatParseMode::MAINTENANCE_PERCENT: 206 | parse_maintainence_percent_data(actData); 207 | break; 208 | 209 | case StatParseMode::PRODUCT_COUNTERS: 210 | parse_product_counter_data(actData); 211 | break; 212 | } 213 | } 214 | 215 | void CoffeeMaker::parse_maintainence_percent_data(const std::vector& data) { 216 | for (size_t i = 0; i < joe->maintenancePercentages.size(); i++) { 217 | joe->maintenancePercentages[i].percent = static_cast(get_stat_val(data, i, 1)); 218 | SPDLOG_DEBUG("{}: {}%", joe->maintenancePercentages[i].name, joe->maintenancePercentages[i].percent); 219 | } 220 | 221 | // Invoke the event handler: 222 | if (joe->maintenancePercentagesChangedEventHandler) { 223 | joe->maintenancePercentagesChangedEventHandler(joe->maintenancePercentages); 224 | } 225 | } 226 | 227 | void CoffeeMaker::parse_maintainence_counter_data(const std::vector& data) { 228 | for (size_t i = 0; i < joe->maintenanceCounters.size(); i++) { 229 | joe->maintenanceCounters[i].count = static_cast(get_stat_val(data, i, 2)); 230 | SPDLOG_DEBUG("{}: {}", joe->maintenanceCounters[i].name, joe->maintenanceCounters[i].count); 231 | } 232 | 233 | // Invoke the event handler: 234 | if (joe->maintenanceCountersChangedEventHandler) { 235 | joe->maintenanceCountersChangedEventHandler(joe->maintenanceCounters); 236 | } 237 | } 238 | 239 | void CoffeeMaker::parse_product_counter_data(const std::vector& data) { 240 | joe->statTotalCount = get_stat_val(data, 0, 3); 241 | SPDLOG_INFO("Total number of products: {}", joe->statTotalCount); 242 | 243 | for (Product& p : joe->products) { 244 | size_t code = p.code_to_size_t(); 245 | size_t result = get_stat_val(data, code, 3); 246 | if (result != 0xFFFF) { 247 | p.statCounter = result; 248 | SPDLOG_DEBUG("Product {}: {}", p.name, result); 249 | } else { 250 | p.statCounter = 0; 251 | SPDLOG_WARN("Product {} has invalid counter!", p.name); 252 | } 253 | } 254 | 255 | // Invoke the event handler: 256 | if (joe->productStatisticCountersChangedEventHandler) { 257 | joe->productStatisticCountersChangedEventHandler(joe); 258 | } 259 | } 260 | 261 | uint16_t CoffeeMaker::to_uint16_t_little_endian(const std::vector& data, size_t offset) { 262 | return (static_cast(data[offset + 1]) << 8) | static_cast(data[offset]); 263 | } 264 | 265 | date::year_month_day CoffeeMaker::to_ymd(const std::vector& data, size_t offset) { 266 | uint16_t date = to_uint16_t_little_endian(data, offset); 267 | return date::year(((date & 65024) >> 9) + 1990) / ((date & 480) >> 5) / (date & 31); 268 | } 269 | 270 | void CoffeeMaker::on_characteristic_read(const std::vector& data, const uuid_t& uuid) { 271 | std::array uuidStr{}; 272 | gattlib_uuid_to_string(&uuid, uuidStr.data(), uuidStr.size()); 273 | SPDLOG_TRACE("Received {} bytes of data from characteristic '{}'.", data.size(), uuidStr.data()); 274 | 275 | // About UUID: 276 | if (gattlib_uuid_cmp(&uuid, &RELEVANT_UUIDS.ABOUT_MACHINE_CHARACTERISTIC_UUID) == GATTLIB_SUCCESS) { 277 | parse_about_data(data); 278 | } 279 | // Machine status: 280 | else if (gattlib_uuid_cmp(&uuid, &RELEVANT_UUIDS.MACHINE_STATUS_CHARACTERISTIC_UUID) == GATTLIB_SUCCESS) { 281 | parse_machine_status(data, manData.key); 282 | } 283 | // Product progress: 284 | else if (gattlib_uuid_cmp(&uuid, &RELEVANT_UUIDS.PRODUCT_PROGRESS_CHARACTERISTIC_UUID) == GATTLIB_SUCCESS) { 285 | parse_product_progress(data, manData.key); 286 | } 287 | // RX: 288 | else if (gattlib_uuid_cmp(&uuid, &RELEVANT_UUIDS.UART_RX_CHARACTERISTIC_UUID) == GATTLIB_SUCCESS) { 289 | parse_rx(data, manData.key); 290 | } 291 | // Statistics Command: 292 | else if (gattlib_uuid_cmp(&uuid, &RELEVANT_UUIDS.STATISTICS_COMMAND_CHARACTERISTIC_UUID) == GATTLIB_SUCCESS) { 293 | parse_statistics_command(data, manData.key); 294 | } 295 | // Statistics Data: 296 | else if (gattlib_uuid_cmp(&uuid, &RELEVANT_UUIDS.STATISTICS_DATA_CHARACTERISTIC_UUID) == GATTLIB_SUCCESS) { 297 | parse_statistics_data(data, manData.key); 298 | } else { 299 | // TODO print 300 | } 301 | } 302 | void CoffeeMaker::request_status() { 303 | bleDevice.read_characteristic(RELEVANT_UUIDS.MACHINE_STATUS_CHARACTERISTIC_UUID); 304 | } 305 | 306 | void CoffeeMaker::request_progress() { 307 | bleDevice.read_characteristic(RELEVANT_UUIDS.PRODUCT_PROGRESS_CHARACTERISTIC_UUID); 308 | } 309 | 310 | void CoffeeMaker::request_about_info() { 311 | bleDevice.read_characteristic(RELEVANT_UUIDS.ABOUT_MACHINE_CHARACTERISTIC_UUID); 312 | } 313 | 314 | void CoffeeMaker::read_rx() { 315 | bleDevice.read_characteristic(RELEVANT_UUIDS.UART_RX_CHARACTERISTIC_UUID); 316 | } 317 | 318 | void CoffeeMaker::write_tx(const std::string& s) { 319 | const std::vector data(s.begin(), s.end()); 320 | write_tx(data); 321 | } 322 | 323 | void CoffeeMaker::write_tx(const std::vector& data) { 324 | write(RELEVANT_UUIDS.UART_TX_CHARACTERISTIC_UUID, data, true, true); 325 | } 326 | 327 | bool CoffeeMaker::write(const uuid_t& characteristic, const std::vector& data, bool encode, bool overrideKey) { 328 | std::vector encodedData = data; 329 | if (encode) { 330 | encodedData[0] = manData.key; 331 | if (overrideKey) { 332 | encodedData[encodedData.size() - 1] = manData.key; 333 | } 334 | encodedData = bt::encDecBytes(encodedData, manData.key); 335 | } 336 | SPDLOG_TRACE("Wrote: {}", to_hex_string(encodedData)); 337 | return bleDevice.write(characteristic, encodedData); 338 | } 339 | 340 | void CoffeeMaker::shutdown() { 341 | SPDLOG_DEBUG("Shutting down the coffee maker..."); 342 | static const std::vector command{0x00, 0x46, 0x02}; 343 | write(RELEVANT_UUIDS.P_MODE_CHARACTERISTIC_UUID, command, true, false); 344 | } 345 | 346 | void CoffeeMaker::request_coffee() { 347 | SPDLOG_DEBUG("Requesting coffee..."); 348 | static const std::string commandHexStr = "00030004280000020001000000000000"; 349 | // static const std::string commandHexStr = "77e93dd55381d3dba32bfa98a4a3faf9"; // Decoded: 2A03000414000001000100000000002A 350 | static const std::vector command = from_hex_string(commandHexStr); 351 | write(RELEVANT_UUIDS.START_PRODUCT_CHARACTERISTIC_UUID, command, true, false); 352 | } 353 | 354 | void CoffeeMaker::request_coffee(const Product& product) { 355 | const std::string commandHexStr = product.to_bt_command(); 356 | const std::vector command = from_hex_string(commandHexStr); 357 | write(RELEVANT_UUIDS.START_PRODUCT_CHARACTERISTIC_UUID, command, true, true); 358 | } 359 | 360 | void CoffeeMaker::append_prod_stat_bits(std::vector data) const { 361 | std::array bArr{0}; 362 | 363 | for (const Product& p : joe->products) { 364 | size_t code = p.code_to_size_t(); 365 | 366 | code /= 4; 367 | size_t arrOffset = code / 8; 368 | assert(arrOffset < bArr.size()); 369 | bArr[arrOffset] |= (1 << (code % 8)); 370 | } 371 | 372 | data.push_back(bArr[0]); 373 | data.push_back(bArr[1]); 374 | } 375 | 376 | void CoffeeMaker::request_statistics(StatParseMode mode) { 377 | // Request statistics: 378 | write(RELEVANT_UUIDS.STATISTICS_COMMAND_CHARACTERISTIC_UUID, build_stats_cmd(mode), true, true); 379 | statParserMode = mode; 380 | statDataReady = false; 381 | 382 | // Wait until the statistics are ready: 383 | std::chrono::milliseconds retryInterval{500}; 384 | for (size_t i = 0; i < 20 && !statDataReady; i++) { 385 | std::this_thread::sleep_for(std::chrono::milliseconds(retryInterval)); 386 | bleDevice.read_characteristic(RELEVANT_UUIDS.STATISTICS_COMMAND_CHARACTERISTIC_UUID); 387 | } 388 | 389 | // Read statistics: 390 | bleDevice.read_characteristic(RELEVANT_UUIDS.STATISTICS_DATA_CHARACTERISTIC_UUID); 391 | } 392 | 393 | void CoffeeMaker::stay_in_ble() { 394 | SPDLOG_DEBUG("Sending stay in BLE mode..."); 395 | static const std::vector command{0x00, 0x7F, 0x80}; 396 | write(RELEVANT_UUIDS.P_MODE_CHARACTERISTIC_UUID, command, true, false); 397 | } 398 | 399 | void CoffeeMaker::on_connected() { 400 | // Ensure we have the key for deobfuscation ready: 401 | const std::vector& manData = bleDevice.get_mam_data(); 402 | if (manData.empty()) { 403 | SPDLOG_WARN("Failed to connect. Invalid manufacturer data."); 404 | disconnect(); 405 | return; 406 | } 407 | parse_man_data(manData); 408 | 409 | // Send the initial heartbeat: 410 | stay_in_ble(); 411 | 412 | // Request basic information: 413 | request_about_info(); 414 | 415 | // Start the heartbeat thread: 416 | assert(!heartbeatThread); 417 | heartbeatThread = std::make_optional(&CoffeeMaker::heartbeat_run, this); 418 | SPDLOG_INFO("Connected."); 419 | } 420 | 421 | void CoffeeMaker::on_disconnected() { 422 | if (state == CoffeeMakerState::CONNECTING || state == CoffeeMakerState::CONNECTED) { 423 | disconnect(); 424 | } 425 | } 426 | 427 | bool CoffeeMaker::connect() { 428 | set_state(CoffeeMakerState::CONNECTING); 429 | if (bleDevice.connect()) { 430 | set_state(CoffeeMakerState::CONNECTED); 431 | return true; 432 | } 433 | set_state(CoffeeMakerState::DISCONNECTED); 434 | SPDLOG_WARN("Failed to connect."); 435 | return false; 436 | } 437 | 438 | void CoffeeMaker::disconnect() { 439 | if (state == CoffeeMakerState::CONNECTING || state == CoffeeMakerState::CONNECTED) { 440 | set_state(CoffeeMakerState::DISCONNECTING); 441 | 442 | // Send the disconnect command: 443 | static const std::vector command{0x00, 0x7F, 0x81}; 444 | write(RELEVANT_UUIDS.P_MODE_CHARACTERISTIC_UUID, command, true, false); 445 | 446 | // Join the heartbeat thread: 447 | assert(heartbeatThread); 448 | heartbeatThread->join(); 449 | heartbeatThread = std::nullopt; 450 | set_state(CoffeeMakerState::DISCONNECTED); 451 | SPDLOG_INFO("Disconnected."); 452 | } 453 | } 454 | 455 | CoffeeMakerState CoffeeMaker::get_state() const { return state; } 456 | 457 | const std::shared_ptr& CoffeeMaker::get_joe() const { return joe; } 458 | 459 | const ManufacturerData& CoffeeMaker::get_man_data() const { return manData; } 460 | 461 | const AboutData& CoffeeMaker::get_about_data() const { return aboutData; } 462 | 463 | const std::vector& CoffeeMaker::get_alerts() const { return alerts; } 464 | 465 | void CoffeeMaker::set_state(CoffeeMakerState state) { 466 | if (state != this->state) { 467 | this->state = state; 468 | // Invoke the state event handler: 469 | if (stateChangedEventHandler) { 470 | stateChangedEventHandler(this->state); 471 | } 472 | } 473 | } 474 | 475 | void CoffeeMaker::heartbeat_run() { 476 | SPDLOG_INFO("Heartbeat thread started."); 477 | // NOLINTNEXTLINE (altera-id-dependent-backward-branch) 478 | while (state == CoffeeMakerState::CONNECTED || state == CoffeeMakerState::CONNECTING) { 479 | stay_in_ble(); 480 | request_status(); 481 | std::this_thread::sleep_for(std::chrono::seconds{1}); 482 | } 483 | SPDLOG_INFO("Heartbeat thread ready to be joined."); 484 | } 485 | 486 | std::vector CoffeeMaker::build_stats_cmd(StatParseMode mode) { 487 | std::vector result; 488 | result.resize(5); 489 | // Padding: 490 | result[0] = 0; 491 | 492 | // Statistics type 493 | result[1] = (mode & 0xFF00) >> 8; 494 | result[2] = mode & 0x00FF; 495 | 496 | // Padding: 497 | if (mode == StatParseMode::PRODUCT_COUNTERS) { 498 | // Append all products. An alternative is 0xFFFF to force all products. 499 | // append_prod_stat_bits() is broken currently since it does not 500 | result[3] = 0xFF; 501 | result[4] = 0xFF; 502 | } else { 503 | result[3] = 1; 504 | result[4] = 0; 505 | } 506 | 507 | return result; 508 | } 509 | 510 | void CoffeeMaker::lock() { 511 | write(RELEVANT_UUIDS.BARISTA_MODE_CHARACTERISTIC_UUID, {{0x00, 0x01}}, true, false); 512 | SPDLOG_INFO("Coffee maker locked."); 513 | } 514 | 515 | void CoffeeMaker::unlock() { 516 | write(RELEVANT_UUIDS.BARISTA_MODE_CHARACTERISTIC_UUID, {{0x00, 0x00}}, true, false); 517 | SPDLOG_INFO("Coffee maker unlocked."); 518 | } 519 | 520 | //--------------------------------------------------------------------------- 521 | } // namespace jutta_bt_proto 522 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/jutta_bt_proto/CoffeeMakerLoader.cpp: -------------------------------------------------------------------------------- 1 | #include "jutta_bt_proto/CoffeeMakerLoader.hpp" 2 | #include "io/csv.hpp" 3 | #include "jutta_bt_proto/Utils.hpp" 4 | #include "logger/Logger.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | //--------------------------------------------------------------------------- 17 | namespace jutta_bt_proto { 18 | //--------------------------------------------------------------------------- 19 | void ItemsOption::to_bt_command(std::vector& command) const { 20 | if (argument[0] == 'F') { 21 | std::string offsetStr = argument.substr(1); 22 | std::istringstream iss(offsetStr); 23 | size_t offset = 0; 24 | iss >> offset; 25 | command[offset - 1] = defaultValue; 26 | } else { 27 | SPDLOG_ERROR("Invalid argument when converting to a BT command '{}'", argument); 28 | } 29 | } 30 | 31 | void MinMaxOption::to_bt_command(std::vector& command) const { 32 | if (argument[0] == 'F') { 33 | std::string offsetStr = argument.substr(1); 34 | std::istringstream iss(offsetStr); 35 | size_t offset = 0; 36 | iss >> offset; 37 | uint8_t v = value / step; 38 | command[offset - 1] = to_hex_string(std::vector{v}); 39 | } else { 40 | SPDLOG_ERROR("Invalid argument when converting to a BT command '{}'", argument); 41 | } 42 | } 43 | 44 | std::string Product::to_bt_command() const { 45 | std::vector command; 46 | command.resize(17); 47 | for (std::string& s : command) { 48 | s = "00"; 49 | } 50 | 51 | if (strength) { 52 | strength->to_bt_command(command); 53 | } 54 | 55 | if (temperature) { 56 | temperature->to_bt_command(command); 57 | } 58 | 59 | if (waterAmount) { 60 | waterAmount->to_bt_command(command); 61 | } 62 | 63 | if (milkFoamAmount) { 64 | milkFoamAmount->to_bt_command(command); 65 | } 66 | 67 | // TODO: Add GRINDER_FREENESS 68 | 69 | command[0] = code; 70 | 71 | std::string result = "00"; 72 | for (const std::string& s : command) { 73 | result += s; 74 | } 75 | SPDLOG_DEBUG("Product command: {}", result); 76 | return result; 77 | } 78 | 79 | size_t Product::code_to_size_t() const { 80 | std::vector codeVec = from_hex_string(code); 81 | size_t codeVal = 0; 82 | for (const uint8_t c : codeVec) { 83 | codeVal <<= 8; 84 | codeVal |= c; 85 | } 86 | return codeVal; 87 | } 88 | 89 | std::unordered_map load_machines(const std::filesystem::path& path) { 90 | SPDLOG_INFO("Loading machines..."); 91 | std::unordered_map result; 92 | io::CSVReader<4, io::trim_chars<' ', '\t'>, io::no_quote_escape<';'>> in(path); 93 | // Skip the first line: 94 | in.next_line(); 95 | size_t articleNumber = 0; 96 | std::string name; 97 | std::string fileName; 98 | uint8_t version = 0; 99 | while (in.read_row(articleNumber, name, fileName, version)) { 100 | result.emplace(articleNumber, Machine(articleNumber, std::string{name}, std::string{fileName}, version)); 101 | } 102 | SPDLOG_INFO("Loaded {} machines.", result.size()); 103 | return result; 104 | } 105 | 106 | std::optional load_items_option(const tinyxml2::XMLElement* option) { 107 | std::string argument = option->Attribute("Argument"); 108 | std::string defaultValue = option->Attribute("Default"); 109 | std::vector items; 110 | for (const tinyxml2::XMLElement* e = option->FirstChildElement("ITEM"); e != nullptr; e = e->NextSiblingElement("ITEM")) { 111 | std::string name = e->Attribute("Name"); 112 | std::string value = e->Attribute("Value"); 113 | items.emplace_back(std::move(name), std::move(value)); 114 | } 115 | return std::make_optional(std::move(argument), std::move(defaultValue), std::move(items)); 116 | } 117 | 118 | std::optional load_min_max_option(const tinyxml2::XMLElement* option) { 119 | std::string argument = option->Attribute("Argument"); 120 | uint8_t value = option->IntAttribute("Value"); 121 | uint8_t min = option->IntAttribute("Min"); 122 | uint8_t max = option->IntAttribute("Max"); 123 | uint8_t step = option->IntAttribute("Step"); 124 | return std::make_optional(std::move(argument), value, min, max, step); 125 | } 126 | 127 | void load_products(std::vector* products, tinyxml2::XMLElement* joe) { 128 | tinyxml2::XMLElement* productsXml = joe->FirstChildElement("PRODUCTS"); 129 | for (const tinyxml2::XMLElement* e = productsXml->FirstChildElement("PRODUCT"); e != nullptr; e = e->NextSiblingElement("PRODUCT")) { 130 | std::string name = e->Attribute("Name"); 131 | std::string code = e->Attribute("Code"); 132 | 133 | // Strength: 134 | std::optional strength; 135 | const tinyxml2::XMLElement* strengthXml = e->FirstChildElement("COFFEE_STRENGTH"); 136 | if (strengthXml) { 137 | strength = load_items_option(strengthXml); 138 | } 139 | 140 | // Temperature: 141 | std::optional temperature; 142 | const tinyxml2::XMLElement* temperatureXml = e->FirstChildElement("TEMPERATURE"); 143 | if (temperatureXml) { 144 | temperature = load_items_option(temperatureXml); 145 | } 146 | 147 | // Water amount: 148 | std::optional waterAmount; 149 | const tinyxml2::XMLElement* waterAmountXml = e->FirstChildElement("WATER_AMOUNT"); 150 | if (waterAmountXml) { 151 | waterAmount = load_min_max_option(waterAmountXml); 152 | } 153 | 154 | // Milk Foam Amount: 155 | std::optional milkFoamAmount; 156 | const tinyxml2::XMLElement* milkFoamAmountXml = e->FirstChildElement("MILK_FOAM_AMOUNT"); 157 | if (milkFoamAmountXml) { 158 | milkFoamAmount = load_min_max_option(milkFoamAmountXml); 159 | } 160 | 161 | products->emplace_back(std::move(name), std::move(code), std::move(strength), std::move(temperature), std::move(waterAmount), std::move(milkFoamAmount)); 162 | } 163 | } 164 | 165 | void load_alerts(std::vector* alerts, tinyxml2::XMLElement* joe) { 166 | tinyxml2::XMLElement* alertsXml = joe->FirstChildElement("ALERTS"); 167 | for (const tinyxml2::XMLElement* e = alertsXml->FirstChildElement("ALERT"); e != nullptr; e = e->NextSiblingElement("ALERT")) { 168 | size_t bit = e->IntAttribute("Bit"); 169 | std::string name = e->Attribute("Name"); 170 | const char* typeCStr = e->Attribute("Type"); 171 | std::string type; 172 | if (typeCStr) { 173 | type = typeCStr; 174 | } 175 | alerts->emplace_back(bit, std::move(name), std::move(type)); 176 | } 177 | } 178 | 179 | void load_maintenance_counters(std::vector& maintenanceCounters, tinyxml2::XMLElement* joe) { 180 | tinyxml2::XMLElement* statisticXml = joe->FirstChildElement("STATISTIC"); 181 | assert(statisticXml); 182 | tinyxml2::XMLElement* maintenanceXml = statisticXml->FirstChildElement("MAINTENANCEPAGE"); 183 | assert(maintenanceXml); 184 | 185 | for (const tinyxml2::XMLElement* bankXml = maintenanceXml->FirstChildElement("BANK"); bankXml != nullptr; bankXml = bankXml->NextSiblingElement("BANK")) { 186 | std::string name = bankXml->Attribute("Name"); 187 | if (name == "Maintenance Counter") { 188 | for (const tinyxml2::XMLElement* e = bankXml->FirstChildElement("TEXTITEM"); e != nullptr; e = e->NextSiblingElement("TEXTITEM")) { 189 | std::string name = e->Attribute("Type"); 190 | maintenanceCounters.emplace_back(std::move(name), 0); 191 | } 192 | break; 193 | } 194 | } 195 | } 196 | 197 | void load_maintenance_percentages(std::vector& maintenancePercentages, tinyxml2::XMLElement* joe) { 198 | tinyxml2::XMLElement* statisticXml = joe->FirstChildElement("STATISTIC"); 199 | assert(statisticXml); 200 | tinyxml2::XMLElement* maintenanceXml = statisticXml->FirstChildElement("MAINTENANCEPAGE"); 201 | assert(maintenanceXml); 202 | 203 | for (const tinyxml2::XMLElement* bankXml = maintenanceXml->FirstChildElement("BANK"); bankXml != nullptr; bankXml = bankXml->NextSiblingElement("BANK")) { 204 | std::string name = bankXml->Attribute("Name"); 205 | if (name == "Maintenance Percent") { 206 | for (const tinyxml2::XMLElement* e = bankXml->FirstChildElement("TEXTITEM"); e != nullptr; e = e->NextSiblingElement("TEXTITEM")) { 207 | std::string name = e->Attribute("Type"); 208 | maintenancePercentages.emplace_back(std::move(name), 0); 209 | } 210 | break; 211 | } 212 | } 213 | } 214 | 215 | std::shared_ptr load_joe(const Machine* machine) { 216 | tinyxml2::XMLDocument doc; 217 | std::string path = "machinefiles/" + machine->fileName + ".xml"; 218 | SPDLOG_INFO("Loading JOE from '{}'...", path); 219 | tinyxml2::XMLError result = doc.LoadFile(path.c_str()); 220 | assert(result == tinyxml2::XML_SUCCESS); 221 | tinyxml2::XMLElement* joe = doc.FirstChildElement("JOE"); 222 | 223 | std::string dated = joe->Attribute("dated"); 224 | std::vector products; 225 | load_products(&products, joe); 226 | std::vector alerts; 227 | load_alerts(&alerts, joe); 228 | 229 | std::vector maintenanceCounters; 230 | load_maintenance_counters(maintenanceCounters, joe); 231 | 232 | std::vector maintenancePercentages; 233 | load_maintenance_percentages(maintenancePercentages, joe); 234 | 235 | SPDLOG_INFO("JOE loaded."); 236 | return std::make_shared(std::move(dated), machine, std::move(products), std::move(alerts), std::move(maintenanceCounters), std::move(maintenancePercentages)); 237 | } 238 | //--------------------------------------------------------------------------- 239 | } // namespace jutta_bt_proto 240 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/jutta_bt_proto/Utils.cpp: -------------------------------------------------------------------------------- 1 | #include "jutta_bt_proto/Utils.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | //--------------------------------------------------------------------------- 11 | namespace jutta_bt_proto { 12 | //--------------------------------------------------------------------------- 13 | std::string to_hex_string(const std::vector& data) { 14 | static const std::array HEX_CHARS{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; 15 | 16 | std::string result; 17 | result.resize(data.size() * 2); 18 | for (size_t i = 0; i < data.size(); i++) { 19 | result[i * 2] = HEX_CHARS[data[i] >> 4]; 20 | result[(i * 2) + 1] = HEX_CHARS[data[i] & 0x0F]; 21 | } 22 | 23 | return result; 24 | } 25 | 26 | uint8_t get_hex_char_val(char c) { 27 | uint8_t i = static_cast(std::toupper(c)); 28 | if (i >= 0x30 && i <= 0x39) { 29 | return i -= 0x30; 30 | } 31 | if (i >= 0x41 && i <= 0x46) { 32 | return i = i - 0x41 + 10; 33 | } 34 | assert(false); 35 | } 36 | 37 | std::vector from_hex_string(const std::string& hex) { 38 | std::vector result; 39 | result.resize(hex.size() / 2); 40 | for (size_t i = 0; i < hex.size(); i += 2) { 41 | result[i / 2] = (get_hex_char_val(hex[i]) << 4) | get_hex_char_val(hex[i + 1]); 42 | } 43 | return result; 44 | } 45 | //--------------------------------------------------------------------------- 46 | } // namespace jutta_bt_proto 47 | //--------------------------------------------------------------------------- -------------------------------------------------------------------------------- /src/logger/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | add_library(logger SHARED Logger.cpp) 4 | target_link_libraries(logger PUBLIC spdlog::spdlog stdc++fs) 5 | 6 | set(LOG_LEVEL Default CACHE STRING "Sets the log level") 7 | set_property(CACHE LOG_LEVEL PROPERTY STRINGS Default Trace Debug Info Warn Error Critical Off) 8 | 9 | if(${LOG_LEVEL} STREQUAL Trace) 10 | set (LOG_LEVEL TRACE) 11 | message(STATUS "Using log level Trace") 12 | elseif(${LOG_LEVEL} STREQUAL Debug) 13 | set (LOG_LEVEL DEBUG) 14 | message(STATUS "Using log level Debug") 15 | elseif(${LOG_LEVEL} STREQUAL Info) 16 | set (LOG_LEVEL INFO) 17 | message(STATUS "Using log level Info") 18 | elseif(${LOG_LEVEL} STREQUAL Warn) 19 | set (LOG_LEVEL WARN) 20 | message(STATUS "Using log level Warn") 21 | elseif(${LOG_LEVEL} STREQUAL Error) 22 | set (LOG_LEVEL ERROR) 23 | message(STATUS "Using log level Error") 24 | elseif(${LOG_LEVEL} STREQUAL Critical) 25 | set (LOG_LEVEL CRITICAL) 26 | message(STATUS "Using log level Critical") 27 | elseif(${LOG_LEVEL} STREQUAL Off) 28 | set (LOG_LEVEL OFF) 29 | message(STATUS "Using log level Off") 30 | else() 31 | if((NOT ${LOG_LEVEL} STREQUAL Default) AND (NOT LOG_LEVEL STREQUAL "")) 32 | message(WARNING "Log level unknown, use -DLOG_LEVEL=[Trace, Debug, Info, Warn, Error, Critical, Off]") 33 | endif() 34 | set (LOG_LEVEL $,DEBUG,INFO>) 35 | message(STATUS "Setting LOG_LEVEL to according to build type") 36 | endif() 37 | 38 | target_compile_definitions(logger INTERFACE SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_${LOG_LEVEL}) 39 | install(TARGETS logger) 40 | -------------------------------------------------------------------------------- /src/logger/Logger.cpp: -------------------------------------------------------------------------------- 1 | #include "logger/Logger.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #ifdef _WIN32 12 | #include 13 | #endif // _WIN32 14 | 15 | namespace logger { 16 | constexpr int THREAD_QUEUE_LENGTH = 8192; 17 | constexpr int FILE_ROTATION_TIME = 1048576 * 5; 18 | 19 | void setup_logger(const spdlog::level::level_enum level) { 20 | if (not std::filesystem::exists(logger::log_folder)) { 21 | std::filesystem::create_directory(logger::log_folder); 22 | } 23 | spdlog::init_thread_pool(THREAD_QUEUE_LENGTH, 1); 24 | spdlog::sink_ptr console_sink = std::make_shared(); 25 | console_sink->set_pattern("[%^%=8l%$] [thread %t]\t%v"); 26 | #ifdef _WIN32 27 | std::string s = (logger::log_folder / "jutta.log").string(); 28 | spdlog::sink_ptr file_sink = std::make_shared(s, FILE_ROTATION_TIME, 3); 29 | #else // _WIN32 30 | spdlog::sink_ptr file_sink = std::make_shared(logger::log_folder / "jutta.log", FILE_ROTATION_TIME, 3); 31 | #endif 32 | file_sink->set_pattern("[%H:%M:%S %z] [%=8l] [thread %t] [%@]\t%v"); 33 | std::vector sinks{file_sink, console_sink}; 34 | std::shared_ptr logger = std::make_shared("", sinks.begin(), sinks.end(), spdlog::thread_pool(), spdlog::async_overflow_policy::block); 35 | logger->set_level(level); 36 | spdlog::set_default_logger(logger); 37 | } 38 | 39 | void set_log_level(const spdlog::level::level_enum level) { 40 | spdlog::default_logger()->set_level(level); 41 | } 42 | 43 | void deactivate_logger() { 44 | logger::set_log_level(spdlog::level::off); 45 | } 46 | } // namespace logger 47 | -------------------------------------------------------------------------------- /src/resources/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | # Ensure machine files exist: 4 | if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/machinefiles) 5 | message(FATAL_ERROR "Machine files not found. Please make sure you read the chapter about machine files in the README.md first.") 6 | endif() 7 | 8 | if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/machinefiles/JOE_MACHINES.TXT) 9 | message(FATAL_ERROR "'JOE_MACHINES.TXT' machine file not found. Please make sure you read the chapter about machine files in the README.md first.") 10 | endif() 11 | 12 | file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/machinefiles DESTINATION ${CMAKE_BINARY_DIR}) 13 | -------------------------------------------------------------------------------- /src/resources/extract_apk.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 1 ]; then 4 | echo "Invalid amount of arguments!" >&2 5 | echo "$0 JURA_JOE_APK_PATH.apk" >&2 6 | exit -1 7 | fi 8 | 9 | APK=$1 10 | 11 | echo "Validating APK..." 12 | 13 | unzip -l $APK | grep -q "assets/machinefiles/"; 14 | if [ "$?" != "0" ] 15 | then 16 | echo "Invalid APK. Does not contain machinefiles." >&2 17 | exit -1 18 | fi; 19 | 20 | unzip -l $APK | grep -q "assets/JOE_MACHINES.TXT"; 21 | if [ "$?" != "0" ] 22 | then 23 | echo "Invalid APK. Does not contain JOE_MACHINES.TXT." >&2 24 | exit -1 25 | fi; 26 | 27 | echo "APK valid." 28 | 29 | echo "Extracting '$APK'..." 30 | [ -e "machinefiles" ] && rm -rf "machinefiles" 31 | mkdir "machinefiles" 32 | unzip -q $APK "assets/machinefiles/*" -d "machinefiles/" 33 | mv machinefiles/assets/machinefiles/*.xml machinefiles/ 34 | unzip -q $APK assets/JOE_MACHINES.TXT -d machinefiles/ 35 | mv machinefiles/assets/JOE_MACHINES.TXT machinefiles/ 36 | rm -rf machinefiles/assets 37 | echo "done." -------------------------------------------------------------------------------- /src/test_exec/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | if(JUTTA_BT_PROTO_BUILD_TEST_EXEC) 4 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) 5 | 6 | # Default: 7 | set(EXECUTABLE_NAME "test_exec") 8 | set(EXECUTABLE_MAIN "test_exec.cpp") 9 | 10 | add_executable(${EXECUTABLE_NAME} ${EXECUTABLE_MAIN}) 11 | target_link_libraries(${EXECUTABLE_NAME} PRIVATE logger bt jutta_bt_proto) 12 | set_property(SOURCE ${EXECUTABLE_MAIN} PROPERTY COMPILE_DEFINITIONS) 13 | 14 | # Scanner 15 | set(EXECUTABLE_NAME "test_scanner") 16 | set(EXECUTABLE_MAIN "test_scanner.cpp") 17 | 18 | add_executable(${EXECUTABLE_NAME} ${EXECUTABLE_MAIN}) 19 | target_link_libraries(${EXECUTABLE_NAME} PRIVATE logger bt jutta_bt_proto) 20 | set_property(SOURCE ${EXECUTABLE_MAIN} PROPERTY COMPILE_DEFINITIONS) 21 | endif() 22 | -------------------------------------------------------------------------------- /src/test_exec/test_exec.cpp: -------------------------------------------------------------------------------- 1 | #include "bt/BLEHelper.hpp" 2 | #include "jutta_bt_proto/CoffeeMaker.hpp" 3 | #include "jutta_bt_proto/CoffeeMakerLoader.hpp" 4 | #include "logger/Logger.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | int main(int /*argc*/, char** /*argv*/) { 14 | logger::setup_logger(spdlog::level::debug); 15 | SPDLOG_INFO("Starting test exec..."); 16 | while (true) { 17 | SPDLOG_INFO("Scanning..."); 18 | bool canceled = false; 19 | std::shared_ptr result = bt::scan_for_device("TT214H BlueFrog", &canceled); 20 | if (!result) { 21 | SPDLOG_INFO("No coffee maker found. Sleeping..."); 22 | std::this_thread::sleep_for(std::chrono::seconds{2}); 23 | continue; 24 | } 25 | SPDLOG_INFO("Coffee maker found."); 26 | jutta_bt_proto::CoffeeMaker coffeeMaker(std::string{result->name}, std::string{result->addr}); 27 | coffeeMaker.joeChangedEventHandler.append([](const std::shared_ptr& joe) { 28 | joe->alertsChangedEventHandler.append([](const std::vector& alerts) { 29 | for (const jutta_bt_proto::Alert* alert : alerts) { 30 | // NOLINTNEXTLINE (bugprone-lambda-function-name) 31 | SPDLOG_INFO("New alert '{}' with type '{}'.", alert->name, alert->type); 32 | } 33 | }); 34 | }); 35 | if (coffeeMaker.connect()) { 36 | while (coffeeMaker.get_state() == jutta_bt_proto::CONNECTED) { 37 | coffeeMaker.request_statistics(jutta_bt_proto::StatParseMode::MAINTENANCE_COUNTER); 38 | coffeeMaker.request_statistics(jutta_bt_proto::StatParseMode::MAINTENANCE_PERCENT); 39 | coffeeMaker.request_statistics(jutta_bt_proto::StatParseMode::PRODUCT_COUNTERS); 40 | std::this_thread::sleep_for(std::chrono::seconds{5}); 41 | } 42 | } 43 | coffeeMaker.disconnect(); 44 | SPDLOG_INFO("Disconnected. Waiting 5 seconds before reconnecting."); 45 | std::this_thread::sleep_for(std::chrono::seconds{5}); 46 | } 47 | return 0; 48 | } 49 | -------------------------------------------------------------------------------- /src/test_exec/test_scanner.cpp: -------------------------------------------------------------------------------- 1 | #include "bt/BLEHelper.hpp" 2 | #include "logger/Logger.hpp" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | int main(int /*argc*/, char** /*argv*/) { 11 | logger::setup_logger(spdlog::level::info); 12 | SPDLOG_INFO("Starting scanner..."); 13 | std::unordered_set devices; 14 | while (true) { 15 | SPDLOG_DEBUG("Scanning..."); 16 | bool canceled = false; 17 | std::shared_ptr result = bt::scan_for_device("TT214H BlueFrog", &canceled); 18 | if (result && !devices.contains(result->addr)) { 19 | devices.insert(result->addr); 20 | SPDLOG_INFO("New device found: {} ({})", result->name, result->addr); 21 | SPDLOG_INFO("Total: {}", devices.size()); 22 | continue; 23 | } 24 | SPDLOG_INFO("No new deviced found."); 25 | } 26 | return 0; 27 | } 28 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | find_package(Catch2 REQUIRED) 4 | include(Catch) 5 | 6 | add_executable(proto_bt_tests Tests.cpp) 7 | 8 | set_target_properties(proto_bt_tests PROPERTIES UNITY_BUILD OFF) 9 | target_link_libraries(proto_bt_tests PRIVATE Catch2::Catch2 bt jutta_bt_proto) 10 | 11 | catch_discover_tests(proto_bt_tests) 12 | 13 | -------------------------------------------------------------------------------- /tests/Tests.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | 3 | #include "bt/ByteEncDecoder.hpp" 4 | #include "jutta_bt_proto/Utils.hpp" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | TEST_CASE("Empty", "[encDecBytes]") { 12 | const std::vector data; 13 | uint8_t key = 42; 14 | const std::vector result = bt::encDecBytes(data, key); 15 | REQUIRE(data.size() == result.size()); 16 | } 17 | 18 | TEST_CASE("OneElement", "[encDecBytes]") { 19 | const std::vector data{24}; 20 | uint8_t key = 42; 21 | const std::vector result = bt::encDecBytes(data, key); 22 | REQUIRE(data.size() == result.size()); 23 | REQUIRE(result[0] == 66); 24 | } 25 | 26 | TEST_CASE("OneElementTest", "[encDecBytes]") { 27 | std::string hex = "77e93dd55381d3dba32bfa98a4a3faf9"; 28 | std::vector input = jutta_bt_proto::from_hex_string(hex); 29 | uint8_t key = 42; 30 | std::vector result = bt::encDecBytes(input, key); 31 | std::string resultHex = jutta_bt_proto::to_hex_string(result); 32 | REQUIRE(input.size() == result.size()); 33 | } 34 | 35 | TEST_CASE("ManyElements", "[encDecBytes]") { 36 | std::random_device dev; 37 | std::mt19937 rng(dev()); 38 | std::uniform_int_distribution dist(10, 1024); 39 | 40 | std::vector data; 41 | for (size_t i = 0; i < static_cast(dist(rng)); i++) { 42 | data.push_back(static_cast(dist(rng))); 43 | } 44 | uint8_t key = static_cast(dist(rng)); 45 | const std::vector result = bt::encDecBytes(data, key); 46 | REQUIRE(data.size() == result.size()); 47 | } 48 | 49 | TEST_CASE("InverseOneElement", "[encDecBytes]") { 50 | const std::vector data{24}; 51 | uint8_t key = 42; 52 | const std::vector result = bt::encDecBytes(bt::encDecBytes(data, key), key); 53 | REQUIRE(data.size() == result.size()); 54 | REQUIRE(data[0] == result[0]); 55 | } 56 | 57 | TEST_CASE("InverseManyElements", "[encDecBytes]") { 58 | std::random_device dev; 59 | std::mt19937 rng(dev()); 60 | std::uniform_int_distribution dist(10, 1024); 61 | 62 | std::vector data; 63 | for (size_t i = 0; i < static_cast(dist(rng)); i++) { 64 | data.push_back(static_cast(dist(rng))); 65 | } 66 | uint8_t key = static_cast(dist(rng)); 67 | const std::vector result = bt::encDecBytes(bt::encDecBytes(data, key), key); 68 | REQUIRE(data.size() == result.size()); 69 | for (size_t i = 0; i < data.size(); i++) { 70 | REQUIRE(data[i] == result[i]); 71 | } 72 | } 73 | 74 | TEST_CASE("InverseManyElementsRepeated", "[encDecBytes]") { 75 | std::random_device dev; 76 | std::mt19937 rng(dev()); 77 | std::uniform_int_distribution dist(10, 1024); 78 | 79 | for (size_t e = 0; e < 42; e++) { 80 | std::vector data; 81 | for (size_t i = 0; i < static_cast(dist(rng)); i++) { 82 | data.push_back(static_cast(dist(rng))); 83 | } 84 | uint8_t key = static_cast(dist(rng)); 85 | const std::vector result = bt::encDecBytes(bt::encDecBytes(data, key), key); 86 | REQUIRE(data.size() == result.size()); 87 | for (size_t i = 0; i < data.size(); i++) { 88 | REQUIRE(data[i] == result[i]); 89 | } 90 | } 91 | } 92 | 93 | TEST_CASE("Uppercase", "[toFormHex]") { 94 | std::string s = "0123456789ABCDEF"; 95 | const std::vector tmp = jutta_bt_proto::from_hex_string(s); 96 | REQUIRE(tmp.size() == s.size() / 2); 97 | std::string result = jutta_bt_proto::to_hex_string(tmp); 98 | REQUIRE(result.size() == tmp.size() * 2); 99 | REQUIRE(result == s); 100 | } 101 | 102 | TEST_CASE("Lowercase", "[toFormHex]") { 103 | std::string s = "0123456789abcdef"; 104 | const std::vector tmp = jutta_bt_proto::from_hex_string(s); 105 | REQUIRE(tmp.size() == s.size() / 2); 106 | std::string result = jutta_bt_proto::to_hex_string(tmp); 107 | REQUIRE(result.size() == tmp.size() * 2); 108 | REQUIRE(result == "0123456789ABCDEF"); 109 | } 110 | --------------------------------------------------------------------------------