Procházet zdrojové kódy

Add seos from https://gitlab.com/bettse/flipper_seos

git-subtree-dir: seos
git-subtree-mainline: bcd127d7c4036e448a34f6041c2801a4ceca5984
git-subtree-split: 190ba1dc6e4854eb0f4d32ff5c39f64dec106f51
Willy-JL před 9 měsíci
rodič
revize
ecacc82a5a
100 změnil soubory, kde provedl 11692 přidání a 0 odebrání
  1. 11 0
      seos/.catalog/README.md
  2. binární
      seos/.catalog/screenshots/menu.png
  3. binární
      seos/.catalog/screenshots/read_success.png
  4. 246 0
      seos/.clang-format
  5. 2 0
      seos/.gitattributes
  6. 41 0
      seos/.github/workflows/build.yml
  7. 4 0
      seos/.gitignore
  8. 1 0
      seos/.gitsubtree
  9. 5 0
      seos/CHANGELOG.md
  10. 661 0
      seos/LICENSE
  11. 60 0
      seos/README.md
  12. 110 0
      seos/aes_cmac.c
  13. 11 0
      seos/aes_cmac.h
  14. 19 0
      seos/application.fam
  15. 17 0
      seos/boards/nrf52840dongle_nrf52840.overlay
  16. 3 0
      seos/demo.mkv
  17. 3 0
      seos/demo.mp4
  18. 108 0
      seos/des_cmac.c
  19. 11 0
      seos/des_cmac.h
  20. 10 0
      seos/example.seos
  21. 14 0
      seos/files/seos.nfc
  22. 13 0
      seos/headers/app_common.h
  23. 200 0
      seos/headers/app_conf.h
  24. 1307 0
      seos/headers/ble_gatt_aci.h
  25. 195 0
      seos/headers/ble_vs_codes.h
  26. 185 0
      seos/headers/stm32_wpan_common.h
  27. 360 0
      seos/headers/tl.h
  28. 0 0
      seos/images/.gitkeep
  29. binární
      seos/images/DolphinMafia_115x62.png
  30. binární
      seos/images/DolphinNice_96x59.png
  31. binární
      seos/images/Nfc_10px.png
  32. binární
      seos/images/RFIDDolphinReceive_97x61.png
  33. binární
      seos/images/RFIDDolphinSend_97x61.png
  34. 7 0
      seos/keys-example.txt
  35. 28 0
      seos/keys.c
  36. 7 0
      seos/keys.h
  37. 60 0
      seos/memmem.c
  38. 30 0
      seos/scenes/seos_scene.c
  39. 29 0
      seos/scenes/seos_scene.h
  40. 45 0
      seos/scenes/seos_scene_about.c
  41. 85 0
      seos/scenes/seos_scene_ble_central.c
  42. 92 0
      seos/scenes/seos_scene_ble_peripheral.c
  43. 17 0
      seos/scenes/seos_scene_config.h
  44. 52 0
      seos/scenes/seos_scene_delete.c
  45. 40 0
      seos/scenes/seos_scene_delete_success.c
  46. 65 0
      seos/scenes/seos_scene_emulate.c
  47. 26 0
      seos/scenes/seos_scene_file_select.c
  48. 109 0
      seos/scenes/seos_scene_info.c
  49. 120 0
      seos/scenes/seos_scene_main_menu.c
  50. 56 0
      seos/scenes/seos_scene_read.c
  51. 64 0
      seos/scenes/seos_scene_read_error.c
  52. 109 0
      seos/scenes/seos_scene_read_success.c
  53. 79 0
      seos/scenes/seos_scene_save_name.c
  54. 42 0
      seos/scenes/seos_scene_save_success.c
  55. 98 0
      seos/scenes/seos_scene_saved_menu.c
  56. 61 0
      seos/scenes/seos_scene_scanner_menu.c
  57. 46 0
      seos/scenes/seos_scene_zero_keys.c
  58. 349 0
      seos/secure_messaging.c
  59. 42 0
      seos/secure_messaging.h
  60. 305 0
      seos/seos.c
  61. 5 0
      seos/seos.h
  62. binární
      seos/seos.png
  63. 492 0
      seos/seos_att.c
  64. 100 0
      seos/seos_att.h
  65. 4 0
      seos/seos_att_i.h
  66. 176 0
      seos/seos_central.c
  67. 33 0
      seos/seos_central.h
  68. 4 0
      seos/seos_central_i.h
  69. 346 0
      seos/seos_characteristic.c
  70. 36 0
      seos/seos_characteristic.h
  71. 4 0
      seos/seos_characteristic_i.h
  72. 145 0
      seos/seos_common.c
  73. 112 0
      seos/seos_common.h
  74. 734 0
      seos/seos_emulator.c
  75. 61 0
      seos/seos_emulator.h
  76. 5 0
      seos/seos_emulator_i.h
  77. 877 0
      seos/seos_hci.c
  78. 78 0
      seos/seos_hci.h
  79. 484 0
      seos/seos_hci_h5.c
  80. 82 0
      seos/seos_hci_h5.h
  81. 4 0
      seos/seos_hci_h5_i.h
  82. 4 0
      seos/seos_hci_i.h
  83. 129 0
      seos/seos_i.h
  84. 235 0
      seos/seos_l2cap.c
  85. 44 0
      seos/seos_l2cap.h
  86. 4 0
      seos/seos_l2cap_i.h
  87. 475 0
      seos/seos_native_peripheral.c
  88. 31 0
      seos/seos_native_peripheral.h
  89. 4 0
      seos/seos_native_peripheral_i.h
  90. 136 0
      seos/seos_profile.c
  91. 49 0
      seos/seos_profile.h
  92. 475 0
      seos/seos_reader.c
  93. 45 0
      seos/seos_reader.h
  94. 5 0
      seos/seos_reader_i.h
  95. 228 0
      seos/seos_service.c
  96. 56 0
      seos/seos_service.h
  97. 17 0
      seos/seos_service_uuid.inc
  98. 234 0
      seos/uart.c
  99. 24 0
      seos/uart.h
  100. 55 0
      seos/uart_i.h

+ 11 - 0
seos/.catalog/README.md

@@ -0,0 +1,11 @@
+## Keys
+
+**The app uses all zero keys by default**. If you'd like to use your own keys, use the format of the 'keys-example.txt' to specify them, and place into 'SD Card/apps_data/seos/keys.txt'
+
+## Note
+
+This software incorporates a third-party implementation of Seos™ technology. It is not developed, authorized, licensed, or endorsed by HID Global, ASSA ABLOY, or any of their affiliates. References to Seos™ are solely for descriptive and compatibility purposes.
+
+No guarantee of compatibility or functionality is made. This implementation may not work with all Seos™-enabled systems, and its performance, security, and reliability are not assured. Users assume all risks associated with its use.
+
+Seos™, HID Global, and ASSA ABLOY are trademarks or registered trademarks of their respective owners. This software is not associated with or sponsored by them in any way.

binární
seos/.catalog/screenshots/menu.png


binární
seos/.catalog/screenshots/read_success.png


+ 246 - 0
seos/.clang-format

@@ -0,0 +1,246 @@
+---
+Language:        Cpp
+AccessModifierOffset: -4
+AlignAfterOpenBracket: AlwaysBreak
+AlignArrayOfStructures: None
+AlignConsecutiveAssignments:
+  Enabled:         false
+  AcrossEmptyLines: false
+  AcrossComments:  false
+  AlignCompound:   false
+  AlignFunctionPointers: false
+  PadOperators:    true
+AlignConsecutiveBitFields:
+  Enabled:         true
+  AcrossEmptyLines: true
+  AcrossComments:  true
+  AlignCompound:   false
+  AlignFunctionPointers: false
+  PadOperators:    true
+AlignConsecutiveDeclarations:
+  Enabled:         false
+  AcrossEmptyLines: false
+  AcrossComments:  false
+  AlignCompound:   false
+  AlignFunctionPointers: false
+  PadOperators:    true
+AlignConsecutiveMacros:
+  Enabled:         true
+  AcrossEmptyLines: false
+  AcrossComments:  true
+  AlignCompound:   true
+  AlignFunctionPointers: false
+  PadOperators:    true
+AlignConsecutiveShortCaseStatements:
+  Enabled:         false
+  AcrossEmptyLines: false
+  AcrossComments:  false
+  AlignCaseColons: false
+AlignEscapedNewlines: Left
+AlignOperands:   Align
+AlignTrailingComments:
+  Kind:            Never
+  OverEmptyLines:  0
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowBreakBeforeNoexceptSpecifier: Never
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortCompoundRequirementOnASingleLine: true
+AllowShortEnumsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortLambdasOnASingleLine: All
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: Yes
+AttributeMacros:
+  - __capability
+BinPackArguments: false
+BinPackParameters: false
+BitFieldColonSpacing: Both
+BraceWrapping:
+  AfterCaseLabel:  false
+  AfterClass:      false
+  AfterControlStatement: Never
+  AfterEnum:       false
+  AfterExternBlock: false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  BeforeCatch:     false
+  BeforeElse:      false
+  BeforeLambdaBody: false
+  BeforeWhile:     false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakAdjacentStringLiterals: true
+BreakAfterAttributes: Leave
+BreakAfterJavaFieldAnnotations: false
+BreakArrays:     true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: Always
+BreakBeforeBraces: Attach
+BreakBeforeInlineASMColon: OnlyMultiline
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializers: BeforeComma
+BreakInheritanceList: BeforeColon
+BreakStringLiterals: false
+ColumnLimit:     99
+CommentPragmas:  '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: false
+DisableFormat:   false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: false
+ForEachMacros:
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+  - M_EACH
+IfMacros:
+  - KJ_IF_MAYBE
+IncludeBlocks:   Preserve
+IncludeCategories:
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '^(<|"(gtest|gmock|isl|json)/)'
+    Priority:        3
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseBlocks: false
+IndentCaseLabels: false
+IndentExternBlock: AfterExternBlock
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentRequiresClause: false
+IndentWidth:     4
+IndentWrappedFunctionNames: true
+InsertBraces:    false
+InsertNewlineAtEOF: true
+InsertTrailingCommas: None
+IntegerLiteralSeparator:
+  Binary:          0
+  BinaryMinDigits: 0
+  Decimal:         0
+  DecimalMinDigits: 0
+  Hex:             0
+  HexMinDigits:    0
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+KeepEmptyLinesAtEOF: false
+LambdaBodyIndentation: Signature
+LineEnding:      DeriveLF
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 4
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: true
+ObjCSpaceBeforeProtocolList: true
+PackConstructorInitializers: BinPack
+PenaltyBreakAssignment: 10
+PenaltyBreakBeforeFirstCallParameter: 30
+PenaltyBreakComment: 10
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakScopeResolution: 500
+PenaltyBreakString: 10
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 100
+PenaltyIndentedWhitespace: 0
+PenaltyReturnTypeOnItsOwnLine: 60
+PointerAlignment: Left
+PPIndentWidth:   -1
+QualifierAlignment: Leave
+ReferenceAlignment: Pointer
+ReflowComments:  false
+RemoveBracesLLVM: false
+RemoveParentheses: Leave
+RemoveSemicolon: true
+RequiresClausePosition: OwnLine
+RequiresExpressionIndentation: OuterScope
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SkipMacroDefinitionBody: false
+SortIncludes:    Never
+SortJavaStaticImport: Before
+SortUsingDeclarations: Never
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeJsonColon: false
+SpaceBeforeParens: Never
+SpaceBeforeParensOptions:
+  AfterControlStatements: false
+  AfterForeachMacros: false
+  AfterFunctionDefinitionName: false
+  AfterFunctionDeclarationName: false
+  AfterIfMacros:   false
+  AfterOverloadedOperator: false
+  AfterPlacementOperator: true
+  AfterRequiresInClause: false
+  AfterRequiresInExpression: false
+  BeforeNonEmptyParentheses: false
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceBeforeSquareBrackets: false
+SpaceInEmptyBlock: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles:  Never
+SpacesInContainerLiterals: false
+SpacesInLineCommentPrefix:
+  Minimum:         1
+  Maximum:         -1
+SpacesInParens:  Never
+SpacesInParensOptions:
+  InCStyleCasts:   false
+  InConditionalStatements: false
+  InEmptyParentheses: false
+  Other:           false
+SpacesInSquareBrackets: false
+Standard:        c++20
+StatementAttributeLikeMacros:
+  - Q_EMIT
+StatementMacros:
+  - Q_UNUSED
+  - QT_REQUIRE_VERSION
+TabWidth:        4
+UseTab:          Never
+VerilogBreakBetweenInstancePorts: true
+WhitespaceSensitiveMacros:
+  - STRINGIZE
+  - PP_STRINGIZE
+  - BOOST_PP_STRINGIZE
+  - NS_SWIFT_NAME
+  - CF_SWIFT_NAME
+...
+

+ 2 - 0
seos/.gitattributes

@@ -0,0 +1,2 @@
+*.mkv filter=lfs diff=lfs merge=lfs -text
+*.mp4 filter=lfs diff=lfs merge=lfs -text

+ 41 - 0
seos/.github/workflows/build.yml

@@ -0,0 +1,41 @@
+name: "FAP: Build for multiple SDK sources"
+# This will build your app for dev and release channels on GitHub. 
+# It will also build your app every day to make sure it's up to date with the latest SDK changes.
+# See https://github.com/marketplace/actions/build-flipper-application-package-fap for more information
+
+on:
+  push:
+    ## put your main branch name under "branches"
+    #branches: 
+    #  - master 
+  pull_request:
+  schedule: 
+    # do a build every day
+    - cron: "1 1 * * *"
+
+jobs:
+  ufbt-build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          - name: dev channel
+            sdk-channel: dev
+          - name: release channel
+            sdk-channel: release
+          # You can add unofficial channels here. See ufbt action docs for more info.
+    name: 'ufbt: Build for ${{ matrix.name }}'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Build with ufbt
+        uses: flipperdevices/flipperzero-ufbt-action@v0.1
+        id: build-app
+        with:
+          sdk-channel: ${{ matrix.sdk-channel }}
+      - name: Upload app artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          # See ufbt action docs for other output variables
+          name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }}
+          path: ${{ steps.build-app.outputs.fap-artifacts }}

+ 4 - 0
seos/.gitignore

@@ -0,0 +1,4 @@
+dist
+.vscode
+keys.txt
+*_keys.txt

+ 1 - 0
seos/.gitsubtree

@@ -0,0 +1 @@
+https://gitlab.com/bettse/flipper_seos main /

+ 5 - 0
seos/CHANGELOG.md

@@ -0,0 +1,5 @@
+## 1.1
+ - Add native BLE
+
+## 1.0
+ - Public release

+ 661 - 0
seos/LICENSE

@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 2025  Eric Betts
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<https://www.gnu.org/licenses/>.

+ 60 - 0
seos/README.md

@@ -0,0 +1,60 @@
+# Flipper Seos
+
+Flipper app for reading and emulating Seos-compatible cards/fobs/mobile credentials.
+
+![Demo Video](demo.mp4)
+
+## Note
+
+This software incorporates a third-party implementation of Seos™ technology. It is not developed, authorized, licensed, or endorsed by HID Global, ASSA ABLOY, or any of their affiliates. References to Seos™ are solely for descriptive and compatibility purposes.
+
+No guarantee of compatibility or functionality is made. This implementation may not work with all Seos™-enabled systems, and its performance, security, and reliability are not assured. Users assume all risks associated with its use.
+
+Seos™, HID Global, and ASSA ABLOY are trademarks or registered trademarks of their respective owners. This software is not associated with or sponsored by them in any way.
+
+## To do:
+
+- Fix iso14443a-4 framing
+- ASN.1 for serializing/deserializing
+- Support for larger message wrapping/unwrapping
+- When parsing incoming data, use buffer + len instead of BitBuffer so I can increment buffer pointer as I parse header(s)
+- CMAC checking where I missed it
+
+## Keys
+
+The app uses all 00 keys by default. If you'd like to use your own keys, use the format of the `keys-example.txt` to specify them, and place into `SD Card/apps_data/seos/keys.txt`
+
+## Hardware for BLE support (experimental)
+
+1. Install/setup nordic SDK
+1. Install Toolchain manager
+1. Launch Toolchain manager
+1. Next to SDK version click down arroy and "open terminal"
+1. navigate to `samples/bluetooth/hci_uart_3wire`
+
+### nRF52840
+
+1. Edit `boards/nrf52840dk_nrf52840.overlay` and change current-speed to 460800 to match Flipper app.
+
+1. `west build -b nrf52840dk_nrf52840 -p auto`
+1. `west flash`
+
+### nRF52840 dongle
+
+1. Copy `boards/nrf52840dongle_nrf52840.overlay` to `hci_uart_3wire`
+1. Might need to: `nrfutil install nrf5sdk-tools`
+1. `west build -b nrf52840dongle_nrf52840 -p auto`
+1. `nrfutil nrf5sdk-tools pkg generate --hw-version 52 --sd-req=0x00  --application ./build/hci_uart_3wire/zephyr/zephyr.hex --application-version 1 app.zip`
+1. Put dongle into DFU by pressing 'reset' button
+1. `nrfutil nrf5sdk-tools dfu usb-serial -pkg app.zip -p /dev/cu.usbmodemD39BF26162261`
+
+### Connection
+
+| flipper purpose | pin | color  | nRF52840 dk pin | nRF52840 dongle pin |
+| --------------- | --- | ------ | --------------- | ------------------- |
+| rx              | 16  | yellow | P0.06           | P0.20               |
+| tx              | 15  | orange | P0.08           | P0.24               |
+| gnd             | 11  | black  | any ground      | GND                 |
+| power           | 5v  | red    | VIN 3-5v        | VBUS                |
+
+

+ 110 - 0
seos/aes_cmac.c

@@ -0,0 +1,110 @@
+#include "aes_cmac.h"
+
+#define BLOCK_SIZE 16
+
+#define TAG "AESCMAC"
+
+static uint8_t zeroes[] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+static uint8_t Rb[] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87};
+
+void aes_cmac_padBlock(uint8_t* block, size_t len) {
+    block[len] = 0x80;
+}
+
+bool aes_cmac_aes(uint8_t* key, uint8_t* plain, size_t plain_len, uint8_t* enc) {
+    uint8_t iv[BLOCK_SIZE];
+    memset(iv, 0, BLOCK_SIZE);
+    mbedtls_aes_context ctx;
+    mbedtls_aes_init(&ctx);
+    mbedtls_aes_setkey_enc(&ctx, key, BLOCK_SIZE * 8);
+    int rtn = mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_ENCRYPT, plain_len, iv, plain, enc);
+    mbedtls_aes_free(&ctx);
+
+    return rtn == 0;
+}
+
+void aes_cmac_bitShiftLeft(uint8_t* input, uint8_t* output, size_t len) {
+    size_t last = len - 1;
+    for(size_t i = 0; i < last; i++) {
+        output[i] = input[i] << 1;
+        if(input[i + 1] & 0x80) {
+            output[i] += 0x01;
+        }
+    }
+    output[last] = input[last] << 1;
+}
+
+// x = a ^ b
+void aes_cmac_xor(uint8_t* a, uint8_t* b, uint8_t* x, size_t len) {
+    for(size_t i = 0; i < len; i++) {
+        x[i] = a[i] ^ b[i];
+    }
+}
+
+bool aes_cmac_generateSubkeys(uint8_t* key, uint8_t* subkey1, uint8_t* subkey2) {
+    uint8_t l[BLOCK_SIZE] = {0};
+    aes_cmac_aes(key, zeroes, BLOCK_SIZE, l);
+
+    aes_cmac_bitShiftLeft(l, subkey1, BLOCK_SIZE);
+    if(l[0] & 0x80) {
+        aes_cmac_xor(subkey1, Rb, subkey1, BLOCK_SIZE);
+    }
+
+    aes_cmac_bitShiftLeft(subkey1, subkey2, BLOCK_SIZE);
+    if(subkey1[0] & 0x80) {
+        aes_cmac_xor(subkey2, Rb, subkey2, BLOCK_SIZE);
+    }
+
+    return true;
+}
+
+bool aes_cmac(uint8_t* key, size_t key_len, uint8_t* message, size_t message_len, uint8_t* cmac) {
+    uint8_t subkey1[BLOCK_SIZE] = {0};
+    uint8_t subkey2[BLOCK_SIZE] = {0};
+    uint8_t blockCount = (message_len + BLOCK_SIZE - 1) / BLOCK_SIZE;
+    bool lastBlockCompleteFlag;
+    uint8_t lastBlockIndex;
+    uint8_t lastBlock[BLOCK_SIZE] = {0};
+
+    // Only support key length of 16 bytes
+    if(key_len != BLOCK_SIZE) {
+        return false;
+    }
+
+    aes_cmac_generateSubkeys(key, subkey1, subkey2);
+
+    if(blockCount == 0) {
+        blockCount = 1;
+        lastBlockCompleteFlag = false;
+    } else {
+        lastBlockCompleteFlag = (message_len % BLOCK_SIZE == 0);
+    }
+    lastBlockIndex = blockCount - 1;
+
+    if(lastBlockCompleteFlag) {
+        memcpy(lastBlock, message + (lastBlockIndex * BLOCK_SIZE), BLOCK_SIZE);
+        aes_cmac_xor(lastBlock, subkey1, lastBlock, BLOCK_SIZE);
+    } else {
+        memcpy(lastBlock, message + (lastBlockIndex * BLOCK_SIZE), message_len % BLOCK_SIZE);
+        aes_cmac_padBlock(lastBlock, message_len % BLOCK_SIZE);
+        aes_cmac_xor(lastBlock, subkey2, lastBlock, BLOCK_SIZE);
+    }
+
+    uint8_t x[BLOCK_SIZE];
+    uint8_t y[BLOCK_SIZE];
+    memset(x, 0, sizeof(x));
+    memset(y, 0, sizeof(y));
+
+    for(size_t i = 0; i < lastBlockIndex; i++) {
+        aes_cmac_xor(x, message + (i * BLOCK_SIZE), y, BLOCK_SIZE);
+        aes_cmac_aes(key, y, BLOCK_SIZE, x);
+    }
+
+    aes_cmac_xor(x, lastBlock, y, BLOCK_SIZE);
+
+    bool success = aes_cmac_aes(key, y, BLOCK_SIZE, cmac);
+
+    return success;
+}

+ 11 - 0
seos/aes_cmac.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <mbedtls/aes.h>
+
+#include <furi.h>
+
+bool aes_cmac(uint8_t* key, size_t key_len, uint8_t* message, size_t message_len, uint8_t* cmac);

+ 19 - 0
seos/application.fam

@@ -0,0 +1,19 @@
+# For details & more options, see documentation/AppManifests.md in firmware repo
+
+App(
+    appid="seos",  # Must be unique
+    name="Seos compatible",  # Displayed in menus
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="seos_app",
+    stack_size=10 * 1024,
+    fap_category="NFC",
+    # Optional values
+    fap_version="1.0",
+    fap_icon="seos.png",  # 10x10 1-bit PNG
+    fap_description="Seos compatible reader/emulator",
+    fap_author="bettse",
+    fap_weburl="https://gitlab.com/bettse/flipper_seos",
+    fap_icon_assets="images",  # Image assets to compile for this application
+    fap_file_assets="files",
+    fap_libs=["mbedtls"],
+)

+ 17 - 0
seos/boards/nrf52840dongle_nrf52840.overlay

@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: Apache-2.0 */
+
+&uart0 {
+	compatible = "nordic,nrf-uart";
+	current-speed = <460800>;
+	status = "okay";
+};
+
+/ {
+	chosen {
+                zephyr,console = &uart0;
+                zephyr,shell-uart = &uart0;
+                zephyr,uart-mcumgr = &uart0;
+                zephyr,bt-mon-uart = &uart0;
+                zephyr,bt-c2h-uart = &uart0;
+	};
+};

+ 3 - 0
seos/demo.mkv

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cfad22babd8f4b7279c28700ed2cc22a210b7e5c05fddc12bc04fa950c250af4
+size 9505830

+ 3 - 0
seos/demo.mp4

@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6815159f47ad83bacf397fb2cd5c54dd98d736bde538cda87e6645ea4f54a1fa
+size 9526855

+ 108 - 0
seos/des_cmac.c

@@ -0,0 +1,108 @@
+#include "des_cmac.h"
+
+#define BLOCK_SIZE 8
+
+#define TAG "DESCMAC"
+
+static uint8_t zeroes[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+static uint8_t Rb[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b};
+
+void des_cmac_padBlock(uint8_t* block, size_t len) {
+    block[len] = 0x80;
+}
+
+bool des_cmac_des3(uint8_t* key, uint8_t* plain, size_t plain_len, uint8_t* enc) {
+    uint8_t iv[BLOCK_SIZE];
+    memset(iv, 0, BLOCK_SIZE);
+    mbedtls_des3_context ctx;
+    mbedtls_des3_init(&ctx);
+    mbedtls_des3_set2key_enc(&ctx, key);
+    int rtn = mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_ENCRYPT, plain_len, iv, plain, enc);
+    mbedtls_des3_free(&ctx);
+
+    return rtn == 0;
+}
+
+void des_cmac_bitShiftLeft(uint8_t* input, uint8_t* output, size_t len) {
+    size_t last = len - 1;
+    for(size_t i = 0; i < last; i++) {
+        output[i] = input[i] << 1;
+        if(input[i + 1] & 0x80) {
+            output[i] += 0x01;
+        }
+    }
+    output[last] = input[last] << 1;
+}
+
+// x = a ^ b
+void des_cmac_xor(uint8_t* a, uint8_t* b, uint8_t* x, size_t len) {
+    for(size_t i = 0; i < len; i++) {
+        x[i] = a[i] ^ b[i];
+    }
+}
+
+bool des_cmac_generateSubkeys(uint8_t* key, uint8_t* subkey1, uint8_t* subkey2) {
+    uint8_t l[BLOCK_SIZE] = {0};
+    des_cmac_des3(key, zeroes, BLOCK_SIZE, l);
+
+    des_cmac_bitShiftLeft(l, subkey1, BLOCK_SIZE);
+    if(l[0] & 0x80) {
+        des_cmac_xor(subkey1, Rb, subkey1, BLOCK_SIZE);
+    }
+
+    des_cmac_bitShiftLeft(subkey1, subkey2, BLOCK_SIZE);
+    if(subkey1[0] & 0x80) {
+        des_cmac_xor(subkey2, Rb, subkey2, BLOCK_SIZE);
+    }
+
+    return true;
+}
+
+bool des_cmac(uint8_t* key, size_t key_len, uint8_t* message, size_t message_len, uint8_t* cmac) {
+    uint8_t subkey1[BLOCK_SIZE] = {0};
+    uint8_t subkey2[BLOCK_SIZE] = {0};
+    uint8_t blockCount = (message_len + BLOCK_SIZE - 1) / BLOCK_SIZE;
+    bool lastBlockCompleteFlag;
+    uint8_t lastBlockIndex;
+    uint8_t lastBlock[BLOCK_SIZE] = {0};
+
+    // Only support key length of 16 bytes
+    if(key_len != 16) {
+        return false;
+    }
+
+    des_cmac_generateSubkeys(key, subkey1, subkey2);
+
+    if(blockCount == 0) {
+        blockCount = 1;
+        lastBlockCompleteFlag = false;
+    } else {
+        lastBlockCompleteFlag = (message_len % BLOCK_SIZE == 0);
+    }
+    lastBlockIndex = blockCount - 1;
+
+    if(lastBlockCompleteFlag) {
+        memcpy(lastBlock, message + (lastBlockIndex * BLOCK_SIZE), BLOCK_SIZE);
+        des_cmac_xor(lastBlock, subkey1, lastBlock, BLOCK_SIZE);
+    } else {
+        memcpy(lastBlock, message + (lastBlockIndex * BLOCK_SIZE), message_len % BLOCK_SIZE);
+        des_cmac_padBlock(lastBlock, message_len % BLOCK_SIZE);
+        des_cmac_xor(lastBlock, subkey2, lastBlock, BLOCK_SIZE);
+    }
+
+    uint8_t x[BLOCK_SIZE];
+    uint8_t y[BLOCK_SIZE];
+    memset(x, 0, sizeof(x));
+    memset(y, 0, sizeof(y));
+
+    for(size_t i = 0; i < lastBlockIndex; i++) {
+        des_cmac_xor(x, message + (i * BLOCK_SIZE), y, BLOCK_SIZE);
+        des_cmac_des3(key, y, BLOCK_SIZE, x);
+    }
+
+    des_cmac_xor(x, lastBlock, y, BLOCK_SIZE);
+
+    bool success = des_cmac_des3(key, y, BLOCK_SIZE, cmac);
+
+    return success;
+}

+ 11 - 0
seos/des_cmac.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <mbedtls/des.h>
+
+#include <furi.h>
+
+bool des_cmac(uint8_t* key, size_t key_len, uint8_t* message, size_t message_len, uint8_t* cmac);

+ 10 - 0
seos/example.seos

@@ -0,0 +1,10 @@
+Filetype: Flipper Seos Credential
+Version: 1
+Diversifier Length: 8
+Diversifier: 00 00 00 00 00 00 00 00
+SIO Length: 54
+SIO: 30 34 ...
+# Optional
+Priv Key: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+Auth Key: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+ADF Response: cd 02 ....

+ 14 - 0
seos/files/seos.nfc

@@ -0,0 +1,14 @@
+Filetype: Flipper NFC device
+Version: 4
+# Device type can be ISO14443-3A, ISO14443-3B, ISO14443-4A, ISO14443-4B, ISO15693-3, FeliCa, NTAG/Ultralight, Mifare Classic, Mifare DESFire, SLIX, ST25TB
+Device type: ISO14443-4A
+# UID is common for all formats
+UID: 08 22 F3 5A
+# ISO14443-3A specific data
+ATQA: 00 04
+SAK: 20
+# ISO14443-4A specific data
+T0: 78
+TA(1): 77
+TB(1): 94
+TC(1): 02

+ 13 - 0
seos/headers/app_common.h

@@ -0,0 +1,13 @@
+#pragma once
+
+#include <stdint.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+
+#include <core/common_defines.h>
+//#include <interface/patterns/ble_thread/tl/tl.h>
+#include "tl.h"
+
+#include "app_conf.h"

+ 200 - 0
seos/headers/app_conf.h

@@ -0,0 +1,200 @@
+#pragma once
+
+#include <ble/core/ble_defs.h>
+
+#define CFG_TX_POWER (0x19) /* +0dBm */
+
+#define CFG_IDENTITY_ADDRESS GAP_PUBLIC_ADDR
+
+/**
+ * Define IO Authentication
+ */
+#define CFG_ENCRYPTION_KEY_SIZE_MAX (16)
+#define CFG_ENCRYPTION_KEY_SIZE_MIN (8)
+
+/**
+ * Define IO capabilities
+ */
+#define CFG_IO_CAPABILITY IO_CAP_DISPLAY_YES_NO
+
+/**
+ * Define Secure Connections Support
+ */
+#define CFG_SC_SUPPORT SC_PAIRING_OPTIONAL
+
+/**
+ * Define PHY
+ */
+#define ALL_PHYS_PREFERENCE 0x00
+#define RX_2M_PREFERRED     0x02
+#define TX_2M_PREFERRED     0x02
+#define TX_1M               0x01
+#define TX_2M               0x02
+#define RX_1M               0x01
+#define RX_2M               0x02
+
+/******************************************************************************
+ * BLE Stack
+ ******************************************************************************/
+/**
+ * Maximum number of simultaneous connections that the device will support.
+ * Valid values are from 1 to 8
+ */
+#define CFG_BLE_NUM_LINK 2
+
+/**
+ * Maximum number of Services that can be stored in the GATT database.
+ * Note that the GAP and GATT services are automatically added so this parameter should be 2 plus the number of user services
+ */
+#define CFG_BLE_NUM_GATT_SERVICES 8
+
+/**
+ * Maximum number of Attributes
+ * (i.e. the number of characteristic + the number of characteristic values + the number of descriptors, excluding the services)
+ * that can be stored in the GATT database.
+ * Note that certain characteristics and relative descriptors are added automatically during device initialization
+ * so this parameters should be 9 plus the number of user Attributes
+ */
+#define CFG_BLE_NUM_GATT_ATTRIBUTES 68
+
+/**
+ * Maximum supported ATT_MTU size
+ */
+#define CFG_BLE_MAX_ATT_MTU (256 + 128 + 16 + 8 + 4 + 2)
+
+/**
+ * Size of the storage area for Attribute values
+ *  This value depends on the number of attributes used by application. In particular the sum of the following quantities (in octets) should be made for each attribute:
+ *  - attribute value length
+ *  - 5, if UUID is 16 bit; 19, if UUID is 128 bit
+ *  - 2, if server configuration descriptor is used
+ *  - 2*DTM_NUM_LINK, if client configuration descriptor is used
+ *  - 2, if extended properties is used
+ *  The total amount of memory needed is the sum of the above quantities for each attribute.
+ */
+#define CFG_BLE_ATT_VALUE_ARRAY_SIZE (1344)
+
+/**
+ * Prepare Write List size in terms of number of packet
+ */
+#define CFG_BLE_PREPARE_WRITE_LIST_SIZE BLE_PREP_WRITE_X_ATT(CFG_BLE_MAX_ATT_MTU)
+
+/**
+ * Number of allocated memory blocks
+ */
+#define CFG_BLE_MBLOCK_COUNT \
+    (BLE_MBLOCKS_CALC(CFG_BLE_PREPARE_WRITE_LIST_SIZE, CFG_BLE_MAX_ATT_MTU, CFG_BLE_NUM_LINK))
+
+/**
+ * Enable or disable the Extended Packet length feature. Valid values are 0 or 1.
+ */
+#define CFG_BLE_DATA_LENGTH_EXTENSION 1
+
+/**
+ * Sleep clock accuracy in Slave mode (ppm value)
+ */
+#define CFG_BLE_SLAVE_SCA 500
+
+/**
+ * Sleep clock accuracy in Master mode
+ * 0 : 251 ppm to 500 ppm
+ * 1 : 151 ppm to 250 ppm
+ * 2 : 101 ppm to 150 ppm
+ * 3 : 76 ppm to 100 ppm
+ * 4 : 51 ppm to 75 ppm
+ * 5 : 31 ppm to 50 ppm
+ * 6 : 21 ppm to 30 ppm
+ * 7 : 0 ppm to 20 ppm
+ */
+#define CFG_BLE_MASTER_SCA 0
+
+/**
+ *  Source for the low speed clock for RF wake-up
+ *  1 : external high speed crystal HSE/32/32
+ *  0 : external low speed crystal ( no calibration )
+ */
+#define CFG_BLE_LSE_SOURCE                                                        \
+    SHCI_C2_BLE_INIT_CFG_BLE_LS_CLK_LSE | SHCI_C2_BLE_INIT_CFG_BLE_LS_OTHER_DEV | \
+        SHCI_C2_BLE_INIT_CFG_BLE_LS_CALIB
+
+/**
+ * Start up time of the high speed (16 or 32 MHz) crystal oscillator in units of 625/256 us (~2.44 us)
+ */
+#define CFG_BLE_HSE_STARTUP_TIME 0x148
+
+/**
+ * Maximum duration of the connection event when the device is in Slave mode in units of 625/256 us (~2.44 us)
+ */
+#define CFG_BLE_MAX_CONN_EVENT_LENGTH (0xFFFFFFFF)
+
+/**
+ * Viterbi Mode
+ * 1 : enabled
+ * 0 : disabled
+ */
+#define CFG_BLE_VITERBI_MODE 1
+
+/**
+ * BLE stack Options flags to be configured with:
+ * - SHCI_C2_BLE_INIT_OPTIONS_LL_ONLY
+ * - SHCI_C2_BLE_INIT_OPTIONS_LL_HOST
+ * - SHCI_C2_BLE_INIT_OPTIONS_NO_SVC_CHANGE_DESC
+ * - SHCI_C2_BLE_INIT_OPTIONS_WITH_SVC_CHANGE_DESC
+ * - SHCI_C2_BLE_INIT_OPTIONS_DEVICE_NAME_RO
+ * - SHCI_C2_BLE_INIT_OPTIONS_DEVICE_NAME_RW
+ * - SHCI_C2_BLE_INIT_OPTIONS_EXT_ADV
+ * - SHCI_C2_BLE_INIT_OPTIONS_NO_EXT_ADV
+ * - SHCI_C2_BLE_INIT_OPTIONS_CS_ALGO2
+ * - SHCI_C2_BLE_INIT_OPTIONS_NO_CS_ALGO2
+ * - SHCI_C2_BLE_INIT_OPTIONS_POWER_CLASS_1
+ * - SHCI_C2_BLE_INIT_OPTIONS_POWER_CLASS_2_3
+ * which are used to set following configuration bits:
+ * (bit 0): 1: LL only
+ *          0: LL + host
+ * (bit 1): 1: no service change desc.
+ *          0: with service change desc.
+ * (bit 2): 1: device name Read-Only
+ *          0: device name R/W
+ * (bit 3): 1: extended advertizing supported       [NOT SUPPORTED]
+ *          0: extended advertizing not supported   [NOT SUPPORTED]
+ * (bit 4): 1: CS Algo #2 supported
+ *          0: CS Algo #2 not supported
+ * (bit 7): 1: LE Power Class 1
+ *          0: LE Power Class 2-3
+ * other bits: reserved (shall be set to 0)
+ */
+#define CFG_BLE_OPTIONS                                                                 \
+    (SHCI_C2_BLE_INIT_OPTIONS_LL_HOST | SHCI_C2_BLE_INIT_OPTIONS_WITH_SVC_CHANGE_DESC | \
+     SHCI_C2_BLE_INIT_OPTIONS_DEVICE_NAME_RO | SHCI_C2_BLE_INIT_OPTIONS_EXT_ADV |       \
+     SHCI_C2_BLE_INIT_OPTIONS_CS_ALGO2 | SHCI_C2_BLE_INIT_OPTIONS_POWER_CLASS_2_3)
+
+/**
+ * Queue length of BLE Event
+ * This parameter defines the number of asynchronous events that can be stored in the HCI layer before
+ * being reported to the application. When a command is sent to the BLE core coprocessor, the HCI layer
+ * is waiting for the event with the Num_HCI_Command_Packets set to 1. The receive queue shall be large
+ * enough to store all asynchronous events received in between.
+ * When CFG_TLBLE_MOST_EVENT_PAYLOAD_SIZE is set to 27, this allow to store three 255 bytes long asynchronous events
+ * between the HCI command and its event.
+ * This parameter depends on the value given to CFG_TLBLE_MOST_EVENT_PAYLOAD_SIZE. When the queue size is to small,
+ * the system may hang if the queue is full with asynchronous events and the HCI layer is still waiting
+ * for a CC/CS event, In that case, the notification TL_BLE_HCI_ToNot() is called to indicate
+ * to the application a HCI command did not receive its command event within 30s (Default HCI Timeout).
+ */
+#define CFG_TLBLE_EVT_QUEUE_LENGTH 5
+/**
+ * This parameter should be set to fit most events received by the HCI layer. It defines the buffer size of each element
+ * allocated in the queue of received events and can be used to optimize the amount of RAM allocated by the Memory Manager.
+ * It should not exceed 255 which is the maximum HCI packet payload size (a greater value is a lost of memory as it will
+ * never be used)
+ * With the current wireless firmware implementation, this parameter shall be kept to 255
+ *
+ */
+#define CFG_TLBLE_MOST_EVENT_PAYLOAD_SIZE \
+    255 /**< Set to 255 with the memory manager and the mailbox */
+
+#define TL_BLE_EVENT_FRAME_SIZE (TL_EVT_HDR_SIZE + CFG_TLBLE_MOST_EVENT_PAYLOAD_SIZE)
+
+/* Various defines for compatibility with -Wundef - thanks, ST */
+#define CFG_DEBUG_TRACE_FULL  0
+#define CFG_DEBUG_TRACE_LIGHT 0

+ 1307 - 0
seos/headers/ble_gatt_aci.h

@@ -0,0 +1,1307 @@
+/*****************************************************************************
+ * @file    ble_gatt_aci.h
+ * @brief   STM32WB BLE API (gatt_aci)
+ *          Auto-generated file: do not edit!
+ *****************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2018-2024 STMicroelectronics.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ *****************************************************************************
+ */
+
+#ifndef BLE_GATT_ACI_H__
+#define BLE_GATT_ACI_H__
+
+//#include "ble_types.h"
+#include <ble/core/auto/ble_types.h>
+
+/**
+ * @brief ACI_GATT_INIT
+ * Initializes the GATT layer for server and client roles. It also adds the
+ * GATT service with Service Changed Characteristic.
+ * Until this command is issued the GATT channel does not process any commands
+ * even if the connection is opened. This command has to be given before using
+ * any of the GAP features.
+ * 
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_init(void);
+
+/**
+ * @brief ACI_GATT_ADD_SERVICE
+ * Add a service to GATT Server. When a service is created in the server, the
+ * host needs to reserve the handle ranges for this service using
+ * Max_Attribute_Records parameter. This parameter specifies the maximum number
+ * of attribute records that can be added to this service (including the
+ * service attribute, include attribute, characteristic attribute,
+ * characteristic value attribute and characteristic descriptor attribute).
+ * Handle of the created service is returned in command complete event. Service
+ * declaration is taken from the service pool.
+ * The attributes for characteristics and descriptors are allocated from the
+ * attribute pool.
+ * 
+ * @param Service_UUID_Type UUID type: 0x01 = 16 bits UUID while 0x02 = 128
+ *        bits UUID
+ * @param Service_UUID See @ref Service_UUID_t
+ * @param Service_Type Service type.
+ *        Values:
+ *        - 0x01: Primary Service
+ *        - 0x02: Secondary Service
+ * @param Max_Attribute_Records Maximum number of attribute records that can be
+ *        added to this service
+ * @param[out] Service_Handle Handle of the Service.
+ *        When this service is added, a handle is allocated by the server for
+ *        this service.
+ *        Server also allocates a range of handles for this service from
+ *        serviceHandle to <serviceHandle + max_attr_records - 1>
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_add_service(
+    uint8_t Service_UUID_Type,
+    const Service_UUID_t* Service_UUID,
+    uint8_t Service_Type,
+    uint8_t Max_Attribute_Records,
+    uint16_t* Service_Handle);
+
+/**
+ * @brief ACI_GATT_INCLUDE_SERVICE
+ * Include a service given by Include_Start_Handle and Include_End_Handle to
+ * another service given by Service_Handle. Attribute server creates an INCLUDE
+ * definition attribute and return the handle of this attribute in
+ * Included_handle.
+ * 
+ * @param Service_Handle Handle of the Service to which another service has to
+ *        be included.
+ * @param Include_Start_Handle Start Handle of the Service which has to be
+ *        included in service
+ * @param Include_End_Handle End Handle of the Service which has to be included
+ *        in service
+ * @param Include_UUID_Type UUID type: 0x01 = 16 bits UUID while 0x02 = 128
+ *        bits UUID
+ * @param Include_UUID See @ref Include_UUID_t
+ * @param[out] Include_Handle Handle of the include declaration
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_include_service(
+    uint16_t Service_Handle,
+    uint16_t Include_Start_Handle,
+    uint16_t Include_End_Handle,
+    uint8_t Include_UUID_Type,
+    const Include_UUID_t* Include_UUID,
+    uint16_t* Include_Handle);
+
+/**
+ * @brief ACI_GATT_ADD_CHAR
+ * Adds a characteristic to a service.
+ * The command returns the handle of the declaration attribute. The attribute
+ * that holds the Characteristic Value is always allocated at the next handle
+ * (Char_Handle + 1). The Characteristic Value is immediately followed, in
+ * order, by:
+ * - the Server Characteristic Configuration descriptor if CHAR_PROP_BROADCAST
+ * is selected;
+ * - the Client Characteristic Configuration descriptor if CHAR_PROP_NOTIFY or
+ * CHAR_PROP_INDICATE properties is selected;
+ * - the Characteristic Extended Properties descriptor if CHAR_PROP_EXT is
+ * selected.
+ * For instance, if CHAR_PROP_NOTIFY is selected but not CHAR_PROP_BROADCAST
+ * nor CHAR_PROP_EXT, then the Client Characteristic Configuration attribute
+ * handle is Char_Handle + 2.
+ * Additional descriptors can be added to the characteristic by calling the
+ * ACI_GATT_ADD_CHAR_DESC command immediately after calling this command.
+ * 
+ * @param Service_Handle Handle of the Service to which the characteristic will
+ *        be added
+ * @param Char_UUID_Type UUID type: 0x01 = 16 bits UUID while 0x02 = 128 bits
+ *        UUID
+ * @param Char_UUID See @ref Char_UUID_t
+ * @param Char_Value_Length Maximum length of the characteristic value.
+ * @param Char_Properties Characteristic Properties (Volume 3, Part G, section
+ *        3.3.1.1 of Bluetooth Core Specification)
+ *        Flags:
+ *        - 0x00: CHAR_PROP_NONE
+ *        - 0x01: CHAR_PROP_BROADCAST (Broadcast)
+ *        - 0x02: CHAR_PROP_READ (Read)
+ *        - 0x04: CHAR_PROP_WRITE_WITHOUT_RESP (Write w/o resp)
+ *        - 0x08: CHAR_PROP_WRITE (Write)
+ *        - 0x10: CHAR_PROP_NOTIFY (Notify)
+ *        - 0x20: CHAR_PROP_INDICATE (Indicate)
+ *        - 0x40: CHAR_PROP_SIGNED_WRITE (Authenticated Signed Writes)
+ *        - 0x80: CHAR_PROP_EXT (Extended Properties)
+ * @param Security_Permissions Security permission flags.
+ *        Flags:
+ *        - 0x00: None
+ *        - 0x01: AUTHEN_READ (Need authentication to read)
+ *        - 0x02: AUTHOR_READ (Need authorization to read)
+ *        - 0x04: ENCRY_READ (Need encryption to read)
+ *        - 0x08: AUTHEN_WRITE (need authentication to write)
+ *        - 0x10: AUTHOR_WRITE (need authorization to write)
+ *        - 0x20: ENCRY_WRITE (need encryption to write)
+ * @param GATT_Evt_Mask GATT event mask.
+ *        Flags:
+ *        - 0x00: GATT_DONT_NOTIFY_EVENTS
+ *        - 0x01: GATT_NOTIFY_ATTRIBUTE_WRITE
+ *        - 0x02: GATT_NOTIFY_WRITE_REQ_AND_WAIT_FOR_APPL_RESP
+ *        - 0x04: GATT_NOTIFY_READ_REQ_AND_WAIT_FOR_APPL_RESP
+ *        - 0x08: GATT_NOTIFY_NOTIFICATION_COMPLETION
+ * @param Enc_Key_Size Minimum encryption key size required to read the
+ *        characteristic.
+ *        Values:
+ *        - 0x07 ... 0x10
+ * @param Is_Variable Specify if the characteristic value has a fixed length or
+ *        a variable length.
+ *        Values:
+ *        - 0x00: Fixed length
+ *        - 0x01: Variable length
+ * @param[out] Char_Handle Handle of the characteristic that has been added (it
+ *        is the handle of the characteristic declaration).
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_add_char(
+    uint16_t Service_Handle,
+    uint8_t Char_UUID_Type,
+    const Char_UUID_t* Char_UUID,
+    uint16_t Char_Value_Length,
+    uint8_t Char_Properties,
+    uint8_t Security_Permissions,
+    uint8_t GATT_Evt_Mask,
+    uint8_t Enc_Key_Size,
+    uint8_t Is_Variable,
+    uint16_t* Char_Handle);
+
+/**
+ * @brief ACI_GATT_ADD_CHAR_DESC
+ * Adds a characteristic descriptor to a service.
+ * Note that this command allocates the new handle for the descriptor after the
+ * currently allocated handles. It is therefore advisable to call this command
+ * following the call of the command ACI_GATT_ADD_CHAR which created the
+ * characteristic containing this descriptor.
+ * 
+ * @param Service_Handle Handle of service to which the characteristic belongs
+ * @param Char_Handle Handle of the characteristic to which description has to
+ *        be added
+ * @param Char_Desc_Uuid_Type UUID type: 0x01 = 16 bits UUID while 0x02 = 128
+ *        bits UUID
+ * @param Char_Desc_Uuid See @ref Char_Desc_Uuid_t
+ * @param Char_Desc_Value_Max_Len The maximum length of the descriptor value
+ * @param Char_Desc_Value_Length Current Length of the characteristic
+ *        descriptor value
+ * @param Char_Desc_Value Value of the characteristic description
+ * @param Security_Permissions Security permission flags.
+ *        Flags:
+ *        - 0x00: None
+ *        - 0x01: AUTHEN_READ (Need authentication to read)
+ *        - 0x02: AUTHOR_READ (Need authorization to read)
+ *        - 0x04: ENCRY_READ (Need encryption to read)
+ *        - 0x08: AUTHEN_WRITE (need authentication to write)
+ *        - 0x10: AUTHOR_WRITE (need authorization to write)
+ *        - 0x20: ENCRY_WRITE (need encryption to write)
+ * @param Access_Permissions Access permission
+ *        Flags:
+ *        - 0x00: None
+ *        - 0x01: READ
+ *        - 0x02: WRITE
+ *        - 0x04: WRITE_WO_RESP
+ *        - 0x08: SIGNED_WRITE
+ * @param GATT_Evt_Mask GATT event mask.
+ *        Flags:
+ *        - 0x00: GATT_DONT_NOTIFY_EVENTS
+ *        - 0x01: GATT_NOTIFY_ATTRIBUTE_WRITE
+ *        - 0x02: GATT_NOTIFY_WRITE_REQ_AND_WAIT_FOR_APPL_RESP
+ *        - 0x04: GATT_NOTIFY_READ_REQ_AND_WAIT_FOR_APPL_RESP
+ * @param Enc_Key_Size Minimum encryption key size required to read the
+ *        characteristic.
+ *        Values:
+ *        - 0x07 ... 0x10
+ * @param Is_Variable Specify if the characteristic value has a fixed length or
+ *        a variable length.
+ *        Values:
+ *        - 0x00: Fixed length
+ *        - 0x01: Variable length
+ * @param[out] Char_Desc_Handle Handle of the characteristic descriptor
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_add_char_desc(
+    uint16_t Service_Handle,
+    uint16_t Char_Handle,
+    uint8_t Char_Desc_Uuid_Type,
+    const Char_Desc_Uuid_t* Char_Desc_Uuid,
+    uint8_t Char_Desc_Value_Max_Len,
+    uint8_t Char_Desc_Value_Length,
+    const uint8_t* Char_Desc_Value,
+    uint8_t Security_Permissions,
+    uint8_t Access_Permissions,
+    uint8_t GATT_Evt_Mask,
+    uint8_t Enc_Key_Size,
+    uint8_t Is_Variable,
+    uint16_t* Char_Desc_Handle);
+
+/**
+ * @brief ACI_GATT_UPDATE_CHAR_VALUE
+ * Updates a characteristic value in a service. If notifications (or
+ * indications) are enabled on that characteristic, a notification (or
+ * indication) is sent to any client that has registered for notifications (or
+ * indications) via the Client Characteristic Configuration.
+ * Notes:
+ * - The command is disallowed if it would cause the generation of an
+ * indication on a bearer which is still awaiting confirmation of a previous
+ * indication.
+ * - The command does not execute and returns BLE_STATUS_BUSY if notifications
+ * from a previous call are not completed. The application can enable and wait
+ * for the event ACI_GATT_NOTIFICATION_COMPLETE_EVENT to avoid this case.
+ * - The command does not execute and returns BLE_STATUS_INSUFFICIENT_RESOURCES
+ * if there is no more room in the TX pool to allocate notification (or
+ * indication) packets. This happens if notifications (or indications) are
+ * enabled and the application calls this command at an higher rate than what
+ * is allowed by the link. Throughput on BLE link depends on connection
+ * interval and connection length parameters (decided by the Central, see
+ * ACI_L2CAP_CONNECTION_PARAMETER_UPDATE_REQ for more information on how to
+ * suggest new connection parameters from a Peripheral). The application can
+ * wait for the event ACI_GATT_TX_POOL_AVAILABLE_EVENT before retrying a call
+ * to this command. It can also retry the call until it does not return
+ * BLE_STATUS_INSUFFICIENT_RESOURCES anymore.
+ * - When calling this command, the characteristic value is updated only if the
+ * command returns BLE_STATUS_SUCCESS or BLE_STATUS_SEC_PERMISSION_ERROR. The
+ * security permission error means that at least one client has not been
+ * notified due to security requirements not met.
+ * 
+ * @param Service_Handle Handle of service to which the characteristic belongs
+ * @param Char_Handle Handle of the characteristic declaration
+ * @param Val_Offset The offset from which the attribute value has to be
+ *        updated.
+ *        If this is set to 0 and the attribute value is of variable length,
+ *        then the length of the attribute will be set to the
+ *        Char_Value_Length.
+ *        If the Val_Offset is set to a value greater than 0, then the length
+ *        of the attribute will be set to the maximum length as specified for
+ *        the attribute while adding the characteristic.
+ * @param Char_Value_Length Length of the Char_Value parameter in octets.
+ *        On STM32WB, this value must not exceed (BLE_CMD_MAX_PARAM_LEN - 6)
+ *        i.e. 249 for BLE_CMD_MAX_PARAM_LEN default value.
+ * @param Char_Value Characteristic value
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_update_char_value(
+    uint16_t Service_Handle,
+    uint16_t Char_Handle,
+    uint8_t Val_Offset,
+    uint8_t Char_Value_Length,
+    const uint8_t* Char_Value);
+
+/**
+ * @brief ACI_GATT_DEL_CHAR
+ * Deletes the specified characteristic from the service.
+ * 
+ * @param Serv_Handle Handle of service to which the characteristic belongs
+ * @param Char_Handle Handle of the characteristic which has to be deleted
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_del_char(uint16_t Serv_Handle, uint16_t Char_Handle);
+
+/**
+ * @brief ACI_GATT_DEL_SERVICE
+ * Deletes the specified service from the GATT server database.
+ * 
+ * @param Serv_Handle Handle of the service to be deleted
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_del_service(uint16_t Serv_Handle);
+
+/**
+ * @brief ACI_GATT_DEL_INCLUDE_SERVICE
+ * Deletes the Include definition from the service.
+ * 
+ * @param Serv_Handle Handle of the service to which the include service
+ *        belongs
+ * @param Include_Handle Handle of the included service which has to be deleted
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_del_include_service(uint16_t Serv_Handle, uint16_t Include_Handle);
+
+/**
+ * @brief ACI_GATT_SET_EVENT_MASK
+ * Masks events from the GATT. If the bit in the GATT_Evt_Mask is set to a one,
+ * then the event associated with that bit will be enabled.
+ * The default configuration is all the events masked.
+ * 
+ * @param GATT_Evt_Mask GATT/ATT event mask.
+ *        Values:
+ *        - 0x00000001: ACI_GATT_ATTRIBUTE_MODIFIED_EVENT
+ *        - 0x00000002: ACI_GATT_PROC_TIMEOUT_EVENT
+ *        - 0x00000004: ACI_ATT_EXCHANGE_MTU_RESP_EVENT
+ *        - 0x00000008: ACI_ATT_FIND_INFO_RESP_EVENT
+ *        - 0x00000010: ACI_ATT_FIND_BY_TYPE_VALUE_RESP_EVENT
+ *        - 0x00000020: ACI_ATT_READ_BY_TYPE_RESP_EVENT
+ *        - 0x00000040: ACI_ATT_READ_RESP_EVENT
+ *        - 0x00000080: ACI_ATT_READ_BLOB_RESP_EVENT
+ *        - 0x00000100: ACI_ATT_READ_MULTIPLE_RESP_EVENT
+ *        - 0x00000200: ACI_ATT_READ_BY_GROUP_TYPE_RESP_EVENT
+ *        - 0x00000800: ACI_ATT_PREPARE_WRITE_RESP_EVENT
+ *        - 0x00001000: ACI_ATT_EXEC_WRITE_RESP_EVENT
+ *        - 0x00002000: ACI_GATT_INDICATION_EVENT
+ *        - 0x00004000: ACI_GATT_NOTIFICATION_EVENT
+ *        - 0x00008000: ACI_GATT_ERROR_RESP_EVENT
+ *        - 0x00010000: ACI_GATT_PROC_COMPLETE_EVENT
+ *        - 0x00020000: ACI_GATT_DISC_READ_CHAR_BY_UUID_RESP_EVENT
+ *        - 0x00040000: ACI_GATT_TX_POOL_AVAILABLE_EVENT
+ *        - 0x00100000: ACI_GATT_READ_EXT_EVENT
+ *        - 0x00200000: ACI_GATT_INDICATION_EXT_EVENT
+ *        - 0x00400000: ACI_GATT_NOTIFICATION_EXT_EVENT
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_set_event_mask(uint32_t GATT_Evt_Mask);
+
+/**
+ * @brief ACI_GATT_EXCHANGE_CONFIG
+ * Performs an ATT MTU exchange procedure.
+ * When the ATT MTU exchange procedure is completed, a
+ * ACI_ATT_EXCHANGE_MTU_RESP_EVENT event is generated. A
+ * ACI_GATT_PROC_COMPLETE_EVENT event is also generated to indicate the end of
+ * the procedure.
+ * 
+ * @param Connection_Handle Connection handle for which the command applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_exchange_config(uint16_t Connection_Handle);
+
+/**
+ * @brief ACI_ATT_FIND_INFO_REQ
+ * Sends a Find Information Request.
+ * This command is used to obtain the mapping of attribute handles with their
+ * associated types. The responses of the procedure are given through the
+ * ACI_ATT_FIND_INFO_RESP_EVENT event. The end of the procedure is indicated by
+ * a ACI_GATT_PROC_COMPLETE_EVENT event.
+ * 
+ * @param Connection_Handle Connection handle for which the command applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF
+ * @param Start_Handle First requested handle number
+ * @param End_Handle Last requested handle number
+ * @return Value indicating success or error code.
+ */
+tBleStatus
+    aci_att_find_info_req(uint16_t Connection_Handle, uint16_t Start_Handle, uint16_t End_Handle);
+
+/**
+ * @brief ACI_ATT_FIND_BY_TYPE_VALUE_REQ
+ * Sends a Find By Type Value Request
+ * The Find By Type Value Request is used to obtain the handles of attributes
+ * that have a given 16-bit UUID attribute type and a given attribute value.
+ * The responses of the procedure are given through the
+ * ACI_ATT_FIND_BY_TYPE_VALUE_RESP_EVENT event.
+ * The end of the procedure is indicated by a ACI_GATT_PROC_COMPLETE_EVENT
+ * event.
+ * 
+ * @param Connection_Handle Connection handle for which the command applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF
+ * @param Start_Handle First requested handle number
+ * @param End_Handle Last requested handle number
+ * @param UUID 2 octet UUID to find (little-endian)
+ * @param Attribute_Val_Length Length of attribute value (maximum value is
+ *        ATT_MTU - 7).
+ * @param Attribute_Val Attribute value to find
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_att_find_by_type_value_req(
+    uint16_t Connection_Handle,
+    uint16_t Start_Handle,
+    uint16_t End_Handle,
+    uint16_t UUID,
+    uint8_t Attribute_Val_Length,
+    const uint8_t* Attribute_Val);
+
+/**
+ * @brief ACI_ATT_READ_BY_TYPE_REQ
+ * Sends a Read By Type Request.
+ * The Read By Type Request is used to obtain the values of attributes where
+ * the attribute type is known but the handle is not known.
+ * The responses are given through the ACI_ATT_READ_BY_TYPE_RESP_EVENT event.
+ * 
+ * @param Connection_Handle Connection handle for which the command applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF
+ * @param Start_Handle First requested handle number
+ * @param End_Handle Last requested handle number
+ * @param UUID_Type UUID type: 0x01 = 16 bits UUID while 0x02 = 128 bits UUID
+ * @param UUID See @ref UUID_t
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_att_read_by_type_req(
+    uint16_t Connection_Handle,
+    uint16_t Start_Handle,
+    uint16_t End_Handle,
+    uint8_t UUID_Type,
+    const UUID_t* UUID);
+
+/**
+ * @brief ACI_ATT_READ_BY_GROUP_TYPE_REQ
+ * Sends a Read By Group Type Request.
+ * The Read By Group Type Request is used to obtain the values of grouping
+ * attributes where the attribute type is known but the handle is not known.
+ * Grouping attributes are defined at GATT layer. The grouping attribute types
+ * are: "Primary Service", "Secondary Service" and "Characteristic".
+ * The responses of the procedure are given through the
+ * ACI_ATT_READ_BY_GROUP_TYPE_RESP_EVENT event.
+ * The end of the procedure is indicated by a ACI_GATT_PROC_COMPLETE_EVENT.
+ * 
+ * @param Connection_Handle Connection handle for which the command applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF
+ * @param Start_Handle First requested handle number
+ * @param End_Handle Last requested handle number
+ * @param UUID_Type UUID type: 0x01 = 16 bits UUID while 0x02 = 128 bits UUID
+ * @param UUID See @ref UUID_t
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_att_read_by_group_type_req(
+    uint16_t Connection_Handle,
+    uint16_t Start_Handle,
+    uint16_t End_Handle,
+    uint8_t UUID_Type,
+    const UUID_t* UUID);
+
+/**
+ * @brief ACI_ATT_PREPARE_WRITE_REQ
+ * Sends a Prepare Write Request.
+ * The Prepare Write Request is used to request the server to prepare to write
+ * the value of an attribute.
+ * The responses of the procedure are given through the
+ * ACI_ATT_PREPARE_WRITE_RESP_EVENT event.
+ * The end of the procedure is indicated by a ACI_GATT_PROC_COMPLETE_EVENT.
+ * 
+ * @param Connection_Handle Connection handle for which the command applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF
+ * @param Attr_Handle Handle of the attribute to be written
+ * @param Val_Offset The offset of the first octet to be written
+ * @param Attribute_Val_Length Length of attribute value (maximum value is
+ *        ATT_MTU - 5).
+ * @param Attribute_Val The value of the attribute to be written
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_att_prepare_write_req(
+    uint16_t Connection_Handle,
+    uint16_t Attr_Handle,
+    uint16_t Val_Offset,
+    uint8_t Attribute_Val_Length,
+    const uint8_t* Attribute_Val);
+
+/**
+ * @brief ACI_ATT_EXECUTE_WRITE_REQ
+ * Sends an Execute Write Request.
+ * The Execute Write Request is used to request the server to write or cancel
+ * the write of all the prepared values currently held in the prepare queue
+ * from this client.
+ * The result of the procedure is given through the
+ * ACI_ATT_EXEC_WRITE_RESP_EVENT event.
+ * The end of the procedure is indicated by a ACI_GATT_PROC_COMPLETE_EVENT
+ * event.
+ * 
+ * @param Connection_Handle Connection handle for which the command applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF
+ * @param Execute Execute or cancel writes.
+ *        Values:
+ *        - 0x00: Cancel all prepared writes
+ *        - 0x01: Immediately write all pending prepared values
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_att_execute_write_req(uint16_t Connection_Handle, uint8_t Execute);
+
+/**
+ * @brief ACI_GATT_DISC_ALL_PRIMARY_SERVICES
+ * Starts the GATT client procedure to discover all primary services on the
+ * server.
+ * The responses of the procedure are given through the
+ * ACI_ATT_READ_BY_GROUP_TYPE_RESP_EVENT event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_disc_all_primary_services(uint16_t Connection_Handle);
+
+/**
+ * @brief ACI_GATT_DISC_PRIMARY_SERVICE_BY_UUID
+ * Starts the procedure to discover the primary services of the specified UUID
+ * on the server.
+ * The responses of the procedure are given through the
+ * ACI_ATT_FIND_BY_TYPE_VALUE_RESP_EVENT event.
+ * The end of the procedure is indicated by a ACI_GATT_PROC_COMPLETE_EVENT
+ * event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param UUID_Type UUID type: 0x01 = 16 bits UUID while 0x02 = 128 bits UUID
+ * @param UUID See @ref UUID_t
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_disc_primary_service_by_uuid(
+    uint16_t Connection_Handle,
+    uint8_t UUID_Type,
+    const UUID_t* UUID);
+
+/**
+ * @brief ACI_GATT_FIND_INCLUDED_SERVICES
+ * Starts the procedure to find all included services.
+ * The responses of the procedure are given through the
+ * ACI_ATT_READ_BY_TYPE_RESP_EVENT event.
+ * The end of the procedure is indicated by a ACI_GATT_PROC_COMPLETE_EVENT
+ * event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Start_Handle Start attribute handle of the service
+ * @param End_Handle End attribute handle of the service
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_find_included_services(
+    uint16_t Connection_Handle,
+    uint16_t Start_Handle,
+    uint16_t End_Handle);
+
+/**
+ * @brief ACI_GATT_DISC_ALL_CHAR_OF_SERVICE
+ * Starts the procedure to discover all the characteristics of a given service.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. Before procedure completion the response packets are given
+ * through ACI_ATT_READ_BY_TYPE_RESP_EVENT event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Start_Handle Start attribute handle of the service
+ * @param End_Handle End attribute handle of the service
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_disc_all_char_of_service(
+    uint16_t Connection_Handle,
+    uint16_t Start_Handle,
+    uint16_t End_Handle);
+
+/**
+ * @brief ACI_GATT_DISC_CHAR_BY_UUID
+ * Starts the procedure to discover all the characteristics specified by a
+ * UUID.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. Before procedure completion the response packets are given
+ * through ACI_GATT_DISC_READ_CHAR_BY_UUID_RESP_EVENT event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Start_Handle Start attribute handle of the service
+ * @param End_Handle End attribute handle of the service
+ * @param UUID_Type UUID type: 0x01 = 16 bits UUID while 0x02 = 128 bits UUID
+ * @param UUID See @ref UUID_t
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_disc_char_by_uuid(
+    uint16_t Connection_Handle,
+    uint16_t Start_Handle,
+    uint16_t End_Handle,
+    uint8_t UUID_Type,
+    const UUID_t* UUID);
+
+/**
+ * @brief ACI_GATT_DISC_ALL_CHAR_DESC
+ * Starts the procedure to discover all characteristic descriptors on the
+ * server.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. Before procedure completion the response packets are given
+ * through ACI_ATT_FIND_INFO_RESP_EVENT event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Char_Handle Handle of the characteristic value
+ * @param End_Handle End handle of the characteristic
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_disc_all_char_desc(
+    uint16_t Connection_Handle,
+    uint16_t Char_Handle,
+    uint16_t End_Handle);
+
+/**
+ * @brief ACI_GATT_READ_CHAR_VALUE
+ * Starts the procedure to read the attribute value.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. Before procedure completion the response packet is given through
+ * ACI_ATT_READ_RESP_EVENT event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Attr_Handle Handle of the characteristic value to be read
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_read_char_value(uint16_t Connection_Handle, uint16_t Attr_Handle);
+
+/**
+ * @brief ACI_GATT_READ_USING_CHAR_UUID
+ * This command sends a Read By Type Request packet to the server in order to
+ * read the value attribute of the characteristics specified by the UUID.
+ * When the procedure is completed, an ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. Before procedure completion, the response packet is given through
+ * one ACI_GATT_DISC_READ_CHAR_BY_UUID_RESP_EVENT event per reported attribute.
+ * Note: the number of bytes of a value reported by
+ * ACI_GATT_DISC_READ_CHAR_BY_UUID_RESP_EVENT event cannot exceed
+ * BLE_EVT_MAX_PARAM_LEN - 7 i.e. 248 bytes for default value of
+ * BLE_EVT_MAX_PARAM_LEN.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Start_Handle Starting handle of the range to be searched
+ * @param End_Handle End handle of the range to be searched
+ * @param UUID_Type UUID type: 0x01 = 16 bits UUID while 0x02 = 128 bits UUID
+ * @param UUID See @ref UUID_t
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_read_using_char_uuid(
+    uint16_t Connection_Handle,
+    uint16_t Start_Handle,
+    uint16_t End_Handle,
+    uint8_t UUID_Type,
+    const UUID_t* UUID);
+
+/**
+ * @brief ACI_GATT_READ_LONG_CHAR_VALUE
+ * Starts the procedure to read a long characteristic value.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. Before procedure completion the response packets are given
+ * through ACI_ATT_READ_BLOB_RESP_EVENT event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Attr_Handle Handle of the characteristic value to be read
+ * @param Val_Offset Offset from which the value needs to be read
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_read_long_char_value(
+    uint16_t Connection_Handle,
+    uint16_t Attr_Handle,
+    uint16_t Val_Offset);
+
+/**
+ * @brief ACI_GATT_READ_MULTIPLE_CHAR_VALUE
+ * Starts a procedure to read multiple characteristic values from a server.
+ * The command must specify the handles of the characteristic values to be
+ * read.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. Before procedure completion the response packets are given
+ * through ACI_ATT_READ_MULTIPLE_RESP_EVENT event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Number_of_Handles Number of handles in the following table
+ *        Values:
+ *        - 0x02 ... 0x7E
+ * @param Handle_Entry See @ref Handle_Entry_t
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_read_multiple_char_value(
+    uint16_t Connection_Handle,
+    uint8_t Number_of_Handles,
+    const Handle_Entry_t* Handle_Entry);
+
+/**
+ * @brief ACI_GATT_WRITE_CHAR_VALUE
+ * Starts the procedure to write a characteristic value.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated.
+ * The length of the value to be written must not exceed (ATT_MTU - 3). On
+ * STM32WB, it must also not exceed (BLE_CMD_MAX_PARAM_LEN - 5) i.e. 250 for
+ * BLE_CMD_MAX_PARAM_LEN default value.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Attr_Handle Handle of the characteristic value to be written
+ * @param Attribute_Val_Length Length of the value to be written
+ * @param Attribute_Val Value to be written
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_write_char_value(
+    uint16_t Connection_Handle,
+    uint16_t Attr_Handle,
+    uint8_t Attribute_Val_Length,
+    const uint8_t* Attribute_Val);
+
+/**
+ * @brief ACI_GATT_WRITE_LONG_CHAR_VALUE
+ * Starts the procedure to write a long characteristic value.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. During the procedure, ACI_ATT_PREPARE_WRITE_RESP_EVENT and
+ * ACI_ATT_EXEC_WRITE_RESP_EVENT events are raised.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Attr_Handle Handle of the characteristic value to be written
+ * @param Val_Offset Offset at which the attribute has to be written
+ * @param Attribute_Val_Length Length of the value to be written.
+ *        On STM32WB, this value must not exceed (BLE_CMD_MAX_PARAM_LEN - 7)
+ *        i.e. 248 for BLE_CMD_MAX_PARAM_LEN default value.
+ * @param Attribute_Val Value to be written
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_write_long_char_value(
+    uint16_t Connection_Handle,
+    uint16_t Attr_Handle,
+    uint16_t Val_Offset,
+    uint8_t Attribute_Val_Length,
+    const uint8_t* Attribute_Val);
+
+/**
+ * @brief ACI_GATT_WRITE_CHAR_RELIABLE
+ * Starts the procedure to write a characteristic reliably.
+ * When the procedure is completed, a  ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. During the procedure, ACI_ATT_PREPARE_WRITE_RESP_EVENT and
+ * ACI_ATT_EXEC_WRITE_RESP_EVENT events are raised.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Attr_Handle Handle of the attribute to be written
+ * @param Val_Offset Offset at which the attribute has to be written
+ * @param Attribute_Val_Length Length of the value to be written.
+ *        On STM32WB, this value must not exceed (BLE_CMD_MAX_PARAM_LEN - 7)
+ *        i.e. 248 for BLE_CMD_MAX_PARAM_LEN default value.
+ * @param Attribute_Val Value to be written
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_write_char_reliable(
+    uint16_t Connection_Handle,
+    uint16_t Attr_Handle,
+    uint16_t Val_Offset,
+    uint8_t Attribute_Val_Length,
+    const uint8_t* Attribute_Val);
+
+/**
+ * @brief ACI_GATT_WRITE_LONG_CHAR_DESC
+ * Starts the procedure to write a long characteristic descriptor.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. During the procedure, ACI_ATT_PREPARE_WRITE_RESP_EVENT and
+ * ACI_ATT_EXEC_WRITE_RESP_EVENT events are raised.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Attr_Handle Handle of the attribute to be written
+ * @param Val_Offset Offset at which the attribute has to be written
+ * @param Attribute_Val_Length Length of the value to be written.
+ *        On STM32WB, this value must not exceed (BLE_CMD_MAX_PARAM_LEN - 7)
+ *        i.e. 248 for BLE_CMD_MAX_PARAM_LEN default value.
+ * @param Attribute_Val Value to be written
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_write_long_char_desc(
+    uint16_t Connection_Handle,
+    uint16_t Attr_Handle,
+    uint16_t Val_Offset,
+    uint8_t Attribute_Val_Length,
+    const uint8_t* Attribute_Val);
+
+/**
+ * @brief ACI_GATT_READ_LONG_CHAR_DESC
+ * Starts the procedure to read a long characteristic value.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. Before procedure completion the response packets are given
+ * through ACI_ATT_READ_BLOB_RESP_EVENT event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Attr_Handle Handle of the characteristic descriptor
+ * @param Val_Offset Offset from which the value needs to be read
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_read_long_char_desc(
+    uint16_t Connection_Handle,
+    uint16_t Attr_Handle,
+    uint16_t Val_Offset);
+
+/**
+ * @brief ACI_GATT_WRITE_CHAR_DESC
+ * Starts the procedure to write a characteristic descriptor.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated.
+ * The length of the value to be written must not exceed (ATT_MTU - 3). On
+ * STM32WB, it must also not exceed (BLE_CMD_MAX_PARAM_LEN - 5) i.e. 250 for
+ * BLE_CMD_MAX_PARAM_LEN default value.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Attr_Handle Handle of the attribute to be written
+ * @param Attribute_Val_Length Length of the value to be written
+ * @param Attribute_Val Value to be written
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_write_char_desc(
+    uint16_t Connection_Handle,
+    uint16_t Attr_Handle,
+    uint8_t Attribute_Val_Length,
+    const uint8_t* Attribute_Val);
+
+/**
+ * @brief ACI_GATT_READ_CHAR_DESC
+ * Starts the procedure to read the descriptor specified.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated.
+ * Before procedure completion the response packet is given through
+ * ACI_ATT_READ_RESP_EVENT event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Attr_Handle Handle of the descriptor to be read
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_read_char_desc(uint16_t Connection_Handle, uint16_t Attr_Handle);
+
+/**
+ * @brief ACI_GATT_WRITE_WITHOUT_RESP
+ * Starts the procedure to write a characteristic value without waiting for any
+ * response from the server. No events are generated after this command is
+ * executed.
+ * The length of the value to be written must not exceed (ATT_MTU - 3). On
+ * STM32WB, it must also not exceed (BLE_CMD_MAX_PARAM_LEN - 5) i.e. 250 for
+ * BLE_CMD_MAX_PARAM_LEN default value.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Attr_Handle Handle of the characteristic value to be written
+ * @param Attribute_Val_Length Length of the value to be written
+ * @param Attribute_Val Value to be written
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_write_without_resp(
+    uint16_t Connection_Handle,
+    uint16_t Attr_Handle,
+    uint8_t Attribute_Val_Length,
+    const uint8_t* Attribute_Val);
+
+/**
+ * @brief ACI_GATT_SIGNED_WRITE_WITHOUT_RESP
+ * Starts a signed write without response from the server.
+ * The procedure is used to write a characteristic value with an authentication
+ * signature without waiting for any response from the server. It cannot be
+ * used when the link is encrypted.
+ * The length of the value to be written must not exceed (ATT_MTU - 15). On
+ * STM32WB, it must also not exceed (BLE_CMD_MAX_PARAM_LEN - 5) i.e. 250 for
+ * BLE_CMD_MAX_PARAM_LEN default value.
+ * 
+ * @param Connection_Handle Connection handle for which the command applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF
+ * @param Attr_Handle Handle of the characteristic value to be written
+ * @param Attribute_Val_Length Length of the value to be written
+ * @param Attribute_Val Value to be written
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_signed_write_without_resp(
+    uint16_t Connection_Handle,
+    uint16_t Attr_Handle,
+    uint8_t Attribute_Val_Length,
+    const uint8_t* Attribute_Val);
+
+/**
+ * @brief ACI_GATT_CONFIRM_INDICATION
+ * Allow application to confirm indication. This command has to be sent when
+ * the application receives the event ACI_GATT_INDICATION_EVENT.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_confirm_indication(uint16_t Connection_Handle);
+
+/**
+ * @brief ACI_GATT_WRITE_RESP
+ * Allow or reject a write request from a client.
+ * This command has to be sent by the application when it receives the
+ * ACI_GATT_WRITE_PERMIT_REQ_EVENT. If the write can be allowed, then the
+ * status and error code have to be set to 0. If the write cannot be allowed,
+ * then the status has to be set to 1 and the error code has to be set to the
+ * error code that has to be passed to the client.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Attr_Handle Handle of the attribute that was passed in the event
+ *        ACI_GATT_WRITE_PERMIT_REQ_EVENT
+ * @param Write_status If the value can be written or not.
+ *        Values:
+ *        - 0x00: The value can be written to the attribute specified by
+ *          attr_handle
+ *        - 0x01: The value cannot be written to the attribute specified by the
+ *          attr_handle
+ * @param Error_Code The error code that has to be passed to the client in case
+ *        the write has to be rejected
+ * @param Attribute_Val_Length Length of the value to be written as passed in
+ *        the event ACI_GATT_WRITE_PERMIT_REQ_EVENT
+ * @param Attribute_Val Value as passed in the event
+ *        ACI_GATT_WRITE_PERMIT_REQ_EVENT
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_write_resp(
+    uint16_t Connection_Handle,
+    uint16_t Attr_Handle,
+    uint8_t Write_status,
+    uint8_t Error_Code,
+    uint8_t Attribute_Val_Length,
+    const uint8_t* Attribute_Val);
+
+/**
+ * @brief ACI_GATT_ALLOW_READ
+ * Allow the GATT server to send a response to a read request from a client.
+ * The application has to send this command when it receives the
+ * ACI_GATT_READ_PERMIT_REQ_EVENT or ACI_GATT_READ_MULTI_PERMIT_REQ_EVENT. This
+ * command indicates to the stack that the response can be sent to the client.
+ * So if the application wishes to update any of the attributes before they are
+ * read by the client, it must update the characteristic values using the
+ * ACI_GATT_UPDATE_CHAR_VALUE and then give this command. The application
+ * should perform the required operations within 30 seconds. Otherwise the GATT
+ * procedure will be timeout.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_allow_read(uint16_t Connection_Handle);
+
+/**
+ * @brief ACI_GATT_SET_SECURITY_PERMISSION
+ * This command sets the security permission for the attribute handle
+ * specified. It is allowed only for the Client Characteristic Configuration
+ * descriptor.
+ * 
+ * @param Serv_Handle Handle of the service which contains the attribute whose
+ *        security permission has to be modified
+ * @param Attr_Handle Handle of the attribute whose security permission has to
+ *        be modified
+ * @param Security_Permissions Security permission flags.
+ *        Flags:
+ *        - 0x00: None
+ *        - 0x01: AUTHEN_READ (Need authentication to read)
+ *        - 0x02: AUTHOR_READ (Need authorization to read)
+ *        - 0x04: ENCRY_READ (Need encryption to read)
+ *        - 0x08: AUTHEN_WRITE (need authentication to write)
+ *        - 0x10: AUTHOR_WRITE (need authorization to write)
+ *        - 0x20: ENCRY_WRITE (need encryption to write)
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_set_security_permission(
+    uint16_t Serv_Handle,
+    uint16_t Attr_Handle,
+    uint8_t Security_Permissions);
+
+/**
+ * @brief ACI_GATT_SET_DESC_VALUE
+ * This command sets the value of the descriptor specified by Char_Desc_Handle.
+ * 
+ * @param Serv_Handle Handle of the service which contains the characteristic
+ *        descriptor
+ * @param Char_Handle Handle of the characteristic which contains the
+ *        descriptor
+ * @param Char_Desc_Handle Handle of the descriptor whose value has to be set
+ * @param Val_Offset Offset from which the descriptor value has to be updated
+ * @param Char_Desc_Value_Length Length of the descriptor value
+ * @param Char_Desc_Value Descriptor value
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_set_desc_value(
+    uint16_t Serv_Handle,
+    uint16_t Char_Handle,
+    uint16_t Char_Desc_Handle,
+    uint16_t Val_Offset,
+    uint8_t Char_Desc_Value_Length,
+    const uint8_t* Char_Desc_Value);
+
+/**
+ * @brief ACI_GATT_READ_HANDLE_VALUE
+ * Reads the value of the attribute handle specified from the local GATT
+ * database.
+ * 
+ * @param Attr_Handle Handle of the attribute to read
+ * @param Offset Offset from which the value needs to be read
+ * @param Value_Length_Requested Maximum number of octets to be returned as
+ *        attribute value
+ * @param[out] Length Length of the attribute value
+ * @param[out] Value_Length Length in octets of the Value parameter
+ * @param[out] Value Attribute value
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_read_handle_value(
+    uint16_t Attr_Handle,
+    uint16_t Offset,
+    uint16_t Value_Length_Requested,
+    uint16_t* Length,
+    uint16_t* Value_Length,
+    uint8_t* Value);
+
+/**
+ * @brief ACI_GATT_UPDATE_CHAR_VALUE_EXT
+ * This command is a more flexible version of ACI_GATT_UPDATE_CHAR_VALUE to
+ * support update of long attribute up to 512 bytes and indicate selectively
+ * the generation of Indication/Notification.
+ * The description notes for the ACI_GATT_UPDATE_CHAR_VALUE command also apply
+ * here.
+ * 
+ * @param Conn_Handle_To_Notify Specifies the client(s) to be notified.
+ *        Values:
+ *        - 0x0000: Notify all subscribed clients on their unenhanced ATT
+ *          bearer
+ *        - 0x0001 ... 0x0EFF: Notify one client on the specified unenhanced
+ *          ATT bearer (the parameter is the connection handle)
+ *        - 0xEA00 ... 0xEA3F: Notify one client on the specified enhanced ATT
+ *          bearer (the LSB-byte of the parameter is the connection-oriented
+ *          channel index)
+ * @param Service_Handle Handle of service to which the characteristic belongs
+ * @param Char_Handle Handle of the characteristic declaration
+ * @param Update_Type Allow Notification or Indication generation, if enabled
+ *        in the client characteristic configuration descriptor
+ *        Flags:
+ *        - 0x00: Do not notify
+ *        - 0x01: Notification
+ *        - 0x02: Indication
+ * @param Char_Length Total length of the characteristic value.
+ *        In case of a variable size characteristic, this field specifies the
+ *        new length of the characteristic value after the update; in case of
+ *        fixed length characteristic this field is ignored.
+ * @param Value_Offset The offset from which the attribute value has to be
+ *        updated.
+ * @param Value_Length Length of the Value parameter in octets.
+ *        On STM32WB, this value must not exceed (BLE_CMD_MAX_PARAM_LEN - 12)
+ *        i.e. 243 for BLE_CMD_MAX_PARAM_LEN default value.
+ * @param Value Updated characteristic value
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_update_char_value_ext(
+    uint16_t Conn_Handle_To_Notify,
+    uint16_t Service_Handle,
+    uint16_t Char_Handle,
+    uint8_t Update_Type,
+    uint16_t Char_Length,
+    uint16_t Value_Offset,
+    uint8_t Value_Length,
+    const uint8_t* Value);
+
+/**
+ * @brief ACI_GATT_DENY_READ
+ * This command is used to deny the GATT server to send a response to a read
+ * request from a client.
+ * The application may send this command when it receives the
+ * ACI_GATT_READ_PERMIT_REQ_EVENT or  ACI_GATT_READ_MULTI_PERMIT_REQ_EVENT.
+ * This command indicates to the stack that the client is not allowed to read
+ * the requested characteristic due to e.g. application restrictions.
+ * The Error code shall be either 0x08 (Insufficient Authorization) or a value
+ * in the range 0x80-0x9F (Application Error).
+ * The application should issue the ACI_GATT_DENY_READ  or ACI_GATT_ALLOW_READ
+ * command within 30 seconds from the reception of the
+ * ACI_GATT_READ_PERMIT_REQ_EVENT or ACI_GATT_READ_MULTI_PERMIT_REQ_EVENT
+ * events; otherwise the GATT procedure issues a timeout.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Error_Code Error code for the command
+ *        Values:
+ *        - 0x08: Insufficient Authorization
+ *        - 0x80 ... 0x9F: Application Error
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_deny_read(uint16_t Connection_Handle, uint8_t Error_Code);
+
+/**
+ * @brief ACI_GATT_SET_ACCESS_PERMISSION
+ * This command sets the access permission for the attribute handle specified.
+ * 
+ * @param Serv_Handle Handle of the service which contains the attribute whose
+ *        access permission has to be modified
+ * @param Attr_Handle Handle of the attribute whose security permission has to
+ *        be modified
+ * @param Access_Permissions Access permission
+ *        Flags:
+ *        - 0x00: None
+ *        - 0x01: READ
+ *        - 0x02: WRITE
+ *        - 0x04: WRITE_WO_RESP
+ *        - 0x08: SIGNED_WRITE
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_set_access_permission(
+    uint16_t Serv_Handle,
+    uint16_t Attr_Handle,
+    uint8_t Access_Permissions);
+
+/**
+ * @brief ACI_GATT_STORE_DB
+ * This command forces the saving of the GATT database for all active
+ * connections. Note that, by default, the GATT database is saved per active
+ * connection at the time of disconnection.
+ * 
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_store_db(void);
+
+/**
+ * @brief ACI_GATT_SEND_MULT_NOTIFICATION
+ * This command sends a Multiple Handle Value Notification over the ATT bearer
+ * specified in parameter. The handles provided as parameters must be the
+ * handles of the characteristic declarations.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Number_of_Handles Number of handles in the following table
+ *        Values:
+ *        - 0x02 ... 0x7E
+ * @param Handle_Entry See @ref Handle_Entry_t
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_send_mult_notification(
+    uint16_t Connection_Handle,
+    uint8_t Number_of_Handles,
+    const Handle_Entry_t* Handle_Entry);
+
+/**
+ * @brief ACI_GATT_READ_MULTIPLE_VAR_CHAR_VALUE
+ * Starts a procedure to read multiple variable length characteristic values
+ * from a server.
+ * The command must specify the handles of the characteristic values to be
+ * read.
+ * When the procedure is completed, a ACI_GATT_PROC_COMPLETE_EVENT event is
+ * generated. Before procedure completion the response packets are given
+ * through ACI_ATT_READ_MULTIPLE_RESP_EVENT event.
+ * 
+ * @param Connection_Handle Specifies the ATT bearer for which the command
+ *        applies.
+ *        Values:
+ *        - 0x0000 ... 0x0EFF: Unenhanced ATT bearer (the parameter is the
+ *          connection handle)
+ *        - 0xEA00 ... 0xEA3F: Enhanced ATT bearer (the LSB-byte of the
+ *          parameter is the connection-oriented channel index)
+ * @param Number_of_Handles Number of handles in the following table
+ *        Values:
+ *        - 0x02 ... 0x7E
+ * @param Handle_Entry See @ref Handle_Entry_t
+ * @return Value indicating success or error code.
+ */
+tBleStatus aci_gatt_read_multiple_var_char_value(
+    uint16_t Connection_Handle,
+    uint8_t Number_of_Handles,
+    const Handle_Entry_t* Handle_Entry);
+
+#endif /* BLE_GATT_ACI_H__ */

+ 195 - 0
seos/headers/ble_vs_codes.h

@@ -0,0 +1,195 @@
+/*****************************************************************************
+ * @file    ble_vs_codes.h
+ * @brief   STM32WB BLE API (vendor specific event codes)
+ *          Auto-generated file: do not edit!
+ *****************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2018-2024 STMicroelectronics.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ *****************************************************************************
+ */
+
+#ifndef BLE_VS_CODES_H__
+#define BLE_VS_CODES_H__
+
+/* Vendor specific codes of ACI GAP events
+ */
+
+/* ACI_GAP_LIMITED_DISCOVERABLE_EVENT code */
+#define ACI_GAP_LIMITED_DISCOVERABLE_VSEVT_CODE 0x0400U
+
+/* ACI_GAP_PAIRING_COMPLETE_EVENT code */
+#define ACI_GAP_PAIRING_COMPLETE_VSEVT_CODE 0x0401U
+
+/* ACI_GAP_PASS_KEY_REQ_EVENT code */
+#define ACI_GAP_PASS_KEY_REQ_VSEVT_CODE 0x0402U
+
+/* ACI_GAP_AUTHORIZATION_REQ_EVENT code */
+#define ACI_GAP_AUTHORIZATION_REQ_VSEVT_CODE 0x0403U
+
+/* ACI_GAP_PERIPHERAL_SECURITY_INITIATED_EVENT code */
+#define ACI_GAP_PERIPHERAL_SECURITY_INITIATED_VSEVT_CODE 0x0404U
+
+/* ACI_GAP_BOND_LOST_EVENT code */
+#define ACI_GAP_BOND_LOST_VSEVT_CODE 0x0405U
+
+/* ACI_GAP_PROC_COMPLETE_EVENT code */
+#define ACI_GAP_PROC_COMPLETE_VSEVT_CODE 0x0407U
+
+/* ACI_GAP_ADDR_NOT_RESOLVED_EVENT code */
+#define ACI_GAP_ADDR_NOT_RESOLVED_VSEVT_CODE 0x0408U
+
+/* ACI_GAP_NUMERIC_COMPARISON_VALUE_EVENT code */
+#define ACI_GAP_NUMERIC_COMPARISON_VALUE_VSEVT_CODE 0x0409U
+
+/* ACI_GAP_KEYPRESS_NOTIFICATION_EVENT code */
+#define ACI_GAP_KEYPRESS_NOTIFICATION_VSEVT_CODE 0x040AU
+
+/* Vendor specific codes of ACI GATT/ATT events
+ */
+
+/* ACI_GATT_ATTRIBUTE_MODIFIED_EVENT code */
+#define ACI_GATT_ATTRIBUTE_MODIFIED_VSEVT_CODE 0x0C01U
+
+/* ACI_GATT_PROC_TIMEOUT_EVENT code */
+#define ACI_GATT_PROC_TIMEOUT_VSEVT_CODE 0x0C02U
+
+/* ACI_ATT_EXCHANGE_MTU_RESP_EVENT code */
+#define ACI_ATT_EXCHANGE_MTU_RESP_VSEVT_CODE 0x0C03U
+
+/* ACI_ATT_FIND_INFO_RESP_EVENT code */
+#define ACI_ATT_FIND_INFO_RESP_VSEVT_CODE 0x0C04U
+
+/* ACI_ATT_FIND_BY_TYPE_VALUE_RESP_EVENT code */
+#define ACI_ATT_FIND_BY_TYPE_VALUE_RESP_VSEVT_CODE 0x0C05U
+
+/* ACI_ATT_READ_BY_TYPE_RESP_EVENT code */
+#define ACI_ATT_READ_BY_TYPE_RESP_VSEVT_CODE 0x0C06U
+
+/* ACI_ATT_READ_RESP_EVENT code */
+#define ACI_ATT_READ_RESP_VSEVT_CODE 0x0C07U
+
+/* ACI_ATT_READ_BLOB_RESP_EVENT code */
+#define ACI_ATT_READ_BLOB_RESP_VSEVT_CODE 0x0C08U
+
+/* ACI_ATT_READ_MULTIPLE_RESP_EVENT code */
+#define ACI_ATT_READ_MULTIPLE_RESP_VSEVT_CODE 0x0C09U
+
+/* ACI_ATT_READ_BY_GROUP_TYPE_RESP_EVENT code */
+#define ACI_ATT_READ_BY_GROUP_TYPE_RESP_VSEVT_CODE 0x0C0AU
+
+/* ACI_ATT_PREPARE_WRITE_RESP_EVENT code */
+#define ACI_ATT_PREPARE_WRITE_RESP_VSEVT_CODE 0x0C0CU
+
+/* ACI_ATT_EXEC_WRITE_RESP_EVENT code */
+#define ACI_ATT_EXEC_WRITE_RESP_VSEVT_CODE 0x0C0DU
+
+/* ACI_GATT_INDICATION_EVENT code */
+#define ACI_GATT_INDICATION_VSEVT_CODE 0x0C0EU
+
+/* ACI_GATT_NOTIFICATION_EVENT code */
+#define ACI_GATT_NOTIFICATION_VSEVT_CODE 0x0C0FU
+
+/* ACI_GATT_PROC_COMPLETE_EVENT code */
+#define ACI_GATT_PROC_COMPLETE_VSEVT_CODE 0x0C10U
+
+/* ACI_GATT_ERROR_RESP_EVENT code */
+#define ACI_GATT_ERROR_RESP_VSEVT_CODE 0x0C11U
+
+/* ACI_GATT_DISC_READ_CHAR_BY_UUID_RESP_EVENT code */
+#define ACI_GATT_DISC_READ_CHAR_BY_UUID_RESP_VSEVT_CODE 0x0C12U
+
+/* ACI_GATT_WRITE_PERMIT_REQ_EVENT code */
+#define ACI_GATT_WRITE_PERMIT_REQ_VSEVT_CODE 0x0C13U
+
+/* ACI_GATT_READ_PERMIT_REQ_EVENT code */
+#define ACI_GATT_READ_PERMIT_REQ_VSEVT_CODE 0x0C14U
+
+/* ACI_GATT_READ_MULTI_PERMIT_REQ_EVENT code */
+#define ACI_GATT_READ_MULTI_PERMIT_REQ_VSEVT_CODE 0x0C15U
+
+/* ACI_GATT_TX_POOL_AVAILABLE_EVENT code */
+#define ACI_GATT_TX_POOL_AVAILABLE_VSEVT_CODE 0x0C16U
+
+/* ACI_GATT_SERVER_CONFIRMATION_EVENT code */
+#define ACI_GATT_SERVER_CONFIRMATION_VSEVT_CODE 0x0C17U
+
+/* ACI_GATT_PREPARE_WRITE_PERMIT_REQ_EVENT code */
+#define ACI_GATT_PREPARE_WRITE_PERMIT_REQ_VSEVT_CODE 0x0C18U
+
+/* ACI_GATT_EATT_BEARER_EVENT code */
+#define ACI_GATT_EATT_BEARER_VSEVT_CODE 0x0C19U
+
+/* ACI_GATT_MULT_NOTIFICATION_EVENT code */
+#define ACI_GATT_MULT_NOTIFICATION_VSEVT_CODE 0x0C1AU
+
+/* ACI_GATT_NOTIFICATION_COMPLETE_EVENT code */
+#define ACI_GATT_NOTIFICATION_COMPLETE_VSEVT_CODE 0x0C1BU
+
+/* ACI_GATT_READ_EXT_EVENT code */
+#define ACI_GATT_READ_EXT_VSEVT_CODE 0x0C1DU
+
+/* ACI_GATT_INDICATION_EXT_EVENT code */
+#define ACI_GATT_INDICATION_EXT_VSEVT_CODE 0x0C1EU
+
+/* ACI_GATT_NOTIFICATION_EXT_EVENT code */
+#define ACI_GATT_NOTIFICATION_EXT_VSEVT_CODE 0x0C1FU
+
+/* Vendor specific codes of ACI L2CAP events
+ */
+
+/* ACI_L2CAP_CONNECTION_UPDATE_RESP_EVENT code */
+#define ACI_L2CAP_CONNECTION_UPDATE_RESP_VSEVT_CODE 0x0800U
+
+/* ACI_L2CAP_PROC_TIMEOUT_EVENT code */
+#define ACI_L2CAP_PROC_TIMEOUT_VSEVT_CODE 0x0801U
+
+/* ACI_L2CAP_CONNECTION_UPDATE_REQ_EVENT code */
+#define ACI_L2CAP_CONNECTION_UPDATE_REQ_VSEVT_CODE 0x0802U
+
+/* ACI_L2CAP_COMMAND_REJECT_EVENT code */
+#define ACI_L2CAP_COMMAND_REJECT_VSEVT_CODE 0x080AU
+
+/* ACI_L2CAP_COC_CONNECT_EVENT code */
+#define ACI_L2CAP_COC_CONNECT_VSEVT_CODE 0x0810U
+
+/* ACI_L2CAP_COC_CONNECT_CONFIRM_EVENT code */
+#define ACI_L2CAP_COC_CONNECT_CONFIRM_VSEVT_CODE 0x0811U
+
+/* ACI_L2CAP_COC_RECONF_EVENT code */
+#define ACI_L2CAP_COC_RECONF_VSEVT_CODE 0x0812U
+
+/* ACI_L2CAP_COC_RECONF_CONFIRM_EVENT code */
+#define ACI_L2CAP_COC_RECONF_CONFIRM_VSEVT_CODE 0x0813U
+
+/* ACI_L2CAP_COC_DISCONNECT_EVENT code */
+#define ACI_L2CAP_COC_DISCONNECT_VSEVT_CODE 0x0814U
+
+/* ACI_L2CAP_COC_FLOW_CONTROL_EVENT code */
+#define ACI_L2CAP_COC_FLOW_CONTROL_VSEVT_CODE 0x0815U
+
+/* ACI_L2CAP_COC_RX_DATA_EVENT code */
+#define ACI_L2CAP_COC_RX_DATA_VSEVT_CODE 0x0816U
+
+/* ACI_L2CAP_COC_TX_POOL_AVAILABLE_EVENT code */
+#define ACI_L2CAP_COC_TX_POOL_AVAILABLE_VSEVT_CODE 0x0817U
+
+/* Vendor specific codes of ACI HAL events
+ */
+
+/* ACI_HAL_END_OF_RADIO_ACTIVITY_EVENT code */
+#define ACI_HAL_END_OF_RADIO_ACTIVITY_VSEVT_CODE 0x0004U
+
+/* ACI_HAL_SCAN_REQ_REPORT_EVENT code */
+#define ACI_HAL_SCAN_REQ_REPORT_VSEVT_CODE 0x0005U
+
+/* ACI_HAL_FW_ERROR_EVENT code */
+#define ACI_HAL_FW_ERROR_VSEVT_CODE 0x0006U
+
+#endif /* BLE_VS_CODES_H__ */

+ 185 - 0
seos/headers/stm32_wpan_common.h

@@ -0,0 +1,185 @@
+/**
+ ******************************************************************************
+ * @file    stm32_wpan_common.h
+ * @author  MCD Application Team
+ * @brief   Common file to utilities
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2018-2021 STMicroelectronics.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+/* Define to prevent recursive inclusion -------------------------------------*/
+#ifndef __STM32_WPAN_COMMON_H
+#define __STM32_WPAN_COMMON_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#if defined(__CC_ARM) || defined(__ARMCC_VERSION)
+#define __ASM           __asm /*!< asm keyword for ARM Compiler          */
+#define __INLINE        __inline /*!< inline keyword for ARM Compiler       */
+#define __STATIC_INLINE static __inline
+#elif defined(__ICCARM__)
+#define __ASM __asm /*!< asm keyword for IAR Compiler          */
+#define __INLINE \
+    inline /*!< inline keyword for IAR Compiler. Only available in High optimization mode! */
+#define __STATIC_INLINE static inline
+#elif defined(__GNUC__)
+#define __ASM           __asm /*!< asm keyword for GNU Compiler          */
+#define __INLINE        inline /*!< inline keyword for GNU Compiler       */
+#define __STATIC_INLINE static inline
+#endif
+
+#include <stdint.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include "cmsis_compiler.h"
+
+/* -------------------------------- *
+   *  Basic definitions               *
+   * -------------------------------- */
+
+#undef NULL
+#define NULL 0U
+
+#undef FALSE
+#define FALSE 0U
+
+#undef TRUE
+#define TRUE (!0U)
+
+/* -------------------------------- *
+   *  Critical Section definition     *
+   * -------------------------------- */
+#undef BACKUP_PRIMASK
+#define BACKUP_PRIMASK() uint32_t primask_bit = __get_PRIMASK()
+
+#undef DISABLE_IRQ
+#define DISABLE_IRQ() __disable_irq()
+
+#undef RESTORE_PRIMASK
+#define RESTORE_PRIMASK() __set_PRIMASK(primask_bit)
+
+/* -------------------------------- *
+   *  Macro delimiters                *
+   * -------------------------------- */
+#undef M_BEGIN
+#define M_BEGIN do {
+#undef M_END
+#define M_END \
+    }         \
+    while(0)
+
+/* -------------------------------- *
+   *  Some useful macro definitions   *
+   * -------------------------------- */
+#undef MAX
+#define MAX(a, b) (((a) > (b)) ? (a) : (b))
+
+#undef MIN
+#define MIN(a, b) (((a) < (b)) ? (a) : (b))
+
+#undef MODINC
+#define MODINC(a, m)        \
+    M_BEGIN(a)++;           \
+    if((a) >= (m)) (a) = 0; \
+    M_END
+
+#undef MODDEC
+#define MODDEC(a, m)               \
+    M_BEGIN if((a) == 0)(a) = (m); \
+    (a)--;                         \
+    M_END
+
+#undef MODADD
+#define MODADD(a, b, m)        \
+    M_BEGIN(a) += (b);         \
+    if((a) >= (m)) (a) -= (m); \
+    M_END
+
+#undef MODSUB
+#define MODSUB(a, b, m) MODADD(a, (m) - (b), m)
+
+#undef ALIGN
+#ifdef WIN32
+#define ALIGN(n)
+#else
+#define ALIGN(n) __attribute__((aligned(n)))
+#endif
+
+#undef PAUSE
+#define PAUSE(t)              \
+    M_BEGIN                   \
+    volatile int _i;          \
+    for(_i = t; _i > 0; _i--) \
+        ;                     \
+    M_END
+#undef DIVF
+#define DIVF(x, y) ((x) / (y))
+
+#undef DIVC
+#define DIVC(x, y) (((x) + (y) - 1) / (y))
+
+#undef DIVR
+#define DIVR(x, y) (((x) + ((y) / 2)) / (y))
+
+#undef SHRR
+#define SHRR(x, n) ((((x) >> ((n) - 1)) + 1) >> 1)
+
+#undef BITN
+#define BITN(w, n) (((w)[(n) / 32] >> ((n) % 32)) & 1)
+
+#undef BITNSET
+#define BITNSET(w, n, b)                              \
+    M_BEGIN(w)[(n) / 32] |= ((U32)(b)) << ((n) % 32); \
+    M_END
+
+/* -------------------------------- *
+ *  Section attribute               *
+ * -------------------------------- */
+#undef PLACE_IN_SECTION
+#define PLACE_IN_SECTION(__x__) __attribute__((section(__x__)))
+
+/* ----------------------------------- *
+ *  Packed usage (compiler dependent)  *
+ * ----------------------------------- */
+#undef PACKED__
+#undef PACKED_STRUCT
+
+#if defined(__CC_ARM)
+#if defined(__GNUC__)
+/* GNU extension */
+#define PACKED__      __attribute__((packed))
+#define PACKED_STRUCT struct PACKED__
+#elif defined(__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050U)
+#define PACKED__      __attribute__((packed))
+#define PACKED_STRUCT struct PACKED__
+#else
+#define PACKED__(TYPE) __packed TYPE
+#define PACKED_STRUCT  PACKED__(struct)
+#endif
+#elif defined(__GNUC__)
+#define PACKED__      __attribute__((packed))
+#define PACKED_STRUCT struct PACKED__
+#elif defined(__ICCARM__)
+#define PACKED_STRUCT __packed struct
+#else
+#define PACKED_STRUCT __packed struct
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /*__STM32_WPAN_COMMON_H */

+ 360 - 0
seos/headers/tl.h

@@ -0,0 +1,360 @@
+/**
+ ******************************************************************************
+ * @file    tl.h
+ * @author  MCD Application Team
+ * @brief   Header for tl module
+ ******************************************************************************
+ * @attention
+ *
+ * Copyright (c) 2018-2021 STMicroelectronics.
+ * All rights reserved.
+ *
+ * This software is licensed under terms that can be found in the LICENSE file
+ * in the root directory of this software component.
+ * If no LICENSE file comes with this software, it is provided AS-IS.
+ *
+ ******************************************************************************
+ */
+
+/* Define to prevent recursive inclusion -------------------------------------*/
+#ifndef __TL_H
+#define __TL_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Includes ------------------------------------------------------------------*/
+#include "stm32_wpan_common.h"
+
+/* Exported defines -----------------------------------------------------------*/
+#define TL_BLECMD_PKT_TYPE     (0x01)
+#define TL_ACL_DATA_PKT_TYPE   (0x02)
+#define TL_BLEEVT_PKT_TYPE     (0x04)
+#define TL_OTCMD_PKT_TYPE      (0x08)
+#define TL_OTRSP_PKT_TYPE      (0x09)
+#define TL_CLICMD_PKT_TYPE     (0x0A)
+#define TL_OTNOT_PKT_TYPE      (0x0C)
+#define TL_OTACK_PKT_TYPE      (0x0D)
+#define TL_CLINOT_PKT_TYPE     (0x0E)
+#define TL_CLIACK_PKT_TYPE     (0x0F)
+#define TL_SYSCMD_PKT_TYPE     (0x10)
+#define TL_SYSRSP_PKT_TYPE     (0x11)
+#define TL_SYSEVT_PKT_TYPE     (0x12)
+#define TL_CLIRESP_PKT_TYPE    (0x15)
+#define TL_M0CMD_PKT_TYPE      (0x16)
+#define TL_LOCCMD_PKT_TYPE     (0x20)
+#define TL_LOCRSP_PKT_TYPE     (0x21)
+#define TL_TRACES_APP_PKT_TYPE (0x40)
+#define TL_TRACES_WL_PKT_TYPE  (0x41)
+
+#define TL_CMD_HDR_SIZE        (4)
+#define TL_EVT_HDR_SIZE        (3)
+#define TL_EVT_CS_PAYLOAD_SIZE (4)
+
+#define TL_BLEEVT_CC_OPCODE (0x0E)
+#define TL_BLEEVT_CS_OPCODE (0x0F)
+#define TL_BLEEVT_VS_OPCODE (0xFF)
+
+#define TL_BLEEVT_CC_PACKET_SIZE (TL_EVT_HDR_SIZE + sizeof(TL_CcEvt_t))
+#define TL_BLEEVT_CC_BUFFER_SIZE (sizeof(TL_PacketHeader_t) + TL_BLEEVT_CC_PACKET_SIZE)
+/* Exported types ------------------------------------------------------------*/
+/**< Packet header */
+typedef PACKED_STRUCT {
+    uint32_t* next;
+    uint32_t* prev;
+}
+TL_PacketHeader_t;
+
+/*******************************************************************************
+ * Event type
+ */
+
+/**
+ * This the payload of TL_Evt_t for a command status event
+ */
+typedef PACKED_STRUCT {
+    uint8_t status;
+    uint8_t numcmd;
+    uint16_t cmdcode;
+}
+TL_CsEvt_t;
+
+/**
+ * This the payload of TL_Evt_t for a command complete event, only used a pointer
+ */
+typedef PACKED_STRUCT {
+    uint8_t numcmd;
+    uint16_t cmdcode;
+    uint8_t payload[2];
+}
+TL_CcEvt_t;
+
+/**
+ * This the payload of TL_Evt_t for an asynchronous event, only used a pointer
+ */
+typedef PACKED_STRUCT {
+    uint16_t subevtcode;
+    uint8_t payload[2];
+}
+TL_AsynchEvt_t;
+
+/**
+ * This the payload of TL_Evt_t, only used a pointer
+ */
+typedef PACKED_STRUCT {
+    uint8_t evtcode;
+    uint8_t plen;
+    uint8_t payload[2];
+}
+TL_Evt_t;
+
+typedef PACKED_STRUCT {
+    uint8_t type;
+    TL_Evt_t evt;
+}
+TL_EvtSerial_t;
+
+/**
+ * This format shall be used for all events (asynchronous and command response) reported
+ * by the CPU2 except for the command response of a system command where the header is not there
+ * and the format to be used shall be TL_EvtSerial_t.
+ * Note: Be careful that the asynchronous events reported by the CPU2 on the system channel do
+ * include the header and shall use TL_EvtPacket_t format. Only the command response format on the
+ * system channel is different.
+ */
+typedef PACKED_STRUCT {
+    TL_PacketHeader_t header;
+    TL_EvtSerial_t evtserial;
+}
+TL_EvtPacket_t;
+
+/*****************************************************************************************
+ * Command type
+ */
+
+typedef PACKED_STRUCT {
+    uint16_t cmdcode;
+    uint8_t plen;
+    uint8_t payload[255];
+}
+TL_Cmd_t;
+
+typedef PACKED_STRUCT {
+    uint8_t type;
+    TL_Cmd_t cmd;
+}
+TL_CmdSerial_t;
+
+typedef PACKED_STRUCT {
+    TL_PacketHeader_t header;
+    TL_CmdSerial_t cmdserial;
+}
+TL_CmdPacket_t;
+
+/*****************************************************************************************
+ * HCI ACL DATA type
+ */
+typedef PACKED_STRUCT {
+    uint8_t type;
+    uint16_t handle;
+    uint16_t length;
+    uint8_t acl_data[1];
+}
+TL_AclDataSerial_t;
+
+typedef PACKED_STRUCT {
+    TL_PacketHeader_t header;
+    TL_AclDataSerial_t AclDataSerial;
+}
+TL_AclDataPacket_t;
+
+typedef struct {
+    uint8_t* p_BleSpareEvtBuffer;
+    uint8_t* p_SystemSpareEvtBuffer;
+    uint8_t* p_AsynchEvtPool;
+    uint32_t AsynchEvtPoolSize;
+    uint8_t* p_TracesEvtPool;
+    uint32_t TracesEvtPoolSize;
+} TL_MM_Config_t;
+
+typedef struct {
+    uint8_t* p_ThreadOtCmdRspBuffer;
+    uint8_t* p_ThreadCliRspBuffer;
+    uint8_t* p_ThreadNotAckBuffer;
+    uint8_t* p_ThreadCliNotBuffer;
+} TL_TH_Config_t;
+
+typedef struct {
+    uint8_t* p_LldTestsCliCmdRspBuffer;
+    uint8_t* p_LldTestsM0CmdBuffer;
+} TL_LLD_tests_Config_t;
+
+typedef struct {
+    uint8_t* p_BleLldCmdRspBuffer;
+    uint8_t* p_BleLldM0CmdBuffer;
+} TL_BLE_LLD_Config_t;
+
+typedef struct {
+    uint8_t* p_Mac_802_15_4_CmdRspBuffer;
+    uint8_t* p_Mac_802_15_4_NotAckBuffer;
+} TL_MAC_802_15_4_Config_t;
+
+typedef struct {
+    uint8_t* p_ZigbeeOtCmdRspBuffer;
+    uint8_t* p_ZigbeeNotAckBuffer;
+    uint8_t* p_ZigbeeNotifRequestBuffer;
+} TL_ZIGBEE_Config_t;
+
+/**
+ * @brief Contain the BLE HCI Init Configuration
+ * @{
+ */
+typedef struct {
+    void (*IoBusEvtCallBack)(TL_EvtPacket_t* phcievt);
+    void (*IoBusAclDataTxAck)(void);
+    uint8_t* p_cmdbuffer;
+    uint8_t* p_AclDataBuffer;
+} TL_BLE_InitConf_t;
+
+/**
+ * @brief Contain the SYSTEM HCI Init Configuration
+ * @{
+ */
+typedef struct {
+    void (*IoBusCallBackCmdEvt)(TL_EvtPacket_t* phcievt);
+    void (*IoBusCallBackUserEvt)(TL_EvtPacket_t* phcievt);
+    uint8_t* p_cmdbuffer;
+} TL_SYS_InitConf_t;
+
+/*****************************************************************************************
+ * Event type copied from ble_legacy.h 
+ */
+
+typedef PACKED_STRUCT {
+    uint8_t type;
+    uint8_t data[1];
+}
+hci_uart_pckt;
+
+typedef PACKED_STRUCT {
+    uint8_t evt;
+    uint8_t plen;
+    uint8_t data[1];
+}
+hci_event_pckt;
+
+typedef PACKED_STRUCT {
+    uint8_t subevent;
+    uint8_t data[1];
+}
+evt_le_meta_event;
+
+/**
+ * Vendor specific event for BLE core.
+ */
+typedef PACKED_STRUCT {
+    uint16_t ecode; /**< One of the BLE core event codes. */
+    uint8_t data[1];
+}
+evt_blecore_aci;
+
+/* Bluetooth 48 bit address (in little-endian order).
+ */
+typedef uint8_t tBDAddr[6];
+
+/* Exported constants --------------------------------------------------------*/
+/* External variables --------------------------------------------------------*/
+/* Exported macros -----------------------------------------------------------*/
+/* Exported functions ------------------------------------------------------- */
+
+/******************************************************************************
+ * GENERAL
+ ******************************************************************************/
+void TL_Enable(void);
+void TL_Init(void);
+
+/******************************************************************************
+ * BLE
+ ******************************************************************************/
+int32_t TL_BLE_Init(void* pConf);
+int32_t TL_BLE_SendCmd(uint8_t* buffer, uint16_t size);
+int32_t TL_BLE_SendAclData(uint8_t* buffer, uint16_t size);
+
+/******************************************************************************
+ * SYSTEM
+ ******************************************************************************/
+int32_t TL_SYS_Init(void* pConf);
+int32_t TL_SYS_SendCmd(uint8_t* buffer, uint16_t size);
+
+/******************************************************************************
+ * THREAD
+ ******************************************************************************/
+void TL_THREAD_Init(TL_TH_Config_t* p_Config);
+void TL_OT_SendCmd(void);
+void TL_CLI_SendCmd(void);
+void TL_OT_CmdEvtReceived(TL_EvtPacket_t* Otbuffer);
+void TL_THREAD_NotReceived(TL_EvtPacket_t* Notbuffer);
+void TL_THREAD_SendAck(void);
+void TL_THREAD_CliSendAck(void);
+void TL_THREAD_CliNotReceived(TL_EvtPacket_t* Notbuffer);
+
+/******************************************************************************
+ * LLD TESTS
+ ******************************************************************************/
+void TL_LLDTESTS_Init(TL_LLD_tests_Config_t* p_Config);
+void TL_LLDTESTS_SendCliCmd(void);
+void TL_LLDTESTS_ReceiveCliRsp(TL_CmdPacket_t* Notbuffer);
+void TL_LLDTESTS_SendCliRspAck(void);
+void TL_LLDTESTS_ReceiveM0Cmd(TL_CmdPacket_t* Notbuffer);
+void TL_LLDTESTS_SendM0CmdAck(void);
+
+/******************************************************************************
+ * BLE LLD
+ ******************************************************************************/
+void TL_BLE_LLD_Init(TL_BLE_LLD_Config_t* p_Config);
+void TL_BLE_LLD_SendCliCmd(void);
+void TL_BLE_LLD_ReceiveCliRsp(TL_CmdPacket_t* Notbuffer);
+void TL_BLE_LLD_SendCliRspAck(void);
+void TL_BLE_LLD_ReceiveM0Cmd(TL_CmdPacket_t* Notbuffer);
+void TL_BLE_LLD_SendM0CmdAck(void);
+void TL_BLE_LLD_SendCmd(void);
+void TL_BLE_LLD_ReceiveRsp(TL_CmdPacket_t* Notbuffer);
+void TL_BLE_LLD_SendRspAck(void);
+/******************************************************************************
+ * MEMORY MANAGER
+ ******************************************************************************/
+void TL_MM_Init(TL_MM_Config_t* p_Config);
+void TL_MM_EvtDone(TL_EvtPacket_t* hcievt);
+
+/******************************************************************************
+ * TRACES
+ ******************************************************************************/
+void TL_TRACES_Init(void);
+void TL_TRACES_EvtReceived(TL_EvtPacket_t* hcievt);
+
+/******************************************************************************
+ * MAC 802.15.4
+ ******************************************************************************/
+void TL_MAC_802_15_4_Init(TL_MAC_802_15_4_Config_t* p_Config);
+void TL_MAC_802_15_4_SendCmd(void);
+void TL_MAC_802_15_4_CmdEvtReceived(TL_EvtPacket_t* Otbuffer);
+void TL_MAC_802_15_4_NotReceived(TL_EvtPacket_t* Notbuffer);
+void TL_MAC_802_15_4_SendAck(void);
+
+/******************************************************************************
+ * ZIGBEE
+ ******************************************************************************/
+void TL_ZIGBEE_Init(TL_ZIGBEE_Config_t* p_Config);
+void TL_ZIGBEE_SendM4RequestToM0(void);
+void TL_ZIGBEE_SendM4AckToM0Notify(void);
+void TL_ZIGBEE_NotReceived(TL_EvtPacket_t* Notbuffer);
+void TL_ZIGBEE_CmdEvtReceived(TL_EvtPacket_t* Otbuffer);
+void TL_ZIGBEE_M0RequestReceived(TL_EvtPacket_t* Otbuffer);
+void TL_ZIGBEE_SendM4AckToM0Request(void);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /*__TL_H */

+ 0 - 0
seos/images/.gitkeep


binární
seos/images/DolphinMafia_115x62.png


binární
seos/images/DolphinNice_96x59.png


binární
seos/images/Nfc_10px.png


binární
seos/images/RFIDDolphinReceive_97x61.png


binární
seos/images/RFIDDolphinSend_97x61.png


+ 7 - 0
seos/keys-example.txt

@@ -0,0 +1,7 @@
+Filetype: Seos keys
+Version: 1
+SEOS_ADF_OID_LEN: 17
+SEOS_ADF_OID: 2b 06 01 04 01 81 e4 38 01 01 02 01 18 01 01 02 02
+SEOS_ADF1_PRIV_ENC: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+SEOS_ADF1_PRIV_MAC: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+SEOS_ADF1_READ: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

+ 28 - 0
seos/keys.c

@@ -0,0 +1,28 @@
+#include <stdint.h>
+#include <stddef.h>
+
+size_t SEOS_ADF_OID_LEN = 17;
+uint8_t SEOS_ADF_OID[] = {
+    0x2b,
+    0x06,
+    0x01,
+    0x04,
+    0x01,
+    0x81,
+    0xe4,
+    0x38,
+    0x01,
+    0x01,
+    0x02,
+    0x01,
+    0x18,
+    0x01,
+    0x01,
+    0x02,
+    0x02};
+uint8_t SEOS_ADF1_PRIV_ENC[] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+uint8_t SEOS_ADF1_PRIV_MAC[] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+uint8_t SEOS_ADF1_READ[] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

+ 7 - 0
seos/keys.h

@@ -0,0 +1,7 @@
+#pragma once
+
+extern size_t SEOS_ADF_OID_LEN;
+extern uint8_t SEOS_ADF_OID[32];
+extern uint8_t SEOS_ADF1_PRIV_ENC[16];
+extern uint8_t SEOS_ADF1_PRIV_MAC[16];
+extern uint8_t SEOS_ADF1_READ[16];

+ 60 - 0
seos/memmem.c

@@ -0,0 +1,60 @@
+/* Copyright (C) 1991-2025 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 2, or (at your option)
+   any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License along
+   with this program; if not, write to the Free Software Foundation,
+   Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.  */
+
+/*
+
+@deftypefn Supplemental void* memmem (const void *@var{haystack}, @
+  size_t @var{haystack_len} const void *@var{needle}, size_t @var{needle_len})
+
+Returns a pointer to the first occurrence of @var{needle} (length
+@var{needle_len}) in @var{haystack} (length @var{haystack_len}).
+Returns @code{NULL} if not found.
+
+@end deftypefn
+
+*/
+
+#include <stddef.h>
+#include <string.h>
+
+#ifndef _LIBC
+#define __builtin_expect(expr, val) (expr)
+#endif
+
+#undef memmem
+
+/* Return the first occurrence of NEEDLE in HAYSTACK.  */
+void* memmem(const void* haystack, size_t haystack_len, const void* needle, size_t needle_len) {
+    const char* begin;
+    const char* const last_possible = (const char*)haystack + haystack_len - needle_len;
+
+    if(needle_len == 0)
+        /* The first occurrence of the empty string is deemed to occur at
+       the beginning of the string.  */
+        return (void*)haystack;
+
+    /* Sanity check, otherwise the loop might search through the whole
+     memory.  */
+    if(__builtin_expect(haystack_len < needle_len, 0)) return NULL;
+
+    for(begin = (const char*)haystack; begin <= last_possible; ++begin)
+        if(begin[0] == ((const char*)needle)[0] &&
+           !memcmp((const void*)&begin[1], (const void*)((const char*)needle + 1), needle_len - 1))
+            return (void*)begin;
+
+    return NULL;
+}

+ 30 - 0
seos/scenes/seos_scene.c

@@ -0,0 +1,30 @@
+#include "seos_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const seos_on_enter_handlers[])(void*) = {
+#include "seos_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const seos_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "seos_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const seos_on_exit_handlers[])(void* context) = {
+#include "seos_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers seos_scene_handlers = {
+    .on_enter_handlers = seos_on_enter_handlers,
+    .on_event_handlers = seos_on_event_handlers,
+    .on_exit_handlers = seos_on_exit_handlers,
+    .scene_num = SeosSceneNum,
+};

+ 29 - 0
seos/scenes/seos_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) SeosScene##id,
+typedef enum {
+#include "seos_scene_config.h"
+    SeosSceneNum,
+} SeosScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers seos_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "seos_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "seos_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "seos_scene_config.h"
+#undef ADD_SCENE

+ 45 - 0
seos/scenes/seos_scene_about.c

@@ -0,0 +1,45 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+static char* about =
+    "This software incorporates a third-party implementation of Seos™ technology. It is not developed, authorized, licensed, or endorsed by HID Global, ASSA ABLOY, or any of their affiliates. References to Seos™ are solely for descriptive and compatibility purposes.\nNo guarantee of compatibility or functionality is made. This implementation may not work with all Seos™-enabled systems, and its performance, security, and reliability are not assured. Users assume all risks associated with its use.\nSeos™, HID Global, and ASSA ABLOY are trademarks or registered trademarks of their respective owners. This software is not associated with or sponsored by them in any way.";
+
+void seos_scene_about_widget_callback(GuiButtonType result, InputType type, void* context) {
+    Seos* seos = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, result);
+    }
+}
+
+void seos_scene_about_on_enter(void* context) {
+    Seos* seos = context;
+
+    furi_string_reset(seos->text_box_store);
+    FuriString* str = seos->text_box_store;
+    furi_string_cat_printf(str, "%s\n", about);
+
+    text_box_set_font(seos->text_box, TextBoxFontText);
+    text_box_set_text(seos->text_box, furi_string_get_cstr(seos->text_box_store));
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewTextBox);
+}
+
+bool seos_scene_about_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(seos->scene_manager);
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_previous_scene(seos->scene_manager);
+    }
+    return consumed;
+}
+
+void seos_scene_about_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear views
+    text_box_reset(seos->text_box);
+}

+ 85 - 0
seos/scenes/seos_scene_ble_central.c

@@ -0,0 +1,85 @@
+#include "../seos_i.h"
+#include "../seos_central.h"
+#include "../seos_common.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneBleCentral"
+
+void seos_scene_ble_central_on_enter(void* context) {
+    Seos* seos = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    // Setup view
+    Popup* popup = seos->popup;
+    popup_set_header(popup, "Starting...", 68, 20, AlignLeft, AlignTop);
+    // popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+    seos->seos_central = seos_central_alloc(seos);
+    seos_central_start(seos->seos_central, seos->flow_mode);
+
+    seos_blink_start(seos);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_ble_central_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    Popup* popup = seos->popup;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventReaderSuccess) {
+            notification_message(seos->notifications, &sequence_success);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadSuccess);
+            consumed = true;
+        } else if(event.event == SeosCustomEventReaderError) {
+            notification_message(seos->notifications, &sequence_error);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadError);
+            consumed = true;
+        } else if(event.event == SeosCustomEventHCIInit) {
+            popup_set_header(popup, "Init", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventScan) {
+            popup_set_header(popup, "Scanning...", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventFound) {
+            popup_set_header(popup, "Device\nfound", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventConnected) {
+            popup_set_header(popup, "Connected", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventAuthenticated) {
+            popup_set_header(popup, "Auth'd", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventSIORequested) {
+            popup_set_header(popup, "SIO\nRequested", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        if(seos->credential.sio_len > 0) {
+            scene_manager_search_and_switch_to_previous_scene(
+                seos->scene_manager, SeosSceneSavedMenu);
+        } else {
+            scene_manager_previous_scene(seos->scene_manager);
+        }
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void seos_scene_ble_central_on_exit(void* context) {
+    Seos* seos = context;
+
+    if(seos->seos_central) {
+        FURI_LOG_D(TAG, "Cleanup");
+        seos_central_stop(seos->seos_central);
+        seos_central_free(seos->seos_central);
+        seos->seos_central = NULL;
+    }
+
+    // Clear view
+    popup_reset(seos->popup);
+
+    seos_blink_stop(seos);
+}

+ 92 - 0
seos/scenes/seos_scene_ble_peripheral.c

@@ -0,0 +1,92 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneBleReader"
+
+void seos_scene_ble_peripheral_on_enter(void* context) {
+    Seos* seos = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    // Setup view
+    Popup* popup = seos->popup;
+    popup_set_header(popup, "Starting", 68, 30, AlignLeft, AlignTop);
+    if(seos->flow_mode == FLOW_READER) {
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+    } else if(seos->flow_mode == FLOW_CRED) {
+        popup_set_icon(popup, 0, 3, &I_RFIDDolphinSend_97x61);
+    }
+
+    if(seos->has_external_ble) {
+        seos->seos_characteristic = seos_characteristic_alloc(seos);
+        seos_characteristic_start(seos->seos_characteristic, seos->flow_mode);
+    } else {
+        seos->native_peripheral = seos_native_peripheral_alloc(seos);
+        seos_native_peripheral_start(seos->native_peripheral, seos->flow_mode);
+    }
+
+    seos_blink_start(seos);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_ble_peripheral_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    Popup* popup = seos->popup;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventReaderSuccess) {
+            notification_message(seos->notifications, &sequence_success);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadSuccess);
+            consumed = true;
+        } else if(event.event == SeosCustomEventReaderError) {
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadError);
+            consumed = true;
+        } else if(event.event == SeosCustomEventHCIInit) {
+            popup_set_header(popup, "Init", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventAdvertising) {
+            popup_set_header(popup, "Advertising", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventConnected) {
+            popup_set_header(popup, "Connected", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventAuthenticated) {
+            popup_set_header(popup, "Auth'd", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventSIORequested) {
+            popup_set_header(popup, "SIO\nRequested", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        if(seos->credential.sio_len > 0) {
+            scene_manager_search_and_switch_to_previous_scene(
+                seos->scene_manager, SeosSceneSavedMenu);
+        } else {
+            scene_manager_previous_scene(seos->scene_manager);
+        }
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void seos_scene_ble_peripheral_on_exit(void* context) {
+    Seos* seos = context;
+
+    if(seos->seos_characteristic) {
+        seos_characteristic_stop(seos->seos_characteristic);
+        seos_characteristic_free(seos->seos_characteristic);
+        seos->seos_characteristic = NULL;
+    }
+    if(seos->native_peripheral) {
+        seos_native_peripheral_stop(seos->native_peripheral);
+        seos_native_peripheral_free(seos->native_peripheral);
+        seos->native_peripheral = NULL;
+    }
+
+    // Clear view
+    popup_reset(seos->popup);
+
+    seos_blink_stop(seos);
+}

+ 17 - 0
seos/scenes/seos_scene_config.h

@@ -0,0 +1,17 @@
+ADD_SCENE(seos, main_menu, MainMenu)
+ADD_SCENE(seos, emulate, Emulate)
+ADD_SCENE(seos, read, Read)
+ADD_SCENE(seos, file_select, FileSelect)
+ADD_SCENE(seos, saved_menu, SavedMenu)
+ADD_SCENE(seos, read_success, ReadSuccess)
+ADD_SCENE(seos, save_name, SaveName)
+ADD_SCENE(seos, save_success, SaveSuccess)
+ADD_SCENE(seos, about, About)
+ADD_SCENE(seos, delete, Delete)
+ADD_SCENE(seos, delete_success, DeleteSuccess)
+ADD_SCENE(seos, read_error, ReadError)
+ADD_SCENE(seos, info, Info)
+ADD_SCENE(seos, ble_peripheral, BlePeripheral)
+ADD_SCENE(seos, ble_central, BleCentral)
+ADD_SCENE(seos, scanner_menu, ScannerMenu)
+ADD_SCENE(seos, zero_keys, ZeroKeys)

+ 52 - 0
seos/scenes/seos_scene_delete.c

@@ -0,0 +1,52 @@
+#include "../seos_i.h"
+
+void seos_scene_delete_widget_callback(GuiButtonType result, InputType type, void* context) {
+    Seos* seos = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, result);
+    }
+}
+
+void seos_scene_delete_on_enter(void* context) {
+    Seos* seos = context;
+    SeosEmulator* seos_emulator = seos->seos_emulator;
+
+    // Setup Custom Widget view
+    char temp_str[141];
+    snprintf(temp_str, sizeof(temp_str), "\e#Delete %s?\e#", seos_emulator->name);
+    widget_add_text_box_element(
+        seos->widget, 0, 0, 128, 23, AlignCenter, AlignCenter, temp_str, false);
+    widget_add_button_element(
+        seos->widget, GuiButtonTypeLeft, "Back", seos_scene_delete_widget_callback, seos);
+    widget_add_button_element(
+        seos->widget, GuiButtonTypeRight, "Delete", seos_scene_delete_widget_callback, seos);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewWidget);
+}
+
+bool seos_scene_delete_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    SeosEmulator* seos_emulator = seos->seos_emulator;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            return scene_manager_previous_scene(seos->scene_manager);
+        } else if(event.event == GuiButtonTypeRight) {
+            if(seos_emulator_delete(seos_emulator, true)) {
+                scene_manager_next_scene(seos->scene_manager, SeosSceneDeleteSuccess);
+            } else {
+                scene_manager_search_and_switch_to_previous_scene(
+                    seos->scene_manager, SeosSceneMainMenu);
+            }
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void seos_scene_delete_on_exit(void* context) {
+    Seos* seos = context;
+
+    widget_reset(seos->widget);
+}

+ 40 - 0
seos/scenes/seos_scene_delete_success.c

@@ -0,0 +1,40 @@
+#include "../seos_i.h"
+
+void seos_scene_delete_success_popup_callback(void* context) {
+    Seos* seos = context;
+    view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventViewExit);
+}
+
+void seos_scene_delete_success_on_enter(void* context) {
+    Seos* seos = context;
+
+    // Setup view
+    Popup* popup = seos->popup;
+    popup_set_icon(popup, 0, 2, &I_DolphinMafia_115x62);
+    popup_set_header(popup, "Deleted", 83, 19, AlignLeft, AlignBottom);
+    popup_set_timeout(popup, 1500);
+    popup_set_context(popup, seos);
+    popup_set_callback(popup, seos_scene_delete_success_popup_callback);
+    popup_enable_timeout(popup);
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_delete_success_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventViewExit) {
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                seos->scene_manager, SeosSceneMainMenu);
+        }
+    }
+    return consumed;
+}
+
+void seos_scene_delete_success_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    popup_reset(seos->popup);
+}

+ 65 - 0
seos/scenes/seos_scene_emulate.c

@@ -0,0 +1,65 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneEmulate"
+
+void seos_scene_emulate_on_enter(void* context) {
+    Seos* seos = context;
+    dolphin_deed(DolphinDeedNfcEmulate);
+
+    // Setup view
+    Popup* popup = seos->popup;
+    popup_set_header(popup, "Emulating", 68, 30, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_RFIDDolphinSend_97x61);
+
+    nfc_device_load(seos->nfc_device, APP_ASSETS_PATH("seos.nfc"));
+
+    const Iso14443_4aData* data = nfc_device_get_data(seos->nfc_device, NfcProtocolIso14443_4a);
+    seos->listener = nfc_listener_alloc(seos->nfc, NfcProtocolIso14443_4a, data);
+    nfc_listener_start(seos->listener, seos_worker_listener_callback, seos);
+
+    seos_blink_start(seos);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_emulate_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    Popup* popup = seos->popup;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventEmulate) {
+            popup_set_header(popup, "Emulating", 68, 30, AlignLeft, AlignTop);
+        } else if(event.event == SeosCustomEventAIDSelected) {
+            popup_set_header(popup, "AID\nSelected", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventADFMatched) {
+            popup_set_header(popup, "ADF\nMatched", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventAuthenticated) {
+            popup_set_header(popup, "Auth'd", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventSIORequested) {
+            popup_set_header(popup, "SIO\nRequested", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void seos_scene_emulate_on_exit(void* context) {
+    Seos* seos = context;
+
+    if(seos->listener) {
+        nfc_listener_stop(seos->listener);
+        nfc_listener_free(seos->listener);
+        seos->listener = NULL;
+    }
+
+    // Clear view
+    popup_reset(seos->popup);
+
+    seos_blink_stop(seos);
+}

+ 26 - 0
seos/scenes/seos_scene_file_select.c

@@ -0,0 +1,26 @@
+#include "../seos_i.h"
+#include "../seos_emulator.h"
+
+void seos_scene_file_select_on_enter(void* context) {
+    Seos* seos = context;
+    SeosEmulator* seos_emulator = seos->seos_emulator;
+    // Process file_select return
+    seos_emulator_set_loading_callback(seos_emulator, seos_show_loading_popup, seos);
+    if(seos_emulator_file_select(seos_emulator)) {
+        seos->flow_mode = FLOW_CRED;
+        scene_manager_next_scene(seos->scene_manager, SeosSceneSavedMenu);
+    } else {
+        scene_manager_search_and_switch_to_previous_scene(seos->scene_manager, SeosSceneMainMenu);
+    }
+    seos_emulator_set_loading_callback(seos_emulator, NULL, seos);
+}
+
+bool seos_scene_file_select_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void seos_scene_file_select_on_exit(void* context) {
+    UNUSED(context);
+}

+ 109 - 0
seos/scenes/seos_scene_info.c

@@ -0,0 +1,109 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneInfo"
+
+static uint8_t empty[16] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+void seos_scene_info_widget_callback(GuiButtonType result, InputType type, void* context) {
+    furi_assert(context);
+    Seos* seos = context;
+
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, result);
+    }
+}
+
+void seos_scene_info_on_enter(void* context) {
+    Seos* seos = context;
+    Widget* widget = seos->widget;
+    SeosCredential credential = seos->credential;
+
+    FuriString* primary_str = furi_string_alloc_set("Info");
+    FuriString* secondary_str_label = furi_string_alloc();
+    FuriString* secondary_str_value = furi_string_alloc();
+    FuriString* details_str = furi_string_alloc();
+    FuriString* keys_str = furi_string_alloc();
+
+    furi_string_set(secondary_str_label, "Diversifier:");
+    for(size_t i = 0; i < credential.diversifier_len; i++) {
+        furi_string_cat_printf(secondary_str_value, "%02X", credential.diversifier[i]);
+    }
+
+    // RID
+    if(credential.sio_len > 3 && credential.sio[2] == 0x81) {
+        size_t len = credential.sio[3];
+        furi_string_set(details_str, "RID:");
+        for(size_t i = 0; i < len; i++) {
+            furi_string_cat_printf(details_str, "%02X", credential.sio[4 + i]);
+        }
+        if(len >= 4 && credential.sio[3 + 1] == 0x01) {
+            furi_string_cat_printf(details_str, "(retail)");
+        } else if(len == 2) {
+            furi_string_cat_printf(details_str, "(ER)");
+        } else {
+            furi_string_cat_printf(details_str, "(other)");
+        }
+    }
+
+    // keys
+    if(memcmp(credential.priv_key, empty, sizeof(empty)) != 0) {
+        furi_string_cat_printf(keys_str, "+keys");
+    }
+
+    widget_add_string_element(
+        widget, 64, 5, AlignCenter, AlignCenter, FontPrimary, furi_string_get_cstr(primary_str));
+
+    widget_add_string_element(
+        widget,
+        64,
+        20,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(secondary_str_label));
+
+    widget_add_string_element(
+        widget,
+        64,
+        30,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(secondary_str_value));
+
+    widget_add_string_element(
+        widget, 64, 40, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(details_str));
+
+    widget_add_string_element(
+        widget, 64, 50, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(keys_str));
+
+    furi_string_free(primary_str);
+    furi_string_free(secondary_str_label);
+    furi_string_free(secondary_str_value);
+    furi_string_free(details_str);
+    furi_string_free(keys_str);
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewWidget);
+}
+
+bool seos_scene_info_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(seos->scene_manager);
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_previous_scene(seos->scene_manager);
+    }
+    return consumed;
+}
+
+void seos_scene_info_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    widget_reset(seos->widget);
+}

+ 120 - 0
seos/scenes/seos_scene_main_menu.c

@@ -0,0 +1,120 @@
+#include "../seos_i.h"
+
+#define TAG "SceneMainMenu"
+
+enum SubmenuIndex {
+    SubmenuIndexSaved,
+    SubmenuIndexRead,
+    SubmenuIndexBLEReader,
+    SubmenuIndexScannerMenu,
+    SubmenuIndexBLECredInterrogate,
+    SubmenuIndexAbout,
+    SubmenuIndexInspect,
+};
+
+void seos_scene_main_menu_submenu_callback(void* context, uint32_t index) {
+    Seos* seos = context;
+    view_dispatcher_send_custom_event(seos->view_dispatcher, index);
+}
+
+void seos_scene_main_menu_on_enter(void* context) {
+    Seos* seos = context;
+    Submenu* submenu = seos->submenu;
+    submenu_reset(submenu);
+
+    submenu_add_item(
+        submenu, "Saved", SubmenuIndexSaved, seos_scene_main_menu_submenu_callback, seos);
+    submenu_add_item(
+        submenu, "Read NFC", SubmenuIndexRead, seos_scene_main_menu_submenu_callback, seos);
+    submenu_add_item(
+        submenu,
+        "Start BLE Reader",
+        SubmenuIndexBLEReader,
+        seos_scene_main_menu_submenu_callback,
+        seos);
+    if(seos->has_external_ble) {
+        submenu_add_item(
+            submenu,
+            "Scanners >",
+            SubmenuIndexScannerMenu,
+            seos_scene_main_menu_submenu_callback,
+            seos);
+        /*
+        submenu_add_item(
+            submenu,
+            "BLE Cred Interrogate",
+            SubmenuIndexBLECredInterrogate,
+            seos_scene_main_menu_submenu_callback,
+            seos);
+            */
+    }
+    /*
+    submenu_add_item(
+        submenu, "Inspect", SubmenuIndexInspect, seos_scene_main_menu_submenu_callback, seos);
+    */
+    submenu_add_item(
+        submenu, "About", SubmenuIndexAbout, seos_scene_main_menu_submenu_callback, seos);
+
+    submenu_set_selected_item(
+        seos->submenu, scene_manager_get_scene_state(seos->scene_manager, SeosSceneMainMenu));
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewMenu);
+}
+
+bool seos_scene_main_menu_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexRead) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneMainMenu, SubmenuIndexRead);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneRead);
+            consumed = true;
+        } else if(event.event == SubmenuIndexBLEReader) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneMainMenu, SubmenuIndexBLEReader);
+            seos->flow_mode = FLOW_READER;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBlePeripheral);
+            consumed = true;
+        } else if(event.event == SubmenuIndexScannerMenu) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneMainMenu, SubmenuIndexScannerMenu);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneScannerMenu);
+            consumed = true;
+        } else if(event.event == SubmenuIndexBLECredInterrogate) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneMainMenu, SubmenuIndexBLECredInterrogate);
+            seos->flow_mode = FLOW_READER;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBleCentral);
+            consumed = true;
+        } else if(event.event == SubmenuIndexSaved) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneMainMenu, SubmenuIndexSaved);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneFileSelect);
+            consumed = true;
+        } else if(event.event == SubmenuIndexInspect) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneMainMenu, SubmenuIndexInspect);
+            seos->flow_mode = FLOW_INSPECT;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneEmulate);
+            consumed = true;
+        } else if(event.event == SubmenuIndexAbout) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneMainMenu, SubmenuIndexAbout);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneAbout);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        while(scene_manager_previous_scene(seos->scene_manager))
+            ;
+    }
+
+    return consumed;
+}
+
+void seos_scene_main_menu_on_exit(void* context) {
+    Seos* seos = context;
+
+    submenu_reset(seos->submenu);
+}

+ 56 - 0
seos/scenes/seos_scene_read.c

@@ -0,0 +1,56 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneRead"
+
+void seos_scene_read_on_enter(void* context) {
+    Seos* seos = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    // Setup view
+    Popup* popup = seos->popup;
+    popup_set_header(popup, "Reading", 68, 30, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+    seos->poller = nfc_poller_alloc(seos->nfc, NfcProtocolIso14443_4a);
+    nfc_poller_start(seos->poller, seos_worker_poller_callback, seos);
+
+    seos_blink_start(seos);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_read_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventReaderSuccess) {
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadSuccess);
+            consumed = true;
+        } else if(event.event == SeosCustomEventReaderError) {
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadError);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(seos->scene_manager, SeosSceneMainMenu);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void seos_scene_read_on_exit(void* context) {
+    Seos* seos = context;
+
+    if(seos->poller) {
+        nfc_poller_stop(seos->poller);
+        nfc_poller_free(seos->poller);
+        seos->poller = NULL;
+    }
+
+    // Clear view
+    popup_reset(seos->popup);
+
+    seos_blink_stop(seos);
+}

+ 64 - 0
seos/scenes/seos_scene_read_error.c

@@ -0,0 +1,64 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneReadCardSuccess"
+
+void seos_scene_read_error_widget_callback(GuiButtonType result, InputType type, void* context) {
+    furi_assert(context);
+    Seos* seos = context;
+
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, result);
+    }
+}
+
+void seos_scene_read_error_on_enter(void* context) {
+    Seos* seos = context;
+    Widget* widget = seos->widget;
+
+    // Send notification
+    notification_message(seos->notifications, &sequence_success);
+    FuriString* primary_str = furi_string_alloc_set("Read Errror");
+    FuriString* secondary_str = furi_string_alloc_set("Try again?");
+
+    widget_add_button_element(
+        widget, GuiButtonTypeLeft, "Retry", seos_scene_read_error_widget_callback, seos);
+
+    widget_add_string_element(
+        widget, 64, 5, AlignCenter, AlignCenter, FontPrimary, furi_string_get_cstr(primary_str));
+
+    widget_add_string_element(
+        widget,
+        64,
+        20,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(secondary_str));
+
+    furi_string_free(primary_str);
+    furi_string_free(secondary_str);
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewWidget);
+}
+
+bool seos_scene_read_error_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(seos->scene_manager);
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(seos->scene_manager, SeosSceneMainMenu);
+        consumed = true;
+    }
+    return consumed;
+}
+
+void seos_scene_read_error_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    widget_reset(seos->widget);
+}

+ 109 - 0
seos/scenes/seos_scene_read_success.c

@@ -0,0 +1,109 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneReadCardSuccess"
+
+void seos_scene_read_success_widget_callback(GuiButtonType result, InputType type, void* context) {
+    furi_assert(context);
+    Seos* seos = context;
+
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, result);
+    }
+}
+
+void seos_scene_read_success_on_enter(void* context) {
+    Seos* seos = context;
+    SeosCredential credential = seos->credential;
+    Widget* widget = seos->widget;
+
+    dolphin_deed(DolphinDeedNfcReadSuccess);
+
+    FuriString* primary_str = furi_string_alloc_set("SIO Captured");
+    FuriString* secondary_str_label = furi_string_alloc();
+    FuriString* secondary_str_value = furi_string_alloc();
+    FuriString* details_str = furi_string_alloc();
+
+    furi_string_set(secondary_str_label, "Diversifier:");
+    for(size_t i = 0; i < credential.diversifier_len; i++) {
+        furi_string_cat_printf(secondary_str_value, "%02X", credential.diversifier[i]);
+    }
+
+    // RID
+    if(credential.sio_len > 3 && credential.sio[2] == 0x81) {
+        size_t len = credential.sio[3];
+        furi_string_set(details_str, "RID:");
+        for(size_t i = 0; i < len; i++) {
+            furi_string_cat_printf(details_str, "%02X", credential.sio[4 + i]);
+        }
+        if(len >= 4 && credential.sio[3 + 1] == 0x01) {
+            furi_string_cat_printf(details_str, "(retail)");
+        } else if(len == 2) {
+            furi_string_cat_printf(details_str, "(ER)");
+        } else {
+            furi_string_cat_printf(details_str, "(other)");
+        }
+    }
+
+    widget_add_button_element(
+        widget, GuiButtonTypeLeft, "Save", seos_scene_read_success_widget_callback, seos);
+
+    widget_add_button_element(
+        widget, GuiButtonTypeRight, "Emulate", seos_scene_read_success_widget_callback, seos);
+
+    widget_add_string_element(
+        widget, 64, 5, AlignCenter, AlignCenter, FontPrimary, furi_string_get_cstr(primary_str));
+
+    widget_add_string_element(
+        widget,
+        64,
+        20,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(secondary_str_label));
+
+    widget_add_string_element(
+        widget,
+        64,
+        30,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(secondary_str_value));
+
+    widget_add_string_element(
+        widget, 64, 45, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(details_str));
+
+    furi_string_free(primary_str);
+    furi_string_free(secondary_str_label);
+    furi_string_free(secondary_str_value);
+    furi_string_free(details_str);
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewWidget);
+}
+
+bool seos_scene_read_success_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            scene_manager_next_scene(seos->scene_manager, SeosSceneSaveName);
+            consumed = true;
+        } else if(event.event == GuiButtonTypeRight) {
+            scene_manager_next_scene(seos->scene_manager, SeosSceneSavedMenu);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(seos->scene_manager, SeosSceneMainMenu);
+        consumed = true;
+    }
+    return consumed;
+}
+
+void seos_scene_read_success_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    widget_reset(seos->widget);
+}

+ 79 - 0
seos/scenes/seos_scene_save_name.c

@@ -0,0 +1,79 @@
+#include "../seos_i.h"
+#include <lib/toolbox/name_generator.h>
+#include <gui/modules/validators.h>
+#include <toolbox/path.h>
+
+#define SEOS_APP_FILE_PREFIX "Seos"
+
+void seos_scene_save_name_text_input_callback(void* context) {
+    Seos* seos = context;
+
+    view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventTextInputDone);
+}
+
+void seos_scene_save_name_on_enter(void* context) {
+    Seos* seos = context;
+
+    // Setup view
+    TextInput* text_input = seos->text_input;
+    bool dev_name_empty = false;
+    if(!strcmp(seos->dev_name, "")) {
+        name_generator_make_auto(seos->text_store, sizeof(seos->text_store), SEOS_APP_FILE_PREFIX);
+        dev_name_empty = true;
+    } else {
+        seos_text_store_set(seos, seos->dev_name);
+    }
+    text_input_set_header_text(text_input, "Name the card");
+    text_input_set_result_callback(
+        text_input,
+        seos_scene_save_name_text_input_callback,
+        seos,
+        seos->text_store,
+        sizeof(seos->text_store),
+        dev_name_empty);
+
+    FuriString* folder_path;
+    folder_path = furi_string_alloc_set(STORAGE_APP_DATA_PATH_PREFIX);
+
+    if(furi_string_end_with(seos->load_path, SEOS_APP_EXTENSION)) {
+        path_extract_dirname(furi_string_get_cstr(seos->load_path), folder_path);
+    }
+
+    ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
+        furi_string_get_cstr(folder_path), SEOS_APP_EXTENSION, seos->dev_name);
+    text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewTextInput);
+
+    furi_string_free(folder_path);
+}
+
+bool seos_scene_save_name_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventTextInputDone) {
+            strlcpy(seos->dev_name, seos->text_store, strlen(seos->text_store) + 1);
+            if(seos_credential_save(seos, seos->text_store)) {
+                scene_manager_next_scene(seos->scene_manager, SeosSceneSaveSuccess);
+                consumed = true;
+            } else {
+                consumed = scene_manager_search_and_switch_to_previous_scene(
+                    seos->scene_manager, SeosSceneMainMenu);
+            }
+        }
+    }
+    return consumed;
+}
+
+void seos_scene_save_name_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    void* validator_context = text_input_get_validator_callback_context(seos->text_input);
+    text_input_set_validator(seos->text_input, NULL, NULL);
+    validator_is_file_free(validator_context);
+
+    text_input_reset(seos->text_input);
+}

+ 42 - 0
seos/scenes/seos_scene_save_success.c

@@ -0,0 +1,42 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+void seos_scene_save_success_popup_callback(void* context) {
+    Seos* seos = context;
+    view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventViewExit);
+}
+
+void seos_scene_save_success_on_enter(void* context) {
+    Seos* seos = context;
+    dolphin_deed(DolphinDeedNfcSave);
+
+    // Setup view
+    Popup* popup = seos->popup;
+    popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59);
+    popup_set_header(popup, "Saved!", 13, 22, AlignLeft, AlignBottom);
+    popup_set_timeout(popup, 1500);
+    popup_set_context(popup, seos);
+    popup_set_callback(popup, seos_scene_save_success_popup_callback);
+    popup_enable_timeout(popup);
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_save_success_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventViewExit) {
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                seos->scene_manager, SeosSceneMainMenu);
+        }
+    }
+    return consumed;
+}
+
+void seos_scene_save_success_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    popup_reset(seos->popup);
+}

+ 98 - 0
seos/scenes/seos_scene_saved_menu.c

@@ -0,0 +1,98 @@
+#include "../seos_i.h"
+
+enum SubmenuIndex {
+    SubmenuIndexEmulate,
+    SubmenuIndexBLEEmulateCentral,
+    SubmenuIndexBLEEmulatePeripheral,
+    SubmenuIndexDelete,
+    SubmenuIndexInfo,
+};
+
+void seos_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
+    Seos* seos = context;
+
+    view_dispatcher_send_custom_event(seos->view_dispatcher, index);
+}
+
+void seos_scene_saved_menu_on_enter(void* context) {
+    Seos* seos = context;
+    Submenu* submenu = seos->submenu;
+
+    submenu_add_item(
+        submenu, "NFC Emulate", SubmenuIndexEmulate, seos_scene_saved_menu_submenu_callback, seos);
+
+    if(seos->has_external_ble) {
+        submenu_add_item(
+            submenu,
+            "BLE Emulate Central",
+            SubmenuIndexBLEEmulateCentral,
+            seos_scene_saved_menu_submenu_callback,
+            seos);
+    }
+    submenu_add_item(
+        submenu,
+        "BLE Emulate Peripheral",
+        SubmenuIndexBLEEmulatePeripheral,
+        seos_scene_saved_menu_submenu_callback,
+        seos);
+
+    submenu_add_item(
+        submenu, "Info", SubmenuIndexInfo, seos_scene_saved_menu_submenu_callback, seos);
+    submenu_add_item(
+        submenu, "Delete", SubmenuIndexDelete, seos_scene_saved_menu_submenu_callback, seos);
+
+    submenu_set_selected_item(
+        seos->submenu, scene_manager_get_scene_state(seos->scene_manager, SeosSceneSavedMenu));
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewMenu);
+}
+
+bool seos_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        scene_manager_set_scene_state(seos->scene_manager, SeosSceneSavedMenu, event.event);
+
+        if(event.event == SubmenuIndexEmulate) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneSavedMenu, SubmenuIndexEmulate);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneEmulate);
+            consumed = true;
+        } else if(event.event == SubmenuIndexBLEEmulateCentral) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneSavedMenu, SubmenuIndexBLEEmulateCentral);
+            seos->flow_mode = FLOW_CRED;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBleCentral);
+            consumed = true;
+        } else if(event.event == SubmenuIndexBLEEmulatePeripheral) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneSavedMenu, SubmenuIndexBLEEmulatePeripheral);
+            seos->flow_mode = FLOW_CRED;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBlePeripheral);
+            consumed = true;
+        } else if(event.event == SubmenuIndexInfo) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneSavedMenu, SubmenuIndexInfo);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneInfo);
+            consumed = true;
+        } else if(event.event == SubmenuIndexDelete) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneSavedMenu, SubmenuIndexDelete);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneDelete);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        memset((void*)&seos->credential, 0, sizeof(seos->credential));
+        scene_manager_search_and_switch_to_previous_scene(seos->scene_manager, SeosSceneMainMenu);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void seos_scene_saved_menu_on_exit(void* context) {
+    Seos* seos = context;
+
+    submenu_reset(seos->submenu);
+}

+ 61 - 0
seos/scenes/seos_scene_scanner_menu.c

@@ -0,0 +1,61 @@
+#include "../seos_i.h"
+enum SubmenuIndex {
+    SubmenuIndexBLEReaderScanner,
+    SubmenuIndexBLECredScanner,
+};
+
+void seos_scene_scanner_menu_submenu_callback(void* context, uint32_t index) {
+    Seos* seos = context;
+    view_dispatcher_send_custom_event(seos->view_dispatcher, index);
+}
+
+void seos_scene_scanner_menu_on_enter(void* context) {
+    Seos* seos = context;
+    Submenu* submenu = seos->submenu;
+
+    submenu_add_item(
+        submenu,
+        "Start BLE Reader Scanner",
+        SubmenuIndexBLEReaderScanner,
+        seos_scene_scanner_menu_submenu_callback,
+        seos);
+    submenu_add_item(
+        submenu,
+        "Start BLE Cred Scanner",
+        SubmenuIndexBLECredScanner,
+        seos_scene_scanner_menu_submenu_callback,
+        seos);
+
+    submenu_set_selected_item(
+        seos->submenu, scene_manager_get_scene_state(seos->scene_manager, SeosSceneMainMenu));
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewMenu);
+}
+
+bool seos_scene_scanner_menu_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexBLEReaderScanner) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneMainMenu, SubmenuIndexBLEReaderScanner);
+            seos->flow_mode = FLOW_READER_SCANNER;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBleCentral);
+            consumed = true;
+        } else if(event.event == SubmenuIndexBLECredScanner) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneMainMenu, SubmenuIndexBLECredScanner);
+            seos->flow_mode = FLOW_CRED_SCANNER;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBleCentral);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void seos_scene_scanner_menu_on_exit(void* context) {
+    Seos* seos = context;
+    submenu_reset(seos->submenu);
+}

+ 46 - 0
seos/scenes/seos_scene_zero_keys.c

@@ -0,0 +1,46 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneZeroKeys"
+
+void seos_scene_zero_keys_popup_callback(void* context) {
+    Seos* seos = context;
+    view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventViewExit);
+}
+
+void seos_scene_zero_keys_on_enter(void* context) {
+    Seos* seos = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    // Setup view
+    Popup* popup = seos->popup;
+    popup_set_header(popup, "NO KEYS", 64, 16, AlignCenter, AlignTop);
+    popup_set_text(popup, "Using all zero keys", 64, 36, AlignCenter, AlignTop);
+    popup_set_timeout(popup, 5 * 1000);
+    popup_set_context(popup, seos);
+    popup_set_callback(popup, seos_scene_zero_keys_popup_callback);
+    popup_enable_timeout(popup);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_zero_keys_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventViewExit) {
+            scene_manager_next_scene(seos->scene_manager, SeosSceneMainMenu);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void seos_scene_zero_keys_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    popup_reset(seos->popup);
+}

+ 349 - 0
seos/secure_messaging.c

@@ -0,0 +1,349 @@
+#include "secure_messaging.h"
+
+#define TAG "SecureMessaging"
+
+uint8_t padding[16] =
+    {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+SecureMessaging* secure_messaging_alloc(AuthParameters* params) {
+    SecureMessaging* secure_messaging = malloc(sizeof(SecureMessaging));
+    memset(secure_messaging, 0, sizeof(SecureMessaging));
+
+    secure_messaging->cipher = params->cipher;
+    if(params->cipher == AES_128_CBC) {
+        memcpy(secure_messaging->aesContext, params->rndICC, 8);
+        memcpy(secure_messaging->aesContext + 8, params->UID, 8);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        memcpy(secure_messaging->desContext, params->rndICC, 4);
+        memcpy(secure_messaging->desContext + 4, params->UID, 4);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    size_t index = 0;
+    uint8_t buffer[38];
+    memset(buffer, 0, sizeof(buffer));
+    index += 4; // skip 4 bytes where iteration will be put
+    memcpy(buffer + index, params->cNonce, 8);
+    index += 8;
+    memcpy(buffer + index, params->rNonce, 8);
+    index += 8;
+    buffer[index++] = params->cipher;
+    buffer[index++] = params->cipher;
+    memcpy(buffer + index, params->rndICC, 8);
+    index += 8;
+    memcpy(buffer + index, params->UID, 8);
+    index += 8;
+
+    size_t iterations = 1;
+    size_t unit = 0;
+    if(params->hash == SHA1) {
+        unit = 160 / 8;
+    } else if(params->hash == SHA256) {
+        unit = 256 / 8;
+    }
+    // FURI_LOG_D(TAG, "secure_messaging_alloc hash %d unit %d", hash, unit);
+
+    // More than enough space for the hash
+    uint8_t accumulator[64];
+    memset(accumulator, 0, sizeof(accumulator));
+    for(size_t i = 0; i < 32; i += unit) {
+        buffer[3] = iterations++;
+        if(params->hash == SHA1) {
+            mbedtls_sha1_context ctx;
+            mbedtls_sha1_init(&ctx);
+            mbedtls_sha1_starts(&ctx);
+            mbedtls_sha1_update(&ctx, buffer, index);
+            mbedtls_sha1_finish(&ctx, accumulator + i);
+            mbedtls_sha1_free(&ctx);
+        } else if(params->hash == SHA256) {
+            mbedtls_sha256_context ctx;
+            mbedtls_sha256_init(&ctx);
+            mbedtls_sha256_starts(&ctx, 0);
+            mbedtls_sha256_update(&ctx, buffer, index);
+            mbedtls_sha256_finish(&ctx, accumulator + i);
+            mbedtls_sha256_free(&ctx);
+        } else {
+            FURI_LOG_W(TAG, "Could not match hash algorithm");
+        }
+    }
+
+    memcpy(secure_messaging->PrivacyKey, accumulator, 16);
+    memcpy(secure_messaging->CMACKey, accumulator + 16, 16);
+
+    return secure_messaging;
+}
+
+void secure_messaging_free(SecureMessaging* secure_messaging) {
+    furi_assert(secure_messaging);
+    // Nothing to free;
+    free(secure_messaging);
+}
+
+void secure_messaging_increment_context(SecureMessaging* secure_messaging) {
+    uint8_t* context = NULL;
+    size_t context_len = 0;
+    if(secure_messaging->cipher == AES_128_CBC) {
+        context = secure_messaging->aesContext;
+        context_len = sizeof(secure_messaging->aesContext);
+    } else if(secure_messaging->cipher == TWO_KEY_3DES_CBC_MODE) {
+        context = secure_messaging->desContext;
+        context_len = sizeof(secure_messaging->desContext);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+        return;
+    }
+    do {
+    } while(++context[--context_len] == 0 && context_len > 0);
+}
+
+void secure_messaging_wrap_apdu(
+    SecureMessaging* secure_messaging,
+    uint8_t* message,
+    size_t message_len,
+    BitBuffer* tx_buffer) {
+    secure_messaging_increment_context(secure_messaging);
+    uint8_t cipher = secure_messaging->cipher;
+
+    if(message_len > SECURE_MESSAGING_MAX_SIZE) {
+        FURI_LOG_W(TAG, "Message too long to wrap");
+        return;
+    }
+
+    uint8_t clear[SECURE_MESSAGING_MAX_SIZE];
+    memset(clear, 0, sizeof(clear));
+    memcpy(clear, message, message_len);
+    clear[message_len] = 0x80;
+    uint8_t block_size = cipher == AES_128_CBC ? 16 : 8;
+    uint8_t block_count = (message_len + block_size - 1) / block_size;
+    size_t clear_len = block_count * block_size;
+
+    uint8_t encrypted[SECURE_MESSAGING_MAX_SIZE];
+    if(cipher == AES_128_CBC) {
+        seos_worker_aes_encrypt(secure_messaging->PrivacyKey, clear_len, clear, encrypted);
+    } else if(cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_encrypt(secure_messaging->PrivacyKey, clear_len, clear, encrypted);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    uint8_t header[] = {0x0c, 0xcb, 0x3f, 0xff};
+    uint8_t encrypted_prefix[] = {0x85, clear_len};
+    uint8_t no_something[] = {0x97, 0x00};
+
+    uint8_t cmac[16];
+    BitBuffer* cmacInput = bit_buffer_alloc(256);
+    bit_buffer_reset(cmacInput);
+    if(cipher == AES_128_CBC) {
+        uint8_t* context = secure_messaging->aesContext;
+        bit_buffer_append_bytes(cmacInput, context, block_size);
+        bit_buffer_append_bytes(cmacInput, header, sizeof(header));
+        bit_buffer_append_bytes(cmacInput, padding, block_size - sizeof(header));
+        bit_buffer_append_bytes(cmacInput, encrypted_prefix, sizeof(encrypted_prefix));
+        bit_buffer_append_bytes(cmacInput, encrypted, clear_len);
+        bit_buffer_append_bytes(cmacInput, no_something, sizeof(no_something));
+        // Need to pad [encrypted_prefix, encrypted, no_something], and encrypted is by definition block_size units,so we just need to pad to align the other 4.
+        bit_buffer_append_bytes(cmacInput, padding, block_size - 4);
+
+        aes_cmac(
+            secure_messaging->CMACKey,
+            sizeof(secure_messaging->CMACKey),
+            (uint8_t*)bit_buffer_get_data(cmacInput),
+            bit_buffer_get_size_bytes(cmacInput),
+            cmac);
+    } else if(cipher == TWO_KEY_3DES_CBC_MODE) {
+        uint8_t* context = secure_messaging->desContext;
+        bit_buffer_append_bytes(cmacInput, context, block_size);
+        bit_buffer_append_bytes(cmacInput, header, sizeof(header));
+        bit_buffer_append_bytes(cmacInput, padding, block_size - sizeof(header));
+        bit_buffer_append_bytes(cmacInput, encrypted_prefix, sizeof(encrypted_prefix));
+        bit_buffer_append_bytes(cmacInput, encrypted, clear_len);
+        bit_buffer_append_bytes(cmacInput, no_something, sizeof(no_something));
+        // Need to pad [encrypted_prefix, encrypted, no_something], and encrypted is by definition block_size units,so we just need to pad to align the other 4.
+        bit_buffer_append_bytes(cmacInput, padding, block_size - 4);
+
+        des_cmac(
+            secure_messaging->CMACKey,
+            sizeof(secure_messaging->CMACKey),
+            (uint8_t*)bit_buffer_get_data(cmacInput),
+            bit_buffer_get_size_bytes(cmacInput),
+            cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    bit_buffer_free(cmacInput);
+
+    uint8_t apdu_len[] = {2 + clear_len + 2 + 2 + 8};
+    uint8_t cmac_prefix[] = {0x8e, 0x08};
+    uint8_t Le[] = {0x00};
+
+    bit_buffer_reset(tx_buffer);
+    bit_buffer_append_bytes(tx_buffer, header, sizeof(header));
+    bit_buffer_append_bytes(tx_buffer, apdu_len, sizeof(apdu_len));
+    bit_buffer_append_bytes(tx_buffer, encrypted_prefix, sizeof(encrypted_prefix));
+    bit_buffer_append_bytes(tx_buffer, encrypted, clear_len);
+    bit_buffer_append_bytes(tx_buffer, no_something, sizeof(no_something));
+    bit_buffer_append_bytes(tx_buffer, cmac_prefix, sizeof(cmac_prefix));
+    bit_buffer_append_bytes(tx_buffer, cmac, 8);
+    bit_buffer_append_bytes(tx_buffer, Le, sizeof(Le));
+}
+
+void secure_messaging_unwrap_rapdu(SecureMessaging* secure_messaging, BitBuffer* rx_buffer) {
+    secure_messaging_increment_context(secure_messaging);
+    // 8540 <encrypted> 99029000 8e08 <cmac>
+
+    size_t encrypted_len = bit_buffer_get_byte(rx_buffer, 1);
+    const uint8_t* encrypted = bit_buffer_get_data(rx_buffer) + 2;
+    // const uint8_t *cmac = bit_buffer_get_data(rx_buffer) + 2 + encrypted_len + 6;
+
+    uint8_t clear[SECURE_MESSAGING_MAX_SIZE];
+    memset(clear, 0, sizeof(clear));
+
+    // TODO: check cmac
+    if(secure_messaging->cipher == AES_128_CBC) {
+        seos_worker_aes_decrypt(secure_messaging->PrivacyKey, encrypted_len, encrypted, clear);
+    } else if(secure_messaging->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_decrypt(secure_messaging->PrivacyKey, encrypted_len, encrypted, clear);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    size_t clear_len = encrypted_len;
+    do {
+    } while(clear[--clear_len] == 0 && clear_len > 0);
+    bit_buffer_reset(rx_buffer);
+    bit_buffer_append_bytes(rx_buffer, clear, clear_len);
+}
+
+// Assumes it is an iso14443a-4 and doesn't have framing bytes
+/*
+0ccb3fff
+16
+  8508
+    4088b37ca72bc7ae
+  9700
+  8e08
+    85345f0f5c44b980
+00
+*/
+void secure_messaging_unwrap_apdu(SecureMessaging* secure_messaging, BitBuffer* rx_buffer) {
+    secure_messaging_increment_context(secure_messaging);
+
+    // TODO: check cmac
+    const uint8_t* encrypted = bit_buffer_get_data(rx_buffer) + 7;
+    size_t encrypted_len = bit_buffer_get_byte(rx_buffer, 6);
+
+    if(encrypted_len > SECURE_MESSAGING_MAX_SIZE) {
+        FURI_LOG_W(TAG, "message too large (%d) to unwrap", encrypted_len);
+        return;
+    }
+
+    uint8_t clear[SECURE_MESSAGING_MAX_SIZE];
+    memset(clear, 0, sizeof(clear));
+    if(secure_messaging->cipher == AES_128_CBC) {
+        seos_worker_aes_decrypt(secure_messaging->PrivacyKey, encrypted_len, encrypted, clear);
+    } else if(secure_messaging->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_decrypt(secure_messaging->PrivacyKey, encrypted_len, encrypted, clear);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    do {
+    } while(clear[--encrypted_len] == 0 && encrypted_len > 0);
+    bit_buffer_reset(rx_buffer);
+    bit_buffer_append_bytes(rx_buffer, clear, encrypted_len);
+}
+
+void secure_messaging_wrap_rapdu(
+    SecureMessaging* secure_messaging,
+    uint8_t* message,
+    size_t message_len,
+    BitBuffer* tx_buffer) {
+    secure_messaging_increment_context(secure_messaging);
+    uint8_t cipher = secure_messaging->cipher;
+
+    if(message_len > SECURE_MESSAGING_MAX_SIZE) {
+        FURI_LOG_W(TAG, "Message too long to wrap");
+        return;
+    }
+
+    // Copy into clear so we can pad it.
+    uint8_t clear[SECURE_MESSAGING_MAX_SIZE];
+    memset(clear, 0, sizeof(clear));
+    memcpy(clear, message, message_len);
+    clear[message_len] = 0x80;
+    uint8_t block_size = cipher == AES_128_CBC ? 16 : 8;
+    uint8_t block_count = (message_len + block_size - 1) / block_size;
+    size_t clear_len = block_count * block_size;
+
+    uint8_t encrypted[SECURE_MESSAGING_MAX_SIZE];
+    if(cipher == AES_128_CBC) {
+        seos_worker_aes_encrypt(secure_messaging->PrivacyKey, clear_len, clear, encrypted);
+    } else if(cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_encrypt(secure_messaging->PrivacyKey, clear_len, clear, encrypted);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    uint8_t encrypted_prefix[] = {0x85, clear_len};
+    uint8_t cmac_prefix[] = {0x8e, 0x08};
+    uint8_t something[] = {0x99, 0x02, 0x90, 0x00};
+
+    uint8_t cmac[16];
+    BitBuffer* cmacInput = bit_buffer_alloc(256);
+    bit_buffer_reset(cmacInput);
+    if(cipher == AES_128_CBC) {
+        uint8_t* context = secure_messaging->aesContext;
+        bit_buffer_append_bytes(cmacInput, context, block_size);
+        bit_buffer_append_bytes(cmacInput, encrypted_prefix, sizeof(encrypted_prefix));
+        bit_buffer_append_bytes(cmacInput, encrypted, clear_len);
+        bit_buffer_append_bytes(cmacInput, something, sizeof(something));
+        // Need to pad to multiple of block size, but context and encrypted are already block size.
+        bit_buffer_append_bytes(
+            cmacInput, padding, block_size - sizeof(encrypted_prefix) - sizeof(something));
+
+        aes_cmac(
+            secure_messaging->CMACKey,
+            sizeof(secure_messaging->CMACKey),
+            (uint8_t*)bit_buffer_get_data(cmacInput),
+            bit_buffer_get_size_bytes(cmacInput),
+            cmac);
+    } else if(cipher == TWO_KEY_3DES_CBC_MODE) {
+        uint8_t* context = secure_messaging->desContext;
+        bit_buffer_append_bytes(cmacInput, context, block_size);
+        bit_buffer_append_bytes(cmacInput, encrypted_prefix, sizeof(encrypted_prefix));
+        bit_buffer_append_bytes(cmacInput, encrypted, clear_len);
+        bit_buffer_append_bytes(cmacInput, something, sizeof(something));
+        // Need to pad to multiple of block size, but context and encrypted are already block size.
+        bit_buffer_append_bytes(
+            cmacInput, padding, block_size - sizeof(encrypted_prefix) - sizeof(something));
+
+        des_cmac(
+            secure_messaging->CMACKey,
+            sizeof(secure_messaging->CMACKey),
+            (uint8_t*)bit_buffer_get_data(cmacInput),
+            bit_buffer_get_size_bytes(cmacInput),
+            cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+    bit_buffer_free(cmacInput);
+
+    bit_buffer_append_bytes(tx_buffer, encrypted_prefix, sizeof(encrypted_prefix));
+    bit_buffer_append_bytes(tx_buffer, encrypted, clear_len);
+    bit_buffer_append_bytes(tx_buffer, something, sizeof(something));
+    bit_buffer_append_bytes(tx_buffer, cmac_prefix, sizeof(cmac_prefix));
+    bit_buffer_append_bytes(tx_buffer, cmac, 8);
+    // Success (9000) is appended by common code before transmission
+
+    /*
+8540
+  2b4f4e5598193e71cc94ff6dc2b24d1e9feae4182d7315b8b11a8a034670fe8c369f2a98c256dd6f4decf28277a180ea1c4ce515812abfa683e1e004bc66d757
+9902
+  9000
+8e08
+  a860944446f6d53d
+9000
+*/
+}

+ 42 - 0
seos/secure_messaging.h

@@ -0,0 +1,42 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdlib.h>
+
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+#include <mbedtls/sha1.h>
+#include <mbedtls/sha256.h>
+
+#include "seos_common.h"
+#include "aes_cmac.h"
+#include "des_cmac.h"
+
+#define SECURE_MESSAGING_MAX_SIZE 128
+
+typedef struct {
+    uint8_t cipher;
+    uint8_t PrivacyKey[16];
+    uint8_t CMACKey[16];
+    uint8_t aesContext[16];
+    uint8_t desContext[8];
+} SecureMessaging;
+
+SecureMessaging* secure_messaging_alloc(AuthParameters* params);
+
+void secure_messaging_free(SecureMessaging* secure_messaging);
+
+void secure_messaging_wrap_apdu(
+    SecureMessaging* secure_messaging,
+    uint8_t* message,
+    size_t message_len,
+    BitBuffer* tx_buffer);
+
+void secure_messaging_unwrap_apdu(SecureMessaging* secure_messaging, BitBuffer* rx_buffer);
+
+void secure_messaging_unwrap_rapdu(SecureMessaging* secure_messaging, BitBuffer* rx_buffer);
+void secure_messaging_wrap_rapdu(
+    SecureMessaging* secure_messaging,
+    uint8_t* message,
+    size_t message_len,
+    BitBuffer* tx_buffer);

+ 305 - 0
seos/seos.c

@@ -0,0 +1,305 @@
+#include "seos_i.h"
+
+#define TAG "Seos"
+
+#define SEOS_KEYS_FILENAME "keys"
+
+bool seos_load_keys(Seos* seos) {
+    const char* file_header = "Seos keys";
+    const uint32_t file_version = 1;
+    bool parsed = false;
+    FlipperFormat* file = flipper_format_file_alloc(seos->storage);
+    FuriString* path = furi_string_alloc();
+    FuriString* temp_str = furi_string_alloc();
+    uint32_t version = 0;
+
+    do {
+        furi_string_printf(
+            path, "%s/%s%s", STORAGE_APP_DATA_PATH_PREFIX, SEOS_KEYS_FILENAME, ".txt");
+        // Open file
+        if(!flipper_format_file_open_existing(file, furi_string_get_cstr(path))) break;
+        if(!flipper_format_read_header(file, temp_str, &version)) break;
+        if(!furi_string_equal_str(temp_str, file_header) || (version != file_version)) {
+            break;
+        }
+
+        if(!flipper_format_read_uint32(file, "SEOS_ADF_OID_LEN", (uint32_t*)&SEOS_ADF_OID_LEN, 1))
+            break;
+        if(!flipper_format_read_hex(file, "SEOS_ADF_OID", SEOS_ADF_OID, SEOS_ADF_OID_LEN)) break;
+        if(!flipper_format_read_hex(file, "SEOS_ADF1_PRIV_ENC", SEOS_ADF1_PRIV_ENC, 16)) break;
+        if(!flipper_format_read_hex(file, "SEOS_ADF1_PRIV_MAC", SEOS_ADF1_PRIV_MAC, 16)) break;
+        if(!flipper_format_read_hex(file, "SEOS_ADF1_READ", SEOS_ADF1_READ, 16)) break;
+
+        parsed = true;
+    } while(false);
+
+    if(parsed) {
+        FURI_LOG_I(TAG, "Keys loaded");
+        BitBuffer* tmp = bit_buffer_alloc(SEOS_ADF_OID_LEN);
+        bit_buffer_append_bytes(tmp, SEOS_ADF_OID, SEOS_ADF_OID_LEN);
+        seos_log_bitbuffer(TAG, "Keys for ADF OID loaded", tmp);
+        bit_buffer_free(tmp);
+    } else {
+        FURI_LOG_I(TAG, "Using default keys");
+    }
+
+    furi_string_free(path);
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+
+    return parsed;
+}
+
+bool seos_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    Seos* seos = context;
+    return scene_manager_handle_custom_event(seos->scene_manager, event);
+}
+
+bool seos_back_event_callback(void* context) {
+    furi_assert(context);
+    Seos* seos = context;
+    return scene_manager_handle_back_event(seos->scene_manager);
+}
+
+void seos_tick_event_callback(void* context) {
+    furi_assert(context);
+    Seos* seos = context;
+    scene_manager_handle_tick_event(seos->scene_manager);
+}
+
+Seos* seos_alloc() {
+    Seos* seos = malloc(sizeof(Seos));
+
+    seos->has_external_ble = false;
+    furi_hal_power_enable_otg();
+
+    seos->view_dispatcher = view_dispatcher_alloc();
+    seos->scene_manager = scene_manager_alloc(&seos_scene_handlers, seos);
+    view_dispatcher_set_event_callback_context(seos->view_dispatcher, seos);
+    view_dispatcher_set_custom_event_callback(seos->view_dispatcher, seos_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(seos->view_dispatcher, seos_back_event_callback);
+    view_dispatcher_set_tick_event_callback(seos->view_dispatcher, seos_tick_event_callback, 100);
+
+    seos->nfc = nfc_alloc();
+
+    // Nfc device
+    seos->nfc_device = nfc_device_alloc();
+    nfc_device_set_loading_callback(seos->nfc_device, seos_show_loading_popup, seos);
+
+    // Open GUI record
+    seos->gui = furi_record_open(RECORD_GUI);
+    view_dispatcher_attach_to_gui(seos->view_dispatcher, seos->gui, ViewDispatcherTypeFullscreen);
+
+    // Open Notification record
+    seos->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    // Submenu
+    seos->submenu = submenu_alloc();
+    view_dispatcher_add_view(seos->view_dispatcher, SeosViewMenu, submenu_get_view(seos->submenu));
+
+    // Popup
+    seos->popup = popup_alloc();
+    view_dispatcher_add_view(seos->view_dispatcher, SeosViewPopup, popup_get_view(seos->popup));
+
+    // Loading
+    seos->loading = loading_alloc();
+    view_dispatcher_add_view(
+        seos->view_dispatcher, SeosViewLoading, loading_get_view(seos->loading));
+
+    // Text Input
+    seos->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        seos->view_dispatcher, SeosViewTextInput, text_input_get_view(seos->text_input));
+
+    // TextBox
+    seos->text_box = text_box_alloc();
+    view_dispatcher_add_view(
+        seos->view_dispatcher, SeosViewTextBox, text_box_get_view(seos->text_box));
+    seos->text_box_store = furi_string_alloc();
+
+    // Custom Widget
+    seos->widget = widget_alloc();
+    view_dispatcher_add_view(seos->view_dispatcher, SeosViewWidget, widget_get_view(seos->widget));
+
+    seos->storage = furi_record_open(RECORD_STORAGE);
+    seos->dialogs = furi_record_open(RECORD_DIALOGS);
+    seos->load_path = furi_string_alloc();
+
+    seos->seos_emulator = seos_emulator_alloc(&seos->credential);
+
+    seos->keys_loaded = seos_load_keys(seos);
+
+    return seos;
+}
+
+void seos_free(Seos* seos) {
+    furi_assert(seos);
+
+    furi_hal_power_disable_otg();
+
+    nfc_free(seos->nfc);
+
+    // Nfc device
+    nfc_device_free(seos->nfc_device);
+
+    // Submenu
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewMenu);
+    submenu_free(seos->submenu);
+
+    // Popup
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewPopup);
+    popup_free(seos->popup);
+
+    // Loading
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewLoading);
+    loading_free(seos->loading);
+
+    // TextInput
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewTextInput);
+    text_input_free(seos->text_input);
+
+    // TextBox
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewTextBox);
+    text_box_free(seos->text_box);
+    furi_string_free(seos->text_box_store);
+
+    // Custom Widget
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewWidget);
+    widget_free(seos->widget);
+
+    // View Dispatcher
+    view_dispatcher_free(seos->view_dispatcher);
+
+    // Scene Manager
+    scene_manager_free(seos->scene_manager);
+
+    // GUI
+    furi_record_close(RECORD_GUI);
+    seos->gui = NULL;
+
+    // Notifications
+    furi_record_close(RECORD_NOTIFICATION);
+    seos->notifications = NULL;
+
+    furi_string_free(seos->load_path);
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_DIALOGS);
+
+    if(seos->seos_emulator) {
+        seos_emulator_free(seos->seos_emulator);
+        seos->seos_emulator = NULL;
+    }
+
+    free(seos);
+}
+
+void seos_text_store_set(Seos* seos, const char* text, ...) {
+    va_list args;
+    va_start(args, text);
+
+    vsnprintf(seos->text_store, sizeof(seos->text_store), text, args);
+
+    va_end(args);
+}
+
+void seos_text_store_clear(Seos* seos) {
+    memset(seos->text_store, 0, sizeof(seos->text_store));
+}
+
+bool seos_credential_save(Seos* seos, const char* dev_name) {
+    bool saved = false;
+    FlipperFormat* file = flipper_format_file_alloc(seos->storage);
+    FuriString* temp_str = furi_string_alloc();
+    bool use_load_path = true;
+
+    do {
+        if(use_load_path && !furi_string_empty(seos->load_path)) {
+            // Get directory name
+            path_extract_dirname(furi_string_get_cstr(seos->load_path), temp_str);
+            // Make path to file to save
+            furi_string_cat_printf(temp_str, "/%s%s", dev_name, SEOS_APP_EXTENSION);
+        } else {
+            // First remove file if it was saved
+            furi_string_printf(
+                temp_str, "%s/%s%s", STORAGE_APP_DATA_PATH_PREFIX, dev_name, SEOS_APP_EXTENSION);
+        }
+
+        // Open file
+        if(!flipper_format_file_open_always(file, furi_string_get_cstr(temp_str))) break;
+
+        // Write header
+        if(!flipper_format_write_header_cstr(file, seos_file_header, seos_file_version)) break;
+
+        if(!flipper_format_write_uint32(
+               file, "Diversifier Length", (uint32_t*)&(seos->credential.diversifier_len), 1))
+            break;
+        if(!flipper_format_write_hex(
+               file, "Diversifier", seos->credential.diversifier, seos->credential.diversifier_len))
+            break;
+        if(!flipper_format_write_uint32(
+               file, "SIO Length", (uint32_t*)&(seos->credential.sio_len), 1))
+            break;
+        if(!flipper_format_write_hex(file, "SIO", seos->credential.sio, seos->credential.sio_len))
+            break;
+
+        saved = true;
+    } while(false);
+
+    if(!saved) {
+        dialog_message_show_storage_error(seos->dialogs, "Can not save\nfile");
+    }
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+    return saved;
+}
+
+static const NotificationSequence seos_sequence_blink_start_blue = {
+    &message_blink_start_10,
+    &message_blink_set_color_blue,
+    &message_do_not_reset,
+    NULL,
+};
+
+static const NotificationSequence seos_sequence_blink_stop = {
+    &message_blink_stop,
+    NULL,
+};
+
+void seos_blink_start(Seos* seos) {
+    notification_message(seos->notifications, &seos_sequence_blink_start_blue);
+}
+
+void seos_blink_stop(Seos* seos) {
+    notification_message(seos->notifications, &seos_sequence_blink_stop);
+}
+
+void seos_show_loading_popup(void* context, bool show) {
+    Seos* seos = context;
+
+    if(show) {
+        // Raise timer priority so that animations can play
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+        view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewLoading);
+    } else {
+        // Restore default timer priority
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
+    }
+}
+
+int32_t seos_app(void* p) {
+    UNUSED(p);
+    Seos* seos = seos_alloc();
+
+    if(seos->keys_loaded) {
+        scene_manager_next_scene(seos->scene_manager, SeosSceneMainMenu);
+    } else {
+        scene_manager_next_scene(seos->scene_manager, SeosSceneZeroKeys);
+    }
+
+    view_dispatcher_run(seos->view_dispatcher);
+
+    seos_free(seos);
+
+    return 0;
+}

+ 5 - 0
seos/seos.h

@@ -0,0 +1,5 @@
+#pragma once
+
+typedef struct Seos Seos;
+
+bool seos_credential_save(Seos* seos, const char* dev_name);

binární
seos/seos.png


+ 492 - 0
seos/seos_att.c

@@ -0,0 +1,492 @@
+#include "seos_att_i.h"
+
+#define TAG "SeosAtt"
+
+struct att_read_by_type_req {
+    uint8_t opcode;
+    uint16_t start_handle;
+    uint16_t end_handle;
+    union {
+        uint16_t short_uuid;
+        uint8_t long_uuid[16];
+    } attribute_type;
+} __packed;
+
+struct att_find_info_req {
+    uint8_t opcode;
+    uint16_t start_handle;
+    uint16_t end_handle;
+} __packed;
+
+struct att_find_type_value_req {
+    uint8_t opcode;
+    uint16_t start_handle;
+    uint16_t end_handle;
+    uint16_t att_type;
+    uint8_t att_value[0];
+} __packed;
+
+struct att_write_req {
+    uint8_t opcode;
+    uint16_t handle;
+} __packed;
+
+static uint8_t seos_reader_service_backwards[] =
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00};
+static uint8_t seos_cred_service_backwards[] =
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x01, 0x98, 0x00, 0x00};
+
+SeosAtt* seos_att_alloc(Seos* seos) {
+    SeosAtt* seos_att = malloc(sizeof(SeosAtt));
+    memset(seos_att, 0, sizeof(SeosAtt));
+
+    seos_att->seos = seos;
+    seos_att->seos_l2cap = seos_l2cap_alloc(seos);
+    seos_l2cap_set_receive_callback(seos_att->seos_l2cap, seos_att_process_payload, seos_att);
+    seos_l2cap_set_central_connection_callback(
+        seos_att->seos_l2cap, seos_att_central_connection_start, seos_att);
+
+    seos_att->tx_buf = bit_buffer_alloc(128);
+
+    seos_att->tx_mtu = 0;
+    seos_att->rx_mtu = 0x0200;
+
+    return seos_att;
+}
+
+void seos_att_free(SeosAtt* seos_att) {
+    furi_assert(seos_att);
+
+    seos_l2cap_free(seos_att->seos_l2cap);
+
+    bit_buffer_free(seos_att->tx_buf);
+    free(seos_att);
+}
+
+void seos_att_start(SeosAtt* seos_att, BleMode mode, FlowMode flow_mode) {
+    seos_att->ble_mode = mode;
+    seos_att->flow_mode = flow_mode;
+    seos_l2cap_start(seos_att->seos_l2cap, mode, flow_mode);
+}
+
+void seos_att_stop(SeosAtt* seos_att) {
+    seos_l2cap_stop(seos_att->seos_l2cap);
+}
+
+void seos_att_central_connection_start(void* context) {
+    SeosAtt* seos_att = (SeosAtt*)context;
+    FURI_LOG_D(TAG, "central connnection start");
+    bit_buffer_reset(seos_att->tx_buf);
+
+    if(seos_att->flow_mode == FLOW_READER) {
+        uint16_t start = 0x0001;
+        uint16_t end = 0xffff;
+        uint16_t attribute_type = PRIMARY_SERVICE;
+
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_FIND_BY_TYPE_VALUE_REQ);
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)&start, sizeof(start));
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)&end, sizeof(end));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&attribute_type, sizeof(attribute_type));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, seos_cred_service_backwards, sizeof(seos_cred_service_backwards));
+    } else if(seos_att->flow_mode == FLOW_CRED) {
+        // First thing to do with any new connection is the MTU
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_EXCHANGE_MTU_REQ);
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&seos_att->rx_mtu, sizeof(seos_att->rx_mtu));
+    }
+
+    seos_l2cap_send(seos_att->seos_l2cap, seos_att->tx_buf);
+}
+
+void seos_att_notify(SeosAtt* seos_att, uint16_t handle, BitBuffer* content) {
+    seos_log_bitbuffer(TAG, "notify", content);
+    size_t content_len = bit_buffer_get_size_bytes(content);
+
+    BitBuffer* tx = bit_buffer_alloc(1 + sizeof(handle) + content_len);
+
+    bit_buffer_append_byte(tx, ATT_HANDLE_VALUE_NTF);
+    bit_buffer_append_bytes(tx, (uint8_t*)&handle, sizeof(handle));
+    bit_buffer_append_bytes(tx, bit_buffer_get_data(content), content_len);
+
+    seos_l2cap_send(seos_att->seos_l2cap, tx);
+    bit_buffer_free(tx);
+}
+
+void seos_att_write_request(SeosAtt* seos_att, BitBuffer* content) {
+    seos_log_bitbuffer(TAG, "write_request", content);
+    size_t content_len = bit_buffer_get_size_bytes(content);
+
+    BitBuffer* tx = bit_buffer_alloc(1 + sizeof(seos_att->value_handle) + content_len);
+
+    bit_buffer_append_byte(tx, ATT_WRITE_CMD);
+    bit_buffer_append_bytes(
+        tx, (uint8_t*)&(seos_att->value_handle), sizeof(seos_att->value_handle));
+    bit_buffer_append_bytes(tx, bit_buffer_get_data(content), content_len);
+
+    seos_l2cap_send(seos_att->seos_l2cap, tx);
+    bit_buffer_free(tx);
+}
+
+// TODO: figure out the proper name for data that comes in
+void seos_att_process_payload(void* context, BitBuffer* message) {
+    SeosAtt* seos_att = (SeosAtt*)context;
+    // seos_log_bitbuffer(TAG, "process payload", message);
+
+    bit_buffer_reset(seos_att->tx_buf);
+    const uint8_t* data = bit_buffer_get_data(message);
+    uint8_t opcode = data[0];
+    struct att_read_by_type_req* s;
+    uint16_t* start_handle;
+    uint16_t* end_handle;
+    uint16_t attribute_type;
+    size_t length = 0;
+
+    if(seos_att->ble_mode == BLE_CENTRAL && ((opcode & 0x01) == 0x00)) {
+        bool reject = true;
+        FURI_LOG_I(
+            TAG,
+            "%s request(0x%02x) when operating as Central",
+            reject ? "Rejecting" : "Ignoring",
+            opcode);
+        if(reject) {
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_ERROR_RSP);
+            bit_buffer_append_byte(seos_att->tx_buf, opcode);
+            bit_buffer_append_bytes(seos_att->tx_buf, data + 1, sizeof(uint16_t));
+            bit_buffer_append_byte(seos_att->tx_buf, 0x0a);
+            seos_l2cap_send(seos_att->seos_l2cap, seos_att->tx_buf);
+        }
+        return;
+    }
+
+    switch(opcode) {
+    case ATT_ERROR_RSP:
+        uint8_t error_with_opcode = data[1];
+        FURI_LOG_W(TAG, "Error with command %02x", error_with_opcode);
+        break;
+    case ATT_EXCHANGE_MTU_REQ: // MTU request
+
+        // Trying a new way to copy the uint16_t
+        seos_att->tx_mtu = *(uint16_t*)(data + 1);
+        FURI_LOG_D(TAG, "MTU REQ = %04x", seos_att->tx_mtu);
+
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_EXCHANGE_MTU_RSP);
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&seos_att->rx_mtu, sizeof(seos_att->rx_mtu));
+        break;
+    case ATT_EXCHANGE_MTU_RSP:
+        seos_att->tx_mtu = *(uint16_t*)(data + 1);
+        FURI_LOG_D(TAG, "MTU RSP = %04x", seos_att->tx_mtu);
+
+        uint16_t start = 0x0001;
+        uint16_t end = 0xffff;
+        attribute_type = PRIMARY_SERVICE;
+
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_FIND_BY_TYPE_VALUE_REQ);
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)&start, sizeof(start));
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)&end, sizeof(end));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&attribute_type, sizeof(attribute_type));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf,
+            seos_reader_service_backwards,
+            sizeof(seos_reader_service_backwards));
+        break;
+    case ATT_READ_BY_TYPE_REQ:
+        s = (struct att_read_by_type_req*)(data);
+        FURI_LOG_D(
+            TAG,
+            "ATT read by type %04x - %04x, type %04x",
+            s->start_handle,
+            s->end_handle,
+            s->attribute_type.short_uuid);
+
+        if(s->attribute_type.short_uuid == CHARACTERISTIC) {
+            if(s->start_handle == 0x0006) {
+                bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_TYPE_RSP);
+                uint8_t response[] = {0x07, 0x07, 0x00, 0x20, 0x08, 0x00, 0x05, 0x2a};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+
+            } else if(s->start_handle == 0x000a) {
+                bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_TYPE_RSP);
+                uint8_t flow_mode_byte = seos_att->flow_mode == FLOW_READER ? 0x00 : 0x01;
+                uint8_t response[] = {0x15, 0x0b, 0x00,           0x14, 0x0c, 0x00, 0x02, 0x00,
+                                      0x00, 0x7a, 0x17,           0x00, 0x00, 0x80, 0x00, 0x10,
+                                      0x00, 0x00, flow_mode_byte, 0xaa, 0x00, 0x00};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            }
+        } else if(s->attribute_type.short_uuid == DEVICE_NAME) {
+            if(s->start_handle == 0x0001) {
+                uint8_t response[] = {0x09, 0x03, 0x00, 0x46, 0x6c, 0x69, 0x70, 0x70, 0x65, 0x72};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            }
+        }
+
+        if(bit_buffer_get_size_bytes(seos_att->tx_buf) == 0) {
+            FURI_LOG_W(TAG, "Return error");
+            bit_buffer_reset(seos_att->tx_buf);
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_ERROR_RSP);
+            bit_buffer_append_byte(seos_att->tx_buf, opcode);
+            bit_buffer_append_bytes(
+                seos_att->tx_buf, (uint8_t*)&s->start_handle, sizeof(s->start_handle));
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_ERROR_CODE_ATTRIBUTE_NOT_FOUND);
+        }
+        break;
+    case ATT_READ_BY_TYPE_RSP:
+        seos_log_buffer(
+            TAG, "ATT_READ_BY_TYPE_RSP", (uint8_t*)data, bit_buffer_get_size_bytes(message));
+
+        uint16_t* handle;
+        handle = (uint16_t*)(data + 2);
+        seos_att->characteristic_handle = *handle;
+        // skip properties byte
+        handle = (uint16_t*)(data + 5);
+        seos_att->value_handle = *handle;
+
+        *handle = *handle + 1;
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_FIND_INFORMATION_REQ);
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)handle, sizeof(uint16_t));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&seos_att->service_end_handle, sizeof(uint16_t));
+        break;
+    case ATT_READ_BY_GROUP_TYPE_REQ:
+        s = (struct att_read_by_type_req*)(data);
+        FURI_LOG_D(
+            TAG,
+            "ATT read by group type %04x - %04x, type %04x",
+            s->start_handle,
+            s->end_handle,
+            s->attribute_type.short_uuid);
+        if(s->attribute_type.short_uuid == PRIMARY_SERVICE) {
+            if(s->start_handle == 0x0001) {
+                bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_GROUP_TYPE_RSP);
+                uint8_t response[] = {
+                    0x06, 0x01, 0x00, 0x05, 0x00, 0x00, 0x18, 0x06, 0x00, 0x09, 0x00, 0x01, 0x18};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            } else if(s->start_handle == 0x000a) {
+                bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_GROUP_TYPE_RSP);
+                uint8_t flow_mode_byte = seos_att->flow_mode == FLOW_READER ? 0x00 : 0x01;
+                uint8_t response[] = {0x14, 0x0a, 0x00, 0x0d,           0x00, 0x02, 0x00,
+                                      0x00, 0x7a, 0x17, 0x00,           0x00, 0x80, 0x00,
+                                      0x10, 0x00, 0x00, flow_mode_byte, 0x98, 0x00, 0x00};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            }
+        }
+
+        // Didn't match either attribute_type or start_handle
+        if(bit_buffer_get_size_bytes(seos_att->tx_buf) == 0) {
+            FURI_LOG_W(TAG, "Return error");
+            bit_buffer_reset(seos_att->tx_buf);
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_ERROR_RSP);
+            bit_buffer_append_byte(seos_att->tx_buf, opcode);
+            bit_buffer_append_bytes(
+                seos_att->tx_buf, (uint8_t*)&s->start_handle, sizeof(s->start_handle));
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_ERROR_CODE_ATTRIBUTE_NOT_FOUND);
+        }
+        break;
+    case ATT_READ_BY_GROUP_TYPE_RSP:
+        seos_log_buffer(
+            TAG, "ATT_READ_BY_GROUP_TYPE_RSP", (uint8_t*)data, bit_buffer_get_size_bytes(message));
+        // NOTE: this might not be actively used
+
+        uint8_t size = data[1];
+        size_t i = 2;
+        do {
+            start_handle = (uint16_t*)(data + i);
+            end_handle = (uint16_t*)(data + sizeof(uint16_t) + i);
+
+            // +4 for 2 uint16_t
+            if(size == (sizeof(seos_cred_service_backwards) + 4)) {
+                if(memcmp(
+                       seos_cred_service_backwards,
+                       data + i + 4,
+                       sizeof(seos_cred_service_backwards)) == 0) {
+                    seos_att->service_start_handle = *start_handle;
+                    seos_att->service_end_handle = *end_handle;
+                }
+            }
+            i += size;
+        } while(i < bit_buffer_get_size_bytes(message));
+        *end_handle = *end_handle + 1;
+
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_GROUP_TYPE_REQ);
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)end_handle, sizeof(uint16_t));
+        uint8_t suffix[] = {0xff, 0xff, 0x00, 0x28};
+        bit_buffer_append_bytes(seos_att->tx_buf, suffix, sizeof(suffix));
+
+        break;
+    case ATT_FIND_INFORMATION_REQ:
+        struct att_find_info_req* e = (struct att_find_info_req*)(data);
+        FURI_LOG_D(TAG, "ATT find information %04x - %04x", e->start_handle, e->end_handle);
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_FIND_INFORMATION_RSP);
+
+        if(e->start_handle == 0x0009) {
+            uint8_t response[] = {0x01, 0x09, 0x00, 0x02, 0x29};
+            bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+        } else if(e->start_handle == 0x000d) {
+            uint8_t response[] = {0x01, 0x0d, 0x00, 0x02, 0x29};
+            bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+        } else {
+            FURI_LOG_W(TAG, "unhandled handle in ATT_FIND_INFORMATION_REQ");
+        }
+        break;
+    case ATT_FIND_INFORMATION_RSP:
+        seos_log_buffer(
+            TAG, "ATT_FIND_INFORMATION_RSP", (uint8_t*)data, bit_buffer_get_size_bytes(message));
+
+        // 05 01 3c00 0029 3d00 0229
+        uint8_t format = data[1];
+        if(format == 1) { // short UUID
+            uint16_t* handle;
+            uint16_t* uuid;
+            size_t i = 2;
+            do {
+                handle = (uint16_t*)(data + i);
+                uuid = (uint16_t*)(data + i + 2);
+                i += 4;
+                if(*uuid == CCCD) {
+                    seos_att->cccd_handle = *handle;
+                }
+            } while(i < bit_buffer_get_size_bytes(message));
+
+            if(seos_att->cccd_handle > 0) {
+                FURI_LOG_I(TAG, "Subscribe to phone/device");
+                uint16_t value = ENABLE_NOTIFICATION_VALUE;
+                bit_buffer_append_byte(seos_att->tx_buf, ATT_WRITE_CMD);
+                bit_buffer_append_bytes(
+                    seos_att->tx_buf, (uint8_t*)&seos_att->cccd_handle, sizeof(uint16_t));
+                bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)&value, sizeof(value));
+            }
+        }
+
+        break;
+    case ATT_FIND_BY_TYPE_VALUE_REQ:
+        struct att_find_type_value_req* t = (struct att_find_type_value_req*)(data);
+        FURI_LOG_D(
+            TAG,
+            "ATT_FIND_BY_TYPE_VALUE_REQ %04x - %04x for %04x",
+            t->start_handle,
+            t->end_handle,
+            t->att_type);
+        if(t->att_type == PRIMARY_SERVICE) {
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_FIND_BY_TYPE_VALUE_RSP);
+            if(seos_att->flow_mode == FLOW_CRED) {
+                uint8_t response[] = {0x0a, 0x00, 0x0e, 0x00};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            } else if(seos_att->flow_mode == FLOW_READER) {
+                uint8_t response[] = {0x0c, 0x00, 0x0e, 0x00};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            }
+        }
+        break;
+    case ATT_FIND_BY_TYPE_VALUE_RSP:
+        seos_log_buffer(
+            TAG, "ATT_FIND_BY_TYPE_VALUE_RSP", (uint8_t*)data, bit_buffer_get_size_bytes(message));
+        start_handle = (uint16_t*)(data + 1);
+        end_handle = (uint16_t*)(data + 3);
+
+        seos_att->service_start_handle = *start_handle;
+        seos_att->service_end_handle = *end_handle;
+
+        attribute_type = CHARACTERISTIC;
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_TYPE_REQ);
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)start_handle, sizeof(uint16_t));
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)end_handle, sizeof(uint16_t));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&attribute_type, sizeof(attribute_type));
+        break;
+    case ATT_WRITE_REQ:
+        struct att_write_req* w = (struct att_write_req*)(data);
+        length = bit_buffer_get_size_bytes(message) - sizeof(struct att_write_req);
+        FURI_LOG_D(TAG, "ATT Write Req %d bytes to %04x", length, w->handle);
+        if(w->handle == 0x0009) {
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_WRITE_RSP);
+        } else if(w->handle == 0x000d) {
+            uint16_t* value = (uint16_t*)(data + sizeof(struct att_write_req));
+            if(*value == DISABLE_NOTIFICATION_VALUE) {
+                FURI_LOG_I(TAG, "Unsubscribe");
+            } else if(*value == ENABLE_NOTIFICATION_VALUE) {
+                FURI_LOG_I(TAG, "Subscribe");
+                if(seos_att->on_subscribe) {
+                    // comes in as 0x000d, but we need to use 0x000c: I'm sure there is a reason for this that I'm just not aware of
+                    seos_att->on_subscribe(seos_att->on_subscribe_context, w->handle - 1);
+                    bit_buffer_append_byte(seos_att->tx_buf, ATT_WRITE_RSP);
+                } else {
+                    FURI_LOG_W(TAG, "No on_subscribe callback defined");
+                }
+            }
+        }
+        break;
+    case ATT_WRITE_RSP:
+        FURI_LOG_D(TAG, "ATT_WRITE_RSP");
+        break;
+    case ATT_WRITE_CMD:
+        struct att_write_req* c = (struct att_write_req*)(data);
+        length = bit_buffer_get_size_bytes(message) - sizeof(struct att_write_req);
+        FURI_LOG_D(TAG, "ATT Write CMD %d bytes to %04x", length, c->handle);
+
+        if(c->handle == 0x000c) {
+            BitBuffer* attribute_value = bit_buffer_alloc(length);
+            bit_buffer_append_bytes(
+                attribute_value,
+                bit_buffer_get_data(message) + sizeof(struct att_write_req),
+                length);
+            if(seos_att->write_request) {
+                seos_att->write_request(seos_att->write_request_context, attribute_value);
+            }
+            bit_buffer_free(attribute_value);
+        } else {
+            seos_log_bitbuffer(TAG, "write to unsupported handle", message);
+        }
+        // No response to CMD expected
+        break;
+    case ATT_HANDLE_VALUE_NTF:
+        struct att_write_req* n = (struct att_write_req*)(data);
+        length = bit_buffer_get_size_bytes(message) - sizeof(struct att_write_req);
+        FURI_LOG_D(TAG, "ATT handle value notify %d bytes to %04x", length, n->handle);
+        if(n->handle == 0x000d) {
+            if(seos_att->notify) {
+                seos_att->notify(
+                    seos_att->notify_context,
+                    bit_buffer_get_data(message) + sizeof(struct att_write_req),
+                    length);
+            }
+        } else {
+            FURI_LOG_W(TAG, "Notify with unhandled handle");
+        }
+        break;
+    case ATT_HANDLE_VALUE_CFM:
+        FURI_LOG_I(TAG, "Indication confirmation");
+        break;
+    default:
+        FURI_LOG_W(TAG, "seos_att_process_message no handler for 0x%02x", opcode);
+        break;
+    }
+
+    if(bit_buffer_get_size_bytes(seos_att->tx_buf) > 0) {
+        seos_log_bitbuffer(TAG, "sending", seos_att->tx_buf);
+        seos_l2cap_send(seos_att->seos_l2cap, seos_att->tx_buf);
+    }
+}
+
+void seos_att_set_on_subscribe_callback(
+    SeosAtt* seos_att,
+    SeosAttOnSubscribeCallback callback,
+    void* context) {
+    seos_att->on_subscribe = callback;
+    seos_att->on_subscribe_context = context;
+}
+
+void seos_att_set_write_request_callback(
+    SeosAtt* seos_att,
+    SeosAttWriteRequestCallback callback,
+    void* context) {
+    seos_att->write_request = callback;
+    seos_att->write_request_context = context;
+}
+
+void seos_att_set_notify_callback(SeosAtt* seos_att, SeosAttNotifyCallback callback, void* context) {
+    seos_att->notify = callback;
+    seos_att->notify_context = context;
+}

+ 100 - 0
seos/seos_att.h

@@ -0,0 +1,100 @@
+#pragma once
+
+#include <furi.h>
+#include <lib/toolbox/bit_buffer.h>
+
+#include "seos_l2cap.h"
+#include "seos_common.h"
+
+#define ATT_ERROR_RSP              0x01
+#define ATT_EXCHANGE_MTU_REQ       0x02
+#define ATT_EXCHANGE_MTU_RSP       0x03
+#define ATT_FIND_INFORMATION_REQ   0x04
+#define ATT_FIND_INFORMATION_RSP   0x05
+#define ATT_FIND_BY_TYPE_VALUE_REQ 0x06
+#define ATT_FIND_BY_TYPE_VALUE_RSP 0x07
+#define ATT_READ_BY_TYPE_REQ       0x08
+#define ATT_READ_BY_TYPE_RSP       0x09
+#define ATT_READ_REQ               0x0a
+#define ATT_READ_RSP               0x0b
+#define ATT_READ_BLOB_REQ          0x0c
+#define ATT_READ_BLOB_RSP          0x0d
+#define ATT_READ_MULTIPLE_REQ      0x0e
+#define ATT_READ_MULTIPLE_RSP      0x0f
+#define ATT_READ_BY_GROUP_TYPE_REQ 0x10
+#define ATT_READ_BY_GROUP_TYPE_RSP 0x11
+#define ATT_WRITE_REQ              0x12
+#define ATT_WRITE_RSP              0x13
+#define ATT_HANDLE_VALUE_NTF       0x1b
+#define ATT_HANDLE_VALUE_IND       0x1d
+#define ATT_HANDLE_VALUE_CFM       0x1e
+
+#define ATT_READ_MULTIPLE_VARIABLE_REQ 0x20
+#define ATT_READ_MULTIPLE_VARIABLE_RSP 0x21
+
+#define ATT_WRITE_CMD 0x52
+
+#define ATT_ERROR_CODE_ATTRIBUTE_NOT_FOUND   0x0a
+#define ATT_ERROR_CODE_INVALID_HANDLE        0x01
+#define ATT_ERROR_CODE_REQUEST_NOT_SUPPORTED 0x06
+
+#define PRIMARY_SERVICE 0x2800
+#define CHARACTERISTIC  0x2803
+#define CCCD            0x2902
+#define DEVICE_NAME     0x2a00
+// service_changed = 2a05
+
+#define DISABLE_NOTIFICATION_VALUE 0x0000
+#define ENABLE_NOTIFICATION_VALUE  0x0001
+#define ENABLE_INDICATION_VALUE    0x0002
+
+typedef void (*SeosAttOnSubscribeCallback)(void* context, uint16_t handle);
+typedef void (*SeosAttWriteRequestCallback)(void* context, BitBuffer* attribute_value);
+typedef void (*SeosAttNotifyCallback)(void* context, const uint8_t* buffer, size_t buffer_len);
+
+typedef struct {
+    Seos* seos;
+    SeosL2Cap* seos_l2cap;
+    BitBuffer* tx_buf;
+    uint16_t tx_mtu;
+    uint16_t rx_mtu;
+
+    SeosAttOnSubscribeCallback on_subscribe;
+    void* on_subscribe_context;
+
+    SeosAttWriteRequestCallback write_request;
+    void* write_request_context;
+
+    SeosAttNotifyCallback notify;
+    void* notify_context;
+
+    BleMode ble_mode;
+    FlowMode flow_mode;
+
+    uint16_t service_start_handle;
+    uint16_t service_end_handle;
+    uint16_t characteristic_handle;
+    uint16_t value_handle;
+    uint16_t cccd_handle;
+} SeosAtt;
+
+SeosAtt* seos_att_alloc(Seos* seos);
+void seos_att_free(SeosAtt* seos_att);
+void seos_att_start(SeosAtt* seos_att, BleMode mode, FlowMode flow_mode);
+void seos_att_stop(SeosAtt* seos_att);
+void seos_att_notify(SeosAtt* seos_att, uint16_t handle, BitBuffer* content);
+void seos_att_write_request(SeosAtt* seos_att, BitBuffer* content);
+void seos_att_process_payload(void* context, BitBuffer* message);
+void seos_att_central_connection_start(void* context);
+
+void seos_att_set_on_subscribe_callback(
+    SeosAtt* seos_att,
+    SeosAttOnSubscribeCallback callback,
+    void* context);
+
+void seos_att_set_write_request_callback(
+    SeosAtt* seos_att,
+    SeosAttWriteRequestCallback callback,
+    void* context);
+
+void seos_att_set_notify_callback(SeosAtt* seos_att, SeosAttNotifyCallback callback, void* context);

+ 4 - 0
seos/seos_att_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_att.h"

+ 176 - 0
seos/seos_central.c

@@ -0,0 +1,176 @@
+#include "seos_central_i.h"
+
+#define TAG "SeosCentral"
+
+static uint8_t success[] = {0x90, 0x00};
+static uint8_t file_not_found[] = {0x6A, 0x82};
+
+static uint8_t select_header[] = {0x00, 0xa4, 0x04, 0x00};
+static uint8_t standard_seos_aid[] = {0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+static uint8_t select_adf_header[] = {0x80, 0xa5, 0x04, 0x00};
+static uint8_t general_authenticate_1[] =
+    {0x00, 0x87, 0x00, 0x01, 0x04, 0x7c, 0x02, 0x81, 0x00, 0x00};
+static uint8_t general_authenticate_2_header[] = {0x00, 0x87, 0x00, 0x01};
+static uint8_t secure_messaging_header[] = {0x0c, 0xcb, 0x3f, 0xff};
+
+SeosCentral* seos_central_alloc(Seos* seos) {
+    SeosCentral* seos_central = malloc(sizeof(SeosCentral));
+    memset(seos_central, 0, sizeof(SeosCentral));
+    seos_central->seos = seos;
+    seos_central->credential = &seos->credential;
+
+    seos_central->phase = SELECT_AID;
+    // Using DES for greater compatibilty
+    seos_central->params.cipher = TWO_KEY_3DES_CBC_MODE;
+    seos_central->params.hash = SHA1;
+
+    memset(seos_central->params.rndICC, 0x0d, sizeof(seos_central->params.rndICC));
+    memset(seos_central->params.rNonce, 0x0c, sizeof(seos_central->params.rNonce));
+
+    seos_central->secure_messaging = NULL;
+
+    seos_central->seos_att = seos_att_alloc(seos);
+    seos_att_set_notify_callback(seos_central->seos_att, seos_central_notify, seos_central);
+
+    return seos_central;
+}
+
+void seos_central_free(SeosCentral* seos_central) {
+    furi_assert(seos_central);
+    seos_att_free(seos_central->seos_att);
+    free(seos_central);
+}
+
+void seos_central_start(SeosCentral* seos_central, FlowMode mode) {
+    seos_att_start(seos_central->seos_att, BLE_CENTRAL, mode);
+}
+
+void seos_central_stop(SeosCentral* seos_central) {
+    seos_att_stop(seos_central->seos_att);
+}
+
+void seos_central_notify(void* context, const uint8_t* buffer, size_t buffer_len) {
+    SeosCentral* seos_central = (SeosCentral*)context;
+    seos_log_buffer(TAG, "notify", (uint8_t*)buffer, buffer_len);
+    const uint8_t* data = buffer;
+    if(data[0] == 0xe1) {
+        FURI_LOG_W(TAG, "Reader BLE Error code: %02x", data[1]);
+        return;
+    }
+
+    BitBuffer* response = bit_buffer_alloc(128);
+
+    if(data[0] != BLE_START) {
+        FURI_LOG_W(TAG, "Unexpected start of BLE packet");
+    }
+    const uint8_t* apdu = data + 1; // Match name to nfc version for easier copying
+
+    if(memcmp(apdu, select_header, sizeof(select_header)) == 0) {
+        if(memcmp(apdu + sizeof(select_header) + 1, standard_seos_aid, sizeof(standard_seos_aid)) ==
+           0) {
+            bit_buffer_append_byte(response, BLE_START);
+            seos_emulator_select_aid(response);
+            bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+            seos_central->phase = SELECT_ADF;
+        } else {
+            bit_buffer_append_byte(response, BLE_START);
+            bit_buffer_append_bytes(response, (uint8_t*)file_not_found, sizeof(file_not_found));
+        }
+    } else if(memcmp(apdu, select_adf_header, sizeof(select_adf_header)) == 0) {
+        // is our adf in the list?
+        // +1 to skip APDU length byte
+        void* p = memmem(
+            apdu + sizeof(select_adf_header) + 1,
+            apdu[sizeof(select_adf_header)],
+            SEOS_ADF_OID,
+            SEOS_ADF_OID_LEN);
+        if(p) {
+            seos_log_buffer(TAG, "Matched ADF", p, SEOS_ADF_OID_LEN);
+
+            bit_buffer_append_byte(response, BLE_START);
+
+            seos_emulator_select_adf(&seos_central->params, seos_central->credential, response);
+
+            bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+            seos_central->phase = GENERAL_AUTHENTICATION_1;
+        } else {
+            FURI_LOG_W(TAG, "Failed to match any ADF OID");
+        }
+    } else if(memcmp(apdu, general_authenticate_1, sizeof(general_authenticate_1)) == 0) {
+        bit_buffer_append_byte(response, BLE_START);
+
+        seos_emulator_general_authenticate_1(response, seos_central->params);
+
+        bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+        seos_central->phase = GENERAL_AUTHENTICATION_2;
+    } else if(memcmp(apdu, general_authenticate_2_header, sizeof(general_authenticate_2_header)) == 0) {
+        bit_buffer_append_byte(response, BLE_START);
+
+        if(seos_emulator_general_authenticate_2(
+               apdu, buffer_len - 1, seos_central->credential, &seos_central->params, response)) {
+            FURI_LOG_I(TAG, "Authenticated");
+
+            view_dispatcher_send_custom_event(
+                seos_central->seos->view_dispatcher, SeosCustomEventAuthenticated);
+            seos_central->secure_messaging = secure_messaging_alloc(&seos_central->params);
+            bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+        } else {
+            bit_buffer_reset(response);
+        }
+        seos_central->phase = REQUEST_SIO;
+    } else if(memcmp(apdu, secure_messaging_header, sizeof(secure_messaging_header)) == 0) {
+        uint8_t request_sio[] = {0x5c, 0x02, 0xff, 0x00};
+
+        bit_buffer_append_byte(response, BLE_START);
+
+        if(seos_central->secure_messaging) {
+            FURI_LOG_D(TAG, "Unwrap secure message");
+
+            // c0 0ccb3fff 16 8508fa8395d30de4e8e097008e085da7edbd833b002d00
+            // Ignore 1 BLE_START byte
+            size_t bytes_to_ignore = 1;
+            BitBuffer* tmp = bit_buffer_alloc(buffer_len);
+            bit_buffer_append_bytes(tmp, buffer + bytes_to_ignore, buffer_len - bytes_to_ignore);
+
+            seos_log_bitbuffer(TAG, "NFC received(wrapped)", tmp);
+            secure_messaging_unwrap_apdu(seos_central->secure_messaging, tmp);
+            seos_log_bitbuffer(TAG, "NFC received(clear)", tmp);
+
+            const uint8_t* message = bit_buffer_get_data(tmp);
+            if(memcmp(message, request_sio, sizeof(request_sio)) == 0) {
+                view_dispatcher_send_custom_event(
+                    seos_central->seos->view_dispatcher, SeosCustomEventSIORequested);
+                BitBuffer* sio_file = bit_buffer_alloc(128);
+                bit_buffer_append_bytes(sio_file, message + 2, 2); // fileId
+                bit_buffer_append_byte(sio_file, seos_central->credential->sio_len);
+                bit_buffer_append_bytes(
+                    sio_file, seos_central->credential->sio, seos_central->credential->sio_len);
+
+                seos_log_bitbuffer(TAG, "sio_file", sio_file);
+                secure_messaging_wrap_rapdu(
+                    seos_central->secure_messaging,
+                    (uint8_t*)bit_buffer_get_data(sio_file),
+                    bit_buffer_get_size_bytes(sio_file),
+                    response);
+
+                bit_buffer_free(sio_file);
+            } else {
+                FURI_LOG_W(TAG, "Did not match the cleartext request");
+            }
+            bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+
+            bit_buffer_free(tmp);
+        } else {
+            uint8_t no_sm[] = {0x69, 0x88};
+            bit_buffer_append_bytes(response, no_sm, sizeof(no_sm));
+        }
+
+    } else {
+        FURI_LOG_W(TAG, "no match for attribute_value");
+    }
+
+    if(bit_buffer_get_size_bytes(response) > 0) {
+        seos_att_write_request(seos_central->seos_att, response);
+    }
+    bit_buffer_free(response);
+}

+ 33 - 0
seos/seos_central.h

@@ -0,0 +1,33 @@
+#pragma once
+
+#include <furi.h>
+#include <lib/toolbox/bit_buffer.h>
+
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+
+#include "secure_messaging.h"
+#include "seos_common.h"
+#include "seos.h"
+#include "seos_att.h"
+#include "keys.h"
+
+#define BLE_START 0xc0
+
+typedef struct {
+    Seos* seos;
+    SeosAtt* seos_att;
+
+    SeosPhase phase;
+
+    AuthParameters params;
+    SecureMessaging* secure_messaging;
+
+    SeosCredential* credential;
+} SeosCentral;
+
+SeosCentral* seos_central_alloc(Seos* seos);
+void seos_central_free(SeosCentral* seos_central);
+void seos_central_start(SeosCentral* seos_central, FlowMode mode);
+void seos_central_stop(SeosCentral* seos_central);
+void seos_central_notify(void* context, const uint8_t* buffer, size_t buffer_len);

+ 4 - 0
seos/seos_central_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_central.h"
+#include "seos_i.h"

+ 346 - 0
seos/seos_characteristic.c

@@ -0,0 +1,346 @@
+#include "seos_characteristic_i.h"
+
+#define TAG "SeosCharacteristic"
+
+static uint8_t standard_seos_aid[] = {0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+static uint8_t general_authenticate_1[] =
+    {0x00, 0x87, 0x00, 0x01, 0x04, 0x7c, 0x02, 0x81, 0x00, 0x00};
+static uint8_t cd02[] = {0xcd, 0x02};
+
+static uint8_t ga1_response[] = {0x7c, 0x0a, 0x81, 0x08};
+
+// Emulation
+static uint8_t success[] = {0x90, 0x00};
+static uint8_t file_not_found[] = {0x6A, 0x82};
+
+static uint8_t select_header[] = {0x00, 0xa4, 0x04, 0x00};
+static uint8_t select_adf_header[] = {0x80, 0xa5, 0x04, 0x00};
+static uint8_t general_authenticate_2_header[] = {0x00, 0x87, 0x00, 0x01};
+static uint8_t secure_messaging_header[] = {0x0c, 0xcb, 0x3f, 0xff};
+
+SeosCharacteristic* seos_characteristic_alloc(Seos* seos) {
+    SeosCharacteristic* seos_characteristic = malloc(sizeof(SeosCharacteristic));
+    memset(seos_characteristic, 0, sizeof(SeosCharacteristic));
+    seos_characteristic->seos = seos;
+    seos_characteristic->credential = &seos->credential;
+
+    seos_characteristic->phase = SELECT_AID;
+    seos_characteristic->secure_messaging = NULL;
+
+    seos_characteristic->params.key_no = 1;
+    memset(seos_characteristic->params.cNonce, 0x0c, sizeof(seos_characteristic->params.cNonce));
+    memset(seos_characteristic->params.UID, 0x0d, sizeof(seos_characteristic->params.UID));
+
+    seos_characteristic->seos_att = seos_att_alloc(seos);
+
+    seos_att_set_on_subscribe_callback(
+        seos_characteristic->seos_att, seos_characteristic_on_subscribe, seos_characteristic);
+
+    seos_att_set_write_request_callback(
+        seos_characteristic->seos_att, seos_characteristic_write_request, seos_characteristic);
+
+    return seos_characteristic;
+}
+
+void seos_characteristic_free(SeosCharacteristic* seos_characteristic) {
+    furi_assert(seos_characteristic);
+    seos_att_free(seos_characteristic->seos_att);
+    free(seos_characteristic);
+}
+
+void seos_characteristic_start(SeosCharacteristic* seos_characteristic, FlowMode mode) {
+    seos_characteristic->flow_mode = mode;
+    if(seos_characteristic->flow_mode == FLOW_CRED) {
+        seos_characteristic->params.key_no = 0;
+        seos_characteristic->params.cipher = TWO_KEY_3DES_CBC_MODE;
+        seos_characteristic->params.hash = SHA1;
+
+        memset(
+            seos_characteristic->params.rndICC, 0x0d, sizeof(seos_characteristic->params.rndICC));
+        memset(
+            seos_characteristic->params.rNonce, 0x0c, sizeof(seos_characteristic->params.rNonce));
+        memset(seos_characteristic->params.UID, 0x00, sizeof(seos_characteristic->params.UID));
+        memset(
+            seos_characteristic->params.cNonce, 0x00, sizeof(seos_characteristic->params.cNonce));
+    }
+    seos_att_start(seos_characteristic->seos_att, BLE_PERIPHERAL, mode);
+}
+
+void seos_characteristic_stop(SeosCharacteristic* seos_characteristic) {
+    seos_att_stop(seos_characteristic->seos_att);
+}
+
+void seos_characteristic_reader_flow(
+    SeosCharacteristic* seos_characteristic,
+    BitBuffer* attribute_value,
+    BitBuffer* payload) {
+    const uint8_t* data = bit_buffer_get_data(attribute_value);
+    const uint8_t* rx_data = data + 1; // Match name to nfc version for easier copying
+
+    // 022f20180014000400
+    // 520c00
+    // c0 6f0c840a a0000004400001010001
+    // 9000
+    if(memcmp(data + 5, standard_seos_aid, sizeof(standard_seos_aid)) == 0) { // response to select
+        FURI_LOG_I(TAG, "Select ADF");
+        uint8_t select_adf_header[] = {
+            0x80, 0xa5, 0x04, 0x00, (uint8_t)SEOS_ADF_OID_LEN + 2, 0x06, (uint8_t)SEOS_ADF_OID_LEN};
+
+        bit_buffer_append_bytes(payload, select_adf_header, sizeof(select_adf_header));
+        bit_buffer_append_bytes(payload, SEOS_ADF_OID, SEOS_ADF_OID_LEN);
+        seos_characteristic->phase = SELECT_ADF;
+    } else if(memcmp(data + 1, cd02, sizeof(cd02)) == 0) {
+        if(seos_reader_select_adf_response(
+               attribute_value, 1, seos_characteristic->credential, &seos_characteristic->params)) {
+            // Craft response
+            general_authenticate_1[3] = seos_characteristic->params.key_no;
+            bit_buffer_append_bytes(
+                payload, general_authenticate_1, sizeof(general_authenticate_1));
+            seos_characteristic->phase = GENERAL_AUTHENTICATION_1;
+        }
+    } else if(memcmp(data + 1, ga1_response, sizeof(ga1_response)) == 0) {
+        memcpy(seos_characteristic->params.rndICC, data + 5, 8);
+
+        // Craft response
+        uint8_t cryptogram[32 + 8];
+        memset(cryptogram, 0, sizeof(cryptogram));
+        seos_reader_generate_cryptogram(
+            seos_characteristic->credential, &seos_characteristic->params, cryptogram);
+
+        uint8_t ga_header[] = {
+            0x00,
+            0x87,
+            0x00,
+            seos_characteristic->params.key_no,
+            sizeof(cryptogram) + 4,
+            0x7c,
+            sizeof(cryptogram) + 2,
+            0x82,
+            sizeof(cryptogram)};
+
+        bit_buffer_append_bytes(payload, ga_header, sizeof(ga_header));
+        bit_buffer_append_bytes(payload, cryptogram, sizeof(cryptogram));
+
+        seos_characteristic->phase = GENERAL_AUTHENTICATION_2;
+    } else if(rx_data[0] == 0x7C && rx_data[2] == 0x82) { // ga2 response
+        if(rx_data[3] == 40) {
+            if(!seos_reader_verify_cryptogram(&seos_characteristic->params, rx_data + 4)) {
+                FURI_LOG_W(TAG, "Card cryptogram failed verification");
+                return;
+            }
+            FURI_LOG_I(TAG, "Authenticated");
+            view_dispatcher_send_custom_event(
+                seos_characteristic->seos->view_dispatcher, SeosCustomEventAuthenticated);
+        } else {
+            FURI_LOG_W(TAG, "Unhandled card cryptogram size %d", rx_data[3]);
+        }
+
+        seos_characteristic->secure_messaging =
+            secure_messaging_alloc(&seos_characteristic->params);
+
+        SecureMessaging* secure_messaging = seos_characteristic->secure_messaging;
+
+        uint8_t message[] = {0x5c, 0x02, 0xff, 0x00};
+        secure_messaging_wrap_apdu(secure_messaging, message, sizeof(message), payload);
+        seos_characteristic->phase = REQUEST_SIO;
+        view_dispatcher_send_custom_event(
+            seos_characteristic->seos->view_dispatcher, SeosCustomEventSIORequested);
+    } else if(seos_characteristic->phase == REQUEST_SIO) {
+        SecureMessaging* secure_messaging = seos_characteristic->secure_messaging;
+
+        BitBuffer* rx_buffer = bit_buffer_alloc(bit_buffer_get_size_bytes(attribute_value) - 1);
+        bit_buffer_append_bytes(
+            rx_buffer, rx_data, bit_buffer_get_size_bytes(attribute_value) - 1);
+        seos_log_bitbuffer(TAG, "BLE response(wrapped)", rx_buffer);
+        secure_messaging_unwrap_rapdu(secure_messaging, rx_buffer);
+        seos_log_bitbuffer(TAG, "BLE response(clear)", rx_buffer);
+
+        // Skip fileId
+        seos_characteristic->credential->sio_len = bit_buffer_get_byte(rx_buffer, 2);
+        if(seos_characteristic->credential->sio_len >
+           sizeof(seos_characteristic->credential->sio)) {
+            FURI_LOG_W(TAG, "SIO too long to save");
+            return;
+        }
+        memcpy(
+            seos_characteristic->credential->sio,
+            bit_buffer_get_data(rx_buffer) + 3,
+            seos_characteristic->credential->sio_len);
+        FURI_LOG_I(TAG, "SIO Captured, %d bytes", seos_characteristic->credential->sio_len);
+
+        Seos* seos = seos_characteristic->seos;
+        view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventReaderSuccess);
+        bit_buffer_free(rx_buffer);
+
+        seos_characteristic->phase = SELECT_AID;
+    } else if(data[0] == 0xe1) {
+        //ignore
+    } else {
+        FURI_LOG_W(TAG, "No match for write request");
+    }
+}
+
+void seos_characteristic_cred_flow(
+    SeosCharacteristic* seos_characteristic,
+    BitBuffer* attribute_value,
+    BitBuffer* payload) {
+    const uint8_t* data = bit_buffer_get_data(attribute_value);
+    const uint8_t* apdu = data + 1; // Match name to nfc version for easier copying
+
+    if(memcmp(apdu, select_header, sizeof(select_header)) == 0) {
+        if(memcmp(apdu + sizeof(select_header) + 1, standard_seos_aid, sizeof(standard_seos_aid)) ==
+           0) {
+            seos_emulator_select_aid(payload);
+            bit_buffer_append_bytes(payload, (uint8_t*)success, sizeof(success));
+        } else {
+            bit_buffer_append_bytes(payload, (uint8_t*)file_not_found, sizeof(file_not_found));
+        }
+    } else if(memcmp(apdu, select_adf_header, sizeof(select_adf_header)) == 0) {
+        // is our adf in the list?
+        // +1 to skip APDU length byte
+        void* p = memmem(
+            apdu + sizeof(select_adf_header) + 1,
+            apdu[sizeof(select_adf_header)],
+            SEOS_ADF_OID,
+            SEOS_ADF_OID_LEN);
+        if(p) {
+            seos_log_buffer(TAG, "Matched ADF", p, SEOS_ADF_OID_LEN);
+
+            seos_emulator_select_adf(
+                &seos_characteristic->params, seos_characteristic->credential, payload);
+            bit_buffer_append_bytes(payload, (uint8_t*)success, sizeof(success));
+        } else {
+            FURI_LOG_W(TAG, "Failed to match any ADF OID");
+        }
+
+    } else if(memcmp(apdu, general_authenticate_1, sizeof(general_authenticate_1)) == 0) {
+        seos_emulator_general_authenticate_1(payload, seos_characteristic->params);
+        bit_buffer_append_bytes(payload, (uint8_t*)success, sizeof(success));
+    } else if(memcmp(apdu, general_authenticate_2_header, sizeof(general_authenticate_2_header)) == 0) {
+        if(!seos_emulator_general_authenticate_2(
+               apdu,
+               bit_buffer_get_size_bytes(attribute_value),
+               seos_characteristic->credential,
+               &seos_characteristic->params,
+               payload)) {
+            FURI_LOG_W(TAG, "Failure in General Authenticate 2");
+        } else {
+            bit_buffer_append_bytes(payload, (uint8_t*)success, sizeof(success));
+        }
+
+        view_dispatcher_send_custom_event(
+            seos_characteristic->seos->view_dispatcher, SeosCustomEventAuthenticated);
+        // Prepare for future communication
+        seos_characteristic->secure_messaging =
+            secure_messaging_alloc(&seos_characteristic->params);
+    } else if(memcmp(apdu, secure_messaging_header, sizeof(secure_messaging_header)) == 0) {
+        uint8_t request_sio[] = {0x5c, 0x02, 0xff, 0x00};
+
+        if(seos_characteristic->secure_messaging) {
+            FURI_LOG_D(TAG, "Unwrap secure message");
+
+            // c0 0ccb3fff 16 8508fa8395d30de4e8e097008e085da7edbd833b002d00
+            // Ignore 1 BLE_START byte
+            size_t bytes_to_ignore = 1;
+            BitBuffer* tmp = bit_buffer_alloc(bit_buffer_get_size_bytes(attribute_value));
+            bit_buffer_append_bytes(
+                tmp,
+                bit_buffer_get_data(attribute_value) + bytes_to_ignore,
+                bit_buffer_get_size_bytes(attribute_value) - bytes_to_ignore);
+
+            seos_log_bitbuffer(TAG, "received(wrapped)", tmp);
+            secure_messaging_unwrap_apdu(seos_characteristic->secure_messaging, tmp);
+            seos_log_bitbuffer(TAG, "received(clear)", tmp);
+
+            const uint8_t* message = bit_buffer_get_data(tmp);
+            if(memcmp(message, request_sio, sizeof(request_sio)) == 0) {
+                view_dispatcher_send_custom_event(
+                    seos_characteristic->seos->view_dispatcher, SeosCustomEventSIORequested);
+
+                BitBuffer* sio_file = bit_buffer_alloc(128);
+                bit_buffer_append_bytes(sio_file, message + 2, 2); // fileId
+                bit_buffer_append_byte(sio_file, seos_characteristic->credential->sio_len);
+                bit_buffer_append_bytes(
+                    sio_file,
+                    seos_characteristic->credential->sio,
+                    seos_characteristic->credential->sio_len);
+
+                secure_messaging_wrap_rapdu(
+                    seos_characteristic->secure_messaging,
+                    (uint8_t*)bit_buffer_get_data(sio_file),
+                    bit_buffer_get_size_bytes(sio_file),
+                    payload);
+                bit_buffer_append_bytes(payload, (uint8_t*)success, sizeof(success));
+
+                bit_buffer_free(sio_file);
+            }
+
+            bit_buffer_free(tmp);
+        } else {
+            uint8_t no_sm[] = {0x69, 0x88};
+            bit_buffer_append_bytes(payload, no_sm, sizeof(no_sm));
+        }
+    } else if(data[0] == 0xe1) {
+        // ignore
+    } else {
+        FURI_LOG_W(TAG, "no match for attribute_value");
+    }
+}
+
+void seos_characteristic_write_request(void* context, BitBuffer* attribute_value) {
+    SeosCharacteristic* seos_characteristic = (SeosCharacteristic*)context;
+    seos_log_bitbuffer(TAG, "write request", attribute_value);
+
+    BitBuffer* payload = bit_buffer_alloc(128); // TODO: MTU
+    const uint8_t* data = bit_buffer_get_data(attribute_value);
+
+    if(data[0] != BLE_START && data[0] != 0xe1) {
+        FURI_LOG_W(TAG, "Unexpected start of BLE packet");
+    }
+
+    if(seos_characteristic->flow_mode == FLOW_READER) {
+        seos_characteristic_reader_flow(seos_characteristic, attribute_value, payload);
+    } else if(seos_characteristic->flow_mode == FLOW_CRED) {
+        seos_characteristic_cred_flow(seos_characteristic, attribute_value, payload);
+    }
+
+    if(bit_buffer_get_size_bytes(payload) > 0) {
+        BitBuffer* tx = bit_buffer_alloc(1 + 2 + 1 + bit_buffer_get_size_bytes(payload));
+
+        bit_buffer_append_byte(tx, BLE_START);
+        bit_buffer_append_bytes(
+            tx, bit_buffer_get_data(payload), bit_buffer_get_size_bytes(payload));
+
+        seos_att_notify(seos_characteristic->seos_att, seos_characteristic->handle, tx);
+        bit_buffer_free(tx);
+    }
+
+    bit_buffer_free(payload);
+}
+
+void seos_characteristic_on_subscribe(void* context, uint16_t handle) {
+    SeosCharacteristic* seos_characteristic = (SeosCharacteristic*)context;
+    FURI_LOG_D(TAG, "seos_characteristic_on_subscribe %04x", handle);
+    /*
+    if(seos_characteristic->handle != 0) {
+        FURI_LOG_W(TAG, "Ignoring subscribe; already subscribed");
+        return;
+    }
+    */
+
+    seos_characteristic->handle = handle;
+
+    // Send initial select
+    uint8_t select_header[] = {0x00, 0xa4, 0x04, 0x00, (uint8_t)sizeof(standard_seos_aid)};
+
+    BitBuffer* tx = bit_buffer_alloc(1 + sizeof(select_header) + sizeof(standard_seos_aid));
+
+    bit_buffer_append_byte(tx, BLE_START);
+    bit_buffer_append_bytes(tx, select_header, sizeof(select_header));
+    bit_buffer_append_bytes(tx, standard_seos_aid, sizeof(standard_seos_aid));
+    seos_log_bitbuffer(TAG, "initial select", tx);
+
+    seos_att_notify(seos_characteristic->seos_att, seos_characteristic->handle, tx);
+    seos_characteristic->phase = SELECT_AID;
+    bit_buffer_free(tx);
+}

+ 36 - 0
seos/seos_characteristic.h

@@ -0,0 +1,36 @@
+#pragma once
+
+#include <furi.h>
+#include <lib/toolbox/bit_buffer.h>
+
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+
+#include "secure_messaging.h"
+#include "seos_common.h"
+#include "seos.h"
+#include "seos_att.h"
+#include "keys.h"
+
+#define BLE_START 0xc0
+
+typedef struct {
+    Seos* seos;
+    SeosAtt* seos_att;
+    uint16_t handle;
+
+    SeosPhase phase;
+
+    FlowMode flow_mode;
+
+    AuthParameters params;
+    SecureMessaging* secure_messaging;
+    SeosCredential* credential;
+} SeosCharacteristic;
+
+SeosCharacteristic* seos_characteristic_alloc(Seos* seos);
+void seos_characteristic_free(SeosCharacteristic* seos_characteristic);
+void seos_characteristic_start(SeosCharacteristic* seos_characteristic, FlowMode mode);
+void seos_characteristic_stop(SeosCharacteristic* seos_characteristic);
+void seos_characteristic_write_request(void* context, BitBuffer* attribute_value);
+void seos_characteristic_on_subscribe(void* context, uint16_t handle);

+ 4 - 0
seos/seos_characteristic_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_characteristic.h"
+#include "seos_i.h"

+ 145 - 0
seos/seos_common.c

@@ -0,0 +1,145 @@
+#include "seos_common.h"
+
+char* seos_file_header = "Flipper Seos Credential";
+uint32_t seos_file_version = 1;
+
+void seos_log_buffer(char* TAG, char* prefix, uint8_t* buffer, size_t buffer_len) {
+    char display[SEOS_WORKER_MAX_BUFFER_SIZE * 2 + 1];
+
+    size_t limit = MIN((size_t)SEOS_WORKER_MAX_BUFFER_SIZE, buffer_len);
+    memset(display, 0, sizeof(display));
+    for(uint8_t i = 0; i < limit; i++) {
+        snprintf(display + (i * 2), sizeof(display), "%02x", buffer[i]);
+    }
+    if(prefix) {
+        FURI_LOG_D(TAG, "%s %d: %s", prefix, limit, display);
+    } else {
+        FURI_LOG_D(TAG, "Buffer %d: %s", limit, display);
+    }
+}
+
+void seos_log_bitbuffer(char* TAG, char* prefix, BitBuffer* buffer) {
+    furi_assert(buffer);
+
+    size_t length = bit_buffer_get_size_bytes(buffer);
+    const uint8_t* data = bit_buffer_get_data(buffer);
+
+    char display[SEOS_WORKER_MAX_BUFFER_SIZE * 2 + 1];
+
+    size_t limit = MIN((size_t)SEOS_WORKER_MAX_BUFFER_SIZE, length);
+    memset(display, 0, sizeof(display));
+    for(uint8_t i = 0; i < limit; i++) {
+        snprintf(display + (i * 2), sizeof(display), "%02x", data[i]);
+    }
+    if(prefix) {
+        FURI_LOG_D(TAG, "%s %d: %s", prefix, length, display);
+    } else {
+        FURI_LOG_D(TAG, "Buffer %d: %s", length, display);
+    }
+}
+
+void seos_common_copy_credential(const SeosCredential* src, SeosCredential* dst) {
+    furi_assert(src);
+    furi_assert(dst);
+
+    dst->diversifier_len = src->diversifier_len;
+    memcpy(dst->diversifier, src->diversifier, dst->diversifier_len);
+    dst->sio_len = src->sio_len;
+    memcpy(dst->sio, src->sio, dst->sio_len);
+}
+
+void seos_worker_diversify_key(
+    uint8_t master_key_value[16],
+    uint8_t* diversifier,
+    size_t diversifier_len,
+    uint8_t* adf_oid,
+    size_t adf_oid_len,
+    uint8_t algo_id1,
+    uint8_t algo_id2,
+    uint8_t keyId,
+    bool is_encryption,
+    uint8_t* div_key) {
+    char* TAG = "SeosCommon";
+    // 0000000000000000000000 04 00 0080 01 0907 01 2B0601040181E4380101020118010102 3D50AD518CD820
+    size_t index = 0;
+    uint8_t buffer[128];
+    memset(buffer, 0, sizeof(buffer));
+    index += 11;
+    buffer[index++] = is_encryption ? 0x04 : 0x06;
+    index++; // separation
+    index++; // 0x00 that goes with 0x80 to indicate 128bit key
+    buffer[index++] = 0x80;
+    buffer[index++] = 0x01; // i
+    buffer[index++] = algo_id1;
+    buffer[index++] = algo_id2;
+    buffer[index++] = keyId;
+    memcpy(buffer + index, adf_oid, adf_oid_len);
+    index += adf_oid_len;
+    memcpy(buffer + index, diversifier, diversifier_len);
+    index += diversifier_len;
+
+    aes_cmac(master_key_value, 16, buffer, index, div_key);
+
+    char display[33];
+    memset(display, 0, sizeof(display));
+    for(uint8_t i = 0; i < 16; i++) {
+        snprintf(display + (i * 2), sizeof(display), "%02x", div_key[i]);
+    }
+    FURI_LOG_I(TAG, "Diversified %s key: %s", is_encryption ? "Encrypt" : "Mac", display);
+}
+
+void seos_worker_aes_decrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* encrypted,
+    uint8_t* clear) {
+    uint8_t iv[16];
+    memset(iv, 0, sizeof(iv));
+    mbedtls_aes_context ctx;
+    mbedtls_aes_init(&ctx);
+    mbedtls_aes_setkey_dec(&ctx, key, 128);
+    mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_DECRYPT, length, iv, encrypted, clear);
+    mbedtls_aes_free(&ctx);
+}
+
+void seos_worker_des_decrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* encrypted,
+    uint8_t* clear) {
+    uint8_t iv[8];
+    memset(iv, 0, sizeof(iv));
+    mbedtls_des3_context ctx;
+    mbedtls_des3_init(&ctx);
+    mbedtls_des3_set2key_dec(&ctx, key);
+    mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_DECRYPT, length, iv, encrypted, clear);
+    mbedtls_des3_free(&ctx);
+}
+
+void seos_worker_aes_encrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* clear,
+    uint8_t* encrypted) {
+    uint8_t iv[16];
+    memset(iv, 0, sizeof(iv));
+    mbedtls_aes_context ctx;
+    mbedtls_aes_init(&ctx);
+    mbedtls_aes_setkey_enc(&ctx, key, 128);
+    mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_ENCRYPT, length, iv, clear, encrypted);
+    mbedtls_aes_free(&ctx);
+}
+
+void seos_worker_des_encrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* clear,
+    uint8_t* encrypted) {
+    uint8_t iv[8];
+    memset(iv, 0, sizeof(iv));
+    mbedtls_des3_context ctx;
+    mbedtls_des3_init(&ctx);
+    mbedtls_des3_set2key_enc(&ctx, key);
+    mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_ENCRYPT, length, iv, clear, encrypted);
+    mbedtls_des3_free(&ctx);
+}

+ 112 - 0
seos/seos_common.h

@@ -0,0 +1,112 @@
+#pragma once
+
+#include <stdlib.h>
+#include <stdint.h>
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <lib/toolbox/bit_buffer.h>
+
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+
+#include "aes_cmac.h"
+#include "des_cmac.h"
+
+#define TWO_KEY_3DES_CBC_MODE   2
+#define THREE_KEY_3DES_CBC_MODE 4
+#define SHA1                    6
+#define SHA256                  7
+#define AES_128_CBC             9
+
+#define SEOS_WORKER_MAX_BUFFER_SIZE 128
+#define SEOS_WORKER_CMAC_SIZE       8
+
+#define SEOS_APP_EXTENSION        ".seos"
+#define SEOS_FILE_NAME_MAX_LENGTH 32
+
+extern char* seos_file_header;
+extern uint32_t seos_file_version;
+
+typedef enum {
+    BLE_PERIPHERAL,
+    BLE_CENTRAL,
+} BleMode;
+
+typedef enum {
+    FLOW_TEST,
+    FLOW_READER,
+    FLOW_CRED,
+    FLOW_READER_SCANNER,
+    FLOW_CRED_SCANNER,
+    FLOW_INSPECT,
+} FlowMode;
+
+typedef enum {
+    SELECT_AID,
+    SELECT_ADF,
+    GENERAL_AUTHENTICATION_1,
+    GENERAL_AUTHENTICATION_2,
+    REQUEST_SIO,
+} SeosPhase;
+
+typedef struct {
+    uint8_t diversifier[16];
+    size_t diversifier_len;
+    uint8_t sio[128];
+    size_t sio_len;
+    uint8_t priv_key[16];
+    uint8_t auth_key[16];
+    uint8_t adf_response[72];
+} SeosCredential;
+
+typedef struct {
+    uint8_t rndICC[8];
+    uint8_t UID[8];
+    uint8_t cNonce[16];
+    uint8_t rNonce[16];
+    uint8_t priv_key[16];
+    uint8_t auth_key[16];
+    uint8_t key_no;
+    uint8_t cipher;
+    uint8_t hash;
+} AuthParameters;
+
+void seos_log_bitbuffer(char* TAG, char* prefix, BitBuffer* buffer);
+void seos_log_buffer(char* TAG, char* prefix, uint8_t* buffer, size_t buffer_len);
+
+void seos_common_copy_credential(const SeosCredential* src, SeosCredential* dst);
+
+void seos_worker_diversify_key(
+    uint8_t master_key_value[16],
+    uint8_t* diversifier,
+    size_t diversifier_len,
+    uint8_t* adf_oid,
+    size_t adf_oid_len,
+    uint8_t algo_id1,
+    uint8_t algo_id2,
+    uint8_t reference_qualifier,
+    bool is_encryption,
+    uint8_t* div_key);
+
+void seos_worker_aes_decrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* encrypted,
+    uint8_t* clear);
+void seos_worker_des_decrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* encrypted,
+    uint8_t* clear);
+
+void seos_worker_aes_encrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* clear,
+    uint8_t* encrypted);
+void seos_worker_des_encrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* clear,
+    uint8_t* encrypted);

+ 734 - 0
seos/seos_emulator.c

@@ -0,0 +1,734 @@
+#include "seos_emulator_i.h"
+
+#define TAG "SeosEmulator"
+
+#define NAD_MASK 0x08
+
+static uint8_t select_header[] = {0x00, 0xa4, 0x04, 0x00};
+static uint8_t standard_seos_aid[] = {0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+static uint8_t MOBILE_SEOS_ADMIN_CARD[] =
+    {0xa0, 0x00, 0x00, 0x03, 0x82, 0x00, 0x2d, 0x00, 0x01, 0x01};
+static uint8_t OPERATION_SELECTOR[] = {0xa0, 0x00, 0x00, 0x03, 0x82, 0x00, 0x2f, 0x00, 0x01, 0x01};
+static uint8_t OPERATION_SELECTOR_POST_RESET[] =
+    {0xa0, 0x00, 0x00, 0x03, 0x82, 0x00, 0x31, 0x00, 0x01, 0x01};
+
+static uint8_t SEOS_APPLET_FCI[] =
+    {0x6F, 0x0C, 0x84, 0x0A, 0xA0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+static uint8_t FILE_NOT_FOUND[] = {0x6A, 0x82};
+static uint8_t success[] = {0x90, 0x00};
+
+static uint8_t select_adf_header[] = {0x80, 0xa5, 0x04, 0x00};
+static uint8_t general_authenticate_1[] =
+    {0x00, 0x87, 0x00, 0x01, 0x04, 0x7c, 0x02, 0x81, 0x00, 0x00};
+static uint8_t general_authenticate_1_response_header[] = {0x7c, 0x0a, 0x81, 0x08};
+static uint8_t general_authenticate_2_header[] = {0x00, 0x87, 0x00, 0x01};
+static uint8_t secure_messaging_header[] = {0x0c, 0xcb, 0x3f, 0xff};
+static uint8_t empty[16] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+SeosEmulator* seos_emulator_alloc(SeosCredential* credential) {
+    SeosEmulator* seos_emulator = malloc(sizeof(SeosEmulator));
+    memset(seos_emulator, 0, sizeof(SeosEmulator));
+
+    // Using DES for greater compatibilty
+    seos_emulator->params.cipher = TWO_KEY_3DES_CBC_MODE;
+    seos_emulator->params.hash = SHA1;
+
+    memset(seos_emulator->params.rndICC, 0x0d, sizeof(seos_emulator->params.rndICC));
+    memset(seos_emulator->params.rNonce, 0x0c, sizeof(seos_emulator->params.rNonce));
+    seos_emulator->credential = credential;
+
+    seos_emulator->secure_messaging = NULL;
+
+    seos_emulator->storage = furi_record_open(RECORD_STORAGE);
+    seos_emulator->dialogs = furi_record_open(RECORD_DIALOGS);
+    seos_emulator->load_path = furi_string_alloc();
+    seos_emulator->tx_buffer = bit_buffer_alloc(SEOS_WORKER_MAX_BUFFER_SIZE);
+
+    return seos_emulator;
+}
+
+void seos_emulator_free(SeosEmulator* seos_emulator) {
+    furi_assert(seos_emulator);
+
+    if(seos_emulator->secure_messaging) {
+        secure_messaging_free(seos_emulator->secure_messaging);
+    }
+
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_DIALOGS);
+    furi_string_free(seos_emulator->load_path);
+    bit_buffer_free(seos_emulator->tx_buffer);
+    free(seos_emulator);
+}
+
+void seos_emulator_set_loading_callback(
+    SeosEmulator* seos_emulator,
+    SeosLoadingCallback callback,
+    void* context) {
+    furi_assert(seos_emulator);
+
+    seos_emulator->loading_cb = callback;
+    seos_emulator->loading_cb_ctx = context;
+}
+
+static bool
+    seos_emulator_file_load(SeosEmulator* seos_emulator, FuriString* path, bool show_dialog) {
+    bool parsed = false;
+    FlipperFormat* file = flipper_format_file_alloc(seos_emulator->storage);
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
+    bool deprecated_version = false;
+
+    if(seos_emulator->loading_cb) {
+        seos_emulator->loading_cb(seos_emulator->loading_cb_ctx, true);
+    }
+
+    memset(
+        seos_emulator->credential->diversifier, 0, sizeof(seos_emulator->credential->diversifier));
+    memset(seos_emulator->credential->sio, 0, sizeof(seos_emulator->credential->sio));
+    do {
+        if(!flipper_format_file_open_existing(file, furi_string_get_cstr(path))) break;
+
+        // Read and verify file header
+        uint32_t version = 0;
+        if(!flipper_format_read_header(file, temp_str, &version)) break;
+        if(furi_string_cmp_str(temp_str, seos_file_header) || (version != seos_file_version)) {
+            deprecated_version = true;
+            break;
+        }
+
+        if(!flipper_format_read_uint32(
+               file,
+               "Diversifier Length",
+               (uint32_t*)&(seos_emulator->credential->diversifier_len),
+               1))
+            break;
+        if(!flipper_format_read_hex(
+               file,
+               "Diversifier",
+               seos_emulator->credential->diversifier,
+               seos_emulator->credential->diversifier_len))
+            break;
+
+        if(!flipper_format_read_uint32(
+               file, "SIO Length", (uint32_t*)&(seos_emulator->credential->sio_len), 1))
+            break;
+        if(!flipper_format_read_hex(
+               file, "SIO", seos_emulator->credential->sio, seos_emulator->credential->sio_len))
+            break;
+
+        // optional
+        memset(
+            seos_emulator->credential->priv_key, 0, sizeof(seos_emulator->credential->priv_key));
+        memset(
+            seos_emulator->credential->auth_key, 0, sizeof(seos_emulator->credential->auth_key));
+        memset(
+            seos_emulator->credential->adf_response,
+            0,
+            sizeof(seos_emulator->credential->adf_response));
+        flipper_format_read_hex(
+            file,
+            "Priv Key",
+            seos_emulator->credential->priv_key,
+            sizeof(seos_emulator->credential->priv_key));
+        flipper_format_read_hex(
+            file,
+            "Auth Key",
+            seos_emulator->credential->auth_key,
+            sizeof(seos_emulator->credential->auth_key));
+        if(memcmp(seos_emulator->credential->priv_key, empty, sizeof(empty)) != 0) {
+            FURI_LOG_I(TAG, "+ Priv Key");
+        }
+        if(memcmp(seos_emulator->credential->priv_key, empty, sizeof(empty)) != 0) {
+            FURI_LOG_I(TAG, "+ Auth Key");
+        }
+        flipper_format_read_hex(
+            file,
+            "ADF Response",
+            seos_emulator->credential->adf_response,
+            sizeof(seos_emulator->credential->adf_response));
+        parsed = true;
+    } while(false);
+
+    if(seos_emulator->loading_cb) {
+        seos_emulator->loading_cb(seos_emulator->loading_cb_ctx, false);
+    }
+
+    if((!parsed) && (show_dialog)) {
+        if(deprecated_version) {
+            dialog_message_show_storage_error(seos_emulator->dialogs, "File format deprecated");
+        } else {
+            dialog_message_show_storage_error(seos_emulator->dialogs, "Can not parse\nfile");
+        }
+    }
+
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+
+    return parsed;
+}
+
+bool seos_emulator_file_select(SeosEmulator* seos_emulator) {
+    furi_assert(seos_emulator);
+    bool res = false;
+
+    FuriString* seos_app_folder = furi_string_alloc_set(STORAGE_APP_DATA_PATH_PREFIX);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, SEOS_APP_EXTENSION, &I_Nfc_10px);
+    browser_options.base_path = STORAGE_APP_DATA_PATH_PREFIX;
+
+    res = dialog_file_browser_show(
+        seos_emulator->dialogs, seos_emulator->load_path, seos_app_folder, &browser_options);
+
+    furi_string_free(seos_app_folder);
+    if(res) {
+        FuriString* filename;
+        filename = furi_string_alloc();
+        path_extract_filename(seos_emulator->load_path, filename, true);
+        strncpy(seos_emulator->name, furi_string_get_cstr(filename), SEOS_FILE_NAME_MAX_LENGTH);
+        res = seos_emulator_file_load(seos_emulator, seos_emulator->load_path, true);
+        furi_string_free(filename);
+    }
+
+    return res;
+}
+
+bool seos_emulator_delete(SeosEmulator* seos_emulator, bool use_load_path) {
+    furi_assert(seos_emulator);
+    bool deleted = false;
+    FuriString* file_path;
+    file_path = furi_string_alloc();
+
+    do {
+        // Delete original file
+        if(use_load_path && !furi_string_empty(seos_emulator->load_path)) {
+            furi_string_set(file_path, seos_emulator->load_path);
+        } else {
+            furi_string_printf(
+                file_path, APP_DATA_PATH("%s%s"), seos_emulator->name, SEOS_APP_EXTENSION);
+        }
+        if(!storage_simply_remove(seos_emulator->storage, furi_string_get_cstr(file_path))) break;
+        deleted = true;
+    } while(0);
+
+    if(!deleted) {
+        dialog_message_show_storage_error(seos_emulator->dialogs, "Can not remove file");
+    }
+
+    furi_string_free(file_path);
+    return deleted;
+}
+
+void seos_emulator_select_aid(BitBuffer* tx_buffer) {
+    FURI_LOG_D(TAG, "Select AID");
+    bit_buffer_append_bytes(tx_buffer, SEOS_APPLET_FCI, sizeof(SEOS_APPLET_FCI));
+}
+
+void seos_emulator_general_authenticate_1(BitBuffer* tx_buffer, AuthParameters params) {
+    bit_buffer_append_bytes(
+        tx_buffer,
+        general_authenticate_1_response_header,
+        sizeof(general_authenticate_1_response_header));
+    bit_buffer_append_bytes(tx_buffer, params.rndICC, sizeof(params.rndICC));
+}
+
+// 0a00
+// 00870001 2c7c 2a82 28 bbb4e9156136f27f687e2967865dfe812e33c95ddcf9294a4340d26da3e76db0220d1163c591e5b8 00
+bool seos_emulator_general_authenticate_2(
+    const uint8_t* buffer,
+    size_t buffer_len,
+    SeosCredential* credential,
+    AuthParameters* params,
+    BitBuffer* tx_buffer) {
+    FURI_LOG_D(TAG, "seos_emulator_general_authenticate_2");
+    UNUSED(buffer_len);
+
+    uint8_t* rx_data = (uint8_t*)buffer;
+    uint8_t* cryptogram = rx_data + sizeof(general_authenticate_2_header) + 5;
+    size_t encrypted_len = 32;
+    uint8_t* mac = cryptogram + encrypted_len;
+
+    params->key_no = rx_data[3];
+
+    if(memcmp(credential->priv_key, empty, sizeof(empty)) == 0) {
+        seos_worker_diversify_key(
+            SEOS_ADF1_READ,
+            credential->diversifier,
+            credential->diversifier_len,
+            SEOS_ADF_OID,
+            SEOS_ADF_OID_LEN,
+            params->cipher,
+            params->hash,
+            params->key_no,
+            true,
+            params->priv_key);
+    } else {
+        memcpy(params->priv_key, credential->priv_key, sizeof(params->priv_key));
+    }
+    if(memcmp(credential->auth_key, empty, sizeof(empty)) == 0) {
+        seos_worker_diversify_key(
+            SEOS_ADF1_READ,
+            credential->diversifier,
+            credential->diversifier_len,
+            SEOS_ADF_OID,
+            SEOS_ADF_OID_LEN,
+            params->cipher,
+            params->hash,
+            params->key_no,
+            false,
+            params->auth_key);
+    } else {
+        memcpy(params->auth_key, credential->auth_key, sizeof(params->auth_key));
+    }
+
+    uint8_t cmac[16];
+    if(params->cipher == AES_128_CBC) {
+        aes_cmac(params->auth_key, sizeof(params->auth_key), cryptogram, encrypted_len, cmac);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        des_cmac(params->auth_key, sizeof(params->auth_key), cryptogram, encrypted_len, cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+        return false;
+    }
+
+    if(memcmp(cmac, mac, SEOS_WORKER_CMAC_SIZE) != 0) {
+        FURI_LOG_W(TAG, "Incorrect cryptogram mac %02x... vs %02x...", cmac[0], mac[0]);
+        return false;
+    }
+
+    uint8_t clear[32];
+    if(params->cipher == AES_128_CBC) {
+        seos_worker_aes_decrypt(params->priv_key, encrypted_len, cryptogram, clear);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_decrypt(params->priv_key, encrypted_len, cryptogram, clear);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    size_t index = 0;
+    memcpy(params->UID, clear + index, sizeof(params->UID));
+    index += sizeof(params->UID);
+    if(memcmp(clear + index, params->rndICC, sizeof(params->rndICC)) != 0) {
+        FURI_LOG_W(TAG, "Incorrect rndICC returned");
+        return false;
+    }
+    index += sizeof(params->rndICC);
+    memcpy(params->cNonce, clear + index, sizeof(params->cNonce));
+    index += sizeof(params->cNonce);
+
+    // Construct response
+    uint8_t response_header[] = {0x7c, 0x2a, 0x82, 0x28};
+    memset(clear, 0, sizeof(clear));
+    memset(cmac, 0, sizeof(cmac));
+    index = 0;
+    memcpy(clear + index, params->rndICC, sizeof(params->rndICC));
+    index += sizeof(params->rndICC);
+    memcpy(clear + index, params->UID, sizeof(params->UID));
+    index += sizeof(params->UID);
+    memcpy(clear + index, params->rNonce, sizeof(params->rNonce));
+    index += sizeof(params->rNonce);
+
+    uint8_t encrypted[32];
+    if(params->cipher == AES_128_CBC) {
+        seos_worker_aes_encrypt(params->priv_key, sizeof(clear), clear, encrypted);
+
+        aes_cmac(params->auth_key, sizeof(params->auth_key), encrypted, sizeof(encrypted), cmac);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_encrypt(params->priv_key, sizeof(clear), clear, encrypted);
+        des_cmac(params->auth_key, sizeof(params->auth_key), encrypted, sizeof(encrypted), cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    bit_buffer_append_bytes(tx_buffer, response_header, sizeof(response_header));
+    bit_buffer_append_bytes(tx_buffer, encrypted, sizeof(encrypted));
+    bit_buffer_append_bytes(tx_buffer, cmac, SEOS_WORKER_CMAC_SIZE);
+
+    return true;
+}
+
+void seos_emulator_des_adf_payload(SeosCredential* credential, uint8_t* buffer) {
+    // Synethic IV
+    /// random bytes
+    uint8_t rnd[4] = {0, 0, 0, 0};
+    uint8_t cmac[8] = {0};
+    /// cmac
+    des_cmac(SEOS_ADF1_PRIV_MAC, sizeof(SEOS_ADF1_PRIV_MAC), rnd, sizeof(rnd), cmac);
+    uint8_t iv[8];
+    memcpy(iv + 0, rnd, sizeof(rnd));
+    memcpy(iv + sizeof(rnd), cmac, sizeof(iv) - sizeof(rnd));
+
+    // Copy IV to buffer because mbedtls_des3_crypt_cbc mutates it
+    memcpy(buffer + 0, iv, sizeof(iv));
+
+    uint8_t clear[0x30];
+    memset(clear, 0, sizeof(clear));
+    size_t index = 0;
+
+    // OID
+    clear[index++] = 0x06;
+    clear[index++] = SEOS_ADF_OID_LEN, memcpy(clear + index, SEOS_ADF_OID, SEOS_ADF_OID_LEN);
+    index += SEOS_ADF_OID_LEN;
+    // diversifier
+    clear[index++] = 0xcf;
+    clear[index++] = credential->diversifier_len;
+    memcpy(clear + index, credential->diversifier, credential->diversifier_len);
+    index += credential->diversifier_len;
+
+    mbedtls_des3_context ctx;
+    mbedtls_des3_init(&ctx);
+    mbedtls_des3_set2key_enc(&ctx, SEOS_ADF1_PRIV_ENC);
+    mbedtls_des3_crypt_cbc(
+        &ctx, MBEDTLS_DES_ENCRYPT, sizeof(clear), iv, clear, buffer + sizeof(iv));
+    mbedtls_des3_free(&ctx);
+}
+
+void seos_emulator_aes_adf_payload(SeosCredential* credential, uint8_t* buffer) {
+    // Synethic IV
+    /// random bytes
+    uint8_t rnd[8] = {0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t cmac[16] = {0};
+    /// cmac
+    aes_cmac(SEOS_ADF1_PRIV_MAC, sizeof(SEOS_ADF1_PRIV_MAC), rnd, sizeof(rnd), cmac);
+    uint8_t iv[16];
+    memcpy(iv + 0, rnd, sizeof(rnd));
+    memcpy(iv + sizeof(rnd), cmac, sizeof(iv) - sizeof(rnd));
+
+    // Copy IV to buffer because mbedtls_aes_crypt_cbc mutates it
+    memcpy(buffer + 0, iv, sizeof(iv));
+
+    uint8_t clear[0x30];
+    memset(clear, 0, sizeof(clear));
+    size_t index = 0;
+
+    // OID
+    clear[index++] = 0x06;
+    clear[index++] = SEOS_ADF_OID_LEN;
+    memcpy(clear + index, SEOS_ADF_OID, SEOS_ADF_OID_LEN);
+    index += SEOS_ADF_OID_LEN;
+    // diversifier
+    clear[index++] = 0xcf;
+    clear[index++] = credential->diversifier_len;
+    memcpy(clear + index, credential->diversifier, credential->diversifier_len);
+    index += credential->diversifier_len;
+
+    mbedtls_aes_context ctx;
+    mbedtls_aes_init(&ctx);
+    mbedtls_aes_setkey_enc(&ctx, SEOS_ADF1_PRIV_ENC, sizeof(SEOS_ADF1_PRIV_ENC) * 8);
+    mbedtls_aes_crypt_cbc(
+        &ctx, MBEDTLS_AES_ENCRYPT, sizeof(clear), iv, clear, buffer + sizeof(iv));
+    mbedtls_aes_free(&ctx);
+}
+
+void seos_emulator_select_adf(
+    AuthParameters* params,
+    SeosCredential* credential,
+    BitBuffer* tx_buffer) {
+    FURI_LOG_D(TAG, "Select ADF");
+    // Shortcut if the credential file contained the hardcoded response
+    if(credential->adf_response[2] != 0x00 && credential->adf_response[2] == params->cipher) {
+        FURI_LOG_I(TAG, "Using hardcoded ADF Response");
+        bit_buffer_append_bytes(
+            tx_buffer, credential->adf_response, sizeof(credential->adf_response));
+        seos_log_bitbuffer(TAG, "Select ADF (0xcd02...)", tx_buffer);
+        return;
+    }
+
+    size_t prefix_len = bit_buffer_get_size_bytes(tx_buffer);
+    size_t des_cryptogram_length = 56;
+    size_t aes_cryptogram_length = 64;
+    uint8_t header[] = {0xcd, 0x02, params->cipher, params->hash};
+    bit_buffer_append_bytes(tx_buffer, header, sizeof(header));
+
+    // cryptogram
+    // 06112b0601040181e438010102011801010202 cf 07 3d4c010c71cfa7 e2d0b41a00cc5e494c8d52b6e562592399fe614a
+    uint8_t buffer[64];
+    uint8_t cmac[16];
+    memset(buffer, 0, sizeof(buffer));
+    if(params->cipher == AES_128_CBC) {
+        uint8_t cryptogram_prefix[] = {0x85, aes_cryptogram_length};
+        bit_buffer_append_bytes(tx_buffer, cryptogram_prefix, sizeof(cryptogram_prefix));
+
+        seos_emulator_aes_adf_payload(credential, buffer);
+        bit_buffer_append_bytes(tx_buffer, buffer, aes_cryptogram_length);
+
+        aes_cmac(
+            SEOS_ADF1_PRIV_MAC,
+            sizeof(SEOS_ADF1_PRIV_MAC),
+            (uint8_t*)bit_buffer_get_data(tx_buffer) + prefix_len,
+            bit_buffer_get_size_bytes(tx_buffer) - prefix_len,
+            cmac);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        uint8_t cryptogram_prefix[] = {0x85, des_cryptogram_length};
+        bit_buffer_append_bytes(tx_buffer, cryptogram_prefix, sizeof(cryptogram_prefix));
+
+        seos_emulator_des_adf_payload(credential, buffer);
+        bit_buffer_append_bytes(tx_buffer, buffer, des_cryptogram_length);
+
+        // +2 / -2 is to ignore iso14a framing
+        des_cmac(
+            SEOS_ADF1_PRIV_MAC,
+            sizeof(SEOS_ADF1_PRIV_MAC),
+            (uint8_t*)bit_buffer_get_data(tx_buffer) + prefix_len,
+            bit_buffer_get_size_bytes(tx_buffer) - prefix_len,
+            cmac);
+    }
+
+    uint8_t cmac_prefix[] = {0x8e, 0x08};
+    bit_buffer_append_bytes(tx_buffer, cmac_prefix, sizeof(cmac_prefix));
+    bit_buffer_append_bytes(tx_buffer, cmac, SEOS_WORKER_CMAC_SIZE);
+
+    seos_log_bitbuffer(TAG, "Select ADF (0xcd02...)", tx_buffer);
+}
+
+NfcCommand seos_worker_listener_inspect_reader(Seos* seos) {
+    SeosEmulator* seos_emulator = seos->seos_emulator;
+    BitBuffer* tx_buffer = seos_emulator->tx_buffer;
+    NfcCommand ret = NfcCommandContinue;
+
+    const uint8_t* rx_data = bit_buffer_get_data(seos_emulator->rx_buffer);
+    bool NAD = (rx_data[0] & NAD_MASK) == NAD_MASK;
+    uint8_t offset = NAD ? 2 : 1;
+
+    // + x to skip stuff before APDU
+    const uint8_t* apdu = rx_data + offset;
+
+    if(memcmp(apdu, select_header, sizeof(select_header)) == 0) {
+        if(memcmp(
+               apdu + sizeof(select_header) + 1, OPERATION_SELECTOR, sizeof(OPERATION_SELECTOR)) ==
+           0) {
+            uint8_t enableInspection[] = {
+                0x6f, 0x08, 0x85, 0x06, 0x02, 0x01, 0x40, 0x02, 0x01, 0x00};
+
+            bit_buffer_append_bytes(tx_buffer, enableInspection, sizeof(enableInspection));
+            view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventAIDSelected);
+        } else {
+            bit_buffer_append_bytes(tx_buffer, (uint8_t*)FILE_NOT_FOUND, sizeof(FILE_NOT_FOUND));
+        }
+    } else if(bit_buffer_get_size_bytes(seos_emulator->rx_buffer) > (size_t)(offset + 2)) {
+        FURI_LOG_I(TAG, "NFC stop; %d bytes", bit_buffer_get_size_bytes(seos_emulator->rx_buffer));
+        ret = NfcCommandStop;
+    }
+
+    return ret;
+}
+
+NfcCommand seos_worker_listener_process_message(Seos* seos) {
+    SeosEmulator* seos_emulator = seos->seos_emulator;
+    BitBuffer* tx_buffer = seos_emulator->tx_buffer;
+    NfcCommand ret = NfcCommandContinue;
+
+    const uint8_t* rx_data = bit_buffer_get_data(seos_emulator->rx_buffer);
+    bool NAD = (rx_data[0] & NAD_MASK) == NAD_MASK;
+    uint8_t offset = NAD ? 2 : 1;
+
+    // + x to skip stuff before APDU
+    const uint8_t* apdu = rx_data + offset;
+
+    if(memcmp(apdu, select_header, sizeof(select_header)) == 0) {
+        if(memcmp(apdu + sizeof(select_header) + 1, standard_seos_aid, sizeof(standard_seos_aid)) ==
+           0) {
+            seos_emulator_select_aid(seos_emulator->tx_buffer);
+            view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventAIDSelected);
+
+        } else if(
+            memcmp(
+                apdu + sizeof(select_header) + 1,
+                OPERATION_SELECTOR_POST_RESET,
+                sizeof(OPERATION_SELECTOR_POST_RESET)) == 0) {
+            FURI_LOG_I(TAG, "OPERATION_SELECTOR_POST_RESET");
+            bit_buffer_append_bytes(
+                seos_emulator->tx_buffer, (uint8_t*)FILE_NOT_FOUND, sizeof(FILE_NOT_FOUND));
+        } else if(
+            memcmp(
+                apdu + sizeof(select_header) + 1,
+                OPERATION_SELECTOR,
+                sizeof(OPERATION_SELECTOR)) == 0) {
+            FURI_LOG_I(TAG, "OPERATION_SELECTOR");
+            bit_buffer_append_bytes(
+                seos_emulator->tx_buffer, (uint8_t*)FILE_NOT_FOUND, sizeof(FILE_NOT_FOUND));
+        } else if(
+            memcmp(
+                apdu + sizeof(select_header) + 1,
+                MOBILE_SEOS_ADMIN_CARD,
+                sizeof(MOBILE_SEOS_ADMIN_CARD)) == 0) {
+            FURI_LOG_I(TAG, "MOBILE_SEOS_ADMIN_CARD");
+            bit_buffer_append_bytes(
+                seos_emulator->tx_buffer, (uint8_t*)FILE_NOT_FOUND, sizeof(FILE_NOT_FOUND));
+        } else {
+            seos_log_bitbuffer(TAG, "Reject select", seos_emulator->rx_buffer);
+            bit_buffer_append_bytes(
+                seos_emulator->tx_buffer, (uint8_t*)FILE_NOT_FOUND, sizeof(FILE_NOT_FOUND));
+        }
+    } else if(memcmp(apdu, select_adf_header, sizeof(select_adf_header)) == 0) {
+        // is our adf in the list?
+        // +1 to skip APDU length byte
+        void* p = memmem(
+            apdu + sizeof(select_adf_header) + 1,
+            apdu[sizeof(select_adf_header)],
+            SEOS_ADF_OID,
+            SEOS_ADF_OID_LEN);
+        if(p) {
+            BitBuffer* tmp = bit_buffer_alloc(SEOS_ADF_OID_LEN);
+            bit_buffer_append_bytes(tmp, p, SEOS_ADF_OID_LEN);
+            seos_log_bitbuffer(TAG, "Matched ADF", tmp);
+            bit_buffer_free(tmp);
+            view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventADFMatched);
+
+            seos_emulator_select_adf(
+                &seos_emulator->params, seos_emulator->credential, seos_emulator->tx_buffer);
+        } else {
+            FURI_LOG_W(TAG, "Failed to match any ADF OID");
+        }
+    } else if(memcmp(apdu, general_authenticate_1, sizeof(general_authenticate_1)) == 0) {
+        seos_emulator_general_authenticate_1(seos_emulator->tx_buffer, seos_emulator->params);
+    } else if(memcmp(apdu, general_authenticate_2_header, sizeof(general_authenticate_2_header)) == 0) {
+        if(!seos_emulator_general_authenticate_2(
+               apdu,
+               bit_buffer_get_size_bytes(seos_emulator->rx_buffer),
+               seos_emulator->credential,
+               &seos_emulator->params,
+               seos_emulator->tx_buffer)) {
+            FURI_LOG_W(TAG, "Failure in General Authenticate 2");
+            ret = NfcCommandStop;
+            return ret;
+        }
+        view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventAuthenticated);
+        // Prepare for future communication
+        seos_emulator->secure_messaging = secure_messaging_alloc(&seos_emulator->params);
+    } else if(memcmp(apdu, secure_messaging_header, sizeof(secure_messaging_header)) == 0) {
+        uint8_t request_sio[] = {0x5c, 0x02, 0xff, 0x00};
+
+        if(seos_emulator->secure_messaging) {
+            FURI_LOG_D(TAG, "Unwrap secure message");
+
+            // 0b00 0ccb3fff 16 8508fa8395d30de4e8e097008e085da7edbd833b002d00
+            // Ignore 2 iso frame bytes
+            size_t bytes_to_ignore = offset;
+            BitBuffer* tmp = bit_buffer_alloc(bit_buffer_get_size_bytes(seos_emulator->rx_buffer));
+            bit_buffer_append_bytes(
+                tmp,
+                bit_buffer_get_data(seos_emulator->rx_buffer) + bytes_to_ignore,
+                bit_buffer_get_size_bytes(seos_emulator->rx_buffer) - bytes_to_ignore);
+
+            seos_log_bitbuffer(TAG, "NFC received(wrapped)", tmp);
+            secure_messaging_unwrap_apdu(seos_emulator->secure_messaging, tmp);
+            seos_log_bitbuffer(TAG, "NFC received(clear)", tmp);
+
+            const uint8_t* message = bit_buffer_get_data(tmp);
+            if(memcmp(message, request_sio, sizeof(request_sio)) == 0) {
+                view_dispatcher_send_custom_event(
+                    seos->view_dispatcher, SeosCustomEventSIORequested);
+                BitBuffer* sio_file = bit_buffer_alloc(128);
+                bit_buffer_append_bytes(sio_file, message + 2, 2); // fileId
+                bit_buffer_append_byte(sio_file, seos_emulator->credential->sio_len);
+                bit_buffer_append_bytes(
+                    sio_file, seos_emulator->credential->sio, seos_emulator->credential->sio_len);
+
+                secure_messaging_wrap_rapdu(
+                    seos_emulator->secure_messaging,
+                    (uint8_t*)bit_buffer_get_data(sio_file),
+                    bit_buffer_get_size_bytes(sio_file),
+                    tx_buffer);
+
+                bit_buffer_free(sio_file);
+            }
+
+            bit_buffer_free(tmp);
+        } else {
+            uint8_t no_sm[] = {0x69, 0x88};
+            bit_buffer_append_bytes(tx_buffer, no_sm, sizeof(no_sm));
+        }
+    } else {
+        // I'm trying to find a good place to re-assert that we're emulating so we don't get stuck on a previous UI screen when we emulate repeatedly
+        view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventEmulate);
+    }
+
+    return ret;
+}
+
+NfcCommand seos_worker_listener_callback(NfcGenericEvent event, void* context) {
+    furi_assert(context);
+    furi_assert(event.protocol == NfcProtocolIso14443_4a);
+    furi_assert(event.event_data);
+    Seos* seos = context;
+    SeosEmulator* seos_emulator = seos->seos_emulator;
+
+    NfcCommand ret = NfcCommandContinue;
+    Iso14443_4aListenerEvent* iso14443_4a_event = event.event_data;
+    Iso14443_3aListener* iso14443_listener = event.instance;
+    seos_emulator->iso14443_listener = iso14443_listener;
+
+    BitBuffer* tx_buffer = seos_emulator->tx_buffer;
+    bit_buffer_reset(tx_buffer);
+
+    switch(iso14443_4a_event->type) {
+    case Iso14443_4aListenerEventTypeReceivedData:
+        seos_emulator->rx_buffer = iso14443_4a_event->data->buffer;
+        const uint8_t* rx_data = bit_buffer_get_data(seos_emulator->rx_buffer);
+        bool NAD = (rx_data[0] & NAD_MASK) == NAD_MASK;
+        uint8_t offset = NAD ? 2 : 1;
+
+        if(bit_buffer_get_size_bytes(iso14443_4a_event->data->buffer) == offset) {
+            FURI_LOG_I(TAG, "No contents in frame");
+            break;
+        }
+
+        seos_log_bitbuffer(TAG, "NFC received", seos_emulator->rx_buffer);
+
+        // Some ISO14443a framing I need to figure out
+        bit_buffer_append_bytes(tx_buffer, rx_data, offset);
+
+        if(seos->flow_mode == FLOW_CRED) {
+            ret = seos_worker_listener_process_message(seos);
+        } else if(seos->flow_mode == FLOW_INSPECT) {
+            ret = seos_worker_listener_inspect_reader(seos);
+        }
+
+        if(bit_buffer_get_size_bytes(seos_emulator->tx_buffer) >
+           offset) { // contents belong iso framing
+
+            if(memcmp(
+                   FILE_NOT_FOUND,
+                   bit_buffer_get_data(tx_buffer) + bit_buffer_get_size_bytes(tx_buffer) -
+                       sizeof(FILE_NOT_FOUND),
+                   sizeof(FILE_NOT_FOUND)) != 0) {
+                bit_buffer_append_bytes(tx_buffer, success, sizeof(success));
+            }
+
+            iso14443_crc_append(Iso14443CrcTypeA, tx_buffer);
+
+            seos_log_bitbuffer(TAG, "NFC transmit", seos_emulator->tx_buffer);
+
+            NfcError error = nfc_listener_tx((Nfc*)iso14443_listener, tx_buffer);
+            if(error != NfcErrorNone) {
+                FURI_LOG_W(TAG, "Tx error: %d", error);
+                break;
+            }
+        } else {
+            iso14443_crc_append(Iso14443CrcTypeA, tx_buffer);
+
+            seos_log_bitbuffer(TAG, "NFC transmit", seos_emulator->tx_buffer);
+
+            NfcError error = nfc_listener_tx((Nfc*)iso14443_listener, tx_buffer);
+            if(error != NfcErrorNone) {
+                FURI_LOG_W(TAG, "Tx error: %d", error);
+                break;
+            }
+        }
+        break;
+    case Iso14443_4aListenerEventTypeHalted:
+        FURI_LOG_I(TAG, "Halted");
+        break;
+    case Iso14443_4aListenerEventTypeFieldOff:
+        FURI_LOG_I(TAG, "Field Off");
+        break;
+    }
+
+    if(ret == NfcCommandStop) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventReaderError);
+    }
+    return ret;
+}

+ 61 - 0
seos/seos_emulator.h

@@ -0,0 +1,61 @@
+#pragma once
+
+#include <storage/storage.h>
+#include <dialogs/dialogs.h>
+#include <lib/toolbox/path.h>
+#include <lib/nfc/protocols/nfc_generic_event.h>
+#include <lib/nfc/protocols/iso14443_4a/iso14443_4a_listener.h>
+#include <lib/nfc/helpers/iso14443_crc.h>
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+
+#include "secure_messaging.h"
+
+typedef void (*SeosLoadingCallback)(void* context, bool state);
+
+typedef struct {
+    Iso14443_3aListener* iso14443_listener;
+    BitBuffer* tx_buffer;
+    BitBuffer* rx_buffer;
+
+    AuthParameters params;
+
+    SecureMessaging* secure_messaging;
+
+    SeosCredential* credential;
+
+    char name[SEOS_FILE_NAME_MAX_LENGTH + 1];
+    FuriString* load_path;
+    SeosLoadingCallback loading_cb;
+    void* loading_cb_ctx;
+    Storage* storage;
+    DialogsApp* dialogs;
+} SeosEmulator;
+
+NfcCommand seos_worker_listener_callback(NfcGenericEvent event, void* context);
+
+SeosEmulator* seos_emulator_alloc(SeosCredential* credential);
+
+void seos_emulator_free(SeosEmulator* seos_emulator);
+
+void seos_emulator_set_loading_callback(
+    SeosEmulator* seos_emulator,
+    SeosLoadingCallback callback,
+    void* context);
+
+bool seos_emulator_file_select(SeosEmulator* seos_emulator);
+bool seos_emulator_delete(SeosEmulator* seos_emulator, bool use_load_path);
+
+void seos_emulator_general_authenticate_1(BitBuffer* tx_buffer, AuthParameters params);
+bool seos_emulator_general_authenticate_2(
+    const uint8_t* buffer,
+    size_t buffer_len,
+    SeosCredential* credential,
+    AuthParameters* params,
+    BitBuffer* tx_buffer);
+
+void seos_emulator_select_aid(BitBuffer* tx_buffer);
+void seos_emulator_select_adf(
+    AuthParameters* params,
+    SeosCredential* credential,
+    BitBuffer* tx_buffer);

+ 5 - 0
seos/seos_emulator_i.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_emulator.h"
+#include "keys.h"

+ 877 - 0
seos/seos_hci.c

@@ -0,0 +1,877 @@
+#include "seos_hci_i.h"
+
+#define TAG "SeosHci"
+
+#define OGF_LINK_CTL   0x01
+#define OCF_DISCONNECT 0x0006
+
+#define OGF_HOST_CTL                0x03
+#define OCF_SET_EVENT_MASK          0x0001
+#define OCF_RESET                   0x0003
+#define OCF_READ_LE_HOST_SUPPORTED  0x006c
+#define OCF_WRITE_LE_HOST_SUPPORTED 0x006d
+
+#define OGF_INFO_PARAM         0x04
+#define OCF_READ_LOCAL_VERSION 0x0001
+#define OCF_READ_BUFFER_SIZE   0x0005
+#define OCF_READ_BD_ADDR       0x0009
+
+#define OGF_STATUS_PARAM 0x05
+#define OCF_READ_RSSI    0x0005
+
+#define OGF_LE_CTL                           0x08
+#define OCF_LE_SET_EVENT_MASK                0x0001
+#define OCF_LE_READ_BUFFER_SIZE              0x0002
+#define OCF_LE_READ_LOCAL_SUPPORTED_FEATURES 0x0003
+#define OCF_LE_SET_RANDOM_ADDRESS            0x0005
+#define OCF_LE_SET_ADVERTISING_PARAMETERS    0x0006
+#define OCF_LE_SET_ADVERTISING_DATA          0x0008
+#define OCF_LE_SET_SCAN_RESPONSE_DATA        0x0009
+#define OCF_LE_SET_ADVERTISE_ENABLE          0x000a
+#define OCF_LE_SET_SCAN_PARAMETERS           0x000b
+#define OCF_LE_SET_SCAN_ENABLE               0x000c
+#define OCF_LE_CREATE_CONNECTION             0x000d
+
+#define OGF_VENDOR_CTL       0x3F
+#define OCF_LE_LTK_NEG_REPLY 0x001B
+
+/* Obtain OGF from OpCode */
+#define BT_OGF(opcode) (((opcode) >> 10) & 0x3f)
+/* Obtain OCF from OpCode */
+#define BT_OCF(opcode) ((opcode) & 0x3FF)
+
+#define BT_OP(ogf, ocf) ((ocf) | ((ogf) << 10))
+
+#define BT_HCI_EVT_DISCONN_COMPLETE      0x05 // HCI_Disconnection_Complete
+#define BT_HCI_EVT_QOS_SETUP_COMPLETE    0x0d
+#define BT_HCI_EVT_CMD_COMPLETE          0x0e
+#define BT_HCI_EVT_CMD_STATUS            0x0f
+#define BT_HCI_EVT_HARDWARE_ERROR        0x10
+#define BT_HCI_EVT_NUM_COMPLETED_PACKETS 0x13
+#define BT_HCI_EVT_LE_META               0x3e // HCI_LE_Connection_Complete
+
+#define HCI_LE_CONNECTION_COMPLETE 0x01
+#define HCI_LE_ADVERTISING_REPORT  0x02
+
+// Consider making this an enum that shifts a bit in the apropriate amount
+#define CAP_TWIST_AND_GO 0x02
+#define CAP_ALLOW_TAP    0x04
+#define CAP_APP_SPECIFIC 0x08
+#define CAP_ENHANCED_TAP 0x40
+
+static uint8_t seos_reader_service_backwards[] =
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00};
+static uint8_t seos_cred_service_backwards[] =
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x01, 0x98, 0x00, 0x00};
+
+static uint8_t empty_mac[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+// Occationally scan stop's completion doesn't get caught.
+// Use the timer callback to call it again
+void seos_hci_timer(void* context) {
+    FURI_LOG_I(TAG, "RUN TIMER");
+    SeosHci* seos_hci = (SeosHci*)context;
+    if(seos_hci->mode == BLE_PERIPHERAL) {
+        seos_hci_enable_advertising(seos_hci, seos_hci->adv_status);
+    } else if(seos_hci->mode == BLE_CENTRAL) {
+        seos_hci_set_scan(seos_hci, seos_hci->scan_status);
+    }
+}
+
+void seos_hci_clear_known_addresses(SeosHci* seos_hci) {
+    for(size_t i = 0; i < MAX_SCANNED_ADDRESS; i++) {
+        ScanAddress* scan_address = &seos_hci->scanned_addresses[i];
+        scan_address->used = false;
+        memset(scan_address->address, 0, MAC_ADDRESS_LEN);
+    }
+}
+
+SeosHci* seos_hci_alloc(Seos* seos) {
+    SeosHci* seos_hci = malloc(sizeof(SeosHci));
+    memset(seos_hci, 0, sizeof(SeosHci));
+
+    seos_hci->device_found = false;
+    seos_hci->connection_handle = 0;
+
+    seos_hci->seos = seos;
+    seos_hci->seos_hci_h5 = seos_hci_h5_alloc();
+    seos_hci->timer = furi_timer_alloc(seos_hci_timer, FuriTimerTypeOnce, seos_hci);
+    seos_hci_h5_set_init_callback(seos_hci->seos_hci_h5, seos_hci_init, seos_hci);
+    seos_hci_h5_set_receive_callback(seos_hci->seos_hci_h5, seos_hci_recv, seos_hci);
+
+    seos_hci_clear_known_addresses(seos_hci);
+
+    return seos_hci;
+}
+
+void seos_hci_free(SeosHci* seos_hci) {
+    furi_assert(seos_hci);
+    if(furi_timer_is_running(seos_hci->timer)) {
+        FURI_LOG_D(TAG, "clear timer");
+        furi_timer_stop(seos_hci->timer);
+    }
+    furi_timer_free(seos_hci->timer);
+
+    seos_hci_h5_set_init_callback(seos_hci->seos_hci_h5, NULL, NULL);
+    seos_hci_h5_set_receive_callback(seos_hci->seos_hci_h5, NULL, NULL);
+
+    seos_hci_h5_free(seos_hci->seos_hci_h5);
+    free(seos_hci);
+}
+
+void seos_hci_start(SeosHci* seos_hci, BleMode mode, FlowMode flow_mode) {
+    seos_hci->device_found = false;
+    seos_hci->connection_handle = 0;
+    seos_hci->flow_mode = flow_mode;
+    seos_hci->mode = mode;
+    seos_hci_h5_start(seos_hci->seos_hci_h5);
+}
+
+void seos_hci_stop(SeosHci* seos_hci) {
+    if(seos_hci->connection_handle > 0) {
+        uint16_t opcode = BT_OP(OGF_LINK_CTL, OCF_DISCONNECT);
+        BitBuffer* disconnect = bit_buffer_alloc(5);
+        bit_buffer_append_bytes(disconnect, (uint8_t*)&opcode, sizeof(opcode));
+        bit_buffer_append_bytes(
+            disconnect,
+            (uint8_t*)&seos_hci->connection_handle,
+            sizeof(seos_hci->connection_handle));
+        bit_buffer_append_byte(disconnect, 0x00);
+        seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, disconnect);
+    }
+
+    seos_hci->device_found = false;
+    seos_hci->connection_handle = 0;
+    seos_hci_h5_stop(seos_hci->seos_hci_h5);
+    if(seos_hci->mode == BLE_PERIPHERAL) {
+        if(seos_hci->adv_status) {
+            seos_hci_enable_advertising(seos_hci, false);
+        }
+    } else if(seos_hci->mode == BLE_CENTRAL) {
+        if(seos_hci->scan_status) {
+            seos_hci_set_scan(seos_hci, false);
+        }
+    }
+    if(furi_timer_is_running(seos_hci->timer)) {
+        FURI_LOG_D(TAG, "clear timer");
+        furi_timer_stop(seos_hci->timer);
+    }
+    seos_hci_clear_known_addresses(seos_hci);
+}
+
+bool seos_hci_known_address(SeosHci* seos_hci, const uint8_t Address[MAC_ADDRESS_LEN]) {
+    // Does it exist in the list?
+    for(size_t i = 0; i < MAX_SCANNED_ADDRESS; i++) {
+        ScanAddress* scan_address = &seos_hci->scanned_addresses[i];
+        if(scan_address->used) {
+            if(memcmp(Address, scan_address->address, MAC_ADDRESS_LEN) == 0) {
+                return true;
+            }
+        }
+    }
+
+    // Not in list, add
+    for(size_t i = 0; i < MAX_SCANNED_ADDRESS; i++) {
+        ScanAddress* scan_address = &seos_hci->scanned_addresses[i];
+        if(!scan_address->used) {
+            memcpy(scan_address->address, Address, MAC_ADDRESS_LEN);
+            scan_address->used = true;
+            // It wasn't previously known
+            return false;
+        }
+    }
+
+    return false;
+}
+
+void seos_hci_handle_event_cmd_complete_ogf_host(SeosHci* seos_hci, uint16_t OCF, BitBuffer* frame) {
+    UNUSED(frame);
+
+    BitBuffer* message = bit_buffer_alloc(128);
+    switch(OCF) {
+    case OCF_RESET:
+        uint8_t le_read_local_supported_features[] = {0x03, 0x20, 0x00};
+        bit_buffer_append_bytes(
+            message, le_read_local_supported_features, sizeof(le_read_local_supported_features));
+        break;
+    case OCF_SET_EVENT_MASK:
+        uint8_t set_le_event_mask[] = {
+            0x01, 0x20, 0x08, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+        bit_buffer_append_bytes(message, set_le_event_mask, sizeof(set_le_event_mask));
+        break;
+    case OCF_READ_LE_HOST_SUPPORTED:
+        FURI_LOG_D(TAG, "OCF_READ_LE_HOST_SUPPORTED");
+        break;
+    case OCF_WRITE_LE_HOST_SUPPORTED:
+        FURI_LOG_D(TAG, "OCF_WRITE_LE_HOST_SUPPORTED");
+        uint8_t read_le_host_supported[] = {0x6c, 0x0c, 0x00};
+        bit_buffer_append_bytes(message, read_le_host_supported, sizeof(read_le_host_supported));
+        break;
+    default:
+        FURI_LOG_W(TAG, "Unhandled OCF %04x", OCF);
+        break;
+    }
+    if(bit_buffer_get_size_bytes(message) > 0) {
+        seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    }
+    bit_buffer_free(message);
+}
+
+void seos_hci_handle_event_cmd_complete_ogf_info(SeosHci* seos_hci, uint16_t OCF, BitBuffer* frame) {
+    UNUSED(frame);
+
+    BitBuffer* message = bit_buffer_alloc(128);
+    switch(OCF) {
+    case OCF_READ_LOCAL_VERSION:
+        FURI_LOG_D(TAG, "OCF_READ_LOCAL_VERSION");
+        uint8_t read_bd_addr[] = {0x09, 0x10, 0x00};
+        bit_buffer_append_bytes(message, read_bd_addr, sizeof(read_bd_addr));
+        break;
+    case OCF_READ_BD_ADDR:
+        // 040e0a05091000 e2f284 dad4d4
+        FURI_LOG_D(TAG, "OCF_READ_BD_ADDR");
+        if(memcmp(bit_buffer_get_data(frame) + 7, empty_mac, sizeof(empty_mac)) == 0) {
+            uint8_t vendor_set_addr[] = {0x06, 0xfc, 0x06, 0x0, 0x0, 0x1, 0x2, 0x21, 0xAD};
+            bit_buffer_append_bytes(message, vendor_set_addr, sizeof(vendor_set_addr));
+        } else {
+            uint16_t opcode = BT_OP(OGF_LE_CTL, OCF_LE_READ_BUFFER_SIZE);
+            uint8_t length = 0;
+            bit_buffer_append_bytes(message, (uint8_t*)&opcode, sizeof(opcode));
+            bit_buffer_append_byte(message, length);
+        }
+        break;
+    default:
+        FURI_LOG_W(TAG, "Unhandled OCF %04x", OCF);
+        break;
+    }
+
+    if(bit_buffer_get_size_bytes(message) > 0) {
+        seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    }
+    bit_buffer_free(message);
+}
+
+void seos_hci_handle_event_cmd_complete_ogf_le(SeosHci* seos_hci, uint16_t OCF, BitBuffer* frame) {
+    UNUSED(frame);
+
+    BitBuffer* message = bit_buffer_alloc(128);
+    switch(OCF) {
+    case OCF_LE_SET_EVENT_MASK:
+        uint8_t read_local_version[] = {0x01, 0x10, 0x00};
+        bit_buffer_append_bytes(message, read_local_version, sizeof(read_local_version));
+        break;
+    case OCF_LE_READ_BUFFER_SIZE:
+        FURI_LOG_D(TAG, "OCF_LE_READ_BUFFER_SIZE");
+        uint8_t le_set_random_address[] = {0x05, 0x20, 0x06, 0xCA, 0xFE, 0x00, 0x00, 0x00, 0x03};
+        bit_buffer_append_bytes(message, le_set_random_address, sizeof(le_set_random_address));
+        break;
+    case OCF_LE_SET_ADVERTISING_DATA:
+        seos_hci_enable_advertising(seos_hci, true);
+        break;
+    case OCF_LE_SET_SCAN_RESPONSE_DATA:
+        uint8_t flow_mode_byte = seos_hci->flow_mode == FLOW_READER ? 0x00 : 0x01;
+        // TODO: Use seos_reader_service_backwards
+        uint8_t adv_data[] = {0x08, 0x20, 0x20, 0x15,           0x02, 0x01, 0x06, 0x11, 0x07,
+                              0x02, 0x00, 0x00, 0x7a,           0x17, 0x00, 0x00, 0x80, 0x00,
+                              0x10, 0x00, 0x00, flow_mode_byte, 0x98, 0x00, 0x00, 0x00, 0x00,
+                              0x00, 0x00, 0x00, 0x00,           0x00, 0x00, 0x00, 0x00};
+        bit_buffer_append_bytes(message, adv_data, sizeof(adv_data));
+        break;
+    case OCF_LE_SET_ADVERTISE_ENABLE:
+        if(furi_timer_is_running(seos_hci->timer)) {
+            FURI_LOG_D(TAG, "clear timer");
+            furi_timer_stop(seos_hci->timer);
+        }
+        uint8_t status = bit_buffer_get_byte(frame, 6);
+        if(status == 0) {
+            if(seos_hci->adv_status) {
+                FURI_LOG_I(TAG, "*** Advertising enabled ***");
+                view_dispatcher_send_custom_event(
+                    seos_hci->seos->view_dispatcher, SeosCustomEventAdvertising);
+
+            } else {
+                FURI_LOG_I(TAG, "*** Advertising disabled ***");
+            }
+        } else {
+            FURI_LOG_W(TAG, "Advertising enabled FAILED");
+        }
+        break;
+    case OCF_LE_SET_SCAN_PARAMETERS:
+        seos_hci_set_scan(seos_hci, true);
+        break;
+    case OCF_LE_SET_SCAN_ENABLE:
+        if(furi_timer_is_running(seos_hci->timer)) {
+            FURI_LOG_D(TAG, "clear timer");
+            furi_timer_stop(seos_hci->timer);
+        }
+        if(seos_hci->scan_status) { // enabled
+            FURI_LOG_I(TAG, "Scan enable complete. new state: %d", seos_hci->scan_status);
+            view_dispatcher_send_custom_event(
+                seos_hci->seos->view_dispatcher, SeosCustomEventScan);
+        } else if(seos_hci->device_found) {
+            // Scanning stopped, try to connect
+            seos_hci_connect(seos_hci);
+        }
+
+        break;
+    case OCF_LE_READ_LOCAL_SUPPORTED_FEATURES:
+        // FURI_LOG_D(TAG, "Local Supported Features");
+        uint8_t set_event_mask[] = {
+            0x01, 0x0c, 0x08, 0xff, 0xff, 0xfb, 0xff, 0x07, 0xf8, 0xbf, 0x3d};
+        bit_buffer_append_bytes(message, set_event_mask, sizeof(set_event_mask));
+        break;
+    case OCF_LE_SET_RANDOM_ADDRESS:
+        // FURI_LOG_D(TAG, "opcode = %04x", BT_OP(0x3f, 0x0006)); <--- reverse this in byte array
+        uint8_t vendor_set_addr[] = {0x06, 0xfc, 0x06, 0x0, 0x0, 0x1, 0x2, 0x21, 0xAD};
+        bit_buffer_append_bytes(message, vendor_set_addr, sizeof(vendor_set_addr));
+        break;
+    case OCF_LE_SET_ADVERTISING_PARAMETERS:
+        // TODO: make this more dynamic
+        uint8_t capabilities = CAP_TWIST_AND_GO | CAP_ALLOW_TAP | CAP_APP_SPECIFIC |
+                               CAP_ENHANCED_TAP;
+        int8_t tap_rssi = -75;
+        int8_t twist_rssi = -75;
+        int8_t seamless_rssi = -75;
+        int8_t app_rssi = -75;
+        uint8_t mfg_data[] = {0x14,     0xff,       0x2e,          0x01,     0x15, capabilities,
+                              tap_rssi, twist_rssi, seamless_rssi, app_rssi, 0x2a, 0x46,
+                              0x4c,     0x30,       0x4b,          0x37,     0x5a, 0x30,
+                              0x31,     0x55,       0x31};
+        uint8_t device_name[] = {0x08, 0x09, 0x46, 0x6c, 0x69, 0x70, 0x70, 0x65, 0x72};
+        uint8_t ad_len = 0;
+        ad_len += sizeof(device_name);
+        ad_len += sizeof(mfg_data);
+
+        uint8_t header[] = {0x09, 0x20, 0x20, ad_len};
+        bit_buffer_append_bytes(message, header, sizeof(header));
+        bit_buffer_append_bytes(message, device_name, sizeof(device_name));
+        bit_buffer_append_bytes(message, mfg_data, sizeof(mfg_data));
+        for(int i = 0; i < (31 - ad_len); i++) {
+            bit_buffer_append_byte(message, 0);
+        }
+        break;
+    default:
+        FURI_LOG_W(TAG, "Unhandled OCF %04x", OCF);
+        break;
+    }
+
+    if(bit_buffer_get_size_bytes(message) > 0) {
+        seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    }
+    bit_buffer_free(message);
+}
+
+void seos_hci_handle_event_cmd_complete_ogf_vendor(
+    SeosHci* seos_hci,
+    uint16_t OCF,
+    BitBuffer* frame) {
+    UNUSED(frame);
+
+    BitBuffer* message = bit_buffer_alloc(128);
+    switch(OCF) {
+    case 0x0006:
+        if(seos_hci->mode == BLE_PERIPHERAL) {
+            // Flipper as Reader
+            uint8_t adv_param[] = {
+                0x06,
+                0x20,
+                0x0f,
+                0xa0,
+                0x00,
+                0xa0,
+                0x00,
+                0x00,
+                0x00,
+                0x01,
+                0xDE,
+                0xAF,
+                0xBE,
+                0xEF,
+                0xCA,
+                0xFE,
+                0x07,
+                0x00};
+            bit_buffer_append_bytes(message, adv_param, sizeof(adv_param));
+        } else if(seos_hci->mode == BLE_CENTRAL) {
+            // Flipper as device/credential
+            seos_hci_send_scan_params(seos_hci);
+        }
+        break;
+    default:
+        FURI_LOG_W(TAG, "Unhandled OCF %04x", OCF);
+        break;
+    }
+
+    if(bit_buffer_get_size_bytes(message) > 0) {
+        seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    }
+    bit_buffer_free(message);
+}
+
+void seos_hci_handle_event_cmd_complete(SeosHci* seos_hci, BitBuffer* frame) {
+    BitBuffer* message = bit_buffer_alloc(128);
+    const uint8_t* data = bit_buffer_get_data(frame);
+
+    uint8_t event_type = data[0];
+    uint8_t sub_event_type = data[1];
+    uint8_t ncmd = data[3];
+    uint16_t cmd = data[5] << 8 | data[4];
+    uint8_t status = data[6];
+    if(status == 0) {
+        /*
+        FURI_LOG_D(
+            TAG,
+            "event %d sub event %d ncmd %d cmd %04x status %d",
+            event_type,
+            sub_event_type,
+            ncmd,
+            cmd,
+            status);
+            */
+    } else {
+        FURI_LOG_W(
+            TAG,
+            "event %d sub event %d ncmd %d cmd %d status %d",
+            event_type,
+            sub_event_type,
+            ncmd,
+            cmd,
+            status);
+        bit_buffer_free(message);
+        return;
+    }
+
+    uint16_t OGF = BT_OGF(cmd);
+    uint16_t OCF = BT_OCF(cmd);
+    // FURI_LOG_D(TAG, "OGF = %04x OCF = %04x", OGF, OCF);
+
+    switch(OGF) {
+    case OGF_HOST_CTL:
+        seos_hci_handle_event_cmd_complete_ogf_host(seos_hci, OCF, frame);
+        break;
+    case OGF_INFO_PARAM:
+        seos_hci_handle_event_cmd_complete_ogf_info(seos_hci, OCF, frame);
+        break;
+    case OGF_LE_CTL:
+        seos_hci_handle_event_cmd_complete_ogf_le(seos_hci, OCF, frame);
+        break;
+    case OGF_VENDOR_CTL:
+        seos_hci_handle_event_cmd_complete_ogf_vendor(seos_hci, OCF, frame);
+        break;
+    default:
+        FURI_LOG_W(TAG, "Unhandled OGF %04x", OGF);
+        break;
+    }
+
+    bit_buffer_free(message);
+}
+
+void seos_hci_enable_advertising(SeosHci* seos_hci, bool enable) {
+    seos_hci->adv_status = enable;
+    FURI_LOG_I(TAG, "Enable Advertising: %s", enable ? "true" : "false");
+    uint8_t adv_enable[] = {0x0a, 0x20, 0x01, enable ? 0x01 : 0x00};
+    BitBuffer* message = bit_buffer_alloc(sizeof(adv_enable));
+    bit_buffer_append_bytes(message, adv_enable, sizeof(adv_enable));
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    bit_buffer_free(message);
+
+    FURI_LOG_I(TAG, "Start timer to make sure adv change ran");
+    size_t delay = 1000 /*ms*/ / (1000.0f / furi_kernel_get_tick_frequency());
+    furi_check(furi_timer_start(seos_hci->timer, delay) == FuriStatusOk);
+}
+
+void seos_hci_send_scan_params(SeosHci* seos_hci) {
+    uint8_t LE_Scan_Type = 0x00;
+    uint8_t Scanning_Filter_Policy = 0x00;
+    uint16_t opcode = BT_OP(OGF_LE_CTL, OCF_LE_SET_SCAN_PARAMETERS);
+    uint8_t scan_param[] = {
+        0xff, 0xff, 0x07, LE_Scan_Type, 0x10, 0x00, 0x10, 0x00, 0x00, Scanning_Filter_Policy};
+    BitBuffer* message = bit_buffer_alloc(sizeof(scan_param));
+    memcpy(scan_param, (uint8_t*)&opcode, sizeof(opcode));
+    bit_buffer_append_bytes(message, scan_param, sizeof(scan_param));
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    bit_buffer_free(message);
+}
+
+void seos_hci_set_scan(SeosHci* seos_hci, bool enable) {
+    FURI_LOG_I(TAG, "Start Scan: %s", enable ? "true" : "false");
+    seos_hci->scan_status = enable;
+    uint16_t opcode = BT_OP(OGF_LE_CTL, OCF_LE_SET_SCAN_ENABLE);
+    uint8_t set_scan[] = {0xff, 0xff, 0x02, enable ? 0x01 : 0x00, 0x00};
+    memcpy(set_scan, (uint8_t*)&opcode, sizeof(opcode));
+    BitBuffer* message = bit_buffer_alloc(sizeof(set_scan));
+    bit_buffer_append_bytes(message, set_scan, sizeof(set_scan));
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    bit_buffer_free(message);
+
+    FURI_LOG_I(TAG, "Start timer to make sure set scan ran");
+    size_t delay = 100 /*ms*/ / (1000.0f / furi_kernel_get_tick_frequency());
+    furi_check(furi_timer_start(seos_hci->timer, delay) == FuriStatusOk);
+}
+
+// TODO: test this: hci create le conn - writing: 010d 2019 6000 3000 00 01 2db88ee137c3 000600120000002a0004000600
+void seos_hci_connect(SeosHci* seos_hci) {
+    FURI_LOG_I(TAG, "seos_hci_connect");
+    uint16_t opcode = BT_OP(OGF_LE_CTL, OCF_LE_CREATE_CONNECTION);
+    // Values arbitrarily copied from https://stackoverflow.com/questions/71250571/how-to-send-le-extended-create-connection-in-ble-with-raspberry-pi
+    uint8_t connect[] = {
+        0xff,
+        0xff, //opcode
+        0x19, // length
+        0x60,
+        0x00, // LE_Scan_Interval
+        0x60,
+        0x00, // LE_Scan_Window
+        0x00, // Initiator_Filter_Policy
+        seos_hci->address_type, // Peer_Address_Type
+        0xFF,
+        0xFF,
+        0xFF,
+        0xFF,
+        0xFF,
+        0xFF, // Peer_Address
+        0x01, // Own_Address_Type
+        0x18,
+        0x00, // Connection_Interval_Min
+        0x28,
+        0x00, // Connection_Interval_Max
+        0x00,
+        0x00, // Max_Latency
+        0x90,
+        0x00, // Supervision_Timeout
+        0x00,
+        0x00, // Min_CE_Length
+        0x00,
+        0x00, // Max_CE_Length
+    };
+    memcpy(connect, (uint8_t*)&opcode, sizeof(opcode));
+    memcpy(connect + 9, seos_hci->address, 6);
+    BitBuffer* message = bit_buffer_alloc(sizeof(connect));
+    bit_buffer_append_bytes(message, connect, sizeof(connect));
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    bit_buffer_free(message);
+}
+
+void seos_hci_handle_event_le_meta(SeosHci* seos_hci, BitBuffer* frame) {
+    const uint8_t* data = bit_buffer_get_data(frame);
+    // uint8_t length = data[2];
+    uint8_t subevent_code = data[3];
+
+    switch(subevent_code) {
+    case HCI_LE_CONNECTION_COMPLETE:
+        uint8_t status = data[4];
+        if(status != 0x00) {
+            FURI_LOG_W(TAG, "Connection complete with non-zero status");
+            return;
+        }
+
+        seos_hci->connection_handle = data[6] << 8 | data[5];
+        uint8_t role = data[7];
+        uint8_t peer_address_type = data[8];
+        // and more...
+
+        FURI_LOG_D(
+            TAG,
+            "connection complete: handle %04x role %d peer_address_type %d",
+            seos_hci->connection_handle,
+            role,
+            peer_address_type);
+        view_dispatcher_send_custom_event(
+            seos_hci->seos->view_dispatcher, SeosCustomEventConnected);
+
+        if(role == 0x00) { // I'm a central!
+            if(seos_hci->central_connection_callback) {
+                seos_hci->central_connection_callback(seos_hci->central_connection_context);
+            } else {
+                FURI_LOG_W(TAG, "No central_connection_callback defined");
+            }
+        } else if(role == 0x01) { // I'm a peripheral!
+        }
+
+        break;
+    case HCI_LE_ADVERTISING_REPORT:
+        // Prevent interruptions to handling a device by a second advertisement
+        if(seos_hci->device_found) {
+            break;
+        }
+        // TODO: Support single packet with multiple reports
+        uint8_t num_reports = data[4];
+        uint8_t Event_Type = data[5];
+        uint8_t Address_Type = data[6];
+        const uint8_t* Address = data + 7;
+        uint8_t Data_Length = data[13];
+        const uint8_t* adv_data = data + 14;
+        char name[20];
+        memset(name, 0, sizeof(name));
+        if(Event_Type != 0 || Data_Length < sizeof(seos_reader_service_backwards)) {
+            break;
+        }
+
+        /*
+        FURI_LOG_D(
+            TAG,
+            "Adv %d reports: event type %d address type %d data len %d",
+            num_reports,
+            Event_Type,
+            Address_Type,
+            Data_Length);
+            */
+        // seos_log_buffer(TAG, "ADV_IND", (uint8_t*)adv_data, Data_Length);
+
+        uint8_t i = 0;
+        do {
+            uint8_t l = adv_data[i++];
+            uint8_t t = adv_data[i++];
+            const uint8_t* val = adv_data + i;
+            i += l - 1; // subtract one so we don't overcount the type byte
+            switch(t) {
+            case 0x07:
+                if(seos_hci->flow_mode == FLOW_CRED) {
+                    // You're acting like a credential, looking for readers to connect and send to
+                    if(memcmp(
+                           val,
+                           seos_reader_service_backwards,
+                           sizeof(seos_reader_service_backwards)) == 0) {
+                        seos_hci->device_found = true;
+                    }
+                } else if(seos_hci->flow_mode == FLOW_READER) {
+                    if(memcmp(
+                           val,
+                           seos_cred_service_backwards,
+                           sizeof(seos_cred_service_backwards)) == 0) {
+                        seos_hci->device_found = true;
+                    }
+                } else if(seos_hci->flow_mode == FLOW_READER_SCANNER) {
+                    // Reader scanner looks for readers, it doesn't act like a reader (as in FLOW_READER)
+                    if(memcmp(
+                           val,
+                           seos_reader_service_backwards,
+                           sizeof(seos_reader_service_backwards)) == 0) {
+                        if(!seos_hci_known_address(seos_hci, Address)) {
+                            notification_message(
+                                seos_hci->seos->notifications, &sequence_single_vibro);
+                        }
+                    }
+                } else if(seos_hci->flow_mode == FLOW_CRED_SCANNER) {
+                    // Cred scanner looks for devices advertising credential service, it doesn't act like a credential (as in FLOW_CRED)
+                    if(memcmp(
+                           val,
+                           seos_cred_service_backwards,
+                           sizeof(seos_cred_service_backwards)) == 0) {
+                        if(!seos_hci_known_address(seos_hci, Address)) {
+                            notification_message(
+                                seos_hci->seos->notifications, &sequence_single_vibro);
+                        }
+                    }
+                }
+                break;
+            case 0x08: // Short device name
+            case 0x09: // full device name
+                memcpy(name, val, l - 1);
+                break;
+            }
+        } while(i < Data_Length - 1);
+
+        seos_hci->adv_report_count += num_reports;
+
+        if(seos_hci->device_found) {
+            FURI_LOG_I(TAG, "Matched Seos Reader Service: %s", name);
+            seos_hci->address_type = Address_Type;
+            memcpy(seos_hci->address, Address, sizeof(seos_hci->address));
+            seos_hci_set_scan(seos_hci, false);
+            view_dispatcher_send_custom_event(
+                seos_hci->seos->view_dispatcher, SeosCustomEventFound);
+        }
+        break;
+    default:
+        FURI_LOG_W(TAG, "LE Meta event with unknown subevent code");
+        break;
+    }
+}
+
+void seos_hci_event_handler(SeosHci* seos_hci, BitBuffer* frame) {
+    const uint8_t* data = bit_buffer_get_data(frame);
+    uint8_t sub_event_type = data[1];
+    // uint8_t length = data[2];
+
+    if(sub_event_type == BT_HCI_EVT_CMD_STATUS) {
+        struct bt_hci_evt_cmd_status {
+            uint8_t status;
+            uint8_t ncmd;
+            uint16_t opcode;
+        } __packed;
+        struct bt_hci_evt_cmd_status* status = (struct bt_hci_evt_cmd_status*)(data + 3);
+        if(status->status == 0) {
+            /*
+            FURI_LOG_D(
+                TAG,
+                "Status: status %d ncmd 0x%02x opcode %04x",
+                status->status,
+                status->ncmd,
+                status->opcode);
+                */
+        } else {
+            // Unknown HCI command (0x01)
+            FURI_LOG_W(
+                TAG,
+                "Status: status %d ncmd 0x%02x opcode %04x",
+                status->status,
+                status->ncmd,
+                status->opcode);
+        }
+    } else if(sub_event_type == BT_HCI_EVT_CMD_COMPLETE) {
+        seos_hci_handle_event_cmd_complete(seos_hci, frame);
+    } else if(sub_event_type == BT_HCI_EVT_LE_META) {
+        seos_hci_handle_event_le_meta(seos_hci, frame);
+    } else if(sub_event_type == BT_HCI_EVT_DISCONN_COMPLETE) {
+        // FURI_LOG_D(TAG, "BT_HCI_EVT_CMD_COMPLETE");
+        seos_hci_handle_event_cmd_complete(seos_hci, frame);
+    } else if(sub_event_type == BT_HCI_EVT_LE_META) {
+        // FURI_LOG_D(TAG, "BT_HCI_EVT_LE_META");
+        seos_hci_handle_event_le_meta(seos_hci, frame);
+    } else if(sub_event_type == BT_HCI_EVT_DISCONN_COMPLETE) {
+        // FURI_LOG_D(TAG, "BT_HCI_EVT_DISCONN_COMPLETE");
+        seos_hci->connection_handle = 0;
+
+        if(seos_hci->mode == BLE_PERIPHERAL) {
+            if(seos_hci->adv_status) {
+                FURI_LOG_W(TAG, "Disconnect. Restart Advertising");
+                seos_hci_enable_advertising(seos_hci, true);
+            }
+        } else if(seos_hci->mode == BLE_CENTRAL) {
+            FURI_LOG_W(TAG, "Disconnect. Scan again");
+            seos_hci->device_found = false;
+            seos_hci_set_scan(seos_hci, true);
+        }
+    } else if(sub_event_type == BT_HCI_EVT_HARDWARE_ERROR) {
+        FURI_LOG_W(TAG, "BT_HCI_EVT_HARDWARE_ERROR");
+    } else if(sub_event_type == BT_HCI_EVT_NUM_COMPLETED_PACKETS) {
+        // FURI_LOG_D(TAG, "BT_HCI_EVT_NUM_COMPLETED_PACKETS");
+        struct bt_hci_evt_num_completed_packets {
+            uint8_t num_handles;
+            uint16_t handle;
+            uint16_t count;
+        } __attribute__((packed));
+        struct bt_hci_evt_num_completed_packets* evt =
+            (struct bt_hci_evt_num_completed_packets*)(data + 3);
+        if(evt->num_handles == 1) {
+            // FURI_LOG_D(TAG, "Number of completed packets for %04x: %d", evt->handle, evt->count);
+        } else {
+            FURI_LOG_D(TAG, "Number of completed packets for multiple handles");
+        }
+        if(seos_hci->completed_packets_callback) {
+            seos_hci->completed_packets_callback(seos_hci->completed_packets_context);
+        }
+    } else {
+        FURI_LOG_W(TAG, "Unhandled event subtype %02x", sub_event_type);
+    }
+}
+
+void seos_hci_acldata_send(SeosHci* seos_hci, uint8_t flags, BitBuffer* tx) {
+    // seos_log_buffer("seos_hci_acldata_send", tx);
+    uint16_t tx_len = bit_buffer_get_size_bytes(tx);
+
+    uint16_t handle = seos_hci->connection_handle | (flags << 12);
+
+    BitBuffer* response = bit_buffer_alloc(tx_len + sizeof(handle) + sizeof(tx_len));
+    bit_buffer_append_bytes(response, (uint8_t*)&handle, sizeof(handle));
+    bit_buffer_append_bytes(response, (uint8_t*)&tx_len, sizeof(tx_len));
+    // tx
+    bit_buffer_append_bytes(response, bit_buffer_get_data(tx), tx_len);
+
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_ACLDATA_PKT, response);
+
+    bit_buffer_free(response);
+}
+
+void seos_hci_acldata_handler(SeosHci* seos_hci, BitBuffer* frame) {
+    const uint8_t* data = bit_buffer_get_data(frame);
+    // 0 is 0x02 for ACL DATA
+
+    uint16_t handle = (data[2] << 8 | data[1]) & 0x0FFF;
+    uint8_t flags = data[2] >> 4;
+    uint16_t length = data[4] << 8 | data[3];
+
+    /*
+    uint8_t Broadcast_Flag = flags >> 2;
+    uint8_t Packet_Boundary_Flag = flags & 0x03;
+
+    FURI_LOG_D(
+        TAG,
+        "ACLDATA handle %04x Broadcast_Flag %02x Packet_Boundary_Flag %02x length %d",
+        handle,
+        Broadcast_Flag,
+        Packet_Boundary_Flag,
+        length);
+        */
+    if(handle != seos_hci->connection_handle) {
+        FURI_LOG_W(TAG, "Mismatched handle values");
+    }
+
+    BitBuffer* pdu = bit_buffer_alloc(length);
+    bit_buffer_append_bytes(pdu, data + 1 /*ACL DATA */ + sizeof(handle) + sizeof(length), length);
+    if(seos_hci->receive_callback) {
+        seos_hci->receive_callback(seos_hci->receive_callback_context, handle, flags, pdu);
+    }
+    bit_buffer_free(pdu);
+}
+
+size_t seos_hci_recv(void* context, BitBuffer* frame) {
+    SeosHci* seos_hci = (SeosHci*)context;
+    // seos_log_buffer("HCI Frame", frame);
+
+    const uint8_t* data = bit_buffer_get_data(frame);
+    uint8_t event_type = data[0];
+    // TODO: consider `bit_buffer_starts_with_byte`
+    switch(event_type) {
+    case HCI_EVENT_PKT:
+        seos_hci_event_handler(seos_hci, frame);
+        break;
+    case HCI_ACLDATA_PKT:
+        seos_hci_acldata_handler(seos_hci, frame);
+        break;
+    default:
+        FURI_LOG_W(TAG, "Haven't added support for other HCI commands yet: %02x", event_type);
+        seos_log_bitbuffer(TAG, "unhandled", frame);
+        break;
+    }
+
+    return 0;
+}
+
+// TODO: Consider making this a general "when the state changes" callback which would check if H5 is active (or needs to be reset)
+void seos_hci_init(void* context) {
+    SeosHci* seos_hci = (SeosHci*)context;
+    BitBuffer* message = bit_buffer_alloc(128);
+
+    uint8_t reset[] = {0x03, 0x0c, 0x00};
+    bit_buffer_append_bytes(message, reset, sizeof(reset));
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    view_dispatcher_send_custom_event(seos_hci->seos->view_dispatcher, SeosCustomEventHCIInit);
+
+    bit_buffer_free(message);
+}
+
+void seos_hci_set_receive_callback(
+    SeosHci* seos_hci,
+    SeosHciReceiveCallback callback,
+    void* context) {
+    seos_hci->receive_callback = callback;
+    seos_hci->receive_callback_context = context;
+}
+
+void seos_hci_set_completed_packets_callback(
+    SeosHci* seos_hci,
+    SeosHciCompletedPacketsCallback callback,
+    void* context) {
+    seos_hci->completed_packets_callback = callback;
+    seos_hci->completed_packets_context = context;
+}
+
+void seos_hci_set_central_connection_callback(
+    SeosHci* seos_hci,
+    SeosHciCentralConnectionCallback callback,
+    void* context) {
+    seos_hci->central_connection_callback = callback;
+    seos_hci->central_connection_context = context;
+}

+ 78 - 0
seos/seos_hci.h

@@ -0,0 +1,78 @@
+#pragma once
+
+#include <furi.h>
+#include "seos_hci_h5.h"
+#include "seos_common.h"
+
+#define MAC_ADDRESS_LEN     6
+#define MAX_SCANNED_ADDRESS 5
+
+typedef void (
+    *SeosHciReceiveCallback)(void* context, uint16_t handle, uint8_t flags, BitBuffer* pdu);
+
+typedef void (*SeosHciCompletedPacketsCallback)(void* context);
+typedef void (*SeosHciCentralConnectionCallback)(void* context);
+
+typedef struct {
+    uint8_t address[MAC_ADDRESS_LEN];
+    bool used;
+} ScanAddress;
+
+typedef struct {
+    Seos* seos;
+    SeosHciH5* seos_hci_h5;
+    uint16_t connection_handle;
+
+    SeosHciReceiveCallback receive_callback;
+    void* receive_callback_context;
+
+    SeosHciCompletedPacketsCallback completed_packets_callback;
+    void* completed_packets_context;
+
+    SeosHciCentralConnectionCallback central_connection_callback;
+    void* central_connection_context;
+
+    BleMode mode;
+
+    FlowMode flow_mode;
+
+    bool scan_status;
+    bool adv_status;
+    uint8_t address[MAC_ADDRESS_LEN];
+    uint8_t address_type;
+
+    size_t adv_report_count;
+    bool device_found;
+
+    FuriTimer* timer;
+
+    ScanAddress scanned_addresses[MAX_SCANNED_ADDRESS];
+} SeosHci;
+
+SeosHci* seos_hci_alloc(Seos* seos);
+void seos_hci_free(SeosHci* seos_hci);
+void seos_hci_start(SeosHci* seos_hci, BleMode mode, FlowMode flow_mode);
+void seos_hci_stop(SeosHci* seos_hci);
+size_t seos_hci_recv(void* context, BitBuffer* frame);
+void seos_hci_acldata_send(SeosHci* seos_hci, uint8_t flags, BitBuffer* tx);
+void seos_hci_init(void* context);
+
+void seos_hci_set_receive_callback(
+    SeosHci* seos_hci,
+    SeosHciReceiveCallback callback,
+    void* context);
+
+void seos_hci_set_completed_packets_callback(
+    SeosHci* seos_hci,
+    SeosHciCompletedPacketsCallback callback,
+    void* context);
+
+void seos_hci_set_central_connection_callback(
+    SeosHci* seos_hci,
+    SeosHciCentralConnectionCallback callback,
+    void* context);
+
+void seos_hci_set_scan(SeosHci* seos_hci, bool enable);
+void seos_hci_enable_advertising(SeosHci* seos_hci, bool enable);
+void seos_hci_send_scan_params(SeosHci* seos_hci);
+void seos_hci_connect(SeosHci* seos_hci);

+ 484 - 0
seos/seos_hci_h5.c

@@ -0,0 +1,484 @@
+#include "seos_hci_h5_i.h"
+
+#define TAG "SeosHciH5"
+
+#define MAX_OUT_OF_ORDER 3
+
+/* Convenience macros for reading Three-wire header values */
+#define H5_HDR_SEQ(hdr)      ((hdr)[0] & 0x07)
+#define H5_HDR_ACK(hdr)      (((hdr)[0] >> 3) & 0x07)
+#define H5_HDR_CRC(hdr)      (((hdr)[0] >> 6) & 0x01)
+#define H5_HDR_RELIABLE(hdr) (((hdr)[0] >> 7) & 0x01)
+#define H5_HDR_PKT_TYPE(hdr) ((hdr)[1] & 0x0f)
+#define H5_HDR_LEN(hdr)      ((((hdr)[1] >> 4) & 0x0f) + ((hdr)[2] << 4))
+
+#define SLIP_DELIMITER 0xc0
+#define SLIP_ESC       0xdb
+#define SLIP_ESC_DELIM 0xdc
+#define SLIP_ESC_ESC   0xdd
+
+#define MESSAGE_QUEUE_SIZE 10
+
+/* H5 state flags */
+enum {
+    H5_RX_ESC, /* SLIP escape mode */
+    H5_TX_ACK_REQ, /* Pending ack to send */
+    H5_WAKEUP_DISABLE, /* Device cannot wake host */
+    H5_HW_FLOW_CONTROL, /* Use HW flow control */
+};
+
+typedef struct {
+    size_t len;
+    uint8_t buf[SEOS_UART_RX_BUF_SIZE];
+} HCI_MESSAGE;
+
+int32_t seos_hci_h5_task(void* context);
+void seos_hci_h5_link_control(SeosHciH5* seos_hci_h5, uint8_t* data, size_t len);
+
+void seos_hci_h5_sync(SeosHciH5* seos_hci_h5) {
+    seos_hci_h5->out_of_order_count = 0;
+    uint8_t sync[] = {0x01, 0x7e};
+    seos_hci_h5_link_control(seos_hci_h5, sync, sizeof(sync));
+}
+
+SeosHciH5* seos_hci_h5_alloc() {
+    SeosHciH5* seos_hci_h5 = malloc(sizeof(SeosHciH5));
+    memset(seos_hci_h5, 0, sizeof(SeosHciH5));
+    seos_hci_h5->tx_win = H5_TX_WIN_MAX;
+    seos_hci_h5->stage = STOPPED;
+
+    seos_hci_h5->uart = seos_uart_alloc();
+    seos_uart_set_receive_callback(seos_hci_h5->uart, seos_hci_h5_recv, seos_hci_h5);
+
+    seos_hci_h5->thread =
+        furi_thread_alloc_ex("SeosHciH5Worker", 5 * 1024, seos_hci_h5_task, seos_hci_h5);
+    seos_hci_h5->messages = furi_message_queue_alloc(MESSAGE_QUEUE_SIZE, sizeof(HCI_MESSAGE));
+    seos_hci_h5->mq_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    furi_thread_start(seos_hci_h5->thread);
+
+    // Give the UART threads a little time to get started
+    furi_delay_ms(3);
+
+    return seos_hci_h5;
+}
+
+void seos_hci_h5_free(SeosHciH5* seos_hci_h5) {
+    furi_assert(seos_hci_h5);
+
+    furi_message_queue_free(seos_hci_h5->messages);
+    furi_mutex_free(seos_hci_h5->mq_mutex);
+    furi_thread_free(seos_hci_h5->thread);
+    seos_uart_free(seos_hci_h5->uart);
+    free(seos_hci_h5);
+}
+
+void seos_hci_h5_start(SeosHciH5* seos_hci_h5) {
+    seos_hci_h5->stage = STARTED;
+    seos_hci_h5->out_of_order_count = 0;
+
+    /* Send initial sync request */
+    seos_hci_h5_sync(seos_hci_h5);
+}
+void seos_hci_h5_stop(SeosHciH5* seos_hci_h5) {
+    seos_hci_h5->stage = STOPPED;
+    furi_thread_flags_set(furi_thread_get_id(seos_hci_h5->thread), WorkerEvtStop);
+    furi_thread_join(seos_hci_h5->thread);
+}
+
+void seos_hci_h5_peer_reset(SeosHciH5* seos_hci_h5) {
+    seos_hci_h5->state = H5_UNINITIALIZED;
+    seos_hci_h5->tx_seq = 0;
+    seos_hci_h5->tx_ack = 0;
+}
+
+void seos_hci_h5_reset_rx(SeosHciH5* seos_hci_h5) {
+    bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_RX_ESC, false);
+}
+
+void seos_hci_h5_link_control(SeosHciH5* seos_hci_h5, uint8_t* data, size_t len) {
+    BitBuffer* message = bit_buffer_alloc(len);
+    bit_buffer_append_bytes(message, data, len);
+
+    seos_hci_h5_send(seos_hci_h5, HCI_3WIRE_LINK_PKT, message);
+    bit_buffer_free(message);
+}
+
+void seos_hci_h5_handle_internal_rx(SeosHciH5* seos_hci_h5, BitBuffer* rx_skb) {
+    uint8_t sync_req[] = {0x01, 0x7e};
+    uint8_t sync_rsp[] = {0x02, 0x7d};
+    uint8_t conf_req[3] = {0x03, 0xfc};
+    uint8_t conf_rsp[] = {0x04, 0x7b};
+    uint8_t wakeup_req[] = {0x05, 0xfa};
+    uint8_t woken_req[] = {0x06, 0xf9};
+    uint8_t sleep_req[] = {0x07, 0x78};
+    const uint8_t* header = bit_buffer_get_data(rx_skb);
+    const uint8_t* data = bit_buffer_get_data(rx_skb) + 4;
+
+    if(H5_HDR_PKT_TYPE(header) != HCI_3WIRE_LINK_PKT) return;
+
+    if(H5_HDR_LEN(header) < 2) return;
+
+    conf_req[2] = seos_hci_h5->tx_win & 0x07;
+
+    if(memcmp(data, sync_req, 2) == 0) {
+        if(seos_hci_h5->state == H5_ACTIVE) seos_hci_h5_peer_reset(seos_hci_h5);
+        seos_hci_h5_link_control(seos_hci_h5, sync_rsp, 2);
+    } else if(memcmp(data, sync_rsp, 2) == 0) {
+        if(seos_hci_h5->state == H5_ACTIVE) seos_hci_h5_peer_reset(seos_hci_h5);
+        seos_hci_h5->state = H5_INITIALIZED;
+        seos_hci_h5_link_control(seos_hci_h5, conf_req, 3);
+    } else if(memcmp(data, conf_req, 2) == 0) {
+        seos_hci_h5_link_control(seos_hci_h5, conf_rsp, 2);
+        seos_hci_h5_link_control(seos_hci_h5, conf_req, 3);
+    } else if(memcmp(data, conf_rsp, 2) == 0) {
+        if(H5_HDR_LEN(header) > 2) seos_hci_h5->tx_win = (data[2] & 0x07);
+        FURI_LOG_D(TAG, "---- Three-wire init complete. tx_win %u ----", seos_hci_h5->tx_win);
+        seos_hci_h5->state = H5_ACTIVE;
+        if(seos_hci_h5->init_callback) {
+            seos_hci_h5->init_callback(seos_hci_h5->init_callback_context);
+        }
+        return;
+    } else if(memcmp(data, sleep_req, 2) == 0) {
+        FURI_LOG_D(TAG, "Peer went to sleep");
+        seos_hci_h5->sleep = H5_SLEEPING;
+        return;
+    } else if(memcmp(data, woken_req, 2) == 0) {
+        FURI_LOG_D(TAG, "Peer woke up");
+        seos_hci_h5->sleep = H5_AWAKE;
+    } else if(memcmp(data, wakeup_req, 2) == 0) {
+        FURI_LOG_D(TAG, "Peer requested wakeup");
+        seos_hci_h5_link_control(seos_hci_h5, woken_req, 2);
+        seos_hci_h5->sleep = H5_AWAKE;
+    } else {
+        FURI_LOG_D(TAG, "Link Control: 0x%02hhx 0x%02hhx", data[0], data[1]);
+        return;
+    }
+
+    // hci_uart_tx_wakeup(hu);
+}
+
+static void seos_hci_h5_slip_delim(BitBuffer* skb) {
+    const char delim = SLIP_DELIMITER;
+
+    bit_buffer_append_byte(skb, delim);
+}
+
+static void seos_hci_h5_slip_one_byte(BitBuffer* skb, uint8_t c) {
+    const char esc_delim[2] = {SLIP_ESC, SLIP_ESC_DELIM};
+    const char esc_esc[2] = {SLIP_ESC, SLIP_ESC_ESC};
+
+    switch(c) {
+    case SLIP_DELIMITER:
+        bit_buffer_append_byte(skb, esc_delim[0]);
+        bit_buffer_append_byte(skb, esc_delim[1]);
+        break;
+    case SLIP_ESC:
+        bit_buffer_append_byte(skb, esc_esc[0]);
+        bit_buffer_append_byte(skb, esc_esc[1]);
+
+        break;
+    default:
+        bit_buffer_append_byte(skb, c);
+    }
+}
+
+static void seos_hci_h5_unslip_one_byte(SeosHciH5* seos_hci_h5, BitBuffer* skb, uint8_t c) {
+    const uint8_t delim = SLIP_DELIMITER, esc = SLIP_ESC;
+    uint8_t byte = c;
+
+    bool test = bit_lib_get_bit((uint8_t*)&seos_hci_h5->flags, H5_RX_ESC);
+    if(!test && c == SLIP_ESC) {
+        bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_RX_ESC, true);
+        return;
+    }
+
+    if(test) {
+        bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_RX_ESC, false);
+        switch(c) {
+        case SLIP_ESC_DELIM:
+            byte = delim;
+            break;
+        case SLIP_ESC_ESC:
+            byte = esc;
+            break;
+        default:
+            FURI_LOG_W(TAG, "Invalid esc byte 0x%02hhx", c);
+            seos_hci_h5_reset_rx(seos_hci_h5);
+            return;
+        }
+    }
+
+    bit_buffer_append_byte(skb, byte);
+}
+
+void seos_hci_h5_send(SeosHciH5* seos_hci_h5, uint8_t pkt_type, BitBuffer* message) {
+    SeosUart* seos_uart = seos_hci_h5->uart;
+
+    const uint8_t* message_buf = message ? bit_buffer_get_data(message) : NULL;
+    size_t message_len = message ? bit_buffer_get_size_bytes(message) : 0;
+
+    if(message_len > 0) {
+        // seos_log_buffer("seos_hci_h5_send", message);
+    }
+    uint8_t header[4];
+
+    /*
+   * Max len of packet: (original len + 4 (H5 hdr) + 2 (crc)) * 2
+   * (because bytes 0xc0 and 0xdb are escaped, worst case is when
+   * the packet is all made of 0xc0 and 0xdb) + 2 (0xc0
+   * delimiters at start and end).
+   */
+    size_t max_packet_len = (message_len + 6) * 2 + 2;
+
+    BitBuffer* nskb = bit_buffer_alloc(max_packet_len);
+    seos_hci_h5_slip_delim(nskb);
+
+    bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_TX_ACK_REQ, false);
+    header[0] = seos_hci_h5->tx_ack << 3;
+
+    /* Reliable packet? */
+    if(pkt_type == HCI_ACLDATA_PKT || pkt_type == HCI_COMMAND_PKT) {
+        header[0] |= 1 << 7;
+        header[0] |= seos_hci_h5->tx_seq;
+        seos_hci_h5->tx_seq = (seos_hci_h5->tx_seq + 1) % 8;
+    }
+
+    header[1] = pkt_type | ((message_len & 0x0f) << 4);
+    header[2] = message_len >> 4;
+    header[3] = ~((header[0] + header[1] + header[2]) & 0xff);
+
+    /*
+    FURI_LOG_D(
+        TAG,
+        "tx: seq %u ack %u crc %u rel %u type %u len %u",
+        H5_HDR_SEQ(header),
+        H5_HDR_ACK(header),
+        H5_HDR_CRC(header),
+        H5_HDR_RELIABLE(header),
+        H5_HDR_PKT_TYPE(header),
+        H5_HDR_LEN(header));
+    */
+
+    for(size_t i = 0; i < sizeof(header); i++) {
+        seos_hci_h5_slip_one_byte(nskb, header[i]);
+    }
+    for(size_t i = 0; i < message_len; i++) {
+        seos_hci_h5_slip_one_byte(nskb, message_buf[i]);
+    }
+
+    seos_hci_h5_slip_delim(nskb);
+
+    seos_uart_send(
+        seos_uart, (uint8_t*)bit_buffer_get_data(nskb), bit_buffer_get_size_bytes(nskb));
+    bit_buffer_free(nskb);
+}
+
+size_t seos_hci_h5_recv(void* context, uint8_t* buffer, size_t len) {
+    SeosHciH5* seos_hci_h5 = (SeosHciH5*)context;
+
+    // Must get start byte + 4 byte header + end byte to even give a shit
+    if(len < 6) {
+        return 0;
+    }
+
+    if(buffer[0] != SLIP_DELIMITER) {
+        // FURI_LOG_E(TAG, "UART didn't start with SLIP_DELIMITER (%02x)", buffer[0]);
+        return 1;
+    }
+
+    BitBuffer* rx_skb = bit_buffer_alloc(len);
+
+    // i = 1 -> Skip first c0 byte
+    for(size_t i = 1; i < len; i++) {
+        uint8_t c = buffer[i];
+        seos_hci_h5_unslip_one_byte(seos_hci_h5, rx_skb, c);
+    }
+    // seos_log_buffer("HCI H5 Recv", rx_skb);
+
+    // From h5_rx_3wire_hdr
+    const uint8_t* header = bit_buffer_get_data(rx_skb);
+
+    /*
+    FURI_LOG_D(
+        TAG,
+        "rx: seq %u ack %u crc %u rel %u type %u len %u",
+        H5_HDR_SEQ(header),
+        H5_HDR_ACK(header),
+        H5_HDR_CRC(header),
+        H5_HDR_RELIABLE(header),
+        H5_HDR_PKT_TYPE(header),
+        H5_HDR_LEN(header));
+        */
+
+    if(((header[0] + header[1] + header[2] + header[3]) & 0xff) != 0xff) {
+        FURI_LOG_W(TAG, "Invalid header checksum");
+        bit_buffer_free(rx_skb);
+        return len;
+    }
+
+    if(len < (size_t)(1 + 4 + H5_HDR_LEN(header) + 1)) {
+        // FURI_LOG_W(TAG, "Incomplete packet (%d), wait for more data", len);
+        bit_buffer_free(rx_skb);
+        return 0;
+    }
+
+    if(H5_HDR_RELIABLE(header) && H5_HDR_SEQ(header) != seos_hci_h5->tx_ack) {
+        FURI_LOG_W(
+            TAG, "Out-of-order packet arrived (%u != %u)", H5_HDR_SEQ(header), seos_hci_h5->tx_ack);
+        bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_TX_ACK_REQ, true);
+        seos_hci_h5_reset_rx(seos_hci_h5);
+
+        seos_hci_h5->out_of_order_count++;
+        if(seos_hci_h5->out_of_order_count >= MAX_OUT_OF_ORDER) {
+            seos_hci_h5_sync(seos_hci_h5);
+        }
+        bit_buffer_free(rx_skb);
+        return len;
+    }
+    // When a packet isn't out of order, reset the count
+    seos_hci_h5->out_of_order_count = 0;
+
+    if(seos_hci_h5->state != H5_ACTIVE && H5_HDR_PKT_TYPE(header) != HCI_3WIRE_LINK_PKT) {
+        FURI_LOG_W(TAG, "Non-link packet received in non-active state");
+        seos_hci_h5_reset_rx(seos_hci_h5);
+        bit_buffer_free(rx_skb);
+        return len;
+    }
+
+    if(H5_HDR_CRC(header)) {
+        // TODO: Check header
+    }
+
+    // From h5_complete_rx_pkt
+    if(H5_HDR_RELIABLE(header)) {
+        seos_hci_h5->tx_ack = (seos_hci_h5->tx_ack + 1) % 8;
+        bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_TX_ACK_REQ, true);
+        //hci_uart_tx_wakeup(hu);
+    }
+
+    seos_hci_h5->rx_ack = H5_HDR_ACK(header);
+    size_t payload_len = H5_HDR_LEN(header);
+
+    // Determine amount of data we consumed
+    BitBuffer* tmp = bit_buffer_alloc(len);
+    for(size_t i = 0; i < 4 + payload_len; i++) {
+        seos_hci_h5_slip_one_byte(tmp, bit_buffer_get_byte(rx_skb, i));
+    }
+    size_t consumed = 1 + bit_buffer_get_size_bytes(tmp) + 1;
+    if(consumed > len) {
+        // At one point I was double processing the header, and not processing all the paylaod, and ended up with consumed > len.
+        // This is to track if there are edge cases I missed.
+        FURI_LOG_W(TAG, "Consumed %d > len %d", consumed, len);
+        consumed = len;
+    }
+    bit_buffer_free(tmp);
+
+    // Remove from send queue?
+    // h5_pkt_cull(h5);
+
+    switch(H5_HDR_PKT_TYPE(header)) {
+    case HCI_EVENT_PKT:
+    case HCI_ACLDATA_PKT:
+    case HCI_SCODATA_PKT:
+    case HCI_ISODATA_PKT:
+        uint32_t space = furi_message_queue_get_space(seos_hci_h5->messages);
+        if(space > 0) {
+            HCI_MESSAGE message = {};
+            message.len = 4 + payload_len;
+            if(message.len > sizeof(message.buf)) {
+                FURI_LOG_W(TAG, "Too big to queue");
+                return len;
+            }
+            memcpy(message.buf, bit_buffer_get_data(rx_skb), message.len);
+
+            if(furi_mutex_acquire(seos_hci_h5->mq_mutex, FuriWaitForever) == FuriStatusOk) {
+                furi_message_queue_put(seos_hci_h5->messages, &message, FuriWaitForever);
+                furi_mutex_release(seos_hci_h5->mq_mutex);
+            }
+            if(space < MESSAGE_QUEUE_SIZE / 2) {
+                FURI_LOG_D(TAG, "Queue message.  %ld remaining", space);
+            }
+        } else {
+            if(seos_hci_h5->stage != STOPPED) {
+                FURI_LOG_E(TAG, "No space in message queue");
+            }
+        }
+        break;
+    default:
+        seos_hci_h5_handle_internal_rx(seos_hci_h5, rx_skb);
+        break;
+    }
+
+    if(bit_lib_get_bit((uint8_t*)&seos_hci_h5->flags, H5_TX_ACK_REQ)) {
+        seos_hci_h5_send(seos_hci_h5, HCI_3WIRE_ACK_PKT, NULL);
+    }
+
+    seos_hci_h5_reset_rx(seos_hci_h5);
+
+    bit_buffer_free(rx_skb);
+
+    // FURI_LOG_D(TAG, "%d consumed", consumed);
+    return consumed;
+}
+
+int32_t seos_hci_h5_task(void* context) {
+    SeosHciH5* seos_hci_h5 = (SeosHciH5*)context;
+    bool running = true;
+
+    while(running) {
+        uint32_t events = furi_thread_flags_get();
+        if(events & WorkerEvtStop) {
+            running = false;
+            break;
+        }
+
+        if(furi_mutex_acquire(seos_hci_h5->mq_mutex, 1) == FuriStatusOk) {
+            uint32_t count = furi_message_queue_get_count(seos_hci_h5->messages);
+            if(count > 0) {
+                if(count > MESSAGE_QUEUE_SIZE / 2) {
+                    FURI_LOG_I(TAG, "Dequeue message [%ld messages]", count);
+                }
+
+                HCI_MESSAGE message = {};
+                FuriStatus status =
+                    furi_message_queue_get(seos_hci_h5->messages, &message, FuriWaitForever);
+                if(status != FuriStatusOk) {
+                    FURI_LOG_W(TAG, "furi_message_queue_get fail %d", status);
+                }
+
+                if(seos_hci_h5->receive_callback) {
+                    size_t payload_len = H5_HDR_LEN(message.buf);
+                    BitBuffer* frame = bit_buffer_alloc(payload_len + 1);
+                    bit_buffer_append_byte(frame, H5_HDR_PKT_TYPE(message.buf));
+                    bit_buffer_append_bytes(frame, message.buf + 4, payload_len);
+                    seos_hci_h5->receive_callback(seos_hci_h5->receive_callback_context, frame);
+                    bit_buffer_free(frame);
+                }
+            }
+            furi_mutex_release(seos_hci_h5->mq_mutex);
+        } else {
+            FURI_LOG_W(TAG, "Failed to acquire mutex");
+        }
+
+        // A beat for event flags
+        // furi_delay_ms(1);
+    }
+
+    return 0;
+}
+
+void seos_hci_h5_set_receive_callback(
+    SeosHciH5* seos_hci_h5,
+    SeosHciH5ReceiveCallback callback,
+    void* context) {
+    seos_hci_h5->receive_callback = callback;
+    seos_hci_h5->receive_callback_context = context;
+}
+
+void seos_hci_h5_set_init_callback(
+    SeosHciH5* seos_hci_h5,
+    SeosHciH5InitCallback callback,
+    void* context) {
+    seos_hci_h5->init_callback = callback;
+    seos_hci_h5->init_callback_context = context;
+}

+ 82 - 0
seos/seos_hci_h5.h

@@ -0,0 +1,82 @@
+#pragma once
+
+#include <string.h>
+#include <stdlib.h>
+
+#include <furi.h>
+#include <lib/toolbox/bit_buffer.h>
+#include <lib/bit_lib/bit_lib.h>
+
+#include "uart.h"
+
+// include/net/bluetooth/hci.h
+/* HCI data types */
+#define HCI_COMMAND_PKT 0x01
+#define HCI_ACLDATA_PKT 0x02
+#define HCI_SCODATA_PKT 0x03
+#define HCI_EVENT_PKT   0x04
+#define HCI_ISODATA_PKT 0x05
+#define HCI_DIAG_PKT    0xf0
+#define HCI_VENDOR_PKT  0xff
+
+#define HCI_3WIRE_ACK_PKT  0
+#define HCI_3WIRE_LINK_PKT 0x0F
+
+#define H5_TX_WIN_MAX 4
+
+typedef size_t (*SeosHciH5ReceiveCallback)(void* context, BitBuffer* frame);
+typedef void (*SeosHciH5InitCallback)(void* context);
+
+typedef struct {
+    SeosUart* uart;
+
+    uint8_t tx_seq; /* Next seq number to send */
+    uint8_t tx_ack; /* Next ack number to send */
+    uint8_t tx_win; /* Sliding window size */
+    uint8_t rx_ack; /* Last ack number received */
+
+    unsigned long flags;
+
+    enum {
+        H5_UNINITIALIZED,
+        H5_INITIALIZED,
+        H5_ACTIVE,
+    } state;
+    enum {
+        H5_AWAKE,
+        H5_SLEEPING,
+        H5_WAKING_UP,
+    } sleep;
+
+    enum {
+        STOPPED,
+        STARTED
+    } stage;
+
+    SeosHciH5ReceiveCallback receive_callback;
+    void* receive_callback_context;
+
+    SeosHciH5InitCallback init_callback;
+    void* init_callback_context;
+
+    size_t out_of_order_count;
+
+    FuriMessageQueue* messages;
+    FuriMutex* mq_mutex;
+    FuriThread* thread;
+} SeosHciH5;
+
+SeosHciH5* seos_hci_h5_alloc();
+void seos_hci_h5_free(SeosHciH5* seos_hci_h5);
+void seos_hci_h5_start(SeosHciH5* seos_hci_h5);
+void seos_hci_h5_stop(SeosHciH5* seos_hci_h5);
+void seos_hci_h5_send(SeosHciH5* seos_hci_h5, uint8_t pkt_type, BitBuffer* message);
+size_t seos_hci_h5_recv(void* context, uint8_t* buffer, size_t len);
+void seos_hci_h5_set_receive_callback(
+    SeosHciH5* seos_hci_h5,
+    SeosHciH5ReceiveCallback callback,
+    void* context);
+void seos_hci_h5_set_init_callback(
+    SeosHciH5* seos_hci_h5,
+    SeosHciH5InitCallback callback,
+    void* context);

+ 4 - 0
seos/seos_hci_h5_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_hci_h5.h"

+ 4 - 0
seos/seos_hci_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_hci.h"

+ 129 - 0
seos/seos_i.h

@@ -0,0 +1,129 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <notification/notification_messages.h>
+#include <storage/storage.h>
+
+#include <gui/modules/submenu.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/loading.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/text_box.h>
+#include <gui/modules/widget.h>
+
+#include <input/input.h>
+
+#include <lib/nfc/nfc.h>
+#include <nfc/nfc_listener.h>
+#include <nfc/nfc_poller.h>
+#include <nfc/nfc_device.h>
+
+/* generated by fbt from .png files in images folder */
+#include <seos_icons.h>
+
+#include "seos.h"
+#include "keys.h"
+#include "seos_hci.h"
+#include "seos_characteristic.h"
+#include "seos_native_peripheral.h"
+#include "seos_central.h"
+#include "seos_common.h"
+#include "seos_reader.h"
+#include "seos_emulator.h"
+#include "scenes/seos_scene.h"
+#include "des_cmac.h"
+#include "aes_cmac.h"
+
+#define SEOS_TEXT_STORE_SIZE 128
+
+enum SeosCustomEvent {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    SeosCustomEventReserved = 100,
+
+    SeosCustomEventViewExit,
+    SeosCustomEventTextInputDone,
+    // Read card events
+    SeosCustomEventReaderError,
+    SeosCustomEventReaderSuccess,
+
+    SeosCustomEventHCIInit,
+    // Events during emulating or reading
+    SeosCustomEventScan,
+    SeosCustomEventFound,
+    SeosCustomEventEmulate,
+    SeosCustomEventADFMatched,
+    SeosCustomEventAIDSelected,
+    SeosCustomEventConnected,
+    SeosCustomEventAuthenticated,
+    SeosCustomEventSIORequested,
+    SeosCustomEventAdvertising,
+};
+
+struct Seos {
+    bool is_debug_enabled;
+    ViewDispatcher* view_dispatcher;
+    Gui* gui;
+    NotificationApp* notifications;
+    SceneManager* scene_manager;
+    Storage* storage;
+
+    char text_store[SEOS_TEXT_STORE_SIZE + 1];
+    FuriString* text_box_store;
+
+    // Common Views
+    Submenu* submenu;
+    Popup* popup;
+    Loading* loading;
+    TextInput* text_input;
+    TextBox* text_box;
+    Widget* widget;
+
+    Nfc* nfc;
+    NfcListener* listener;
+    NfcPoller* poller;
+    NfcDevice* nfc_device;
+
+    SeosCredential credential;
+
+    // NFC
+    SeosEmulator* seos_emulator;
+    SeosReader* seos_reader;
+
+    // BLE
+    bool has_external_ble;
+    SeosCharacteristic* seos_characteristic;
+    SeosCentral* seos_central;
+    FlowMode flow_mode;
+
+    char dev_name[SEOS_FILE_NAME_MAX_LENGTH + 1];
+    FuriString* load_path;
+    DialogsApp* dialogs;
+
+    bool keys_loaded;
+    Bt* bt;
+    FuriHalBleProfileBase* ble_profile;
+    SeosNativePeripheral* native_peripheral;
+};
+
+typedef enum {
+    SeosViewMenu,
+    SeosViewPopup,
+    SeosViewLoading,
+    SeosViewTextInput,
+    SeosViewTextBox,
+    SeosViewWidget,
+} SeosView;
+
+void seos_text_store_set(Seos* seos, const char* text, ...);
+
+void seos_text_store_clear(Seos* seos);
+
+void seos_blink_start(Seos* seos);
+
+void seos_blink_stop(Seos* seos);
+
+void seos_show_loading_popup(void* context, bool show);

+ 235 - 0
seos/seos_l2cap.c

@@ -0,0 +1,235 @@
+#include "seos_l2cap_i.h"
+
+#define TAG "SeosL2Cap"
+
+#define ACL_START_NO_FLUSH 0x00
+#define ACL_CONT           0x01
+#define ACL_START          0x02
+
+#define CID_ATT       0x0004
+#define CID_LE_SIGNAL 0x0005
+
+// 5.4.2. HCI ACL Data packets
+#define SEOS_MAX_ACLDATA_SEND_LENGTH 23
+
+struct l2cap_header {
+    uint16_t payload_len;
+    uint16_t cid;
+} __packed;
+
+SeosL2Cap* seos_l2cap_alloc(Seos* seos) {
+    SeosL2Cap* seos_l2cap = malloc(sizeof(SeosL2Cap));
+    memset(seos_l2cap, 0, sizeof(SeosL2Cap));
+
+    // TODO: match MTU
+    seos_l2cap->tx_accumulator = bit_buffer_alloc(256);
+    seos_l2cap->rx_accumulator = bit_buffer_alloc(256);
+
+    seos_l2cap->seos = seos;
+    seos_l2cap->seos_hci = seos_hci_alloc(seos);
+
+    // Callback to lower level services to call this one
+    seos_hci_set_receive_callback(seos_l2cap->seos_hci, seos_l2cap_recv, seos_l2cap);
+    seos_hci_set_completed_packets_callback(
+        seos_l2cap->seos_hci, seos_l2cap_send_next_chunk, seos_l2cap);
+    seos_hci_set_central_connection_callback(
+        seos_l2cap->seos_hci, seos_l2cap_central_connection, seos_l2cap);
+
+    return seos_l2cap;
+}
+
+void seos_l2cap_free(SeosL2Cap* seos_l2cap) {
+    furi_assert(seos_l2cap);
+
+    bit_buffer_free(seos_l2cap->tx_accumulator);
+    bit_buffer_free(seos_l2cap->rx_accumulator);
+
+    seos_hci_free(seos_l2cap->seos_hci);
+    free(seos_l2cap);
+}
+
+void seos_l2cap_start(SeosL2Cap* seos_l2cap, BleMode mode, FlowMode flow_mode) {
+    seos_hci_start(seos_l2cap->seos_hci, mode, flow_mode);
+}
+
+void seos_l2cap_stop(SeosL2Cap* seos_l2cap) {
+    seos_hci_stop(seos_l2cap->seos_hci);
+}
+
+void seos_l2cap_recv(void* context, uint16_t handle, uint8_t flags, BitBuffer* pdu) {
+    SeosL2Cap* seos_l2cap = (SeosL2Cap*)context;
+    seos_l2cap->handle = handle;
+
+    // seos_log_bitbuffer(TAG, "recv", pdu);
+    // uint8_t Broadcast_Flag = flags >> 2;
+    uint8_t Packet_Boundary_Flag = flags & 0x03;
+
+    const uint8_t* data = bit_buffer_get_data(pdu);
+    struct l2cap_header* header = (struct l2cap_header*)(data);
+
+    switch(Packet_Boundary_Flag) {
+    case ACL_CONT:
+        if(bit_buffer_get_size_bytes(seos_l2cap->rx_accumulator) == 0) {
+            FURI_LOG_I(TAG, "Request to att continue when no previous data received");
+            seos_log_bitbuffer(TAG, "cont", pdu);
+            return;
+        }
+        // No header this time
+        bit_buffer_append_bytes(seos_l2cap->rx_accumulator, data, bit_buffer_get_size_bytes(pdu));
+
+        // Check for overage
+        if(bit_buffer_get_size_bytes(seos_l2cap->rx_accumulator) > seos_l2cap->pdu_len) {
+            FURI_LOG_W(
+                TAG,
+                "Oh shit, too much data: %d > %d",
+                bit_buffer_get_size_bytes(seos_l2cap->rx_accumulator),
+                seos_l2cap->pdu_len);
+            seos_log_bitbuffer(TAG, "cont", seos_l2cap->rx_accumulator);
+        }
+        // Full PDU
+        if(bit_buffer_get_size_bytes(seos_l2cap->rx_accumulator) == seos_l2cap->pdu_len) {
+            // FURI_LOG_I(TAG, "Complete reassembled PDU");
+            // When we've accumulated, we've skipped copying the header, so we can pass the accumulator directly in.
+            if(seos_l2cap->receive_callback) {
+                seos_l2cap->receive_callback(
+                    seos_l2cap->receive_callback_context, seos_l2cap->rx_accumulator);
+            }
+        }
+        break;
+    case ACL_START:
+    case ACL_START_NO_FLUSH:
+        uint16_t payload_len = header->payload_len;
+        uint16_t cid = header->cid;
+
+        if(bit_buffer_get_size_bytes(pdu) < header->payload_len) {
+            // FURI_LOG_W(TAG, "Incomplete PDU");
+            seos_l2cap->pdu_len = payload_len;
+            bit_buffer_reset(seos_l2cap->rx_accumulator);
+            bit_buffer_append_bytes(
+                seos_l2cap->rx_accumulator,
+                data + sizeof(struct l2cap_header),
+                bit_buffer_get_size_bytes(pdu) - sizeof(struct l2cap_header));
+            return;
+        }
+
+        if(cid == CID_ATT) {
+            BitBuffer* payload = bit_buffer_alloc(payload_len);
+            bit_buffer_append_bytes(payload, data + sizeof(struct l2cap_header), payload_len);
+            if(seos_l2cap->receive_callback) {
+                seos_l2cap->receive_callback(seos_l2cap->receive_callback_context, payload);
+            }
+            bit_buffer_free(payload);
+        } else if(cid == CID_LE_SIGNAL) {
+            seos_log_bitbuffer(TAG, "LE Signal", pdu);
+        } else {
+            FURI_LOG_W(TAG, "Unhandled CID: %d", cid);
+        }
+
+        break;
+    default:
+        FURI_LOG_W(TAG, "unhandled Packet_Boundary_Flag %d", Packet_Boundary_Flag);
+        break;
+    }
+}
+
+void seos_l2cap_central_connection(void* context) {
+    SeosL2Cap* seos_l2cap = (SeosL2Cap*)context;
+    if(seos_l2cap->central_connection_callback) {
+        seos_l2cap->central_connection_callback(seos_l2cap->central_connection_context);
+    }
+}
+
+void seos_l2cap_send_chunk(void* context) {
+    SeosL2Cap* seos_l2cap = (SeosL2Cap*)context;
+    // FURI_LOG_D(TAG, "seos_l2cap_send_chunk");
+    uint16_t cid = CID_ATT;
+
+    uint8_t Packet_Boundary_Flag = 0x00;
+
+    bool continuation = bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator) <
+                        seos_l2cap->pdu_len;
+    if(continuation) {
+        Packet_Boundary_Flag = 0x01;
+    }
+
+    uint16_t tx_len =
+        MIN((size_t)SEOS_MAX_ACLDATA_SEND_LENGTH,
+            bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator));
+    if(tx_len == 0) {
+        seos_l2cap->pdu_len = 0;
+        return;
+    }
+
+    BitBuffer* response = bit_buffer_alloc(tx_len + sizeof(cid) + sizeof(tx_len));
+
+    if(!continuation) {
+        //pdu length
+        bit_buffer_append_bytes(
+            response, (uint8_t*)&seos_l2cap->pdu_len, sizeof(seos_l2cap->pdu_len));
+        // cid
+        bit_buffer_append_bytes(response, (uint8_t*)&cid, sizeof(cid));
+    }
+    // tx
+    bit_buffer_append_bytes(response, bit_buffer_get_data(seos_l2cap->tx_accumulator), tx_len);
+    FURI_LOG_D(TAG, "send_chunk: %d/%d bytes", tx_len, seos_l2cap->pdu_len);
+
+    seos_hci_acldata_send(seos_l2cap->seos_hci, Packet_Boundary_Flag, response);
+    bit_buffer_free(response);
+
+    // trim off send bytes
+    int new_len = bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator) - tx_len;
+    if(new_len <= 0) {
+        bit_buffer_reset(seos_l2cap->tx_accumulator);
+        seos_l2cap->pdu_len = 0;
+        // FURI_LOG_D(TAG, "TX accumulator empty");
+        return;
+    }
+
+    BitBuffer* tmp = bit_buffer_alloc(new_len);
+    bit_buffer_append_bytes(
+        tmp, bit_buffer_get_data(seos_l2cap->tx_accumulator) + tx_len, new_len);
+    bit_buffer_reset(seos_l2cap->tx_accumulator);
+    bit_buffer_append_bytes(
+        seos_l2cap->tx_accumulator, bit_buffer_get_data(tmp), bit_buffer_get_size_bytes(tmp));
+    bit_buffer_free(tmp);
+    // FURI_LOG_D(TAG, "tx accumulator length = %d", bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator));
+}
+
+void seos_l2cap_send(SeosL2Cap* seos_l2cap, BitBuffer* content) {
+    // seos_log_buffer("seos_l2cap_send", content);
+
+    if(bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator) > 0) {
+        FURI_LOG_W(TAG, "Failed to add message to L2CAP accumulator: it isn't empty");
+        return;
+    }
+    bit_buffer_append_bytes(
+        seos_l2cap->tx_accumulator,
+        bit_buffer_get_data(content),
+        bit_buffer_get_size_bytes(content));
+    seos_l2cap->pdu_len = bit_buffer_get_size_bytes(content);
+
+    seos_l2cap_send_chunk(seos_l2cap);
+}
+
+void seos_l2cap_send_next_chunk(void* context) {
+    SeosL2Cap* seos_l2cap = (SeosL2Cap*)context;
+    if(bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator) > 0) {
+        seos_l2cap_send_chunk(seos_l2cap);
+    }
+}
+
+void seos_l2cap_set_receive_callback(
+    SeosL2Cap* seos_l2cap,
+    SeosL2CapReceiveCallback callback,
+    void* context) {
+    seos_l2cap->receive_callback = callback;
+    seos_l2cap->receive_callback_context = context;
+}
+
+void seos_l2cap_set_central_connection_callback(
+    SeosL2Cap* seos_l2cap,
+    SeosL2CapCentralConnectionCallback callback,
+    void* context) {
+    seos_l2cap->central_connection_callback = callback;
+    seos_l2cap->central_connection_context = context;
+}

+ 44 - 0
seos/seos_l2cap.h

@@ -0,0 +1,44 @@
+#pragma once
+
+#include <furi.h>
+#include <lib/toolbox/bit_buffer.h>
+
+#include "seos_hci.h"
+#include "seos_common.h"
+
+typedef void (*SeosL2CapReceiveCallback)(void* context, BitBuffer* payload);
+typedef void (*SeosL2CapCentralConnectionCallback)(void* context);
+
+typedef struct {
+    Seos* seos;
+    SeosHci* seos_hci;
+    // Where to collect up data
+    BitBuffer* tx_accumulator;
+    BitBuffer* rx_accumulator;
+    uint16_t pdu_len;
+    uint16_t handle;
+
+    SeosL2CapReceiveCallback receive_callback;
+    void* receive_callback_context;
+
+    SeosL2CapCentralConnectionCallback central_connection_callback;
+    void* central_connection_context;
+} SeosL2Cap;
+
+SeosL2Cap* seos_l2cap_alloc(Seos* seos);
+void seos_l2cap_free(SeosL2Cap* seos_l2cap);
+void seos_l2cap_start(SeosL2Cap* seos_l2cap, BleMode mode, FlowMode flow_mode);
+void seos_l2cap_stop(SeosL2Cap* seos_l2cap);
+void seos_l2cap_recv(void* context, uint16_t handle, uint8_t flags, BitBuffer* pdu);
+void seos_l2cap_send_next_chunk(void* context);
+void seos_l2cap_send(SeosL2Cap* seos_l2cap, BitBuffer* content);
+void seos_l2cap_central_connection(void* context);
+void seos_l2cap_set_receive_callback(
+    SeosL2Cap* seos_l2cap,
+    SeosL2CapReceiveCallback callback,
+    void* context);
+
+void seos_l2cap_set_central_connection_callback(
+    SeosL2Cap* seos_l2cap,
+    SeosL2CapCentralConnectionCallback callback,
+    void* context);

+ 4 - 0
seos/seos_l2cap_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_l2cap.h"

+ 475 - 0
seos/seos_native_peripheral.c

@@ -0,0 +1,475 @@
+#include "seos_native_peripheral_i.h"
+
+#define TAG "SeosNativePeripheral"
+
+#define MESSAGE_QUEUE_SIZE 10
+
+static uint8_t standard_seos_aid[] = {0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+static uint8_t cd02[] = {0xcd, 0x02};
+static uint8_t general_authenticate_1[] =
+    {0x00, 0x87, 0x00, 0x01, 0x04, 0x7c, 0x02, 0x81, 0x00, 0x00};
+static uint8_t ga1_response[] = {0x7c, 0x0a, 0x81, 0x08};
+
+// Emulation
+static uint8_t success[] = {0x90, 0x00};
+static uint8_t file_not_found[] = {0x6A, 0x82};
+
+static uint8_t select_header[] = {0x00, 0xa4, 0x04, 0x00};
+static uint8_t select_adf_header[] = {0x80, 0xa5, 0x04, 0x00};
+static uint8_t general_authenticate_2_header[] = {0x00, 0x87, 0x00, 0x01};
+static uint8_t secure_messaging_header[] = {0x0c, 0xcb, 0x3f, 0xff};
+
+int32_t seos_native_peripheral_task(void* context);
+
+typedef struct {
+    size_t len;
+    uint8_t buf[BLE_SVC_SEOS_CHAR_VALUE_LEN_MAX];
+} NativePeripheralMessage;
+
+static void seos_ble_connection_status_callback(BtStatus status, void* context) {
+    furi_assert(context);
+    SeosNativePeripheral* seos_native_peripheral = context;
+    if(status == BtStatusConnected) {
+        view_dispatcher_send_custom_event(
+            seos_native_peripheral->seos->view_dispatcher, SeosCustomEventConnected);
+    } else if(status == BtStatusAdvertising) {
+        view_dispatcher_send_custom_event(
+            seos_native_peripheral->seos->view_dispatcher, SeosCustomEventAdvertising);
+    }
+}
+
+static uint16_t seos_svc_callback(SeosServiceEvent event, void* context) {
+    SeosNativePeripheral* seos_native_peripheral = context;
+    uint16_t bytes_available = 0;
+
+    if(event.event == SeosServiceEventTypeDataReceived) {
+        uint32_t space = furi_message_queue_get_space(seos_native_peripheral->messages);
+        if(space > 0) {
+            NativePeripheralMessage message = {.len = event.data.size};
+            memcpy(message.buf, event.data.buffer, event.data.size);
+
+            if(furi_mutex_acquire(seos_native_peripheral->mq_mutex, FuriWaitForever) ==
+               FuriStatusOk) {
+                furi_message_queue_put(
+                    seos_native_peripheral->messages, &message, FuriWaitForever);
+                furi_mutex_release(seos_native_peripheral->mq_mutex);
+            }
+            if(space < MESSAGE_QUEUE_SIZE / 2) {
+                FURI_LOG_D(TAG, "Queue message.  %ld remaining", space);
+            }
+            bytes_available = (space - 1) * sizeof(NativePeripheralMessage);
+        } else {
+            FURI_LOG_E(TAG, "No space in message queue");
+        }
+    }
+
+    return bytes_available;
+}
+
+SeosNativePeripheral* seos_native_peripheral_alloc(Seos* seos) {
+    SeosNativePeripheral* seos_native_peripheral = malloc(sizeof(SeosNativePeripheral));
+    memset(seos_native_peripheral, 0, sizeof(SeosNativePeripheral));
+
+    seos_native_peripheral->seos = seos;
+    seos_native_peripheral->credential = &seos->credential;
+    seos_native_peripheral->bt = furi_record_open(RECORD_BT);
+
+    seos_native_peripheral->phase = SELECT_AID;
+    seos_native_peripheral->secure_messaging = NULL;
+    seos_native_peripheral->params.key_no = 1;
+    memset(
+        seos_native_peripheral->params.cNonce,
+        0x0c,
+        sizeof(seos_native_peripheral->params.cNonce));
+    memset(seos_native_peripheral->params.UID, 0x0d, sizeof(seos_native_peripheral->params.UID));
+
+    seos_native_peripheral->thread = furi_thread_alloc_ex(
+        "SeosNativePeripheralWorker",
+        5 * 1024,
+        seos_native_peripheral_task,
+        seos_native_peripheral);
+    seos_native_peripheral->messages =
+        furi_message_queue_alloc(MESSAGE_QUEUE_SIZE, sizeof(NativePeripheralMessage));
+    seos_native_peripheral->mq_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+
+    return seos_native_peripheral;
+}
+
+void seos_native_peripheral_free(SeosNativePeripheral* seos_native_peripheral) {
+    furi_assert(seos_native_peripheral);
+
+    furi_record_close(RECORD_BT);
+
+    furi_message_queue_free(seos_native_peripheral->messages);
+    furi_mutex_free(seos_native_peripheral->mq_mutex);
+    furi_thread_free(seos_native_peripheral->thread);
+
+    free(seos_native_peripheral);
+}
+
+void seos_native_peripheral_start(SeosNativePeripheral* seos_native_peripheral, FlowMode mode) {
+    seos_native_peripheral->flow_mode = mode;
+    if(seos_native_peripheral->flow_mode == FLOW_CRED) {
+        seos_native_peripheral->params.key_no = 0;
+        seos_native_peripheral->params.cipher = TWO_KEY_3DES_CBC_MODE;
+        seos_native_peripheral->params.hash = SHA1;
+
+        memset(
+            seos_native_peripheral->params.rndICC,
+            0x0d,
+            sizeof(seos_native_peripheral->params.rndICC));
+        memset(
+            seos_native_peripheral->params.rNonce,
+            0x0c,
+            sizeof(seos_native_peripheral->params.rNonce));
+        memset(
+            seos_native_peripheral->params.UID, 0x00, sizeof(seos_native_peripheral->params.UID));
+        memset(
+            seos_native_peripheral->params.cNonce,
+            0x00,
+            sizeof(seos_native_peripheral->params.cNonce));
+    }
+
+    bt_disconnect(seos_native_peripheral->bt);
+
+    BleProfileParams params = {
+        .mode = mode,
+    };
+
+    // Wait 2nd core to update nvm storage
+    furi_delay_ms(200);
+    seos_native_peripheral->ble_profile =
+        bt_profile_start(seos_native_peripheral->bt, ble_profile_seos, &params);
+    furi_check(seos_native_peripheral->ble_profile);
+    bt_set_status_changed_callback(
+        seos_native_peripheral->bt, seos_ble_connection_status_callback, seos_native_peripheral);
+    ble_profile_seos_set_event_callback(
+        seos_native_peripheral->ble_profile,
+        sizeof(seos_native_peripheral->event_buffer),
+        seos_svc_callback,
+        seos_native_peripheral);
+    furi_hal_bt_start_advertising();
+    view_dispatcher_send_custom_event(
+        seos_native_peripheral->seos->view_dispatcher, SeosCustomEventAdvertising);
+
+    furi_thread_start(seos_native_peripheral->thread);
+}
+
+void seos_native_peripheral_stop(SeosNativePeripheral* seos_native_peripheral) {
+    furi_hal_bt_stop_advertising();
+    bt_set_status_changed_callback(seos_native_peripheral->bt, NULL, NULL);
+    bt_disconnect(seos_native_peripheral->bt);
+
+    // Wait 2nd core to update nvm storage
+    furi_delay_ms(200);
+    bt_keys_storage_set_default_path(seos_native_peripheral->bt);
+
+    furi_check(bt_profile_restore_default(seos_native_peripheral->bt));
+
+    furi_thread_flags_set(furi_thread_get_id(seos_native_peripheral->thread), WorkerEvtStop);
+    furi_thread_join(seos_native_peripheral->thread);
+}
+
+void seos_native_peripheral_process_message_cred(
+    SeosNativePeripheral* seos_native_peripheral,
+    NativePeripheralMessage message) {
+    BitBuffer* response = bit_buffer_alloc(128); // TODO: MTU
+
+    uint8_t* data = message.buf;
+    if(data[0] != BLE_START && data[0] != 0xe1) {
+        FURI_LOG_W(TAG, "Unexpected start of BLE packet");
+    }
+
+    const uint8_t* apdu = data + 1; // Match name to nfc version for easier copying
+
+    if(memcmp(apdu, select_header, sizeof(select_header)) == 0) {
+        if(memcmp(apdu + sizeof(select_header) + 1, standard_seos_aid, sizeof(standard_seos_aid)) ==
+           0) {
+            seos_emulator_select_aid(response);
+            bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+        } else {
+            bit_buffer_append_bytes(response, (uint8_t*)file_not_found, sizeof(file_not_found));
+        }
+    } else if(memcmp(apdu, select_adf_header, sizeof(select_adf_header)) == 0) {
+        // is our adf in the list?
+        // +1 to skip APDU length byte
+        void* p = memmem(
+            apdu + sizeof(select_adf_header) + 1,
+            apdu[sizeof(select_adf_header)],
+            SEOS_ADF_OID,
+            SEOS_ADF_OID_LEN);
+        if(p) {
+            seos_log_buffer(TAG, "Matched ADF", p, SEOS_ADF_OID_LEN);
+
+            seos_emulator_select_adf(
+                &seos_native_peripheral->params, seos_native_peripheral->credential, response);
+            bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+        } else {
+            FURI_LOG_W(TAG, "Failed to match any ADF OID");
+        }
+
+    } else if(memcmp(apdu, general_authenticate_1, sizeof(general_authenticate_1)) == 0) {
+        seos_emulator_general_authenticate_1(response, seos_native_peripheral->params);
+        bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+    } else if(memcmp(apdu, general_authenticate_2_header, sizeof(general_authenticate_2_header)) == 0) {
+        if(!seos_emulator_general_authenticate_2(
+               apdu,
+               message.len - 1,
+               seos_native_peripheral->credential,
+               &seos_native_peripheral->params,
+               response)) {
+            FURI_LOG_W(TAG, "Failure in General Authenticate 2");
+        } else {
+            bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+        }
+
+        view_dispatcher_send_custom_event(
+            seos_native_peripheral->seos->view_dispatcher, SeosCustomEventAuthenticated);
+        // Prepare for future communication
+        seos_native_peripheral->secure_messaging =
+            secure_messaging_alloc(&seos_native_peripheral->params);
+    } else if(memcmp(apdu, secure_messaging_header, sizeof(secure_messaging_header)) == 0) {
+        uint8_t request_sio[] = {0x5c, 0x02, 0xff, 0x00};
+
+        if(seos_native_peripheral->secure_messaging) {
+            FURI_LOG_D(TAG, "Unwrap secure message");
+
+            // c0 0ccb3fff 16 8508fa8395d30de4e8e097008e085da7edbd833b002d00
+            // Ignore 1 BLE_START byte
+            size_t bytes_to_ignore = 1;
+            BitBuffer* tmp = bit_buffer_alloc(message.len);
+            bit_buffer_append_bytes(
+                tmp, message.buf + bytes_to_ignore, message.len - bytes_to_ignore);
+
+            seos_log_bitbuffer(TAG, "received(wrapped)", tmp);
+            secure_messaging_unwrap_apdu(seos_native_peripheral->secure_messaging, tmp);
+            seos_log_bitbuffer(TAG, "received(clear)", tmp);
+
+            const uint8_t* message = bit_buffer_get_data(tmp);
+            if(memcmp(message, request_sio, sizeof(request_sio)) == 0) {
+                view_dispatcher_send_custom_event(
+                    seos_native_peripheral->seos->view_dispatcher, SeosCustomEventSIORequested);
+
+                BitBuffer* sio_file = bit_buffer_alloc(128);
+                bit_buffer_append_bytes(sio_file, message + 2, 2); // fileId
+                bit_buffer_append_byte(sio_file, seos_native_peripheral->credential->sio_len);
+                bit_buffer_append_bytes(
+                    sio_file,
+                    seos_native_peripheral->credential->sio,
+                    seos_native_peripheral->credential->sio_len);
+
+                secure_messaging_wrap_rapdu(
+                    seos_native_peripheral->secure_messaging,
+                    (uint8_t*)bit_buffer_get_data(sio_file),
+                    bit_buffer_get_size_bytes(sio_file),
+                    response);
+                bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+
+                bit_buffer_free(sio_file);
+            }
+
+            bit_buffer_free(tmp);
+        } else {
+            uint8_t no_sm[] = {0x69, 0x88};
+            bit_buffer_append_bytes(response, no_sm, sizeof(no_sm));
+        }
+    } else if(data[0] == 0xe1) {
+        // ignore
+    } else {
+        FURI_LOG_W(TAG, "no match for message");
+    }
+
+    if(bit_buffer_get_size_bytes(response) > 0) {
+        BitBuffer* tx = bit_buffer_alloc(1 + 2 + 1 + bit_buffer_get_size_bytes(response));
+
+        bit_buffer_append_byte(tx, BLE_START);
+        bit_buffer_append_bytes(
+            tx, bit_buffer_get_data(response), bit_buffer_get_size_bytes(response));
+        ble_profile_seos_tx(
+            seos_native_peripheral->ble_profile,
+            (uint8_t*)bit_buffer_get_data(tx),
+            bit_buffer_get_size_bytes(tx));
+        bit_buffer_free(tx);
+    }
+
+    bit_buffer_free(response);
+}
+
+void seos_native_peripheral_process_message_reader(
+    SeosNativePeripheral* seos_native_peripheral,
+    NativePeripheralMessage message) {
+    BitBuffer* response = bit_buffer_alloc(128); // TODO: MTU
+
+    uint8_t* data = message.buf;
+    uint8_t* rx_data = data + 1; // Match name to nfc version for easier copying
+
+    if(data[0] != BLE_START && data[0] != 0xe1) {
+        FURI_LOG_W(TAG, "Unexpected start of BLE packet");
+    }
+
+    if(memcmp(data + 5, standard_seos_aid, sizeof(standard_seos_aid)) == 0) { // response to select
+        FURI_LOG_I(TAG, "Select ADF");
+        uint8_t select_adf_header[] = {
+            0x80, 0xa5, 0x04, 0x00, (uint8_t)SEOS_ADF_OID_LEN + 2, 0x06, (uint8_t)SEOS_ADF_OID_LEN};
+
+        bit_buffer_append_bytes(response, select_adf_header, sizeof(select_adf_header));
+        bit_buffer_append_bytes(response, SEOS_ADF_OID, SEOS_ADF_OID_LEN);
+        seos_native_peripheral->phase = SELECT_ADF;
+
+    } else if(memcmp(data + 1, cd02, sizeof(cd02)) == 0) {
+        BitBuffer* attribute_value = bit_buffer_alloc(message.len);
+        bit_buffer_append_bytes(attribute_value, message.buf, message.len);
+        if(seos_reader_select_adf_response(
+               attribute_value,
+               1,
+               seos_native_peripheral->credential,
+               &seos_native_peripheral->params)) {
+            // Craft response
+            general_authenticate_1[3] = seos_native_peripheral->params.key_no;
+            bit_buffer_append_bytes(
+                response, general_authenticate_1, sizeof(general_authenticate_1));
+            seos_native_peripheral->phase = GENERAL_AUTHENTICATION_1;
+        }
+        bit_buffer_free(attribute_value);
+    } else if(memcmp(data + 1, ga1_response, sizeof(ga1_response)) == 0) {
+        memcpy(seos_native_peripheral->params.rndICC, data + 5, 8);
+
+        // Craft response
+        uint8_t cryptogram[32 + 8];
+        memset(cryptogram, 0, sizeof(cryptogram));
+        seos_reader_generate_cryptogram(
+            seos_native_peripheral->credential, &seos_native_peripheral->params, cryptogram);
+
+        uint8_t ga_header[] = {
+            0x00,
+            0x87,
+            0x00,
+            seos_native_peripheral->params.key_no,
+            sizeof(cryptogram) + 4,
+            0x7c,
+            sizeof(cryptogram) + 2,
+            0x82,
+            sizeof(cryptogram)};
+
+        bit_buffer_append_bytes(response, ga_header, sizeof(ga_header));
+        bit_buffer_append_bytes(response, cryptogram, sizeof(cryptogram));
+
+        seos_native_peripheral->phase = GENERAL_AUTHENTICATION_2;
+    } else if(rx_data[0] == 0x7C && rx_data[2] == 0x82) { // ga2 response
+        if(rx_data[3] == 40) {
+            if(!seos_reader_verify_cryptogram(&seos_native_peripheral->params, rx_data + 4)) {
+                FURI_LOG_W(TAG, "Card cryptogram failed verification");
+                bit_buffer_free(response);
+                return;
+            }
+            FURI_LOG_I(TAG, "Authenticated");
+            view_dispatcher_send_custom_event(
+                seos_native_peripheral->seos->view_dispatcher, SeosCustomEventAuthenticated);
+        } else {
+            FURI_LOG_W(TAG, "Unhandled card cryptogram size %d", rx_data[3]);
+        }
+
+        seos_native_peripheral->secure_messaging =
+            secure_messaging_alloc(&seos_native_peripheral->params);
+
+        SecureMessaging* secure_messaging = seos_native_peripheral->secure_messaging;
+
+        uint8_t message[] = {0x5c, 0x02, 0xff, 0x00};
+        secure_messaging_wrap_apdu(secure_messaging, message, sizeof(message), response);
+        seos_native_peripheral->phase = REQUEST_SIO;
+        view_dispatcher_send_custom_event(
+            seos_native_peripheral->seos->view_dispatcher, SeosCustomEventSIORequested);
+    } else if(seos_native_peripheral->phase == REQUEST_SIO) {
+        SecureMessaging* secure_messaging = seos_native_peripheral->secure_messaging;
+
+        BitBuffer* rx_buffer = bit_buffer_alloc(message.len - 1);
+        bit_buffer_append_bytes(rx_buffer, rx_data, message.len - 1);
+        seos_log_bitbuffer(TAG, "BLE response(wrapped)", rx_buffer);
+        secure_messaging_unwrap_rapdu(secure_messaging, rx_buffer);
+        seos_log_bitbuffer(TAG, "BLE response(clear)", rx_buffer);
+
+        // Skip fileId
+        seos_native_peripheral->credential->sio_len = bit_buffer_get_byte(rx_buffer, 2);
+        if(seos_native_peripheral->credential->sio_len >
+           sizeof(seos_native_peripheral->credential->sio)) {
+            FURI_LOG_W(TAG, "SIO too long to save");
+            bit_buffer_free(response);
+            return;
+        }
+        memcpy(
+            seos_native_peripheral->credential->sio,
+            bit_buffer_get_data(rx_buffer) + 3,
+            seos_native_peripheral->credential->sio_len);
+        FURI_LOG_I(TAG, "SIO Captured, %d bytes", seos_native_peripheral->credential->sio_len);
+
+        Seos* seos = seos_native_peripheral->seos;
+        view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventReaderSuccess);
+        bit_buffer_free(rx_buffer);
+
+        seos_native_peripheral->phase = SELECT_AID;
+
+    } else if(data[0] == 0xe1) {
+        //ignore
+    } else {
+        FURI_LOG_W(TAG, "No match for write request");
+        seos_log_buffer(TAG, "No match for reader incoming", message.buf, message.len);
+    }
+
+    if(bit_buffer_get_size_bytes(response) > 0) {
+        BitBuffer* tx = bit_buffer_alloc(1 + 2 + 1 + bit_buffer_get_size_bytes(response));
+
+        bit_buffer_append_byte(tx, BLE_START);
+        bit_buffer_append_bytes(
+            tx, bit_buffer_get_data(response), bit_buffer_get_size_bytes(response));
+        ble_profile_seos_tx(
+            seos_native_peripheral->ble_profile,
+            (uint8_t*)bit_buffer_get_data(tx),
+            bit_buffer_get_size_bytes(tx));
+        bit_buffer_free(tx);
+    }
+
+    bit_buffer_free(response);
+}
+
+int32_t seos_native_peripheral_task(void* context) {
+    SeosNativePeripheral* seos_native_peripheral = (SeosNativePeripheral*)context;
+    bool running = true;
+
+    while(running) {
+        uint32_t events = furi_thread_flags_get();
+        if(events & WorkerEvtStop) {
+            running = false;
+            break;
+        }
+
+        if(furi_mutex_acquire(seos_native_peripheral->mq_mutex, 1) == FuriStatusOk) {
+            uint32_t count = furi_message_queue_get_count(seos_native_peripheral->messages);
+            if(count > 0) {
+                if(count > MESSAGE_QUEUE_SIZE / 2) {
+                    FURI_LOG_I(TAG, "Dequeue message [%ld messages]", count);
+                }
+
+                NativePeripheralMessage message = {};
+                FuriStatus status = furi_message_queue_get(
+                    seos_native_peripheral->messages, &message, FuriWaitForever);
+                if(status != FuriStatusOk) {
+                    FURI_LOG_W(TAG, "furi_message_queue_get fail %d", status);
+                }
+
+                if(seos_native_peripheral->flow_mode == FLOW_READER) {
+                    seos_native_peripheral_process_message_reader(seos_native_peripheral, message);
+                } else if(seos_native_peripheral->flow_mode == FLOW_CRED) {
+                    seos_native_peripheral_process_message_cred(seos_native_peripheral, message);
+                }
+            }
+            furi_mutex_release(seos_native_peripheral->mq_mutex);
+        } else {
+            FURI_LOG_W(TAG, "Failed to acquire mutex");
+        }
+
+        // A beat for event flags
+        furi_delay_ms(1);
+    }
+
+    return 0;
+}

+ 31 - 0
seos/seos_native_peripheral.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <bt/bt_service/bt.h>
+#include "seos_common.h"
+#include "seos_profile.h"
+
+typedef struct {
+    Seos* seos;
+
+    Bt* bt;
+    FuriHalBleProfileBase* ble_profile;
+
+    uint8_t event_buffer[128];
+
+    SeosPhase phase;
+    AuthParameters params;
+    SecureMessaging* secure_messaging;
+    SeosCredential* credential;
+    FlowMode flow_mode;
+
+    FuriMessageQueue* messages;
+    FuriMutex* mq_mutex;
+    FuriThread* thread;
+} SeosNativePeripheral;
+
+SeosNativePeripheral* seos_native_peripheral_alloc(Seos* seos);
+
+void seos_native_peripheral_free(SeosNativePeripheral* seos_native_peripheral);
+
+void seos_native_peripheral_start(SeosNativePeripheral* seos_native_peripheral, FlowMode mode);
+void seos_native_peripheral_stop(SeosNativePeripheral* seos_native_peripheral);

+ 4 - 0
seos/seos_native_peripheral_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_native_peripheral.h"

+ 136 - 0
seos/seos_profile.c

@@ -0,0 +1,136 @@
+#include "seos_profile.h"
+
+#include <gap.h>
+#include <furi_ble/profile_interface.h>
+#include "seos_service.h"
+#include "seos_service_uuid.inc"
+#include <ble/core/ble_defs.h>
+#include <furi.h>
+
+#define TAG "SeosProfile"
+
+typedef struct {
+    FuriHalBleProfileBase base;
+    BleServiceSeos* seos_svc;
+    FuriHalBleProfileParams params;
+} BleProfileSeos;
+_Static_assert(offsetof(BleProfileSeos, base) == 0, "Wrong layout");
+
+static FuriHalBleProfileBase* ble_profile_seos_start(FuriHalBleProfileParams profile_params) {
+    BleProfileSeos* profile = malloc(sizeof(BleProfileSeos));
+    BleProfileParams* params = profile_params;
+
+    profile->params = profile_params;
+    profile->base.config = ble_profile_seos;
+
+    profile->seos_svc = ble_svc_seos_start(params->mode);
+
+    return &profile->base;
+}
+
+static void ble_profile_seos_stop(FuriHalBleProfileBase* profile) {
+    furi_check(profile);
+    furi_check(profile->config == ble_profile_seos);
+
+    BleProfileSeos* seos_profile = (BleProfileSeos*)profile;
+    ble_svc_seos_stop(seos_profile->seos_svc);
+}
+
+// AN5289: 4.7, in order to use flash controller interval must be at least 25ms + advertisement, which is 30 ms
+// Since we don't use flash controller anymore interval can be lowered to 7.5ms
+#define CONNECTION_INTERVAL_MIN (0x06)
+// Up to 45 ms
+#define CONNECTION_INTERVAL_MAX (0x24)
+
+static GapConfig seos_reader_template_config = {
+    .adv_service.UUID_Type = UUID_TYPE_128,
+    .adv_service.Service_UUID_128 = BLE_SVC_SEOS_READER_UUID,
+    .mfg_data =
+        {0x2e,
+         0x01,
+         0x15,
+         0x4b,
+         0xe2,
+         0xb6,
+         0xb6,
+         0xb6,
+         0x2a,
+         0x46,
+         0x4c,
+         0x30,
+         0x4b,
+         0x37,
+         0x5a,
+         0x30,
+         0x31,
+         0x55,
+         0x31},
+    .mfg_data_len = 19,
+    .adv_name = "Seos",
+    .bonding_mode = false,
+    .pairing_method = GapPairingNone,
+    .conn_param = {
+        .conn_int_min = CONNECTION_INTERVAL_MIN,
+        .conn_int_max = CONNECTION_INTERVAL_MAX,
+        .slave_latency = 0,
+        .supervisor_timeout = 0,
+    }};
+
+static GapConfig seos_cred_template_config = {
+    .adv_service.UUID_Type = UUID_TYPE_128,
+    .adv_service.Service_UUID_128 = BLE_SVC_SEOS_CRED_UUID,
+    .adv_name = "Flp",
+    .bonding_mode = false,
+    .pairing_method = GapPairingNone,
+    .conn_param = {
+        .conn_int_min = CONNECTION_INTERVAL_MIN,
+        .conn_int_max = CONNECTION_INTERVAL_MAX,
+        .slave_latency = 0,
+        .supervisor_timeout = 0,
+    }};
+
+static void
+    ble_profile_seos_get_config(GapConfig* config, FuriHalBleProfileParams profile_params) {
+    BleProfileParams* params = profile_params;
+
+    FURI_LOG_D(TAG, "ble_profile_seos_get_config FlowMode %d", params->mode);
+    furi_check(config);
+    if(params->mode == FLOW_READER) {
+        memcpy(config, &seos_reader_template_config, sizeof(GapConfig));
+    } else if(params->mode == FLOW_CRED) {
+        memcpy(config, &seos_cred_template_config, sizeof(GapConfig));
+    }
+    // Set mac address
+    memcpy(config->mac_address, furi_hal_version_get_ble_mac(), sizeof(config->mac_address));
+}
+
+static const FuriHalBleProfileTemplate profile_callbacks = {
+    .start = ble_profile_seos_start,
+    .stop = ble_profile_seos_stop,
+    .get_gap_config = ble_profile_seos_get_config,
+};
+
+const FuriHalBleProfileTemplate* ble_profile_seos = &profile_callbacks;
+
+void ble_profile_seos_set_event_callback(
+    FuriHalBleProfileBase* profile,
+    uint16_t buff_size,
+    FuriHalBtSeosCallback callback,
+    void* context) {
+    furi_check(profile && (profile->config == ble_profile_seos));
+
+    BleProfileSeos* seos_profile = (BleProfileSeos*)profile;
+    ble_svc_seos_set_callbacks(seos_profile->seos_svc, buff_size, callback, context);
+}
+
+bool ble_profile_seos_tx(FuriHalBleProfileBase* profile, uint8_t* data, uint16_t size) {
+    furi_check(profile && (profile->config == ble_profile_seos));
+
+    BleProfileSeos* seos_profile = (BleProfileSeos*)profile;
+
+    if(size > BLE_PROFILE_SEOS_PACKET_SIZE_MAX) {
+        return false;
+    }
+
+    return ble_svc_seos_update_tx(seos_profile->seos_svc, data, size);
+}

+ 49 - 0
seos/seos_profile.h

@@ -0,0 +1,49 @@
+#pragma once
+
+#include <furi_ble/profile_interface.h>
+
+#include "seos_common.h"
+#include "seos_service.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define BLE_PROFILE_SEOS_PACKET_SIZE_MAX BLE_SVC_SEOS_DATA_LEN_MAX
+
+typedef struct ble_profile_params {
+    FlowMode mode;
+} BleProfileParams;
+
+/** Seos service callback type */
+typedef SeosServiceEventCallback FuriHalBtSeosCallback;
+
+/** Seos profile descriptor */
+extern const FuriHalBleProfileTemplate* ble_profile_seos;
+
+/** Send data through BLE
+ *
+ * @param profile       Profile instance
+ * @param data          data buffer
+ * @param size          data buffer size
+ *
+ * @return      true on success
+ */
+bool ble_profile_seos_tx(FuriHalBleProfileBase* profile, uint8_t* data, uint16_t size);
+
+/** Set Seos service events callback
+ *
+ * @param profile       Profile instance
+ * @param buffer_size   Applicaition buffer size
+ * @param calback       FuriHalBtSeosCallback instance
+ * @param context       pointer to context
+ */
+void ble_profile_seos_set_event_callback(
+    FuriHalBleProfileBase* profile,
+    uint16_t buff_size,
+    FuriHalBtSeosCallback callback,
+    void* context);
+
+#ifdef __cplusplus
+}
+#endif

+ 475 - 0
seos/seos_reader.c

@@ -0,0 +1,475 @@
+#include "seos_reader_i.h"
+
+#define TAG "SeosReader"
+
+static uint8_t success[] = {0x90, 0x00};
+static uint8_t select[] =
+    {0x00, 0xa4, 0x04, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00};
+static uint8_t SEOS_APPLET_FCI[] =
+    {0x6F, 0x0C, 0x84, 0x0A, 0xA0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+
+// TODO: support value from keys file
+static uint8_t select_adf[] = {0x80, 0xa5, 0x04, 0x00, 0x13, 0x06, 0x11, 0x2b, 0x06,
+                               0x01, 0x04, 0x01, 0x81, 0xe4, 0x38, 0x01, 0x01, 0x02,
+                               0x01, 0x18, 0x01, 0x01, 0x02, 0x02, 0x00};
+
+static uint8_t general_authenticate_1[] =
+    {0x00, 0x87, 0x00, 0x01, 0x04, 0x7c, 0x02, 0x81, 0x00, 0x00};
+
+SeosReader* seos_reader_alloc(SeosCredential* credential, Iso14443_4aPoller* iso14443_4a_poller) {
+    SeosReader* seos_reader = malloc(sizeof(SeosReader));
+    memset(seos_reader, 0, sizeof(SeosReader));
+    seos_reader->params.key_no = 1;
+    seos_reader->secure_messaging = NULL;
+    memset(seos_reader->params.cNonce, 0x0c, sizeof(seos_reader->params.cNonce));
+    memset(seos_reader->params.UID, 0x0d, sizeof(seos_reader->params.UID));
+
+    seos_reader->credential = credential;
+    seos_reader->iso14443_4a_poller = iso14443_4a_poller;
+
+    seos_reader->tx_buffer = bit_buffer_alloc(SEOS_WORKER_MAX_BUFFER_SIZE);
+    seos_reader->rx_buffer = bit_buffer_alloc(SEOS_WORKER_MAX_BUFFER_SIZE);
+
+    return seos_reader;
+}
+
+void seos_reader_free(SeosReader* seos_reader) {
+    furi_assert(seos_reader);
+    bit_buffer_free(seos_reader->tx_buffer);
+    bit_buffer_free(seos_reader->rx_buffer);
+    if(seos_reader->secure_messaging) {
+        secure_messaging_free(seos_reader->secure_messaging);
+    }
+    free(seos_reader);
+}
+
+bool seos_reader_request_sio(SeosReader* seos_reader) {
+    SecureMessaging* secure_messaging = seos_reader->secure_messaging;
+    furi_assert(secure_messaging);
+    Iso14443_4aPoller* iso14443_4a_poller = seos_reader->iso14443_4a_poller;
+    BitBuffer* tx_buffer = seos_reader->tx_buffer;
+    BitBuffer* rx_buffer = seos_reader->rx_buffer;
+    Iso14443_4aError error;
+
+    uint8_t message[] = {0x5c, 0x02, 0xff, 0x00};
+    secure_messaging_wrap_apdu(secure_messaging, message, sizeof(message), tx_buffer);
+
+    seos_log_bitbuffer(TAG, "NFC transmit", tx_buffer);
+    error = iso14443_4a_poller_send_block(iso14443_4a_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4aErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error);
+        return false;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    seos_log_bitbuffer(TAG, "NFC response(wrapped)", rx_buffer);
+    secure_messaging_unwrap_rapdu(secure_messaging, rx_buffer);
+    seos_log_bitbuffer(TAG, "NFC response(clear)", rx_buffer);
+
+    // Skip fileId
+    seos_reader->credential->sio_len = bit_buffer_get_byte(rx_buffer, 2);
+    if(seos_reader->credential->sio_len > sizeof(seos_reader->credential->sio)) {
+        FURI_LOG_W(TAG, "SIO too long to save");
+        return false;
+    }
+    memcpy(
+        seos_reader->credential->sio,
+        bit_buffer_get_data(rx_buffer) + 3,
+        seos_reader->credential->sio_len);
+
+    return true;
+}
+
+void seos_reader_generate_cryptogram(
+    SeosCredential* credential,
+    AuthParameters* params,
+    uint8_t* cryptogram) {
+    seos_worker_diversify_key(
+        SEOS_ADF1_READ,
+        credential->diversifier,
+        credential->diversifier_len,
+        SEOS_ADF_OID,
+        SEOS_ADF_OID_LEN,
+        params->cipher,
+        params->hash,
+        params->key_no,
+        true,
+        params->priv_key);
+    seos_worker_diversify_key(
+        SEOS_ADF1_READ,
+        credential->diversifier,
+        credential->diversifier_len,
+        SEOS_ADF_OID,
+        SEOS_ADF_OID_LEN,
+        params->cipher,
+        params->hash,
+        params->key_no,
+        false,
+        params->auth_key);
+
+    uint8_t clear[32];
+    memset(clear, 0, sizeof(clear));
+    size_t index = 0;
+    memcpy(clear + index, params->UID, sizeof(params->UID));
+    index += sizeof(params->UID);
+    memcpy(clear + index, params->rndICC, sizeof(params->rndICC));
+    index += sizeof(params->rndICC);
+    memcpy(clear + index, params->cNonce, sizeof(params->cNonce));
+    index += sizeof(params->cNonce);
+
+    uint8_t cmac[16];
+    if(params->cipher == AES_128_CBC) {
+        seos_worker_aes_encrypt(params->priv_key, sizeof(clear), clear, cryptogram);
+
+        aes_cmac(params->auth_key, sizeof(params->auth_key), cryptogram, index, cmac);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_encrypt(params->priv_key, sizeof(clear), clear, cryptogram);
+
+        des_cmac(params->auth_key, sizeof(params->auth_key), cryptogram, index, cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+    memcpy(cryptogram + sizeof(clear), cmac, SEOS_WORKER_CMAC_SIZE);
+}
+
+bool seos_reader_verify_cryptogram(AuthParameters* params, const uint8_t* cryptogram) {
+    // cryptogram is 40 bytes: 32 byte encrypted + 8 byte cmac
+    size_t encrypted_len = 32;
+    uint8_t* mac = (uint8_t*)cryptogram + encrypted_len;
+    uint8_t cmac[16];
+    if(params->cipher == AES_128_CBC) {
+        aes_cmac(
+            params->auth_key, sizeof(params->auth_key), (uint8_t*)cryptogram, encrypted_len, cmac);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        des_cmac(
+            params->auth_key, sizeof(params->auth_key), (uint8_t*)cryptogram, encrypted_len, cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    if(memcmp(cmac, mac, SEOS_WORKER_CMAC_SIZE) != 0) {
+        FURI_LOG_W(TAG, "Incorrect cryptogram mac %02x... vs %02x...", cmac[0], mac[0]);
+        return false;
+    }
+
+    uint8_t clear[32];
+    memset(clear, 0, sizeof(clear));
+    if(params->cipher == AES_128_CBC) {
+        seos_worker_aes_decrypt(params->priv_key, encrypted_len, cryptogram, clear);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_decrypt(params->priv_key, encrypted_len, cryptogram, clear);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    // rndICC[8], UID[8], rNonce[16]
+    uint8_t* rndICC = clear;
+    if(memcmp(rndICC, params->rndICC, sizeof(params->rndICC)) != 0) {
+        FURI_LOG_W(TAG, "Incorrect rndICC returned");
+        return false;
+    }
+    uint8_t* UID = clear + 8;
+    if(memcmp(UID, params->UID, sizeof(params->UID)) != 0) {
+        FURI_LOG_W(TAG, "Incorrect UID returned");
+        return false;
+    }
+
+    memcpy(params->rNonce, clear + 8 + 8, sizeof(params->rNonce));
+    return true;
+}
+
+NfcCommand seos_reader_select_aid(SeosReader* seos_reader) {
+    Iso14443_4aPoller* iso14443_4a_poller = seos_reader->iso14443_4a_poller;
+    BitBuffer* tx_buffer = seos_reader->tx_buffer;
+    BitBuffer* rx_buffer = seos_reader->rx_buffer;
+
+    NfcCommand ret = NfcCommandContinue;
+    Iso14443_4aError error;
+
+    bit_buffer_append_bytes(tx_buffer, select, sizeof(select));
+    error = iso14443_4a_poller_send_block(iso14443_4a_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4aErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    seos_log_bitbuffer(TAG, "NFC response", rx_buffer);
+
+    // TODO: validate response
+
+    if(memcmp(
+           bit_buffer_get_data(rx_buffer) + bit_buffer_get_size_bytes(rx_buffer) - sizeof(success),
+           success,
+           sizeof(success)) != 0) {
+        FURI_LOG_W(TAG, "Non-success response");
+        return NfcCommandStop;
+    }
+
+    if(memcmp(bit_buffer_get_data(rx_buffer), SEOS_APPLET_FCI, sizeof(SEOS_APPLET_FCI)) != 0) {
+        FURI_LOG_W(TAG, "Unexpected select AID response");
+        return NfcCommandStop;
+    }
+
+    return ret;
+}
+
+NfcCommand seos_reader_select_adf(SeosReader* seos_reader) {
+    Iso14443_4aPoller* iso14443_4a_poller = seos_reader->iso14443_4a_poller;
+    BitBuffer* tx_buffer = seos_reader->tx_buffer;
+    BitBuffer* rx_buffer = seos_reader->rx_buffer;
+
+    NfcCommand ret = NfcCommandContinue;
+    Iso14443_4aError error;
+
+    bit_buffer_append_bytes(tx_buffer, select_adf, sizeof(select_adf));
+    error = iso14443_4a_poller_send_block(iso14443_4a_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4aErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+    return ret;
+}
+
+bool seos_reader_select_adf_response(
+    BitBuffer* rx_buffer,
+    size_t offset,
+    SeosCredential* credential,
+    AuthParameters* params) {
+    seos_log_bitbuffer(TAG, "response", rx_buffer);
+
+    // cd 02 0206
+    // 85 38 41c01a89db89aecf 4b35b4f18dc4045b2a3d65cdd1c1944e8c8548f786e6c51128a5c8546a27120a7e44ba0f4cd7218a026ea1a73a9211a9
+    // 8e 08 20f830009042cb85
+
+    uint8_t expected_header[] = {0xcd, 0x02};
+    if(bit_buffer_get_size_bytes(rx_buffer) < sizeof(expected_header)) {
+        FURI_LOG_W(TAG, "Invalid response length");
+        return false;
+    }
+    // handle when the buffer starts with other stuff
+    const uint8_t* rx_data = bit_buffer_get_data(rx_buffer) + offset;
+    if(memcmp(rx_data, expected_header, sizeof(expected_header)) != 0) {
+        FURI_LOG_W(TAG, "Invalid response");
+        return false;
+    }
+    params->cipher = rx_data[2];
+    params->hash = rx_data[3];
+
+    size_t bufLen = 0;
+    uint8_t clear[0x40];
+
+    if(params->cipher == AES_128_CBC) {
+        size_t ivLen = 16;
+        bufLen = rx_data[5] - ivLen;
+        uint8_t* iv = (uint8_t*)rx_data + 6;
+        uint8_t* enc = (uint8_t*)rx_data + 6 + ivLen;
+        mbedtls_aes_context ctx;
+        mbedtls_aes_init(&ctx);
+        mbedtls_aes_setkey_dec(&ctx, SEOS_ADF1_PRIV_ENC, sizeof(SEOS_ADF1_PRIV_ENC) * 8);
+        mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_DECRYPT, bufLen, iv, enc, clear);
+        mbedtls_aes_free(&ctx);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        size_t ivLen = 8;
+        bufLen = rx_data[5] - ivLen;
+        uint8_t* iv = (uint8_t*)rx_data + 6;
+        uint8_t* enc = (uint8_t*)rx_data + 6 + ivLen;
+
+        mbedtls_des3_context ctx;
+        mbedtls_des3_init(&ctx);
+        mbedtls_des3_set2key_dec(&ctx, SEOS_ADF1_PRIV_ENC);
+        mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_DECRYPT, bufLen, iv, enc, clear);
+        mbedtls_des3_free(&ctx);
+    }
+
+    // 06112b0601040181e438010102011801010202 cf 07 3d4c010c71cfa7 e2d0b41a00cc5e494c8d52b6e562592399fe614a
+    if(clear[0] != 0x06) {
+        FURI_LOG_W(TAG, "Missing expected 0x06 at start of clear");
+        return false;
+    }
+    size_t oidLen = clear[1];
+    if(clear[2 + oidLen] != 0xCF) {
+        FURI_LOG_W(TAG, "Missing expected 0xCF after OID");
+        return false;
+    }
+    credential->diversifier_len = clear[2 + oidLen + 1];
+    if(credential->diversifier_len > sizeof(credential->diversifier)) {
+        FURI_LOG_W(TAG, "diversifier too large");
+        return false;
+    }
+
+    uint8_t* diversifier = clear + 2 + oidLen + 2;
+    memcpy(credential->diversifier, diversifier, credential->diversifier_len);
+
+    char display[SEOS_WORKER_MAX_BUFFER_SIZE * 2 + 1];
+    memset(display, 0, sizeof(display));
+    for(uint8_t i = 0; i < credential->diversifier_len; i++) {
+        snprintf(display + (i * 2), sizeof(display), "%02x", diversifier[i]);
+    }
+    FURI_LOG_D(TAG, "diversifier: %s", display);
+
+    return true;
+}
+
+NfcCommand seos_reader_general_authenticate_1(SeosReader* seos_reader) {
+    Iso14443_4aPoller* iso14443_4a_poller = seos_reader->iso14443_4a_poller;
+    BitBuffer* tx_buffer = seos_reader->tx_buffer;
+    BitBuffer* rx_buffer = seos_reader->rx_buffer;
+
+    NfcCommand ret = NfcCommandContinue;
+    Iso14443_4aError error;
+
+    general_authenticate_1[3] = seos_reader->params.key_no;
+    bit_buffer_append_bytes(tx_buffer, general_authenticate_1, sizeof(general_authenticate_1));
+    seos_log_bitbuffer(TAG, "NFC transmit", tx_buffer);
+
+    error = iso14443_4a_poller_send_block(iso14443_4a_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4aErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    seos_log_bitbuffer(TAG, "NFC response", rx_buffer);
+    // 7c0a8108018cde7d6049edb09000
+
+    uint8_t expected_header[] = {0x7c, 0x0a, 0x81, 0x08};
+    const uint8_t* rx_data = bit_buffer_get_data(rx_buffer);
+    if(memcmp(rx_data, expected_header, sizeof(expected_header)) != 0) {
+        FURI_LOG_W(TAG, "Invalid response");
+        return NfcCommandStop;
+    }
+
+    memcpy(seos_reader->params.rndICC, rx_data + 4, 8);
+
+    return ret;
+}
+
+NfcCommand seos_reader_general_authenticate_2(SeosReader* seos_reader) {
+    Iso14443_4aPoller* iso14443_4a_poller = seos_reader->iso14443_4a_poller;
+    BitBuffer* tx_buffer = seos_reader->tx_buffer;
+    BitBuffer* rx_buffer = seos_reader->rx_buffer;
+
+    NfcCommand ret = NfcCommandContinue;
+    Iso14443_4aError error;
+
+    uint8_t cryptogram[32 + 8];
+    memset(cryptogram, 0, sizeof(cryptogram));
+    seos_reader_generate_cryptogram(seos_reader->credential, &seos_reader->params, cryptogram);
+
+    uint8_t ga_header[] = {
+        0x00, 0x87, 0x00, seos_reader->params.key_no, 0x2c, 0x7c, 0x2a, 0x82, 0x28};
+
+    bit_buffer_append_bytes(tx_buffer, ga_header, sizeof(ga_header));
+    bit_buffer_append_bytes(tx_buffer, cryptogram, sizeof(cryptogram));
+    seos_log_bitbuffer(TAG, "NFC transmit", tx_buffer);
+
+    error = iso14443_4a_poller_send_block(iso14443_4a_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4aErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    seos_log_bitbuffer(TAG, "NFC response", rx_buffer);
+
+    const uint8_t* rx_data = bit_buffer_get_data(rx_buffer);
+    if(rx_data[0] != 0x7C || rx_data[2] != 0x82) {
+        FURI_LOG_W(TAG, "Invalid rx_data");
+        return NfcCommandStop;
+    }
+
+    if(rx_data[3] == 40) {
+        if(!seos_reader_verify_cryptogram(&seos_reader->params, rx_data + 4)) {
+            FURI_LOG_W(TAG, "Card cryptogram failed verification");
+            return NfcCommandStop;
+        }
+        FURI_LOG_I(TAG, "Authenticated");
+    } else {
+        FURI_LOG_W(TAG, "Unhandled card cryptogram size %d", rx_data[3]);
+    }
+
+    seos_reader->secure_messaging = secure_messaging_alloc(&seos_reader->params);
+
+    return ret;
+}
+
+NfcCommand seos_state_machine(Seos* seos, Iso14443_4aPoller* iso14443_4a_poller) {
+    furi_assert(seos);
+    NfcCommand ret = NfcCommandContinue;
+
+    SeosReader* seos_reader = seos_reader_alloc(&seos->credential, iso14443_4a_poller);
+    seos->seos_reader = seos_reader;
+
+    do {
+        ret = seos_reader_select_aid(seos_reader);
+        if(ret == NfcCommandStop) break;
+
+        ret = seos_reader_select_adf(seos_reader);
+        if(ret == NfcCommandStop) break;
+
+        if(!seos_reader_select_adf_response(
+               seos_reader->rx_buffer, 0, seos_reader->credential, &seos_reader->params)) {
+            break;
+        }
+
+        ret = seos_reader_general_authenticate_1(seos_reader);
+        if(ret == NfcCommandStop) break;
+
+        ret = seos_reader_general_authenticate_2(seos_reader);
+        if(ret == NfcCommandStop) break;
+
+        if(seos_reader_request_sio(seos_reader)) {
+            view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventReaderSuccess);
+        }
+
+    } while(false);
+
+    // An error occurred
+    if(ret == NfcCommandStop) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventReaderError);
+    }
+    seos_reader_free(seos_reader);
+
+    return NfcCommandStop;
+}
+
+NfcCommand seos_worker_poller_callback(NfcGenericEvent event, void* context) {
+    furi_assert(event.protocol == NfcProtocolIso14443_4a);
+    NfcCommand ret = NfcCommandContinue;
+
+    Seos* seos = context;
+
+    const Iso14443_4aPollerEvent* iso14443_4a_event = event.event_data;
+    Iso14443_4aPoller* iso14443_4a_poller = event.instance;
+
+    if(iso14443_4a_event->type == Iso14443_4aPollerEventTypeReady) {
+        // view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventPollerDetect);
+
+        nfc_device_set_data(
+            seos->nfc_device, NfcProtocolIso14443_4a, nfc_poller_get_data(seos->poller));
+
+        ret = seos_state_machine(seos, iso14443_4a_poller);
+
+        // furi_thread_set_current_priority(FuriThreadPriorityLowest);
+    } else if(iso14443_4a_event->type == Iso14443_4aPollerEventTypeError) {
+        Iso14443_4aPollerEventData* data = iso14443_4a_event->data;
+        Iso14443_4aError error = data->error;
+        FURI_LOG_W(TAG, "Iso14443_4aError %i", error);
+        // I was hoping to catch MFC here, but it seems to be treated the same (None) as no card being present.
+        switch(error) {
+        case Iso14443_4aErrorNone:
+            break;
+        case Iso14443_4aErrorNotPresent:
+            break;
+        case Iso14443_4aErrorProtocol:
+            ret = NfcCommandStop;
+            break;
+        case Iso14443_4aErrorTimeout:
+            break;
+        }
+    }
+
+    return ret;
+}

+ 45 - 0
seos/seos_reader.h

@@ -0,0 +1,45 @@
+#pragma once
+
+#include <storage/storage.h>
+#include <dialogs/dialogs.h>
+#include <lib/toolbox/path.h>
+#include <lib/nfc/protocols/nfc_generic_event.h>
+#include <lib/nfc/protocols/iso14443_4a/iso14443_4a_poller.h>
+#include <lib/nfc/helpers/iso14443_crc.h>
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+
+#include "secure_messaging.h"
+
+NfcCommand seos_worker_poller_callback(NfcGenericEvent event, void* context);
+
+typedef struct {
+    Iso14443_4aPoller* iso14443_4a_poller;
+    BitBuffer* tx_buffer;
+    BitBuffer* rx_buffer;
+
+    AuthParameters params;
+    SecureMessaging* secure_messaging;
+
+    SeosCredential* credential;
+
+} SeosReader;
+
+SeosReader* seos_reader_alloc(SeosCredential* credential, Iso14443_4aPoller* iso14443_4a_poller);
+
+void seos_reader_free(SeosReader* seos_reader);
+
+bool seos_reader_save(SeosReader* seos_reader, const char* dev_name);
+
+bool seos_reader_select_adf_response(
+    BitBuffer* rx_buffer,
+    size_t offset,
+    SeosCredential* credential,
+    AuthParameters* params);
+
+void seos_reader_generate_cryptogram(
+    SeosCredential* credential,
+    AuthParameters* params,
+    uint8_t* cryptogram);
+
+bool seos_reader_verify_cryptogram(AuthParameters* params, const uint8_t* cryptogram);

+ 5 - 0
seos/seos_reader_i.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_reader.h"
+#include "keys.h"

+ 228 - 0
seos/seos_service.c

@@ -0,0 +1,228 @@
+#include "seos_service.h"
+#include "headers/app_common.h"
+#include <furi_ble/event_dispatcher.h>
+#include <furi_ble/gatt.h>
+
+#include "headers/ble_vs_codes.h"
+#include "headers/ble_gatt_aci.h"
+
+#include <furi.h>
+
+#include "seos_service_uuid.inc"
+#include <stdint.h>
+
+#define TAG "BtSeosSvc"
+
+static uint8_t select[] =
+    {0xc0, 0x00, 0xa4, 0x04, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+
+typedef enum {
+    SeosSvcGattCharacteristicRxTx = 0,
+    SeosSvcGattCharacteristicCount,
+} SeosSvcGattCharacteristicId;
+
+typedef struct {
+    const void* data_ptr;
+    uint16_t data_len;
+} SeosSvcDataWrapper;
+
+static bool
+    ble_svc_seos_data_callback(const void* context, const uint8_t** data, uint16_t* data_len) {
+    FURI_LOG_D(TAG, "ble_svc_seos_data_callback");
+    const SeosSvcDataWrapper* report_data = context;
+    if(data) {
+        *data = report_data->data_ptr;
+        *data_len = report_data->data_len;
+    } else {
+        *data_len = BLE_SVC_SEOS_DATA_LEN_MAX;
+    }
+    return false;
+}
+
+static BleGattCharacteristicParams ble_svc_seos_chars[SeosSvcGattCharacteristicCount] = {
+    [SeosSvcGattCharacteristicRxTx] =
+        {.name = "SEOS",
+         .data_prop_type = FlipperGattCharacteristicDataCallback,
+         .data.callback.fn = ble_svc_seos_data_callback,
+         .data.callback.context = NULL,
+         .uuid.Char_UUID_128 = BLE_SVC_SEOS_READER_CHAR_UUID, // changed before init
+         .uuid_type = UUID_TYPE_128,
+         .char_properties = CHAR_PROP_WRITE_WITHOUT_RESP | CHAR_PROP_NOTIFY,
+         .security_permissions = ATTR_PERMISSION_NONE,
+         .gatt_evt_mask = GATT_NOTIFY_ATTRIBUTE_WRITE,
+         .is_variable = CHAR_VALUE_LEN_VARIABLE},
+};
+
+struct BleServiceSeos {
+    uint16_t svc_handle;
+    BleGattCharacteristicInstance chars[SeosSvcGattCharacteristicCount];
+    FuriMutex* buff_size_mtx;
+    uint32_t buff_size;
+    uint16_t bytes_ready_to_receive;
+    SeosServiceEventCallback callback;
+    void* context;
+    GapSvcEventHandler* event_handler;
+    FlowMode mode;
+};
+
+static BleEventAckStatus ble_svc_seos_event_handler(void* event, void* context) {
+    BleServiceSeos* seos_svc = (BleServiceSeos*)context;
+    BleEventAckStatus ret = BleEventNotAck;
+    hci_event_pckt* event_pckt = (hci_event_pckt*)(((hci_uart_pckt*)event)->data);
+    evt_blecore_aci* blecore_evt = (evt_blecore_aci*)event_pckt->data;
+    aci_gatt_attribute_modified_event_rp0* attribute_modified;
+
+    if(event_pckt->evt == HCI_LE_META_EVT_CODE) {
+    } else if(event_pckt->evt == HCI_DISCONNECTION_COMPLETE_EVT_CODE) {
+    } else if(event_pckt->evt == HCI_VENDOR_SPECIFIC_DEBUG_EVT_CODE) {
+        if(blecore_evt->ecode == ACI_GATT_ATTRIBUTE_MODIFIED_VSEVT_CODE) {
+            attribute_modified = (aci_gatt_attribute_modified_event_rp0*)blecore_evt->data;
+            if(attribute_modified->Attr_Handle ==
+               seos_svc->chars[SeosSvcGattCharacteristicRxTx].handle + 2) {
+                // Descriptor handle
+                ret = BleEventAckFlowEnable;
+                FURI_LOG_D(TAG, "Descriptor event %d bytes", attribute_modified->Attr_Data_Length);
+
+                if(attribute_modified->Attr_Data_Length == 2) {
+                    uint16_t* value = (uint16_t*)attribute_modified->Attr_Data;
+                    if(*value == 1) { // ENABLE_NOTIFICATION_VALUE)
+                        if(seos_svc->mode == FLOW_READER) {
+                            SeosSvcDataWrapper report_data = {
+                                .data_ptr = select, .data_len = sizeof(select)};
+
+                            ble_gatt_characteristic_update(
+                                seos_svc->svc_handle,
+                                &seos_svc->chars[SeosSvcGattCharacteristicRxTx],
+                                &report_data);
+                        } else if(seos_svc->mode == FLOW_CRED) {
+                            FURI_LOG_D(TAG, "No action for FLOW_CRED after subscribe");
+                        }
+                    }
+                }
+            } else if(
+                attribute_modified->Attr_Handle ==
+                seos_svc->chars[SeosSvcGattCharacteristicRxTx].handle + 1) {
+                FURI_LOG_D(TAG, "Received %d bytes", attribute_modified->Attr_Data_Length);
+                if(seos_svc->callback) {
+                    furi_check(
+                        furi_mutex_acquire(seos_svc->buff_size_mtx, FuriWaitForever) ==
+                        FuriStatusOk);
+                    SeosServiceEvent event = {
+                        .event = SeosServiceEventTypeDataReceived,
+                        .data = {
+                            .buffer = attribute_modified->Attr_Data,
+                            .size = attribute_modified->Attr_Data_Length,
+                        }};
+                    uint32_t buff_free_size = seos_svc->callback(event, seos_svc->context);
+                    FURI_LOG_D(TAG, "Available buff size: %ld", buff_free_size);
+                    furi_check(furi_mutex_release(seos_svc->buff_size_mtx) == FuriStatusOk);
+                } else {
+                    FURI_LOG_W(TAG, "No seos_cvs->callback defined");
+                }
+                ret = BleEventAckFlowEnable;
+            }
+        } else if(blecore_evt->ecode == ACI_GATT_SERVER_CONFIRMATION_VSEVT_CODE) {
+            FURI_LOG_T(TAG, "Ack received");
+            if(seos_svc->callback) {
+                SeosServiceEvent event = {
+                    .event = SeosServiceEventTypeDataSent,
+                };
+                seos_svc->callback(event, seos_svc->context);
+            }
+            ret = BleEventAckFlowEnable;
+        } else {
+            FURI_LOG_D(
+                TAG,
+                "ble_svc_seos_event_handler unhandled blecore_evt->ecode %d",
+                blecore_evt->ecode);
+        }
+    } else {
+        FURI_LOG_D(
+            TAG, "ble_svc_seos_event_handler unhandled event_pckt->evt %d", event_pckt->evt);
+    }
+    return ret;
+}
+
+BleServiceSeos* ble_svc_seos_start(FlowMode mode) {
+    BleServiceSeos* seos_svc = malloc(sizeof(BleServiceSeos));
+    seos_svc->mode = mode;
+
+    seos_svc->event_handler =
+        ble_event_dispatcher_register_svc_handler(ble_svc_seos_event_handler, seos_svc);
+
+    if(mode == FLOW_READER) {
+        if(!ble_gatt_service_add(
+               UUID_TYPE_128, &reader_service_uuid, PRIMARY_SERVICE, 12, &seos_svc->svc_handle)) {
+            free(seos_svc);
+            return NULL;
+        }
+    } else if(mode == FLOW_CRED) {
+        if(!ble_gatt_service_add(
+               UUID_TYPE_128, &cred_service_uuid, PRIMARY_SERVICE, 12, &seos_svc->svc_handle)) {
+            free(seos_svc);
+            return NULL;
+        }
+    }
+
+    for(uint8_t i = 0; i < SeosSvcGattCharacteristicCount; i++) {
+        if(i == SeosSvcGattCharacteristicRxTx) {
+            if(mode == FLOW_READER) {
+                uint8_t uuid[] = BLE_SVC_SEOS_READER_CHAR_UUID;
+                memcpy(
+                    ble_svc_seos_chars[SeosSvcGattCharacteristicRxTx].uuid.Char_UUID_128,
+                    uuid,
+                    sizeof(uuid));
+            } else if(mode == FLOW_CRED) {
+                uint8_t uuid[] = BLE_SVC_SEOS_CRED_CHAR_UUID;
+                memcpy(
+                    ble_svc_seos_chars[SeosSvcGattCharacteristicRxTx].uuid.Char_UUID_128,
+                    uuid,
+                    sizeof(uuid));
+            }
+            ble_gatt_characteristic_init(
+                seos_svc->svc_handle, &ble_svc_seos_chars[i], &seos_svc->chars[i]);
+        }
+    }
+
+    seos_svc->buff_size_mtx = furi_mutex_alloc(FuriMutexTypeNormal);
+
+    return seos_svc;
+}
+
+void ble_svc_seos_set_callbacks(
+    BleServiceSeos* seos_svc,
+    uint16_t buff_size,
+    SeosServiceEventCallback callback,
+    void* context) {
+    furi_check(seos_svc);
+    seos_svc->callback = callback;
+    seos_svc->context = context;
+    seos_svc->buff_size = buff_size;
+    seos_svc->bytes_ready_to_receive = buff_size;
+}
+
+void ble_svc_seos_stop(BleServiceSeos* seos_svc) {
+    furi_check(seos_svc);
+
+    ble_event_dispatcher_unregister_svc_handler(seos_svc->event_handler);
+
+    for(uint8_t i = 0; i < SeosSvcGattCharacteristicCount; i++) {
+        ble_gatt_characteristic_delete(seos_svc->svc_handle, &seos_svc->chars[i]);
+    }
+    ble_gatt_service_delete(seos_svc->svc_handle);
+    furi_mutex_free(seos_svc->buff_size_mtx);
+    free(seos_svc);
+}
+
+bool ble_svc_seos_update_tx(BleServiceSeos* seos_svc, uint8_t* data, uint16_t data_len) {
+    if(data_len > BLE_SVC_SEOS_DATA_LEN_MAX) {
+        return false;
+    }
+
+    SeosSvcDataWrapper report_data = {.data_ptr = data, .data_len = data_len};
+
+    ble_gatt_characteristic_update(
+        seos_svc->svc_handle, &seos_svc->chars[SeosSvcGattCharacteristicRxTx], &report_data);
+
+    return true;
+}

+ 56 - 0
seos/seos_service.h

@@ -0,0 +1,56 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "seos_common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* 
+ * Seos service. Implements RPC over BLE, with flow control.
+ */
+
+#define BLE_SVC_SEOS_DATA_LEN_MAX       (486)
+#define BLE_SVC_SEOS_CHAR_VALUE_LEN_MAX (243)
+
+typedef enum {
+    SeosServiceEventTypeDataReceived,
+    SeosServiceEventTypeDataSent,
+    SeosServiceEventTypesBleResetRequest,
+} SeosServiceEventType;
+
+typedef struct {
+    uint8_t* buffer;
+    uint16_t size;
+} SeosServiceData;
+
+typedef struct {
+    SeosServiceEventType event;
+    SeosServiceData data;
+} SeosServiceEvent;
+
+typedef uint16_t (*SeosServiceEventCallback)(SeosServiceEvent event, void* context);
+
+typedef struct BleServiceSeos BleServiceSeos;
+
+BleServiceSeos* ble_svc_seos_start(FlowMode mode);
+
+void ble_svc_seos_stop(BleServiceSeos* service);
+
+void ble_svc_seos_set_callbacks(
+    BleServiceSeos* service,
+    uint16_t buff_size,
+    SeosServiceEventCallback callback,
+    void* context);
+
+void ble_svc_seos_set_rpc_active(BleServiceSeos* service, bool active);
+
+void ble_svc_seos_notify_buffer_is_empty(BleServiceSeos* service);
+
+bool ble_svc_seos_update_tx(BleServiceSeos* service, uint8_t* data, uint16_t data_len);
+
+#ifdef __cplusplus
+}
+#endif

+ 17 - 0
seos/seos_service_uuid.inc

@@ -0,0 +1,17 @@
+#include <ble/core/auto/ble_types.h>
+
+#define BLE_SVC_SEOS_READER_UUID      \
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00 }
+
+#define BLE_SVC_SEOS_READER_CHAR_UUID      \
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0xaa, 0x00, 0x00}
+
+static const Service_UUID_t reader_service_uuid = { .Service_UUID_128 = BLE_SVC_SEOS_READER_UUID };
+
+#define BLE_SVC_SEOS_CRED_UUID      \
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x01, 0x98, 0x00, 0x00 }
+
+#define BLE_SVC_SEOS_CRED_CHAR_UUID      \
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x01, 0xaa, 0x00, 0x00}
+
+static const Service_UUID_t cred_service_uuid = { .Service_UUID_128 = BLE_SVC_SEOS_CRED_UUID };

+ 234 - 0
seos/uart.c

@@ -0,0 +1,234 @@
+#include "seos_i.h"
+#include "uart.h"
+
+#define TAG "SeosUART"
+
+#define SEOS_UART_BAUD 460800
+
+static void seos_uart_on_irq_rx_dma_cb(
+    FuriHalSerialHandle* handle,
+    FuriHalSerialRxEvent ev,
+    size_t size,
+    void* context) {
+    SeosUart* seos_uart = (SeosUart*)context;
+    if(ev & (FuriHalSerialRxEventData | FuriHalSerialRxEventIdle)) {
+        uint8_t data[FURI_HAL_SERIAL_DMA_BUFFER_SIZE] = {0};
+        while(size) {
+            size_t ret = furi_hal_serial_dma_rx(
+                handle,
+                data,
+                (size > FURI_HAL_SERIAL_DMA_BUFFER_SIZE) ? FURI_HAL_SERIAL_DMA_BUFFER_SIZE : size);
+            furi_stream_buffer_send(seos_uart->rx_stream, data, ret, 0);
+            size -= ret;
+        };
+        furi_thread_flags_set(furi_thread_get_id(seos_uart->thread), WorkerEvtRxDone);
+    }
+}
+
+void seos_uart_disable(SeosUart* seos_uart) {
+    furi_assert(seos_uart);
+    furi_thread_flags_set(furi_thread_get_id(seos_uart->thread), WorkerEvtStop);
+    furi_thread_join(seos_uart->thread);
+    furi_thread_free(seos_uart->thread);
+    free(seos_uart);
+}
+
+void seos_uart_serial_init(SeosUart* seos_uart, uint8_t uart_ch) {
+    furi_assert(!seos_uart->serial_handle);
+    SeosUartConfig cfg = seos_uart->cfg;
+
+    seos_uart->serial_handle = furi_hal_serial_control_acquire(uart_ch);
+    furi_assert(seos_uart->serial_handle);
+
+    furi_hal_serial_init(seos_uart->serial_handle, cfg.baudrate);
+    furi_hal_serial_dma_rx_start(
+        seos_uart->serial_handle, seos_uart_on_irq_rx_dma_cb, seos_uart, false);
+}
+
+void seos_uart_serial_deinit(SeosUart* seos_uart) {
+    furi_assert(seos_uart->serial_handle);
+    furi_hal_serial_deinit(seos_uart->serial_handle);
+    furi_hal_serial_control_release(seos_uart->serial_handle);
+    seos_uart->serial_handle = NULL;
+}
+
+void seos_uart_set_baudrate(SeosUart* seos_uart, uint32_t baudrate) {
+    if(baudrate != 0) {
+        furi_hal_serial_set_br(seos_uart->serial_handle, baudrate);
+    } else {
+        FURI_LOG_I(TAG, "No baudrate specified");
+    }
+}
+
+size_t seos_uart_process_buffer(SeosUart* seos_uart, uint8_t* cmd, size_t cmd_len) {
+    if(cmd_len < 2) {
+        return cmd_len;
+    }
+
+    size_t consumed = 0;
+    do {
+        if(seos_uart->receive_callback) {
+            consumed =
+                seos_uart->receive_callback(seos_uart->receive_callback_context, cmd, cmd_len);
+        }
+
+        if(consumed > 0) {
+            memset(cmd, 0, consumed);
+            cmd_len -= consumed;
+            if(cmd_len > 0) {
+                memmove(cmd, cmd + consumed, cmd_len);
+            }
+
+            /*
+            memset(display, 0, SEOS_UART_RX_BUF_SIZE);
+            for(uint8_t i = 0; i < cmd_len; i++) {
+                snprintf(display + (i * 2), sizeof(display), "%02x", cmd[i]);
+            }
+            FURI_LOG_I(TAG, "cmd is now %d bytes: %s", cmd_len, display);
+            */
+        }
+    } while(consumed > 0 && cmd_len > 0);
+    return cmd_len;
+}
+
+int32_t seos_uart_worker(void* context) {
+    SeosUart* seos_uart = (SeosUart*)context;
+    furi_thread_set_current_priority(FuriThreadPriorityHighest);
+
+    memcpy(&seos_uart->cfg, &seos_uart->cfg_new, sizeof(SeosUartConfig));
+
+    seos_uart->rx_stream = furi_stream_buffer_alloc(SEOS_UART_RX_BUF_SIZE, 1);
+
+    seos_uart->tx_sem = furi_semaphore_alloc(1, 1);
+
+    seos_uart->tx_thread =
+        furi_thread_alloc_ex("SeosUartTxWorker", 1.5 * 1024, seos_uart_tx_thread, seos_uart);
+
+    seos_uart_serial_init(seos_uart, seos_uart->cfg.uart_ch);
+    seos_uart_set_baudrate(seos_uart, seos_uart->cfg.baudrate);
+
+    furi_thread_flags_set(furi_thread_get_id(seos_uart->tx_thread), WorkerEvtDevRx);
+
+    furi_thread_start(seos_uart->tx_thread);
+
+    uint8_t cmd[SEOS_UART_RX_BUF_SIZE];
+    size_t cmd_len = 0;
+
+    while(1) {
+        uint32_t events =
+            furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever);
+        furi_check(!(events & FuriFlagError));
+        if(events & WorkerEvtStop) {
+            memset(cmd, 0, cmd_len);
+            cmd_len = 0;
+            break;
+        }
+        if(events & (WorkerEvtRxDone | WorkerEvtDevTxComplete)) {
+            size_t len = furi_stream_buffer_receive(
+                seos_uart->rx_stream, seos_uart->rx_buf, SEOS_UART_RX_BUF_SIZE, 0);
+            if(len > 0) {
+                furi_delay_ms(5); //WTF
+
+                /*
+                char display[SEOS_UART_RX_BUF_SIZE * 2 + 1] = {0};
+                for(uint8_t i = 0; i < len; i++) {
+                    snprintf(display + (i * 2), sizeof(display), "%02x", seos_uart->rx_buf[i]);
+                }
+                FURI_LOG_D(TAG, "RECV %d bytes: %s", len, display);
+                */
+
+                if(cmd_len + len > SEOS_UART_RX_BUF_SIZE) {
+                    FURI_LOG_I(TAG, "OVERFLOW: %d + %d", cmd_len, len);
+                    memset(cmd, 0, cmd_len);
+                    cmd_len = 0;
+                }
+
+                memcpy(cmd + cmd_len, seos_uart->rx_buf, len);
+                cmd_len += len;
+                cmd_len = seos_uart_process_buffer(seos_uart, cmd, cmd_len);
+            }
+        }
+    }
+    seos_uart_serial_deinit(seos_uart);
+
+    furi_thread_flags_set(furi_thread_get_id(seos_uart->tx_thread), WorkerEvtTxStop);
+    furi_thread_join(seos_uart->tx_thread);
+    furi_thread_free(seos_uart->tx_thread);
+
+    furi_stream_buffer_free(seos_uart->rx_stream);
+    furi_semaphore_free(seos_uart->tx_sem);
+    return 0;
+}
+
+SeosUart* seos_uart_enable(SeosUartConfig* cfg) {
+    SeosUart* seos_uart = malloc(sizeof(SeosUart));
+
+    memcpy(&(seos_uart->cfg_new), cfg, sizeof(SeosUartConfig));
+
+    seos_uart->thread =
+        furi_thread_alloc_ex("SeosUartWorker", 5 * 1024, seos_uart_worker, seos_uart);
+
+    furi_thread_start(seos_uart->thread);
+    return seos_uart;
+}
+
+int32_t seos_uart_tx_thread(void* context) {
+    SeosUart* seos_uart = (SeosUart*)context;
+
+    furi_thread_set_current_priority(FuriThreadPriorityHighest);
+    while(1) {
+        uint32_t events =
+            furi_thread_flags_wait(WORKER_ALL_TX_EVENTS, FuriFlagWaitAny, FuriWaitForever);
+        furi_check(!(events & FuriFlagError));
+        if(events & WorkerEvtTxStop) break;
+        if(events & WorkerEvtDevRx) {
+            if(seos_uart->tx_len > 0) {
+                /*
+                char display[SEOS_UART_RX_BUF_SIZE * 2 + 1] = {0};
+                for(uint8_t i = 0; i < seos_uart->tx_len; i++) {
+                    snprintf(display + (i * 2), sizeof(display), "%02x", seos_uart->tx_buf[i]);
+                }
+                FURI_LOG_D(TAG, "SEND %d bytes: %s", seos_uart->tx_len, display);
+                */
+                furi_hal_serial_tx(seos_uart->serial_handle, seos_uart->tx_buf, seos_uart->tx_len);
+            }
+        }
+    }
+    return 0;
+}
+
+void seos_uart_get_config(SeosUart* seos_uart, SeosUartConfig* cfg) {
+    furi_assert(seos_uart);
+    furi_assert(cfg);
+    memcpy(cfg, &(seos_uart->cfg_new), sizeof(SeosUartConfig));
+}
+
+SeosUart* seos_uart_alloc() {
+    SeosUartConfig cfg = {.uart_ch = FuriHalSerialIdLpuart, .baudrate = SEOS_UART_BAUD};
+    SeosUart* seos_uart;
+
+    FURI_LOG_I(TAG, "Enable UART");
+    seos_uart = seos_uart_enable(&cfg);
+
+    seos_uart_get_config(seos_uart, &cfg);
+    return seos_uart;
+}
+
+void seos_uart_free(SeosUart* seos_uart) {
+    seos_uart_disable(seos_uart);
+}
+
+void seos_uart_send(SeosUart* seos_uart, uint8_t* buffer, size_t len) {
+    memset(seos_uart->tx_buf, 0, sizeof(seos_uart->tx_buf));
+    memcpy(seos_uart->tx_buf, buffer, len);
+    seos_uart->tx_len = len;
+    furi_thread_flags_set(furi_thread_get_id(seos_uart->tx_thread), WorkerEvtDevRx);
+}
+
+void seos_uart_set_receive_callback(
+    SeosUart* seos_uart,
+    SeosUartReceiveCallback callback,
+    void* context) {
+    seos_uart->receive_callback = callback;
+    seos_uart->receive_callback_context = context;
+}

+ 24 - 0
seos/uart.h

@@ -0,0 +1,24 @@
+#pragma once
+
+#include "uart_i.h"
+
+int32_t seos_uart_tx_thread(void* context);
+void seos_uart_on_irq_cb(uint8_t data, void* context);
+void seos_uart_serial_init(SeosUart* seos_uart, uint8_t uart_ch);
+void seos_uart_serial_deinit(SeosUart* seos_uart);
+void seos_uart_set_baudrate(SeosUart* seos_uart, uint32_t baudrate);
+int32_t seos_uart_worker(void* context);
+
+SeosUart* seos_uart_enable(SeosUartConfig* cfg);
+void seos_uart_disable(SeosUart* seos_uart);
+void seos_uart_set_config(SeosUart* seos_uart, SeosUartConfig* cfg);
+void seos_uart_get_config(SeosUart* seos_uart, SeosUartConfig* cfg);
+
+SeosUart* seos_uart_alloc();
+void seos_uart_free(SeosUart* seos_uart);
+
+void seos_uart_send(SeosUart* seos_uart, uint8_t* buffer, size_t len);
+void seos_uart_set_receive_callback(
+    SeosUart* seos_uart,
+    SeosUartReceiveCallback callback,
+    void* context);

+ 55 - 0
seos/uart_i.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include <stdlib.h> // malloc
+#include <stdint.h> // uint32_t
+#include <stdarg.h> // __VA_ARGS__
+#include <string.h>
+#include <stdio.h>
+
+#include <furi.h>
+#include <furi_hal.h>
+
+#define SEOS_UART_RX_BUF_SIZE (256)
+
+#define WORKER_ALL_RX_EVENTS (WorkerEvtStop | WorkerEvtRxDone | WorkerEvtDevTxComplete)
+#define WORKER_ALL_TX_EVENTS (WorkerEvtTxStop | WorkerEvtDevRx)
+
+typedef size_t (*SeosUartReceiveCallback)(void* context, uint8_t* buffer, size_t len);
+
+typedef enum {
+    WorkerEvtStop = (1 << 0),
+    WorkerEvtRxDone = (1 << 1),
+
+    WorkerEvtTxStop = (1 << 2),
+    WorkerEvtDevRx = (1 << 3),
+    WorkerEvtDevTxComplete = (1 << 4),
+} WorkerEvtFlags;
+
+typedef struct {
+    uint8_t uart_ch;
+    uint8_t flow_pins;
+    uint8_t baudrate_mode;
+    uint32_t baudrate;
+} SeosUartConfig;
+
+struct SeosUart {
+    SeosUartConfig cfg;
+    SeosUartConfig cfg_new;
+
+    FuriThread* thread;
+    FuriThread* tx_thread;
+
+    FuriStreamBuffer* rx_stream;
+    FuriHalSerialHandle* serial_handle;
+
+    FuriSemaphore* tx_sem;
+
+    uint8_t rx_buf[SEOS_UART_RX_BUF_SIZE];
+    uint8_t tx_buf[SEOS_UART_RX_BUF_SIZE];
+    size_t tx_len;
+
+    SeosUartReceiveCallback receive_callback;
+    void* receive_callback_context;
+};
+
+typedef struct SeosUart SeosUart;