From 6ac91f7dec8f922d9f633d9752ff05933f1fb133 Mon Sep 17 00:00:00 2001 From: Gilles Boccon-Gibod Date: Mon, 16 May 2022 19:42:31 -0700 Subject: [PATCH] initial import --- CONTRIBUTING.md | 29 + LICENSE | 202 + README.md | 31 + apps/README.md | 51 + apps/__init__.py | 0 apps/console.py | 601 +++ apps/controllers.py | 63 + apps/gatt_dump.py | 108 + apps/gg_bridge.py | 211 + apps/hci_bridge.py | 89 + apps/link_relay/__init__.py | 0 apps/link_relay/link_relay.py | 276 ++ apps/link_relay/logging.yml | 21 + apps/pair.py | 312 ++ apps/scan.py | 157 + apps/show.py | 120 + apps/unbond.py | 63 + bumble/__init__.py | 0 bumble/a2dp.py | 554 +++ bumble/att.py | 728 ++++ bumble/avdtp.py | 1921 +++++++++ bumble/bridge.py | 82 + bumble/company_ids.py | 2708 +++++++++++++ bumble/controller.py | 895 +++++ bumble/core.py | 852 ++++ bumble/crypto.py | 229 ++ bumble/device.py | 1256 ++++++ bumble/gap.py | 59 + bumble/gatt.py | 308 ++ bumble/gatt_client.py | 645 +++ bumble/gatt_server.py | 698 ++++ bumble/hci.py | 3471 +++++++++++++++++ bumble/helpers.py | 179 + bumble/hfp.py | 94 + bumble/host.py | 604 +++ bumble/keys.py | 273 ++ bumble/l2cap.py | 1079 +++++ bumble/link.py | 360 ++ bumble/rfcomm.py | 840 ++++ bumble/sdp.py | 1021 +++++ bumble/smp.py | 1514 +++++++ bumble/transport/__init__.py | 95 + bumble/transport/android_emulator.py | 107 + bumble/transport/common.py | 326 ++ .../emulated_bluetooth_packets_pb2.py | 52 + bumble/transport/emulated_bluetooth_pb2.py | 53 + .../transport/emulated_bluetooth_pb2_grpc.py | 207 + .../transport/emulated_bluetooth_vhci_pb2.py | 43 + .../emulated_bluetooth_vhci_pb2_grpc.py | 114 + bumble/transport/file.py | 60 + bumble/transport/hci_socket.py | 146 + bumble/transport/pty.py | 82 + bumble/transport/pyusb.py | 276 ++ bumble/transport/serial.py | 72 + bumble/transport/tcp_client.py | 52 + bumble/transport/tcp_server.py | 88 + bumble/transport/udp.py | 63 + bumble/transport/usb.py | 324 ++ bumble/transport/vhci.py | 59 + bumble/transport/ws_client.py | 49 + bumble/transport/ws_server.py | 81 + bumble/utils.py | 142 + docs/README.md | 22 + docs/images/logo.png | Bin 0 -> 24591 bytes docs/images/logo.svg | 41 + docs/images/logo.vectornator/Artboard0.json | 1 + docs/images/logo.vectornator/Document.json | 1 + docs/images/logo.vectornator/Manifest.json | 1 + docs/images/logo.vectornator/Thumbnail.png | Bin 0 -> 42419 bytes docs/images/logo.vectornator/UndoHistory.json | 1 + docs/images/logo_framed.png | Bin 0 -> 49038 bytes docs/images/logo_framed.svg | 42 + .../logo_framed.vectornator/Artboard0.json | 1 + .../logo_framed.vectornator/Document.json | 1 + .../logo_framed.vectornator/Manifest.json | 1 + .../logo_framed.vectornator/Thumbnail.png | Bin 0 -> 59364 bytes .../logo_framed.vectornator/UndoHistory.json | 1 + docs/mkdocs/mkdocs.yml | 88 + docs/mkdocs/requirements.txt | 6 + docs/mkdocs/src/api/examples.md | 2 + docs/mkdocs/src/api/guide.md | 2 + docs/mkdocs/src/api/reference.md | 19 + docs/mkdocs/src/apps_and_tools/console.md | 31 + docs/mkdocs/src/apps_and_tools/gg_bridge.md | 2 + docs/mkdocs/src/apps_and_tools/hci_bridge.md | 32 + docs/mkdocs/src/apps_and_tools/index.md | 12 + docs/mkdocs/src/apps_and_tools/link_relay.md | 34 + docs/mkdocs/src/apps_and_tools/show.md | 0 docs/mkdocs/src/components/controller.md | 2 + docs/mkdocs/src/components/gatt.md | 2 + docs/mkdocs/src/components/host.md | 2 + .../mkdocs/src/components/security_manager.md | 2 + docs/mkdocs/src/examples/index.md | 72 + docs/mkdocs/src/getting_started.md | 108 + docs/mkdocs/src/hardware/index.md | 19 + docs/mkdocs/src/images/bumble_layers.svg | 1 + docs/mkdocs/src/images/console_screenshot.png | Bin 0 -> 450055 bytes docs/mkdocs/src/images/favicon.ico | Bin 0 -> 15406 bytes docs/mkdocs/src/images/logo.png | Bin 0 -> 24591 bytes docs/mkdocs/src/images/logo_framed.png | Bin 0 -> 49038 bytes docs/mkdocs/src/index.md | 166 + docs/mkdocs/src/platforms/android.md | 79 + docs/mkdocs/src/platforms/index.md | 11 + docs/mkdocs/src/platforms/linux.md | 138 + docs/mkdocs/src/platforms/macos.md | 14 + docs/mkdocs/src/platforms/windows.md | 13 + docs/mkdocs/src/platforms/winusb_driver.png | Bin 0 -> 67769 bytes .../mkdocs/src/transports/android_emulator.md | 21 + docs/mkdocs/src/transports/file.md | 12 + docs/mkdocs/src/transports/hci_socket.md | 17 + docs/mkdocs/src/transports/index.md | 20 + docs/mkdocs/src/transports/pty.md | 12 + docs/mkdocs/src/transports/serial.md | 12 + docs/mkdocs/src/transports/tcp_client.md | 11 + docs/mkdocs/src/transports/tcp_server.md | 13 + docs/mkdocs/src/transports/udp.md | 11 + docs/mkdocs/src/transports/usb.md | 19 + docs/mkdocs/src/transports/vhci.md | 14 + docs/mkdocs/src/transports/ws_client.md | 11 + docs/mkdocs/src/transports/ws_server.md | 11 + docs/mkdocs/src/use_cases/index.md | 11 + docs/mkdocs/src/use_cases/use_case_1.md | 14 + docs/mkdocs/src/use_cases/use_case_2.md | 14 + docs/mkdocs/src/use_cases/use_case_3.md | 14 + docs/mkdocs/src/use_cases/use_case_4.md | 14 + docs/mkdocs/src/use_cases/use_case_5.md | 20 + docs/mkdocs/src/use_cases/use_case_6.md | 14 + docs/mkdocs/theme/partials/footer.html | 54 + environment.yml | 9 + examples/README.md | 84 + examples/a2dp_sink1.json | 5 + examples/async_runner.py | 85 + examples/battery_service.py | 84 + examples/classic1.json | 5 + examples/classic2.json | 4 + examples/device1.json | 7 + examples/device2.json | 8 + examples/device3.json | 4 + examples/get_peer_device_info.py | 96 + examples/hfp_gateway.json | 4 + examples/hfp_handsfree.html | 79 + examples/hfp_handsfree.json | 4 + examples/keyboard.html | 61 + examples/keyboard.json | 5 + examples/keyboard.py | 359 ++ examples/run_a2dp_info.py | 188 + examples/run_a2dp_sink.py | 163 + examples/run_a2dp_source.py | 175 + examples/run_advertiser.py | 48 + examples/run_classic_connect.py | 81 + examples/run_classic_discoverable.py | 104 + examples/run_classic_discovery.py | 64 + examples/run_connect_and_encrypt.py | 58 + examples/run_controller.py | 88 + examples/run_controller_with_scanner.py | 74 + examples/run_gatt_client.py | 98 + examples/run_gatt_client_and_server.py | 124 + examples/run_gatt_server.py | 147 + examples/run_hfp_gateway.py | 209 + examples/run_hfp_handsfree.py | 165 + examples/run_notifier.py | 120 + examples/run_rfcomm_client.py | 201 + examples/run_rfcomm_server.py | 118 + examples/run_scanner.py | 69 + noxfile.py | 21 + pyproject.toml | 5 + setup.cfg | 68 + setup.py | 16 + tasks.py | 57 + tests/a2dp_test.py | 250 ++ tests/avdtp_test.py | 65 + tests/core_test.py | 44 + tests/gatt_test.py | 341 ++ tests/hci_data_001.bin | Bin 0 -> 1944 bytes tests/hci_test.py | 406 ++ tests/import_test.py | 67 + tests/pytest.ini | 2 + tests/rfcomm_test.py | 48 + tests/sdp_test.py | 148 + tests/self_test.py | 321 ++ tests/smp_test.py | 198 + tests/transport_test.py | 74 + utils/generate_company_id_list.py | 38 + web/index.html | 131 + web/scanner.py | 63 + 185 files changed, 32064 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 apps/README.md create mode 100644 apps/__init__.py create mode 100644 apps/console.py create mode 100644 apps/controllers.py create mode 100644 apps/gatt_dump.py create mode 100644 apps/gg_bridge.py create mode 100644 apps/hci_bridge.py create mode 100644 apps/link_relay/__init__.py create mode 100644 apps/link_relay/link_relay.py create mode 100644 apps/link_relay/logging.yml create mode 100644 apps/pair.py create mode 100644 apps/scan.py create mode 100644 apps/show.py create mode 100644 apps/unbond.py create mode 100644 bumble/__init__.py create mode 100644 bumble/a2dp.py create mode 100644 bumble/att.py create mode 100644 bumble/avdtp.py create mode 100644 bumble/bridge.py create mode 100644 bumble/company_ids.py create mode 100644 bumble/controller.py create mode 100644 bumble/core.py create mode 100644 bumble/crypto.py create mode 100644 bumble/device.py create mode 100644 bumble/gap.py create mode 100644 bumble/gatt.py create mode 100644 bumble/gatt_client.py create mode 100644 bumble/gatt_server.py create mode 100644 bumble/hci.py create mode 100644 bumble/helpers.py create mode 100644 bumble/hfp.py create mode 100644 bumble/host.py create mode 100644 bumble/keys.py create mode 100644 bumble/l2cap.py create mode 100644 bumble/link.py create mode 100644 bumble/rfcomm.py create mode 100644 bumble/sdp.py create mode 100644 bumble/smp.py create mode 100644 bumble/transport/__init__.py create mode 100644 bumble/transport/android_emulator.py create mode 100644 bumble/transport/common.py create mode 100644 bumble/transport/emulated_bluetooth_packets_pb2.py create mode 100644 bumble/transport/emulated_bluetooth_pb2.py create mode 100644 bumble/transport/emulated_bluetooth_pb2_grpc.py create mode 100644 bumble/transport/emulated_bluetooth_vhci_pb2.py create mode 100644 bumble/transport/emulated_bluetooth_vhci_pb2_grpc.py create mode 100644 bumble/transport/file.py create mode 100644 bumble/transport/hci_socket.py create mode 100644 bumble/transport/pty.py create mode 100644 bumble/transport/pyusb.py create mode 100644 bumble/transport/serial.py create mode 100644 bumble/transport/tcp_client.py create mode 100644 bumble/transport/tcp_server.py create mode 100644 bumble/transport/udp.py create mode 100644 bumble/transport/usb.py create mode 100644 bumble/transport/vhci.py create mode 100644 bumble/transport/ws_client.py create mode 100644 bumble/transport/ws_server.py create mode 100644 bumble/utils.py create mode 100644 docs/README.md create mode 100644 docs/images/logo.png create mode 100644 docs/images/logo.svg create mode 100644 docs/images/logo.vectornator/Artboard0.json create mode 100644 docs/images/logo.vectornator/Document.json create mode 100644 docs/images/logo.vectornator/Manifest.json create mode 100644 docs/images/logo.vectornator/Thumbnail.png create mode 100644 docs/images/logo.vectornator/UndoHistory.json create mode 100644 docs/images/logo_framed.png create mode 100644 docs/images/logo_framed.svg create mode 100644 docs/images/logo_framed.vectornator/Artboard0.json create mode 100644 docs/images/logo_framed.vectornator/Document.json create mode 100644 docs/images/logo_framed.vectornator/Manifest.json create mode 100644 docs/images/logo_framed.vectornator/Thumbnail.png create mode 100644 docs/images/logo_framed.vectornator/UndoHistory.json create mode 100644 docs/mkdocs/mkdocs.yml create mode 100644 docs/mkdocs/requirements.txt create mode 100644 docs/mkdocs/src/api/examples.md create mode 100644 docs/mkdocs/src/api/guide.md create mode 100644 docs/mkdocs/src/api/reference.md create mode 100644 docs/mkdocs/src/apps_and_tools/console.md create mode 100644 docs/mkdocs/src/apps_and_tools/gg_bridge.md create mode 100644 docs/mkdocs/src/apps_and_tools/hci_bridge.md create mode 100644 docs/mkdocs/src/apps_and_tools/index.md create mode 100644 docs/mkdocs/src/apps_and_tools/link_relay.md create mode 100644 docs/mkdocs/src/apps_and_tools/show.md create mode 100644 docs/mkdocs/src/components/controller.md create mode 100644 docs/mkdocs/src/components/gatt.md create mode 100644 docs/mkdocs/src/components/host.md create mode 100644 docs/mkdocs/src/components/security_manager.md create mode 100644 docs/mkdocs/src/examples/index.md create mode 100644 docs/mkdocs/src/getting_started.md create mode 100644 docs/mkdocs/src/hardware/index.md create mode 100644 docs/mkdocs/src/images/bumble_layers.svg create mode 100644 docs/mkdocs/src/images/console_screenshot.png create mode 100644 docs/mkdocs/src/images/favicon.ico create mode 100644 docs/mkdocs/src/images/logo.png create mode 100644 docs/mkdocs/src/images/logo_framed.png create mode 100644 docs/mkdocs/src/index.md create mode 100644 docs/mkdocs/src/platforms/android.md create mode 100644 docs/mkdocs/src/platforms/index.md create mode 100644 docs/mkdocs/src/platforms/linux.md create mode 100644 docs/mkdocs/src/platforms/macos.md create mode 100644 docs/mkdocs/src/platforms/windows.md create mode 100755 docs/mkdocs/src/platforms/winusb_driver.png create mode 100644 docs/mkdocs/src/transports/android_emulator.md create mode 100644 docs/mkdocs/src/transports/file.md create mode 100644 docs/mkdocs/src/transports/hci_socket.md create mode 100644 docs/mkdocs/src/transports/index.md create mode 100644 docs/mkdocs/src/transports/pty.md create mode 100644 docs/mkdocs/src/transports/serial.md create mode 100644 docs/mkdocs/src/transports/tcp_client.md create mode 100644 docs/mkdocs/src/transports/tcp_server.md create mode 100644 docs/mkdocs/src/transports/udp.md create mode 100644 docs/mkdocs/src/transports/usb.md create mode 100644 docs/mkdocs/src/transports/vhci.md create mode 100644 docs/mkdocs/src/transports/ws_client.md create mode 100644 docs/mkdocs/src/transports/ws_server.md create mode 100644 docs/mkdocs/src/use_cases/index.md create mode 100644 docs/mkdocs/src/use_cases/use_case_1.md create mode 100644 docs/mkdocs/src/use_cases/use_case_2.md create mode 100644 docs/mkdocs/src/use_cases/use_case_3.md create mode 100644 docs/mkdocs/src/use_cases/use_case_4.md create mode 100644 docs/mkdocs/src/use_cases/use_case_5.md create mode 100644 docs/mkdocs/src/use_cases/use_case_6.md create mode 100644 docs/mkdocs/theme/partials/footer.html create mode 100644 environment.yml create mode 100644 examples/README.md create mode 100644 examples/a2dp_sink1.json create mode 100644 examples/async_runner.py create mode 100644 examples/battery_service.py create mode 100644 examples/classic1.json create mode 100644 examples/classic2.json create mode 100644 examples/device1.json create mode 100644 examples/device2.json create mode 100644 examples/device3.json create mode 100644 examples/get_peer_device_info.py create mode 100644 examples/hfp_gateway.json create mode 100644 examples/hfp_handsfree.html create mode 100644 examples/hfp_handsfree.json create mode 100644 examples/keyboard.html create mode 100644 examples/keyboard.json create mode 100644 examples/keyboard.py create mode 100644 examples/run_a2dp_info.py create mode 100644 examples/run_a2dp_sink.py create mode 100644 examples/run_a2dp_source.py create mode 100644 examples/run_advertiser.py create mode 100644 examples/run_classic_connect.py create mode 100644 examples/run_classic_discoverable.py create mode 100644 examples/run_classic_discovery.py create mode 100644 examples/run_connect_and_encrypt.py create mode 100644 examples/run_controller.py create mode 100644 examples/run_controller_with_scanner.py create mode 100644 examples/run_gatt_client.py create mode 100644 examples/run_gatt_client_and_server.py create mode 100644 examples/run_gatt_server.py create mode 100644 examples/run_hfp_gateway.py create mode 100644 examples/run_hfp_handsfree.py create mode 100644 examples/run_notifier.py create mode 100644 examples/run_rfcomm_client.py create mode 100644 examples/run_rfcomm_server.py create mode 100644 examples/run_scanner.py create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tasks.py create mode 100644 tests/a2dp_test.py create mode 100644 tests/avdtp_test.py create mode 100644 tests/core_test.py create mode 100644 tests/gatt_test.py create mode 100644 tests/hci_data_001.bin create mode 100644 tests/hci_test.py create mode 100644 tests/import_test.py create mode 100644 tests/pytest.ini create mode 100644 tests/rfcomm_test.py create mode 100644 tests/sdp_test.py create mode 100644 tests/self_test.py create mode 100644 tests/smp_test.py create mode 100644 tests/transport_test.py create mode 100644 utils/generate_company_id_list.py create mode 100644 web/index.html create mode 100644 web/scanner.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e5c3b28 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement (CLA). You (or your employer) retain the copyright to your +contribution; this simply gives us permission to use and redistribute your +contributions as part of the project. Head over to + to see your current agreements on file or +to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code Reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows +[Google's Open Source Community Guidelines](https://opensource.google/conduct/). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9133190 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ + + _ _ _ + | | | | | | + | |__ _ _ ____ | |__ | | _____ + | _ \| | | | \| _ \| || ___ | + | |_) ) |_| | | | | |_) ) || ____| + |____/|____/|_|_|_|____/ \_)_____) + +Bluetooth Stack for Apps, Emulation, Test and Experimentation +============================================================= + +drawing + +## Documentation + +See the documentation under `docs/mkdocs/src`, or build the static HTML site from the markdown text with: +``` +mkdocs build -f docs/mkdocs/mkdocs.yml +``` + +## License + +Licensed under the [Apache 2.0](LICENSE) License. + +## Disclaimer + +This is not an official Google product. + +This library is in alpha and will be going through a lot of breaking changes. While releases will be stable enough for prototyping, experimentation and research, we do not recommend using it in any production environment yet. +Expect bugs and sharp edges. +Please help by trying it out, reporting bugs, and letting us know what you think! diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 0000000..0abcf09 --- /dev/null +++ b/apps/README.md @@ -0,0 +1,51 @@ +Bumble Apps +=========== + +NOTE: +To run python scripts from this directory when the Bumble package isn't installed in your environment, +put .. in your PYTHONPATH: `export PYTHONPATH=..` + + +Apps +---- + +## `show.py` +Parse a file with HCI packets and print the details of each packet in a human readable form + +## `link_relay.py` +Simple WebSocket relay for virtual RemoteLink instances to communicate with each other through. + +## `hci_bridge.py` +This app acts as a simple bridge between two HCI transports, with a host on one side and +a controller on the other. All the HCI packets bridged between the two are printed on the console +for logging. This bridge also has the ability to short-circuit some HCI packets (respond to them +with a fixed response instead of bridging them to the other side), which may be useful when used with +a host that send custom HCI commands that the controller may not understand. + +### Usage +``` +python hci_bridge.py [command-short-circuit-list] +``` + +### Examples + +#### UDP to HCI UART +``` +python hci_bridge.py udp:0.0.0.0:9000,127.0.0.1:9001 serial:/dev/tty.usbmodem0006839912171,1000000 0x3f:0x0070,0x3f:0x0074,0x3f:0x0077,0x3f:0x0078 +``` + +#### PTY to Link Relay +``` +python hci_bridge.py serial:emulated_uart_pty,1000000 link-relay:ws://127.0.0.1:10723/test +``` + +In this example, an emulator that exposes a PTY as an interface to its HCI UART is running as +a Bluetooth host, and we are connecting it to a virtual controller attached to a link relay +(through which the communication with other virtual controllers will be mediated). + +NOTE: this assumes you're running a Link Relay on port `10723`. + +## `console.py` +A simple text-based-ui interactive Bluetooth device with GATT client capabilities. + + diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/console.py b/apps/console.py new file mode 100644 index 0000000..3ac4957 --- /dev/null +++ b/apps/console.py @@ -0,0 +1,601 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Bumble Tool +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +from bumble.hci import HCI_Constant +import os +import os.path +import logging +import click +from collections import OrderedDict +import colors + +from bumble.core import UUID, AdvertisingData +from bumble.device import Device, Connection, Peer +from bumble.utils import AsyncRunner +from bumble.transport import open_transport_or_link + +from prompt_toolkit import Application +from prompt_toolkit.history import FileHistory +from prompt_toolkit.completion import Completer, Completion, NestedCompleter +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.formatted_text import ANSI +from prompt_toolkit.styles import Style +from prompt_toolkit.filters import Condition +from prompt_toolkit.widgets import TextArea, Frame +from prompt_toolkit.widgets.toolbars import FormattedTextToolbar +from prompt_toolkit.layout import ( + Layout, + HSplit, + Window, + CompletionsMenu, + Float, + FormattedTextControl, + FloatContainer, + ConditionalContainer +) + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +BUMBLE_USER_DIR = os.path.expanduser('~/.bumble') +DEFAULT_PROMPT_HEIGHT = 20 +DEFAULT_RSSI_BAR_WIDTH = 20 +DISPLAY_MIN_RSSI = -100 +DISPLAY_MAX_RSSI = -30 + +# ----------------------------------------------------------------------------- +# Globals +# ----------------------------------------------------------------------------- +App = None + + +# ----------------------------------------------------------------------------- +# Console App +# ----------------------------------------------------------------------------- +class ConsoleApp: + def __init__(self): + self.known_addresses = set() + self.known_attributes = [] + self.device = None + self.connected_peer = None + self.top_tab = 'scan' + + style = Style.from_dict({ + 'output-field': 'bg:#000044 #ffffff', + 'input-field': 'bg:#000000 #ffffff', + 'line': '#004400', + 'error': 'fg:ansired' + }) + + class LiveCompleter(Completer): + def __init__(self, words): + self.words = words + + def get_completions(self, document, complete_event): + prefix = document.text_before_cursor.upper() + for word in [x for x in self.words if x.upper().startswith(prefix)]: + yield Completion(word, start_position=-len(prefix)) + + def make_completer(): + return NestedCompleter.from_nested_dict({ + 'scan': { + 'on': None, + 'off': None + }, + 'advertise': { + 'on': None, + 'off': None + }, + 'show': { + 'scan': None, + 'services': None, + 'attributes': None, + 'log': None + }, + 'connect': LiveCompleter(self.known_addresses), + 'update-parameters': None, + 'encrypt': None, + 'disconnect': None, + 'discover': { + 'services': None, + 'attributes': None + }, + 'read': LiveCompleter(self.known_attributes), + 'write': LiveCompleter(self.known_attributes), + 'quit': None, + 'exit': None + }) + + self.input_field = TextArea( + height=1, + prompt="> ", + multiline=False, + wrap_lines=False, + completer=make_completer(), + history=FileHistory(os.path.join(BUMBLE_USER_DIR, 'history')) + ) + + self.input_field.accept_handler = self.accept_input + + self.output_height = 7 + self.output_lines = [] + self.output = FormattedTextControl() + self.scan_results_text = FormattedTextControl() + self.services_text = FormattedTextControl() + self.attributes_text = FormattedTextControl() + self.log_text = FormattedTextControl() + self.log_height = 20 + self.log_lines = [] + + container = HSplit([ + ConditionalContainer( + Frame(Window(self.scan_results_text), title='Scan Results'), + filter=Condition(lambda: self.top_tab == 'scan') + ), + ConditionalContainer( + Frame(Window(self.services_text), title='Services'), + filter=Condition(lambda: self.top_tab == 'services') + ), + ConditionalContainer( + Frame(Window(self.attributes_text), title='Attributes'), + filter=Condition(lambda: self.top_tab == 'attributes') + ), + ConditionalContainer( + Frame(Window(self.log_text), title='Log'), + filter=Condition(lambda: self.top_tab == 'log') + ), + Frame(Window(self.output), height=self.output_height), + # HorizontalLine(), + FormattedTextToolbar(text=self.get_status_bar_text, style='reverse'), + self.input_field + ]) + + container = FloatContainer( + container, + floats=[ + Float( + xcursor=True, + ycursor=True, + content=CompletionsMenu(max_height=16, scroll_offset=1), + ), + ], + ) + + layout = Layout(container, focused_element=self.input_field) + + kb = KeyBindings() + @kb.add("c-c") + @kb.add("c-q") + def _(event): + event.app.exit() + + self.ui = Application( + layout=layout, + style=style, + key_bindings=kb, + full_screen=True + ) + + async def run_async(self, device_config, transport): + async with await open_transport_or_link(transport) as (hci_source, hci_sink): + if device_config: + self.device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink) + else: + self.device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink) + self.device.listener = DeviceListener(self) + await self.device.power_on() + + # Run the UI + await self.ui.run_async() + + def add_known_address(self, address): + self.known_addresses.add(address) + + def accept_input(self, buff): + if len(self.input_field.text) == 0: + return + self.append_to_output([('', '* '), ('ansicyan', self.input_field.text)], False) + self.ui.create_background_task(self.command(self.input_field.text)) + + def get_status_bar_text(self): + scanning = "ON" if self.device and self.device.is_scanning else "OFF" + + connection_state = 'NONE' + encryption_state = '' + + if self.device: + if self.device.is_connecting: + connection_state = 'CONNECTING' + elif self.connected_peer: + connection = self.connected_peer.connection + connection_parameters = f'{connection.parameters.connection_interval}/{connection.parameters.connection_latency}/{connection.parameters.supervision_timeout}' + connection_state = f'{connection.peer_address} {connection_parameters} {connection.data_length}' + encryption_state = 'ENCRYPTED' if connection.is_encrypted else 'NOT ENCRYPTED' + + return [ + ('ansigreen', f' SCAN: {scanning} '), + ('', ' '), + ('ansiblue', f' CONNECTION: {connection_state} '), + ('', ' '), + ('ansimagenta', f' {encryption_state} ') + ] + + def show_error(self, title, details = None): + appended = [('class:error', title)] + if details: + appended.append(('', f' {details}')) + self.append_to_output(appended) + + def show_scan_results(self, scan_results): + max_lines = 40 # TEMP + lines = [] + keys = list(scan_results.keys())[:max_lines] + for key in keys: + lines.append(scan_results[key].to_display_string()) + self.scan_results_text.text = ANSI('\n'.join(lines)) + self.ui.invalidate() + + def show_services(self, services): + lines = [] + del self.known_attributes[:] + for service in services: + lines.append(('ansicyan', str(service) + '\n')) + + for characteristic in service.characteristics: + lines.append(('ansimagenta', ' ' + str(characteristic) + '\n')) + self.known_attributes.append(f'{service.uuid.to_hex_str()}.{characteristic.uuid.to_hex_str()}') + self.known_attributes.append(f'*.{characteristic.uuid.to_hex_str()}') + self.known_attributes.append(f'#{characteristic.handle:X}') + for descriptor in characteristic.descriptors: + lines.append(('ansigreen', ' ' + str(descriptor) + '\n')) + + self.services_text.text = lines + self.ui.invalidate() + + async def show_attributes(self, attributes): + lines = [] + + for attribute in attributes: + lines.append(('ansicyan', f'{attribute}\n')) + + self.attributes_text.text = lines + self.ui.invalidate() + + def append_to_output(self, line, invalidate=True): + if type(line) is str: + line = [('', line)] + self.output_lines = self.output_lines[-(self.output_height - 3):] + self.output_lines.append(line) + formatted_text = [] + for line in self.output_lines: + formatted_text += line + formatted_text.append(('', '\n')) + self.output.text = formatted_text + if invalidate: + self.ui.invalidate() + + def append_to_log(self, lines, invalidate=True): + self.log_lines.extend(lines.split('\n')) + self.log_lines = self.log_lines[-(self.log_height - 3):] + self.log_text.text = ANSI('\n'.join(self.log_lines)) + if invalidate: + self.ui.invalidate() + + async def discover_services(self): + if not self.connected_peer: + self.show_error('not connected') + return + + # Discover all services, characteristics and descriptors + self.append_to_output('discovering services...') + await self.connected_peer.discover_services() + self.append_to_output(f'found {len(self.connected_peer.services)} services, discovering charateristics...') + await self.connected_peer.discover_characteristics() + self.append_to_output('found characteristics, discovering descriptors...') + for service in self.connected_peer.services: + for characteristic in service.characteristics: + await self.connected_peer.discover_descriptors(characteristic) + self.append_to_output('discovery completed') + + self.show_services(self.connected_peer.services) + + async def discover_attributes(self): + if not self.connected_peer: + self.show_error('not connected') + return + + # Discover all attributes + self.append_to_output('discovering attributes...') + attributes = await self.connected_peer.discover_attributes() + self.append_to_output(f'discovered {len(attributes)} attributes...') + + await self.show_attributes(attributes) + + async def command(self, command): + try: + (keyword, *params) = command.strip().split(' ', 1) + keyword = keyword.replace('-', '_').lower() + handler = getattr(self, f'do_{keyword}', None) + if handler: + await handler(params) + self.ui.invalidate() + else: + self.show_error('unknown command', keyword) + except Exception as error: + self.show_error(str(error)) + + async def do_scan(self, params): + if len(params) == 0: + # Toggle scanning + if self.device.is_scanning: + await self.device.stop_scanning() + else: + await self.device.start_scanning() + elif params[0] == 'on': + await self.device.start_scanning() + self.top_tab = 'scan' + elif params[0] == 'off': + await self.device.stop_scanning() + else: + self.show_error('unsupported arguments for scan command') + + async def do_connect(self, params): + if len(params) != 1: + self.show_error('invalid syntax', 'expected connect
') + return + + self.append_to_output('connecting...') + await self.device.connect(params[0]) + self.top_tab = 'services' + + async def do_disconnect(self, params): + if not self.connected_peer: + self.show_error('not connected') + return + + await self.connected_peer.connection.disconnect() + + async def do_update_parameters(self, params): + if len(params) != 1 or len(params[0].split('/')) != 3: + self.show_error('invalid syntax', 'expected update-parameters -//') + return + + if not self.connected_peer: + self.show_error('not connected') + return + + connection_intervals, connection_latency, supervision_timeout = params[0].split('/') + connection_interval_min, connection_interval_max = [int(x) for x in connection_intervals.split('-')] + connection_latency = int(connection_latency) + supervision_timeout = int(supervision_timeout) + await self.connected_peer.connection.update_parameters( + connection_interval_min, + connection_interval_max, + connection_latency, + supervision_timeout + ) + + async def do_encrypt(self, params): + if not self.connected_peer: + self.show_error('not connected') + return + + await self.connected_peer.connection.encrypt() + + async def do_advertise(self, params): + if len(params) == 0: + # Toggle advertising + if self.device.is_advertising: + await self.device.stop_advertising() + else: + await self.device.start_advertising() + elif params[0] == 'on': + await self.device.start_advertising() + elif params[0] == 'off': + await self.device.stop_advertising() + else: + self.show_error('unsupported arguments for advertise command') + + async def do_show(self, params): + if params: + if params[0] in {'scan', 'services', 'attributes', 'log'}: + self.top_tab = params[0] + self.ui.invalidate() + + async def do_discover(self, params): + if not params: + self.show_error('invalid syntax', 'expected discover services|attributes') + return + + discovery_type = params[0] + if discovery_type == 'services': + await self.discover_services() + elif discovery_type == 'attributes': + await self.discover_attributes() + + async def do_read(self, params): + if not self.connected_peer: + self.show_error('not connected') + return + + if len(params) != 1: + self.show_error('invalid syntax', 'expected read ') + return + + parts = params[0].split('.') + if len(parts) == 2: + service_uuid = UUID(parts[0]) if parts[0] != '*' else None + characteristic_uuid = UUID(parts[1]) + for service in self.connected_peer.services: + if service_uuid is None or service.uuid == service_uuid: + for characteristic in service.characteristics: + if characteristic.uuid == characteristic_uuid: + value = await self.connected_peer.read_value(characteristic) + self.append_to_output(f'VALUE: {value}') + return + self.show_error('no such characteristic') + elif len(parts) == 1: + if parts[0].startswith('#'): + attribute_handle = int(f'{parts[0][1:]}', 16) + value = await self.connected_peer.read_value(attribute_handle) + self.append_to_output(f'VALUE: {value}') + return + else: + self.show_error('no such characteristic') + + async def do_exit(self, params): + self.ui.exit() + + async def do_quit(self, params): + self.ui.exit() + + +# ----------------------------------------------------------------------------- +# Device and Connection Listener +# ----------------------------------------------------------------------------- +class DeviceListener(Device.Listener, Connection.Listener): + def __init__(self, app): + self.app = app + self.scan_results = OrderedDict() + + @AsyncRunner.run_in_task() + async def on_connection(self, connection): + self.app.connected_peer = Peer(connection) + self.app.append_to_output(f'connected to {self.app.connected_peer}') + connection.listener = self + + def on_disconnection(self, reason): + self.app.append_to_output(f'disconnected from {self.app.connected_peer}, reason: {HCI_Constant.error_name(reason)}') + self.app.connected_peer = None + + def on_connection_parameters_update(self): + self.app.append_to_output(f'connection parameters update: {self.app.connected_peer.connection.parameters}') + + def on_connection_phy_update(self): + self.app.append_to_output(f'connection phy update: {self.app.connected_peer.connection.phy}') + + def on_connection_att_mtu_update(self): + self.app.append_to_output(f'connection att mtu update: {self.app.connected_peer.connection.att_mtu}') + + def on_connection_encryption_change(self): + self.app.append_to_output(f'connection encryption change: {"encrypted" if self.app.connected_peer.connection.is_encrypted else "not encrypted"}') + + def on_connection_data_length_change(self): + self.app.append_to_output(f'connection data length change: {self.app.connected_peer.connection.data_length}') + + def on_advertisement(self, address, ad_data, rssi, connectable): + entry_key = f'{address}/{address.address_type}' + entry = self.scan_results.get(entry_key) + if entry: + entry.ad_data = ad_data + entry.rssi = rssi + entry.connectable = connectable + else: + self.app.add_known_address(str(address)) + self.scan_results[entry_key] = ScanResult(address, address.address_type, ad_data, rssi, connectable) + + self.app.show_scan_results(self.scan_results) + + +# ----------------------------------------------------------------------------- +# Scanning +# ----------------------------------------------------------------------------- +class ScanResult: + def __init__(self, address, address_type, ad_data, rssi, connectable): + self.address = address + self.address_type = address_type + self.ad_data = ad_data + self.rssi = rssi + self.connectable = connectable + + def to_display_string(self): + address_type_string = ('P', 'R', 'PI', 'RI')[self.address_type] + address_color = colors.yellow if self.connectable else colors.red + if address_type_string.startswith('P'): + type_color = colors.green + else: + type_color = colors.cyan + + name = self.ad_data.get(AdvertisingData.COMPLETE_LOCAL_NAME) + if name is None: + name = self.ad_data.get(AdvertisingData.SHORTENED_LOCAL_NAME) + if name: + # Convert to string + try: + name = name.decode() + except UnicodeDecodeError: + name = name.hex() + else: + name = '' + + # RSSI bar + blocks = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉'] + bar_width = (self.rssi - DISPLAY_MIN_RSSI) / (DISPLAY_MAX_RSSI - DISPLAY_MIN_RSSI) + bar_width = min(max(bar_width, 0), 1) + bar_ticks = int(bar_width * DEFAULT_RSSI_BAR_WIDTH * 8) + bar_blocks = ('█' * int(bar_ticks / 8)) + blocks[bar_ticks % 8] + bar_string = f'{self.rssi} {bar_blocks}' + bar_padding = ' ' * (DEFAULT_RSSI_BAR_WIDTH + 5 - len(bar_string)) + return f'{address_color(str(self.address))} [{type_color(address_type_string)}] {bar_string} {bar_padding} {name}' + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +class LogHandler(logging.Handler): + def __init__(self, app): + super().__init__() + self.app = app + + def emit(self, record): + message = self.format(record) + self.app.append_to_log(message) + + +# ----------------------------------------------------------------------------- +# Main +# ----------------------------------------------------------------------------- +@click.command() +@click.option('--device-config', help='Device configuration file') +@click.argument('transport') +def main(device_config, transport): + # Ensure that the BUMBLE_USER_DIR directory exists + if not os.path.isdir(BUMBLE_USER_DIR): + os.mkdir(BUMBLE_USER_DIR) + + # Create an instane of the app + app = ConsoleApp() + + # Setup logging + # logging.basicConfig(level = 'FATAL') + # logging.basicConfig(level = 'DEBUG') + root_logger = logging.getLogger() + root_logger.addHandler(LogHandler(app)) + root_logger.setLevel(logging.DEBUG) + + # Run until the user exits + asyncio.run(app.run_async(device_config, transport)) + + +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + main() diff --git a/apps/controllers.py b/apps/controllers.py new file mode 100644 index 0000000..8e5f70e --- /dev/null +++ b/apps/controllers.py @@ -0,0 +1,63 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import asyncio +import sys +import os + +from bumble.controller import Controller +from bumble.link import LocalLink +from bumble.transport import open_transport_or_link + + +# ----------------------------------------------------------------------------- +async def async_main(): + if len(sys.argv) != 3: + print('Usage: controllers.py [ ...]') + print('example: python controllers.py pty:ble1 pty:ble2') + return + + # Create a loccal link to attach the controllers to + link = LocalLink() + + # Create a transport and controller for all requested names + transports = [] + controllers = [] + for index, transport_name in enumerate(sys.argv[1:]): + transport = await open_transport_or_link(transport_name) + transports.append(transport) + controller = Controller(f'C{index}', host_source = transport.source, host_sink = transport.sink, link = link) + controllers.append(controller) + + # Wait until the user interrupts + await asyncio.get_running_loop().create_future() + + # Cleanup + for transport in transports: + transport.close() + + +# ----------------------------------------------------------------------------- +def main(): + logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) + asyncio.run(async_main()) + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/apps/gatt_dump.py b/apps/gatt_dump.py new file mode 100644 index 0000000..c055626 --- /dev/null +++ b/apps/gatt_dump.py @@ -0,0 +1,108 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import os +import logging +import click +from colors import color + +from bumble.core import ProtocolError, TimeoutError +from bumble.device import Device, Peer +from bumble.gatt import show_services +from bumble.transport import open_transport_or_link + + +# ----------------------------------------------------------------------------- +async def dump_gatt_db(peer, done): + # Discover all services + print(color('### Discovering Services and Characteristics', 'magenta')) + await peer.discover_services() + await peer.discover_characteristics() + for service in peer.services: + for characteristic in service.characteristics: + await peer.discover_descriptors(characteristic) + + print(color('=== Services ===', 'yellow')) + show_services(peer.services) + print() + + # Discover all attributes + print(color('=== All Attributes ===', 'yellow')) + attributes = await peer.discover_attributes() + for attribute in attributes: + print(attribute) + try: + value = await peer.read_value(attribute) + print(color(f'{value.hex()}', 'green')) + except ProtocolError as error: + print(color(error, 'red')) + except TimeoutError: + print(color('read timeout', 'red')) + + if done is not None: + done.set_result(None) + + +# ----------------------------------------------------------------------------- +async def async_main(device_config, encrypt, transport, address_or_name): + async with await open_transport_or_link(transport) as (hci_source, hci_sink): + + # Create a device + if device_config: + device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink) + else: + device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink) + await device.power_on() + + if address_or_name: + # Connect to the target peer + connection = await device.connect(address_or_name) + + # Encrypt the connection if required + if encrypt: + await connection.encrypt() + + await dump_gatt_db(Peer(connection), None) + else: + # Wait for a connection + done = asyncio.get_running_loop().create_future() + device.on('connection', lambda connection: asyncio.create_task(dump_gatt_db(Peer(connection), done))) + await device.start_advertising(auto_restart=True) + + print(color('### Waiting for connection...', 'blue')) + await done + + +# ----------------------------------------------------------------------------- +@click.command() +@click.option('--device-config', help='Device configuration', type=click.Path()) +@click.option('--encrypt', help='Encrypt the connection', is_flag=True, default=False) +@click.argument('transport') +@click.argument('address-or-name', required=False) +def main(device_config, encrypt, transport, address_or_name): + """ + Dump the GATT database on a remote device. If ADDRESS_OR_NAME is not specified, + wait for an incoming connection. + """ + logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) + asyncio.run(async_main(device_config, encrypt, transport, address_or_name)) + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/apps/gg_bridge.py b/apps/gg_bridge.py new file mode 100644 index 0000000..ac4ae5a --- /dev/null +++ b/apps/gg_bridge.py @@ -0,0 +1,211 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import os +import logging +import click +from colors import color + +from bumble.device import Device, Peer +from bumble.core import AdvertisingData +from bumble.gatt import Service, Characteristic +from bumble.utils import AsyncRunner +from bumble.transport import open_transport_or_link +from bumble.hci import HCI_Constant + + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +GG_GATTLINK_SERVICE_UUID = 'ABBAFF00-E56A-484C-B832-8B17CF6CBFE8' +GG_GATTLINK_RX_CHARACTERISTIC_UUID = 'ABBAFF01-E56A-484C-B832-8B17CF6CBFE8' +GG_GATTLINK_TX_CHARACTERISTIC_UUID = 'ABBAFF02-E56A-484C-B832-8B17CF6CBFE8' +GG_GATTLINK_L2CAP_CHANNEL_PSM_CHARACTERISTIC_UUID = 'ABBAFF03-E56A-484C-B832-8B17CF6CBFE8' + +GG_PREFERRED_MTU = 256 + + +# ----------------------------------------------------------------------------- +class GattlinkHubBridge(Device.Listener): + def __init__(self): + self.peer = None + self.rx_socket = None + self.tx_socket = None + self.rx_characteristic = None + self.tx_characteristic = None + + @AsyncRunner.run_in_task() + async def on_connection(self, connection): + print(f'=== Connected to {connection}') + self.peer = Peer(connection) + + # Request a larger MTU than the default + server_mtu = await self.peer.request_mtu(GG_PREFERRED_MTU) + print(f'### Server MTU = {server_mtu}') + + # Discover all services + print(color('=== Discovering services', 'yellow')) + await self.peer.discover_service(GG_GATTLINK_SERVICE_UUID) + print(color('=== Services discovered', 'yellow'), self.peer.services) + for service in self.peer.services: + print(service) + services = self.peer.get_services_by_uuid(GG_GATTLINK_SERVICE_UUID) + if not services: + print(color('!!! Gattlink service not found', 'red')) + return + + # Use the first Gattlink (there should only be one anyway) + gattlink_service = services[0] + + # Discover all the characteristics for the service + characteristics = await self.peer.discover_characteristics(service = gattlink_service) + print(color('=== Characteristics discovered', 'yellow')) + for characteristic in characteristics: + if characteristic.uuid == GG_GATTLINK_RX_CHARACTERISTIC_UUID: + self.rx_characteristic = characteristic + elif characteristic.uuid == GG_GATTLINK_TX_CHARACTERISTIC_UUID: + self.tx_characteristic = characteristic + print('RX:', self.rx_characteristic) + print('TX:', self.tx_characteristic) + + # Subscribe to TX + if self.tx_characteristic: + await self.peer.subscribe(self.tx_characteristic, self.on_tx_received) + print(color('=== Subscribed to Gattlink TX', 'yellow')) + else: + print(color('!!! Gattlink TX not found', 'red')) + + def on_connection_failure(self, error): + print(color(f'!!! Connection failed: {error}')) + + def on_disconnection(self, reason): + print(color(f'!!! Disconnected from {self.peer}, reason={HCI_Constant.error_name(reason)}', 'red')) + self.tx_characteristic = None + self.rx_characteristic = None + self.peer = None + + # Called by the GATT client when a notification is received + def on_tx_received(self, value): + print(color('>>> TX:', 'magenta'), value.hex()) + if self.tx_socket: + self.tx_socket.sendto(value) + + # Called by asyncio when the UDP socket is created + def connection_made(self, transport): + pass + + # Called by asyncio when a UDP datagram is received + def datagram_received(self, data, address): + print(color('<<< RX:', 'magenta'), data.hex()) + + # TODO: use a queue instead of creating a task everytime + if self.peer and self.rx_characteristic: + asyncio.create_task(self.peer.write_value(self.rx_characteristic, data)) + + +# ----------------------------------------------------------------------------- +class GattlinkNodeBridge(Device.Listener): + def __init__(self): + self.peer = None + self.rx_socket = None + self.tx_socket = None + + # Called by asyncio when the UDP socket is created + def connection_made(self, transport): + pass + + # Called by asyncio when a UDP datagram is received + def datagram_received(self, data, address): + print(color('<<< RX:', 'magenta'), data.hex()) + + # TODO: use a queue instead of creating a task everytime + if self.peer and self.rx_characteristic: + asyncio.create_task(self.peer.write_value(self.rx_characteristic, data)) + + +# ----------------------------------------------------------------------------- +async def run(hci_transport, device_address, send_host, send_port, receive_host, receive_port): + print('<<< connecting to HCI...') + async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink): + print('<<< connected') + + # Instantiate a bridge object + bridge = GattlinkNodeBridge() + + # Create a UDP to RX bridge (receive from UDP, send to RX) + loop = asyncio.get_running_loop() + await loop.create_datagram_endpoint( + lambda: bridge, + local_addr=(receive_host, receive_port) + ) + + # Create a UDP to TX bridge (receive from TX, send to UDP) + bridge.tx_socket, _ = await loop.create_datagram_endpoint( + lambda: asyncio.DatagramProtocol(), + remote_addr=(send_host, send_port) + ) + + # Create a device to manage the host, with a custom listener + device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink) + device.listener = bridge + await device.power_on() + + # Connect to the peer + # print(f'=== Connecting to {device_address}...') + # await device.connect(device_address) + + # TODO move to class + gattlink_service = Service( + GG_GATTLINK_SERVICE_UUID, + [ + Characteristic( + GG_GATTLINK_L2CAP_CHANNEL_PSM_CHARACTERISTIC_UUID, + Characteristic.READ, + Characteristic.READABLE, + bytes([193, 0]) + ) + ] + ) + device.add_services([gattlink_service]) + device.advertising_data = bytes( + AdvertisingData([ + (AdvertisingData.COMPLETE_LOCAL_NAME, bytes('Bumble GG', 'utf-8')), + (AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS, bytes(reversed(bytes.fromhex('ABBAFF00E56A484CB8328B17CF6CBFE8')))) + ]) + ) + await device.start_advertising() + + # Wait until the source terminates + await hci_source.wait_for_termination() + + +@click.command() +@click.argument('hci_transport') +@click.argument('device_address') +@click.option('-sh', '--send-host', type=str, default='127.0.0.1', help='UDP host to send to') +@click.option('-sp', '--send-port', type=int, default=9001, help='UDP port to send to') +@click.option('-rh', '--receive-host', type=str, default='127.0.0.1', help='UDP host to receive on') +@click.option('-rp', '--receive-port', type=int, default=9000, help='UDP port to receive on') +def main(hci_transport, device_address, send_host, send_port, receive_host, receive_port): + logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) + asyncio.run(run(hci_transport, device_address, send_host, send_port, receive_host, receive_port)) + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/apps/hci_bridge.py b/apps/hci_bridge.py new file mode 100644 index 0000000..0680f52 --- /dev/null +++ b/apps/hci_bridge.py @@ -0,0 +1,89 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import asyncio +import os +import sys + +from bumble import hci, transport +from bumble.bridge import HCI_Bridge + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Main +# ----------------------------------------------------------------------------- +async def async_main(): + if len(sys.argv) < 3: + print('Usage: hci_bridge.py [command-short-circuit-list]') + print('example: python hci_bridge.py udp:0.0.0.0:9000,127.0.0.1:9001 serial:/dev/tty.usbmodem0006839912171,1000000 0x3f:0x0070,0x3f:0x0074,0x3f:0x0077,0x3f:0x0078') + return + + print('>>> connecting to HCI...') + async with await transport.open_transport_or_link(sys.argv[1]) as (hci_host_source, hci_host_sink): + print('>>> connected') + + print('>>> connecting to HCI...') + async with await transport.open_transport_or_link(sys.argv[2]) as (hci_controller_source, hci_controller_sink): + print('>>> connected') + + command_short_circuits = [] + if len(sys.argv) >= 4: + for op_code_str in sys.argv[3].split(','): + if ':' in op_code_str: + ogf, ocf = op_code_str.split(':') + command_short_circuits.append(hci.hci_command_op_code(int(ogf, 16), int(ocf, 16))) + else: + command_short_circuits.append(int(op_code_str, 16)) + + def host_to_controller_filter(hci_packet): + if hci_packet.hci_packet_type == hci.HCI_COMMAND_PACKET and hci_packet.op_code in command_short_circuits: + # Respond with a success response + logger.debug('short-circuiting packet') + response = hci.HCI_Command_Complete_Event( + num_hci_command_packets = 1, + command_opcode = hci_packet.op_code, + return_parameters = bytes([hci.HCI_SUCCESS]) + ) + # Return a packet with 'respond to sender' set to True + return (response.to_bytes(), True) + + _ = HCI_Bridge( + hci_host_source, + hci_host_sink, + hci_controller_source, + hci_controller_sink, + host_to_controller_filter, + None + ) + await asyncio.get_running_loop().create_future() + + +# ----------------------------------------------------------------------------- +def main(): + logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) + asyncio.run(async_main()) + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/apps/link_relay/__init__.py b/apps/link_relay/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/link_relay/link_relay.py b/apps/link_relay/link_relay.py new file mode 100644 index 0000000..c979ea6 --- /dev/null +++ b/apps/link_relay/link_relay.py @@ -0,0 +1,276 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ---------------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------------- +import sys +import websockets +import logging +import json +import asyncio +import argparse +import uuid +import os +from urllib.parse import urlparse +from colors import color + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ---------------------------------------------------------------------------- +# Constants +# ---------------------------------------------------------------------------- +DEFAULT_RELAY_PORT = 10723 + + +# ---------------------------------------------------------------------------- +# Utils +# ---------------------------------------------------------------------------- +def error_to_json(error): + return json.dumps({'error': error}) + + +def error_to_result(error): + return f'result:{error_to_json(error)}' + + +async def broadcast_message(message, connections): + # Send to all the connections + tasks = [connection.send_message(message) for connection in connections] + if tasks: + await asyncio.gather(*tasks) + + +# ---------------------------------------------------------------------------- +# Connection class +# ---------------------------------------------------------------------------- +class Connection: + """ + A Connection represents a client connected to the relay over a websocket + """ + + def __init__(self, room, websocket): + self.room = room + self.websocket = websocket + self.address = str(uuid.uuid4()) + + async def send_message(self, message): + try: + logger.debug(color(f'->{self.address}: {message}', 'yellow')) + return await self.websocket.send(message) + except websockets.exceptions.WebSocketException as error: + logger.info(f'! client "{self}" disconnected: {error}') + await self.cleanup() + + async def send_error(self, error): + return await self.send_message(f'result:{error_to_json(error)}') + + async def receive_message(self): + try: + message = await self.websocket.recv() + logger.debug(color(f'<-{self.address}: {message}', 'blue')) + return message + except websockets.exceptions.WebSocketException as error: + logger.info(color(f'! client "{self}" disconnected: {error}', 'red')) + await self.cleanup() + + async def cleanup(self): + if self.room: + await self.room.remove_connection(self) + + def set_address(self, address): + logger.info(f'Connection address changed: {self.address} -> {address}') + self.address = address + + def __str__(self): + return f'Connection(address="{self.address}", client={self.websocket.remote_address[0]}:{self.websocket.remote_address[1]})' + + +# ---------------------------------------------------------------------------- +# Room class +# ---------------------------------------------------------------------------- +class Room: + """ + A Room is a collection of bridged connections + """ + + def __init__(self, relay, name): + self.relay = relay + self.name = name + self.observers = [] + self.connections = [] + + async def add_connection(self, connection): + logger.info(f'New participant in {self.name}: {connection}') + self.connections.append(connection) + await self.broadcast_message(connection, f'joined:{connection.address}') + + async def remove_connection(self, connection): + if connection in self.connections: + self.connections.remove(connection) + await self.broadcast_message(connection, f'left:{connection.address}') + + def find_connections_by_address(self, address): + return [c for c in self.connections if c.address == address] + + async def bridge_connection(self, connection): + while True: + # Wait for a message + message = await connection.receive_message() + + # Skip empty messages + if message is None: + return + + # Parse the message to decide how to handle it + if message.startswith('@'): + # This is a targetted message + await self.on_targetted_message(connection, message) + elif message.startswith('/'): + # This is an RPC request + await self.on_rpc_request(connection, message) + else: + await connection.send_message(f'result:{error_to_json("error: invalid message")}') + + async def broadcast_message(self, sender, message): + ''' + Send to all connections in the room except back to the sender + ''' + await broadcast_message(message, [c for c in self.connections if c != sender]) + + async def on_rpc_request(self, connection, message): + command, *params = message.split(' ', 1) + if handler := getattr(self, f'on_{command[1:].lower().replace("-","_")}_command', None): + try: + result = await handler(connection, params) + except Exception as error: + result = error_to_result(error) + else: + result = error_to_result('unknown command') + + await connection.send_message(result or 'result:{}') + + async def on_targetted_message(self, connection, message): + target, *payload = message.split(' ', 1) + if not payload: + return error_to_json('missing arguments') + payload = payload[0] + target = target[1:] + + # Determine what targets to send to + if target == '*': + # Send to all connections in the room except the connection from which the message was received + connections = [c for c in self.connections if c != connection] + else: + connections = self.find_connections_by_address(target) + if not connections: + # Unicast with no recipient, let the sender know + await connection.send_message(f'unreachable:{target}') + + # Send to targets + await broadcast_message(f'message:{connection.address}/{payload}', connections) + + async def on_set_address_command(self, connection, params): + if not params: + return error_to_result('missing address') + + current_address = connection.address + new_address = params[0] + connection.set_address(new_address) + await self.broadcast_message(connection, f'address-changed:from={current_address},to={new_address}') + + +# ---------------------------------------------------------------------------- +class Relay: + """ + A relay accepts connections with the following url: ws:///. + Participants in a room can communicate with each other + """ + + def __init__(self, port): + self.port = port + self.rooms = {} + self.observers = [] + + def start(self): + logger.info(f'Starting Relay on port {self.port}') + + return websockets.serve(self.serve, '0.0.0.0', self.port, ping_interval=None) + + async def serve_as_controller(connection): + pass + + async def serve(self, websocket, path): + logger.debug(f'New connection with path {path}') + + # Parse the path + parsed = urlparse(path) + + # Check if this is a controller client + if parsed.path == '/': + return await self.serve_as_controller(Connection('', websocket)) + + # Find or create a room for this connection + room_name = parsed.path[1:].split('/')[0] + if room_name not in self.rooms: + self.rooms[room_name] = Room(self, room_name) + room = self.rooms[room_name] + + # Add the connection to the room + connection = Connection(room, websocket) + await room.add_connection(connection) + + # Bridge until the connection is closed + await room.bridge_connection(connection) + + +# ---------------------------------------------------------------------------- +def main(): + # Check the Python version + if sys.version_info < (3, 6, 1): + print('ERROR: Python 3.6.1 or higher is required') + sys.exit(1) + + logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) + + # Parse arguments + arg_parser = argparse.ArgumentParser(description='Bumble Link Relay') + arg_parser.add_argument('--log-level', default='INFO', help='logger level') + arg_parser.add_argument('--log-config', help='logger config file (YAML)') + arg_parser.add_argument('--port', + type = int, + default = DEFAULT_RELAY_PORT, + help = 'Port to listen on') + args = arg_parser.parse_args() + + # Setup logger + if args.log_config: + from logging import config + config.fileConfig(args.log_config) + else: + logging.basicConfig(level = getattr(logging, args.log_level.upper())) + + # Start a relay + relay = Relay(args.port) + asyncio.get_event_loop().run_until_complete(relay.start()) + asyncio.get_event_loop().run_forever() + + +# ---------------------------------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/apps/link_relay/logging.yml b/apps/link_relay/logging.yml new file mode 100644 index 0000000..0f6df12 --- /dev/null +++ b/apps/link_relay/logging.yml @@ -0,0 +1,21 @@ +[loggers] +keys=root + +[handlers] +keys=stream_handler + +[formatters] +keys=formatter + +[logger_root] +level=DEBUG +handlers=stream_handler + +[handler_stream_handler] +class=StreamHandler +level=DEBUG +formatter=formatter +args=(sys.stderr,) + +[formatter_formatter] +format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s diff --git a/apps/pair.py b/apps/pair.py new file mode 100644 index 0000000..8c14ddc --- /dev/null +++ b/apps/pair.py @@ -0,0 +1,312 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import os +import logging +import click +import aioconsole +from colors import color + +from bumble.device import Device, Peer +from bumble.transport import open_transport_or_link +from bumble.smp import PairingDelegate, PairingConfig +from bumble.smp import error_name as smp_error_name +from bumble.keys import JsonKeyStore +from bumble.core import ProtocolError +from bumble.gatt import ( + GATT_DEVICE_NAME_CHARACTERISTIC, + GATT_GENERIC_ACCESS_SERVICE, + Service, + Characteristic, + CharacteristicValue +) +from bumble.att import ( + ATT_Error, + ATT_INSUFFICIENT_AUTHENTICATION_ERROR, + ATT_INSUFFICIENT_ENCRYPTION_ERROR +) + + +# ----------------------------------------------------------------------------- +class Delegate(PairingDelegate): + def __init__(self, connection, capability_string, prompt): + super().__init__({ + 'keyboard': PairingDelegate.KEYBOARD_INPUT_ONLY, + 'display': PairingDelegate.DISPLAY_OUTPUT_ONLY, + 'display+keyboard': PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT, + 'display+yes/no': PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT, + 'none': PairingDelegate.NO_OUTPUT_NO_INPUT + }[capability_string.lower()]) + + self.peer = Peer(connection) + self.peer_name = None + self.prompt = prompt + + async def update_peer_name(self): + if self.peer_name is not None: + # We already asked the peer + return + + # Try to get the peer's name + if self.peer: + peer_name = await get_peer_name(self.peer) + self.peer_name = f'{peer_name or ""} [{self.peer.connection.peer_address}]' + else: + self.peer_name = '[?]' + + async def accept(self): + if self.prompt: + await self.update_peer_name() + + # Wait a bit to allow some of the log lines to print before we prompt + await asyncio.sleep(1) + + # Prompt for acceptance + print(color('###-----------------------------------', 'yellow')) + print(color(f'### Pairing request from {self.peer_name}', 'yellow')) + print(color('###-----------------------------------', 'yellow')) + while True: + response = await aioconsole.ainput(color('>>> Accept? ', 'yellow')) + response = response.lower().strip() + if response == 'yes': + return True + elif response == 'no': + return False + else: + # Accept silently + return True + + async def compare_numbers(self, number): + await self.update_peer_name() + + # Wait a bit to allow some of the log lines to print before we prompt + await asyncio.sleep(1) + + # Prompt for a numeric comparison + print(color('###-----------------------------------', 'yellow')) + print(color(f'### Pairing with {self.peer_name}', 'yellow')) + print(color('###-----------------------------------', 'yellow')) + while True: + response = await aioconsole.ainput(color(f'>>> Does the other device display {number:06}? ', 'yellow')) + response = response.lower().strip() + if response == 'yes': + return True + elif response == 'no': + return False + + async def get_number(self): + await self.update_peer_name() + + # Wait a bit to allow some of the log lines to print before we prompt + await asyncio.sleep(1) + + # Prompt for a PIN + while True: + try: + print(color('###-----------------------------------', 'yellow')) + print(color(f'### Pairing with {self.peer_name}', 'yellow')) + print(color('###-----------------------------------', 'yellow')) + return int(await aioconsole.ainput(color('>>> Enter PIN: ', 'yellow'))) + except ValueError: + pass + + async def display_number(self, number): + await self.update_peer_name() + + # Wait a bit to allow some of the log lines to print before we prompt + await asyncio.sleep(1) + + # Display a PIN code + print(color('###-----------------------------------', 'yellow')) + print(color(f'### Pairing with {self.peer_name}', 'yellow')) + print(color(f'### PIN: {number:06}', 'yellow')) + print(color('###-----------------------------------', 'yellow')) + + +# ----------------------------------------------------------------------------- +async def get_peer_name(peer): + services = await peer.discover_service(GATT_GENERIC_ACCESS_SERVICE) + if not services: + return None + + values = await peer.read_characteristics_by_uuid(GATT_DEVICE_NAME_CHARACTERISTIC, services[0]) + if values: + return values[0].decode('utf-8') + + +# ----------------------------------------------------------------------------- +AUTHENTICATION_ERROR_RETURNED = [False, False] + + +def read_with_error(connection): + if not connection.is_encrypted: + raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR) + + if AUTHENTICATION_ERROR_RETURNED[0]: + return bytes([1]) + else: + AUTHENTICATION_ERROR_RETURNED[0] = True + raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR) + + +def write_with_error(connection, value): + if not connection.is_encrypted: + raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR) + + if not AUTHENTICATION_ERROR_RETURNED[1]: + AUTHENTICATION_ERROR_RETURNED[1] = True + raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR) + + +# ----------------------------------------------------------------------------- +def on_connection(connection, request): + print(color(f'<<< Connection: {connection}', 'green')) + + # Listen for pairing events + connection.on('pairing_start', on_pairing_start) + connection.on('pairing', on_pairing) + connection.on('pairing_failure', on_pairing_failure) + + # Listen for encryption changes + connection.on( + 'connection_encryption_change', + lambda: on_connection_encryption_change(connection) + ) + + # Request pairing if needed + if request: + print(color('>>> Requesting pairing', 'green')) + connection.request_pairing() + + +# ----------------------------------------------------------------------------- +def on_connection_encryption_change(connection): + print(color('@@@-----------------------------------', 'blue')) + print(color(f'@@@ Connection is {"" if connection.is_encrypted else "not"}encrypted', 'blue')) + print(color('@@@-----------------------------------', 'blue')) + + +# ----------------------------------------------------------------------------- +def on_pairing_start(): + print(color('***-----------------------------------', 'magenta')) + print(color('*** Pairing starting', 'magenta')) + print(color('***-----------------------------------', 'magenta')) + + +# ----------------------------------------------------------------------------- +def on_pairing(keys): + print(color('***-----------------------------------', 'cyan')) + print(color('*** Paired!', 'cyan')) + keys.print(prefix=color('*** ', 'cyan')) + print(color('***-----------------------------------', 'cyan')) + + +# ----------------------------------------------------------------------------- +def on_pairing_failure(reason): + print(color('***-----------------------------------', 'red')) + print(color(f'*** Pairing failed: {smp_error_name(reason)}', 'red')) + print(color('***-----------------------------------', 'red')) + + +# ----------------------------------------------------------------------------- +async def pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, transport, address_or_name): + print('<<< connecting to HCI...') + async with await open_transport_or_link(transport) as (hci_source, hci_sink): + print('<<< connected') + + # Create a device to manage the host + device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink) + + # Set a custom keystore if specified on the command line + if keystore_file: + device.keystore = JsonKeyStore(namespace=None, filename=keystore_file) + + # Print the existing keys before pairing + if print_keys and device.keystore: + print(color('@@@-----------------------------------', 'blue')) + print(color('@@@ Pairing Keys:', 'blue')) + await device.keystore.print(prefix=color('@@@ ', 'blue')) + print(color('@@@-----------------------------------', 'blue')) + + # Expose a GATT characteristic that can be used to trigger pairing by + # responding with an authentication error when read + device.add_service( + Service( + '50DB505C-8AC4-4738-8448-3B1D9CC09CC5', + [ + Characteristic( + '552957FB-CF1F-4A31-9535-E78847E1A714', + Characteristic.READ | Characteristic.WRITE, + Characteristic.READABLE | Characteristic.WRITEABLE, + CharacteristicValue(read=read_with_error, write=write_with_error) + ) + ] + ) + ) + + # Get things going + await device.power_on() + + # Set up a pairing config factory + device.pairing_config_factory = lambda connection: PairingConfig( + sc, + mitm, + bond, + Delegate(connection, io, prompt) + ) + + # Connect to a peer or wait for a connection + device.on('connection', lambda connection: on_connection(connection, request)) + if address_or_name is not None: + print(color(f'=== Connecting to {address_or_name}...', 'green')) + connection = await device.connect(address_or_name) + + if not request: + try: + await connection.pair() + return + except ProtocolError: + pass + else: + # Advertise so that peers can find us and connect + await device.start_advertising(auto_restart=True) + + await hci_source.wait_for_termination() + + +# ----------------------------------------------------------------------------- +@click.command() +@click.option('--sc', type=bool, default=True, help='Use the Secure Connections protocol', show_default=True) +@click.option('--mitm', type=bool, default=True, help='Request MITM protection', show_default=True) +@click.option('--bond', type=bool, default=True, help='Enable bonding', show_default=True) +@click.option('--io', type=click.Choice(['keyboard', 'display', 'display+keyboard', 'display+yes/no', 'none']), default='display+keyboard', show_default=True) +@click.option('--prompt', is_flag=True, help='Prompt to accept/reject pairing request') +@click.option('--request', is_flag=True, help='Request that the connecting peer initiate pairing') +@click.option('--print-keys', is_flag=True, help='Print the bond keys before pairing') +@click.option('--keystore-file', help='File in which to store the pairing keys') +@click.argument('device-config') +@click.argument('transport') +@click.argument('address-or-name', required=False) +def main(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, transport, address_or_name): + logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) + asyncio.run(pair(sc, mitm, bond, io, prompt, request, print_keys, keystore_file, device_config, transport, address_or_name)) + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/apps/scan.py b/apps/scan.py new file mode 100644 index 0000000..045cb57 --- /dev/null +++ b/apps/scan.py @@ -0,0 +1,157 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import os +import logging +import click +from colors import color + +from bumble.device import Device +from bumble.transport import open_transport_or_link +from bumble.keys import JsonKeyStore +from bumble.smp import AddressResolver +from bumble.hci import HCI_LE_Advertising_Report_Event +from bumble.core import AdvertisingData + + +# ----------------------------------------------------------------------------- +def make_rssi_bar(rssi): + DISPLAY_MIN_RSSI = -105 + DISPLAY_MAX_RSSI = -30 + DEFAULT_RSSI_BAR_WIDTH = 30 + + blocks = ['', '▏', '▎', '▍', '▌', '▋', '▊', '▉'] + bar_width = (rssi - DISPLAY_MIN_RSSI) / (DISPLAY_MAX_RSSI - DISPLAY_MIN_RSSI) + bar_width = min(max(bar_width, 0), 1) + bar_ticks = int(bar_width * DEFAULT_RSSI_BAR_WIDTH * 8) + return ('█' * int(bar_ticks / 8)) + blocks[bar_ticks % 8] + + +# ----------------------------------------------------------------------------- +class AdvertisementPrinter: + def __init__(self, min_rssi, resolver): + self.min_rssi = min_rssi + self.resolver = resolver + + def print_advertisement(self, address, address_color, ad_data, rssi): + if self.min_rssi is not None and rssi < self.min_rssi: + return + + address_qualifier = '' + resolution_qualifier = '' + if self.resolver and address.is_resolvable: + resolved = self.resolver.resolve(address) + if resolved is not None: + resolution_qualifier = f'(resolved from {address})' + address = resolved + + address_type_string = ('PUBLIC', 'RANDOM', 'PUBLIC_ID', 'RANDOM_ID')[address.address_type] + if address.is_public: + type_color = 'cyan' + else: + if address.is_static: + type_color = 'green' + address_qualifier = '(static)' + elif address.is_resolvable: + type_color = 'magenta' + address_qualifier = '(resolvable)' + else: + type_color = 'blue' + address_qualifier = '(non-resolvable)' + + rssi_bar = make_rssi_bar(rssi) + separator = '\n ' + print(f'>>> {color(address, address_color)} [{color(address_type_string, type_color)}]{address_qualifier}{resolution_qualifier}:{separator}RSSI:{rssi:4} {rssi_bar}{separator}{ad_data.to_string(separator)}\n') + + def on_advertisement(self, address, ad_data, rssi, connectable): + address_color = 'yellow' if connectable else 'red' + self.print_advertisement(address, address_color, ad_data, rssi) + + def on_advertising_report(self, address, ad_data, rssi, event_type): + print(f'{color("EVENT", "green")}: {HCI_LE_Advertising_Report_Event.event_type_name(event_type)}') + ad_data = AdvertisingData.from_bytes(ad_data) + self.print_advertisement(address, 'yellow', ad_data, rssi) + + +# ----------------------------------------------------------------------------- +async def scan( + min_rssi, + passive, + scan_interval, + scan_window, + filter_duplicates, + raw, + keystore_file, + device_config, + transport +): + print('<<< connecting to HCI...') + async with await open_transport_or_link(transport) as (hci_source, hci_sink): + print('<<< connected') + + if device_config: + device = Device.from_config_file_with_hci(device_config, hci_source, hci_sink) + else: + device = Device.with_hci('Bumble', 'F0:F1:F2:F3:F4:F5', hci_source, hci_sink) + + if keystore_file: + keystore = JsonKeyStore(namespace=None, filename=keystore_file) + device.keystore = keystore + else: + resolver = None + + if device.keystore: + resolving_keys = await device.keystore.get_resolving_keys() + resolver = AddressResolver(resolving_keys) + + printer = AdvertisementPrinter(min_rssi, resolver) + if raw: + device.host.on('advertising_report', printer.on_advertising_report) + else: + device.on('advertisement', printer.on_advertisement) + + await device.power_on() + await device.start_scanning( + active=(not passive), + scan_interval=scan_interval, + scan_window=scan_window, + filter_duplicates=filter_duplicates + ) + + await hci_source.wait_for_termination() + + +# ----------------------------------------------------------------------------- +@click.command() +@click.option('--min-rssi', type=int, help='Minimum RSSI value') +@click.option('--passive', is_flag=True, default=False, help='Perform passive scanning') +@click.option('--scan-interval', type=int, default=60, help='Scan interval') +@click.option('--scan-window', type=int, default=60, help='Scan window') +@click.option('--filter-duplicates', type=bool, default=True, help='Filter duplicates at the controller level') +@click.option('--raw', is_flag=True, default=False, help='Listen for raw advertising reports instead of processed ones') +@click.option('--keystore-file', help='Keystore file to use when resolving addresses') +@click.option('--device-config', help='Device config file for the scanning device') +@click.argument('transport') +def main(min_rssi, passive, scan_interval, scan_window, filter_duplicates, raw, keystore_file, device_config, transport): + logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper()) + asyncio.run(scan(min_rssi, passive, scan_interval, scan_window, filter_duplicates, raw, keystore_file, device_config, transport)) + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/apps/show.py b/apps/show.py new file mode 100644 index 0000000..ad68b49 --- /dev/null +++ b/apps/show.py @@ -0,0 +1,120 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import struct +import click +from colors import color + +from bumble import hci +from bumble.transport import PacketReader +from bumble.helpers import PacketTracer + + +# ----------------------------------------------------------------------------- +class SnoopPacketReader: + ''' + Reader that reads HCI packets from a "snoop" file (based on RFC 1761, but not exactly the same...) + ''' + + DATALINK_H1 = 1001 + DATALINK_H4 = 1002 + DATALINK_BSCP = 1003 + DATALINK_H5 = 1004 + + def __init__(self, source): + self.source = source + + # Read the header + identification_pattern = source.read(8) + if identification_pattern.hex().lower() != '6274736e6f6f7000': + raise ValueError('not a valid snoop file, unexpected identification pattern') + (self.version_number, self.data_link_type) = struct.unpack('>II', source.read(8)) + if self.data_link_type != self.DATALINK_H4 and self.data_link_type != self.DATALINK_H1: + raise ValueError(f'datalink type {self.data_link_type} not supported') + + def next_packet(self): + # Read the record header + header = self.source.read(24) + if len(header) < 24: + return (0, None) + ( + original_length, + included_length, + packet_flags, + cumulative_drops, + timestamp_seconds, + timestamp_microsecond + ) = struct.unpack('>IIIIII', header) + + # Abort on truncated packets + if original_length != included_length: + return (0, None) + + if self.data_link_type == self.DATALINK_H1: + # The packet is un-encapsulated, look at the flags to figure out its type + if packet_flags & 1: + # Controller -> Host + if packet_flags & 2: + packet_type = hci.HCI_EVENT_PACKET + else: + packet_type = hci.HCI_ACL_DATA_PACKET + else: + # Host -> Controller + if packet_flags & 2: + packet_type = hci.HCI_COMMAND_PACKET + else: + packet_type = hci.HCI_ACL_DATA_PACKET + + return (packet_flags & 1, bytes([packet_type]) + self.source.read(included_length)) + else: + return (packet_flags & 1, self.source.read(included_length)) + + +# ----------------------------------------------------------------------------- +# Main +# ----------------------------------------------------------------------------- +@click.command() +@click.option('--format', type=click.Choice(['h4', 'snoop']), default='h4', help='Format of the input file') +@click.argument('filename') +def show(format, filename): + input = open(filename, 'rb') + if format == 'h4': + packet_reader = PacketReader(input) + + def read_next_packet(): + (0, packet_reader.next_packet()) + else: + packet_reader = SnoopPacketReader(input) + read_next_packet = packet_reader.next_packet + + tracer = PacketTracer(emit_message=print) + + while True: + try: + (direction, packet) = read_next_packet() + if packet is None: + break + tracer.trace(hci.HCI_Packet.from_bytes(packet), direction) + + except Exception as error: + print(color(f'!!! {error}', 'red')) + pass + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + show() diff --git a/apps/unbond.py b/apps/unbond.py new file mode 100644 index 0000000..cf1877c --- /dev/null +++ b/apps/unbond.py @@ -0,0 +1,63 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import os +import logging +import click + +from bumble.device import Device +from bumble.keys import JsonKeyStore + + +# ----------------------------------------------------------------------------- +async def unbond(keystore_file, device_config, address): + # Create a device to manage the host + device = Device.from_config_file(device_config) + + # Get all entries in the keystore + if keystore_file: + keystore = JsonKeyStore(None, keystore_file) + else: + keystore = device.keystore + + if keystore is None: + print('no keystore') + return + + if address is None: + await keystore.print() + else: + try: + await keystore.delete(address) + except KeyError: + print('!!! pairing not found') + + +# ----------------------------------------------------------------------------- +@click.command() +@click.option('--keystore-file', help='File in which to store the pairing keys') +@click.argument('device-config') +@click.argument('address', required=False) +def main(keystore_file, device_config, address): + logging.basicConfig(level = os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper()) + asyncio.run(unbond(keystore_file, device_config, address)) + + +# ----------------------------------------------------------------------------- +if __name__ == '__main__': + main() diff --git a/bumble/__init__.py b/bumble/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bumble/a2dp.py b/bumble/a2dp.py new file mode 100644 index 0000000..03c3fd2 --- /dev/null +++ b/bumble/a2dp.py @@ -0,0 +1,554 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import struct +import bitstruct +import logging +from collections import namedtuple +from colors import color + +from .company_ids import COMPANY_IDENTIFIERS +from .sdp import ( + DataElement, + ServiceAttribute, + SDP_PUBLIC_BROWSE_ROOT, + SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, + SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, + SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, + SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, + SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID +) +from .core import ( + BT_L2CAP_PROTOCOL_ID, + BT_AUDIO_SOURCE_SERVICE, + BT_AUDIO_SINK_SERVICE, + BT_AVDTP_PROTOCOL_ID, + BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE, + name_or_number +) + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- + +A2DP_SBC_CODEC_TYPE = 0x00 +A2DP_MPEG_1_2_AUDIO_CODEC_TYPE = 0x01 +A2DP_MPEG_2_4_AAC_CODEC_TYPE = 0x02 +A2DP_ATRAC_FAMILY_CODEC_TYPE = 0x03 +A2DP_NON_A2DP_CODEC_TYPE = 0xFF + +A2DP_CODEC_TYPE_NAMES = { + A2DP_SBC_CODEC_TYPE: 'A2DP_SBC_CODEC_TYPE', + A2DP_MPEG_1_2_AUDIO_CODEC_TYPE: 'A2DP_MPEG_1_2_AUDIO_CODEC_TYPE', + A2DP_MPEG_2_4_AAC_CODEC_TYPE: 'A2DP_MPEG_2_4_AAC_CODEC_TYPE', + A2DP_ATRAC_FAMILY_CODEC_TYPE: 'A2DP_ATRAC_FAMILY_CODEC_TYPE', + A2DP_NON_A2DP_CODEC_TYPE: 'A2DP_NON_A2DP_CODEC_TYPE' +} + + +SBC_SYNC_WORD = 0x9C + +SBC_SAMPLING_FREQUENCIES = [ + 16000, + 22050, + 44100, + 48000 +] + +SBC_MONO_CHANNEL_MODE = 0x00 +SBC_DUAL_CHANNEL_MODE = 0x01 +SBC_STEREO_CHANNEL_MODE = 0x02 +SBC_JOINT_STEREO_CHANNEL_MODE = 0x03 + +SBC_CHANNEL_MODE_NAMES = { + SBC_MONO_CHANNEL_MODE: 'SBC_MONO_CHANNEL_MODE', + SBC_DUAL_CHANNEL_MODE: 'SBC_DUAL_CHANNEL_MODE', + SBC_STEREO_CHANNEL_MODE: 'SBC_STEREO_CHANNEL_MODE', + SBC_JOINT_STEREO_CHANNEL_MODE: 'SBC_JOINT_STEREO_CHANNEL_MODE' +} + +SBC_BLOCK_LENGTHS = [4, 8, 12, 16] + +SBC_SUBBANDS = [4, 8] + +SBC_SNR_ALLOCATION_METHOD = 0x00 +SBC_LOUDNESS_ALLOCATION_METHOD = 0x01 + +SBC_ALLOCATION_METHOD_NAMES = { + SBC_SNR_ALLOCATION_METHOD: 'SBC_SNR_ALLOCATION_METHOD', + SBC_LOUDNESS_ALLOCATION_METHOD: 'SBC_LOUDNESS_ALLOCATION_METHOD' +} + +MPEG_2_4_AAC_SAMPLING_FREQUENCIES = [ + 8000, + 11025, + 12000, + 16000, + 22050, + 24000, + 32000, + 44100, + 48000, + 64000, + 88200, + 96000 +] + +MPEG_2_AAC_LC_OBJECT_TYPE = 0x00 +MPEG_4_AAC_LC_OBJECT_TYPE = 0x01 +MPEG_4_AAC_LTP_OBJECT_TYPE = 0x02 +MPEG_4_AAC_SCALABLE_OBJECT_TYPE = 0x03 + +MPEG_2_4_OBJECT_TYPE_NAMES = { + MPEG_2_AAC_LC_OBJECT_TYPE: 'MPEG_2_AAC_LC_OBJECT_TYPE', + MPEG_4_AAC_LC_OBJECT_TYPE: 'MPEG_4_AAC_LC_OBJECT_TYPE', + MPEG_4_AAC_LTP_OBJECT_TYPE: 'MPEG_4_AAC_LTP_OBJECT_TYPE', + MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 'MPEG_4_AAC_SCALABLE_OBJECT_TYPE' +} + + +# ----------------------------------------------------------------------------- +def flags_to_list(flags, values): + result = [] + for i in range(len(values)): + if flags & (1 << (len(values) - i - 1)): + result.append(values[i]) + return result + + +# ----------------------------------------------------------------------------- +def make_audio_source_service_sdp_records(service_record_handle, version=(1, 3)): + from .avdtp import AVDTP_PSM + version_int = version[0] << 8 | version[1] + return [ + ServiceAttribute(SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, DataElement.unsigned_integer_32(service_record_handle)), + ServiceAttribute(SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, DataElement.sequence([ + DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT) + ])), + ServiceAttribute(SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, DataElement.sequence([ + DataElement.uuid(BT_AUDIO_SOURCE_SERVICE) + ])), + ServiceAttribute(SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([ + DataElement.sequence([ + DataElement.uuid(BT_L2CAP_PROTOCOL_ID), + DataElement.unsigned_integer_16(AVDTP_PSM) + ]), + DataElement.sequence([ + DataElement.uuid(BT_AVDTP_PROTOCOL_ID), + DataElement.unsigned_integer_16(version_int) + ]) + ])), + ServiceAttribute(SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([ + DataElement.uuid(BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE), + DataElement.unsigned_integer_16(version_int) + ])), + ] + + +# ----------------------------------------------------------------------------- +def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)): + from .avdtp import AVDTP_PSM + version_int = version[0] << 8 | version[1] + return [ + ServiceAttribute(SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID, DataElement.unsigned_integer_32(service_record_handle)), + ServiceAttribute(SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID, DataElement.sequence([ + DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT) + ])), + ServiceAttribute(SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID, DataElement.sequence([ + DataElement.uuid(BT_AUDIO_SINK_SERVICE) + ])), + ServiceAttribute(SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([ + DataElement.sequence([ + DataElement.uuid(BT_L2CAP_PROTOCOL_ID), + DataElement.unsigned_integer_16(AVDTP_PSM) + ]), + DataElement.sequence([ + DataElement.uuid(BT_AVDTP_PROTOCOL_ID), + DataElement.unsigned_integer_16(version_int) + ]) + ])), + ServiceAttribute(SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID, DataElement.sequence([ + DataElement.uuid(BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE), + DataElement.unsigned_integer_16(version_int) + ])), + ] + + +# ----------------------------------------------------------------------------- +class SbcMediaCodecInformation( + namedtuple( + 'SbcMediaCodecInformation', + [ + 'sampling_frequency', + 'channel_mode', + 'block_length', + 'subbands', + 'allocation_method', + 'minimum_bitpool_value', + 'maximum_bitpool_value' + ] + ) +): + ''' + A2DP spec - 4.3.2 Codec Specific Information Elements + ''' + + BIT_FIELDS = 'u4u4u4u2u2u8u8' + SAMPLING_FREQUENCY_BITS = { + 16000: 1 << 3, + 32000: 1 << 2, + 44100: 1 << 1, + 48000: 1 + } + CHANNEL_MODE_BITS = { + SBC_MONO_CHANNEL_MODE: 1 << 3, + SBC_DUAL_CHANNEL_MODE: 1 << 2, + SBC_STEREO_CHANNEL_MODE: 1 << 1, + SBC_JOINT_STEREO_CHANNEL_MODE: 1 + } + BLOCK_LENGTH_BITS = { + 4: 1 << 3, + 8: 1 << 2, + 12: 1 << 1, + 16: 1 + } + SUBBANDS_BITS = { + 4: 1 << 1, + 8: 1 + } + ALLOCATION_METHOD_BITS = { + SBC_SNR_ALLOCATION_METHOD: 1 << 1, + SBC_LOUDNESS_ALLOCATION_METHOD: 1 + } + + @staticmethod + def from_bytes(data): + return SbcMediaCodecInformation(*bitstruct.unpack(SbcMediaCodecInformation.BIT_FIELDS, data)) + + @classmethod + def from_discrete_values( + cls, + sampling_frequency, + channel_mode, + block_length, + subbands, + allocation_method, + minimum_bitpool_value, + maximum_bitpool_value + ): + return SbcMediaCodecInformation( + sampling_frequency = cls.SAMPLING_FREQUENCY_BITS[sampling_frequency], + channel_mode = cls.CHANNEL_MODE_BITS[channel_mode], + block_length = cls.BLOCK_LENGTH_BITS[block_length], + subbands = cls.SUBBANDS_BITS[subbands], + allocation_method = cls.ALLOCATION_METHOD_BITS[allocation_method], + minimum_bitpool_value = minimum_bitpool_value, + maximum_bitpool_value = maximum_bitpool_value + ) + + @classmethod + def from_lists( + cls, + sampling_frequencies, + channel_modes, + block_lengths, + subbands, + allocation_methods, + minimum_bitpool_value, + maximum_bitpool_value + ): + return SbcMediaCodecInformation( + sampling_frequency = sum(cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies), + channel_mode = sum(cls.CHANNEL_MODE_BITS[x] for x in channel_modes), + block_length = sum(cls.BLOCK_LENGTH_BITS[x] for x in block_lengths), + subbands = sum(cls.SUBBANDS_BITS[x] for x in subbands), + allocation_method = sum(cls.ALLOCATION_METHOD_BITS[x] for x in allocation_methods), + minimum_bitpool_value = minimum_bitpool_value, + maximum_bitpool_value = maximum_bitpool_value + ) + + def __bytes__(self): + return bitstruct.pack(self.BIT_FIELDS, *self) + + def __str__(self): + channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO'] + allocation_methods = ['SNR', 'Loudness'] + return '\n'.join([ + 'SbcMediaCodecInformation(', + f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, SBC_SAMPLING_FREQUENCIES)])}', + f' channel_mode: {",".join([str(x) for x in flags_to_list(self.channel_mode, channel_modes)])}', + f' block_length: {",".join([str(x) for x in flags_to_list(self.block_length, SBC_BLOCK_LENGTHS)])}', + f' subbands: {",".join([str(x) for x in flags_to_list(self.subbands, SBC_SUBBANDS)])}', + f' allocation_method: {",".join([str(x) for x in flags_to_list(self.allocation_method, allocation_methods)])}', + f' minimum_bitpool_value: {self.minimum_bitpool_value}', + f' maximum_bitpool_value: {self.maximum_bitpool_value}' + ')' + ]) + + +# ----------------------------------------------------------------------------- +class AacMediaCodecInformation( + namedtuple( + 'AacMediaCodecInformation', + [ + 'object_type', + 'sampling_frequency', + 'channels', + 'vbr', + 'bitrate' + ] + ) +): + ''' + A2DP spec - 4.5.2 Codec Specific Information Elements + ''' + + BIT_FIELDS = 'u8u12u2p2u1u23' + OBJECT_TYPE_BITS = { + MPEG_2_AAC_LC_OBJECT_TYPE: 1 << 7, + MPEG_4_AAC_LC_OBJECT_TYPE: 1 << 6, + MPEG_4_AAC_LTP_OBJECT_TYPE: 1 << 5, + MPEG_4_AAC_SCALABLE_OBJECT_TYPE: 1 << 4 + } + SAMPLING_FREQUENCY_BITS = { + 8000: 1 << 11, + 11025: 1 << 10, + 12000: 1 << 9, + 16000: 1 << 8, + 22050: 1 << 7, + 24000: 1 << 6, + 32000: 1 << 5, + 44100: 1 << 4, + 48000: 1 << 3, + 64000: 1 << 2, + 88200: 1 << 1, + 96000: 1 + } + CHANNELS_BITS = { + 1: 1 << 1, + 2: 1 + } + + @staticmethod + def from_bytes(data): + return AacMediaCodecInformation(*bitstruct.unpack(AacMediaCodecInformation.BIT_FIELDS, data)) + + @classmethod + def from_discrete_values( + cls, + object_type, + sampling_frequency, + channels, + vbr, + bitrate + ): + return AacMediaCodecInformation( + object_type = cls.OBJECT_TYPE_BITS[object_type], + sampling_frequency = cls.SAMPLING_FREQUENCY_BITS[sampling_frequency], + channels = cls.CHANNELS_BITS[channels], + vbr = vbr, + bitrate = bitrate + ) + + @classmethod + def from_lists( + cls, + object_types, + sampling_frequencies, + channels, + vbr, + bitrate + ): + return AacMediaCodecInformation( + object_type = sum(cls.OBJECT_TYPE_BITS[x] for x in object_types), + sampling_frequency = sum(cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies), + channels = sum(cls.CHANNELS_BITS[x] for x in channels), + vbr = vbr, + bitrate = bitrate + ) + + def __bytes__(self): + return bitstruct.pack(self.BIT_FIELDS, *self) + + def __str__(self): + object_types = ['MPEG_2_AAC_LC', 'MPEG_4_AAC_LC', 'MPEG_4_AAC_LTP', 'MPEG_4_AAC_SCALABLE', '[4]', '[5]', '[6]', '[7]'] + channels = [1, 2] + return '\n'.join([ + 'AacMediaCodecInformation(', + f' object_type: {",".join([str(x) for x in flags_to_list(self.object_type, object_types)])}', + f' sampling_frequency: {",".join([str(x) for x in flags_to_list(self.sampling_frequency, MPEG_2_4_AAC_SAMPLING_FREQUENCIES)])}', + f' channels: {",".join([str(x) for x in flags_to_list(self.channels, channels)])}', + f' vbr: {self.vbr}', + f' bitrate: {self.bitrate}' + ')' + ]) + + +# ----------------------------------------------------------------------------- +class VendorSpecificMediaCodecInformation: + ''' + A2DP spec - 4.7.2 Codec Specific Information Elements + ''' + + @staticmethod + def from_bytes(data): + (vendor_id, codec_id) = struct.unpack_from('> 6) & 3] + blocks = 4 * (1 + ((header[1] >> 4) & 3)) + channel_mode = (header[1] >> 2) & 3 + channels = 1 if channel_mode == SBC_MONO_CHANNEL_MODE else 2 + subbands = 8 if ((header[1]) & 1) else 4 + bitpool = header[2] + + # Compute the frame length + frame_length = 4 + (4 * subbands * channels) // 8 + if channel_mode in (SBC_MONO_CHANNEL_MODE, SBC_DUAL_CHANNEL_MODE): + frame_length += (blocks * channels * bitpool) // 8 + else: + frame_length += ((1 if channel_mode == SBC_JOINT_STEREO_CHANNEL_MODE else 0) * subbands + blocks * bitpool) // 8 + + # Read the rest of the frame + payload = header + await self.read(frame_length - 4) + + # Emit the next frame + yield SbcFrame(sampling_frequency, blocks, channel_mode, subbands, payload) + + return generate_frames() + + +# ----------------------------------------------------------------------------- +class SbcPacketSource: + def __init__(self, read, mtu, codec_capabilities): + self.read = read + self.mtu = mtu + self.codec_capabilities = codec_capabilities + + @property + def packets(self): + async def generate_packets(): + from .avdtp import MediaPacket # Import here to avoid a circular reference + + sequence_number = 0 + timestamp = 0 + frames = [] + frames_size = 0 + max_rtp_payload = self.mtu - 12 - 1 + + # NOTE: this doesn't support frame fragments + sbc_parser = SbcParser(self.read) + async for frame in sbc_parser.frames: + print(frame) + + if frames_size + len(frame.payload) > max_rtp_payload or len(frames) == 16: + # Need to flush what has been accumulated so far + + # Emit a packet + sbc_payload = bytes([len(frames)]) + b''.join([frame.payload for frame in frames]) + packet = MediaPacket(2, 0, 0, 0, sequence_number, timestamp, 0, [], 96, sbc_payload) + packet.timestamp_seconds = timestamp / frame.sampling_frequency + yield packet + + # Prepare for next packets + sequence_number += 1 + timestamp += sum([frame.sample_count for frame in frames]) + frames = [frame] + frames_size = len(frame.payload) + else: + # Accumulate + frames.append(frame) + frames_size += len(frame.payload) + + return generate_packets() diff --git a/bumble/att.py b/bumble/att.py new file mode 100644 index 0000000..9f82424 --- /dev/null +++ b/bumble/att.py @@ -0,0 +1,728 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# ATT - Attribute Protocol +# +# See Bluetooth spec @ Vol 3, Part F +# +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +from colors import color +from pyee import EventEmitter + +from .core import * +from .hci import * + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +ATT_CID = 0x04 + +ATT_ERROR_RESPONSE = 0x01 +ATT_EXCHANGE_MTU_REQUEST = 0x02 +ATT_EXCHANGE_MTU_RESPONSE = 0x03 +ATT_FIND_INFORMATION_REQUEST = 0x04 +ATT_FIND_INFORMATION_RESPONSE = 0x05 +ATT_FIND_BY_TYPE_VALUE_REQUEST = 0x06 +ATT_FIND_BY_TYPE_VALUE_RESPONSE = 0x07 +ATT_READ_BY_TYPE_REQUEST = 0x08 +ATT_READ_BY_TYPE_RESPONSE = 0x09 +ATT_READ_REQUEST = 0x0A +ATT_READ_RESPONSE = 0x0B +ATT_READ_BLOB_REQUEST = 0x0C +ATT_READ_BLOB_RESPONSE = 0x0D +ATT_READ_MULTIPLE_REQUEST = 0x0E +ATT_READ_MULTIPLE_RESPONSE = 0x0F +ATT_READ_BY_GROUP_TYPE_REQUEST = 0x10 +ATT_READ_BY_GROUP_TYPE_RESPONSE = 0x11 +ATT_WRITE_REQUEST = 0x12 +ATT_WRITE_RESPONSE = 0x13 +ATT_WRITE_COMMAND = 0x52 +ATT_SIGNED_WRITE_COMMAND = 0xD2 +ATT_PREPARE_WRITE_REQUEST = 0x16 +ATT_PREPARE_WRITE_RESPONSE = 0x17 +ATT_EXECUTE_WRITE_REQUEST = 0x18 +ATT_EXECUTE_WRITE_RESPONSE = 0x19 +ATT_HANDLE_VALUE_NOTIFICATION = 0x1B +ATT_HANDLE_VALUE_INDICATION = 0x1D +ATT_HANDLE_VALUE_CONFIRMATION = 0x1E + +ATT_PDU_NAMES = { + ATT_ERROR_RESPONSE: 'ATT_ERROR_RESPONSE', + ATT_EXCHANGE_MTU_REQUEST: 'ATT_EXCHANGE_MTU_REQUEST', + ATT_EXCHANGE_MTU_RESPONSE: 'ATT_EXCHANGE_MTU_RESPONSE', + ATT_FIND_INFORMATION_REQUEST: 'ATT_FIND_INFORMATION_REQUEST', + ATT_FIND_INFORMATION_RESPONSE: 'ATT_FIND_INFORMATION_RESPONSE', + ATT_FIND_BY_TYPE_VALUE_REQUEST: 'ATT_FIND_BY_TYPE_VALUE_REQUEST', + ATT_FIND_BY_TYPE_VALUE_RESPONSE: 'ATT_FIND_BY_TYPE_VALUE_RESPONSE', + ATT_READ_BY_TYPE_REQUEST: 'ATT_READ_BY_TYPE_REQUEST', + ATT_READ_BY_TYPE_RESPONSE: 'ATT_READ_BY_TYPE_RESPONSE', + ATT_READ_REQUEST: 'ATT_READ_REQUEST', + ATT_READ_RESPONSE: 'ATT_READ_RESPONSE', + ATT_READ_BLOB_REQUEST: 'ATT_READ_BLOB_REQUEST', + ATT_READ_BLOB_RESPONSE: 'ATT_READ_BLOB_RESPONSE', + ATT_READ_MULTIPLE_REQUEST: 'ATT_READ_MULTIPLE_REQUEST', + ATT_READ_MULTIPLE_RESPONSE: 'ATT_READ_MULTIPLE_RESPONSE', + ATT_READ_BY_GROUP_TYPE_REQUEST: 'ATT_READ_BY_GROUP_TYPE_REQUEST', + ATT_READ_BY_GROUP_TYPE_RESPONSE: 'ATT_READ_BY_GROUP_TYPE_RESPONSE', + ATT_WRITE_REQUEST: 'ATT_WRITE_REQUEST', + ATT_WRITE_RESPONSE: 'ATT_WRITE_RESPONSE', + ATT_WRITE_COMMAND: 'ATT_WRITE_COMMAND', + ATT_SIGNED_WRITE_COMMAND: 'ATT_SIGNED_WRITE_COMMAND', + ATT_PREPARE_WRITE_REQUEST: 'ATT_PREPARE_WRITE_REQUEST', + ATT_PREPARE_WRITE_RESPONSE: 'ATT_PREPARE_WRITE_RESPONSE', + ATT_EXECUTE_WRITE_REQUEST: 'ATT_EXECUTE_WRITE_REQUEST', + ATT_EXECUTE_WRITE_RESPONSE: 'ATT_EXECUTE_WRITE_RESPONSE', + ATT_HANDLE_VALUE_NOTIFICATION: 'ATT_HANDLE_VALUE_NOTIFICATION', + ATT_HANDLE_VALUE_INDICATION: 'ATT_HANDLE_VALUE_INDICATION', + ATT_HANDLE_VALUE_CONFIRMATION: 'ATT_HANDLE_VALUE_CONFIRMATION' +} + +ATT_REQUESTS = [ + ATT_EXCHANGE_MTU_REQUEST, + ATT_FIND_INFORMATION_REQUEST, + ATT_FIND_BY_TYPE_VALUE_REQUEST, + ATT_READ_BY_TYPE_REQUEST, + ATT_READ_REQUEST, + ATT_READ_BLOB_REQUEST, + ATT_READ_MULTIPLE_REQUEST, + ATT_READ_BY_GROUP_TYPE_REQUEST, + ATT_WRITE_REQUEST, + ATT_PREPARE_WRITE_REQUEST, + ATT_EXECUTE_WRITE_REQUEST +] + +ATT_RESPONSES = [ + ATT_ERROR_RESPONSE, + ATT_EXCHANGE_MTU_RESPONSE, + ATT_FIND_INFORMATION_RESPONSE, + ATT_FIND_BY_TYPE_VALUE_RESPONSE, + ATT_READ_BY_TYPE_RESPONSE, + ATT_READ_RESPONSE, + ATT_READ_BLOB_RESPONSE, + ATT_READ_MULTIPLE_RESPONSE, + ATT_READ_BY_GROUP_TYPE_RESPONSE, + ATT_WRITE_RESPONSE, + ATT_PREPARE_WRITE_RESPONSE, + ATT_EXECUTE_WRITE_RESPONSE +] + +ATT_INVALID_HANDLE_ERROR = 0x01 +ATT_READ_NOT_PERMITTED_ERROR = 0x02 +ATT_WRITE_NOT_PERMITTED_ERROR = 0x03 +ATT_INVALID_PDU_ERROR = 0x04 +ATT_INSUFFICIENT_AUTHENTICATION_ERROR = 0x05 +ATT_REQUEST_NOT_SUPPORTED_ERROR = 0x06 +ATT_INVALID_OFFSET_ERROR = 0x07 +ATT_INSUFFICIENT_AUTHORIZATION_ERROR = 0x08 +ATT_PREPARE_QUEUE_FULL_ERROR = 0x09 +ATT_ATTRIBUTE_NOT_FOUND_ERROR = 0x0A +ATT_ATTRIBUTE_NOT_LONG_ERROR = 0x0B +ATT_INSUFFICIENT_ENCRYPTION_KEY_SIZE_ERROR = 0x0C +ATT_INVALID_ATTRIBUTE_LENGTH_ERROR = 0x0D +ATT_UNLIKELY_ERROR_ERROR = 0x0E +ATT_INSUFFICIENT_ENCRYPTION_ERROR = 0x0F +ATT_UNSUPPORTED_GROUP_TYPE_ERROR = 0x10 +ATT_INSUFFICIENT_RESOURCES_ERROR = 0x11 + +ATT_ERROR_NAMES = { + ATT_INVALID_HANDLE_ERROR: 'ATT_INVALID_HANDLE_ERROR', + ATT_READ_NOT_PERMITTED_ERROR: 'ATT_READ_NOT_PERMITTED_ERROR', + ATT_WRITE_NOT_PERMITTED_ERROR: 'ATT_WRITE_NOT_PERMITTED_ERROR', + ATT_INVALID_PDU_ERROR: 'ATT_INVALID_PDU_ERROR', + ATT_INSUFFICIENT_AUTHENTICATION_ERROR: 'ATT_INSUFFICIENT_AUTHENTICATION_ERROR', + ATT_REQUEST_NOT_SUPPORTED_ERROR: 'ATT_REQUEST_NOT_SUPPORTED_ERROR', + ATT_INVALID_OFFSET_ERROR: 'ATT_INVALID_OFFSET_ERROR', + ATT_INSUFFICIENT_AUTHORIZATION_ERROR: 'ATT_INSUFFICIENT_AUTHORIZATION_ERROR', + ATT_PREPARE_QUEUE_FULL_ERROR: 'ATT_PREPARE_QUEUE_FULL_ERROR', + ATT_ATTRIBUTE_NOT_FOUND_ERROR: 'ATT_ATTRIBUTE_NOT_FOUND_ERROR', + ATT_ATTRIBUTE_NOT_LONG_ERROR: 'ATT_ATTRIBUTE_NOT_LONG_ERROR', + ATT_INSUFFICIENT_ENCRYPTION_KEY_SIZE_ERROR: 'ATT_INSUFFICIENT_ENCRYPTION_KEY_SIZE_ERROR', + ATT_INVALID_ATTRIBUTE_LENGTH_ERROR: 'ATT_INVALID_ATTRIBUTE_LENGTH_ERROR', + ATT_UNLIKELY_ERROR_ERROR: 'ATT_UNLIKELY_ERROR_ERROR', + ATT_INSUFFICIENT_ENCRYPTION_ERROR: 'ATT_INSUFFICIENT_ENCRYPTION_ERROR', + ATT_UNSUPPORTED_GROUP_TYPE_ERROR: 'ATT_UNSUPPORTED_GROUP_TYPE_ERROR', + ATT_INSUFFICIENT_RESOURCES_ERROR: 'ATT_INSUFFICIENT_RESOURCES_ERROR' +} + +ATT_DEFAULT_MTU = 23 + +HANDLE_FIELD_SPEC = {'size': 2, 'mapper': lambda x: f'0x{x:04X}'} +UUID_2_16_FIELD_SPEC = lambda x, y: UUID.parse_uuid(x, y) # noqa: E731 +UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731 + + +# ----------------------------------------------------------------------------- +# Utils +# ----------------------------------------------------------------------------- +def key_with_value(dictionary, target_value): + for key, value in dictionary.items(): + if value == target_value: + return key + return None + + +# ----------------------------------------------------------------------------- +# Exceptions +# ----------------------------------------------------------------------------- +class ATT_Error(Exception): + def __init__(self, error_code, att_handle=0x0000): + self.error_code = error_code + self.att_handle = att_handle + + def __str__(self): + return f'ATT_Error({ATT_PDU.error_name(self.error_code)})' + + +# ----------------------------------------------------------------------------- +# Attribute Protocol +# ----------------------------------------------------------------------------- +class ATT_PDU: + ''' + See Bluetooth spec @ Vol 3, Part F - 3.3 ATTRIBUTE PDU + ''' + pdu_classes = {} + op_code = 0 + + @staticmethod + def from_bytes(pdu): + op_code = pdu[0] + + cls = ATT_PDU.pdu_classes.get(op_code) + if cls is None: + instance = ATT_PDU(pdu) + instance.name = ATT_PDU.pdu_name(op_code) + instance.op_code = op_code + return instance + self = cls.__new__(cls) + ATT_PDU.__init__(self, pdu) + if hasattr(self, 'fields'): + self.init_from_bytes(pdu, 1) + return self + + @staticmethod + def pdu_name(op_code): + return name_or_number(ATT_PDU_NAMES, op_code, 2) + + @staticmethod + def error_name(error_code): + return name_or_number(ATT_ERROR_NAMES, error_code, 2) + + @staticmethod + def subclass(fields): + def inner(cls): + cls.name = cls.__name__.upper() + cls.op_code = key_with_value(ATT_PDU_NAMES, cls.name) + if cls.op_code is None: + raise KeyError(f'PDU name {cls.name} not found in ATT_PDU_NAMES') + cls.fields = fields + + # Register a factory for this class + ATT_PDU.pdu_classes[cls.op_code] = cls + + return cls + + return inner + + def __init__(self, pdu=None, **kwargs): + if hasattr(self, 'fields') and kwargs: + HCI_Object.init_from_fields(self, self.fields, kwargs) + if pdu is None: + pdu = bytes([self.op_code]) + HCI_Object.dict_to_bytes(kwargs, self.fields) + self.pdu = pdu + + def init_from_bytes(self, pdu, offset): + return HCI_Object.init_from_bytes(self, pdu, offset, self.fields) + + def to_bytes(self): + return self.pdu + + @property + def is_command(self): + return ((self.op_code >> 6) & 1) == 1 + + @property + def has_authentication_signature(self): + return ((self.op_code >> 7) & 1) == 1 + + def __bytes__(self): + return self.to_bytes() + + def __str__(self): + result = color(self.name, 'yellow') + if fields := getattr(self, 'fields', None): + result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, ' ') + else: + if len(self.pdu) > 1: + result += f': {self.pdu.hex()}' + return result + + +# ----------------------------------------------------------------------------- +@ATT_PDU.subclass([ + ('request_opcode_in_error', {'size': 1, 'mapper': ATT_PDU.pdu_name}), + ('attribute_handle_in_error', HANDLE_FIELD_SPEC), + ('error_code', {'size': 1, 'mapper': ATT_PDU.error_name}) +]) +class ATT_Error_Response(ATT_PDU): + ''' + See Bluetooth spec @ Vol 3, Part F - 3.4.1.1 Error Response + ''' + + +# ----------------------------------------------------------------------------- +@ATT_PDU.subclass([ + ('client_rx_mtu', 2) +]) +class ATT_Exchange_MTU_Request(ATT_PDU): + ''' + See Bluetooth spec @ Vol 3, Part F - 3.4.2.1 Exchange MTU Request + ''' + + +# ----------------------------------------------------------------------------- +@ATT_PDU.subclass([ + ('server_rx_mtu', 2) +]) +class ATT_Exchange_MTU_Response(ATT_PDU): + ''' + See Bluetooth spec @ Vol 3, Part F - 3.4.2.2 Exchange MTU Response + ''' + + +# ----------------------------------------------------------------------------- +@ATT_PDU.subclass([ + ('starting_handle', HANDLE_FIELD_SPEC), + ('ending_handle', HANDLE_FIELD_SPEC) +]) +class ATT_Find_Information_Request(ATT_PDU): + ''' + See Bluetooth spec @ Vol 3, Part F - 3.4.3.1 Find Information Request + ''' + + +# ----------------------------------------------------------------------------- +@ATT_PDU.subclass([ + ('format', 1), + ('information_data', '*') +]) +class ATT_Find_Information_Response(ATT_PDU): + ''' + See Bluetooth spec @ Vol 3, Part F - 3.4.3.2 Find Information Response + ''' + + def parse_information_data(self): + self.information = [] + offset = 0 + uuid_size = 2 if self.format == 1 else 16 + while offset + uuid_size <= len(self.information_data): + handle = struct.unpack_from(' 0: + value_string = f', value={self.value.hex()}' + else: + value_string = '' + return f'Attribute(handle=0x{self.handle:04X}, type={self.type}, permissions={self.permissions}{value_string})' diff --git a/bumble/avdtp.py b/bumble/avdtp.py new file mode 100644 index 0000000..759e38c --- /dev/null +++ b/bumble/avdtp.py @@ -0,0 +1,1921 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import struct +import time +import logging +from colors import color +from pyee import EventEmitter + +from .core import ( + BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE, + InvalidStateError, + ProtocolError, + name_or_number +) +from .a2dp import ( + A2DP_CODEC_TYPE_NAMES, + A2DP_MPEG_2_4_AAC_CODEC_TYPE, + A2DP_NON_A2DP_CODEC_TYPE, + A2DP_SBC_CODEC_TYPE, + AacMediaCodecInformation, + SbcMediaCodecInformation, + VendorSpecificMediaCodecInformation +) +from . import sdp + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +AVDTP_PSM = 0x0019 + +AVDTP_DEFAULT_RTX_SIG_TIMER = 5 # Seconds + +# Signal Identifiers (AVDTP spec - 8.5 Signal Command Set) +AVDTP_DISCOVER = 0x01 +AVDTP_GET_CAPABILITIES = 0x02 +AVDTP_SET_CONFIGURATION = 0x03 +AVDTP_GET_CONFIGURATION = 0x04 +AVDTP_RECONFIGURE = 0x05 +AVDTP_OPEN = 0x06 +AVDTP_START = 0x07 +AVDTP_CLOSE = 0x08 +AVDTP_SUSPEND = 0x09 +AVDTP_ABORT = 0x0A +AVDTP_SECURITY_CONTROL = 0x0B +AVDTP_GET_ALL_CAPABILITIES = 0x0C +AVDTP_DELAYREPORT = 0x0D + +AVDTP_SIGNAL_NAMES = { + AVDTP_DISCOVER: 'AVDTP_DISCOVER', + AVDTP_GET_CAPABILITIES: 'AVDTP_GET_CAPABILITIES', + AVDTP_SET_CONFIGURATION: 'AVDTP_SET_CONFIGURATION', + AVDTP_GET_CONFIGURATION: 'AVDTP_GET_CONFIGURATION', + AVDTP_RECONFIGURE: 'AVDTP_RECONFIGURE', + AVDTP_OPEN: 'AVDTP_OPEN', + AVDTP_START: 'AVDTP_START', + AVDTP_CLOSE: 'AVDTP_CLOSE', + AVDTP_SUSPEND: 'AVDTP_SUSPEND', + AVDTP_ABORT: 'AVDTP_ABORT', + AVDTP_SECURITY_CONTROL: 'AVDTP_SECURITY_CONTROL', + AVDTP_GET_ALL_CAPABILITIES: 'AVDTP_GET_ALL_CAPABILITIES', + AVDTP_DELAYREPORT: 'AVDTP_DELAYREPORT' +} + +AVDTP_SIGNAL_IDENTIFIERS = { + 'AVDTP_DISCOVER': AVDTP_DISCOVER, + 'AVDTP_GET_CAPABILITIES': AVDTP_GET_CAPABILITIES, + 'AVDTP_SET_CONFIGURATION': AVDTP_SET_CONFIGURATION, + 'AVDTP_GET_CONFIGURATION': AVDTP_GET_CONFIGURATION, + 'AVDTP_RECONFIGURE': AVDTP_RECONFIGURE, + 'AVDTP_OPEN': AVDTP_OPEN, + 'AVDTP_START': AVDTP_START, + 'AVDTP_CLOSE': AVDTP_CLOSE, + 'AVDTP_SUSPEND': AVDTP_SUSPEND, + 'AVDTP_ABORT': AVDTP_ABORT, + 'AVDTP_SECURITY_CONTROL': AVDTP_SECURITY_CONTROL, + 'AVDTP_GET_ALL_CAPABILITIES': AVDTP_GET_ALL_CAPABILITIES, + 'AVDTP_DELAYREPORT': AVDTP_DELAYREPORT +} + +# Error codes (AVDTP spec - 8.20.6.2 ERROR_CODE tables) +AVDTP_BAD_HEADER_FORMAT_ERROR = 0x01 +AVDTP_BAD_LENGTH_ERROR = 0x11 +AVDTP_BAD_ACP_SEID_ERROR = 0x12 +AVDTP_SEP_IN_USE_ERROR = 0x13 +AVDTP_SEP_NOT_IN_USE_ERROR = 0x14 +AVDTP_BAD_SERV_CATEGORY_ERROR = 0x17 +AVDTP_BAD_PAYLOAD_FORMAT_ERROR = 0x18 +AVDTP_NOT_SUPPORTED_COMMAND_ERROR = 0x19 +AVDTP_INVALID_CAPABILITIES_ERROR = 0x1A +AVDTP_BAD_RECOVERY_TYPE_ERROR = 0x22 +AVDTP_BAD_MEDIA_TRANSPORT_FORMAT_ERROR = 0x23 +AVDTP_BAD_RECOVERY_FORMAT_ERROR = 0x25 +AVDTP_BAD_ROHC_FORMAT_ERROR = 0x26 +AVDTP_BAD_CP_FORMAT_ERROR = 0x27 +AVDTP_BAD_MULTIPLEXING_FORMAT_ERROR = 0x28 +AVDTP_UNSUPPORTED_CONFIGURATION_ERROR = 0x29 +AVDTP_BAD_STATE_ERROR = 0x31 + +AVDTP_ERROR_NAMES = { + AVDTP_BAD_HEADER_FORMAT_ERROR: 'AVDTP_BAD_HEADER_FORMAT_ERROR', + AVDTP_BAD_LENGTH_ERROR: 'AVDTP_BAD_LENGTH_ERROR', + AVDTP_BAD_ACP_SEID_ERROR: 'AVDTP_BAD_ACP_SEID_ERROR', + AVDTP_SEP_IN_USE_ERROR: 'AVDTP_SEP_IN_USE_ERROR', + AVDTP_SEP_NOT_IN_USE_ERROR: 'AVDTP_SEP_NOT_IN_USE_ERROR', + AVDTP_BAD_SERV_CATEGORY_ERROR: 'AVDTP_BAD_SERV_CATEGORY_ERROR', + AVDTP_BAD_PAYLOAD_FORMAT_ERROR: 'AVDTP_BAD_PAYLOAD_FORMAT_ERROR', + AVDTP_NOT_SUPPORTED_COMMAND_ERROR: 'AVDTP_NOT_SUPPORTED_COMMAND_ERROR', + AVDTP_INVALID_CAPABILITIES_ERROR: 'AVDTP_INVALID_CAPABILITIES_ERROR', + AVDTP_BAD_RECOVERY_TYPE_ERROR: 'AVDTP_BAD_RECOVERY_TYPE_ERROR', + AVDTP_BAD_MEDIA_TRANSPORT_FORMAT_ERROR: 'AVDTP_BAD_MEDIA_TRANSPORT_FORMAT_ERROR', + AVDTP_BAD_RECOVERY_FORMAT_ERROR: 'AVDTP_BAD_RECOVERY_FORMAT_ERROR', + AVDTP_BAD_ROHC_FORMAT_ERROR: 'AVDTP_BAD_ROHC_FORMAT_ERROR', + AVDTP_BAD_CP_FORMAT_ERROR: 'AVDTP_BAD_CP_FORMAT_ERROR', + AVDTP_BAD_MULTIPLEXING_FORMAT_ERROR: 'AVDTP_BAD_MULTIPLEXING_FORMAT_ERROR', + AVDTP_UNSUPPORTED_CONFIGURATION_ERROR: 'AVDTP_UNSUPPORTED_CONFIGURATION_ERROR', + AVDTP_BAD_STATE_ERROR: 'AVDTP_BAD_STATE_ERROR' +} + +AVDTP_AUDIO_MEDIA_TYPE = 0x00 +AVDTP_VIDEO_MEDIA_TYPE = 0x01 +AVDTP_MULTIMEDIA_MEDIA_TYPE = 0x02 + +AVDTP_MEDIA_TYPE_NAMES = { + AVDTP_AUDIO_MEDIA_TYPE: 'AVDTP_AUDIO_MEDIA_TYPE', + AVDTP_VIDEO_MEDIA_TYPE: 'AVDTP_VIDEO_MEDIA_TYPE', + AVDTP_MULTIMEDIA_MEDIA_TYPE: 'AVDTP_MULTIMEDIA_MEDIA_TYPE' +} + +# TSEP (AVDTP spec - 8.20.3 Stream End-point Type, Source or Sink (TSEP)) +AVDTP_TSEP_SRC = 0x00 +AVDTP_TSEP_SNK = 0x01 + +AVDTP_TSEP_NAMES = { + AVDTP_TSEP_SRC: 'AVDTP_TSEP_SRC', + AVDTP_TSEP_SNK: 'AVDTP_TSEP_SNK' +} + +# Service Categories (AVDTP spec - Table 8.47: Service Category information element field values) +AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY = 0x01 +AVDTP_REPORTING_SERVICE_CATEGORY = 0x02 +AVDTP_RECOVERY_SERVICE_CATEGORY = 0x03 +AVDTP_CONTENT_PROTECTION_SERVICE_CATEGORY = 0x04 +AVDTP_HEADER_COMPRESSION_SERVICE_CATEGORY = 0x05 +AVDTP_MULTIPLEXING_SERVICE_CATEGORY = 0x06 +AVDTP_MEDIA_CODEC_SERVICE_CATEGORY = 0x07 +AVDTP_DELAY_REPORTING_SERVICE_CATEGORY = 0x08 + +AVDTP_SERVICE_CATEGORY_NAMES = { + AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY: 'AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY', + AVDTP_REPORTING_SERVICE_CATEGORY: 'AVDTP_REPORTING_SERVICE_CATEGORY', + AVDTP_RECOVERY_SERVICE_CATEGORY: 'AVDTP_RECOVERY_SERVICE_CATEGORY', + AVDTP_CONTENT_PROTECTION_SERVICE_CATEGORY: 'AVDTP_CONTENT_PROTECTION_SERVICE_CATEGORY', + AVDTP_HEADER_COMPRESSION_SERVICE_CATEGORY: 'AVDTP_HEADER_COMPRESSION_SERVICE_CATEGORY', + AVDTP_MULTIPLEXING_SERVICE_CATEGORY: 'AVDTP_MULTIPLEXING_SERVICE_CATEGORY', + AVDTP_MEDIA_CODEC_SERVICE_CATEGORY: 'AVDTP_MEDIA_CODEC_SERVICE_CATEGORY', + AVDTP_DELAY_REPORTING_SERVICE_CATEGORY: 'AVDTP_DELAY_REPORTING_SERVICE_CATEGORY' +} + +# States (AVDTP spec - 9.1 State Definitions) +AVDTP_IDLE_STATE = 0x00 +AVDTP_CONFIGURED_STATE = 0x01 +AVDTP_OPEN_STATE = 0x02 +AVDTP_STREAMING_STATE = 0x03 +AVDTP_CLOSING_STATE = 0x04 +AVDTP_ABORTING_STATE = 0x05 + +AVDTP_STATE_NAMES = { + AVDTP_IDLE_STATE: 'AVDTP_IDLE_STATE', + AVDTP_CONFIGURED_STATE: 'AVDTP_CONFIGURED_STATE', + AVDTP_OPEN_STATE: 'AVDTP_OPEN_STATE', + AVDTP_STREAMING_STATE: 'AVDTP_STREAMING_STATE', + AVDTP_CLOSING_STATE: 'AVDTP_CLOSING_STATE', + AVDTP_ABORTING_STATE: 'AVDTP_ABORTING_STATE' +} + + +# ----------------------------------------------------------------------------- +async def find_avdtp_service_with_sdp_client(sdp_client): + ''' + Find an AVDTP service, using a connected SDP client, and return its version, + or None if none is found + ''' + + # Search for services with an Audio Sink service class + search_result = await sdp_client.search_attributes( + [BT_ADVANCED_AUDIO_DISTRIBUTION_SERVICE], + [ + sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID + ] + ) + for attribute_list in search_result: + profile_descriptor_list = sdp.ServiceAttribute.find_attribute_in_list( + attribute_list, + sdp.SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID + ) + if profile_descriptor_list: + for profile_descriptor in profile_descriptor_list.value: + if len(profile_descriptor.value) >= 2: + avdtp_version_major = profile_descriptor.value[1].value >> 8 + avdtp_version_minor = profile_descriptor.value[1].value & 0xFF + return (avdtp_version_major, avdtp_version_minor) + + +# ----------------------------------------------------------------------------- +async def find_avdtp_service_with_connection(device, connection): + ''' + Find an AVDTP service, for a connection, and return its version, + or None if none is found + ''' + + sdp_client = sdp.Client(device) + await sdp_client.connect(connection) + service_version = await find_avdtp_service_with_sdp_client(sdp_client) + await sdp_client.disconnect() + + return service_version + + +# ----------------------------------------------------------------------------- +class RealtimeClock: + def now(self): + return time.time() + + async def sleep(self, duration): + await asyncio.sleep(duration) + + +# ----------------------------------------------------------------------------- +class MediaPacket: + @staticmethod + def from_bytes(data): + version = (data[0] >> 6) & 0x03 + padding = (data[0] >> 5) & 0x01 + extension = (data[0] >> 4) & 0x01 + csrc_count = data[0] & 0x0F + marker = (data[1] >> 7) & 0x01 + payload_type = data[1] & 0x7F + sequence_number = struct.unpack_from('>H', data, 2)[0] + timestamp = struct.unpack_from('>I', data, 4)[0] + ssrc = struct.unpack_from('>I', data, 8)[0] + csrc_list = [struct.unpack_from('>I', data, 12 + i)[0] for i in range(csrc_count)] + payload = data[12 + csrc_count * 4:] + + return MediaPacket( + version, + padding, + extension, + marker, + sequence_number, + timestamp, + ssrc, + csrc_list, + payload_type, + payload + ) + + def __init__( + self, + version, + padding, + extension, + marker, + sequence_number, + timestamp, + ssrc, + csrc_list, + payload_type, + payload + ): + self.version = version + self.padding = padding + self.extension = extension + self.marker = marker + self.sequence_number = sequence_number + self.timestamp = timestamp + self.ssrc = ssrc + self.csrc_list = csrc_list + self.payload_type = payload_type + self.payload = payload + + def __bytes__(self): + header = ( + bytes([ + self.version << 6 | self.padding << 5 | self.extension << 4 | len(self.csrc_list), + self.marker << 7 | self.payload_type + ]) + + struct.pack('>HII', self.sequence_number, self.timestamp, self.ssrc) + ) + for csrc in self.csrc_list: + header += struct.pack('>I', csrc) + return header + self.payload + + def __str__(self): + return f'RTP(v={self.version},p={self.padding},x={self.extension},m={self.marker},pt={self.payload_type},sn={self.sequence_number},ts={self.timestamp},ssrc={self.ssrc},csrcs={self.csrc_list},payload_size={len(self.payload)})' + + +# ----------------------------------------------------------------------------- +class MediaPacketPump: + def __init__(self, packets, clock=RealtimeClock()): + self.packets = packets + self.clock = clock + self.pump_task = None + + async def start(self, rtp_channel): + async def pump_packets(): + start_time = 0 + start_timestamp = 0 + + try: + logger.debug('pump starting') + async for packet in self.packets: + # Capture the timestamp of the first packet + if start_time == 0: + start_time = self.clock.now() + start_timestamp = packet.timestamp_seconds + + # Wait until we can send + when = start_time + (packet.timestamp_seconds - start_timestamp) + now = self.clock.now() + if when > now: + delay = when - now + logger.debug(f'waiting for {delay}') + await self.clock.sleep(delay) + + # Emit + rtp_channel.send_pdu(bytes(packet)) + logger.debug(f'{color(">>> sending RTP packet:", "green")} {packet}') + except asyncio.exceptions.CancelledError: + logger.debug('pump canceled') + + # Pump packets + self.pump_task = asyncio.get_running_loop().create_task(pump_packets()) + + async def stop(self): + # Stop the pump + if self.pump_task: + self.pump_task.cancel() + await self.pump_task + self.pump_task = None + + +# ----------------------------------------------------------------------------- +class MessageAssembler: + def __init__(self, callback): + self.callback = callback + self.reset() + + def reset(self): + self.transaction_label = 0 + self.message = None + self.message_type = 0 + self.signal_identifier = 0 + self.number_of_signal_packets = 0 + self.packet_count = 0 + + def on_pdu(self, pdu): + self.packet_count += 1 + + transaction_label = pdu[0] >> 4 + packet_type = (pdu[0] >> 2) & 3 + message_type = pdu[0] & 3 + + logger.debug(f'transaction_label={transaction_label}, packet_type={Protocol.packet_type_name(packet_type)}, message_type={Message.message_type_name(message_type)}') + if packet_type == Protocol.SINGLE_PACKET or packet_type == Protocol.START_PACKET: + if self.message is not None: + # The previous message has not been terminated + logger.warning('received a start or single packet when expecting an end or continuation') + self.reset() + + self.transaction_label = transaction_label + self.signal_identifier = pdu[1] & 0x3F + self.message_type = message_type + + if packet_type == Protocol.SINGLE_PACKET: + self.message = pdu[2:] + self.on_message_complete() + else: + self.number_of_signal_packets = pdu[2] + self.message = pdu[3:] + elif packet_type == Protocol.CONTINUE_PACKET or packet_type == Protocol.END_PACKET: + if self.packet_count == 0: + logger.warning('unexpected continuation') + return + + if transaction_label != self.transaction_label: + logger.warning(f'transaction label mismatch: expected {self.transaction_label}, received {transaction_label}') + return + + if message_type != self.message_type: + logger.warning(f'message type mismatch: expected {self.message_type}, received {message_type}') + return + + self.message += pdu[1:] + + if packet_type == Protocol.END_PACKET: + if self.packet_count != self.number_of_signal_packets: + logger.warning(f'incomplete fragmented message: expected {self.number_of_signal_packets} packets, received {self.packet_count}') + self.reset() + return + + self.on_message_complete() + else: + if self.packet_count > self.number_of_signal_packets: + logger.warning(f'too many packets: expected {self.number_of_signal_packets}, received {self.packet_count}') + self.reset() + return + + def on_message_complete(self): + message = Message.create(self.signal_identifier, self.message_type, self.message) + + try: + self.callback(self.transaction_label, message) + except Exception as error: + logger.warning(color(f'!!! exception in callback: {error}')) + + self.reset() + + +# ----------------------------------------------------------------------------- +class ServiceCapabilities: + @staticmethod + def create(service_category, service_capabilities_bytes): + # Select the appropriate subclass + if service_category == AVDTP_MEDIA_CODEC_SERVICE_CATEGORY: + cls = MediaCodecCapabilities + else: + cls = ServiceCapabilities + + # Create an instance and initialize it + instance = cls.__new__(cls) + instance.service_category = service_category + instance.service_capabilities_bytes = service_capabilities_bytes + instance.init_from_bytes() + + return instance + + @staticmethod + def parse_capabilities(payload): + capabilities = [] + while payload: + service_category = payload[0] + length_of_service_capabilities = payload[1] + service_capabilities_bytes = payload[2:2 + length_of_service_capabilities] + capabilities.append(ServiceCapabilities.create(service_category, service_capabilities_bytes)) + + payload = payload[2 + length_of_service_capabilities:] + + return capabilities + + @staticmethod + def serialize_capabilities(capabilities): + serialized = b'' + for item in capabilities: + serialized += bytes([ + item.service_category, + len(item.service_capabilities_bytes) + ]) + item.service_capabilities_bytes + return serialized + + def init_from_bytes(self): + pass + + def __init__(self, service_category, service_capabilities_bytes=b''): + self.service_category = service_category + self.service_capabilities_bytes = service_capabilities_bytes + + def to_string(self, details=[]): + attributes = ','.join([name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)] + details) + return f'ServiceCapabilities({attributes})' + + def __str__(self): + if self.service_capabilities_bytes: + details = [self.service_capabilities_bytes.hex()] + else: + details = [] + return self.to_string(details) + + +# ----------------------------------------------------------------------------- +class MediaCodecCapabilities(ServiceCapabilities): + def init_from_bytes(self): + self.media_type = self.service_capabilities_bytes[0] + self.media_codec_type = self.service_capabilities_bytes[1] + self.media_codec_information = self.service_capabilities_bytes[2:] + + if self.media_codec_type == A2DP_SBC_CODEC_TYPE: + self.media_codec_information = SbcMediaCodecInformation.from_bytes(self.media_codec_information) + elif self.media_codec_type == A2DP_MPEG_2_4_AAC_CODEC_TYPE: + self.media_codec_information = AacMediaCodecInformation.from_bytes(self.media_codec_information) + elif self.media_codec_type == A2DP_NON_A2DP_CODEC_TYPE: + self.media_codec_information = VendorSpecificMediaCodecInformation.from_bytes(self.media_codec_information) + + def __init__(self, media_type, media_codec_type, media_codec_information): + super().__init__( + AVDTP_MEDIA_CODEC_SERVICE_CATEGORY, + bytes([media_type, media_codec_type]) + bytes(media_codec_information) + ) + self.media_type = media_type + self.media_codec_type = media_codec_type + self.media_codec_information = media_codec_information + + def __str__(self): + details = [ + f'media_type={name_or_number(AVDTP_MEDIA_TYPE_NAMES, self.media_type)}', + f'codec={name_or_number(A2DP_CODEC_TYPE_NAMES, self.media_codec_type)}', + f'codec_info={self.media_codec_information.hex() if type(self.media_codec_information) is bytes else str(self.media_codec_information)}' + ] + return self.to_string(details) + + +# ----------------------------------------------------------------------------- +class EndPointInfo: + @staticmethod + def from_bytes(payload): + return EndPointInfo( + payload[0] >> 2, + payload[0] >> 1 & 1, + payload[1] >> 4, + payload[1] >> 3 & 1 + ) + + def __bytes__(self): + return bytes([ + self.seid << 2 | self.in_use << 1, + self.media_type << 4 | self.tsep << 3 + ]) + + def __init__(self, seid, in_use, media_type, tsep): + self.seid = seid + self.in_use = in_use + self.media_type = media_type + self.tsep = tsep + + +# ----------------------------------------------------------------------------- +class Message: + COMMAND = 0 + GENERAL_REJECT = 1 + RESPONSE_ACCEPT = 2 + RESPONSE_REJECT = 3 + + MESSAGE_TYPE_NAMES = { + COMMAND: 'COMMAND', + GENERAL_REJECT: 'GENERAL_REJECT', + RESPONSE_ACCEPT: 'RESPONSE_ACCEPT', + RESPONSE_REJECT: 'RESPONSE_REJECT' + } + + subclasses = {} # Subclasses, by signal identifier and message type + + @staticmethod + def message_type_name(message_type): + return name_or_number(Message.MESSAGE_TYPE_NAMES, message_type) + + @staticmethod + def subclass(cls): + # Infer the signal identifier and message subtype from the class name + name = cls.__name__ + if name == 'General_Reject': + cls.signal_identifier = 0 + signal_identifier_str = None + message_type = Message.COMMAND + elif name.endswith('_Command'): + signal_identifier_str = name[:-8] + message_type = Message.COMMAND + elif name.endswith('_Response'): + signal_identifier_str = name[:-9] + message_type = Message.RESPONSE_ACCEPT + elif name.endswith('_Reject'): + signal_identifier_str = name[:-7] + message_type = Message.RESPONSE_REJECT + else: + raise ValueError('invalid class name') + + cls.message_type = message_type + + if signal_identifier_str is not None: + for (name, signal_identifier) in AVDTP_SIGNAL_IDENTIFIERS.items(): + if name.lower().endswith(signal_identifier_str.lower()): + cls.signal_identifier = signal_identifier + break + + # Register the subclass + Message.subclasses.setdefault(cls.signal_identifier, {})[cls.message_type] = cls + + return cls + + # Factory method to create a subclass based on the signal identifier and message type + @staticmethod + def create(signal_identifier, message_type, payload): + # Look for a registered subclass + subclasses = Message.subclasses.get(signal_identifier) + if subclasses: + subclass = subclasses.get(message_type) + if subclass: + instance = subclass.__new__(subclass) + instance.payload = payload + instance.init_from_payload() + return instance + + # Instantiate the appropriate class based on the message type + if message_type == Message.RESPONSE_REJECT: + # Assume a simple reject message + instance = Simple_Reject(payload) + instance.init_from_payload() + else: + instance = Message(payload) + instance.signal_identifier = signal_identifier + instance.message_type = message_type + return instance + + def init_from_payload(self): + pass + + def __init__(self, payload=b''): + self.payload = payload + + def to_string(self, details): + base = f'{color(f"{name_or_number(AVDTP_SIGNAL_NAMES, self.signal_identifier)}_{Message.message_type_name(self.message_type)}", "yellow")}' + if details: + if type(details) is str: + return f'{base}: {details}' + else: + return base + ':\n' + '\n'.join([' ' + color(detail, 'cyan') for detail in details]) + else: + return base + + def __str__(self): + return self.to_string(self.payload.hex()) + + +# ----------------------------------------------------------------------------- +class Simple_Command(Message): + ''' + Command message with just one seid + ''' + + def init_from_payload(self): + self.acp_seid = self.payload[0] >> 2 + + def __init__(self, seid): + self.acp_seid = seid + self.payload = bytes([seid << 2]) + + def __str__(self): + return self.to_string([f'ACP SEID: {self.acp_seid}']) + + +# ----------------------------------------------------------------------------- +class Simple_Reject(Message): + ''' + Reject messages with just an error code + ''' + + def init_from_payload(self): + self.error_code = self.payload[0] + + def __init__(self, error_code): + self.error_code = error_code + self.payload = bytes([self.error_code]) + + def __str__(self): + details = [ + f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}' + ] + return self.to_string(details) + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Discover_Command(Message): + ''' + See Bluetooth AVDTP spec - 8.6.1 Stream End Point Discovery Command + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Discover_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.6.2 Stream End Point Discovery Response + ''' + + def init_from_payload(self): + self.endpoints = [] + endpoint_count = len(self.payload) // 2 + for i in range(endpoint_count): + self.endpoints.append(EndPointInfo.from_bytes(self.payload[i * 2:(i + 1) * 2])) + + def __init__(self, endpoints): + self.endpoints = endpoints + self.payload = b''.join([bytes(endpoint) for endpoint in endpoints]) + + def __str__(self): + details = [] + for endpoint in self.endpoints: + details.extend( + [ + f'ACP SEID: {endpoint.seid}', + f' in_use: {endpoint.in_use}', + f' media_type: {name_or_number(AVDTP_MEDIA_TYPE_NAMES, endpoint.media_type)}', + f' tsep: {name_or_number(AVDTP_TSEP_NAMES, endpoint.tsep)}' + ] + ) + return self.to_string(details) + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Get_Capabilities_Command(Simple_Command): + ''' + See Bluetooth AVDTP spec - 8.7.1 Get Capabilities Command + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Get_Capabilities_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.7.2 Get All Capabilities Response + ''' + + def init_from_payload(self): + self.capabilities = ServiceCapabilities.parse_capabilities(self.payload) + + def __init__(self, capabilities): + self.capabilities = capabilities + self.payload = ServiceCapabilities.serialize_capabilities(capabilities) + + def __str__(self): + details = [str(capability) for capability in self.capabilities] + return self.to_string(details) + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Get_Capabilities_Reject(Simple_Reject): + ''' + See Bluetooth AVDTP spec - 8.7.3 Get Capabilities Reject + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Get_All_Capabilities_Command(Get_Capabilities_Command): + ''' + See Bluetooth AVDTP spec - 8.8.1 Get All Capabilities Command + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Get_All_Capabilities_Response(Get_Capabilities_Response): + ''' + See Bluetooth AVDTP spec - 8.8.2 Get All Capabilities Response + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Get_All_Capabilities_Reject(Simple_Reject): + ''' + See Bluetooth AVDTP spec - 8.8.3 Get All Capabilities Reject + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Set_Configuration_Command(Message): + ''' + See Bluetooth AVDTP spec - 8.9.1 Set Configuration Command + ''' + + def init_from_payload(self): + self.acp_seid = self.payload[0] >> 2 + self.int_seid = self.payload[1] >> 2 + self.capabilities = ServiceCapabilities.parse_capabilities(self.payload[2:]) + + def __init__(self, acp_seid, int_seid, capabilities): + self.acp_seid = acp_seid + self.int_seid = int_seid + self.capabilities = capabilities + self.payload = bytes([acp_seid << 2, int_seid << 2]) + ServiceCapabilities.serialize_capabilities(capabilities) + + def __str__(self): + details = [ + f'ACP SEID: {self.acp_seid}', + f'INT SEID: {self.int_seid}' + ] + [str(capability) for capability in self.capabilities] + return self.to_string(details) + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Set_Configuration_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.9.2 Set Configuration Response + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Set_Configuration_Reject(Message): + ''' + See Bluetooth AVDTP spec - 8.9.3 Set Configuration Reject + ''' + + def init_from_payload(self): + self.service_category = self.payload[0] + self.error_code = self.payload[1] + + def __init__(self, service_category, error_code): + self.service_category = service_category + self.error_code = error_code + self.payload = bytes([service_category, self.error_code]) + + def __str__(self): + details = [ + f'service_category: {name_or_number(AVDTP_SERVICE_CATEGORY_NAMES, self.service_category)}', + f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}' + ] + return self.to_string(details) + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Get_Configuration_Command(Simple_Command): + ''' + See Bluetooth AVDTP spec - 8.10.1 Get Configuration Command + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Get_Configuration_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.10.2 Get Configuration Response + ''' + + def init_from_payload(self): + self.capabilities = ServiceCapabilities.parse_capabilities(self.payload) + + def __init__(self, capabilities): + self.capabilities = capabilities + self.payload = ServiceCapabilities.serialize_capabilities(capabilities) + + def __str__(self): + details = [str(capability) for capability in self.capabilities] + return self.to_string(details) + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Get_Configuration_Reject(Simple_Reject): + ''' + See Bluetooth AVDTP spec - 8.10.3 Get Configuration Reject + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Reconfigure_Command(Message): + ''' + See Bluetooth AVDTP spec - 8.11.1 Reconfigure Command + ''' + + def init_from_payload(self): + self.acp_seid = self.payload[0] >> 2 + self.capabilities = ServiceCapabilities.parse_capabilities(self.payload[1:]) + + def __str__(self): + details = [ + f'ACP SEID: {self.acp_seid}', + ] + [str(capability) for capability in self.capabilities] + return self.to_string(details) + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Reconfigure_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.11.2 Reconfigure Response + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Reconfigure_Reject(Set_Configuration_Reject): + ''' + See Bluetooth AVDTP spec - 8.11.3 Reconfigure Reject + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Open_Command(Simple_Command): + ''' + See Bluetooth AVDTP spec - 8.12.1 Open Stream Command + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Open_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.12.2 Open Stream Response + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Open_Reject(Simple_Reject): + ''' + See Bluetooth AVDTP spec - 8.12.3 Open Stream Reject + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Start_Command(Message): + ''' + See Bluetooth AVDTP spec - 8.13.1 Start Stream Command + ''' + + def init_from_payload(self): + self.acp_seids = [x >> 2 for x in self.payload] + + def __init__(self, seids): + self.acp_seids = seids + self.payload = bytes([seid << 2 for seid in self.acp_seids]) + + def __str__(self): + return self.to_string([f'ACP SEIDs: {self.acp_seids}']) + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Start_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.13.2 Start Stream Response + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Start_Reject(Message): + ''' + See Bluetooth AVDTP spec - 8.13.3 Set Configuration Reject + ''' + + def init_from_payload(self): + self.acp_seid = self.payload[0] >> 2 + self.error_code = self.payload[1] + + def __init__(self, acp_seid, error_code): + self.acp_seid = acp_seid + self.error_code = error_code + self.payload = bytes([self.acp_seid << 2, self.error_code]) + + def __str__(self): + details = [ + f'acp_seid: {self.acp_seid}', + f'error_code: {name_or_number(AVDTP_ERROR_NAMES, self.error_code)}' + ] + return self.to_string(details) + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Close_Command(Simple_Command): + ''' + See Bluetooth AVDTP spec - 8.14.1 Close Stream Command + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Close_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.14.2 Close Stream Response + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Close_Reject(Simple_Reject): + ''' + See Bluetooth AVDTP spec - 8.14.3 Close Stream Reject + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Suspend_Command(Start_Command): + ''' + See Bluetooth AVDTP spec - 8.15.1 Suspend Command + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Suspend_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.15.2 Suspend Response + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Suspend_Reject(Start_Reject): + ''' + See Bluetooth AVDTP spec - 8.15.3 Suspend Reject + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Abort_Command(Simple_Command): + ''' + See Bluetooth AVDTP spec - 8.16.1 Abort Command + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Abort_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.16.2 Abort Response + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Security_Control_Command(Message): + ''' + See Bluetooth AVDTP spec - 8.17.1 Security Control Command + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Security_Control_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.17.2 Security Control Response + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class Security_Control_Reject(Simple_Reject): + ''' + See Bluetooth AVDTP spec - 8.17.3 Security Control Reject + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class General_Reject(Message): + ''' + See Bluetooth AVDTP spec - 8.18 General Reject + ''' + + def to_string(self, details): + return f'{color(f"GENERAL_REJECT", "yellow")}' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class DelayReport_Command(Message): + ''' + See Bluetooth AVDTP spec - 8.19.1 Delay Report Command + ''' + + def init_from_payload(self): + self.acp_seid = self.payload[0] >> 2 + self.delay = (self.payload[1] << 8) | (self.payload[2]) + + def __str__(self): + return self.to_string([ + f'ACP_SEID: {self.acp_seid}', + f'delay: {self.delay}' + ]) + + +# ----------------------------------------------------------------------------- +@Message.subclass +class DelayReport_Response(Message): + ''' + See Bluetooth AVDTP spec - 8.19.2 Delay Report Response + ''' + + +# ----------------------------------------------------------------------------- +@Message.subclass +class DelayReport_Reject(Simple_Reject): + ''' + See Bluetooth AVDTP spec - 8.19.3 Delay Report Reject + ''' + + +# ----------------------------------------------------------------------------- +class Protocol: + SINGLE_PACKET = 0 + START_PACKET = 1 + CONTINUE_PACKET = 2 + END_PACKET = 3 + + PACKET_TYPE_NAMES = { + SINGLE_PACKET: 'SINGLE_PACKET', + START_PACKET: 'START_PACKET', + CONTINUE_PACKET: 'CONTINUE_PACKET', + END_PACKET: 'END_PACKET' + } + + @staticmethod + def packet_type_name(packet_type): + return name_or_number(Protocol.PACKET_TYPE_NAMES, packet_type) + + @staticmethod + async def connect(connection, version=(1, 3)): + connector = connection.create_l2cap_connector(AVDTP_PSM) + channel = await connector() + protocol = Protocol(channel, version) + protocol.channel_connector = connector + + return protocol + + def __init__(self, l2cap_channel, version=(1, 3)): + self.l2cap_channel = l2cap_channel + self.version = version + self.rtx_sig_timer = AVDTP_DEFAULT_RTX_SIG_TIMER + self.message_assembler = MessageAssembler(self.on_message) + self.transaction_results = [None] * 16 # Futures for up to 16 transactions + self.transaction_semaphore = asyncio.Semaphore(16) + self.transaction_count = 0 + self.channel_acceptor = None + self.channel_connector = None + self.local_endpoints = [] # Local endpoints, with contiguous seid values + self.remote_endpoints = {} # Remote stream endpoints, by seid + self.streams = {} # Streams, by seid + + # Register to receive PDUs from the channel + l2cap_channel.sink = self.on_pdu + l2cap_channel.on('open', self.on_l2cap_channel_open) + + def get_local_endpoint_by_seid(self, seid): + if seid > 0 and seid <= len(self.local_endpoints): + return self.local_endpoints[seid - 1] + + def add_source(self, codec_capabilities, packet_pump): + seid = len(self.local_endpoints) + 1 + source = LocalSource(self, seid, codec_capabilities, packet_pump) + self.local_endpoints.append(source) + + return source + + def add_sink(self, codec_capabilities): + seid = len(self.local_endpoints) + 1 + sink = LocalSink(self, seid, codec_capabilities) + self.local_endpoints.append(sink) + + return sink + + async def create_stream(self, source, sink): + # Check that the source isn't already used in a stream + if source.in_use: + raise InvalidStateError('source already in use') + + # Create or reuse a new stream to associate the source and the sink + if source.seid in self.streams: + stream = self.streams[source.seid] + else: + stream = Stream(self, source, sink) + self.streams[source.seid] = stream + + # The stream can now be configured + await stream.configure() + + return stream + + async def discover_remote_endpoints(self): + self.remote_endpoints = {} + + response = await self.send_command(Discover_Command()) + for endpoint_entry in response.endpoints: + logger.debug(f'getting endpoint capabilities for endpoint {endpoint_entry.seid}') + get_capabilities_response = await self.get_capabilities(endpoint_entry.seid) + endpoint = DiscoveredStreamEndPoint( + self, + endpoint_entry.seid, + endpoint_entry.media_type, + endpoint_entry.tsep, + endpoint_entry.in_use, + get_capabilities_response.capabilities + ) + self.remote_endpoints[endpoint_entry.seid] = endpoint + + return self.remote_endpoints.values() + + def find_remote_sink_by_codec(self, media_type, codec_type): + for endpoint in self.remote_endpoints.values(): + if not endpoint.in_use and endpoint.media_type == media_type and endpoint.tsep == AVDTP_TSEP_SNK: + has_media_transport = False + has_codec = False + for capabilities in endpoint.capabilities: + if capabilities.service_category == AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY: + has_media_transport = True + elif capabilities.service_category == AVDTP_MEDIA_CODEC_SERVICE_CATEGORY: + if capabilities.media_type == AVDTP_AUDIO_MEDIA_TYPE and capabilities.media_codec_type == codec_type: + has_codec = True + if has_media_transport and has_codec: + return endpoint + + def on_pdu(self, pdu): + self.message_assembler.on_pdu(pdu) + + def on_message(self, transaction_label, message): + logger.debug(f'{color("<<< Received AVDTP message", "magenta")}: [{transaction_label}] {message}') + + # Check that the identifier is not reserved + if message.signal_identifier == 0: + logger.warning('!!! reserved signal identifier') + return + + # Check that the identifier is valid + if message.signal_identifier < 0 or message.signal_identifier > AVDTP_DELAYREPORT: + logger.warning('!!! invalid signal identifier') + self.send_message(transaction_label, General_Reject()) + + if message.message_type == Message.COMMAND: + # Command + handler_name = f'on_{AVDTP_SIGNAL_NAMES.get(message.signal_identifier,"").replace("AVDTP_","").lower()}_command' + handler = getattr(self, handler_name, None) + if handler: + try: + response = handler(message) + self.send_message(transaction_label, response) + except Exception as error: + logger.warning(f'{color("!!! Exception in handler:", "red")} {error}') + else: + logger.warning('unhandled command') + else: + # Response, look for a pending transaction with the same label + transaction_result = self.transaction_results[transaction_label] + if transaction_result is None: + logger.warning(color('!!! no pending transaction for label', 'red')) + return + + transaction_result.set_result(message) + self.transaction_results[transaction_label] = None + self.transaction_semaphore.release() + + def on_l2cap_connection(self, channel): + # Forward the channel to the endpoint that's expecting it + if self.channel_acceptor: + self.channel_acceptor.on_l2cap_connection(channel) + + def on_l2cap_channel_open(self): + logger.debug(color('<<< L2CAP channel open', 'magenta')) + + def send_message(self, transaction_label, message): + logger.debug(f'{color(">>> Sending AVDTP message", "magenta")}: [{transaction_label}] {message}') + max_fragment_size = self.l2cap_channel.mtu - 3 # Enough space for a 3-byte start packet header + payload = message.payload + if len(payload) + 2 <= self.l2cap_channel.mtu: + # Fits in a single packet + packet_type = self.SINGLE_PACKET + else: + packet_type = self.START_PACKET + + done = False + while not done: + first_header_byte = transaction_label << 4 | packet_type << 2 | message.message_type + + if packet_type == self.SINGLE_PACKET: + header = bytes([first_header_byte, message.signal_identifier]) + elif packet_type == self.START_PACKET: + packet_count = (max_fragment_size - 1 + len(payload)) // max_fragment_size + header = bytes([first_header_byte, message.signal_identifier, packet_count]) + else: + header = bytes([first_header_byte]) + + # Send one packet + self.l2cap_channel.send_pdu(header + payload[:max_fragment_size]) + + # Prepare for the next packet + payload = payload[max_fragment_size:] + if payload: + packet_type = self.CONTINUE_PACKET if payload > max_fragment_size else self.END_PACKET + else: + done = True + + async def send_command(self, command): + # TODO: support timeouts + # Send the command + (transaction_label, transaction_result) = await self.start_transaction() + self.send_message(transaction_label, command) + + # Wait for the response + response = await transaction_result + + # Check for errors + if response.message_type == Message.GENERAL_REJECT or response.message_type == Message.RESPONSE_REJECT: + raise ProtocolError(response.error_code, 'avdtp') + + return response + + async def start_transaction(self): + # Wait until we can start a new transaction + await self.transaction_semaphore.acquire() + + # Look for the next free entry to store the transaction result + for i in range(16): + transaction_label = (self.transaction_count + i) % 16 + if self.transaction_results[transaction_label] is None: + transaction_result = asyncio.get_running_loop().create_future() + self.transaction_results[transaction_label] = transaction_result + self.transaction_count += 1 + return (transaction_label, transaction_result) + + assert(False) # Should never reach this + + async def get_capabilities(self, seid): + if self.version > (1, 2): + return await self.send_command(Get_All_Capabilities_Command(seid)) + else: + return await self.send_command(Get_Capabilities_Command(seid)) + + async def set_configuration(self, acp_seid, int_seid, capabilities): + return await self.send_command(Set_Configuration_Command(acp_seid, int_seid, capabilities)) + + async def get_configuration(self, seid): + response = await self.send_command(Get_Configuration_Command(seid)) + return response.capabilities + + async def open(self, seid): + return await self.send_command(Open_Command(seid)) + + async def start(self, seids): + return await self.send_command(Start_Command(seids)) + + async def suspend(self, seids): + return await self.send_command(Suspend_Command(seids)) + + async def close(self, seid): + return await self.send_command(Close_Command(seid)) + + async def abort(self, seid): + return await self.send_command(Abort_Command(seid)) + + def on_discover_command(self, command): + endpoint_infos = [ + EndPointInfo(endpoint.seid, 0, endpoint.media_type, endpoint.tsep) + for endpoint in self.local_endpoints + ] + return Discover_Response(endpoint_infos) + + def on_get_capabilities_command(self, command): + endpoint = self.get_local_endpoint_by_seid(command.acp_seid) + if endpoint is None: + return Get_Capabilities_Reject(AVDTP_BAD_ACP_SEID_ERROR) + + return Get_Capabilities_Response(endpoint.capabilities) + + def on_get_all_capabilities_command(self, command): + endpoint = self.get_local_endpoint_by_seid(command.acp_seid) + if endpoint is None: + return Get_All_Capabilities_Reject(AVDTP_BAD_ACP_SEID_ERROR) + + return Get_All_Capabilities_Response(endpoint.capabilities) + + def on_set_configuration_command(self, command): + endpoint = self.get_local_endpoint_by_seid(command.acp_seid) + if endpoint is None: + return Set_Configuration_Reject(AVDTP_BAD_ACP_SEID_ERROR) + + # Check that the local endpoint isn't in use + if endpoint.in_use: + return Set_Configuration_Reject(AVDTP_SEP_IN_USE_ERROR) + + # Create a stream object for the pair of endpoints + stream = Stream(self, endpoint, StreamEndPointProxy(self, command.int_seid)) + self.streams[command.acp_seid] = stream + + result = stream.on_set_configuration_command(command.capabilities) + return result or Set_Configuration_Response() + + def on_get_configuration_command(self, command): + endpoint = self.get_local_endpoint_by_seid(command.acp_seid) + if endpoint is None: + return Get_Configuration_Reject(AVDTP_BAD_ACP_SEID_ERROR) + if endpoint.stream is None: + return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR) + + return endpoint.stream.on_get_configuration_command() + + def on_reconfigure_command(self, command): + endpoint = self.get_local_endpoint_by_seid(command.acp_seid) + if endpoint is None: + return Reconfigure_Reject(0, AVDTP_BAD_ACP_SEID_ERROR) + if endpoint.stream is None: + return Reconfigure_Reject(0, AVDTP_BAD_STATE_ERROR) + + result = endpoint.stream.on_reconfigure_command(command.capabilities) + return result or Reconfigure_Response() + + def on_open_command(self, command): + endpoint = self.get_local_endpoint_by_seid(command.acp_seid) + if endpoint is None: + return Open_Reject(AVDTP_BAD_ACP_SEID_ERROR) + if endpoint.stream is None: + return Open_Reject(AVDTP_BAD_STATE_ERROR) + + result = endpoint.stream.on_open_command() + return result or Open_Response() + + def on_start_command(self, command): + for seid in command.acp_seids: + endpoint = self.get_local_endpoint_by_seid(seid) + if endpoint is None: + return Start_Reject(seid, AVDTP_BAD_ACP_SEID_ERROR) + if endpoint.stream is None: + return Start_Reject(AVDTP_BAD_STATE_ERROR) + + # Start all streams + # TODO: deal with partial failures + for seid in command.acp_seids: + endpoint = self.get_local_endpoint_by_seid(seid) + result = endpoint.stream.on_start_command() + if result is not None: + return result + + return Start_Response() + + def on_suspend_command(self, command): + for seid in command.acp_seids: + endpoint = self.get_local_endpoint_by_seid(seid) + if endpoint is None: + return Suspend_Reject(seid, AVDTP_BAD_ACP_SEID_ERROR) + if endpoint.stream is None: + return Suspend_Reject(seid, AVDTP_BAD_STATE_ERROR) + + # Suspend all streams + # TODO: deal with partial failures + for seid in command.acp_seids: + endpoint = self.get_local_endpoint_by_seid(seid) + result = endpoint.stream.on_suspend_command() + if result is not None: + return result + + return Suspend_Response() + + def on_close_command(self, command): + endpoint = self.get_local_endpoint_by_seid(command.acp_seid) + if endpoint is None: + return Close_Reject(AVDTP_BAD_ACP_SEID_ERROR) + if endpoint.stream is None: + return Close_Reject(AVDTP_BAD_STATE_ERROR) + + result = endpoint.stream.on_close_command() + return result or Close_Response() + + def on_abort_command(self, command): + endpoint = self.get_local_endpoint_by_seid(command.acp_seid) + if endpoint is None or endpoint.stream is None: + return Abort_Response() + + endpoint.stream.on_abort_command() + return Abort_Response() + + def on_security_control_command(self, command): + endpoint = self.get_local_endpoint_by_seid(command.acp_seid) + if endpoint is None: + return Security_Control_Reject(AVDTP_BAD_ACP_SEID_ERROR) + + result = endpoint.on_security_control_command(command.payload) + return result or Security_Control_Response() + + def on_delayreport_command(self, command): + endpoint = self.get_local_endpoint_by_seid(command.acp_seid) + if endpoint is None: + return DelayReport_Reject(AVDTP_BAD_ACP_SEID_ERROR) + + result = endpoint.on_delayreport_command(command.delay) + return result or DelayReport_Response() + + +# ----------------------------------------------------------------------------- +class Listener(EventEmitter): + @staticmethod + def create_registrar(device): + return device.create_l2cap_registrar(AVDTP_PSM) + + def set_server(self, connection, server): + self.servers[connection.handle] = server + + def __init__(self, registrar, version=(1, 3)): + super().__init__() + self.version = version + self.servers = {} # Servers, by connection handle + + # Listen for incoming L2CAP connections + registrar(self.on_l2cap_connection) + + def on_l2cap_connection(self, channel): + logger.debug(f'{color("<<< incoming L2CAP connection:", "magenta")} {channel}') + + if channel.connection.handle in self.servers: + # This is a channel for a stream endpoint + server = self.servers[channel.connection.handle] + server.on_l2cap_connection(channel) + else: + # This is a new command/response channel + def on_channel_open(): + server = Protocol(channel, self.version) + self.set_server(channel.connection, server) + self.emit('connection', server) + channel.on('open', on_channel_open) + + +# ----------------------------------------------------------------------------- +class Stream: + ''' + Pair of a local and a remote stream endpoint that can stream from one to the other + ''' + + @staticmethod + def state_name(state): + return name_or_number(AVDTP_STATE_NAMES, state) + + def change_state(self, state): + logger.debug(f'{self} state change -> {color(self.state_name(state), "cyan")}') + self.state = state + + def send_media_packet(self, packet): + self.rtp_channel.send_pdu(bytes(packet)) + + async def configure(self): + if self.state != AVDTP_IDLE_STATE: + raise InvalidStateError('current state is not IDLE') + + await self.remote_endpoint.set_configuration( + self.local_endpoint.seid, + self.local_endpoint.configuration + ) + self.change_state(AVDTP_CONFIGURED_STATE) + + async def open(self): + if self.state != AVDTP_CONFIGURED_STATE: + raise InvalidStateError('current state is not CONFIGURED') + + logger.debug('opening remote endpoint') + await self.remote_endpoint.open() + + self.change_state(AVDTP_OPEN_STATE) + + # Create a channel for RTP packets + self.rtp_channel = await self.protocol.channel_connector() + + async def start(self): + # Auto-open if needed + if self.state == AVDTP_CONFIGURED_STATE: + await self.open() + + if self.state != AVDTP_OPEN_STATE: + raise InvalidStateError('current state is not OPEN') + + logger.debug('starting remote endpoint') + await self.remote_endpoint.start() + + logger.debug('starting local endpoint') + await self.local_endpoint.start() + + self.change_state(AVDTP_STREAMING_STATE) + + async def stop(self): + if self.state != AVDTP_STREAMING_STATE: + raise InvalidStateError('current state is not STREAMING') + + logger.debug('stopping local endpoint') + await self.local_endpoint.stop() + + logger.debug('stopping remote endpoint') + await self.remote_endpoint.stop() + + self.change_state(AVDTP_OPEN_STATE) + + async def close(self): + if self.state not in {AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE}: + raise InvalidStateError('current state is not OPEN or STREAMING') + + logger.debug('closing local endpoint') + await self.local_endpoint.close() + + logger.debug('closing remote endpoint') + await self.remote_endpoint.close() + + # Release any channels we may have created + self.change_state(AVDTP_CLOSING_STATE) + if self.rtp_channel: + await self.rtp_channel.disconnect() + self.rtp_channel = None + + # Release the endpoint + self.local_endpoint.in_use = 0 + + self.change_state(AVDTP_IDLE_STATE) + + def on_set_configuration_command(self, configuration): + if self.state != AVDTP_IDLE_STATE: + return Set_Configuration_Reject(AVDTP_BAD_STATE_ERROR) + + result = self.local_endpoint.on_set_configuration_command(configuration) + if result is not None: + return result + + self.change_state(AVDTP_CONFIGURED_STATE) + + def on_get_configuration_command(self, configuration): + if self.state not in {AVDTP_CONFIGURED_STATE, AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE}: + return Get_Configuration_Reject(AVDTP_BAD_STATE_ERROR) + + return self.local_endpoint.on_get_configuration_command(configuration) + + def on_reconfigure_command(self, configuration): + if self.state != AVDTP_OPEN_STATE: + return Reconfigure_Reject(AVDTP_BAD_STATE_ERROR) + + result = self.local_endpoint.on_reconfigure_command(configuration) + if result is not None: + return result + + def on_open_command(self): + if self.state != AVDTP_CONFIGURED_STATE: + return Open_Reject(AVDTP_BAD_STATE_ERROR) + + result = self.local_endpoint.on_open_command() + if result is not None: + return result + + # Register to accept the next channel + self.protocol.channel_acceptor = self + + self.change_state(AVDTP_OPEN_STATE) + + def on_start_command(self): + if self.state != AVDTP_OPEN_STATE: + return Open_Reject(AVDTP_BAD_STATE_ERROR) + + # Check that we have an RTP channel + if self.rtp_channel is None: + logger.warning('received start command before RTP channel establishment') + return Open_Reject(AVDTP_BAD_STATE_ERROR) + + result = self.local_endpoint.on_start_command() + if result is not None: + return result + + self.change_state(AVDTP_STREAMING_STATE) + + def on_suspend_command(self): + if self.state != AVDTP_STREAMING_STATE: + return Open_Reject(AVDTP_BAD_STATE_ERROR) + + result = self.local_endpoint.on_suspend_command() + if result is not None: + return result + + self.change_state(AVDTP_OPEN_STATE) + + def on_close_command(self): + if self.state not in {AVDTP_OPEN_STATE, AVDTP_STREAMING_STATE}: + return Open_Reject(AVDTP_BAD_STATE_ERROR) + + result = self.local_endpoint.on_close_command() + if result is not None: + return result + + self.change_state(AVDTP_CLOSING_STATE) + + if self.rtp_channel is None: + # No channel to release, we're done + self.change_state(AVDTP_IDLE_STATE) + else: + # TODO: set a timer as we wait for the RTP channel to be closed + pass + + def on_abort_command(self): + if self.rtp_channel is None: + # No need to wait + self.change_state(AVDTP_IDLE_STATE) + else: + # Wait for the RTP channel to be closed + self.change_state(AVDTP_ABORTING_STATE) + + def on_l2cap_connection(self, channel): + logger.debug(color('<<< stream channel connected', 'magenta')) + self.rtp_channel = channel + channel.on('open', self.on_l2cap_channel_open) + channel.on('close', self.on_l2cap_channel_close) + + # We don't need more channels + self.protocol.channel_acceptor = None + + def on_l2cap_channel_open(self): + logger.debug(color('<<< stream channel open', 'magenta')) + self.local_endpoint.on_rtp_channel_open() + + def on_l2cap_channel_close(self): + logger.debug(color('<<< stream channel closed', 'magenta')) + self.local_endpoint.on_rtp_channel_close() + self.local_endpoint.in_use = 0 + self.rtp_channel = None + + if self.state in {AVDTP_CLOSING_STATE, AVDTP_ABORTING_STATE}: + self.change_state(AVDTP_IDLE_STATE) + else: + logger.warning('unexpected channel close while not CLOSING or ABORTING') + + def __init__(self, protocol, local_endpoint, remote_endpoint): + ''' + remote_endpoint must be a subclass of StreamEndPointProxy + + ''' + self.protocol = protocol + self.local_endpoint = local_endpoint + self.remote_endpoint = remote_endpoint + self.rtp_channel = None + self.state = AVDTP_IDLE_STATE + + local_endpoint.stream = self + local_endpoint.in_use = 1 + + def __str__(self): + return f'Stream({self.local_endpoint.seid} -> {self.remote_endpoint.seid} {self.state_name(self.state)})' + + +# ----------------------------------------------------------------------------- +class StreamEndPoint: + def __init__(self, seid, media_type, tsep, in_use, capabilities): + self.seid = seid + self.media_type = media_type + self.tsep = tsep + self.in_use = in_use + self.capabilities = capabilities + + def __str__(self): + return '\n'.join([ + 'SEP(', + f' seid={self.seid}', + f' media_type={name_or_number(AVDTP_MEDIA_TYPE_NAMES, self.media_type)}', + f' tsep={name_or_number(AVDTP_TSEP_NAMES, self.tsep)}', + f' in_use={self.in_use}', + ' capabilities=[', + '\n'.join([f' {x}' for x in self.capabilities]), + ' ]', + ')' + ]) + + +# ----------------------------------------------------------------------------- +class StreamEndPointProxy: + def __init__(self, protocol, seid): + self.seid = seid + self.protocol = protocol + + async def set_configuration(self, int_seid, configuration): + return await self.protocol.set_configuration( + self.seid, + int_seid, + configuration + ) + + async def open(self): + return await self.protocol.open(self.seid) + + async def start(self): + return await self.protocol.start([self.seid]) + + async def stop(self): + return await self.protocol.suspend([self.seid]) + + async def close(self): + return await self.protocol.close(self.seid) + + async def abort(self): + return await self.protocol.abort(self.seid) + + +# ----------------------------------------------------------------------------- +class DiscoveredStreamEndPoint(StreamEndPoint, StreamEndPointProxy): + def __init__(self, protocol, seid, media_type, tsep, in_use, capabilities): + StreamEndPoint.__init__(self, seid, media_type, tsep, in_use, capabilities) + StreamEndPointProxy.__init__(self, protocol, seid) + + +# ----------------------------------------------------------------------------- +class LocalStreamEndPoint(StreamEndPoint): + def __init__(self, protocol, seid, media_type, tsep, capabilities, configuration=[]): + super().__init__(seid, media_type, tsep, 0, capabilities) + self.protocol = protocol + self.configuration = configuration + self.stream = None + + async def start(self): + pass + + async def stop(self): + pass + + async def close(self): + pass + + def on_reconfigure_command(self, command): + pass + + def on_get_configuration_command(self): + return Get_Configuration_Response(self.configuration) + + def on_open_command(self): + pass + + def on_start_command(self): + pass + + def on_suspend_command(self): + pass + + def on_close_command(self): + pass + + def on_abort_command(self): + pass + + def on_rtp_channel_open(self): + pass + + def on_rtp_channel_close(self): + pass + + +# ----------------------------------------------------------------------------- +class LocalSource(LocalStreamEndPoint, EventEmitter): + def __init__(self, protocol, seid, codec_capabilities, packet_pump): + capabilities = [ + ServiceCapabilities(AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY), + codec_capabilities + ] + LocalStreamEndPoint.__init__(self, protocol, seid, codec_capabilities.media_type, AVDTP_TSEP_SRC, capabilities, capabilities) + EventEmitter.__init__(self) + self.packet_pump = packet_pump + + async def start(self): + if self.packet_pump: + return await self.packet_pump.start(self.stream.rtp_channel) + else: + self.emit('start', self.stream.rtp_channel) + + async def stop(self): + if self.packet_pump: + return await self.packet_pump.stop() + else: + self.emit('stop') + + def on_set_configuration_command(self, configuration): + # For now, blindly accept the configuration + logger.debug(f'<<< received source configuration: {configuration}') + self.configuration = configuration + + def on_start_command(self): + asyncio.get_running_loop().create_task(self.start()) + + def on_suspend_command(self): + asyncio.get_running_loop().create_task(self.stop()) + + +# ----------------------------------------------------------------------------- +class LocalSink(LocalStreamEndPoint, EventEmitter): + def __init__(self, protocol, seid, codec_capabilities): + capabilities = [ + ServiceCapabilities(AVDTP_MEDIA_TRANSPORT_SERVICE_CATEGORY), + codec_capabilities + ] + LocalStreamEndPoint.__init__(self, protocol, seid, codec_capabilities.media_type, AVDTP_TSEP_SNK, capabilities) + EventEmitter.__init__(self) + + def on_set_configuration_command(self, configuration): + # For now, blindly accept the configuration + logger.debug(f'<<< received sink configuration: {configuration}') + self.configuration = configuration + + def on_rtp_channel_open(self): + logger.debug(color('<<< RTP channel open', 'magenta')) + self.stream.rtp_channel.sink = self.on_avdtp_packet + + def on_avdtp_packet(self, packet): + rtp_packet = MediaPacket.from_bytes(packet) + logger.debug(f'{color("<<< RTP Packet:", "green")} {rtp_packet} {rtp_packet.payload[:16].hex()}') + self.emit('rtp_packet', rtp_packet) diff --git a/bumble/bridge.py b/bumble/bridge.py new file mode 100644 index 0000000..2b4cd94 --- /dev/null +++ b/bumble/bridge.py @@ -0,0 +1,82 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging + +from .hci import HCI_Packet +from .helpers import PacketTracer + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +class HCI_Bridge: + class Forwarder: + def __init__(self, hci_sink, sender_hci_sink, packet_filter, trace): + self.hci_sink = hci_sink + self.sender_hci_sink = sender_hci_sink + self.packet_filter = packet_filter + self.trace = trace + + def on_packet(self, packet): + # Convert the packet bytes to an object + hci_packet = HCI_Packet.from_bytes(packet) + + # Filter the packet + if self.packet_filter is not None: + filtered = self.packet_filter(hci_packet) + if filtered is not None: + packet, respond_to_sender = filtered + hci_packet = HCI_Packet.from_bytes(packet) + if respond_to_sender: + self.sender_hci_sink.on_packet(packet) + return + + # Analyze the packet + self.trace(hci_packet) + + # Bridge the packet + self.hci_sink.on_packet(packet) + + def __init__( + self, + hci_host_source, + hci_host_sink, + hci_controller_source, + hci_controller_sink, + host_to_controller_filter = None, + controller_to_host_filter = None + ): + tracer = PacketTracer(emit_message=logger.info) + host_to_controller_forwarder = HCI_Bridge.Forwarder( + hci_controller_sink, + hci_host_sink, + host_to_controller_filter, + lambda packet: tracer.trace(packet, 0) + ) + hci_host_source.set_packet_sink(host_to_controller_forwarder) + + controller_to_host_forwarder = HCI_Bridge.Forwarder( + hci_host_sink, + hci_controller_sink, + controller_to_host_filter, + lambda packet: tracer.trace(packet, 1) + ) + hci_controller_source.set_packet_sink(controller_to_host_forwarder) diff --git a/bumble/company_ids.py b/bumble/company_ids.py new file mode 100644 index 0000000..c9c9d1a --- /dev/null +++ b/bumble/company_ids.py @@ -0,0 +1,2708 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# The contents of this file are generated by copy/pasting the output of +# the `generate_company_id_list.py` script +# ----------------------------------------------------------------------------- + +COMPANY_IDENTIFIERS = { + 0x0000: "Ericsson Technology Licensing", + 0x0001: "Nokia Mobile Phones", + 0x0002: "Intel Corp.", + 0x0003: "IBM Corp.", + 0x0004: "Toshiba Corp.", + 0x0005: "3Com", + 0x0006: "Microsoft", + 0x0007: "Lucent", + 0x0008: "Motorola", + 0x0009: "Infineon Technologies AG", + 0x000A: "Qualcomm Technologies International, Ltd. (QTIL)", + 0x000B: "Silicon Wave", + 0x000C: "Digianswer A/S", + 0x000D: "Texas Instruments Inc.", + 0x000E: "Parthus Technologies Inc.", + 0x000F: "Broadcom Corporation", + 0x0010: "Mitel Semiconductor", + 0x0011: "Widcomm, Inc.", + 0x0012: "Zeevo, Inc.", + 0x0013: "Atmel Corporation", + 0x0014: "Mitsubishi Electric Corporation", + 0x0015: "RTX Telecom A/S", + 0x0016: "KC Technology Inc.", + 0x0017: "Newlogic", + 0x0018: "Transilica, Inc.", + 0x0019: "Rohde & Schwarz GmbH & Co. KG", + 0x001A: "TTPCom Limited", + 0x001B: "Signia Technologies, Inc.", + 0x001C: "Conexant Systems Inc.", + 0x001D: "Qualcomm", + 0x001E: "Inventel", + 0x001F: "AVM Berlin", + 0x0020: "BandSpeed, Inc.", + 0x0021: "Mansella Ltd", + 0x0022: "NEC Corporation", + 0x0023: "WavePlus Technology Co., Ltd.", + 0x0024: "Alcatel", + 0x0025: "NXP Semiconductors (formerly Philips Semiconductors)", + 0x0026: "C Technologies", + 0x0027: "Open Interface", + 0x0028: "R F Micro Devices", + 0x0029: "Hitachi Ltd", + 0x002A: "Symbol Technologies, Inc.", + 0x002B: "Tenovis", + 0x002C: "Macronix International Co. Ltd.", + 0x002D: "GCT Semiconductor", + 0x002E: "Norwood Systems", + 0x002F: "MewTel Technology Inc.", + 0x0030: "ST Microelectronics", + 0x0031: "Synopsys, Inc.", + 0x0032: "Red-M (Communications) Ltd", + 0x0033: "Commil Ltd", + 0x0034: "Computer Access Technology Corporation (CATC)", + 0x0035: "Eclipse (HQ Espana) S.L.", + 0x0036: "Renesas Electronics Corporation", + 0x0037: "Mobilian Corporation", + 0x0038: "Syntronix Corporation", + 0x0039: "Integrated System Solution Corp.", + 0x003A: "Panasonic Corporation (formerly Matsushita Electric Industrial Co., Ltd.)", + 0x003B: "Gennum Corporation", + 0x003C: "BlackBerry Limited (formerly Research In Motion)", + 0x003D: "IPextreme, Inc.", + 0x003E: "Systems and Chips, Inc", + 0x003F: "Bluetooth SIG, Inc", + 0x0040: "Seiko Epson Corporation", + 0x0041: "Integrated Silicon Solution Taiwan, Inc.", + 0x0042: "CONWISE Technology Corporation Ltd", + 0x0043: "PARROT AUTOMOTIVE SAS", + 0x0044: "Socket Mobile", + 0x0045: "Atheros Communications, Inc.", + 0x0046: "MediaTek, Inc.", + 0x0047: "Bluegiga", + 0x0048: "Marvell Technology Group Ltd.", + 0x0049: "3DSP Corporation", + 0x004A: "Accel Semiconductor Ltd.", + 0x004B: "Continental Automotive Systems", + 0x004C: "Apple, Inc.", + 0x004D: "Staccato Communications, Inc.", + 0x004E: "Avago Technologies", + 0x004F: "APT Ltd.", + 0x0050: "SiRF Technology, Inc.", + 0x0051: "Tzero Technologies, Inc.", + 0x0052: "J&M Corporation", + 0x0053: "Free2move AB", + 0x0054: "3DiJoy Corporation", + 0x0055: "Plantronics, Inc.", + 0x0056: "Sony Ericsson Mobile Communications", + 0x0057: "Harman International Industries, Inc.", + 0x0058: "Vizio, Inc.", + 0x0059: "Nordic Semiconductor ASA", + 0x005A: "EM Microelectronic-Marin SA", + 0x005B: "Ralink Technology Corporation", + 0x005C: "Belkin International, Inc.", + 0x005D: "Realtek Semiconductor Corporation", + 0x005E: "Stonestreet One, LLC", + 0x005F: "Wicentric, Inc.", + 0x0060: "RivieraWaves S.A.S", + 0x0061: "RDA Microelectronics", + 0x0062: "Gibson Guitars", + 0x0063: "MiCommand Inc.", + 0x0064: "Band XI International, LLC", + 0x0065: "Hewlett-Packard Company", + 0x0066: "9Solutions Oy", + 0x0067: "GN Netcom A/S", + 0x0068: "General Motors", + 0x0069: "A&D Engineering, Inc.", + 0x006A: "MindTree Ltd.", + 0x006B: "Polar Electro OY", + 0x006C: "Beautiful Enterprise Co., Ltd.", + 0x006D: "BriarTek, Inc", + 0x006E: "Summit Data Communications, Inc.", + 0x006F: "Sound ID", + 0x0070: "Monster, LLC", + 0x0071: "connectBlue AB", + 0x0072: "ShangHai Super Smart Electronics Co. Ltd.", + 0x0073: "Group Sense Ltd.", + 0x0074: "Zomm, LLC", + 0x0075: "Samsung Electronics Co. Ltd.", + 0x0076: "Creative Technology Ltd.", + 0x0077: "Laird Technologies", + 0x0078: "Nike, Inc.", + 0x0079: "lesswire AG", + 0x007A: "MStar Semiconductor, Inc.", + 0x007B: "Hanlynn Technologies", + 0x007C: "A & R Cambridge", + 0x007D: "Seers Technology Co., Ltd.", + 0x007E: "Sports Tracking Technologies Ltd.", + 0x007F: "Autonet Mobile", + 0x0080: "DeLorme Publishing Company, Inc.", + 0x0081: "WuXi Vimicro", + 0x0082: "Sennheiser Communications A/S", + 0x0083: "TimeKeeping Systems, Inc.", + 0x0084: "Ludus Helsinki Ltd.", + 0x0085: "BlueRadios, Inc.", + 0x0086: "Equinux AG", + 0x0087: "Garmin International, Inc.", + 0x0088: "Ecotest", + 0x0089: "GN ReSound A/S", + 0x008A: "Jawbone", + 0x008B: "Topcon Positioning Systems, LLC", + 0x008C: "Gimbal Inc. (formerly Qualcomm Labs, Inc. and Qualcomm Retail Solutions, Inc.)", + 0x008D: "Zscan Software", + 0x008E: "Quintic Corp", + 0x008F: "Telit Wireless Solutions GmbH (formerly Stollmann E+V GmbH)", + 0x0090: "Funai Electric Co., Ltd.", + 0x0091: "Advanced PANMOBIL systems GmbH & Co. KG", + 0x0092: "ThinkOptics, Inc.", + 0x0093: "Universal Electronics, Inc.", + 0x0094: "Airoha Technology Corp.", + 0x0095: "NEC Lighting, Ltd.", + 0x0096: "ODM Technology, Inc.", + 0x0097: "ConnecteDevice Ltd.", + 0x0098: "zero1.tv GmbH", + 0x0099: "i.Tech Dynamic Global Distribution Ltd.", + 0x009A: "Alpwise", + 0x009B: "Jiangsu Toppower Automotive Electronics Co., Ltd.", + 0x009C: "Colorfy, Inc.", + 0x009D: "Geoforce Inc.", + 0x009E: "Bose Corporation", + 0x009F: "Suunto Oy", + 0x00A0: "Kensington Computer Products Group", + 0x00A1: "SR-Medizinelektronik", + 0x00A2: "Vertu Corporation Limited", + 0x00A3: "Meta Watch Ltd.", + 0x00A4: "LINAK A/S", + 0x00A5: "OTL Dynamics LLC", + 0x00A6: "Panda Ocean Inc.", + 0x00A7: "Visteon Corporation", + 0x00A8: "ARP Devices Limited", + 0x00A9: "MARELLI EUROPE S.P.A. (formerly Magneti Marelli S.p.A.)", + 0x00AA: "CAEN RFID srl", + 0x00AB: "Ingenieur-Systemgruppe Zahn GmbH", + 0x00AC: "Green Throttle Games", + 0x00AD: "Peter Systemtechnik GmbH", + 0x00AE: "Omegawave Oy", + 0x00AF: "Cinetix", + 0x00B0: "Passif Semiconductor Corp", + 0x00B1: "Saris Cycling Group, Inc", + 0x00B2: "​Bekey A/S", + 0x00B3: "​Clarinox Technologies Pty. Ltd.", + 0x00B4: "​BDE Technology Co., Ltd.", + 0x00B5: "Swirl Networks", + 0x00B6: "​Meso international", + 0x00B7: "​TreLab Ltd", + 0x00B8: "​Qualcomm Innovation Center, Inc. (QuIC)", + 0x00B9: "​​Johnson Controls, Inc.", + 0x00BA: "​Starkey Laboratories Inc.", + 0x00BB: "​​S-Power Electronics Limited", + 0x00BC: "​​Ace Sensor Inc", + 0x00BD: "​​Aplix Corporation", + 0x00BE: "​​AAMP of America", + 0x00BF: "​​Stalmart Technology Limited", + 0x00C0: "​​AMICCOM Electronics Corporation", + 0x00C1: "​​Shenzhen Excelsecu Data Technology Co.,Ltd", + 0x00C2: "​​Geneq Inc.", + 0x00C3: "​​adidas AG", + 0x00C4: "​​LG Electronics​", + 0x00C5: "​Onset Computer Corporation", + 0x00C6: "​Selfly BV", + 0x00C7: "​Quuppa Oy.", + 0x00C8: "GeLo Inc", + 0x00C9: "Evluma", + 0x00CA: "MC10", + 0x00CB: "Binauric SE", + 0x00CC: "Beats Electronics", + 0x00CD: "Microchip Technology Inc.", + 0x00CE: "Elgato Systems GmbH", + 0x00CF: "ARCHOS SA", + 0x00D0: "Dexcom, Inc.", + 0x00D1: "Polar Electro Europe B.V.", + 0x00D2: "Dialog Semiconductor B.V.", + 0x00D3: "Taixingbang Technology (HK) Co,. LTD.", + 0x00D4: "Kawantech", + 0x00D5: "Austco Communication Systems", + 0x00D6: "Timex Group USA, Inc.", + 0x00D7: "Qualcomm Technologies, Inc.", + 0x00D8: "Qualcomm Connected Experiences, Inc.", + 0x00D9: "Voyetra Turtle Beach", + 0x00DA: "txtr GmbH", + 0x00DB: "Biosentronics", + 0x00DC: "Procter & Gamble", + 0x00DD: "Hosiden Corporation", + 0x00DE: "Muzik LLC", + 0x00DF: "Misfit Wearables Corp", + 0x00E0: "Google", + 0x00E1: "Danlers Ltd", + 0x00E2: "Semilink Inc", + 0x00E3: "inMusic Brands, Inc", + 0x00E4: "Laird Connectivity, Inc. formerly L.S. Research Inc.", + 0x00E5: "Eden Software Consultants Ltd.", + 0x00E6: "Freshtemp", + 0x00E7: "​KS Technologies", + 0x00E8: "​ACTS Technologies", + 0x00E9: "​Vtrack Systems", + 0x00EA: "​Nielsen-Kellerman Company", + 0x00EB: "Server Technology Inc.", + 0x00EC: "BioResearch Associates", + 0x00ED: "Jolly Logic, LLC", + 0x00EE: "Above Average Outcomes, Inc.", + 0x00EF: "Bitsplitters GmbH", + 0x00F0: "PayPal, Inc.", + 0x00F1: "Witron Technology Limited", + 0x00F2: "Morse Project Inc.", + 0x00F3: "Kent Displays Inc.", + 0x00F4: "Nautilus Inc.", + 0x00F5: "Smartifier Oy", + 0x00F6: "Elcometer Limited", + 0x00F7: "VSN Technologies, Inc.", + 0x00F8: "AceUni Corp., Ltd.", + 0x00F9: "StickNFind", + 0x00FA: "Crystal Code AB", + 0x00FB: "KOUKAAM a.s.", + 0x00FC: "Delphi Corporation", + 0x00FD: "ValenceTech Limited", + 0x00FE: "Stanley Black and Decker", + 0x00FF: "Typo Products, LLC", + 0x0100: "TomTom International BV", + 0x0101: "Fugoo, Inc.", + 0x0102: "Keiser Corporation", + 0x0103: "Bang & Olufsen A/S", + 0x0104: "PLUS Location Systems Pty Ltd", + 0x0105: "Ubiquitous Computing Technology Corporation", + 0x0106: "Innovative Yachtter Solutions", + 0x0107: "William Demant Holding A/S", + 0x0108: "Chicony Electronics Co., Ltd.", + 0x0109: "Atus BV", + 0x010A: "Codegate Ltd", + 0x010B: "ERi, Inc", + 0x010C: "Transducers Direct, LLC", + 0x010D: "DENSO TEN LIMITED (formerly Fujitsu Ten LImited)", + 0x010E: "Audi AG", + 0x010F: "HiSilicon Technologies CO., LIMITED", + 0x0110: "Nippon Seiki Co., Ltd.", + 0x0111: "Steelseries ApS", + 0x0112: "Visybl Inc.", + 0x0113: "Openbrain Technologies, Co., Ltd.", + 0x0114: "Xensr", + 0x0115: "e.solutions", + 0x0116: "10AK Technologies", + 0x0117: "Wimoto Technologies Inc", + 0x0118: "Radius Networks, Inc.", + 0x0119: "Wize Technology Co., Ltd.", + 0x011A: "Qualcomm Labs, Inc.", + 0x011B: "Hewlett Packard Enterprise", + 0x011C: "Baidu", + 0x011D: "Arendi AG", + 0x011E: "Skoda Auto a.s.", + 0x011F: "Volkswagen AG", + 0x0120: "Porsche AG", + 0x0121: "Sino Wealth Electronic Ltd.", + 0x0122: "AirTurn, Inc.", + 0x0123: "Kinsa, Inc", + 0x0124: "HID Global", + 0x0125: "SEAT es", + 0x0126: "Promethean Ltd.", + 0x0127: "Salutica Allied Solutions", + 0x0128: "GPSI Group Pty Ltd", + 0x0129: "Nimble Devices Oy", + 0x012A: "Changzhou Yongse Infotech Co., Ltd.", + 0x012B: "SportIQ", + 0x012C: "TEMEC Instruments B.V.", + 0x012D: "Sony Corporation", + 0x012E: "ASSA ABLOY", + 0x012F: "Clarion Co. Inc.", + 0x0130: "Warehouse Innovations", + 0x0131: "Cypress Semiconductor", + 0x0132: "MADS Inc", + 0x0133: "Blue Maestro Limited", + 0x0134: "Resolution Products, Ltd.", + 0x0135: "Aireware LLC", + 0x0136: "Silvair, Inc.", + 0x0137: "Prestigio Plaza Ltd.", + 0x0138: "NTEO Inc.", + 0x0139: "Focus Systems Corporation", + 0x013A: "Tencent Holdings Ltd.", + 0x013B: "Allegion", + 0x013C: "Murata Manufacturing Co., Ltd.", + 0x013D: "WirelessWERX", + 0x013E: "Nod, Inc.", + 0x013F: "B&B Manufacturing Company", + 0x0140: "Alpine Electronics (China) Co., Ltd", + 0x0141: "FedEx Services", + 0x0142: "Grape Systems Inc.", + 0x0143: "Bkon Connect", + 0x0144: "Lintech GmbH", + 0x0145: "Novatel Wireless", + 0x0146: "Ciright", + 0x0147: "Mighty Cast, Inc.", + 0x0148: "Ambimat Electronics", + 0x0149: "Perytons Ltd.", + 0x014A: "Tivoli Audio, LLC", + 0x014B: "Master Lock", + 0x014C: "Mesh-Net Ltd", + 0x014D: "HUIZHOU DESAY SV AUTOMOTIVE CO., LTD.", + 0x014E: "Tangerine, Inc.", + 0x014F: "B&W Group Ltd.", + 0x0150: "Pioneer Corporation", + 0x0151: "OnBeep", + 0x0152: "Vernier Software & Technology", + 0x0153: "ROL Ergo", + 0x0154: "Pebble Technology", + 0x0155: "NETATMO", + 0x0156: "Accumulate AB", + 0x0157: "Anhui Huami Information Technology Co., Ltd.", + 0x0158: "Inmite s.r.o.", + 0x0159: "ChefSteps, Inc.", + 0x015A: "micas AG", + 0x015B: "Biomedical Research Ltd.", + 0x015C: "Pitius Tec S.L.", + 0x015D: "Estimote, Inc.", + 0x015E: "Unikey Technologies, Inc.", + 0x015F: "Timer Cap Co.", + 0x0160: "Awox formerly AwoX", + 0x0161: "yikes", + 0x0162: "MADSGlobalNZ Ltd.", + 0x0163: "PCH International", + 0x0164: "Qingdao Yeelink Information Technology Co., Ltd.", + 0x0165: "Milwaukee Tool (Formally Milwaukee Electric Tools)", + 0x0166: "MISHIK Pte Ltd", + 0x0167: "Ascensia Diabetes Care US Inc.", + 0x0168: "Spicebox LLC", + 0x0169: "emberlight", + 0x016A: "Cooper-Atkins Corporation", + 0x016B: "Qblinks", + 0x016C: "MYSPHERA", + 0x016D: "LifeScan Inc", + 0x016E: "Volantic AB", + 0x016F: "Podo Labs, Inc", + 0x0170: "Roche Diabetes Care AG", + 0x0171: "Amazon.com Services, LLC (formerly Amazon Fulfillment Service)", + 0x0172: "Connovate Technology Private Limited", + 0x0173: "Kocomojo, LLC", + 0x0174: "Everykey Inc.", + 0x0175: "Dynamic Controls", + 0x0176: "SentriLock", + 0x0177: "I-SYST inc.", + 0x0178: "CASIO COMPUTER CO., LTD.", + 0x0179: "LAPIS Technology Co., Ltd. formerly LAPIS Semiconductor Co., Ltd.", + 0x017A: "Telemonitor, Inc.", + 0x017B: "taskit GmbH", + 0x017C: "Daimler AG", + 0x017D: "BatAndCat", + 0x017E: "BluDotz Ltd", + 0x017F: "XTel Wireless ApS", + 0x0180: "Gigaset Communications GmbH", + 0x0181: "Gecko Health Innovations, Inc.", + 0x0182: "HOP Ubiquitous", + 0x0183: "Walt Disney", + 0x0184: "Nectar", + 0x0185: "bel'apps LLC", + 0x0186: "CORE Lighting Ltd", + 0x0187: "Seraphim Sense Ltd", + 0x0188: "Unico RBC", + 0x0189: "Physical Enterprises Inc.", + 0x018A: "Able Trend Technology Limited", + 0x018B: "Konica Minolta, Inc.", + 0x018C: "Wilo SE", + 0x018D: "Extron Design Services", + 0x018E: "Fitbit, Inc.", + 0x018F: "Fireflies Systems", + 0x0190: "Intelletto Technologies Inc.", + 0x0191: "FDK CORPORATION", + 0x0192: "Cloudleaf, Inc", + 0x0193: "Maveric Automation LLC", + 0x0194: "Acoustic Stream Corporation", + 0x0195: "Zuli", + 0x0196: "Paxton Access Ltd", + 0x0197: "WiSilica Inc.", + 0x0198: "VENGIT Korlatolt Felelossegu Tarsasag", + 0x0199: "SALTO SYSTEMS S.L.", + 0x019A: "TRON Forum (formerly T-Engine Forum)", + 0x019B: "CUBETECH s.r.o.", + 0x019C: "Cokiya Incorporated", + 0x019D: "CVS Health", + 0x019E: "Ceruus", + 0x019F: "Strainstall Ltd", + 0x01A0: "Channel Enterprises (HK) Ltd.", + 0x01A1: "FIAMM", + 0x01A2: "GIGALANE.CO.,LTD", + 0x01A3: "EROAD", + 0x01A4: "Mine Safety Appliances", + 0x01A5: "Icon Health and Fitness", + 0x01A6: "Wille Engineering (formely as Asandoo GmbH)", + 0x01A7: "ENERGOUS CORPORATION", + 0x01A8: "Taobao", + 0x01A9: "Canon Inc.", + 0x01AA: "Geophysical Technology Inc.", + 0x01AB: "Facebook, Inc.", + 0x01AC: "Trividia Health, Inc.", + 0x01AD: "FlightSafety International", + 0x01AE: "Earlens Corporation", + 0x01AF: "Sunrise Micro Devices, Inc.", + 0x01B0: "Star Micronics Co., Ltd.", + 0x01B1: "Netizens Sp. z o.o.", + 0x01B2: "Nymi Inc.", + 0x01B3: "Nytec, Inc.", + 0x01B4: "Trineo Sp. z o.o.", + 0x01B5: "Nest Labs Inc.", + 0x01B6: "LM Technologies Ltd", + 0x01B7: "General Electric Company", + 0x01B8: "i+D3 S.L.", + 0x01B9: "HANA Micron", + 0x01BA: "Stages Cycling LLC", + 0x01BB: "Cochlear Bone Anchored Solutions AB", + 0x01BC: "SenionLab AB", + 0x01BD: "Syszone Co., Ltd", + 0x01BE: "Pulsate Mobile Ltd.", + 0x01BF: "Hong Kong HunterSun Electronic Limited", + 0x01C0: "pironex GmbH", + 0x01C1: "BRADATECH Corp.", + 0x01C2: "Transenergooil AG", + 0x01C3: "Bunch", + 0x01C4: "DME Microelectronics", + 0x01C5: "Bitcraze AB", + 0x01C6: "HASWARE Inc.", + 0x01C7: "Abiogenix Inc.", + 0x01C8: "Poly-Control ApS", + 0x01C9: "Avi-on", + 0x01CA: "Laerdal Medical AS", + 0x01CB: "Fetch My Pet", + 0x01CC: "Sam Labs Ltd.", + 0x01CD: "Chengdu Synwing Technology Ltd", + 0x01CE: "HOUWA SYSTEM DESIGN, k.k.", + 0x01CF: "BSH", + 0x01D0: "Primus Inter Pares Ltd", + 0x01D1: "August Home, Inc", + 0x01D2: "Gill Electronics", + 0x01D3: "Sky Wave Design", + 0x01D4: "Newlab S.r.l.", + 0x01D5: "ELAD srl", + 0x01D6: "G-wearables inc.", + 0x01D7: "Squadrone Systems Inc.", + 0x01D8: "Code Corporation", + 0x01D9: "Savant Systems LLC", + 0x01DA: "Logitech International SA", + 0x01DB: "Innblue Consulting", + 0x01DC: "iParking Ltd.", + 0x01DD: "Koninklijke Philips Electronics N.V.", + 0x01DE: "Minelab Electronics Pty Limited", + 0x01DF: "Bison Group Ltd.", + 0x01E0: "Widex A/S", + 0x01E1: "Jolla Ltd", + 0x01E2: "Lectronix, Inc.", + 0x01E3: "Caterpillar Inc", + 0x01E4: "Freedom Innovations", + 0x01E5: "Dynamic Devices Ltd", + 0x01E6: "Technology Solutions (UK) Ltd", + 0x01E7: "IPS Group Inc.", + 0x01E8: "STIR", + 0x01E9: "Sano, Inc.", + 0x01EA: "Advanced Application Design, Inc.", + 0x01EB: "AutoMap LLC", + 0x01EC: "Spreadtrum Communications Shanghai Ltd", + 0x01ED: "CuteCircuit LTD", + 0x01EE: "Valeo Service", + 0x01EF: "Fullpower Technologies, Inc.", + 0x01F0: "KloudNation", + 0x01F1: "Zebra Technologies Corporation", + 0x01F2: "Itron, Inc.", + 0x01F3: "The University of Tokyo", + 0x01F4: "UTC Fire and Security", + 0x01F5: "Cool Webthings Limited", + 0x01F6: "DJO Global", + 0x01F7: "Gelliner Limited", + 0x01F8: "Anyka (Guangzhou) Microelectronics Technology Co, LTD", + 0x01F9: "Medtronic Inc.", + 0x01FA: "Gozio Inc.", + 0x01FB: "Form Lifting, LLC", + 0x01FC: "Wahoo Fitness, LLC", + 0x01FD: "Kontakt Micro-Location Sp. z o.o.", + 0x01FE: "Radio Systems Corporation", + 0x01FF: "Freescale Semiconductor, Inc.", + 0x0200: "Verifone Systems Pte Ltd. Taiwan Branch", + 0x0201: "AR Timing", + 0x0202: "Rigado LLC", + 0x0203: "Kemppi Oy", + 0x0204: "Tapcentive Inc.", + 0x0205: "Smartbotics Inc.", + 0x0206: "Otter Products, LLC", + 0x0207: "STEMP Inc.", + 0x0208: "LumiGeek LLC", + 0x0209: "InvisionHeart Inc.", + 0x020A: "Macnica Inc.", + 0x020B: "Jaguar Land Rover Limited", + 0x020C: "CoroWare Technologies, Inc", + 0x020D: "Simplo Technology Co., LTD", + 0x020E: "Omron Healthcare Co., LTD", + 0x020F: "Comodule GMBH", + 0x0210: "ikeGPS", + 0x0211: "Telink Semiconductor Co. Ltd", + 0x0212: "Interplan Co., Ltd", + 0x0213: "Wyler AG", + 0x0214: "IK Multimedia Production srl", + 0x0215: "Lukoton Experience Oy", + 0x0216: "MTI Ltd", + 0x0217: "Tech4home, Lda", + 0x0218: "Hiotech AB", + 0x0219: "DOTT Limited", + 0x021A: "Blue Speck Labs, LLC", + 0x021B: "Cisco Systems, Inc", + 0x021C: "Mobicomm Inc", + 0x021D: "Edamic", + 0x021E: "Goodnet, Ltd", + 0x021F: "Luster Leaf Products Inc", + 0x0220: "Manus Machina BV", + 0x0221: "Mobiquity Networks Inc", + 0x0222: "Praxis Dynamics", + 0x0223: "Philip Morris Products S.A.", + 0x0224: "Comarch SA", + 0x0225: "Nestlé Nespresso S.A.", + 0x0226: "Merlinia A/S", + 0x0227: "LifeBEAM Technologies", + 0x0228: "Twocanoes Labs, LLC", + 0x0229: "Muoverti Limited", + 0x022A: "Stamer Musikanlagen GMBH", + 0x022B: "Tesla Motors", + 0x022C: "Pharynks Corporation", + 0x022D: "Lupine", + 0x022E: "Siemens AG", + 0x022F: "Huami (Shanghai) Culture Communication CO., LTD", + 0x0230: "Foster Electric Company, Ltd", + 0x0231: "ETA SA", + 0x0232: "x-Senso Solutions Kft", + 0x0233: "Shenzhen SuLong Communication Ltd", + 0x0234: "FengFan (BeiJing) Technology Co, Ltd", + 0x0235: "Qrio Inc", + 0x0236: "Pitpatpet Ltd", + 0x0237: "MSHeli s.r.l.", + 0x0238: "Trakm8 Ltd", + 0x0239: "JIN CO, Ltd", + 0x023A: "Alatech Tehnology", + 0x023B: "Beijing CarePulse Electronic Technology Co, Ltd", + 0x023C: "Awarepoint", + 0x023D: "ViCentra B.V.", + 0x023E: "Raven Industries", + 0x023F: "WaveWare Technologies Inc.", + 0x0240: "Argenox Technologies", + 0x0241: "Bragi GmbH", + 0x0242: "16Lab Inc", + 0x0243: "Masimo Corp", + 0x0244: "Iotera Inc", + 0x0245: "Endress+Hauser ", + 0x0246: "ACKme Networks, Inc.", + 0x0247: "FiftyThree Inc.", + 0x0248: "Parker Hannifin Corp", + 0x0249: "Transcranial Ltd", + 0x024A: "Uwatec AG", + 0x024B: "Orlan LLC", + 0x024C: "Blue Clover Devices", + 0x024D: "M-Way Solutions GmbH", + 0x024E: "Microtronics Engineering GmbH", + 0x024F: "Schneider Schreibgeräte GmbH", + 0x0250: "Sapphire Circuits LLC", + 0x0251: "Lumo Bodytech Inc.", + 0x0252: "UKC Technosolution", + 0x0253: "Xicato Inc.", + 0x0254: "Playbrush", + 0x0255: "Dai Nippon Printing Co., Ltd.", + 0x0256: "G24 Power Limited", + 0x0257: "AdBabble Local Commerce Inc.", + 0x0258: "Devialet SA", + 0x0259: "ALTYOR", + 0x025A: "University of Applied Sciences Valais/Haute Ecole Valaisanne", + 0x025B: "Five Interactive, LLC dba Zendo", + 0x025C: "NetEase(Hangzhou)Network co.Ltd.", + 0x025D: "Lexmark International Inc.", + 0x025E: "Fluke Corporation", + 0x025F: "Yardarm Technologies", + 0x0260: "SensaRx", + 0x0261: "SECVRE GmbH", + 0x0262: "Glacial Ridge Technologies", + 0x0263: "Identiv, Inc.", + 0x0264: "DDS, Inc.", + 0x0265: "SMK Corporation", + 0x0266: "Schawbel Technologies LLC", + 0x0267: "XMI Systems SA", + 0x0268: "Cerevo", + 0x0269: "Torrox GmbH & Co KG", + 0x026A: "Gemalto", + 0x026B: "DEKA Research & Development Corp.", + 0x026C: "Domster Tadeusz Szydlowski", + 0x026D: "Technogym SPA", + 0x026E: "FLEURBAEY BVBA", + 0x026F: "Aptcode Solutions", + 0x0270: "LSI ADL Technology", + 0x0271: "Animas Corp", + 0x0272: "Alps Alpine Co., Ltd.", + 0x0273: "OCEASOFT", + 0x0274: "Motsai Research", + 0x0275: "Geotab", + 0x0276: "E.G.O. Elektro-Geraetebau GmbH", + 0x0277: "bewhere inc", + 0x0278: "Johnson Outdoors Inc", + 0x0279: "steute Schaltgerate GmbH & Co. KG", + 0x027A: "Ekomini inc.", + 0x027B: "DEFA AS", + 0x027C: "Aseptika Ltd", + 0x027D: "HUAWEI Technologies Co., Ltd.", + 0x027E: "HabitAware, LLC", + 0x027F: "ruwido austria gmbh", + 0x0280: "ITEC corporation", + 0x0281: "StoneL", + 0x0282: "Sonova AG", + 0x0283: "Maven Machines, Inc.", + 0x0284: "Synapse Electronics", + 0x0285: "Standard Innovation Inc.", + 0x0286: "RF Code, Inc.", + 0x0287: "Wally Ventures S.L.", + 0x0288: "Willowbank Electronics Ltd", + 0x0289: "SK Telecom", + 0x028A: "Jetro AS", + 0x028B: "Code Gears LTD", + 0x028C: "NANOLINK APS", + 0x028D: "IF, LLC", + 0x028E: "RF Digital Corp", + 0x028F: "Church & Dwight Co., Inc", + 0x0290: "Multibit Oy", + 0x0291: "CliniCloud Inc", + 0x0292: "SwiftSensors", + 0x0293: "Blue Bite", + 0x0294: "ELIAS GmbH", + 0x0295: "Sivantos GmbH", + 0x0296: "Petzl", + 0x0297: "storm power ltd", + 0x0298: "EISST Ltd", + 0x0299: "Inexess Technology Simma KG", + 0x029A: "Currant, Inc.", + 0x029B: "C2 Development, Inc.", + 0x029C: "Blue Sky Scientific, LLC", + 0x029D: "ALOTTAZS LABS, LLC", + 0x029E: "Kupson spol. s r.o.", + 0x029F: "Areus Engineering GmbH", + 0x02A0: "Impossible Camera GmbH", + 0x02A1: "InventureTrack Systems", + 0x02A2: "LockedUp", + 0x02A3: "Itude", + 0x02A4: "Pacific Lock Company", + 0x02A5: "Tendyron Corporation ( 天地融科技股份有限公司 )", + 0x02A6: "Robert Bosch GmbH", + 0x02A7: "Illuxtron international B.V.", + 0x02A8: "miSport Ltd.", + 0x02A9: "Chargelib", + 0x02AA: "Doppler Lab", + 0x02AB: "BBPOS Limited", + 0x02AC: "RTB Elektronik GmbH & Co. KG", + 0x02AD: "Rx Networks, Inc.", + 0x02AE: "WeatherFlow, Inc.", + 0x02AF: "Technicolor USA Inc.", + 0x02B0: "Bestechnic(Shanghai),Ltd", + 0x02B1: "Raden Inc", + 0x02B2: "JouZen Oy", + 0x02B3: "CLABER S.P.A.", + 0x02B4: "Hyginex, Inc.", + 0x02B5: "HANSHIN ELECTRIC RAILWAY CO.,LTD.", + 0x02B6: "Schneider Electric", + 0x02B7: "Oort Technologies LLC", + 0x02B8: "Chrono Therapeutics", + 0x02B9: "Rinnai Corporation", + 0x02BA: "Swissprime Technologies AG", + 0x02BB: "Koha.,Co.Ltd", + 0x02BC: "Genevac Ltd", + 0x02BD: "Chemtronics", + 0x02BE: "Seguro Technology Sp. z o.o.", + 0x02BF: "Redbird Flight Simulations", + 0x02C0: "Dash Robotics", + 0x02C1: "LINE Corporation", + 0x02C2: "Guillemot Corporation", + 0x02C3: "Techtronic Power Tools Technology Limited", + 0x02C4: "Wilson Sporting Goods", + 0x02C5: "Lenovo (Singapore) Pte Ltd. ( 联想(新加坡) )", + 0x02C6: "Ayatan Sensors", + 0x02C7: "Electronics Tomorrow Limited", + 0x02C8: "VASCO Data Security International, Inc.", + 0x02C9: "PayRange Inc.", + 0x02CA: "ABOV Semiconductor", + 0x02CB: "AINA-Wireless Inc.", + 0x02CC: "Eijkelkamp Soil & Water", + 0x02CD: "BMA ergonomics b.v.", + 0x02CE: "Teva Branded Pharmaceutical Products R&D, Inc.", + 0x02CF: "Anima", + 0x02D0: "3M", + 0x02D1: "Empatica Srl", + 0x02D2: "Afero, Inc.", + 0x02D3: "Powercast Corporation", + 0x02D4: "Secuyou ApS", + 0x02D5: "OMRON Corporation", + 0x02D6: "Send Solutions", + 0x02D7: "NIPPON SYSTEMWARE CO.,LTD.", + 0x02D8: "Neosfar", + 0x02D9: "Fliegl Agrartechnik GmbH", + 0x02DA: "Gilvader", + 0x02DB: "Digi International Inc (R)", + 0x02DC: "DeWalch Technologies, Inc.", + 0x02DD: "Flint Rehabilitation Devices, LLC", + 0x02DE: "Samsung SDS Co., Ltd.", + 0x02DF: "Blur Product Development", + 0x02E0: "University of Michigan", + 0x02E1: "Victron Energy BV", + 0x02E2: "NTT docomo", + 0x02E3: "Carmanah Technologies Corp.", + 0x02E4: "Bytestorm Ltd.", + 0x02E5: "Espressif Incorporated ( 乐鑫信息科技(上海)有限公司 )", + 0x02E6: "Unwire", + 0x02E7: "Connected Yard, Inc.", + 0x02E8: "American Music Environments", + 0x02E9: "Sensogram Technologies, Inc.", + 0x02EA: "Fujitsu Limited", + 0x02EB: "Ardic Technology", + 0x02EC: "Delta Systems, Inc", + 0x02ED: "HTC Corporation ", + 0x02EE: "Citizen Holdings Co., Ltd. ", + 0x02EF: "SMART-INNOVATION.inc", + 0x02F0: "Blackrat Software ", + 0x02F1: "The Idea Cave, LLC", + 0x02F2: "GoPro, Inc.", + 0x02F3: "AuthAir, Inc", + 0x02F4: "Vensi, Inc.", + 0x02F5: "Indagem Tech LLC", + 0x02F6: "Intemo Technologies", + 0x02F7: "DreamVisions co., Ltd.", + 0x02F8: "Runteq Oy Ltd", + 0x02F9: "IMAGINATION TECHNOLOGIES LTD ", + 0x02FA: "CoSTAR TEchnologies", + 0x02FB: "Clarius Mobile Health Corp.", + 0x02FC: "Shanghai Frequen Microelectronics Co., Ltd.", + 0x02FD: "Uwanna, Inc.", + 0x02FE: "Lierda Science & Technology Group Co., Ltd.", + 0x02FF: "Silicon Laboratories", + 0x0300: "World Moto Inc.", + 0x0301: "Giatec Scientific Inc.", + 0x0302: "Loop Devices, Inc", + 0x0303: "IACA electronique", + 0x0304: "Proxy Technologies, Inc.", + 0x0305: "Swipp ApS", + 0x0306: "Life Laboratory Inc. ", + 0x0307: "FUJI INDUSTRIAL CO.,LTD.", + 0x0308: "Surefire, LLC", + 0x0309: "Dolby Labs", + 0x030A: "Ellisys", + 0x030B: "Magnitude Lighting Converters", + 0x030C: "Hilti AG", + 0x030D: "Devdata S.r.l.", + 0x030E: "Deviceworx", + 0x030F: "Shortcut Labs", + 0x0310: "SGL Italia S.r.l.", + 0x0311: "PEEQ DATA", + 0x0312: "Ducere Technologies Pvt Ltd ", + 0x0313: "DiveNav, Inc. ", + 0x0314: "RIIG AI Sp. z o.o.", + 0x0315: "Thermo Fisher Scientific ", + 0x0316: "AG Measurematics Pvt. Ltd. ", + 0x0317: "CHUO Electronics CO., LTD. ", + 0x0318: "Aspenta International ", + 0x0319: "Eugster Frismag AG ", + 0x031A: "Amber wireless GmbH ", + 0x031B: "HQ Inc ", + 0x031C: "Lab Sensor Solutions ", + 0x031D: "Enterlab ApS ", + 0x031E: "Eyefi, Inc.", + 0x031F: "MetaSystem S.p.A. ", + 0x0320: "SONO ELECTRONICS. CO., LTD ", + 0x0321: "Jewelbots ", + 0x0322: "Compumedics Limited ", + 0x0323: "Rotor Bike Components ", + 0x0324: "Astro, Inc. ", + 0x0325: "Amotus Solutions ", + 0x0326: "Healthwear Technologies (Changzhou)Ltd ", + 0x0327: "Essex Electronics ", + 0x0328: "Grundfos A/S", + 0x0329: "Eargo, Inc. ", + 0x032A: "Electronic Design Lab ", + 0x032B: "ESYLUX ", + 0x032C: "NIPPON SMT.CO.,Ltd", + 0x032D: "BM innovations GmbH ", + 0x032E: "indoormap", + 0x032F: "OttoQ Inc ", + 0x0330: "North Pole Engineering ", + 0x0331: "3flares Technologies Inc.", + 0x0332: "Electrocompaniet A.S. ", + 0x0333: "Mul-T-Lock", + 0x0334: "Corentium AS ", + 0x0335: "Enlighted Inc", + 0x0336: "GISTIC", + 0x0337: "AJP2 Holdings, LLC", + 0x0338: "COBI GmbH ", + 0x0339: "Blue Sky Scientific, LLC ", + 0x033A: "Appception, Inc.", + 0x033B: "Courtney Thorne Limited ", + 0x033C: "Virtuosys", + 0x033D: "TPV Technology Limited ", + 0x033E: "Monitra SA", + 0x033F: "Automation Components, Inc. ", + 0x0340: "Letsense s.r.l. ", + 0x0341: "Etesian Technologies LLC ", + 0x0342: "GERTEC BRASIL LTDA. ", + 0x0343: "Drekker Development Pty. Ltd.", + 0x0344: "Whirl Inc ", + 0x0345: "Locus Positioning ", + 0x0346: "Acuity Brands Lighting, Inc ", + 0x0347: "Prevent Biometrics ", + 0x0348: "Arioneo", + 0x0349: "VersaMe ", + 0x034A: "Vaddio ", + 0x034B: "Libratone A/S ", + 0x034C: "HM Electronics, Inc. ", + 0x034D: "TASER International, Inc.", + 0x034E: "SafeTrust Inc. ", + 0x034F: "Heartland Payment Systems ", + 0x0350: "Bitstrata Systems Inc. ", + 0x0351: "Pieps GmbH ", + 0x0352: "iRiding(Xiamen)Technology Co.,Ltd.", + 0x0353: "Alpha Audiotronics, Inc. ", + 0x0354: "TOPPAN FORMS CO.,LTD. ", + 0x0355: "Sigma Designs, Inc. ", + 0x0356: "Spectrum Brands, Inc. ", + 0x0357: "Polymap Wireless ", + 0x0358: "MagniWare Ltd.", + 0x0359: "Novotec Medical GmbH ", + 0x035A: "Medicom Innovation Partner a/s ", + 0x035B: "Matrix Inc. ", + 0x035C: "Eaton Corporation ", + 0x035D: "KYS", + 0x035E: "Naya Health, Inc. ", + 0x035F: "Acromag ", + 0x0360: "Insulet Corporation ", + 0x0361: "Wellinks Inc. ", + 0x0362: "ON Semiconductor", + 0x0363: "FREELAP SA ", + 0x0364: "Favero Electronics Srl ", + 0x0365: "BioMech Sensor LLC ", + 0x0366: "BOLTT Sports technologies Private limited", + 0x0367: "Saphe International ", + 0x0368: "Metormote AB ", + 0x0369: "littleBits ", + 0x036A: "SetPoint Medical ", + 0x036B: "BRControls Products BV ", + 0x036C: "Zipcar ", + 0x036D: "AirBolt Pty Ltd ", + 0x036E: "KeepTruckin Inc ", + 0x036F: "Motiv, Inc. ", + 0x0370: "Wazombi Labs OÜ ", + 0x0371: "ORBCOMM", + 0x0372: "Nixie Labs, Inc.", + 0x0373: "AppNearMe Ltd", + 0x0374: "Holman Industries", + 0x0375: "Expain AS", + 0x0376: "Electronic Temperature Instruments Ltd", + 0x0377: "Plejd AB", + 0x0378: "Propeller Health", + 0x0379: "Shenzhen iMCO Electronic Technology Co.,Ltd", + 0x037A: "Algoria", + 0x037B: "Apption Labs Inc.", + 0x037C: "Cronologics Corporation", + 0x037D: "MICRODIA Ltd.", + 0x037E: "lulabytes S.L.", + 0x037F: "Société des Produits Nestlé S.A. (formerly Nestec S.A.)", + 0x0380: "LLC \"MEGA-F service\"", + 0x0381: "Sharp Corporation", + 0x0382: "Precision Outcomes Ltd", + 0x0383: "Kronos Incorporated", + 0x0384: "OCOSMOS Co., Ltd.", + 0x0385: "Embedded Electronic Solutions Ltd. dba e2Solutions", + 0x0386: "Aterica Inc.", + 0x0387: "BluStor PMC, Inc.", + 0x0388: "Kapsch TrafficCom AB", + 0x0389: "ActiveBlu Corporation", + 0x038A: "Kohler Mira Limited", + 0x038B: "Noke", + 0x038C: "Appion Inc.", + 0x038D: "Resmed Ltd", + 0x038E: "Crownstone B.V.", + 0x038F: "Xiaomi Inc.", + 0x0390: "INFOTECH s.r.o.", + 0x0391: "Thingsquare AB", + 0x0392: "T&D", + 0x0393: "LAVAZZA S.p.A.", + 0x0394: "Netclearance Systems, Inc.", + 0x0395: "SDATAWAY", + 0x0396: "BLOKS GmbH", + 0x0397: "LEGO System A/S", + 0x0398: "Thetatronics Ltd", + 0x0399: "Nikon Corporation", + 0x039A: "NeST", + 0x039B: "South Silicon Valley Microelectronics", + 0x039C: "ALE International", + 0x039D: "CareView Communications, Inc.", + 0x039E: "SchoolBoard Limited", + 0x039F: "Molex Corporation", + 0x03A0: "IVT Wireless Limited", + 0x03A1: "Alpine Labs LLC", + 0x03A2: "Candura Instruments", + 0x03A3: "SmartMovt Technology Co., Ltd", + 0x03A4: "Token Zero Ltd", + 0x03A5: "ACE CAD Enterprise Co., Ltd. (ACECAD)", + 0x03A6: "Medela, Inc", + 0x03A7: "AeroScout", + 0x03A8: "Esrille Inc.", + 0x03A9: "THINKERLY SRL", + 0x03AA: "Exon Sp. z o.o.", + 0x03AB: "Meizu Technology Co., Ltd.", + 0x03AC: "Smablo LTD", + 0x03AD: "XiQ", + 0x03AE: "Allswell Inc.", + 0x03AF: "Comm-N-Sense Corp DBA Verigo", + 0x03B0: "VIBRADORM GmbH", + 0x03B1: "Otodata Wireless Network Inc.", + 0x03B2: "Propagation Systems Limited", + 0x03B3: "Midwest Instruments & Controls", + 0x03B4: "Alpha Nodus, inc.", + 0x03B5: "petPOMM, Inc", + 0x03B6: "Mattel", + 0x03B7: "Airbly Inc.", + 0x03B8: "A-Safe Limited", + 0x03B9: "FREDERIQUE CONSTANT SA", + 0x03BA: "Maxscend Microelectronics Company Limited", + 0x03BB: "Abbott", + 0x03BC: "ASB Bank Ltd", + 0x03BD: "amadas", + 0x03BE: "Applied Science, Inc.", + 0x03BF: "iLumi Solutions Inc.", + 0x03C0: "Arch Systems Inc.", + 0x03C1: "Ember Technologies, Inc.", + 0x03C2: "Snapchat Inc", + 0x03C3: "Casambi Technologies Oy", + 0x03C4: "Pico Technology Inc.", + 0x03C5: "St. Jude Medical, Inc.", + 0x03C6: "Intricon", + 0x03C7: "Structural Health Systems, Inc.", + 0x03C8: "Avvel International", + 0x03C9: "Gallagher Group", + 0x03CA: "In2things Automation Pvt. Ltd.", + 0x03CB: "SYSDEV Srl", + 0x03CC: "Vonkil Technologies Ltd", + 0x03CD: "Wynd Technologies, Inc.", + 0x03CE: "CONTRINEX S.A.", + 0x03CF: "MIRA, Inc.", + 0x03D0: "Watteam Ltd", + 0x03D1: "Density Inc.", + 0x03D2: "IOT Pot India Private Limited", + 0x03D3: "Sigma Connectivity AB", + 0x03D4: "PEG PEREGO SPA", + 0x03D5: "Wyzelink Systems Inc.", + 0x03D6: "Yota Devices LTD", + 0x03D7: "FINSECUR", + 0x03D8: "Zen-Me Labs Ltd", + 0x03D9: "3IWare Co., Ltd.", + 0x03DA: "EnOcean GmbH", + 0x03DB: "Instabeat, Inc", + 0x03DC: "Nima Labs", + 0x03DD: "Andreas Stihl AG & Co. KG", + 0x03DE: "Nathan Rhoades LLC", + 0x03DF: "Grob Technologies, LLC", + 0x03E0: "Actions (Zhuhai) Technology Co., Limited", + 0x03E1: "SPD Development Company Ltd", + 0x03E2: "Sensoan Oy", + 0x03E3: "Qualcomm Life Inc", + 0x03E4: "Chip-ing AG", + 0x03E5: "ffly4u", + 0x03E6: "IoT Instruments Oy", + 0x03E7: "TRUE Fitness Technology", + 0x03E8: "Reiner Kartengeraete GmbH & Co. KG.", + 0x03E9: "SHENZHEN LEMONJOY TECHNOLOGY CO., LTD.", + 0x03EA: "Hello Inc.", + 0x03EB: "Evollve Inc.", + 0x03EC: "Jigowatts Inc.", + 0x03ED: "BASIC MICRO.COM,INC.", + 0x03EE: "CUBE TECHNOLOGIES", + 0x03EF: "foolography GmbH", + 0x03F0: "CLINK", + 0x03F1: "Hestan Smart Cooking Inc.", + 0x03F2: "WindowMaster A/S", + 0x03F3: "Flowscape AB", + 0x03F4: "PAL Technologies Ltd", + 0x03F5: "WHERE, Inc.", + 0x03F6: "Iton Technology Corp.", + 0x03F7: "Owl Labs Inc.", + 0x03F8: "Rockford Corp.", + 0x03F9: "Becon Technologies Co.,Ltd.", + 0x03FA: "Vyassoft Technologies Inc", + 0x03FB: "Nox Medical", + 0x03FC: "Kimberly-Clark", + 0x03FD: "Trimble Navigation Ltd.", + 0x03FE: "Littelfuse", + 0x03FF: "Withings", + 0x0400: "i-developer IT Beratung UG", + 0x0401: "Relations Inc.", + 0x0402: "Sears Holdings Corporation", + 0x0403: "Gantner Electronic GmbH", + 0x0404: "Authomate Inc", + 0x0405: "Vertex International, Inc.", + 0x0406: "Airtago", + 0x0407: "Swiss Audio SA", + 0x0408: "ToGetHome Inc.", + 0x0409: "AXIS", + 0x040A: "Openmatics", + 0x040B: "Jana Care Inc.", + 0x040C: "Senix Corporation", + 0x040D: "NorthStar Battery Company, LLC", + 0x040E: "SKF (U.K.) Limited", + 0x040F: "CO-AX Technology, Inc.", + 0x0410: "Fender Musical Instruments", + 0x0411: "Luidia Inc", + 0x0412: "SEFAM", + 0x0413: "Wireless Cables Inc", + 0x0414: "Lightning Protection International Pty Ltd", + 0x0415: "Uber Technologies Inc", + 0x0416: "SODA GmbH", + 0x0417: "Fatigue Science", + 0x0418: "Reserved", + 0x0419: "Novalogy LTD", + 0x041A: "Friday Labs Limited", + 0x041B: "OrthoAccel Technologies", + 0x041C: "WaterGuru, Inc.", + 0x041D: "Benning Elektrotechnik und Elektronik GmbH & Co. KG", + 0x041E: "Dell Computer Corporation", + 0x041F: "Kopin Corporation", + 0x0420: "TecBakery GmbH", + 0x0421: "Backbone Labs, Inc.", + 0x0422: "DELSEY SA", + 0x0423: "Chargifi Limited", + 0x0424: "Trainesense Ltd.", + 0x0425: "Unify Software and Solutions GmbH & Co. KG", + 0x0426: "Husqvarna AB", + 0x0427: "Focus fleet and fuel management inc", + 0x0428: "SmallLoop, LLC", + 0x0429: "Prolon Inc.", + 0x042A: "BD Medical", + 0x042B: "iMicroMed Incorporated", + 0x042C: "Ticto N.V.", + 0x042D: "Meshtech AS", + 0x042E: "MemCachier Inc.", + 0x042F: "Danfoss A/S", + 0x0430: "SnapStyk Inc.", + 0x0431: "Amway Corporation", + 0x0432: "Silk Labs, Inc.", + 0x0433: "Pillsy Inc.", + 0x0434: "Hatch Baby, Inc.", + 0x0435: "Blocks Wearables Ltd.", + 0x0436: "Drayson Technologies (Europe) Limited", + 0x0437: "eBest IOT Inc.", + 0x0438: "Helvar Ltd", + 0x0439: "Radiance Technologies", + 0x043A: "Nuheara Limited", + 0x043B: "Appside co., ltd.", + 0x043C: "DeLaval", + 0x043D: "Coiler Corporation", + 0x043E: "Thermomedics, Inc.", + 0x043F: "Tentacle Sync GmbH", + 0x0440: "Valencell, Inc.", + 0x0441: "iProtoXi Oy", + 0x0442: "SECOM CO., LTD.", + 0x0443: "Tucker International LLC", + 0x0444: "Metanate Limited", + 0x0445: "Kobian Canada Inc.", + 0x0446: "NETGEAR, Inc.", + 0x0447: "Fabtronics Australia Pty Ltd", + 0x0448: "Grand Centrix GmbH", + 0x0449: "1UP USA.com llc", + 0x044A: "SHIMANO INC.", + 0x044B: "Nain Inc.", + 0x044C: "LifeStyle Lock, LLC", + 0x044D: "VEGA Grieshaber KG", + 0x044E: "Xtrava Inc.", + 0x044F: "TTS Tooltechnic Systems AG & Co. KG", + 0x0450: "Teenage Engineering AB", + 0x0451: "Tunstall Nordic AB", + 0x0452: "Svep Design Center AB", + 0x0453: "Qorvo Utrecht B.V. formerly GreenPeak Technologies BV", + 0x0454: "Sphinx Electronics GmbH & Co KG", + 0x0455: "Atomation", + 0x0456: "Nemik Consulting Inc", + 0x0457: "RF INNOVATION", + 0x0458: "Mini Solution Co., Ltd.", + 0x0459: "Lumenetix, Inc", + 0x045A: "2048450 Ontario Inc", + 0x045B: "SPACEEK LTD", + 0x045C: "Delta T Corporation", + 0x045D: "Boston Scientific Corporation", + 0x045E: "Nuviz, Inc.", + 0x045F: "Real Time Automation, Inc.", + 0x0460: "Kolibree", + 0x0461: "vhf elektronik GmbH", + 0x0462: "Bonsai Systems GmbH", + 0x0463: "Fathom Systems Inc.", + 0x0464: "Bellman & Symfon", + 0x0465: "International Forte Group LLC", + 0x0466: "CycleLabs Solutions inc.", + 0x0467: "Codenex Oy", + 0x0468: "Kynesim Ltd", + 0x0469: "Palago AB", + 0x046A: "INSIGMA INC.", + 0x046B: "PMD Solutions", + 0x046C: "Qingdao Realtime Technology Co., Ltd.", + 0x046D: "BEGA Gantenbrink-Leuchten KG", + 0x046E: "Pambor Ltd.", + 0x046F: "Develco Products A/S", + 0x0470: "iDesign s.r.l.", + 0x0471: "TiVo Corp", + 0x0472: "Control-J Pty Ltd", + 0x0473: "Steelcase, Inc.", + 0x0474: "iApartment co., ltd.", + 0x0475: "Icom inc.", + 0x0476: "Oxstren Wearable Technologies Private Limited", + 0x0477: "Blue Spark Technologies", + 0x0478: "FarSite Communications Limited", + 0x0479: "mywerk system GmbH", + 0x047A: "Sinosun Technology Co., Ltd.", + 0x047B: "MIYOSHI ELECTRONICS CORPORATION", + 0x047C: "POWERMAT LTD", + 0x047D: "Occly LLC", + 0x047E: "OurHub Dev IvS", + 0x047F: "Pro-Mark, Inc.", + 0x0480: "Dynometrics Inc.", + 0x0481: "Quintrax Limited", + 0x0482: "POS Tuning Udo Vosshenrich GmbH & Co. KG", + 0x0483: "Multi Care Systems B.V.", + 0x0484: "Revol Technologies Inc", + 0x0485: "SKIDATA AG", + 0x0486: "DEV TECNOLOGIA INDUSTRIA, COMERCIO E MANUTENCAO DE EQUIPAMENTOS LTDA. - ME", + 0x0487: "Centrica Connected Home", + 0x0488: "Automotive Data Solutions Inc", + 0x0489: "Igarashi Engineering", + 0x048A: "Taelek Oy", + 0x048B: "CP Electronics Limited", + 0x048C: "Vectronix AG", + 0x048D: "S-Labs Sp. z o.o.", + 0x048E: "Companion Medical, Inc.", + 0x048F: "BlueKitchen GmbH", + 0x0490: "Matting AB", + 0x0491: "SOREX - Wireless Solutions GmbH", + 0x0492: "ADC Technology, Inc.", + 0x0493: "Lynxemi Pte Ltd", + 0x0494: "SENNHEISER electronic GmbH & Co. KG", + 0x0495: "LMT Mercer Group, Inc", + 0x0496: "Polymorphic Labs LLC", + 0x0497: "Cochlear Limited", + 0x0498: "METER Group, Inc. USA", + 0x0499: "Ruuvi Innovations Ltd.", + 0x049A: "Situne AS", + 0x049B: "nVisti, LLC", + 0x049C: "DyOcean", + 0x049D: "Uhlmann & Zacher GmbH", + 0x049E: "AND!XOR LLC", + 0x049F: "tictote AB", + 0x04A0: "Vypin, LLC", + 0x04A1: "PNI Sensor Corporation", + 0x04A2: "ovrEngineered, LLC", + 0x04A3: "GT-tronics HK Ltd", + 0x04A4: "Herbert Waldmann GmbH & Co. KG", + 0x04A5: "Guangzhou FiiO Electronics Technology Co.,Ltd", + 0x04A6: "Vinetech Co., Ltd", + 0x04A7: "Dallas Logic Corporation", + 0x04A8: "BioTex, Inc.", + 0x04A9: "DISCOVERY SOUND TECHNOLOGY, LLC", + 0x04AA: "LINKIO SAS", + 0x04AB: "Harbortronics, Inc.", + 0x04AC: "Undagrid B.V.", + 0x04AD: "Shure Inc", + 0x04AE: "ERM Electronic Systems LTD", + 0x04AF: "BIOROWER Handelsagentur GmbH", + 0x04B0: "Weba Sport und Med. Artikel GmbH", + 0x04B1: "Kartographers Technologies Pvt. Ltd.", + 0x04B2: "The Shadow on the Moon", + 0x04B3: "mobike (Hong Kong) Limited", + 0x04B4: "Inuheat Group AB", + 0x04B5: "Swiftronix AB", + 0x04B6: "Diagnoptics Technologies", + 0x04B7: "Analog Devices, Inc.", + 0x04B8: "Soraa Inc.", + 0x04B9: "CSR Building Products Limited", + 0x04BA: "Crestron Electronics, Inc.", + 0x04BB: "Neatebox Ltd", + 0x04BC: "Draegerwerk AG & Co. KGaA", + 0x04BD: "AlbynMedical", + 0x04BE: "Averos FZCO", + 0x04BF: "VIT Initiative, LLC", + 0x04C0: "Statsports International", + 0x04C1: "Sospitas, s.r.o.", + 0x04C2: "Dmet Products Corp.", + 0x04C3: "Mantracourt Electronics Limited", + 0x04C4: "TeAM Hutchins AB", + 0x04C5: "Seibert Williams Glass, LLC", + 0x04C6: "Insta GmbH", + 0x04C7: "Svantek Sp. z o.o.", + 0x04C8: "Shanghai Flyco Electrical Appliance Co., Ltd.", + 0x04C9: "Thornwave Labs Inc", + 0x04CA: "Steiner-Optik GmbH", + 0x04CB: "Novo Nordisk A/S", + 0x04CC: "Enflux Inc.", + 0x04CD: "Safetech Products LLC", + 0x04CE: "GOOOLED S.R.L.", + 0x04CF: "DOM Sicherheitstechnik GmbH & Co. KG", + 0x04D0: "Olympus Corporation", + 0x04D1: "KTS GmbH", + 0x04D2: "Anloq Technologies Inc.", + 0x04D3: "Queercon, Inc", + 0x04D4: "5th Element Ltd", + 0x04D5: "Gooee Limited", + 0x04D6: "LUGLOC LLC", + 0x04D7: "Blincam, Inc.", + 0x04D8: "FUJIFILM Corporation", + 0x04D9: "RandMcNally", + 0x04DA: "Franceschi Marina snc", + 0x04DB: "Engineered Audio, LLC.", + 0x04DC: "IOTTIVE (OPC) PRIVATE LIMITED", + 0x04DD: "4MOD Technology", + 0x04DE: "Lutron Electronics Co., Inc.", + 0x04DF: "Emerson", + 0x04E0: "Guardtec, Inc.", + 0x04E1: "REACTEC LIMITED", + 0x04E2: "EllieGrid", + 0x04E3: "Under Armour", + 0x04E4: "Woodenshark", + 0x04E5: "Avack Oy", + 0x04E6: "Smart Solution Technology, Inc.", + 0x04E7: "REHABTRONICS INC.", + 0x04E8: "STABILO International", + 0x04E9: "Busch Jaeger Elektro GmbH", + 0x04EA: "Pacific Bioscience Laboratories, Inc", + 0x04EB: "Bird Home Automation GmbH", + 0x04EC: "Motorola Solutions", + 0x04ED: "R9 Technology, Inc.", + 0x04EE: "Auxivia", + 0x04EF: "DaisyWorks, Inc", + 0x04F0: "Kosi Limited", + 0x04F1: "Theben AG", + 0x04F2: "InDreamer Techsol Private Limited", + 0x04F3: "Cerevast Medical", + 0x04F4: "ZanCompute Inc.", + 0x04F5: "Pirelli Tyre S.P.A.", + 0x04F6: "McLear Limited", + 0x04F7: "Shenzhen Huiding Technology Co.,Ltd.", + 0x04F8: "Convergence Systems Limited", + 0x04F9: "Interactio", + 0x04FA: "Androtec GmbH", + 0x04FB: "Benchmark Drives GmbH & Co. KG", + 0x04FC: "SwingLync L. L. C.", + 0x04FD: "Tapkey GmbH", + 0x04FE: "Woosim Systems Inc.", + 0x04FF: "Microsemi Corporation", + 0x0500: "Wiliot LTD.", + 0x0501: "Polaris IND", + 0x0502: "Specifi-Kali LLC", + 0x0503: "Locoroll, Inc", + 0x0504: "PHYPLUS Inc", + 0x0505: "Inplay Technologies LLC", + 0x0506: "Hager", + 0x0507: "Yellowcog", + 0x0508: "Axes System sp. z o. o.", + 0x0509: "myLIFTER Inc.", + 0x050A: "Shake-on B.V.", + 0x050B: "Vibrissa Inc.", + 0x050C: "OSRAM GmbH", + 0x050D: "TRSystems GmbH", + 0x050E: "Yichip Microelectronics (Hangzhou) Co.,Ltd.", + 0x050F: "Foundation Engineering LLC", + 0x0510: "UNI-ELECTRONICS, INC.", + 0x0511: "Brookfield Equinox LLC", + 0x0512: "Soprod SA", + 0x0513: "9974091 Canada Inc.", + 0x0514: "FIBRO GmbH", + 0x0515: "RB Controls Co., Ltd.", + 0x0516: "Footmarks", + 0x0517: "Amtronic Sverige AB (formerly Amcore AB)", + 0x0518: "MAMORIO.inc", + 0x0519: "Tyto Life LLC", + 0x051A: "Leica Camera AG", + 0x051B: "Angee Technologies Ltd.", + 0x051C: "EDPS", + 0x051D: "OFF Line Co., Ltd.", + 0x051E: "Detect Blue Limited", + 0x051F: "Setec Pty Ltd", + 0x0520: "Target Corporation", + 0x0521: "IAI Corporation", + 0x0522: "NS Tech, Inc.", + 0x0523: "MTG Co., Ltd.", + 0x0524: "Hangzhou iMagic Technology Co., Ltd", + 0x0525: "HONGKONG NANO IC TECHNOLOGIES CO., LIMITED", + 0x0526: "Honeywell International Inc.", + 0x0527: "Albrecht JUNG", + 0x0528: "Lunera Lighting Inc.", + 0x0529: "Lumen UAB", + 0x052A: "Keynes Controls Ltd", + 0x052B: "Novartis AG", + 0x052C: "Geosatis SA", + 0x052D: "EXFO, Inc.", + 0x052E: "LEDVANCE GmbH", + 0x052F: "Center ID Corp.", + 0x0530: "Adolene, Inc.", + 0x0531: "D&M Holdings Inc.", + 0x0532: "CRESCO Wireless, Inc.", + 0x0533: "Nura Operations Pty Ltd", + 0x0534: "Frontiergadget, Inc.", + 0x0535: "Smart Component Technologies Limited", + 0x0536: "ZTR Control Systems LLC", + 0x0537: "MetaLogics Corporation", + 0x0538: "Medela AG", + 0x0539: "OPPLE Lighting Co., Ltd", + 0x053A: "Savitech Corp.,", + 0x053B: "prodigy", + 0x053C: "Screenovate Technologies Ltd", + 0x053D: "TESA SA", + 0x053E: "CLIM8 LIMITED", + 0x053F: "Silergy Corp", + 0x0540: "SilverPlus, Inc", + 0x0541: "Sharknet srl", + 0x0542: "Mist Systems, Inc.", + 0x0543: "MIWA LOCK CO.,Ltd", + 0x0544: "OrthoSensor, Inc.", + 0x0545: "Candy Hoover Group s.r.l", + 0x0546: "Apexar Technologies S.A.", + 0x0547: "LOGICDATA d.o.o.", + 0x0548: "Knick Elektronische Messgeraete GmbH & Co. KG", + 0x0549: "Smart Technologies and Investment Limited", + 0x054A: "Linough Inc.", + 0x054B: "Advanced Electronic Designs, Inc.", + 0x054C: "Carefree Scott Fetzer Co Inc", + 0x054D: "Sensome", + 0x054E: "FORTRONIK storitve d.o.o.", + 0x054F: "Sinnoz", + 0x0550: "Versa Networks, Inc.", + 0x0551: "Sylero", + 0x0552: "Avempace SARL", + 0x0553: "Nintendo Co., Ltd.", + 0x0554: "National Instruments", + 0x0555: "KROHNE Messtechnik GmbH", + 0x0556: "Otodynamics Ltd", + 0x0557: "Arwin Technology Limited", + 0x0558: "benegear, inc.", + 0x0559: "Newcon Optik", + 0x055A: "CANDY HOUSE, Inc.", + 0x055B: "FRANKLIN TECHNOLOGY INC", + 0x055C: "Lely", + 0x055D: "Valve Corporation", + 0x055E: "Hekatron Vertriebs GmbH", + 0x055F: "PROTECH S.A.S. DI GIRARDI ANDREA & C.", + 0x0560: "Sarita CareTech APS (formerly Sarita CareTech IVS)", + 0x0561: "Finder S.p.A.", + 0x0562: "Thalmic Labs Inc.", + 0x0563: "Steinel Vertrieb GmbH", + 0x0564: "Beghelli Spa", + 0x0565: "Beijing Smartspace Technologies Inc.", + 0x0566: "CORE TRANSPORT TECHNOLOGIES NZ LIMITED", + 0x0567: "Xiamen Everesports Goods Co., Ltd", + 0x0568: "Bodyport Inc.", + 0x0569: "Audionics System, INC.", + 0x056A: "Flipnavi Co.,Ltd.", + 0x056B: "Rion Co., Ltd.", + 0x056C: "Long Range Systems, LLC", + 0x056D: "Redmond Industrial Group LLC", + 0x056E: "VIZPIN INC.", + 0x056F: "BikeFinder AS", + 0x0570: "Consumer Sleep Solutions LLC", + 0x0571: "PSIKICK, INC.", + 0x0572: "AntTail.com", + 0x0573: "Lighting Science Group Corp.", + 0x0574: "AFFORDABLE ELECTRONICS INC", + 0x0575: "Integral Memroy Plc", + 0x0576: "Globalstar, Inc.", + 0x0577: "True Wearables, Inc.", + 0x0578: "Wellington Drive Technologies Ltd", + 0x0579: "Ensemble Tech Private Limited", + 0x057A: "OMNI Remotes", + 0x057B: "Duracell U.S. Operations Inc.", + 0x057C: "Toor Technologies LLC", + 0x057D: "Instinct Performance", + 0x057E: "Beco, Inc", + 0x057F: "Scuf Gaming International, LLC", + 0x0580: "ARANZ Medical Limited", + 0x0581: "LYS TECHNOLOGIES LTD", + 0x0582: "Breakwall Analytics, LLC", + 0x0583: "Code Blue Communications", + 0x0584: "Gira Giersiepen GmbH & Co. KG", + 0x0585: "Hearing Lab Technology", + 0x0586: "LEGRAND", + 0x0587: "Derichs GmbH", + 0x0588: "ALT-TEKNIK LLC", + 0x0589: "Star Technologies", + 0x058A: "START TODAY CO.,LTD.", + 0x058B: "Maxim Integrated Products", + 0x058C: "MERCK Kommanditgesellschaft auf Aktien", + 0x058D: "Jungheinrich Aktiengesellschaft", + 0x058E: "Oculus VR, LLC", + 0x058F: "HENDON SEMICONDUCTORS PTY LTD", + 0x0590: "Pur3 Ltd", + 0x0591: "Viasat Group S.p.A.", + 0x0592: "IZITHERM", + 0x0593: "Spaulding Clinical Research", + 0x0594: "Kohler Company", + 0x0595: "Inor Process AB", + 0x0596: "My Smart Blinds", + 0x0597: "RadioPulse Inc", + 0x0598: "rapitag GmbH", + 0x0599: "Lazlo326, LLC.", + 0x059A: "Teledyne Lecroy, Inc.", + 0x059B: "Dataflow Systems Limited", + 0x059C: "Macrogiga Electronics", + 0x059D: "Tandem Diabetes Care", + 0x059E: "Polycom, Inc.", + 0x059F: "Fisher & Paykel Healthcare", + 0x05A0: "RCP Software Oy", + 0x05A1: "Shanghai Xiaoyi Technology Co.,Ltd.", + 0x05A2: "ADHERIUM(NZ) LIMITED", + 0x05A3: "Axiomware Systems Incorporated", + 0x05A4: "O. E. M. Controls, Inc.", + 0x05A5: "Kiiroo BV", + 0x05A6: "Telecon Mobile Limited", + 0x05A7: "Sonos Inc", + 0x05A8: "Tom Allebrandi Consulting", + 0x05A9: "Monidor", + 0x05AA: "Tramex Limited", + 0x05AB: "Nofence AS", + 0x05AC: "GoerTek Dynaudio Co., Ltd.", + 0x05AD: "INIA", + 0x05AE: "CARMATE MFG.CO.,LTD", + 0x05AF: "OV LOOP, INC. (formerly ONvocal)", + 0x05B0: "NewTec GmbH", + 0x05B1: "Medallion Instrumentation Systems", + 0x05B2: "CAREL INDUSTRIES S.P.A.", + 0x05B3: "Parabit Systems, Inc.", + 0x05B4: "White Horse Scientific ltd", + 0x05B5: "verisilicon", + 0x05B6: "Elecs Industry Co.,Ltd.", + 0x05B7: "Beijing Pinecone Electronics Co.,Ltd.", + 0x05B8: "Ambystoma Labs Inc.", + 0x05B9: "Suzhou Pairlink Network Technology", + 0x05BA: "igloohome", + 0x05BB: "Oxford Metrics plc", + 0x05BC: "Leviton Mfg. Co., Inc.", + 0x05BD: "ULC Robotics Inc.", + 0x05BE: "RFID Global by Softwork SrL", + 0x05BF: "Real-World-Systems Corporation", + 0x05C0: "Nalu Medical, Inc.", + 0x05C1: "P.I.Engineering", + 0x05C2: "Grote Industries", + 0x05C3: "Runtime, Inc.", + 0x05C4: "Codecoup sp. z o.o. sp. k.", + 0x05C5: "SELVE GmbH & Co. KG", + 0x05C6: "Smart Animal Training Systems, LLC", + 0x05C7: "Lippert Components, INC", + 0x05C8: "SOMFY SAS", + 0x05C9: "TBS Electronics B.V.", + 0x05CA: "MHL Custom Inc", + 0x05CB: "LucentWear LLC", + 0x05CC: "WATTS ELECTRONICS", + 0x05CD: "RJ Brands LLC", + 0x05CE: "V-ZUG Ltd", + 0x05CF: "Biowatch SA", + 0x05D0: "Anova Applied Electronics", + 0x05D1: "Lindab AB", + 0x05D2: "frogblue TECHNOLOGY GmbH", + 0x05D3: "Acurable Limited", + 0x05D4: "LAMPLIGHT Co., Ltd.", + 0x05D5: "TEGAM, Inc.", + 0x05D6: "Zhuhai Jieli technology Co.,Ltd", + 0x05D7: "modum.io AG", + 0x05D8: "Farm Jenny LLC", + 0x05D9: "Toyo Electronics Corporation", + 0x05DA: "Applied Neural Research Corp", + 0x05DB: "Avid Identification Systems, Inc.", + 0x05DC: "Petronics Inc.", + 0x05DD: "essentim GmbH", + 0x05DE: "QT Medical INC.", + 0x05DF: "VIRTUALCLINIC.DIRECT LIMITED", + 0x05E0: "Viper Design LLC", + 0x05E1: "Human, Incorporated", + 0x05E2: "stAPPtronics GmbH", + 0x05E3: "Elemental Machines, Inc.", + 0x05E4: "Taiyo Yuden Co., Ltd", + 0x05E5: "INEO ENERGY& SYSTEMS", + 0x05E6: "Motion Instruments Inc.", + 0x05E7: "PressurePro", + 0x05E8: "COWBOY", + 0x05E9: "iconmobile GmbH", + 0x05EA: "ACS-Control-System GmbH", + 0x05EB: "Bayerische Motoren Werke AG", + 0x05EC: "Gycom Svenska AB", + 0x05ED: "Fuji Xerox Co., Ltd", + 0x05EE: "Glide Inc.", + 0x05EF: "SIKOM AS", + 0x05F0: "beken", + 0x05F1: "The Linux Foundation", + 0x05F2: "Try and E CO.,LTD.", + 0x05F3: "SeeScan", + 0x05F4: "Clearity, LLC", + 0x05F5: "GS TAG", + 0x05F6: "DPTechnics", + 0x05F7: "TRACMO, INC.", + 0x05F8: "Anki Inc.", + 0x05F9: "Hagleitner Hygiene International GmbH", + 0x05FA: "Konami Sports Life Co., Ltd.", + 0x05FB: "Arblet Inc.", + 0x05FC: "Masbando GmbH", + 0x05FD: "Innoseis", + 0x05FE: "Niko nv", + 0x05FF: "Wellnomics Ltd", + 0x0600: "iRobot Corporation", + 0x0601: "Schrader Electronics", + 0x0602: "Geberit International AG", + 0x0603: "Fourth Evolution Inc", + 0x0604: "Cell2Jack LLC", + 0x0605: "FMW electronic Futterer u. Maier-Wolf OHG", + 0x0606: "John Deere", + 0x0607: "Rookery Technology Ltd", + 0x0608: "KeySafe-Cloud", + 0x0609: "BUCHI Labortechnik AG", + 0x060A: "IQAir AG", + 0x060B: "Triax Technologies Inc", + 0x060C: "Vuzix Corporation", + 0x060D: "TDK Corporation", + 0x060E: "Blueair AB", + 0x060F: "Signify Netherlands", + 0x0610: "ADH GUARDIAN USA LLC", + 0x0611: "Beurer GmbH", + 0x0612: "Playfinity AS", + 0x0613: "Hans Dinslage GmbH", + 0x0614: "OnAsset Intelligence, Inc.", + 0x0615: "INTER ACTION Corporation", + 0x0616: "OS42 UG (haftungsbeschraenkt)", + 0x0617: "WIZCONNECTED COMPANY LIMITED", + 0x0618: "Audio-Technica Corporation", + 0x0619: "Six Guys Labs, s.r.o.", + 0x061A: "R.W. Beckett Corporation", + 0x061B: "silex technology, inc.", + 0x061C: "Univations Limited", + 0x061D: "SENS Innovation ApS", + 0x061E: "Diamond Kinetics, Inc.", + 0x061F: "Phrame Inc.", + 0x0620: "Forciot Oy", + 0x0621: "Noordung d.o.o.", + 0x0622: "Beam Labs, LLC", + 0x0623: "Philadelphia Scientific (U.K.) Limited", + 0x0624: "Biovotion AG", + 0x0625: "Square Panda, Inc.", + 0x0626: "Amplifico", + 0x0627: "WEG S.A.", + 0x0628: "Ensto Oy", + 0x0629: "PHONEPE PVT LTD", + 0x062A: "Lunatico Astronomia SL", + 0x062B: "MinebeaMitsumi Inc.", + 0x062C: "ASPion GmbH", + 0x062D: "Vossloh-Schwabe Deutschland GmbH", + 0x062E: "Procept", + 0x062F: "ONKYO Corporation", + 0x0630: "Asthrea D.O.O.", + 0x0631: "Fortiori Design LLC", + 0x0632: "Hugo Muller GmbH & Co KG", + 0x0633: "Wangi Lai PLT", + 0x0634: "Fanstel Corp", + 0x0635: "Crookwood", + 0x0636: "ELECTRONICA INTEGRAL DE SONIDO S.A.", + 0x0637: "GiP Innovation Tools GmbH", + 0x0638: "LX SOLUTIONS PTY LIMITED", + 0x0639: "Shenzhen Minew Technologies Co., Ltd.", + 0x063A: "Prolojik Limited", + 0x063B: "Kromek Group Plc", + 0x063C: "Contec Medical Systems Co., Ltd.", + 0x063D: "Xradio Technology Co.,Ltd.", + 0x063E: "The Indoor Lab, LLC", + 0x063F: "LDL TECHNOLOGY", + 0x0640: "Parkifi", + 0x0641: "Revenue Collection Systems FRANCE SAS", + 0x0642: "Bluetrum Technology Co.,Ltd", + 0x0643: "makita corporation", + 0x0644: "Apogee Instruments", + 0x0645: "BM3", + 0x0646: "SGV Group Holding GmbH & Co. KG", + 0x0647: "MED-EL", + 0x0648: "Ultune Technologies", + 0x0649: "Ryeex Technology Co.,Ltd.", + 0x064A: "Open Research Institute, Inc.", + 0x064B: "Scale-Tec, Ltd", + 0x064C: "Zumtobel Group AG", + 0x064D: "iLOQ Oy", + 0x064E: "KRUXWorks Technologies Private Limited", + 0x064F: "Digital Matter Pty Ltd", + 0x0650: "Coravin, Inc.", + 0x0651: "Stasis Labs, Inc.", + 0x0652: "ITZ Innovations- und Technologiezentrum GmbH", + 0x0653: "Meggitt SA", + 0x0654: "Ledlenser GmbH & Co. KG", + 0x0655: "Renishaw PLC", + 0x0656: "ZhuHai AdvanPro Technology Company Limited", + 0x0657: "Meshtronix Limited", + 0x0658: "Payex Norge AS", + 0x0659: "UnSeen Technologies Oy", + 0x065A: "Zound Industries International AB", + 0x065B: "Sesam Solutions BV", + 0x065C: "PixArt Imaging Inc.", + 0x065D: "Panduit Corp.", + 0x065E: "Alo AB", + 0x065F: "Ricoh Company Ltd", + 0x0660: "RTC Industries, Inc.", + 0x0661: "Mode Lighting Limited", + 0x0662: "Particle Industries, Inc.", + 0x0663: "Advanced Telemetry Systems, Inc.", + 0x0664: "RHA TECHNOLOGIES LTD", + 0x0665: "Pure International Limited", + 0x0666: "WTO Werkzeug-Einrichtungen GmbH", + 0x0667: "Spark Technology Labs Inc.", + 0x0668: "Bleb Technology srl", + 0x0669: "Livanova USA, Inc.", + 0x066A: "Brady Worldwide Inc.", + 0x066B: "DewertOkin GmbH", + 0x066C: "Ztove ApS", + 0x066D: "Venso EcoSolutions AB", + 0x066E: "Eurotronik Kranj d.o.o.", + 0x066F: "Hug Technology Ltd", + 0x0670: "Gema Switzerland GmbH", + 0x0671: "Buzz Products Ltd.", + 0x0672: "Kopi", + 0x0673: "Innova Ideas Limited", + 0x0674: "BeSpoon", + 0x0675: "Deco Enterprises, Inc.", + 0x0676: "Expai Solutions Private Limited", + 0x0677: "Innovation First, Inc.", + 0x0678: "SABIK Offshore GmbH", + 0x0679: "4iiii Innovations Inc.", + 0x067A: "The Energy Conservatory, Inc.", + 0x067B: "I.FARM, INC.", + 0x067C: "Tile, Inc.", + 0x067D: "Form Athletica Inc.", + 0x067E: "MbientLab Inc", + 0x067F: "NETGRID S.N.C. DI BISSOLI MATTEO, CAMPOREALE SIMONE, TOGNETTI FEDERICO", + 0x0680: "Mannkind Corporation", + 0x0681: "Trade FIDES a.s.", + 0x0682: "Photron Limited", + 0x0683: "Eltako GmbH", + 0x0684: "Dermalapps, LLC", + 0x0685: "Greenwald Industries", + 0x0686: "inQs Co., Ltd.", + 0x0687: "Cherry GmbH", + 0x0688: "Amsted Digital Solutions Inc.", + 0x0689: "Tacx b.v.", + 0x068A: "Raytac Corporation", + 0x068B: "Jiangsu Teranovo Tech Co., Ltd.", + 0x068C: "Changzhou Sound Dragon Electronics and Acoustics Co., Ltd", + 0x068D: "JetBeep Inc.", + 0x068E: "Razer Inc.", + 0x068F: "JRM Group Limited", + 0x0690: "Eccrine Systems, Inc.", + 0x0691: "Curie Point AB", + 0x0692: "Georg Fischer AG", + 0x0693: "Hach - Danaher", + 0x0694: "T&A Laboratories LLC", + 0x0695: "Koki Holdings Co., Ltd.", + 0x0696: "Gunakar Private Limited", + 0x0697: "Stemco Products Inc", + 0x0698: "Wood IT Security, LLC", + 0x0699: "RandomLab SAS", + 0x069A: "Adero, Inc. (formerly as TrackR, Inc.)", + 0x069B: "Dragonchip Limited", + 0x069C: "Noomi AB", + 0x069D: "Vakaros LLC", + 0x069E: "Delta Electronics, Inc.", + 0x069F: "FlowMotion Technologies AS", + 0x06A0: "OBIQ Location Technology Inc.", + 0x06A1: "Cardo Systems, Ltd", + 0x06A2: "Globalworx GmbH", + 0x06A3: "Nymbus, LLC", + 0x06A4: "Sanyo Techno Solutions Tottori Co., Ltd.", + 0x06A5: "TEKZITEL PTY LTD", + 0x06A6: "Roambee Corporation", + 0x06A7: "Chipsea Technologies (ShenZhen) Corp.", + 0x06A8: "GD Midea Air-Conditioning Equipment Co., Ltd.", + 0x06A9: "Soundmax Electronics Limited", + 0x06AA: "Produal Oy", + 0x06AB: "HMS Industrial Networks AB", + 0x06AC: "Ingchips Technology Co., Ltd.", + 0x06AD: "InnovaSea Systems Inc.", + 0x06AE: "SenseQ Inc.", + 0x06AF: "Shoof Technologies", + 0x06B0: "BRK Brands, Inc.", + 0x06B1: "SimpliSafe, Inc.", + 0x06B2: "Tussock Innovation 2013 Limited", + 0x06B3: "The Hablab ApS", + 0x06B4: "Sencilion Oy", + 0x06B5: "Wabilogic Ltd.", + 0x06B6: "Sociometric Solutions, Inc.", + 0x06B7: "iCOGNIZE GmbH", + 0x06B8: "ShadeCraft, Inc", + 0x06B9: "Beflex Inc.", + 0x06BA: "Beaconzone Ltd", + 0x06BB: "Leaftronix Analogic Solutions Private Limited", + 0x06BC: "TWS Srl", + 0x06BD: "ABB Oy", + 0x06BE: "HitSeed Oy", + 0x06BF: "Delcom Products Inc.", + 0x06C0: "CAME S.p.A.", + 0x06C1: "Alarm.com Holdings, Inc", + 0x06C2: "Measurlogic Inc.", + 0x06C3: "King I Electronics.Co.,Ltd", + 0x06C4: "Dream Labs GmbH", + 0x06C5: "Urban Compass, Inc", + 0x06C6: "Simm Tronic Limited", + 0x06C7: "Somatix Inc", + 0x06C8: "Storz & Bickel GmbH & Co. KG", + 0x06C9: "MYLAPS B.V.", + 0x06CA: "Shenzhen Zhongguang Infotech Technology Development Co., Ltd", + 0x06CB: "Dyeware, LLC", + 0x06CC: "Dongguan SmartAction Technology Co.,Ltd.", + 0x06CD: "DIG Corporation", + 0x06CE: "FIOR & GENTZ", + 0x06CF: "Belparts N.V.", + 0x06D0: "Etekcity Corporation", + 0x06D1: "Meyer Sound Laboratories, Incorporated", + 0x06D2: "CeoTronics AG", + 0x06D3: "TriTeq Lock and Security, LLC", + 0x06D4: "DYNAKODE TECHNOLOGY PRIVATE LIMITED", + 0x06D5: "Sensirion AG", + 0x06D6: "JCT Healthcare Pty Ltd", + 0x06D7: "FUBA Automotive Electronics GmbH", + 0x06D8: "AW Company", + 0x06D9: "Shanghai Mountain View Silicon Co.,Ltd.", + 0x06DA: "Zliide Technologies ApS", + 0x06DB: "Automatic Labs, Inc.", + 0x06DC: "Industrial Network Controls, LLC", + 0x06DD: "Intellithings Ltd.", + 0x06DE: "Navcast, Inc.", + 0x06DF: "Hubbell Lighting, Inc.", + 0x06E0: "Avaya ", + 0x06E1: "Milestone AV Technologies LLC", + 0x06E2: "Alango Technologies Ltd", + 0x06E3: "Spinlock Ltd", + 0x06E4: "Aluna", + 0x06E5: "OPTEX CO.,LTD.", + 0x06E6: "NIHON DENGYO KOUSAKU", + 0x06E7: "VELUX A/S", + 0x06E8: "Almendo Technologies GmbH", + 0x06E9: "Zmartfun Electronics, Inc.", + 0x06EA: "SafeLine Sweden AB", + 0x06EB: "Houston Radar LLC", + 0x06EC: "Sigur", + 0x06ED: "J Neades Ltd", + 0x06EE: "Avantis Systems Limited", + 0x06EF: "ALCARE Co., Ltd.", + 0x06F0: "Chargy Technologies, SL", + 0x06F1: "Shibutani Co., Ltd.", + 0x06F2: "Trapper Data AB", + 0x06F3: "Alfred International Inc.", + 0x06F4: "Near Field Solutions Ltd", + 0x06F5: "Vigil Technologies Inc.", + 0x06F6: "Vitulo Plus BV", + 0x06F7: "WILKA Schliesstechnik GmbH", + 0x06F8: "BodyPlus Technology Co.,Ltd", + 0x06F9: "happybrush GmbH", + 0x06FA: "Enequi AB", + 0x06FB: "Sartorius AG", + 0x06FC: "Tom Communication Industrial Co.,Ltd.", + 0x06FD: "ESS Embedded System Solutions Inc.", + 0x06FE: "Mahr GmbH", + 0x06FF: "Redpine Signals Inc", + 0x0700: "TraqFreq LLC", + 0x0701: "PAFERS TECH", + 0x0702: "Akciju sabiedriba \"SAF TEHNIKA\"", + 0x0703: "Beijing Jingdong Century Trading Co., Ltd.", + 0x0704: "JBX Designs Inc.", + 0x0705: "AB Electrolux", + 0x0706: "Wernher von Braun Center for ASdvanced Research", + 0x0707: "Essity Hygiene and Health Aktiebolag", + 0x0708: "Be Interactive Co., Ltd", + 0x0709: "Carewear Corp.", + 0x070A: "Huf Hülsbeck & Fürst GmbH & Co. KG", + 0x070B: "Element Products, Inc.", + 0x070C: "Beijing Winner Microelectronics Co.,Ltd", + 0x070D: "SmartSnugg Pty Ltd", + 0x070E: "FiveCo Sarl", + 0x070F: "California Things Inc.", + 0x0710: "Audiodo AB", + 0x0711: "ABAX AS", + 0x0712: "Bull Group Company Limited", + 0x0713: "Respiri Limited", + 0x0714: "MindPeace Safety LLC", + 0x0715: "Vgyan Solutions", + 0x0716: "Altonics", + 0x0717: "iQsquare BV", + 0x0718: "IDIBAIX enginneering", + 0x0719: "ECSG", + 0x071A: "REVSMART WEARABLE HK CO LTD", + 0x071B: "Precor", + 0x071C: "F5 Sports, Inc", + 0x071D: "exoTIC Systems", + 0x071E: "DONGGUAN HELE ELECTRONICS CO., LTD", + 0x071F: "Dongguan Liesheng Electronic Co.Ltd", + 0x0720: "Oculeve, Inc.", + 0x0721: "Clover Network, Inc.", + 0x0722: "Xiamen Eholder Electronics Co.Ltd", + 0x0723: "Ford Motor Company", + 0x0724: "Guangzhou SuperSound Information Technology Co.,Ltd", + 0x0725: "Tedee Sp. z o.o.", + 0x0726: "PHC Corporation", + 0x0727: "STALKIT AS", + 0x0728: "Eli Lilly and Company", + 0x0729: "SwaraLink Technologies", + 0x072A: "JMR embedded systems GmbH", + 0x072B: "Bitkey Inc.", + 0x072C: "GWA Hygiene GmbH", + 0x072D: "Safera Oy", + 0x072E: "Open Platform Systems LLC", + 0x072F: "OnePlus Electronics (Shenzhen) Co., Ltd.", + 0x0730: "Wildlife Acoustics, Inc.", + 0x0731: "ABLIC Inc.", + 0x0732: "Dairy Tech, Inc.", + 0x0733: "Iguanavation, Inc.", + 0x0734: "DiUS Computing Pty Ltd", + 0x0735: "UpRight Technologies LTD", + 0x0736: "FrancisFund, LLC", + 0x0737: "LLC Navitek", + 0x0738: "Glass Security Pte Ltd", + 0x0739: "Jiangsu Qinheng Co., Ltd.", + 0x073A: "Chandler Systems Inc.", + 0x073B: "Fantini Cosmi s.p.a.", + 0x073C: "Acubit ApS", + 0x073D: "Beijing Hao Heng Tian Tech Co., Ltd.", + 0x073E: "Bluepack S.R.L.", + 0x073F: "Beijing Unisoc Technologies Co., Ltd.", + 0x0740: "HITIQ LIMITED", + 0x0741: "MAC SRL", + 0x0742: "DML LLC", + 0x0743: "Sanofi", + 0x0744: "SOCOMEC", + 0x0745: "WIZNOVA, Inc.", + 0x0746: "Seitec Elektronik GmbH", + 0x0747: "OR Technologies Pty Ltd", + 0x0748: "GuangZhou KuGou Computer Technology Co.Ltd", + 0x0749: "DIAODIAO (Beijing) Technology Co., Ltd.", + 0x074A: "Illusory Studios LLC", + 0x074B: "Sarvavid Software Solutions LLP", + 0x074C: "iopool s.a.", + 0x074D: "Amtech Systems, LLC", + 0x074E: "EAGLE DETECTION SA", + 0x074F: "MEDIATECH S.R.L.", + 0x0750: "Hamilton Professional Services of Canada Incorporated", + 0x0751: "Changsha JEMO IC Design Co.,Ltd", + 0x0752: "Elatec GmbH", + 0x0753: "JLG Industries, Inc.", + 0x0754: "Michael Parkin", + 0x0755: "Brother Industries, Ltd", + 0x0756: "Lumens For Less, Inc", + 0x0757: "ELA Innovation", + 0x0758: "umanSense AB", + 0x0759: "Shanghai InGeek Cyber Security Co., Ltd.", + 0x075A: "HARMAN CO.,LTD.", + 0x075B: "Smart Sensor Devices AB", + 0x075C: "Antitronics Inc.", + 0x075D: "RHOMBUS SYSTEMS, INC.", + 0x075E: "Katerra Inc.", + 0x075F: "Remote Solution Co., LTD.", + 0x0760: "Vimar SpA", + 0x0761: "Mantis Tech LLC", + 0x0762: "TerOpta Ltd", + 0x0763: "PIKOLIN S.L.", + 0x0764: "WWZN Information Technology Company Limited", + 0x0765: "Voxx International", + 0x0766: "ART AND PROGRAM, INC.", + 0x0767: "NITTO DENKO ASIA TECHNICAL CENTRE PTE. LTD.", + 0x0768: "Peloton Interactive Inc.", + 0x0769: "Force Impact Technologies", + 0x076A: "Dmac Mobile Developments, LLC", + 0x076B: "Engineered Medical Technologies", + 0x076C: "Noodle Technology inc", + 0x076D: "Graesslin GmbH", + 0x076E: "WuQi technologies, Inc.", + 0x076F: "Successful Endeavours Pty Ltd", + 0x0770: "InnoCon Medical ApS", + 0x0771: "Corvex Connected Safety", + 0x0772: "Thirdwayv Inc.", + 0x0773: "Echoflex Solutions Inc.", + 0x0774: "C-MAX Asia Limited", + 0x0775: "4eBusiness GmbH", + 0x0776: "Cyber Transport Control GmbH", + 0x0777: "Cue", + 0x0778: "KOAMTAC INC.", + 0x0779: "Loopshore Oy", + 0x077A: "Niruha Systems Private Limited", + 0x077B: "AmaterZ, Inc.", + 0x077C: "radius co., ltd.", + 0x077D: "Sensority, s.r.o.", + 0x077E: "Sparkage Inc.", + 0x077F: "Glenview Software Corporation", + 0x0780: "Finch Technologies Ltd.", + 0x0781: "Qingping Technology (Beijing) Co., Ltd.", + 0x0782: "DeviceDrive AS", + 0x0783: "ESEMBER LIMITED LIABILITY COMPANY", + 0x0784: "audifon GmbH & Co. KG", + 0x0785: "O2 Micro, Inc.", + 0x0786: "HLP Controls Pty Limited", + 0x0787: "Pangaea Solution", + 0x0788: "BubblyNet, LLC", + 0x078A: "The Wildflower Foundation", + 0x078B: "Optikam Tech Inc.", + 0x078C: "MINIBREW HOLDING B.V", + 0x078D: "Cybex GmbH", + 0x078E: "FUJIMIC NIIGATA, INC.", + 0x078F: "Hanna Instruments, Inc.", + 0x0790: "KOMPAN A/S", + 0x0791: "Scosche Industries, Inc.", + 0x0792: "Provo Craft", + 0x0793: "AEV spol. s r.o.", + 0x0794: "The Coca-Cola Company", + 0x0795: "GASTEC CORPORATION", + 0x0796: "StarLeaf Ltd", + 0x0797: "Water-i.d. GmbH", + 0x0798: "HoloKit, Inc.", + 0x0799: "PlantChoir Inc.", + 0x079A: "GuangDong Oppo Mobile Telecommunications Corp., Ltd.", + 0x079B: "CST ELECTRONICS (PROPRIETARY) LIMITED", + 0x079C: "Sky UK Limited", + 0x079D: "Digibale Pty Ltd", + 0x079E: "Smartloxx GmbH", + 0x079F: "Pune Scientific LLP", + 0x07A0: "Regent Beleuchtungskorper AG", + 0x07A1: "Apollo Neuroscience, Inc.", + 0x07A2: "Roku, Inc.", + 0x07A3: "Comcast Cable", + 0x07A4: "Xiamen Mage Information Technology Co., Ltd.", + 0x07A5: "RAB Lighting, Inc.", + 0x07A6: "Musen Connect, Inc.", + 0x07A7: "Zume, Inc.", + 0x07A8: "conbee GmbH", + 0x07A9: "Bruel & Kjaer Sound & Vibration", + 0x07AA: "The Kroger Co.", + 0x07AB: "Granite River Solutions, Inc.", + 0x07AC: "LoupeDeck Oy", + 0x07AD: "New H3C Technologies Co.,Ltd", + 0x07AE: "Aurea Solucoes Tecnologicas Ltda.", + 0x07AF: "Hong Kong Bouffalo Lab Limited", + 0x07B0: "GV Concepts Inc.", + 0x07B1: "Thomas Dynamics, LLC", + 0x07B2: "Moeco IOT Inc.", + 0x07B3: "2N TELEKOMUNIKACE a.s.", + 0x07B4: "Hormann KG Antriebstechnik", + 0x07B5: "CRONO CHIP, S.L.", + 0x07B6: "Soundbrenner Limited", + 0x07B7: "ETABLISSEMENTS GEORGES RENAULT", + 0x07B8: "iSwip", + 0x07B9: "Epona Biotec Limited", + 0x07BA: "Battery-Biz Inc.", + 0x07BB: "EPIC S.R.L.", + 0x07BC: "KD CIRCUITS LLC", + 0x07BD: "Genedrive Diagnostics Ltd", + 0x07BE: "Axentia Technologies AB", + 0x07BF: "REGULA Ltd.", + 0x07C0: "Biral AG", + 0x07C1: "A.W. Chesterton Company", + 0x07C2: "Radinn AB", + 0x07C3: "CIMTechniques, Inc.", + 0x07C4: "Johnson Health Tech NA", + 0x07C5: "June Life, Inc.", + 0x07C6: "Bluenetics GmbH", + 0x07C7: "iaconicDesign Inc.", + 0x07C8: "WRLDS Creations AB", + 0x07C9: "Skullcandy, Inc.", + 0x07CA: "Modul-System HH AB", + 0x07CB: "West Pharmaceutical Services, Inc.", + 0x07CC: "Barnacle Systems Inc.", + 0x07CD: "Smart Wave Technologies Canada Inc", + 0x07CE: "Shanghai Top-Chip Microelectronics Tech. Co., LTD", + 0x07CF: "NeoSensory, Inc.", + 0x07D0: "Hangzhou Tuya Information Technology Co., Ltd", + 0x07D1: "Shanghai Panchip Microelectronics Co., Ltd", + 0x07D2: "React Accessibility Limited", + 0x07D3: "LIVNEX Co.,Ltd.", + 0x07D4: "Kano Computing Limited", + 0x07D5: "hoots classic GmbH", + 0x07D6: "ecobee Inc.", + 0x07D7: "Nanjing Qinheng Microelectronics Co., Ltd", + 0x07D8: "SOLUTIONS AMBRA INC.", + 0x07D9: "Micro-Design, Inc.", + 0x07DA: "STARLITE Co., Ltd.", + 0x07DB: "Remedee Labs", + 0x07DC: "ThingOS GmbH", + 0x07DD: "Linear Circuits", + 0x07DE: "Unlimited Engineering SL", + 0x07DF: "Snap-on Incorporated", + 0x07E0: "Edifier International Limited", + 0x07E1: "Lucie Labs", + 0x07E2: "Alfred Kaercher SE & Co. KG", + 0x07E3: "Audiowise Technology Inc.", + 0x07E4: "Geeksme S.L.", + 0x07E5: "Minut, Inc.", + 0x07E6: "Autogrow Systems Limited", + 0x07E7: "Komfort IQ, Inc.", + 0x07E8: "Packetcraft, Inc.", + 0x07E9: "Häfele GmbH & Co KG", + 0x07EA: "ShapeLog, Inc.", + 0x07EB: "NOVABASE S.R.L.", + 0x07EC: "Frecce LLC", + 0x07ED: "Joule IQ, INC.", + 0x07EE: "KidzTek LLC", + 0x07EF: "Aktiebolaget Sandvik Coromant", + 0x07F0: "e-moola.com Pty Ltd", + 0x07F1: "GSM Innovations Pty Ltd", + 0x07F2: "SERENE GROUP, INC", + 0x07F3: "DIGISINE ENERGYTECH CO. LTD.", + 0x07F4: "MEDIRLAB Orvosbiologiai Fejleszto Korlatolt Felelossegu Tarsasag", + 0x07F5: "Byton North America Corporation", + 0x07F6: "Shenzhen TonliScience and Technology Development Co.,Ltd", + 0x07F7: "Cesar Systems Ltd.", + 0x07F8: "quip NYC Inc.", + 0x07F9: "Direct Communication Solutions, Inc.", + 0x07FA: "Klipsch Group, Inc.", + 0x07FB: "Access Co., Ltd", + 0x07FC: "Renault SA", + 0x07FD: "JSK CO., LTD.", + 0x07FE: "BIROTA", + 0x07FF: "maxon motor ltd.", + 0x0800: "Optek", + 0x0801: "CRONUS ELECTRONICS LTD", + 0x0802: "NantSound, Inc.", + 0x0803: "Domintell s.a.", + 0x0804: "Andon Health Co.,Ltd", + 0x0805: "Urbanminded Ltd", + 0x0806: "TYRI Sweden AB", + 0x0807: "ECD Electronic Components GmbH Dresden", + 0x0808: "SISTEMAS KERN, SOCIEDAD ANÓMINA", + 0x0809: "Trulli Audio", + 0x080A: "Altaneos", + 0x080B: "Nanoleaf Canada Limited", + 0x080C: "Ingy B.V.", + 0x080D: "Azbil Co.", + 0x080E: "TATTCOM LLC", + 0x080F: "Paradox Engineering SA", + 0x0810: "LECO Corporation", + 0x0811: "Becker Antriebe GmbH", + 0x0812: "Mstream Technologies., Inc.", + 0x0813: "Flextronics International USA Inc.", + 0x0814: "Ossur hf.", + 0x0815: "SKC Inc", + 0x0816: "SPICA SYSTEMS LLC", + 0x0817: "Wangs Alliance Corporation", + 0x0818: "tatwah SA", + 0x0819: "Hunter Douglas Inc", + 0x081A: "Shenzhen Conex", + 0x081B: "DIM3", + 0x081C: "Bobrick Washroom Equipment, Inc.", + 0x081D: "Potrykus Holdings and Development LLC", + 0x081E: "iNFORM Technology GmbH", + 0x081F: "eSenseLab LTD", + 0x0820: "Brilliant Home Technology, Inc.", + 0x0821: "INOVA Geophysical, Inc.", + 0x0822: "adafruit industries", + 0x0823: "Nexite Ltd", + 0x0824: "8Power Limited", + 0x0825: "CME PTE. LTD.", + 0x0826: "Hyundai Motor Company", + 0x0827: "Kickmaker", + 0x0828: "Shanghai Suisheng Information Technology Co., Ltd.", + 0x0829: "HEXAGON", + 0x082A: "Mitutoyo Corporation", + 0x082B: "shenzhen fitcare electronics Co.,Ltd", + 0x082C: "INGICS TECHNOLOGY CO., LTD.", + 0x082D: "INCUS PERFORMANCE LTD.", + 0x082E: "ABB S.p.A.", + 0x082F: "Blippit AB", + 0x0830: "Core Health and Fitness LLC", + 0x0831: "Foxble, LLC", + 0x0832: "Intermotive,Inc.", + 0x0833: "Conneqtech B.V.", + 0x0834: "RIKEN KEIKI CO., LTD.,", + 0x0835: "Canopy Growth Corporation", + 0x0836: "Bitwards Oy", + 0x0837: "vivo Mobile Communication Co., Ltd.", + 0x0838: "Etymotic Research, Inc.", + 0x0839: "A puissance 3", + 0x083A: "BPW Bergische Achsen Kommanditgesellschaft", + 0x083B: "Piaggio Fast Forward", + 0x083C: "BeerTech LTD", + 0x083D: "Tokenize, Inc.", + 0x083E: "Zorachka LTD", + 0x083F: "D-Link Corp.", + 0x0840: "Down Range Systems LLC", + 0x0841: "General Luminaire (Shanghai) Co., Ltd.", + 0x0842: "Tangshan HongJia electronic technology co., LTD.", + 0x0843: "FRAGRANCE DELIVERY TECHNOLOGIES LTD", + 0x0844: "Pepperl + Fuchs GmbH", + 0x0845: "Dometic Corporation", + 0x0846: "USound GmbH", + 0x0847: "DNANUDGE LIMITED", + 0x0848: "JUJU JOINTS CANADA CORP.", + 0x0849: "Dopple Technologies B.V.", + 0x084A: "ARCOM", + 0x084B: "Biotechware SRL", + 0x084C: "ORSO Inc.", + 0x084D: "SafePort", + 0x084E: "Carol Cole Company", + 0x084F: "Embedded Fitness B.V.", + 0x0850: "Yealink (Xiamen) Network Technology Co.,LTD", + 0x0851: "Subeca, Inc.", + 0x0852: "Cognosos, Inc.", + 0x0853: "Pektron Group Limited", + 0x0854: "Tap Sound System", + 0x0855: "Helios Hockey, Inc.", + 0x0856: "Canopy Growth Corporation", + 0x0857: "Parsyl Inc", + 0x0858: "SOUNDBOKS", + 0x0859: "BlueUp", + 0x085A: "DAKATECH", + 0x085B: "RICOH ELECTRONIC DEVICES CO., LTD.", + 0x085C: "ACOS CO.,LTD.", + 0x085D: "Guilin Zhishen Information Technology Co.,Ltd.", + 0x085E: "Krog Systems LLC", + 0x085F: "COMPEGPS TEAM,SOCIEDAD LIMITADA", + 0x0860: "Alflex Products B.V.", + 0x0861: "SmartSensor Labs Ltd", + 0x0862: "SmartDrive Inc.", + 0x0863: "Yo-tronics Technology Co., Ltd.", + 0x0864: "Rafaelmicro", + 0x0865: "Emergency Lighting Products Limited", + 0x0866: "LAONZ Co.,Ltd", + 0x0867: "Western Digital Techologies, Inc.", + 0x0868: "WIOsense GmbH & Co. KG", + 0x0869: "EVVA Sicherheitstechnologie GmbH", + 0x086A: "Odic Incorporated", + 0x086B: "Pacific Track, LLC", + 0x086C: "Revvo Technologies, Inc.", + 0x086D: "Biometrika d.o.o.", + 0x086E: "Vorwerk Elektrowerke GmbH & Co. KG", + 0x086F: "Trackunit A/S", + 0x0870: "Wyze Labs, Inc", + 0x0871: "Dension Elektronikai Kft. (formerly: Dension Audio Systems Ltd.)", + 0x0872: "11 Health & Technologies Limited", + 0x0873: "Innophase Incorporated", + 0x0874: "Treegreen Limited", + 0x0875: "Berner International LLC", + 0x0876: "SmartResQ ApS", + 0x0877: "Tome, Inc.", + 0x0878: "The Chamberlain Group, Inc.", + 0x0879: "MIZUNO Corporation", + 0x087A: "ZRF, LLC", + 0x087B: "BYSTAMP", + 0x087C: "Crosscan GmbH", + 0x087D: "Konftel AB", + 0x087E: "1bar.net Limited", + 0x087F: "Phillips Connect Technologies LLC", + 0x0880: "imagiLabs AB", + 0x0881: "Optalert", + 0x0882: "PSYONIC, Inc.", + 0x0883: "Wintersteiger AG", + 0x0884: "Controlid Industria, Comercio de Hardware e Servicos de Tecnologia Ltda", + 0x0885: "LEVOLOR, INC.", + 0x0886: "Xsens Technologies B.V.", + 0x0887: "Hydro-Gear Limited Partnership", + 0x0888: "EnPointe Fencing Pty Ltd", + 0x0889: "XANTHIO", + 0x088A: "sclak s.r.l.", + 0x088B: "Tricorder Arraay Technologies LLC", + 0x088C: "GB Solution co.,Ltd", + 0x088D: "Soliton Systems K.K.", + 0x088E: "GIGA-TMS INC", + 0x088F: "Tait International Limited", + 0x0890: "NICHIEI INTEC CO., LTD.", + 0x0891: "SmartWireless GmbH & Co. KG", + 0x0892: "Ingenieurbuero Birnfeld UG (haftungsbeschraenkt)", + 0x0893: "Maytronics Ltd", + 0x0894: "EPIFIT", + 0x0895: "Gimer medical", + 0x0896: "Nokian Renkaat Oyj", + 0x0897: "Current Lighting Solutions LLC", + 0x0898: "Sensibo, Inc.", + 0x0899: "SFS unimarket AG", + 0x089A: "Private limited company \"Teltonika\"", + 0x089B: "Saucon Technologies", + 0x089C: "Embedded Devices Co. Company", + 0x089D: "J-J.A.D.E. Enterprise LLC", + 0x089E: "i-SENS, inc.", + 0x089F: "Witschi Electronic Ltd", + 0x08A0: "Aclara Technologies LLC", + 0x08A1: "EXEO TECH CORPORATION", + 0x08A2: "Epic Systems Co., Ltd.", + 0x08A3: "Hoffmann SE", + 0x08A4: "Realme Chongqing Mobile Telecommunications Corp., Ltd.", + 0x08A5: "UMEHEAL Ltd", + 0x08A6: "Intelligenceworks Inc.", + 0x08A7: "TGR 1.618 Limited", + 0x08A8: "Shanghai Kfcube Inc", + 0x08A9: "Fraunhofer IIS", + 0x08AA: "SZ DJI TECHNOLOGY CO.,LTD", + 0x08AB: "Coburn Technology, LLC", + 0x08AC: "Topre Corporation", + 0x08AD: "Kayamatics Limited", + 0x08AE: "Moticon ReGo AG", + 0x08AF: "Polidea Sp. z o.o.", + 0x08B0: "Trivedi Advanced Technologies LLC", + 0x08B1: "CORE|vision BV", + 0x08B2: "PF SCHWEISSTECHNOLOGIE GMBH", + 0x08B3: "IONIQ Skincare GmbH & Co. KG", + 0x08B4: "Sengled Co., Ltd.", + 0x08B5: "TransferFi", + 0x08B6: "Boehringer Ingelheim Vetmedica GmbH", + 0x08B7: "ABB Inc", + 0x08B8: "Check Technology Solutions LLC", + 0x08B9: "U-Shin Ltd.", + 0x08BA: "HYPER ICE, INC.", + 0x08BB: "Tokai-rika co.,ltd.", + 0x08BC: "Prevayl Limited", + 0x08BD: "bf1systems limited", + 0x08BE: "ubisys technologies GmbH", + 0x08BF: "SIRC Co., Ltd.", + 0x08C0: "Accent Advanced Systems SLU", + 0x08C1: "Rayden.Earth LTD", + 0x08C2: "Lindinvent AB", + 0x08C3: "CHIPOLO d.o.o.", + 0x08C4: "CellAssist, LLC", + 0x08C5: "J. Wagner GmbH", + 0x08C6: "Integra Optics Inc", + 0x08C7: "Monadnock Systems Ltd.", + 0x08C8: "Liteboxer Technologies Inc.", + 0x08C9: "Noventa AG", + 0x08CA: "Nubia Technology Co.,Ltd.", + 0x08CB: "JT INNOVATIONS LIMITED", + 0x08CC: "TGM TECHNOLOGY CO., LTD.", + 0x08CD: "ifly", + 0x08CE: "ZIMI CORPORATION", + 0x08CF: "betternotstealmybike UG (with limited liability)", + 0x08D0: "ESTOM Infotech Kft.", + 0x08D1: "Sensovium Inc.", + 0x08D2: "Virscient Limited", + 0x08D3: "Novel Bits, LLC", + 0x08D4: "ADATA Technology Co., LTD.", + 0x08D5: "KEYes", + 0x08D6: "Nome Oy", + 0x08D7: "Inovonics Corp", + 0x08D8: "WARES", + 0x08D9: "Pointr Labs Limited", + 0x08DA: "Miridia Technology Incorporated", + 0x08DB: "Tertium Technology", + 0x08DC: "SHENZHEN AUKEY E BUSINESS CO., LTD", + 0x08DD: "code-Q", + 0x08DE: "Tyco Electronics Corporation a TE Connectivity Ltd Company", + 0x08DF: "IRIS OHYAMA CO.,LTD.", + 0x08E0: "Philia Technology", + 0x08E1: "KOZO KEIKAKU ENGINEERING Inc.", + 0x08E2: "Shenzhen Simo Technology co. LTD", + 0x08E3: "Republic Wireless, Inc.", + 0x08E4: "Rashidov ltd", + 0x08E5: "Crowd Connected Ltd", + 0x08E6: "Eneso Tecnologia de Adaptacion S.L.", + 0x08E7: "Barrot Technology Limited", + 0x08E8: "Naonext", + 0x08E9: "Taiwan Intelligent Home Corp.", + 0x08EA: "COWBELL ENGINEERING CO.,LTD.", + 0x08EB: "Beijing Big Moment Technology Co., Ltd.", + 0x08EC: "Denso Corporation", + 0x08ED: "IMI Hydronic Engineering International SA", + 0x08EE: "ASKEY", + 0x08EF: "Cumulus Digital Systems, Inc", + 0x08F0: "Joovv, Inc.", + 0x08F1: "The L.S. Starrett Company", + 0x08F2: "Microoled", + 0x08F3: "PSP - Pauli Services & Products GmbH", + 0x08F4: "Kodimo Technologies Company Limited", + 0x08F5: "Tymtix Technologies Private Limited", + 0x08F6: "Dermal Photonics Corporation", + 0x08F7: "MTD Products Inc & Affiliates", + 0x08F8: "instagrid GmbH", + 0x08F9: "Spacelabs Medical Inc.", + 0x08FA: "Troo Corporation", + 0x08FB: "Darkglass Electronics Oy", + 0x08FC: "Hill-Rom", + 0x08FD: "BioIntelliSense, Inc.", + 0x08FE: "Ketronixs Sdn Bhd", + 0x08FF: "Plastimold Products, Inc", + 0x0900: "Beijing Zizai Technology Co., LTD.", + 0x0901: "Lucimed", + 0x0902: "TSC Auto-ID Technology Co., Ltd.", + 0x0903: "DATAMARS, Inc.", + 0x0904: "SUNCORPORATION", + 0x0905: "Yandex Services AG", + 0x0906: "Scope Logistical Solutions", + 0x0907: "User Hello, LLC", + 0x0908: "Pinpoint Innovations Limited", + 0x0909: "70mai Co.,Ltd.", + 0x090A: "Zhuhai Hoksi Technology CO.,LTD", + 0x090B: "EMBR labs, INC", + 0x090C: "Radiawave Technologies Co.,Ltd.", + 0x090D: "IOT Invent GmbH", + 0x090E: "OPTIMUSIOT TECH LLP", + 0x090F: "VC Inc.", + 0x0910: "ASR Microelectronics (Shanghai) Co., Ltd.", + 0x0911: "Douglas Lighting Controls Inc.", + 0x0912: "Nerbio Medical Software Platforms Inc", + 0x0913: "Braveheart Wireless, Inc.", + 0x0914: "INEO-SENSE", + 0x0915: "Honda Motor Co., Ltd.", + 0x0916: "Ambient Sensors LLC", + 0x0917: "ASR Microelectronics(ShenZhen)Co., Ltd.", + 0x0918: "Technosphere Labs Pvt. Ltd.", + 0x0919: "NO SMD LIMITED", + 0x091A: "Albertronic BV", + 0x091B: "Luminostics, Inc.", + 0x091C: "Oblamatik AG", + 0x091D: "Innokind, Inc.", + 0x091E: "Melbot Studios, Sociedad Limitada", + 0x091F: "Myzee Technology", + 0x0920: "Omnisense Limited", + 0x0921: "KAHA PTE. LTD.", + 0x0922: "Shanghai MXCHIP Information Technology Co., Ltd.", + 0x0923: "JSB TECH PTE LTD", + 0x0924: "Fundacion Tecnalia Research and Innovation", + 0x0925: "Yukai Engineering Inc.", + 0x0926: "Gooligum Technologies Pty Ltd", + 0x0927: "ROOQ GmbH", + 0x0928: "AiRISTA", + 0x0929: "Qingdao Haier Technology Co., Ltd.", + 0x092A: "Sappl Verwaltungs- und Betriebs GmbH", + 0x092B: "TekHome", + 0x092C: "PCI Private Limited", + 0x092D: "Leggett & Platt, Incorporated", + 0x092E: "PS GmbH", + 0x092F: "C.O.B.O. SpA", + 0x0930: "James Walker RotaBolt Limited", + 0x0931: "BREATHINGS Co., Ltd.", + 0x0932: "BarVision, LLC", + 0x0933: "SRAM", + 0x0934: "KiteSpring Inc.", + 0x0935: "Reconnect, Inc.", + 0x0936: "Elekon AG", + 0x0937: "RealThingks GmbH", + 0x0938: "Henway Technologies, LTD.", + 0x0939: "ASTEM Co.,Ltd.", + 0x093A: "LinkedSemi Microelectronics (Xiamen) Co., Ltd", + 0x093B: "ENSESO LLC", + 0x093C: "Xenoma Inc.", + 0x093D: "Adolf Wuerth GmbH & Co KG", + 0x093E: "Catalyft Labs, Inc.", + 0x093F: "JEPICO Corporation", + 0x0940: "Hero Workout GmbH", + 0x0941: "Rivian Automotive, LLC", + 0x0942: "TRANSSION HOLDINGS LIMITED", + 0x0943: "Inovonics Corp.", + 0x0944: "Agitron d.o.o.", + 0x0945: "Globe (Jiangsu) Co., Ltd", + 0x0946: "AMC International Alfa Metalcraft Corporation AG", + 0x0947: "First Light Technologies Ltd.", + 0x0948: "Wearable Link Limited", + 0x0949: "Metronom Health Europe", + 0x094A: "Zwift, Inc.", + 0x094B: "Kindeva Drug Delivery L.P.", + 0x094C: "GimmiSys GmbH", + 0x094D: "tkLABS INC.", + 0x094E: "PassiveBolt, Inc.", + 0x094F: "Limited Liability Company \"Mikrotikls\"", + 0x0950: "Capetech", + 0x0951: "PPRS", + 0x0952: "Apptricity Corporation", + 0x0953: "LogiLube, LLC", + 0x0954: "Julbo", + 0x0955: "Breville Group", + 0x0956: "Kerlink", + 0x0957: "Ohsung Electronics", + 0x0958: "ZTE Corporation", + 0x0959: "HerdDogg, Inc", + 0x095A: "Selekt Bilgisayar, lletisim Urunleri lnsaat Sanayi ve Ticaret Limited Sirketi", + 0x095B: "Lismore Instruments Limited", + 0x095C: "LogiLube, LLC", + 0x095D: "ETC", + 0x095E: "BioEchoNet inc.", + 0x095F: "NUANCE HEARING LTD", + 0x0960: "Sena Technologies Inc.", + 0x0961: "Linkura AB", + 0x0962: "GL Solutions K.K.", + 0x0963: "Moonbird BV", + 0x0964: "Countrymate Technology Limited", + 0x0965: "Asahi Kasei Corporation", + 0x0966: "PointGuard, LLC", + 0x0967: "Neo Materials and Consulting Inc.", + 0x0968: "Actev Motors, Inc.", + 0x0969: "Woan Technology (Shenzhen) Co., Ltd.", + 0x096A: "dricos, Inc.", + 0x096B: "Guide ID B.V.", + 0x096C: "9374-7319 Quebec inc", + 0x096D: "Gunwerks, LLC", + 0x096E: "Band Industries, inc.", + 0x096F: "Lund Motion Products, Inc.", + 0x0970: "IBA Dosimetry GmbH", + 0x0971: "GA", + 0x0972: "Closed Joint Stock Company \"Zavod Flometr\" (\"Zavod Flometr\" CJSC)", + 0x0973: "Popit Oy", + 0x0974: "ABEYE", + 0x0975: "BlueIOT(Beijing) Technology Co.,Ltd", + 0x0976: "Fauna Audio GmbH", + 0x0977: "TOYOTA motor corporation", + 0x0978: "ZifferEins GmbH & Co. KG", + 0x0979: "BIOTRONIK SE & Co. KG", + 0x097A: "CORE CORPORATION", + 0x097B: "CTEK Sweden AB", + 0x097C: "Thorley Industries, LLC", + 0x097D: "CLB B.V.", + 0x097E: "SonicSensory Inc", + 0x097F: "ISEMAR S.R.L.", + 0x0980: "DEKRA TESTING AND CERTIFICATION, S.A.U.", + 0x0981: "Bernard Krone Holding SE & Co.KG", + 0x0982: "ELPRO-BUCHS AG", + 0x0983: "Feedback Sports LLC", + 0x0984: "TeraTron GmbH", + 0x0985: "Lumos Health Inc.", + 0x0986: "Cello Hill, LLC", + 0x0987: "TSE BRAKES, INC.", + 0x0988: "BHM-Tech Produktionsgesellschaft m.b.H", + 0x0989: "WIKA Alexander Wiegand SE & Co.KG", + 0x098A: "Biovigil", + 0x098B: "Mequonic Engineering, S.L.", + 0x098C: "bGrid B.V.", + 0x098D: "C3-WIRELESS, LLC", + 0x098E: "ADVEEZ", + 0x098F: "Aktiebolaget Regin", + 0x0990: "Anton Paar GmbH", + 0x0991: "Telenor ASA", + 0x0992: "Big Kaiser Precision Tooling Ltd", + 0x0993: "Absolute Audio Labs B.V.", + 0x0994: "VT42 Pty Ltd", + 0x0995: "Bronkhorst High-Tech B.V.", + 0x0996: "C. & E. Fein GmbH", + 0x0997: "NextMind", + 0x0998: "Pixie Dust Technologies, Inc.", + 0x0999: "eTactica ehf", + 0x099A: "New Audio LLC", + 0x099B: "Sendum Wireless Corporation", + 0x099C: "deister electronic GmbH", + 0x099D: "YKK AP Inc.", + 0x099E: "Step One Limited", + 0x099F: "Koya Medical, Inc.", + 0x09A0: "Proof Diagnostics, Inc.", + 0x09A1: "VOS Systems, LLC", + 0x09A2: "ENGAGENOW DATA SCIENCES PRIVATE LIMITED", + 0x09A3: "ARDUINO SA", + 0x09A4: "KUMHO ELECTRICS, INC", + 0x09A5: "Security Enhancement Systems, LLC", + 0x09A6: "BEIJING ELECTRIC VEHICLE CO.,LTD", + 0x09A7: "Paybuddy ApS", + 0x09A8: "KHN Solutions Inc", + 0x09A9: "Nippon Ceramic Co.,Ltd.", + 0x09AA: "PHOTODYNAMIC INCORPORATED", + 0x09AB: "DashLogic, Inc.", + 0x09AC: "Ambiq", + 0x09AD: "Narhwall Inc.", + 0x09AE: "Pozyx NV", + 0x09AF: "ifLink Open Community", + 0x09B0: "Deublin Company, LLC", + 0x09B1: "BLINQY", + 0x09B2: "DYPHI", + 0x09B3: "BlueX Microelectronics Corp Ltd.", + 0x09B4: "PentaLock Aps.", + 0x09B5: "AUTEC Gesellschaft fuer Automationstechnik mbH", + 0x09B6: "Pegasus Technologies, Inc.", + 0x09B7: "Bout Labs, LLC", + 0x09B8: "PlayerData Limited", + 0x09B9: "SAVOY ELECTRONIC LIGHTING", + 0x09BA: "Elimo Engineering Ltd", + 0x09BB: "SkyStream Corporation", + 0x09BC: "Aerosens LLC", + 0x09BD: "Centre Suisse d'Electronique et de Microtechnique SA", + 0x09BE: "Vessel Ltd.", + 0x09BF: "Span.IO, Inc.", + 0x09C0: "AnotherBrain inc.", + 0x09C1: "Rosewill", + 0x09C2: "Universal Audio, Inc.", + 0x09C3: "JAPAN TOBACCO INC.", + 0x09C4: "UVISIO", + 0x09C5: "HungYi Microelectronics Co.,Ltd.", + 0x09C6: "Honor Device Co., Ltd.", + 0x09C7: "Combustion, LLC", + 0x09C8: "XUNTONG", + 0x09C9: "CrowdGlow Ltd", + 0x09CA: "Mobitrace", + 0x09CB: "Hx Engineering, LLC", + 0x09CC: "Senso4s d.o.o.", + 0x09CD: "Blyott", + 0x09CE: "Julius Blum GmbH", + 0x09CF: "BlueStreak IoT, LLC", + 0x09D0: "Chess Wise B.V.", + 0x09D1: "ABLEPAY TECHNOLOGIES AS", + 0x09D2: "Temperature Sensitive Solutions Systems Sweden AB", + 0x09D3: "HeartHero, inc.", + 0x09D4: "ORBIS Inc.", + 0x09D5: "GEAR RADIO ELECTRONICS CORP.", + 0x09D6: "EAR TEKNIK ISITME VE ODIOMETRI CIHAZLARI SANAYI VE TICARET ANONIM SIRKETI", + 0x09D7: "Coyotta", + 0x09D8: "Synergy Tecnologia em Sistemas Ltda", + 0x09D9: "VivoSensMedical GmbH", + 0x09DA: "Nagravision SA", + 0x09DB: "Bionic Avionics Inc.", + 0x09DC: "AON2 Ltd.", + 0x09DD: "Innoware Development AB", + 0x09DE: "JLD Technology Solutions, LLC", + 0x09DF: "Magnus Technology Sdn Bhd", + 0x09E0: "Preddio Technologies Inc.", + 0x09E1: "Tag-N-Trac Inc", + 0x09E2: "Wuhan Linptech Co.,Ltd.", + 0x09E3: "Friday Home Aps", + 0x09E4: "CPS AS", + 0x09E5: "Mobilogix", + 0x09E6: "Masonite Corporation", + 0x09E7: "Kabushikigaisha HANERON", + 0x09E8: "Melange Systems Pvt. Ltd.", + 0x09E9: "LumenRadio AB", + 0x09EA: "Athlos Oy", + 0x09EB: "KEAN ELECTRONICS PTY LTD", + 0x09EC: "Yukon advanced optics worldwide, UAB", + 0x09ED: "Sibel Inc.", + 0x09EE: "OJMAR SA", + 0x09EF: "Steinel Solutions AG", + 0x09F0: "WatchGas B.V.", + 0x09F1: "OM Digital Solutions Corporation", + 0x09F2: "Audeara Pty Ltd", + 0x09F3: "Beijing Zero Zero Infinity Technology Co.,Ltd.", + 0x09F4: "Spectrum Technologies, Inc.", + 0x09F5: "OKI Electric Industry Co., Ltd", + 0x09F6: "Mobile Action Technology Inc.", + 0x09F7: "SENSATEC Co., Ltd.", + 0x09F8: "R.O. S.R.L.", + 0x09F9: "Hangzhou Yaguan Technology Co. LTD", + 0x09FA: "Listen Technologies Corporation", + 0x09FB: "TOITU CO., LTD.", + 0x09FC: "Confidex", + 0x09FD: "Keep Technologies, Inc.", + 0x09FE: "Lichtvision Engineering GmbH", + 0x09FF: "AIRSTAR", + 0x0A00: "Ampler Bikes OU", + 0x0A01: "Cleveron AS", + 0x0A02: "Ayxon-Dynamics GmbH", + 0x0A03: "donutrobotics Co., Ltd.", + 0x0A04: "Flosonics Medical", + 0x0A05: "Southwire Company, LLC", + 0x0A06: "Shanghai wuqi microelectronics Co.,Ltd", + 0x0A07: "Reflow Pty Ltd", + 0x0A08: "Oras Oy", + 0x0A09: "ECCT", + 0x0A0A: "Volan Technology Inc.", + 0x0A0B: "SIANA Systems", + 0x0A0C: "Shanghai Yidian Intelligent Technology Co., Ltd.", + 0x0A0D: "Blue Peacock GmbH", + 0x0A0E: "Roland Corporation", + 0x0A0F: "LIXIL Corporation", + 0x0A10: "SUBARU Corporation", + 0x0A11: "Sensolus", + 0x0A12: "Dyson Technology Limited", + 0x0A13: "Tec4med LifeScience GmbH", + 0x0A14: "CROXEL, INC.", + 0x0A15: "Syng Inc", + 0x0A16: "RIDE VISION LTD", + 0x0A17: "Plume Design Inc", + 0x0A18: "Cambridge Animal Technologies Ltd", + 0x0A19: "Maxell, Ltd.", + 0x0A1A: "Link Labs, Inc.", + 0x0A1B: "Embrava Pty Ltd", + 0x0A1C: "INPEAK S.C.", + 0x0A1D: "API-K", + 0x0A1E: "CombiQ AB", + 0x0A1F: "DeVilbiss Healthcare LLC", + 0x0A20: "Jiangxi Innotech Technology Co., Ltd", + 0x0A21: "Apollogic Sp. z o.o.", + 0x0A22: "DAIICHIKOSHO CO., LTD.", + 0x0A23: "BIXOLON CO.,LTD", + 0x0A24: "Atmosic Technologies, Inc.", + 0x0A25: "Eran Financial Services LLC", + 0x0A26: "Louis Vuitton", + 0x0A27: "AYU DEVICES PRIVATE LIMITED", + 0x0A28: "NanoFlex", + 0x0A29: "Worthcloud Technology Co.,Ltd", + 0x0A2A: "Yamaha Corporation", + 0x0A2B: "PaceBait IVS", + 0x0A2C: "Shenzhen H&T Intelligent Control Co., Ltd", + 0x0A2D: "Shenzhen Feasycom Technology Co., Ltd.", + 0x0A2E: "Zuma Array Limited", + 0x0A2F: "Instamic, Inc.", + 0x0A30: "Air-Weigh", + 0x0A31: "Nevro Corp.", + 0x0A32: "Pinnacle Technology, Inc.", + 0x0A33: "WMF AG", + 0x0A34: "Luxer Corporation", + 0x0A35: "safectory GmbH", + 0x0A36: "NGK SPARK PLUG CO., LTD.", + 0x0A37: "2587702 Ontario Inc.", + 0x0A38: "Bouffalo Lab (Nanjing)., Ltd.", + 0x0A39: "BLUETICKETING SRL", + 0x0A3A: "Incotex Co. Ltd.", + 0x0A3B: "Galileo Technology Limited", + 0x0A3C: "Siteco GmbH", + 0x0A3D: "DELABIE", + 0x0A3E: "Hefei Yunlian Semiconductor Co., Ltd", + 0x0A3F: "Shenzhen Yopeak Optoelectronics Technology Co., Ltd.", + 0x0A40: "GEWISS S.p.A.", + 0x0A41: "OPEX Corporation", + 0x0A42: "Motionalysis, Inc.", + 0x0A43: "Busch Systems International Inc.", + 0x0A44: "Novidan, Inc.", + 0x0A45: "3SI Security Systems, Inc", + 0x0A46: "Beijing HC-Infinite Technology Limited", + 0x0A47: "The Wand Company Ltd", + 0x0A48: "JRC Mobility Inc.", + 0x0A49: "Venture Research Inc.", + 0x0A4A: "Map Large, Inc.", + 0x0A4B: "MistyWest Energy and Transport Ltd.", + 0x0A4C: "SiFli Technologies (shanghai) Inc.", + 0x0A4D: "Lockn Technologies Private Limited", + 0x0A4E: "Toytec Corporation", + 0x0A4F: "VANMOOF Global Holding B.V.", + 0x0A50: "Nextscape Inc.", + 0x0A51: "CSIRO", + 0x0A52: "Follow Sense Europe B.V.", + 0x0A53: "KKM COMPANY LIMITED", + 0x0A54: "SQL Technologies Corp.", + 0x0A55: "Inugo Systems Limited", + 0x0A56: "ambie", + 0x0A57: "Meizhou Guo Wei Electronics Co., Ltd", + 0x0A58: "Indigo Diabetes", + 0x0A59: "TourBuilt, LLC", + 0x0A5A: "Sontheim Industrie Elektronik GmbH", + 0x0A5B: "LEGIC Identsystems AG", + 0x0A5C: "Innovative Design Labs Inc.", + 0x0A5D: "MG Energy Systems B.V.", + 0x0A5E: "LaceClips llc", + 0x0A5F: "stryker", + 0x0A60: "DATANG SEMICONDUCTOR TECHNOLOGY CO.,LTD", + 0x0A61: "Smart Parks B.V.", + 0x0A62: "MOKO TECHNOLOGY Ltd", + 0x0A63: "Gremsy JSC", + 0x0A64: "Geopal system A/S", + 0x0A65: "Lytx, INC.", + 0x0A66: "JUSTMORPH PTE. LTD.", + 0x0A67: "Beijing SuperHexa Century Technology CO. Ltd", + 0x0A68: "Focus Ingenieria SRL", + 0x0A69: "HAPPIEST BABY, INC.", + 0x0A6A: "Scribble Design Inc.", + 0x0A6B: "Olympic Ophthalmics, Inc.", + 0x0A6C: "Pokkels", + 0x0A6D: "KUUKANJYOKIN Co.,Ltd.", + 0x0A6E: "Pac Sane Limited", + 0x0A6F: "Warner Bros.", + 0x0A70: "Ooma", + 0x0A71: "Senquip Pty Ltd", + 0x0A72: "Jumo GmbH & Co. KG", + 0x0A73: "Innohome Oy", + 0x0A74: "MICROSON S.A.", + 0x0A75: "Delta Cycle Corporation", + 0x0A76: "Synaptics Incorporated", + 0x0A77: "JMD PACIFIC PTE. LTD.", + 0x0A78: "Shenzhen Sunricher Technology Limited", + 0x0A79: "Webasto SE", + 0x0A7A: "Emlid Limited", + 0x0A7B: "UniqAir Oy", + 0x0A7C: "WAFERLOCK", + 0x0A7D: "Freedman Electronics Pty Ltd", + 0x0A7E: "Keba AG", + 0x0A7F: "Intuity Medical" +} \ No newline at end of file diff --git a/bumble/controller.py b/bumble/controller.py new file mode 100644 index 0000000..35f7e92 --- /dev/null +++ b/bumble/controller.py @@ -0,0 +1,895 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import asyncio +import itertools +import random + +from .hci import * +from .l2cap import * + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Utils +# ----------------------------------------------------------------------------- +class DataObject: + pass + + +# ----------------------------------------------------------------------------- +class Connection: + def __init__(self, controller, handle, role, peer_address, link): + self.controller = controller + self.handle = handle + self.role = role + self.peer_address = peer_address + self.link = link + self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu) + + def on_hci_acl_data_packet(self, packet): + self.assembler.feed_packet(packet) + self.controller.send_hci_packet(HCI_Number_Of_Completed_Packets_Event([(self.handle, 1)])) + + def on_acl_pdu(self, data): + if self.link: + self.link.send_acl_data(self.controller.random_address, self.peer_address, data) + + +# ----------------------------------------------------------------------------- +class Controller: + def __init__(self, name, host_source = None, host_sink = None, link = None): + self.name = name + self.hci_sink = None + self.link = link + + self.central_connections = {} # Connections where this controller is the central + self.peripheral_connections = {} # Connections where this controller is the peripheral + + self.hci_version = HCI_VERSION_BLUETOOTH_CORE_5_0 + self.hci_revision = 0 + self.lmp_version = HCI_VERSION_BLUETOOTH_CORE_5_0 + self.lmp_subversion = 0 + self.lmp_features = bytes.fromhex('0000000060000000') # BR/EDR Not Supported, LE Supported (Controller) + self.manufacturer_name = 0xFFFF + self.hc_le_data_packet_length = 27 + self.hc_total_num_le_data_packets = 64 + self.supported_commands = bytes.fromhex('2000800000c000000000e40000002822000000000000040000f7ffff7f00000030f0f9ff01008004000000000000000000000000000000000000000000000000') + self.le_features = bytes.fromhex('ff49010000000000') + self.le_states = bytes.fromhex('ffff3fffff030000') + self.avertising_channel_tx_power = 0 + self.white_list_size = 8 + self.resolving_list_size = 8 + self.supported_max_tx_octets = 27 + self.supported_max_tx_time = 10000 # microseconds + self.supported_max_rx_octets = 27 + self.supported_max_rx_time = 10000 # microseconds + self.suggested_max_tx_octets = 27 + self.suggested_max_tx_time = 0x0148 # microseconds + self.default_phy = bytes([0, 0, 0]) + self.le_scan_type = 0 + self.le_scan_interval = 0x10 + self.le_scan_window = 0x10 + self.le_scan_enable = 0 + self.le_scan_own_address_type = Address.RANDOM_DEVICE_ADDRESS + self.le_scanning_filter_policy = 0 + self.le_scan_response_data = None + self.le_address_resolution = False + self.le_rpa_timeout = 0 + self.sync_flow_control = False + self.local_name = 'Bumble' + + self.advertising_interval = 2000 # Fixed for now + self.advertising_data = None + self.advertising_timer_handle = None + + self._random_address = Address('00:00:00:00:00:00') + self._public_address = None + + # Set the source and sink interfaces + if host_source: + host_source.set_packet_sink(self) + self.host = host_sink + + # Add this controller to the link if specified + if link: + link.add_controller(self) + + @property + def host(self): + return self.hci_sink + + @host.setter + def host(self, host): + ''' + Sets the host (sink) for this controller, and set this controller as the controller (sink) for the host + ''' + self.set_packet_sink(host) + if host: + host.controller = self + + def set_packet_sink(self, sink): + ''' + Method from the Packet Source interface + ''' + self.hci_sink = sink + + @property + def public_address(self): + return self._public_address + + @public_address.setter + def public_address(self, address): + if type(address) is str: + address = Address(address) + self._public_address = address + + @property + def random_address(self): + return self._random_address + + @random_address.setter + def random_address(self, address): + if type(address) is str: + address = Address(address) + self._random_address = address + logger.debug(f'new random address: {address}') + + if self.link: + self.link.on_address_changed(self) + + # Packet Sink protocol (packets coming from the host via HCI) + def on_packet(self, packet): + self.on_hci_packet(HCI_Packet.from_bytes(packet)) + + def on_hci_packet(self, packet): + logger.debug(f'{color("<<<", "blue")} [{self.name}] {color("HOST -> CONTROLLER", "blue")}: {packet}') + + # If the packet is a command, invoke the handler for this packet + if packet.hci_packet_type == HCI_COMMAND_PACKET: + self.on_hci_command_packet(packet) + elif packet.hci_packet_type == HCI_EVENT_PACKET: + self.on_hci_event_packet(packet) + elif packet.hci_packet_type == HCI_ACL_DATA_PACKET: + self.on_hci_acl_data_packet(packet) + else: + logger.warning(f'!!! unknown packet type {packet.hci_packet_type}') + + def on_hci_command_packet(self, command): + handler_name = f'on_{command.name.lower()}' + handler = getattr(self, handler_name, self.on_hci_command) + result = handler(command) + if type(result) is bytes: + self.send_hci_packet(HCI_Command_Complete_Event( + num_hci_command_packets = 1, + command_opcode = command.op_code, + return_parameters = result + )) + + def on_hci_event_packet(self, event): + logger.warning('!!! unexpected event packet') + + def on_hci_acl_data_packet(self, packet): + # Look for the connection to which this data belongs + connection = self.find_connection_by_handle(packet.connection_handle) + if connection is None: + logger.warning(f'!!! no connection for handle 0x{packet.connection_handle:04X}') + return + + # Pass the packet to the connection + connection.on_hci_acl_data_packet(packet) + + def send_hci_packet(self, packet): + logger.debug(f'{color(">>>", "green")} [{self.name}] {color("CONTROLLER -> HOST", "green")}: {packet}') + if self.host: + self.host.on_packet(packet.to_bytes()) + + # This method allow the controller to emulate the same API as a transport source + async def wait_for_termination(self): + # For now, just wait forever + await asyncio.get_running_loop().create_future() + + ############################################################ + # Link connections + ############################################################ + def allocate_connection_handle(self): + handle = 0 + max_handle = 0 + for connection in itertools.chain( + self.central_connections.values(), + self.peripheral_connections.values() + ): + max_handle = max(max_handle, connection.handle) + if connection.handle == handle: + # Already used, continue searching after the current max + handle = max_handle + 1 + return handle + + def find_connection_by_address(self, address): + return self.central_connections.get(address) or self.peripheral_connections.get(address) + + def find_connection_by_handle(self, handle): + for connection in itertools.chain( + self.central_connections.values(), + self.peripheral_connections.values() + ): + if connection.handle == handle: + return connection + return None + + def find_central_connection_by_handle(self, handle): + for connection in self.central_connections.values(): + if connection.handle == handle: + return connection + return None + + def on_link_central_connected(self, central_address): + ''' + Called when an incoming connection occurs from a central on the link + ''' + + # Allocate (or reuse) a connection handle + peer_address = central_address + peer_address_type = central_address.address_type + connection = self.peripheral_connections.get(peer_address) + if connection is None: + connection_handle = self.allocate_connection_handle() + connection = Connection(self, connection_handle, BT_PERIPHERAL_ROLE, peer_address, self.link) + self.peripheral_connections[peer_address] = connection + logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}') + + # Then say that the connection has completed + self.send_hci_packet(HCI_LE_Connection_Complete_Event( + status = HCI_SUCCESS, + connection_handle = connection.handle, + role = connection.role, + peer_address_type = peer_address_type, + peer_address = peer_address, + conn_interval = 10, # FIXME + conn_latency = 0, # FIXME + supervision_timeout = 10, # FIXME + master_clock_accuracy = 7 # FIXME + )) + + def on_link_central_disconnected(self, peer_address, reason): + ''' + Called when an active disconnection occurs from a peer + ''' + + # Send a disconnection complete event + if connection := self.peripheral_connections.get(peer_address): + self.send_hci_packet(HCI_Disconnection_Complete_Event( + status = HCI_SUCCESS, + connection_handle = connection.handle, + reason = reason + )) + + # Remove the connection + del self.peripheral_connections[peer_address] + else: + logger.warn(f'!!! No peripheral connection found for {peer_address}') + + def on_link_peripheral_connection_complete(self, le_create_connection_command, status): + ''' + Called by the link when a connection has been made or has failed to be made + ''' + + if status == HCI_SUCCESS: + # Allocate (or reuse) a connection handle + peer_address = le_create_connection_command.peer_address + connection = self.central_connections.get(peer_address) + if connection is None: + connection_handle = self.allocate_connection_handle() + connection = Connection( + self, + connection_handle, + BT_CENTRAL_ROLE, + peer_address, + self.link + ) + self.central_connections[peer_address] = connection + logger.debug(f'New CENTRAL connection handle: 0x{connection_handle:04X}') + else: + connection = None + + # Say that the connection has completed + self.send_hci_packet(HCI_LE_Connection_Complete_Event( + status = status, + connection_handle = connection.handle if connection else 0, + role = BT_CENTRAL_ROLE, + peer_address_type = le_create_connection_command.peer_address_type, + peer_address = le_create_connection_command.peer_address, + conn_interval = le_create_connection_command.conn_interval_min, + conn_latency = le_create_connection_command.conn_latency, + supervision_timeout = le_create_connection_command.supervision_timeout, + master_clock_accuracy = 0 + )) + + def on_link_peripheral_disconnection_complete(self, disconnection_command, status): + ''' + Called when a disconnection has been completed + ''' + + # Send a disconnection complete event + self.send_hci_packet(HCI_Disconnection_Complete_Event( + status = status, + connection_handle = disconnection_command.connection_handle, + reason = disconnection_command.reason + )) + + # Remove the connection + if connection := self.find_central_connection_by_handle(disconnection_command.connection_handle): + logger.debug(f'CENTRAL Connection removed: {connection}') + del self.central_connections[connection.peer_address] + + def on_link_peripheral_disconnected(self, peer_address): + ''' + Called when a connection to a peripheral is broken + ''' + + # Send a disconnection complete event + if connection := self.central_connections.get(peer_address): + self.send_hci_packet(HCI_Disconnection_Complete_Event( + status = HCI_SUCCESS, + connection_handle = connection.handle, + reason = HCI_CONNECTION_TIMEOUT_ERROR + )) + + # Remove the connection + del self.central_connections[peer_address] + else: + logger.warn(f'!!! No central connection found for {peer_address}') + + def on_link_encrypted(self, peer_address, rand, ediv, ltk): + # For now, just setup the encryption without asking the host + if connection := self.find_connection_by_address(peer_address): + self.send_hci_packet( + HCI_Encryption_Change_Event( + status = 0, + connection_handle = connection.handle, + encryption_enabled = 1 + ) + ) + + def on_link_acl_data(self, sender_address, data): + # Look for the connection to which this data belongs + connection = self.find_connection_by_address(sender_address) + if connection is None: + logger.warning(f'!!! no connection for {sender_address}') + return + + # Send the data to the host + # TODO: should fragment + acl_packet = HCI_AclDataPacket(connection.handle, 2, 0, len(data), data) + self.send_hci_packet(acl_packet) + + def on_link_advertising_data(self, sender_address, data): + # Ignore if we're not scanning + if self.le_scan_enable == 0: + return + + # Send a scan report + report = HCI_Object( + HCI_LE_Advertising_Report_Event.REPORT_FIELDS, + event_type = HCI_LE_Advertising_Report_Event.ADV_IND, + address_type = sender_address.address_type, + address = sender_address, + data = data, + rssi = -50 + ) + self.send_hci_packet(HCI_LE_Advertising_Report_Event([report])) + + # Simulate a scan response + report = HCI_Object( + HCI_LE_Advertising_Report_Event.REPORT_FIELDS, + event_type = HCI_LE_Advertising_Report_Event.SCAN_RSP, + address_type = sender_address.address_type, + address = sender_address, + data = data, + rssi = -50 + ) + self.send_hci_packet(HCI_LE_Advertising_Report_Event([report])) + + ############################################################ + # Advertising support + ############################################################ + def on_advertising_timer_fired(self): + self.send_advertising_data() + self.advertising_timer_handle = asyncio.get_running_loop().call_later(self.advertising_interval / 1000.0, self.on_advertising_timer_fired) + + def start_advertising(self): + # Stop any ongoing advertising before we start again + self.stop_advertising() + + # Advertise now + self.advertising_timer_handle = asyncio.get_running_loop().call_soon(self.on_advertising_timer_fired) + + def stop_advertising(self): + if self.advertising_timer_handle is not None: + self.advertising_timer_handle.cancel() + self.advertising_timer_handle = None + + def send_advertising_data(self): + if self.link and self.advertising_data: + self.link.send_advertising_data(self.random_address, self.advertising_data) + + @property + def is_advertising(self): + return self.advertising_timer_handle is not None + + ############################################################ + # HCI handlers + ############################################################ + def on_hci_command(self, command): + logger.warning(color(f'--- Unsupported command {command}', 'red')) + return bytes([HCI_UNKNOWN_HCI_COMMAND_ERROR]) + + def on_hci_create_connection_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.1.5 Create Connection command + ''' + + # TODO: classic mode not supported yet + + def on_hci_disconnect_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.1.6 Disconnect Command + ''' + # First, say that the disconnection is pending + self.send_hci_packet(HCI_Command_Status_Event( + status = HCI_COMMAND_STATUS_PENDING, + num_hci_command_packets = 1, + command_opcode = command.op_code + )) + + # Notify the link of the disconnection + if not (connection := self.find_central_connection_by_handle(command.connection_handle)): + logger.warn('connection not found') + return + + if self.link: + self.link.disconnect(self.random_address, connection.peer_address, command) + else: + # Remove the connection + del self.central_connections[connection.peer_address] + + def on_hci_set_event_mask_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.1 Set Event Mask Command + ''' + self.event_mask = command.event_mask + return bytes([HCI_SUCCESS]) + + def on_hci_reset_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.2 Reset Command + ''' + # TODO: cleanup what needs to be reset + return bytes([HCI_SUCCESS]) + + def on_hci_write_local_name_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.11 Write Local Name Command + ''' + local_name = command.local_name + if len(local_name): + try: + first_null = local_name.find(0) + if first_null >= 0: + local_name = local_name[:first_null] + self.local_name = str(local_name, 'utf-8') + except UnicodeDecodeError: + pass + return bytes([HCI_SUCCESS]) + + def on_hci_read_local_name_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.12 Read Local Name Command + ''' + local_name = bytes(self.local_name, 'utf-8')[:248] + if len(local_name) < 248: + local_name = local_name + bytes(248 - len(local_name)) + + return bytes([HCI_SUCCESS]) + local_name + + def on_hci_read_class_of_device_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.25 Read Class of Device Command + ''' + return bytes([HCI_SUCCESS, 0, 0, 0]) + + def on_hci_write_class_of_device_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.26 Write Class of Device Command + ''' + return bytes([HCI_SUCCESS]) + + def on_hci_read_synchronous_flow_control_enable_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.36 Read Synchronous Flow Control Enable Command + ''' + if self.sync_flow_control: + ret = 1 + else: + ret = 0 + return bytes([HCI_SUCCESS, ret]) + + def on_hci_write_synchronous_flow_control_enable_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.37 Write Synchronous Flow Control Enable Command + ''' + ret = HCI_SUCCESS + if command.synchronous_flow_control_enable == 1: + self.sync_flow_control = True + elif command.synchronous_flow_control_enable == 0: + self.sync_flow_control = False + else: + ret = HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR + return bytes([ret]) + + def on_hci_write_simple_pairing_mode_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.59 Write Simple Pairing Mode Command + ''' + return bytes([HCI_SUCCESS]) + + def on_hci_set_event_mask_page_2_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.69 Set Event Mask Page 2 Command + ''' + self.event_mask_page_2 = command.event_mask_page_2 + return bytes([HCI_SUCCESS]) + + def on_hci_read_le_host_support_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.78 Write LE Host Support Command + ''' + return bytes([HCI_SUCCESS, 1, 0]) + + def on_hci_write_le_host_support_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.79 Write LE Host Support Command + ''' + # TODO / Just ignore for now + return bytes([HCI_SUCCESS]) + + def on_hci_write_authenticated_payload_timeout_command(self, command): + ''' + See Bluetooth spec Vol 2, Part E - 7.3.94 Write Authenticated Payload Timeout Command + ''' + # TODO + return struct.pack('>= 1 + index += 1 + + return names + + +def name_or_number(dictionary, number, width=2): + name = dictionary.get(number) + if name is not None: + return name + return f'[0x{number:0{width}X}]' + + +def padded_bytes(buffer, size): + padding_size = max(size - len(buffer), 0) + return buffer + bytes(padding_size) + + +# ----------------------------------------------------------------------------- +# Exceptions +# ----------------------------------------------------------------------------- +class BaseError(Exception): + """ Base class for errors with an error code, error name and namespace""" + def __init__(self, error_code, error_namespace='', error_name='', details=''): + super().__init__() + self.error_code = error_code + self.error_namespace = error_namespace + self.error_name = error_name + self.details = details + + def __str__(self): + if self.error_namespace: + namespace = f'{self.error_namespace}/' + else: + namespace = '' + if self.error_name: + name = f'{self.error_name} [0x{self.error_code:X}]' + else: + name = f'0x{self.error_code:X}' + + return f'{type(self).__name__}({namespace}{name})' + + +class ProtocolError(BaseError): + """ Protocol Error """ + + +class TimeoutError(Exception): + """ Timeout Error """ + + +class InvalidStateError(Exception): + """ Invalid State Error """ + + +class ConnectionError(BaseError): + """ Connection Error """ + FAILURE = 0x01 + CONNECTION_REFUSED = 0x02 + + +# ----------------------------------------------------------------------------- +# UUID +# +# NOTE: the internal byte representation is in little-endian byte order +# +# Base UUID: 00000000-0000-1000-8000- 00805F9B34FB +# ----------------------------------------------------------------------------- +class UUID: + ''' + See Bluetooth spec Vol 3, Part B - 2.5.1 UUID + ''' + BASE_UUID = bytes.fromhex('00001000800000805F9B34FB') + UUIDS = [] # Registry of all instances created + + def __init__(self, uuid_str_or_int, name = None): + if type(uuid_str_or_int) is int: + self.uuid_bytes = struct.pack('> 13 & 0x7FF), (class_of_device >> 8 & 0x1F), (class_of_device >> 2 & 0x3F)) + + @staticmethod + def pack_class_of_device(service_classes, major_device_class, minor_device_class): + return service_classes << 13 | major_device_class << 8 | minor_device_class << 2 + + @staticmethod + def service_class_labels(service_class_flags): + return bit_flags_to_strings(service_class_flags, DeviceClass.SERVICE_CLASS_LABELS) + + @staticmethod + def major_device_class_name(device_class): + return name_or_number(DeviceClass.MAJOR_DEVICE_CLASS_NAMES, device_class) + + @staticmethod + def minor_device_class_name(major_device_class, minor_device_class): + class_names = DeviceClass.MINOR_DEVICE_CLASS_NAMES.get(major_device_class) + if class_names is None: + return f'#{minor_device_class:02X}' + return name_or_number(class_names, minor_device_class) + + +# ----------------------------------------------------------------------------- +# Advertising Data +# ----------------------------------------------------------------------------- +class AdvertisingData: + # This list is only partial, it still needs to be filled in from the spec + FLAGS = 0x01 + INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x02 + COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS = 0x03 + INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS = 0x04 + COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS = 0x05 + INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS = 0x06 + COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS = 0x07 + SHORTENED_LOCAL_NAME = 0x08 + COMPLETE_LOCAL_NAME = 0x09 + TX_POWER_LEVEL = 0x0A + CLASS_OF_DEVICE = 0x0D + SIMPLE_PAIRING_HASH_C = 0x0E + SIMPLE_PAIRING_HASH_C_192 = 0x0E + SIMPLE_PAIRING_RANDOMIZER_R = 0x0F + SIMPLE_PAIRING_RANDOMIZER_R_192 = 0x0F + DEVICE_ID = 0x10 + SECURITY_MANAGER_TK_VALUE = 0x10 + SECURITY_MANAGER_OUT_OF_BAND_FLAGS = 0x11 + PERIPHERAL_CONNECTION_INTERVAL_RANGE = 0x12 + LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS = 0x14 + LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS = 0x15 + SERVICE_DATA = 0x16 + SERVICE_DATA_16_BIT_UUID = 0x16 + PUBLIC_TARGET_ADDRESS = 0x17 + RANDOM_TARGET_ADDRESS = 0x18 + APPEARANCE = 0x19 + ADVERTISING_INTERVAL = 0x1A + LE_BLUETOOTH_DEVICE_ADDRESS = 0x1B + LE_ROLE = 0x1C + SIMPLE_PAIRING_HASH_C_256 = 0x1D + SIMPLE_PAIRING_RANDOMIZER_R_256 = 0x1E + LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS = 0x1F + SERVICE_DATA_32_BIT_UUID = 0x20 + SERVICE_DATA_128_BIT_UUID = 0x21 + LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE = 0x22 + LE_SECURE_CONNECTIONS_RANDOM_VALUE = 0x23 + URI = 0x24 + INDOOR_POSITIONING = 0x25 + TRANSPORT_DISCOVERY_DATA = 0x26 + LE_SUPPORTED_FEATURES = 0x27 + CHANNEL_MAP_UPDATE_INDICATION = 0x28 + PB_ADV = 0x29 + MESH_MESSAGE = 0x2A + MESH_BEACON = 0x2B + BIGINFO = 0x2C + BROADCAST_CODE = 0x2D + RESOLVABLE_SET_IDENTIFIER = 0x2E + ADVERTISING_INTERVAL_LONG = 0x2F + THREE_D_INFORMATION_DATA = 0x3D + MANUFACTURER_SPECIFIC_DATA = 0xFF + + AD_TYPE_NAMES = { + FLAGS: 'FLAGS', + INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: 'INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS', + COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: 'COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS', + INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: 'INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS', + COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: 'COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS', + INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: 'INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS', + COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: 'COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS', + SHORTENED_LOCAL_NAME: 'SHORTENED_LOCAL_NAME', + COMPLETE_LOCAL_NAME: 'COMPLETE_LOCAL_NAME', + TX_POWER_LEVEL: 'TX_POWER_LEVEL', + CLASS_OF_DEVICE: 'CLASS_OF_DEVICE', + SIMPLE_PAIRING_HASH_C: 'SIMPLE_PAIRING_HASH_C', + SIMPLE_PAIRING_HASH_C_192: 'SIMPLE_PAIRING_HASH_C_192', + SIMPLE_PAIRING_RANDOMIZER_R: 'SIMPLE_PAIRING_RANDOMIZER_R', + SIMPLE_PAIRING_RANDOMIZER_R_192: 'SIMPLE_PAIRING_RANDOMIZER_R_192', + DEVICE_ID: 'DEVICE_ID', + SECURITY_MANAGER_TK_VALUE: 'SECURITY_MANAGER_TK_VALUE', + SECURITY_MANAGER_OUT_OF_BAND_FLAGS: 'SECURITY_MANAGER_OUT_OF_BAND_FLAGS', + PERIPHERAL_CONNECTION_INTERVAL_RANGE: 'PERIPHERAL_CONNECTION_INTERVAL_RANGE', + LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS: 'LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS', + LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS: 'LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS', + SERVICE_DATA: 'SERVICE_DATA', + SERVICE_DATA_16_BIT_UUID: 'SERVICE_DATA_16_BIT_UUID', + PUBLIC_TARGET_ADDRESS: 'PUBLIC_TARGET_ADDRESS', + RANDOM_TARGET_ADDRESS: 'RANDOM_TARGET_ADDRESS', + APPEARANCE: 'APPEARANCE', + ADVERTISING_INTERVAL: 'ADVERTISING_INTERVAL', + LE_BLUETOOTH_DEVICE_ADDRESS: 'LE_BLUETOOTH_DEVICE_ADDRESS', + LE_ROLE: 'LE_ROLE', + SIMPLE_PAIRING_HASH_C_256: 'SIMPLE_PAIRING_HASH_C_256', + SIMPLE_PAIRING_RANDOMIZER_R_256: 'SIMPLE_PAIRING_RANDOMIZER_R_256', + LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS: 'LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS', + SERVICE_DATA_32_BIT_UUID: 'SERVICE_DATA_32_BIT_UUID', + SERVICE_DATA_128_BIT_UUID: 'SERVICE_DATA_128_BIT_UUID', + LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE: 'LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE', + LE_SECURE_CONNECTIONS_RANDOM_VALUE: 'LE_SECURE_CONNECTIONS_RANDOM_VALUE', + URI: 'URI', + INDOOR_POSITIONING: 'INDOOR_POSITIONING', + TRANSPORT_DISCOVERY_DATA: 'TRANSPORT_DISCOVERY_DATA', + LE_SUPPORTED_FEATURES: 'LE_SUPPORTED_FEATURES', + CHANNEL_MAP_UPDATE_INDICATION: 'CHANNEL_MAP_UPDATE_INDICATION', + PB_ADV: 'PB_ADV', + MESH_MESSAGE: 'MESH_MESSAGE', + MESH_BEACON: 'MESH_BEACON', + BIGINFO: 'BIGINFO', + BROADCAST_CODE: 'BROADCAST_CODE', + RESOLVABLE_SET_IDENTIFIER: 'RESOLVABLE_SET_IDENTIFIER', + ADVERTISING_INTERVAL_LONG: 'ADVERTISING_INTERVAL_LONG', + THREE_D_INFORMATION_DATA: 'THREE_D_INFORMATION_DATA', + MANUFACTURER_SPECIFIC_DATA: 'MANUFACTURER_SPECIFIC_DATA' + } + + LE_LIMITED_DISCOVERABLE_MODE_FLAG = 0x01 + LE_GENERAL_DISCOVERABLE_MODE_FLAG = 0x02 + BR_EDR_NOT_SUPPORTED_FLAG = 0x04 + BR_EDR_CONTROLLER_FLAG = 0x08 + BR_EDR_HOST_FLAG = 0x10 + + def __init__(self, ad_structures = []): + self.ad_structures = ad_structures[:] + + @staticmethod + def from_bytes(data): + instance = AdvertisingData() + instance.append(data) + return instance + + @staticmethod + def flags_to_string(flags, short=False): + flag_names = [ + 'LE Limited', + 'LE General', + 'No BR/EDR', + 'BR/EDR C', + 'BR/EDR H' + ] if short else [ + 'LE Limited Discoverable Mode', + 'LE General Discoverable Mode', + 'BR/EDR Not Supported', + 'Simultaneous LE and BR/EDR (Controller)', + 'Simultaneous LE and BR/EDR (Host)' + ] + return ','.join(bit_flags_to_strings(flags, flag_names)) + + @staticmethod + def uuid_list_to_objects(ad_data, uuid_size): + uuids = [] + offset = 0 + while (uuid_size * (offset + 1)) <= len(ad_data): + uuids.append(UUID.from_bytes(ad_data[offset:offset + uuid_size])) + offset += uuid_size + return uuids + + @staticmethod + def uuid_list_to_string(ad_data, uuid_size): + return ', '.join([ + str(uuid) + for uuid in AdvertisingData.uuid_list_to_objects(ad_data, uuid_size) + ]) + + @staticmethod + def ad_data_to_string(ad_type, ad_data): + if ad_type == AdvertisingData.FLAGS: + ad_type_str = 'Flags' + ad_data_str = AdvertisingData.flags_to_string(ad_data[0], short=True) + elif ad_type == AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: + ad_type_str = 'Complete List of 16-bit Service Class UUIDs' + ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 2) + elif ad_type == AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS: + ad_type_str = 'Incomplete List of 16-bit Service Class UUIDs' + ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 2) + elif ad_type == AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: + ad_type_str = 'Complete List of 32-bit Service Class UUIDs' + ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 4) + elif ad_type == AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS: + ad_type_str = 'Incomplete List of 32-bit Service Class UUIDs' + ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 4) + elif ad_type == AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: + ad_type_str = 'Complete List of 128-bit Service Class UUIDs' + ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 16) + elif ad_type == AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS: + ad_type_str = 'Incomplete List of 128-bit Service Class UUIDs' + ad_data_str = AdvertisingData.uuid_list_to_string(ad_data, 16) + elif ad_type == AdvertisingData.SERVICE_DATA_16_BIT_UUID: + ad_type_str = 'Service Data' + uuid = UUID.from_bytes(ad_data[:2]) + ad_data_str = f'service={uuid}, data={ad_data[2:].hex()}' + elif ad_type == AdvertisingData.SERVICE_DATA_32_BIT_UUID: + ad_type_str = 'Service Data' + uuid = UUID.from_bytes(ad_data[:4]) + ad_data_str = f'service={uuid}, data={ad_data[4:].hex()}' + elif ad_type == AdvertisingData.SERVICE_DATA_128_BIT_UUID: + ad_type_str = 'Service Data' + uuid = UUID.from_bytes(ad_data[:16]) + ad_data_str = f'service={uuid}, data={ad_data[16:].hex()}' + elif ad_type == AdvertisingData.SHORTENED_LOCAL_NAME: + ad_type_str = 'Shortened Local Name' + ad_data_str = f'"{ad_data.decode("utf-8")}"' + elif ad_type == AdvertisingData.COMPLETE_LOCAL_NAME: + ad_type_str = 'Complete Local Name' + ad_data_str = f'"{ad_data.decode("utf-8")}"' + elif ad_type == AdvertisingData.TX_POWER_LEVEL: + ad_type_str = 'TX Power Level' + ad_data_str = str(ad_data[0]) + elif ad_type == AdvertisingData.MANUFACTURER_SPECIFIC_DATA: + ad_type_str = 'Manufacturer Specific Data' + company_id = struct.unpack_from(' 0: + ad_type = data[offset] + ad_data = data[offset + 1:offset + length] + self.ad_structures.append((ad_type, ad_data)) + offset += length + + def get(self, type_id, return_all=False, raw=True): + ''' + Get Advertising Data Structure(s) with a given type + + If return_all is True, returns a (possibly empty) list of matches, + else returns the first entry, or None if no structure matches. + ''' + def process_ad_data(ad_data): + return ad_data if raw else self.ad_data_to_object(type_id, ad_data) + + if return_all: + return [process_ad_data(ad[1]) for ad in self.ad_structures if ad[0] == type_id] + else: + return next((process_ad_data(ad[1]) for ad in self.ad_structures if ad[0] == type_id), None) + + def __bytes__(self): + return b''.join([bytes([len(x[1]) + 1, x[0]]) + x[1] for x in self.ad_structures]) + + def to_string(self, separator=', '): + return separator.join([AdvertisingData.ad_data_to_string(x[0], x[1]) for x in self.ad_structures]) + + def __str__(self): + return self.to_string() + + +# ----------------------------------------------------------------------------- +# Connection Parameters +# ----------------------------------------------------------------------------- +class ConnectionParameters: + def __init__(self, connection_interval, connection_latency, supervision_timeout): + self.connection_interval = connection_interval + self.connection_latency = connection_latency + self.supervision_timeout = supervision_timeout + + def __str__(self): + return f'ConnectionParameters(connection_interval={self.connection_interval}, connection_latency={self.connection_latency}, supervision_timeout={self.supervision_timeout}' + + +# ----------------------------------------------------------------------------- +# Connection PHY +# ----------------------------------------------------------------------------- +class ConnectionPHY: + def __init__(self, tx_phy, rx_phy): + self.tx_phy = tx_phy + self.rx_phy = rx_phy + + def __str__(self): + return f'ConnectionPHY(tx_phy={self.tx_phy}, rx_phy={self.rx_phy})' diff --git a/bumble/crypto.py b/bumble/crypto.py new file mode 100644 index 0000000..1462e7f --- /dev/null +++ b/bumble/crypto.py @@ -0,0 +1,229 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Crypto support +# +# See Bluetooth spec Vol 3, Part H - 2.2 CRYPTOGRAPHIC TOOLBOX +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import operator +import platform +if platform.system() != 'Emscripten': + import secrets + from cryptography.hazmat.primitives.ciphers import ( + Cipher, + algorithms, + modes + ) + from cryptography.hazmat.primitives.asymmetric.ec import ( + generate_private_key, + ECDH, + EllipticCurvePublicNumbers, + EllipticCurvePrivateNumbers, + SECP256R1 + ) + from cryptography.hazmat.primitives import cmac +else: + # TODO: implement stubs + pass + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- +class EccKey: + def __init__(self, private_key): + self.private_key = private_key + + @classmethod + def generate(cls): + private_key = generate_private_key(SECP256R1()) + return cls(private_key) + + @classmethod + def from_private_key_bytes(cls, d_bytes, x_bytes, y_bytes): + d = int.from_bytes(d_bytes, byteorder='big', signed=False) + x = int.from_bytes(x_bytes, byteorder='big', signed=False) + y = int.from_bytes(y_bytes, byteorder='big', signed=False) + private_key = EllipticCurvePrivateNumbers(d, EllipticCurvePublicNumbers(x, y, SECP256R1())).private_key() + return cls(private_key) + + @property + def x(self): + return self.private_key.public_key().public_numbers().x.to_bytes(32, byteorder='big') + + @property + def y(self): + return self.private_key.public_key().public_numbers().y.to_bytes(32, byteorder='big') + + def dh(self, public_key_x, public_key_y): + x = int.from_bytes(public_key_x, byteorder='big', signed=False) + y = int.from_bytes(public_key_y, byteorder='big', signed=False) + public_key = EllipticCurvePublicNumbers(x, y, SECP256R1()).public_key() + shared_key = self.private_key.exchange(ECDH(), public_key) + + return shared_key + + +# ----------------------------------------------------------------------------- +# Functions +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +def xor(x, y): + assert(len(x) == len(y)) + return bytes(map(operator.xor, x, y)) + + +# ----------------------------------------------------------------------------- +def r(): + ''' + Generate 16 bytes of random data + ''' + return secrets.token_bytes(16) + + +# ----------------------------------------------------------------------------- +def e(key, data): + ''' + AES-128 ECB, expecting byte-swapped inputs and producing a byte-swapped output. + + See Bluetooth spec Vol 3, Part H - 2.2.1 Security function e + ''' + + cipher = Cipher(algorithms.AES(bytes(reversed(key))), modes.ECB()) + encryptor = cipher.encryptor() + return bytes(reversed(encryptor.update(bytes(reversed(data))))) + + +# ----------------------------------------------------------------------------- +def ah(k, r): + ''' + See Bluetooth spec Vol 3, Part H - 2.2.2 Random Address Hash function ah + ''' + + padding = bytes(13) + r_prime = r + padding + return e(k, r_prime)[0:3] + + +# ----------------------------------------------------------------------------- +def c1(k, r, preq, pres, iat, rat, ia, ra): + ''' + See Bluetooth spec, Vol 3, Part H - 2.2.3 Confirm value generation function c1 for LE Legacy Pairing + ''' + + p1 = bytes([iat, rat]) + preq + pres + p2 = ra + ia + bytes([0, 0, 0, 0]) + return e(k, xor(e(k, xor(r, p1)), p2)) + + +# ----------------------------------------------------------------------------- +def s1(k, r1, r2): + ''' + See Bluetooth spec, Vol 3, Part H - 2.2.4 Key generation function s1 for LE Legacy Pairing + ''' + + return e(k, r2[0:8] + r1[0:8]) + + +# ----------------------------------------------------------------------------- +def aes_cmac(m, k): + ''' + See Bluetooth spec, Vol 3, Part H - 2.2.5 FunctionAES-CMAC + + NOTE: the input and output of this internal function are in big-endian byte order + ''' + mac = cmac.CMAC(algorithms.AES(k)) + mac.update(m) + return mac.finalize() + + +# ----------------------------------------------------------------------------- +def f4(u, v, x, z): + ''' + See Bluetooth spec, Vol 3, Part H - 2.2.6 LE Secure Connections Confirm Value Generation Function f4 + ''' + return bytes(reversed(aes_cmac(bytes(reversed(u)) + bytes(reversed(v)) + z, bytes(reversed(x))))) + + +# ----------------------------------------------------------------------------- +def f5(w, n1, n2, a1, a2): + ''' + See Bluetooth spec, Vol 3, Part H - 2.2.7 LE Secure Connections Key Generation Function f5 + + NOTE: this returns a tuple: (MacKey, LTK) in little-endian byte order + ''' + salt = bytes.fromhex('6C888391AAF5A53860370BDB5A6083BE') + t = aes_cmac(bytes(reversed(w)), salt) + key_id = bytes([0x62, 0x74, 0x6c, 0x65]) + return ( + bytes(reversed(aes_cmac( + bytes([0]) + + key_id + + bytes(reversed(n1)) + + bytes(reversed(n2)) + + bytes(reversed(a1)) + + bytes(reversed(a2)) + + bytes([1, 0]), + t + ))), + bytes(reversed(aes_cmac( + bytes([1]) + + key_id + + bytes(reversed(n1)) + + bytes(reversed(n2)) + + bytes(reversed(a1)) + + bytes(reversed(a2)) + + bytes([1, 0]), + t + ))) + ) + + +# ----------------------------------------------------------------------------- +def f6(w, n1, n2, r, io_cap, a1, a2): + ''' + See Bluetooth spec, Vol 3, Part H - 2.2.8 LE Secure Connections Check Value Generation Function f6 + ''' + return bytes(reversed(aes_cmac( + bytes(reversed(n1)) + + bytes(reversed(n2)) + + bytes(reversed(r)) + + bytes(reversed(io_cap)) + + bytes(reversed(a1)) + + bytes(reversed(a2)), + bytes(reversed(w)) + ))) + + +# ----------------------------------------------------------------------------- +def g2(u, v, x, y): + ''' + See Bluetooth spec, Vol 3, Part H - 2.2.9 LE Secure Connections Numeric Comparison Value Generation Function g2 + ''' + return int.from_bytes( + aes_cmac(bytes(reversed(u)) + bytes(reversed(v)) + bytes(reversed(y)), bytes(reversed(x)))[-4:], + byteorder='big' + ) diff --git a/bumble/device.py b/bumble/device.py new file mode 100644 index 0000000..4dc71b0 --- /dev/null +++ b/bumble/device.py @@ -0,0 +1,1256 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import json +import asyncio +import logging + +from .hci import * +from .host import Host +from .gatt import * +from .gap import GenericAccessService +from .core import AdvertisingData, BT_CENTRAL_ROLE, BT_PERIPHERAL_ROLE +from .utils import AsyncRunner, CompositeEventEmitter, setup_event_forwarding, composite_listener +from . import gatt_client +from . import gatt_server +from . import smp +from . import sdp +from . import l2cap +from . import keys + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +DEVICE_DEFAULT_ADDRESS = '00:00:00:00:00:00' +DEVICE_DEFAULT_ADVERTISING_INTERVAL = 1000 # ms +DEVICE_DEFAULT_ADVERTISING_DATA = '' +DEVICE_DEFAULT_NAME = 'Bumble' +DEVICE_DEFAULT_INQUIRY_LENGTH = 8 # 10.24 seconds +DEVICE_DEFAULT_CLASS_OF_DEVICE = 0 +DEVICE_DEFAULT_SCAN_RESPONSE_DATA = b'' +DEVICE_DEFAULT_DATA_LENGTH = (27, 328, 27, 328) +DEVICE_DEFAULT_SCAN_INTERVAL = 60 # ms +DEVICE_DEFAULT_SCAN_WINDOW = 60 # ms +DEVICE_MIN_SCAN_INTERVAL = 25 +DEVICE_MAX_SCAN_INTERVAL = 10240 +DEVICE_MIN_SCAN_WINDOW = 25 +DEVICE_MAX_SCAN_WINDOW = 10240 + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + + +# ----------------------------------------------------------------------------- +class AdvertisementDataAccumulator: + def __init__(self): + self.advertising_data = AdvertisingData() + self.last_advertisement_type = None + self.connectable = False + self.flushable = False + + def update(self, data, advertisement_type): + if advertisement_type == HCI_LE_Advertising_Report_Event.SCAN_RSP: + if self.last_advertisement_type != HCI_LE_Advertising_Report_Event.SCAN_RSP: + self.advertising_data.append(data) + self.flushable = True + else: + self.advertising_data = AdvertisingData.from_bytes(data) + self.flushable = self.last_advertisement_type != HCI_LE_Advertising_Report_Event.SCAN_RSP + + if advertisement_type == HCI_LE_Advertising_Report_Event.ADV_IND or advertisement_type == HCI_LE_Advertising_Report_Event.ADV_DIRECT_IND: + self.connectable = True + elif advertisement_type == HCI_LE_Advertising_Report_Event.ADV_SCAN_IND or advertisement_type == HCI_LE_Advertising_Report_Event.ADV_NONCONN_IND: + self.connectable = False + + self.last_advertisement_type = advertisement_type + + +# ----------------------------------------------------------------------------- +class Peer: + def __init__(self, connection): + self.connection = connection + + # Create a GATT client for the connection + self.gatt_client = gatt_client.Client(connection) + connection.gatt_client = self.gatt_client + + @property + def services(self): + return self.gatt_client.services + + async def request_mtu(self, mtu): + return await self.gatt_client.request_mtu(mtu) + + async def discover_service(self, uuid): + return await self.gatt_client.discover_service(uuid) + + async def discover_services(self, uuids = []): + return await self.gatt_client.discover_services(uuids) + + async def discover_included_services(self, service): + return await self.gatt_client.discover_included_services(service) + + async def discover_characteristics(self, uuids = [], service = None): + return await self.gatt_client.discover_characteristics(uuids = uuids, service = service) + + async def discover_descriptors(self, characteristic = None, start_handle = None, end_handle = None): + return await self.gatt_client.discover_descriptors(characteristic, start_handle, end_handle) + + async def discover_attributes(self): + return await self.gatt_client.discover_attributes() + + async def subscribe(self, characteristic, subscriber=None): + return await self.gatt_client.subscribe(characteristic, subscriber) + + async def read_value(self, attribute): + return await self.gatt_client.read_value(attribute) + + async def write_value(self, attribute, value, with_response=False): + return await self.gatt_client.write_value(attribute, value, with_response) + + async def read_characteristics_by_uuid(self, uuid, service=None): + return await self.gatt_client.read_characteristics_by_uuid(uuid, service) + + def get_services_by_uuid(self, uuid): + return self.gatt_client.get_services_by_uuid(uuid) + + def get_characteristics_by_uuid(self, uuid, service = None): + return self.gatt_client.get_characteristics_by_uuid(uuid, service) + + def __str__(self): + return f'{self.connection.peer_address} as {self.connection.role_name}' + + +# ----------------------------------------------------------------------------- +class Connection(CompositeEventEmitter): + @composite_listener + class Listener: + def on_disconnection(self, reason): + pass + + def on_connection_parameters_update(self): + pass + + def on_connection_parameters_update_failure(self, error): + pass + + def on_connection_phy_update(self): + pass + + def on_connection_phy_update_failure(self, error): + pass + + def on_connection_att_mtu_update(self): + pass + + def on_connection_encryption_change(self): + pass + + def on_connection_encryption_key_refresh(self): + pass + + def __init__(self, device, handle, transport, peer_address, peer_resolvable_address, role, parameters): + super().__init__() + self.device = device + self.handle = handle + self.transport = transport + self.peer_address = peer_address + self.peer_resolvable_address = peer_resolvable_address + self.role = role + self.parameters = parameters + self.encryption = 0 + self.authenticated = False + self.phy = ConnectionPHY(HCI_LE_1M_PHY, HCI_LE_1M_PHY) + self.att_mtu = ATT_DEFAULT_MTU + self.data_length = DEVICE_DEFAULT_DATA_LENGTH + self.gatt_client = None # Per-connection client + self.gatt_server = device.gatt_server # By default, use the device's shared server + + @property + def role_name(self): + return 'CENTRAL' if self.role == BT_CENTRAL_ROLE else 'PERIPHERAL' + + @property + def is_encrypted(self): + return self.encryption != 0 + + def send_l2cap_pdu(self, cid, pdu): + self.device.send_l2cap_pdu(self.handle, cid, pdu) + + def create_l2cap_connector(self, psm): + return self.device.create_l2cap_connector(self, psm) + + async def disconnect(self, reason = HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR): + return await self.device.disconnect(self, reason) + + async def pair(self): + return await self.device.pair(self) + + def request_pairing(self): + return self.device.request_pairing(self) + + # [Classic only] + async def authenticate(self): + return await self.device.authenticate(self) + + async def encrypt(self): + return await self.device.encrypt(self) + + async def update_parameters( + self, + conn_interval_min, + conn_interval_max, + conn_latency, + supervision_timeout + ): + return await self.device.update_connection_parameters( + self, + conn_interval_min, + conn_interval_max, + conn_latency, + supervision_timeout + ) + + def __str__(self): + return f'Connection(handle=0x{self.handle:04X}, role={self.role_name}, address={self.peer_address})' + + +# ----------------------------------------------------------------------------- +class DeviceConfiguration: + def __init__(self): + # Setup defaults + self.name = DEVICE_DEFAULT_NAME + self.address = DEVICE_DEFAULT_ADDRESS + self.class_of_device = DEVICE_DEFAULT_CLASS_OF_DEVICE + self.scan_response_data = DEVICE_DEFAULT_SCAN_RESPONSE_DATA + self.advertising_interval_min = DEVICE_DEFAULT_ADVERTISING_INTERVAL + self.advertising_interval_max = DEVICE_DEFAULT_ADVERTISING_INTERVAL + self.advertising_data = bytes( + AdvertisingData([(AdvertisingData.COMPLETE_LOCAL_NAME, bytes(self.name, 'utf-8'))]) + ) + self.irk = bytes(16) # This really must be changed for any level of security + self.keystore = None + + def load_from_file(self, filename): + with open(filename, 'r') as file: + config = json.load(file) + + # Load simple properties + self.name = config.get('name', self.name) + self.address = Address(config.get('address', self.address)) + self.class_of_device = config.get('class_of_device', self.class_of_device) + self.advertising_interval_min = config.get('advertising_interval', self.advertising_interval_min) + self.advertising_interval_max = self.advertising_interval_min + self.keystore = config.get('keystore') + + # Load or synthesize an IRK + irk = config.get('irk') + if irk: + self.irk = bytes.fromhex(irk) + else: + # Construct an IRK from the address bytes + # NOTE: this is not secure, but will always give the same IRK for the same address + address_bytes = bytes(self.address) + self.irk = (address_bytes * 3)[:16] + + # Load advertising data + advertising_data = config.get('advertising_data') + if advertising_data: + self.advertising_data = bytes.fromhex(advertising_data) + + +# ----------------------------------------------------------------------------- +# Decorators used with the following Device class +# (we define them outside of the Device class, because defining decorators +# within a class requires unnecessarily complicated acrobatics) +# ----------------------------------------------------------------------------- + +# Decorator that converts the first argument from a connection handle to a connection +def with_connection_from_handle(function): + @functools.wraps(function) + def wrapper(self, connection_handle, *args, **kwargs): + if (connection := self.lookup_connection(connection_handle)) is None: + logger.warn(f'no connection found for handle 0x{connection_handle:04X}') + return + function(self, connection, *args, **kwargs) + return wrapper + + +# Decorator that adds a method to the list of event handlers for host events. +# This assumes that the method name starts with `on_` +def host_event_handler(function): + device_host_event_handlers.append(function.__name__[3:]) + return function + + +# List of host event handlers for the Device class. +# (we define this list outside the class, because referencing a class in method +# decorators is not straightforward) +device_host_event_handlers = [] + + +# ----------------------------------------------------------------------------- +class Device(CompositeEventEmitter): + + @composite_listener + class Listener: + def on_advertisement(self, address, data, rssi, advertisement_type): + pass + + def on_inquiry_result(self, address, class_of_device, data, rssi): + pass + + def on_connection(self, connection): + pass + + def on_connection_failure(self, error): + pass + + def on_characteristic_subscription(self, connection, characteristic, notify_enabled, indicate_enabled): + pass + + @classmethod + def with_hci(cls, name, address, hci_source, hci_sink): + ''' + Create a Device instance with a Host configured to communicate with a controller + through an HCI source/sink + ''' + host = Host(controller_source = hci_source, controller_sink = hci_sink) + return cls(name = name, address = address, host = host) + + @classmethod + def from_config_file(cls, filename): + config = DeviceConfiguration() + config.load_from_file(filename) + return cls(config=config) + + @classmethod + def from_config_file_with_hci(cls, filename, hci_source, hci_sink): + config = DeviceConfiguration() + config.load_from_file(filename) + host = Host(controller_source = hci_source, controller_sink = hci_sink) + return cls(config = config, host = host) + + def __init__(self, name = None, address = None, config = None, host = None, generic_access_service = True): + super().__init__() + + self._host = None + self.powered_on = False + self.advertising = False + self.auto_restart_advertising = False + self.command_timeout = 10 # seconds + self.gatt_server = gatt_server.Server(self) + self.sdp_server = sdp.Server(self) + self.l2cap_channel_manager = l2cap.ChannelManager() + self.advertisement_data = {} + self.scanning = False + self.discovering = False + self.connecting = False + self.disconnecting = False + self.connections = {} # Connections, by connection handle + self.le_enabled = True + self.classic_enabled = False + self.discoverable = False + self.connectable = False + self.inquiry_response = None + self.address_resolver = None + + # Use the initial config or a default + self.public_address = Address('00:00:00:00:00:00') + if config is None: + config = DeviceConfiguration() + self.name = config.name + self.random_address = config.address + self.class_of_device = config.class_of_device + self.scan_response_data = config.scan_response_data + self.advertising_data = config.advertising_data + self.advertising_interval_min = config.advertising_interval_min + self.advertising_interval_max = config.advertising_interval_max + self.keystore = keys.KeyStore.create_for_device(config) + self.irk = config.irk + + # If a name is passed, override the name from the config + if name: + self.name = name + + # If an address is passed, override the address from the config + if address: + if type(address) is str: + address = Address(address) + self.random_address = address + + # Setup SMP + # TODO: allow using a public address + self.smp_manager = smp.Manager(self, self.random_address) + + # Register the SDP server with the L2CAP Channel Manager + self.sdp_server.register(self.l2cap_channel_manager) + + # Add a GAP Service if requested + if generic_access_service: + self.gatt_server.add_service(GenericAccessService(self.name)) + + # Forward some events + setup_event_forwarding(self.gatt_server, self, 'characteristic_subscription') + + # Set the initial host + self.host = host + + @property + def host(self): + return self._host + + @host.setter + def host(self, host): + # Unsubscribe from events from the current host + if self._host: + for event_name in device_host_event_handlers: + self._host.remove_listener(event_name, getattr(self, f'on_{event_name}')) + + # Subscribe to events from the new host + if host: + for event_name in device_host_event_handlers: + host.on(event_name, getattr(self, f'on_{event_name}')) + + # Update the references to the new host + self._host = host + self.l2cap_channel_manager.host = host + + # Set providers for the new host + if host: + host.long_term_key_provider = self.get_long_term_key + host.link_key_provider = self.get_link_key + + @property + def sdp_service_records(self): + return self.sdp_server.service_records + + @sdp_service_records.setter + def sdp_service_records(self, service_records): + self.sdp_server.service_records = service_records + + def lookup_connection(self, connection_handle): + if connection := self.connections.get(connection_handle): + return connection + + def register_l2cap_server(self, psm, server): + self.l2cap_channel_manager.register_server(psm, server) + + def create_l2cap_connector(self, connection, psm): + return lambda: self.l2cap_channel_manager.connect(connection, psm) + + def create_l2cap_registrar(self, psm): + return lambda handler: self.register_l2cap_server(psm, handler) + + def send_l2cap_pdu(self, connection_handle, cid, pdu): + self.host.send_l2cap_pdu(connection_handle, cid, pdu) + + async def send_command(self, command): + try: + return await asyncio.wait_for(self.host.send_command(command), self.command_timeout) + except asyncio.TimeoutError: + logger.warning('!!! Command timed out') + + async def power_on(self): + # Reset the controller + await self.host.reset() + + response = await self.send_command(HCI_Read_BD_ADDR_Command()) + if response.return_parameters.status == HCI_SUCCESS: + logger.debug(color(f'BD_ADDR: {response.return_parameters.bd_addr}', 'yellow')) + self.public_address = response.return_parameters.bd_addr + + if self.le_enabled: + # Set the controller address + await self.send_command(HCI_LE_Set_Random_Address_Command( + random_address = self.random_address + )) + + # Load the address resolving list + if self.keystore: + await self.send_command(HCI_LE_Clear_Resolving_List_Command()) + + resolving_keys = await self.keystore.get_resolving_keys() + for (irk, address) in resolving_keys: + await self.send_command( + HCI_LE_Add_Device_To_Resolving_List_Command( + peer_identity_address_type = address.address_type, + peer_identity_address = address, + peer_irk = irk, + local_irk = self.irk + ) + ) + + # Enable address resolution + # await self.send_command( + # HCI_LE_Set_Address_Resolution_Enable_Command(address_resolution_enable=1) + # ) + + # Create a host-side address resolver + self.address_resolver = smp.AddressResolver(resolving_keys) + + if self.classic_enabled: + await self.send_command( + HCI_Write_Local_Name_Command(local_name=self.name.encode('utf8')) + ) + await self.send_command( + HCI_Write_Class_Of_Device_Command(class_of_device = self.class_of_device) + ) + await self.send_command( + HCI_Write_Simple_Pairing_Mode_Command(simple_pairing_mode = 0x01) + ) + + # Let the SMP manager know about the address + # TODO: allow using a public address + self.smp_manager.address = self.random_address + + # Done + self.powered_on = True + + async def start_advertising(self, auto_restart=False): + self.auto_restart_advertising = auto_restart + + # If we're advertising, stop first + if self.advertising: + await self.stop_advertising() + + # Set/update the advertising data + await self.send_command(HCI_LE_Set_Advertising_Data_Command( + advertising_data = self.advertising_data + )) + + # Set/update the scan response data + await self.send_command(HCI_LE_Set_Scan_Response_Data_Command( + scan_response_data = self.scan_response_data + )) + + # Set the advertising parameters + await self.send_command(HCI_LE_Set_Advertising_Parameters_Command( + # TODO: use real values, not fixed ones + advertising_interval_min = self.advertising_interval_min, + advertising_interval_max = self.advertising_interval_max, + advertising_type = HCI_LE_Set_Advertising_Parameters_Command.ADV_IND, + own_address_type = Address.RANDOM_DEVICE_ADDRESS, # TODO: allow using the public address + peer_address_type = Address.PUBLIC_DEVICE_ADDRESS, + peer_address = Address('00:00:00:00:00:00'), + advertising_channel_map = 7, + advertising_filter_policy = 0 + )) + + # Enable advertising + await self.send_command(HCI_LE_Set_Advertising_Enable_Command( + advertising_enable = 1 + )) + + self.advertising = True + + async def stop_advertising(self): + # Disable advertising + if self.advertising: + await self.send_command(HCI_LE_Set_Advertising_Enable_Command( + advertising_enable = 0 + )) + + self.advertising = False + + @property + def is_advertising(self): + return self.advertising + + async def start_scanning( + self, + active=True, + scan_interval=DEVICE_DEFAULT_SCAN_INTERVAL, # Scan interval in ms + scan_window=DEVICE_DEFAULT_SCAN_WINDOW, # Scan window in ms + own_address_type=Address.RANDOM_DEVICE_ADDRESS, + filter_duplicates=False + ): + # Check that the arguments are legal + if scan_interval < scan_window: + raise ValueError('scan_interval must be >= scan_window') + if scan_interval < DEVICE_MIN_SCAN_INTERVAL or scan_interval > DEVICE_MAX_SCAN_INTERVAL: + raise ValueError('scan_interval out of range') + if scan_window < DEVICE_MIN_SCAN_WINDOW or scan_window > DEVICE_MAX_SCAN_WINDOW: + raise ValueError('scan_interval out of range') + + # Set the scanning parameters + scan_type = HCI_LE_Set_Scan_Parameters_Command.ACTIVE_SCANNING if active else HCI_LE_Set_Scan_Parameters_Command.PASSIVE_SCANNING + await self.send_command(HCI_LE_Set_Scan_Parameters_Command( + le_scan_type = scan_type, + le_scan_interval = int(scan_window / 0.625), + le_scan_window = int(scan_window / 0.625), + own_address_type = own_address_type, + scanning_filter_policy = HCI_LE_Set_Scan_Parameters_Command.BASIC_UNFILTERED_POLICY + )) + + # Enable scanning + await self.send_command(HCI_LE_Set_Scan_Enable_Command( + le_scan_enable = 1, + filter_duplicates = 1 if filter_duplicates else 0 + )) + self.scanning = True + + async def stop_scanning(self): + await self.send_command(HCI_LE_Set_Scan_Enable_Command( + le_scan_enable = 0, + filter_duplicates = 0 + )) + self.scanning = False + + @property + def is_scanning(self): + return self.scanning + + @host_event_handler + def on_advertising_report(self, address, data, rssi, advertisement_type): + if not (accumulator := self.advertisement_data.get(address)): + accumulator = AdvertisementDataAccumulator() + self.advertisement_data[address] = accumulator + accumulator.update(data, advertisement_type) + if accumulator.flushable: + self.emit( + 'advertisement', + address, + accumulator.advertising_data, + rssi, + accumulator.connectable + ) + + async def start_discovery(self): + await self.host.send_command(HCI_Write_Inquiry_Mode_Command(inquiry_mode=HCI_EXTENDED_INQUIRY_MODE)) + + response = await self.send_command(HCI_Inquiry_Command( + lap = HCI_GENERAL_INQUIRY_LAP, + inquiry_length = DEVICE_DEFAULT_INQUIRY_LENGTH, + num_responses = 0 # Unlimited number of responses. + )) + if response.status != HCI_Command_Status_Event.PENDING: + self.discovering = False + raise RuntimeError(f'HCI_Inquiry command failed: {HCI_Constant.status_name(response.status)} ({response.status})') + + self.discovering = True + + async def stop_discovery(self): + await self.send_command(HCI_Inquiry_Cancel_Command()) + self.discovering = False + + @host_event_handler + def on_inquiry_result(self, address, class_of_device, data, rssi): + self.emit( + 'inquiry_result', + address, + class_of_device, + AdvertisingData.from_bytes(data), + rssi + ) + + async def set_scan_enable(self, inquiry_scan_enabled, page_scan_enabled): + if inquiry_scan_enabled and page_scan_enabled: + scan_enable = 0x03 + elif page_scan_enabled: + scan_enable = 0x02 + elif inquiry_scan_enabled: + scan_enable = 0x01 + else: + scan_enable = 0x00 + + return await self.send_command(HCI_Write_Scan_Enable_Command(scan_enable = scan_enable)) + + async def set_discoverable(self, discoverable=True): + self.discoverable = discoverable + if self.classic_enabled: + # Synthesize an inquiry response if none is set already + if self.inquiry_response is None: + self.inquiry_response = bytes( + AdvertisingData([ + (AdvertisingData.COMPLETE_LOCAL_NAME, bytes(self.name, 'utf-8')) + ]) + ) + + # Update the controller + await self.host.send_command( + HCI_Write_Extended_Inquiry_Response_Command( + fec_required = 0, + extended_inquiry_response = self.inquiry_response + ) + ) + await self.set_scan_enable( + inquiry_scan_enabled = self.discoverable, + page_scan_enabled = self.connectable + ) + + async def set_connectable(self, connectable=True): + self.connectable = connectable + if self.classic_enabled: + await self.set_scan_enable( + inquiry_scan_enabled = self.discoverable, + page_scan_enabled = self.connectable + ) + + async def connect(self, peer_address, transport=BT_LE_TRANSPORT): + ''' + Request a connection to a peer. + This method cannot be called if there is already a pending connection. + ''' + + # Adjust the transport automatically if we need to + if transport == BT_LE_TRANSPORT and not self.le_enabled: + transport = BT_BR_EDR_TRANSPORT + elif transport == BT_BR_EDR_TRANSPORT and not self.classic_enabled: + transport = BT_LE_TRANSPORT + + # Check that there isn't already a pending connection + if self.is_connecting: + raise InvalidStateError('connection already pending') + + if type(peer_address) is str: + try: + peer_address = Address(peer_address) + except ValueError: + # If the address is not parssable, assume it is a name instead + logger.debug('looking for peer by name') + peer_address = await self.find_peer_by_name(peer_address, transport) + + # Create a future so that we can wait for the connection's result + pending_connection = asyncio.get_running_loop().create_future() + self.on('connection', pending_connection.set_result) + self.on('connection_failure', pending_connection.set_exception) + + # Tell the controller to connect + if transport == BT_LE_TRANSPORT: + # TODO: use real values, not fixed ones + result = await self.send_command(HCI_LE_Create_Connection_Command( + le_scan_interval = 96, + le_scan_window = 96, + initiator_filter_policy = 0, + peer_address_type = peer_address.address_type, + peer_address = peer_address, + own_address_type = Address.RANDOM_DEVICE_ADDRESS, + conn_interval_min = 12, + conn_interval_max = 24, + conn_latency = 0, + supervision_timeout = 72, + minimum_ce_length = 0, + maximum_ce_length = 0 + )) + else: + # TODO: use real values, not fixed ones + result = await self.send_command(HCI_Create_Connection_Command( + bd_addr = peer_address, + packet_type = 0xCC18, # FIXME: change + page_scan_repetition_mode = HCI_R2_PAGE_SCAN_REPETITION_MODE, + clock_offset = 0x0000, + allow_role_switch = 0x01, + reserved = 0 + )) + + try: + if result.status != HCI_Command_Status_Event.PENDING: + raise RuntimeError(f'HCI_LE_Create_Connection_Command failed: {HCI_Constant.status_name(result.status)} ({result.status})') + + # Wait for the connection process to complete + self.connecting = True + return await pending_connection + finally: + self.remove_listener('connection', pending_connection.set_result) + self.remove_listener('connection_failure', pending_connection.set_exception) + self.connecting = False + + @property + def is_connecting(self): + return self.connecting + + @property + def is_disconnecting(self): + return self.disconnecting + + async def cancel_connection(self): + if not self.is_connecting: + return + await self.send_command(HCI_LE_Create_Connection_Cancel_Command()) + + async def disconnect(self, connection, reason): + # Create a future so that we can wait for the disconnection's result + pending_disconnection = asyncio.get_running_loop().create_future() + self.on('disconnection', pending_disconnection.set_result) + self.on('disconnection_failure', pending_disconnection.set_exception) + + # Request a disconnection + result = await self.send_command(HCI_Disconnect_Command(connection_handle = connection.handle, reason = reason)) + + try: + if result.status != HCI_Command_Status_Event.PENDING: + raise RuntimeError(f'HCI_Disconnect_Command failed: {HCI_Constant.status_name(result.status)} ({result.status})') + + # Wait for the disconnection process to complete + self.disconnecting = True + return await pending_disconnection + finally: + self.remove_listener('disconnection', pending_disconnection.set_result) + self.remove_listener('disconnection_failure', pending_disconnection.set_exception) + self.disconnecting = False + + async def update_connection_parameters( + self, + connection, + conn_interval_min, + conn_interval_max, + conn_latency, + supervision_timeout, + minimum_ce_length = 0, + maximum_ce_length = 0 + ): + ''' + NOTE: the name of the parameters may look odd, but it just follows the names used in the Bluetooth spec. + ''' + await self.send_command(HCI_LE_Connection_Update_Command( + connection_handle = connection.handle, + conn_interval_min = conn_interval_min, + conn_interval_max = conn_interval_max, + conn_latency = conn_latency, + supervision_timeout = supervision_timeout, + minimum_ce_length = minimum_ce_length, + maximum_ce_length = maximum_ce_length + )) + # TODO: check result + + async def find_peer_by_name(self, name, transport=BT_LE_TRANSPORT): + """ + Scan for a peer with a give name and return its address and transport + """ + + # Create a future to wait for an address to be found + peer_address = asyncio.get_running_loop().create_future() + + # Scan/inquire with event handlers to handle scan/inquiry results + def on_peer_found(address, ad_data): + local_name = ad_data.get(AdvertisingData.COMPLETE_LOCAL_NAME) + if local_name is None: + local_name = ad_data.get(AdvertisingData.SHORTENED_LOCAL_NAME) + if local_name is not None: + if local_name.decode('utf-8') == name: + peer_address.set_result(address) + try: + handler = None + if transport == BT_LE_TRANSPORT: + event_name = 'advertisement' + handler = self.on( + event_name, + lambda address, ad_data, rssi, connectable: + on_peer_found(address, ad_data) + ) + + was_scanning = self.scanning + if not self.scanning: + await self.start_scanning(filter_duplicates=True) + + elif transport == BT_BR_EDR_TRANSPORT: + event_name = 'inquiry_result' + handler = self.on( + event_name, + lambda address, class_of_device, eir_data, rssi: + on_peer_found(address, eir_data) + ) + + was_discovering = self.discovering + if not self.discovering: + await self.start_discovery() + else: + return None + + return await peer_address + finally: + if handler is not None: + self.remove_listener(event_name, handler) + + if transport == BT_LE_TRANSPORT and not was_scanning: + await self.stop_scanning() + elif transport == BT_BR_EDR_TRANSPORT and not was_discovering: + await self.stop_discovery() + + @property + def pairing_config_factory(self): + return self.smp_manager.pairing_config_factory + + @pairing_config_factory.setter + def pairing_config_factory(self, pairing_config_factory): + self.smp_manager.pairing_config_factory = pairing_config_factory + + async def pair(self, connection): + return await self.smp_manager.pair(connection) + + def request_pairing(self, connection): + return self.smp_manager.request_pairing(connection) + + async def get_long_term_key(self, connection_handle, rand, ediv): + if (connection := self.lookup_connection(connection_handle)) is None: + return + + # Start by looking for the key in an SMP session + ltk = self.smp_manager.get_long_term_key(connection, rand, ediv) + if ltk is not None: + return ltk + + # Then look for the key in the keystore + if self.keystore is not None: + keys = await self.keystore.get(str(connection.peer_address)) + if keys is not None: + logger.debug('found keys in the key store') + if keys.ltk: + return keys.ltk.value + elif connection.role == BT_CENTRAL_ROLE and keys.ltk_central: + return keys.ltk_central.value + elif connection.role == BT_PERIPHERAL_ROLE and keys.ltk_peripheral: + return keys.ltk_peripheral.value + + async def get_link_key(self, address): + # Look for the key in the keystore + if self.keystore is not None: + keys = await self.keystore.get(str(address)) + if keys is not None: + logger.debug('found keys in the key store') + return keys.link_key.value + + # [Classic only] + async def authenticate(self, connection): + # Set up event handlers + pending_authentication = asyncio.get_running_loop().create_future() + + def on_authentication_complete(): + pending_authentication.set_result(None) + + def on_authentication_failure(error): + pending_authentication.set_exception(error) + + connection.on('connection_authentication_complete', on_authentication_complete) + connection.on('connection_authentication_failure', on_authentication_failure) + + # Request the authentication + try: + result = await self.send_command( + HCI_Authentication_Requested_Command(connection_handle = connection.handle) + ) + if result.status != HCI_COMMAND_STATUS_PENDING: + logger.warn(f'HCI_Authentication_Requested_Command failed: {HCI_Constant.error_name(result.status)}') + raise HCI_Error(result.status) + + # Wait for the authentication to complete + await pending_authentication + finally: + connection.remove_listener('connection_authentication_complete', on_authentication_complete) + connection.remove_listener('connection_authentication_failure', on_authentication_failure) + + async def encrypt(self, connection): + # Set up event handlers + pending_encryption = asyncio.get_running_loop().create_future() + + def on_encryption_change(): + pending_encryption.set_result(None) + + def on_encryption_failure(error_code): + pending_encryption.set_exception(HCI_Error(error_code)) + + connection.on('connection_encryption_change', on_encryption_change) + connection.on('connection_encryption_failure', on_encryption_failure) + + # Request the encryption + try: + if connection.transport == BT_LE_TRANSPORT: + # Look for a key in the key store + if self.keystore is None: + raise RuntimeError('no key store') + + keys = await self.keystore.get(str(connection.peer_address)) + if keys is None: + raise RuntimeError('keys not found in key store') + + if keys.ltk is not None: + ltk = keys.ltk.value + rand = bytes(8) + ediv = 0 + elif keys.ltk_central is not None: + ltk = keys.ltk_central.value + rand = keys.ltk_central.rand + ediv = keys.ltk_central.ediv + else: + raise RuntimeError('no LTK found for peer') + + if connection.role != HCI_CENTRAL_ROLE: + raise InvalidStateError('only centrals can start encryption') + + result = await self.send_command( + HCI_LE_Start_Encryption_Command( + connection_handle = connection.handle, + random_number = rand, + encrypted_diversifier = ediv, + long_term_key = ltk + ) + ) + + if result.status != HCI_COMMAND_STATUS_PENDING: + logger.warn(f'HCI_LE_Start_Encryption_Command failed: {HCI_Constant.error_name(result.status)}') + raise HCI_Error(result.status) + else: + result = await self.send_command( + HCI_Set_Connection_Encryption_Command( + connection_handle = connection.handle, + encryption_enable = 0x01 + ) + ) + + if result.status != HCI_COMMAND_STATUS_PENDING: + logger.warn(f'HCI_Set_Connection_Encryption_Command failed: {HCI_Constant.error_name(result.status)}') + raise HCI_Error(result.status) + + # Wait for the result + await pending_encryption + finally: + connection.remove_listener('connection_encryption_change', on_encryption_change) + connection.remove_listener('connection_encryption_failure', on_encryption_failure) + + # [Classic only] + @host_event_handler + def on_link_key(self, bd_addr, link_key, key_type): + # Store the keys in the key store + if self.keystore: + pairing_keys = keys.PairingKeys() + pairing_keys.link_key = keys.PairingKeys.Key(value = link_key) + + async def store_keys(): + try: + await self.keystore.update(str(bd_addr), pairing_keys) + except Exception as error: + logger.warn(f'!!! error while storing keys: {error}') + + asyncio.create_task(store_keys()) + + def add_service(self, service): + self.gatt_server.add_service(service) + + def add_services(self, services): + self.gatt_server.add_services(services) + + async def notify_subscriber(self, connection, attribute, force=False): + await self.gatt_server.notify_subscriber(connection, attribute, force) + + async def notify_subscribers(self, attribute, force=False): + await self.gatt_server.notify_subscribers(attribute, force) + + async def indicate_subscriber(self, connection, attribute, force=False): + await self.gatt_server.indicate_subscriber(connection, attribute, force) + + async def indicate_subscribers(self, attribute): + await self.gatt_server.indicate_subscribers(attribute) + + @host_event_handler + def on_connection(self, connection_handle, transport, peer_address, peer_resolvable_address, role, connection_parameters): + logger.debug(f'*** Connection: [0x{connection_handle:04X}] {peer_address} as {HCI_Constant.role_name(role)}') + if connection_handle in self.connections: + logger.warn('new connection reuses the same handle as a previous connection') + + # Resolve the peer address if we can + if self.address_resolver: + if peer_address.is_resolvable: + resolved_address = self.address_resolver.resolve(peer_address) + if resolved_address is not None: + logger.debug(f'*** Address resolved as {resolved_address}') + peer_resolvable_address = peer_address + peer_address = resolved_address + + # Create a new connection + connection = Connection( + self, + connection_handle, + transport, + peer_address, + peer_resolvable_address, + role, + connection_parameters + ) + self.connections[connection_handle] = connection + + # We are no longer advertising + self.advertising = False + + # Emit an event to notify listeners of the new connection + self.emit('connection', connection) + + @host_event_handler + def on_connection_failure(self, error_code): + logger.debug(f'*** Connection failed: {error_code}') + error = ConnectionError( + error_code, + 'hci', + HCI_Constant.error_name(error_code) + ) + self.emit('connection_failure', error) + + @host_event_handler + @with_connection_from_handle + def on_disconnection(self, connection, reason): + logger.debug(f'*** Disconnection: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, reason={reason}') + connection.emit('disconnection', reason) + + # Remove the connection from the map + del self.connections[connection.handle] + + # Cleanup subsystems that maintain per-connection state + self.gatt_server.on_disconnection(connection) + + # Restart advertising if auto-restart is enabled + if self.auto_restart_advertising: + logger.debug('restarting advertising') + asyncio.create_task(self.start_advertising(auto_restart=self.auto_restart_advertising)) + + @host_event_handler + def on_disconnection_failure(self, error_code): + logger.debug(f'*** Disconnection failed: {error_code}') + error = ConnectionError( + error_code, + 'hci', + HCI_Constant.error_name(error_code) + ) + self.emit('disconnection_failure', error) + + @host_event_handler + @AsyncRunner.run_in_task() + async def on_inquiry_complete(self): + if self.discovering: + # Inquire again + await self.start_discovery() + + @host_event_handler + @with_connection_from_handle + def on_connection_authentication_complete(self, connection): + logger.debug(f'*** Connection Authentication Complete: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}') + connection.authenticated = True + connection.emit('connection_authentication_complete') + + @host_event_handler + @with_connection_from_handle + def on_connection_authentication_failure(self, connection, error): + logger.debug(f'*** Connection Authentication Failure: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}') + connection.emit('connection_authentication_failure', error) + + @host_event_handler + @with_connection_from_handle + def on_connection_encryption_change(self, connection, encryption): + logger.debug(f'*** Connection Encryption Change: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, encryption={encryption}') + connection.encryption = encryption + connection.emit('connection_encryption_change') + + @host_event_handler + @with_connection_from_handle + def on_connection_encryption_failure(self, connection, error): + logger.debug(f'*** Connection Encryption Failure: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}') + connection.emit('connection_encryption_failure', error) + + @host_event_handler + @with_connection_from_handle + def on_connection_encryption_key_refresh(self, connection): + logger.debug(f'*** Connection Key Refresh: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}') + connection.emit('connection_encryption_key_refresh') + + @host_event_handler + @with_connection_from_handle + def on_connection_parameters_update(self, connection, connection_parameters): + logger.debug(f'*** Connection Parameters Update: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, {connection_parameters}') + connection.parameters = connection_parameters + connection.emit('connection_parameters_update') + + @host_event_handler + @with_connection_from_handle + def on_connection_parameters_update_failure(self, connection, error): + logger.debug(f'*** Connection Parameters Update Failed: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}') + connection.emit('connection_parameters_update_failure', error) + + @host_event_handler + @with_connection_from_handle + def on_connection_phy_update(self, connection, connection_phy): + logger.debug(f'*** Connection PHY Update: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, {connection_phy}') + connection.phy = connection_phy + connection.emit('connection_phy_update') + + @host_event_handler + @with_connection_from_handle + def on_connection_phy_update_failure(self, connection, error): + logger.debug(f'*** Connection PHY Update Failed: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, error={error}') + connection.emit('connection_phy_update_failure', error) + + @host_event_handler + @with_connection_from_handle + def on_connection_att_mtu_update(self, connection, att_mtu): + logger.debug(f'*** Connection ATT MTU Update: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}, {att_mtu}') + connection.att_mtu = att_mtu + connection.emit('connection_att_mtu_update') + + @host_event_handler + @with_connection_from_handle + def on_connection_data_length_change(self, connection, max_tx_octets, max_tx_time, max_rx_octets, max_rx_time): + logger.debug(f'*** Connection Data Length Change: [0x{connection.handle:04X}] {connection.peer_address} as {connection.role_name}') + connection.data_length = (max_tx_octets, max_tx_time, max_rx_octets, max_rx_time) + connection.emit('connection_data_length_change') + + @with_connection_from_handle + def on_pairing_start(self, connection): + connection.emit('pairing_start') + + @with_connection_from_handle + def on_pairing(self, connection, keys): + connection.emit('pairing', keys) + + @with_connection_from_handle + def on_pairing_failure(self, connection, reason): + connection.emit('pairing_failure', reason) + + @host_event_handler + @with_connection_from_handle + def on_gatt_pdu(self, connection, pdu): + # Parse the L2CAP payload into an ATT PDU object + att_pdu = ATT_PDU.from_bytes(pdu) + + # Conveniently, even-numbered op codes are client->server and + # odd-numbered ones are server->client + if att_pdu.op_code & 1: + if connection.gatt_client is None: + logger.warn(color('no GATT client for connection 0x{connection_handle:04X}')) + return + connection.gatt_client.on_gatt_pdu(att_pdu) + else: + if connection.gatt_server is None: + logger.warn(color('no GATT server for connection 0x{connection_handle:04X}')) + return + connection.gatt_server.on_gatt_pdu(connection, att_pdu) + + @host_event_handler + @with_connection_from_handle + def on_smp_pdu(self, connection, pdu): + self.smp_manager.on_smp_pdu(connection, pdu) + + @host_event_handler + @with_connection_from_handle + def on_l2cap_pdu(self, connection, cid, pdu): + self.l2cap_channel_manager.on_pdu(connection, cid, pdu) + + def __str__(self): + return f'Device(name="{self.name}", random_address="{self.random_address}"", public_address="{self.public_address}")' diff --git a/bumble/gap.py b/bumble/gap.py new file mode 100644 index 0000000..8341215 --- /dev/null +++ b/bumble/gap.py @@ -0,0 +1,59 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import struct + +from .gatt import ( + Service, + Characteristic, + GATT_GENERIC_ACCESS_SERVICE, + GATT_DEVICE_NAME_CHARACTERISTIC, + GATT_APPEARANCE_CHARACTERISTIC +) + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +class GenericAccessService(Service): + def __init__(self, device_name, appearance = (0, 0)): + device_name_characteristic = Characteristic( + GATT_DEVICE_NAME_CHARACTERISTIC, + Characteristic.READ, + Characteristic.READABLE, + device_name.encode('utf-8')[:248] + ) + + appearance_characteristic = Characteristic( + GATT_APPEARANCE_CHARACTERISTIC, + Characteristic.READ, + Characteristic.READABLE, + struct.pack('= {ATT_DEFAULT_MTU}') + if mtu > 0xFFFF: + raise ValueError('MTU must be <= 0xFFFF') + + # We can only send one request per connection + if self.mtu_exchange_done: + return + + # Send the request + self.mtu_exchange_done = True + response = await self.send_request(ATT_Exchange_MTU_Request(client_rx_mtu = mtu)) + if response.op_code == ATT_ERROR_RESPONSE: + raise ProtocolError( + response.error_code, + 'att', + ATT_PDU.error_name(response.error_code), + response + ) + + self.mtu = max(ATT_DEFAULT_MTU, response.server_rx_mtu) + return self.mtu + + def get_services_by_uuid(self, uuid): + return [service for service in self.services if service.uuid == uuid] + + def get_characteristics_by_uuid(self, uuid, service = None): + services = [service] if service else self.services + return [c for c in [c for s in services for c in s.characteristics] if c.uuid == uuid] + + def on_service_discovered(self, service): + ''' Add a service to the service list if it wasn't already there ''' + already_known = False + for existing_service in self.services: + if existing_service.handle == service.handle: + already_known = True + break + if not already_known: + self.services.append(service) + + async def discover_services(self, uuids = None): + ''' + See Vol 3, Part G - 4.4.1 Discover All Primary Services + ''' + starting_handle = 0x0001 + services = [] + while starting_handle < 0xFFFF: + response = await self.send_request( + ATT_Read_By_Group_Type_Request( + starting_handle = starting_handle, + ending_handle = 0xFFFF, + attribute_group_type = GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE + ) + ) + if response is None: + # TODO raise appropriate exception + return [] + + # Check if we reached the end of the iteration + if response.op_code == ATT_ERROR_RESPONSE: + if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR: + # Unexpected end + logger.waning(f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}') + # TODO raise appropriate exception + return + break + + for attribute_handle, end_group_handle, attribute_value in response.attributes: + if attribute_handle < starting_handle or end_group_handle < attribute_handle: + # Something's not right + logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}') + return + + # Create a primary service object + service = Service(UUID.from_bytes(attribute_value), [], True) + service.handle = attribute_handle + service.end_group_handle = end_group_handle + + # Filter out returned services based on the given uuids list + if (not uuids) or (service.uuid in uuids): + services.append(service) + + # Add the service to the peer's service list + self.on_service_discovered(service) + + # Stop if for some reason the list was empty + if not response.attributes: + break + + # Move on to the next chunk + starting_handle = response.attributes[-1][1] + 1 + + return services + + async def discover_service(self, uuid): + ''' + See Vol 3, Part G - 4.4.2 Discover Primary Service by Service UUID + ''' + + # Force uuid to be a UUID object + if type(uuid) is str: + uuid = UUID(uuid) + + starting_handle = 0x0001 + services = [] + while starting_handle < 0xFFFF: + response = await self.send_request( + ATT_Find_By_Type_Value_Request( + starting_handle = starting_handle, + ending_handle = 0xFFFF, + attribute_type = GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE, + attribute_value = uuid.to_pdu_bytes() + ) + ) + if response is None: + # TODO raise appropriate exception + return [] + + # Check if we reached the end of the iteration + if response.op_code == ATT_ERROR_RESPONSE: + if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR: + # Unexpected end + logger.waning(f'!!! unexpected error while discovering services: {HCI_Constant.error_name(response.error_code)}') + # TODO raise appropriate exception + return + break + + for attribute_handle, end_group_handle in response.handles_information: + if attribute_handle < starting_handle or end_group_handle < attribute_handle: + # Something's not right + logger.warning(f'bogus handle values: {attribute_handle} {end_group_handle}') + return + + # Create a primary service object + service = Service(uuid, [], True) + service.handle = attribute_handle + service.end_group_handle = end_group_handle + + # Add the service to the peer's service list + services.append(service) + self.on_service_discovered(service) + + # Check if we've reached the end already + if end_group_handle == 0xFFFF: + break + + # Stop if for some reason the list was empty + if not response.handles_information: + break + + # Move on to the next chunk + starting_handle = response.handles_information[-1][1] + 1 + + return services + + async def discover_included_services(self, service): + ''' + See Vol 3, Part G - 4.5.1 Find Included Services + ''' + # TODO + return [] + + async def discover_characteristics(self, uuids, service): + ''' + See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2 Discover Characteristics by UUID + ''' + + # Cast the UUIDs type from string to object if needed + uuids = [UUID(uuid) if type(uuid) is str else uuid for uuid in uuids] + + # Decide which services to discover for + services = [service] if service else self.services + + # Perform characteristic discovery for each service + discovered_characteristics = [] + for service in services: + starting_handle = service.handle + ending_handle = service.end_group_handle + + characteristics = [] + while starting_handle <= ending_handle: + response = await self.send_request( + ATT_Read_By_Type_Request( + starting_handle = starting_handle, + ending_handle = ending_handle, + attribute_type = GATT_CHARACTERISTIC_ATTRIBUTE_TYPE + ) + ) + if response is None: + # TODO raise appropriate exception + return [] + + # Check if we reached the end of the iteration + if response.op_code == ATT_ERROR_RESPONSE: + if response.error_code != ATT_ATTRIBUTE_NOT_FOUND_ERROR: + # Unexpected end + logger.warning(f'!!! unexpected error while discovering characteristics: {HCI_Constant.error_name(response.error_code)}') + # TODO raise appropriate exception + return + break + + # Stop if for some reason the list was empty + if not response.attributes: + break + + # Process all characteristics returned in this iteration + for attribute_handle, attribute_value in response.attributes: + if attribute_handle < starting_handle: + # Something's not right + logger.warning(f'bogus handle value: {attribute_handle}') + return [] + + properties, handle = struct.unpack_from(' mtu - 3: + value = value[:mtu - 3] + + # Notify + notification = ATT_Handle_Value_Notification( + attribute_handle = attribute.handle, + attribute_value = value + ) + logger.debug(f'GATT Notify from server: [0x{connection.handle:04X}] {notification}') + self.send_gatt_pdu(connection.handle, notification.to_bytes()) + + async def notify_subscribers(self, attribute, force=False): + # Get all the connections for which there's at least one subscription + connections = [ + connection for connection in [ + self.device.lookup_connection(connection_handle) + for (connection_handle, subscribers) in self.subscribers.items() + if force or subscribers.get(attribute.handle) + ] + if connection is not None + ] + + # Notify for each connection + if connections: + await asyncio.wait([ + self.notify_subscriber(connection, attribute, force) + for connection in connections + ]) + + async def indicate_subscriber(self, connection, attribute, force=False): + # Check if there's a subscriber + if not force: + subscribers = self.subscribers.get(connection.handle) + if not subscribers: + logger.debug('not indicating, no subscribers') + return + cccd = subscribers.get(attribute.handle) + if not cccd: + logger.debug(f'not indicating, no subscribers for handle {attribute.handle:04X}') + return + if len(cccd) != 2 or (cccd[0] & 0x02 == 0): + logger.debug(f'not indicating, cccd={cccd.hex()}') + return + + # Get the value + value = attribute.read_value(connection) + + # Truncate if needed + mtu = self.get_mtu(connection) + if len(value) > mtu - 3: + value = value[:mtu - 3] + + # Indicate + indication = ATT_Handle_Value_Indication( + attribute_handle = attribute.handle, + attribute_value = value + ) + logger.debug(f'GATT Indicate from server: [0x{connection.handle:04X}] {indication}') + + # Wait until we can send (only one pending indication at a time per connection) + async with self.indication_semaphores[connection.handle]: + assert(self.pending_confirmations[connection.handle] is None) + + # Create a future value to hold the eventual response + self.pending_confirmations[connection.handle] = asyncio.get_running_loop().create_future() + + try: + self.send_gatt_pdu(connection.handle, indication.to_bytes()) + await asyncio.wait_for(self.pending_confirmations[connection.handle], GATT_REQUEST_TIMEOUT) + except asyncio.TimeoutError: + logger.warning(color('!!! GATT Indicate timeout', 'red')) + raise TimeoutError(f'GATT timeout for {indication.name}') + finally: + self.pending_confirmations[connection.handle] = None + + async def indicate_subscribers(self, attribute): + # Get all the connections for which there's at least one subscription + connections = [ + connection for connection in [ + self.device.lookup_connection(connection_handle) + for (connection_handle, subscribers) in self.subscribers.items() + if subscribers.get(attribute.handle) + ] + if connection is not None + ] + + # Indicate for each connection + if connections: + await asyncio.wait([ + self.indicate_subscriber(connection, attribute) + for connection in connections + ]) + + def on_disconnection(self, connection): + if connection.handle in self.mtus: + del self.mtus[connection.handle] + if connection.handle in self.subscribers: + del self.subscribers[connection.handle] + if connection.handle in self.indication_semaphores: + del self.indication_semaphores[connection.handle] + if connection.handle in self.pending_confirmations: + del self.pending_confirmations[connection.handle] + + def on_gatt_pdu(self, connection, att_pdu): + logger.debug(f'GATT Request to server: [0x{connection.handle:04X}] {att_pdu}') + handler_name = f'on_{att_pdu.name.lower()}' + handler = getattr(self, handler_name, None) + if handler is not None: + try: + handler(connection, att_pdu) + except ATT_Error as error: + logger.debug(f'normal exception returned by handler: {error}') + response = ATT_Error_Response( + request_opcode_in_error = att_pdu.op_code, + attribute_handle_in_error = error.att_handle, + error_code = error.error_code + ) + self.send_response(connection, response) + except Exception as error: + logger.warning(f'{color("!!! Exception in handler:", "red")} {error}') + response = ATT_Error_Response( + request_opcode_in_error = att_pdu.op_code, + attribute_handle_in_error = 0x0000, + error_code = ATT_UNLIKELY_ERROR_ERROR + ) + self.send_response(connection, response) + raise error + else: + # No specific handler registered + if att_pdu.op_code in ATT_REQUESTS: + # Invoke the generic handler + self.on_att_request(connection, att_pdu) + else: + # Just ignore + logger.warning(f'{color("--- Ignoring GATT Request from [0x{connection.handle:04X}]:", "red")} {att_pdu}') + + def get_mtu(self, connection): + return self.mtus.get(connection.handle, ATT_DEFAULT_MTU) + + ####################################################### + # ATT handlers + ####################################################### + def on_att_request(self, connection, pdu): + ''' + Handler for requests without a more specific handler + ''' + logger.warning(f'{color(f"--- Unsupported ATT Request from [0x{connection.handle:04X}]:", "red")} {pdu}') + response = ATT_Error_Response( + request_opcode_in_error = pdu.op_code, + attribute_handle_in_error = 0x0000, + error_code = ATT_REQUEST_NOT_SUPPORTED_ERROR + ) + self.send_response(connection, response) + + def on_att_exchange_mtu_request(self, connection, request): + ''' + See Bluetooth spec Vol 3, Part F - 3.4.2.1 Exchange MTU Request + ''' + mtu = max(ATT_DEFAULT_MTU, min(self.max_mtu, request.client_rx_mtu)) + self.mtus[connection.handle] = mtu + self.send_response(connection, ATT_Exchange_MTU_Response(server_rx_mtu = mtu)) + + # Notify the device + self.device.on_connection_att_mtu_update(connection.handle, mtu) + + def on_att_find_information_request(self, connection, request): + ''' + See Bluetooth spec Vol 3, Part F - 3.4.3.1 Find Information Request + ''' + + # Check the request parameters + if request.starting_handle == 0 or request.starting_handle > request.ending_handle: + self.send_response(connection, ATT_Error_Response( + request_opcode_in_error = request.op_code, + attribute_handle_in_error = request.starting_handle, + error_code = ATT_INVALID_HANDLE_ERROR + )) + return + + # Build list of returned attributes + pdu_space_available = self.get_mtu(connection) - 2 + attributes = [] + uuid_size = 0 + for attribute in ( + attribute for attribute in self.attributes if + attribute.handle >= request.starting_handle and + attribute.handle <= request.ending_handle + ): + # TODO: check permissions + + this_uuid_size = len(attribute.type.to_pdu_bytes()) + + if attributes: + # Check if this attribute has the same type size as the previous one + if this_uuid_size != uuid_size: + break + + # Check if there's enough space for one more entry + uuid_size = this_uuid_size + if pdu_space_available < 2 + uuid_size: + break + + # Add the attribute to the list + attributes.append(attribute) + pdu_space_available -= 2 + uuid_size + + # Return the list of attributes + if attributes: + information_data_list = [ + struct.pack('= request.starting_handle and + attribute.handle <= request.ending_handle and + attribute.type == request.attribute_type and + attribute.read_value(connection) == request.attribute_value and + pdu_space_available >= 4 + ): + # TODO: check permissions + + # Add the attribute to the list + attributes.append(attribute) + pdu_space_available -= 4 + + # Return the list of attributes + if attributes: + handles_information_list = [] + for attribute in attributes: + if attribute.type in { + GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE, + GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE, + GATT_CHARACTERISTIC_ATTRIBUTE_TYPE + }: + # Part of a group + group_end_handle = attribute.end_group_handle + else: + # Not part of a group + group_end_handle = attribute.handle + handles_information_list.append(struct.pack('= request.starting_handle and + attribute.handle <= request.ending_handle and + pdu_space_available + ): + # TODO: check permissions + + # Check the attribute value size + attribute_value = attribute.read_value(connection) + max_attribute_size = min(mtu - 4, 253) + if len(attribute_value) > max_attribute_size: + # We need to truncate + attribute_value = attribute_value[:max_attribute_size] + if attributes and len(attributes[0][1]) != len(attribute_value): + # Not the same size as previous attribute, stop here + break + + # Check if there is enough space + entry_size = 2 + len(attribute_value) + if pdu_space_available < entry_size: + break + + # Add the attribute to the list + attributes.append((attribute.handle, attribute_value)) + pdu_space_available -= entry_size + + if attributes: + attribute_data_list = [struct.pack(' len(value): + response = ATT_Error_Response( + request_opcode_in_error = request.op_code, + attribute_handle_in_error = request.attribute_handle, + error_code = ATT_INVALID_OFFSET_ERROR + ) + elif len(value) <= mtu - 1: + response = ATT_Error_Response( + request_opcode_in_error = request.op_code, + attribute_handle_in_error = request.attribute_handle, + error_code = ATT_ATTRIBUTE_NOT_LONG_ERROR + ) + else: + part_size = min(mtu - 1, len(value) - request.value_offset) + response = ATT_Read_Blob_Response( + part_attribute_value = value[request.value_offset:request.value_offset + part_size] + ) + else: + response = ATT_Error_Response( + request_opcode_in_error = request.op_code, + attribute_handle_in_error = request.attribute_handle, + error_code = ATT_INVALID_HANDLE_ERROR + ) + self.send_response(connection, response) + + def on_att_read_by_group_type_request(self, connection, request): + ''' + See Bluetooth spec Vol 3, Part F - 3.4.4.9 Read by Group Type Request + ''' + if request.attribute_group_type not in { + GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE, + GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE, + GATT_INCLUDE_ATTRIBUTE_TYPE + }: + response = ATT_Error_Response( + request_opcode_in_error = request.op_code, + attribute_handle_in_error = request.starting_handle, + error_code = ATT_UNSUPPORTED_GROUP_TYPE_ERROR + ) + self.send_response(connection, response) + return + + mtu = self.get_mtu(connection) + pdu_space_available = mtu - 2 + attributes = [] + for attribute in ( + attribute for attribute in self.attributes if + attribute.type == request.attribute_group_type and + attribute.handle >= request.starting_handle and + attribute.handle <= request.ending_handle and + pdu_space_available + ): + # Check the attribute value size + attribute_value = attribute.read_value(connection) + max_attribute_size = min(mtu - 6, 251) + if len(attribute_value) > max_attribute_size: + # We need to truncate + attribute_value = attribute_value[:max_attribute_size] + if attributes and len(attributes[0][2]) != len(attribute_value): + # Not the same size as previous attributes, stop here + break + + # Check if there is enough space + entry_size = 4 + len(attribute_value) + if pdu_space_available < entry_size: + break + + # Add the attribute to the list + attributes.append((attribute.handle, attribute.end_group_handle, attribute_value)) + pdu_space_available -= entry_size + + if attributes: + attribute_data_list = [ + struct.pack(' GATT_MAX_ATTRIBUTE_VALUE_SIZE: + self.send_response(connection, ATT_Error_Response( + request_opcode_in_error = request.op_code, + attribute_handle_in_error = request.attribute_handle, + error_code = ATT_INVALID_ATTRIBUTE_LENGTH_ERROR + )) + return + + # Accept the value + attribute.write_value(connection, request.attribute_value) + + # Done + self.send_response(connection, ATT_Write_Response()) + + def on_att_write_command(self, connection, request): + ''' + See Bluetooth spec Vol 3, Part F - 3.4.5.3 Write Command + ''' + + # Check that the attribute exists + attribute = self.get_attribute(request.attribute_handle) + if attribute is None: + return + + # TODO: check permissions + + # Check the request parameters + if len(request.attribute_value) > GATT_MAX_ATTRIBUTE_VALUE_SIZE: + return + + # Accept the value + try: + attribute.write_value(connection, request.attribute_value) + except Exception as error: + logger.warning(f'!!! ignoring exception: {error}') + + def on_att_handle_value_confirmation(self, connection, confirmation): + ''' + See Bluetooth spec Vol 3, Part F - 3.4.7.3 Handle Value Confirmation + ''' + if self.pending_confirmations[connection.handle] is None: + # Not expected! + logger.warning('!!! unexpected confirmation, there is no pending indication') + return + + self.pending_confirmations[connection.handle].set_result(None) diff --git a/bumble/hci.py b/bumble/hci.py new file mode 100644 index 0000000..a5af647 --- /dev/null +++ b/bumble/hci.py @@ -0,0 +1,3471 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import struct +import collections +import logging +import functools +from colors import color + +from .core import * + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Utils +# ----------------------------------------------------------------------------- +def hci_command_op_code(ogf, ocf): + return (ogf << 10 | ocf) + + +def key_with_value(dictionary, target_value): + for key, value in dictionary.items(): + if value == target_value: + return key + return None + + +def indent_lines(str): + return '\n'.join([' ' + line for line in str.split('\n')]) + + +def map_null_terminated_utf8_string(utf8_bytes): + try: + terminator = utf8_bytes.find(0) + if terminator < 0: + return utf8_bytes + return utf8_bytes[0:terminator].decode('utf8') + except UnicodeDecodeError: + return utf8_bytes + + +def map_class_of_device(class_of_device): + service_classes, major_device_class, minor_device_class = DeviceClass.split_class_of_device(class_of_device) + return f'[{class_of_device:06X}] Services({",".join(DeviceClass.service_class_labels(service_classes))}),Class({DeviceClass.major_device_class_name(major_device_class)}|{DeviceClass.minor_device_class_name(major_device_class, minor_device_class)})' + + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- + +# HCI Version +HCI_VERSION_BLUETOOTH_CORE_1_0B = 0 +HCI_VERSION_BLUETOOTH_CORE_1_1 = 1 +HCI_VERSION_BLUETOOTH_CORE_1_2 = 2 +HCI_VERSION_BLUETOOTH_CORE_2_0_EDR = 3 +HCI_VERSION_BLUETOOTH_CORE_2_1_EDR = 4 +HCI_VERSION_BLUETOOTH_CORE_3_0_HS = 5 +HCI_VERSION_BLUETOOTH_CORE_4_0 = 6 +HCI_VERSION_BLUETOOTH_CORE_4_1 = 7 +HCI_VERSION_BLUETOOTH_CORE_4_2 = 8 +HCI_VERSION_BLUETOOTH_CORE_5_0 = 9 +HCI_VERSION_BLUETOOTH_CORE_5_1 = 10 +HCI_VERSION_BLUETOOTH_CORE_5_2 = 11 + +# HCI Packet types +HCI_COMMAND_PACKET = 0x01 +HCI_ACL_DATA_PACKET = 0x02 +HCI_SYNCHRONOUS_DATA_PACKET = 0x03 +HCI_EVENT_PACKET = 0x04 + +# HCI Event Codes +HCI_INQUIRY_COMPLETE_EVENT = 0x01 +HCI_INQUIRY_RESULT_EVENT = 0x02 +HCI_CONNECTION_COMPLETE_EVENT = 0x03 +HCI_CONNECTION_REQUEST_EVENT = 0x04 +HCI_DISCONNECTION_COMPLETE_EVENT = 0x05 +HCI_AUTHENTICATION_COMPLETE_EVENT = 0x06 +HCI_REMOTE_NAME_REQUEST_COMPLETE_EVENT = 0x07 +HCI_ENCRYPTION_CHANGE_EVENT = 0x08 +HCI_CHANGE_CONNECTION_LINK_KEY_COMPLETE_EVENT = 0x09 +HCI_LINK_KEY_TYPE_CHANGED_EVENT = 0x0A +HCI_READ_REMOTE_SUPPORTED_FEATURES_COMPLETE_EVENT = 0x0B +HCI_READ_REMOTE_VERSION_INFORMATION_COMPLETE_EVENT = 0x0C +HCI_QOS_SETUP_COMPLETE_EVENT = 0x0D +HCI_COMMAND_COMPLETE_EVENT = 0x0E +HCI_COMMAND_STATUS_EVENT = 0x0F +HCI_HARDWARE_ERROR_EVENT = 0x10 +HCI_FLUSH_OCCURRED_EVENT = 0x11 +HCI_ROLE_CHANGE_EVENT = 0x12 +HCI_NUMBER_OF_COMPLETED_PACKETS_EVENT = 0x13 +HCI_MODE_CHANGE_EVENT = 0x14 +HCI_RETURN_LINK_KEYS_EVENT = 0x15 +HCI_PIN_CODE_REQUEST_EVENT = 0x16 +HCI_LINK_KEY_REQUEST_EVENT = 0x17 +HCI_LINK_KEY_NOTIFICATION_EVENT = 0x18 +HCI_LOOPBACK_COMMAND_EVENT = 0x19 +HCI_DATA_BUFFER_OVERFLOW_EVENT = 0x1A +HCI_MAX_SLOTS_CHANGE_EVENT = 0x1B +HCI_READ_CLOCK_OFFSET_COMPLETE_EVENT = 0x1C +HCI_CONNECTION_PACKET_TYPE_CHANGED_EVENT = 0x1D +HCI_QOS_VIOLATION_EVENT = 0x1E +HCI_PAGE_SCAN_REPETITION_MODE_CHANGE_EVENT = 0x20 +HCI_FLOW_SPECIFICATION_COMPLETE_EVENT = 0x21 +HCI_INQUIRY_RESULT_WITH_RSSI_EVENT = 0x22 +HCI_READ_REMOTE_EXTENDED_FEATURES_COMPLETE_EVENT = 0x23 +HCI_SYNCHRONOUS_CONNECTION_COMPLETE_EVENT = 0x2C +HCI_SYNCHRONOUS_CONNECTION_CHANGED_EVENT = 0x2D +HCI_SNIFF_SUBRATING_EVENT = 0x2E +HCI_EXTENDED_INQUIRY_RESULT_EVENT = 0x2F +HCI_ENCRYPTION_KEY_REFRESH_COMPLETE_EVENT = 0x30 +HCI_IO_CAPABILITY_REQUEST_EVENT = 0x31 +HCI_IO_CAPABILITY_RESPONSE_EVENT = 0x32 +HCI_USER_CONFIRMATION_REQUEST_EVENT = 0x33 +HCI_USER_PASSKEY_REQUEST_EVENT = 0x34 +HCI_REMOTE_OOB_DATA_REQUEST = 0x35 +HCI_SIMPLE_PAIRING_COMPLETE_EVENT = 0x36 +HCI_LINK_SUPERVISION_TIMEOUT_CHANGED_EVENT = 0x38 +HCI_ENHANCED_FLUSH_COMPLETE_EVENT = 0x39 +HCI_USER_PASSKEY_NOTIFICATION_EVENT = 0x3B +HCI_KEYPRESS_NOTIFICATION_EVENT = 0x3C +HCI_REMOTE_HOST_SUPPORTED_FEATURES_NOTIFICATION_EVENT = 0x3D +HCI_LE_META_EVENT = 0x3E +HCI_NUMBER_OF_COMPLETED_DATA_BLOCKS_EVENT = 0x48 + +HCI_EVENT_NAMES = { + HCI_INQUIRY_COMPLETE_EVENT: 'HCI_INQUIRY_COMPLETE_EVENT', + HCI_INQUIRY_RESULT_EVENT: 'HCI_INQUIRY_RESULT_EVENT', + HCI_CONNECTION_COMPLETE_EVENT: 'HCI_CONNECTION_COMPLETE_EVENT', + HCI_CONNECTION_REQUEST_EVENT: 'HCI_CONNECTION_REQUEST_EVENT', + HCI_DISCONNECTION_COMPLETE_EVENT: 'HCI_DISCONNECTION_COMPLETE_EVENT', + HCI_AUTHENTICATION_COMPLETE_EVENT: 'HCI_AUTHENTICATION_COMPLETE_EVENT', + HCI_REMOTE_NAME_REQUEST_COMPLETE_EVENT: 'HCI_REMOTE_NAME_REQUEST_COMPLETE_EVENT', + HCI_ENCRYPTION_CHANGE_EVENT: 'HCI_ENCRYPTION_CHANGE_EVENT', + HCI_CHANGE_CONNECTION_LINK_KEY_COMPLETE_EVENT: 'HCI_CHANGE_CONNECTION_LINK_KEY_COMPLETE_EVENT', + HCI_LINK_KEY_TYPE_CHANGED_EVENT: 'HCI_LINK_KEY_TYPE_CHANGED_EVENT', + HCI_INQUIRY_RESULT_WITH_RSSI_EVENT: 'HCI_INQUIRY_RESULT_WITH_RSSI_EVENT', + HCI_READ_REMOTE_SUPPORTED_FEATURES_COMPLETE_EVENT: 'HCI_READ_REMOTE_SUPPORTED_FEATURES_COMPLETE_EVENT', + HCI_READ_REMOTE_VERSION_INFORMATION_COMPLETE_EVENT: 'HCI_READ_REMOTE_VERSION_INFORMATION_COMPLETE_EVENT', + HCI_QOS_SETUP_COMPLETE_EVENT: 'HCI_QOS_SETUP_COMPLETE_EVENT', + HCI_SYNCHRONOUS_CONNECTION_COMPLETE_EVENT: 'HCI_SYNCHRONOUS_CONNECTION_COMPLETE_EVENT', + HCI_SYNCHRONOUS_CONNECTION_CHANGED_EVENT: 'HCI_SYNCHRONOUS_CONNECTION_CHANGED_EVENT', + HCI_SNIFF_SUBRATING_EVENT: 'HCI_SNIFF_SUBRATING_EVENT', + HCI_COMMAND_COMPLETE_EVENT: 'HCI_COMMAND_COMPLETE_EVENT', + HCI_COMMAND_STATUS_EVENT: 'HCI_COMMAND_STATUS_EVENT', + HCI_HARDWARE_ERROR_EVENT: 'HCI_HARDWARE_ERROR_EVENT', + HCI_FLUSH_OCCURRED_EVENT: 'HCI_FLUSH_OCCURRED_EVENT', + HCI_ROLE_CHANGE_EVENT: 'HCI_ROLE_CHANGE_EVENT', + HCI_NUMBER_OF_COMPLETED_PACKETS_EVENT: 'HCI_NUMBER_OF_COMPLETED_PACKETS_EVENT', + HCI_MODE_CHANGE_EVENT: 'HCI_MODE_CHANGE_EVENT', + HCI_RETURN_LINK_KEYS_EVENT: 'HCI_RETURN_LINK_KEYS_EVENT', + HCI_PIN_CODE_REQUEST_EVENT: 'HCI_PIN_CODE_REQUEST_EVENT', + HCI_LINK_KEY_REQUEST_EVENT: 'HCI_LINK_KEY_REQUEST_EVENT', + HCI_LINK_KEY_NOTIFICATION_EVENT: 'HCI_LINK_KEY_NOTIFICATION_EVENT', + HCI_LOOPBACK_COMMAND_EVENT: 'HCI_LOOPBACK_COMMAND_EVENT', + HCI_DATA_BUFFER_OVERFLOW_EVENT: 'HCI_DATA_BUFFER_OVERFLOW_EVENT', + HCI_MAX_SLOTS_CHANGE_EVENT: 'HCI_MAX_SLOTS_CHANGE_EVENT', + HCI_READ_CLOCK_OFFSET_COMPLETE_EVENT: 'HCI_READ_CLOCK_OFFSET_COMPLETE_EVENT', + HCI_CONNECTION_PACKET_TYPE_CHANGED_EVENT: 'HCI_CONNECTION_PACKET_TYPE_CHANGED_EVENT', + HCI_QOS_VIOLATION_EVENT: 'HCI_QOS_VIOLATION_EVENT', + HCI_PAGE_SCAN_REPETITION_MODE_CHANGE_EVENT: 'HCI_PAGE_SCAN_REPETITION_MODE_CHANGE_EVENT', + HCI_FLOW_SPECIFICATION_COMPLETE_EVENT: 'HCI_FLOW_SPECIFICATION_COMPLETE_EVENT', + HCI_READ_REMOTE_EXTENDED_FEATURES_COMPLETE_EVENT: 'HCI_READ_REMOTE_EXTENDED_FEATURES_COMPLETE_EVENT', + HCI_EXTENDED_INQUIRY_RESULT_EVENT: 'HCI_EXTENDED_INQUIRY_RESULT_EVENT', + HCI_ENCRYPTION_KEY_REFRESH_COMPLETE_EVENT: 'HCI_ENCRYPTION_KEY_REFRESH_COMPLETE_EVENT', + HCI_IO_CAPABILITY_REQUEST_EVENT: 'HCI_IO_CAPABILITY_REQUEST_EVENT', + HCI_IO_CAPABILITY_RESPONSE_EVENT: 'HCI_IO_CAPABILITY_RESPONSE_EVENT', + HCI_USER_CONFIRMATION_REQUEST_EVENT: 'HCI_USER_CONFIRMATION_REQUEST_EVENT', + HCI_USER_PASSKEY_REQUEST_EVENT: 'HCI_USER_PASSKEY_REQUEST_EVENT', + HCI_REMOTE_OOB_DATA_REQUEST: 'HCI_REMOTE_OOB_DATA_REQUEST', + HCI_SIMPLE_PAIRING_COMPLETE_EVENT: 'HCI_SIMPLE_PAIRING_COMPLETE_EVENT', + HCI_LINK_SUPERVISION_TIMEOUT_CHANGED_EVENT: 'HCI_LINK_SUPERVISION_TIMEOUT_CHANGED_EVENT', + HCI_ENHANCED_FLUSH_COMPLETE_EVENT: 'HCI_ENHANCED_FLUSH_COMPLETE_EVENT', + HCI_USER_PASSKEY_NOTIFICATION_EVENT: 'HCI_USER_PASSKEY_NOTIFICATION_EVENT', + HCI_KEYPRESS_NOTIFICATION_EVENT: 'HCI_KEYPRESS_NOTIFICATION_EVENT', + HCI_REMOTE_HOST_SUPPORTED_FEATURES_NOTIFICATION_EVENT: 'HCI_REMOTE_HOST_SUPPORTED_FEATURES_NOTIFICATION_EVENT', + HCI_LE_META_EVENT: 'HCI_LE_META_EVENT' +} + +# HCI Subevent Codes +HCI_LE_CONNECTION_COMPLETE_EVENT = 0x01 +HCI_LE_ADVERTISING_REPORT_EVENT = 0x02 +HCI_LE_CONNECTION_UPDATE_COMPLETE_EVENT = 0x03 +HCI_LE_READ_REMOTE_FEATURES_COMPLETE_EVENT = 0x04 +HCI_LE_LONG_TERM_KEY_REQUEST_EVENT = 0x05 +HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_EVENT = 0x06 +HCI_LE_DATA_LENGTH_CHANGE_EVENT = 0x07 +HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMPLETE_EVENT = 0x08 +HCI_LE_GENERATE_DHKEY_COMPLETE_EVENT = 0x09 +HCI_LE_ENHANCED_CONNECTION_COMPLETE_EVENT = 0x0A +HCI_LE_DIRECTED_ADVERTISING_REPORT_EVENT = 0x0B +HCI_LE_PHY_UPDATE_COMPLETE_EVENT = 0x0C +HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT = 0x0D +HCI_LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHED_EVENT = 0x0E +HCI_LE_PERIODIC_ADVERTISING_REPORT_EVENT = 0x0F +HCI_LE_PERIODIC_ADVERTISING_SYNC_LOST_EVENT = 0x10 +HCI_LE_SCAN_TIMEOUT_EVENT = 0x11 +HCI_LE_ADVERTISING_SET_TERMINATED_EVENT = 0x12 +HCI_LE_SCAN_REQUEST_RECEIVED_EVENT = 0x13 +HCI_LE_CHANNEL_SELECTION_ALGORITHM_EVENT = 0x14 + +HCI_SUBEVENT_NAMES = { + HCI_LE_CONNECTION_COMPLETE_EVENT: 'HCI_LE_CONNECTION_COMPLETE_EVENT', + HCI_LE_ADVERTISING_REPORT_EVENT: 'HCI_LE_ADVERTISING_REPORT_EVENT', + HCI_LE_CONNECTION_UPDATE_COMPLETE_EVENT: 'HCI_LE_CONNECTION_UPDATE_COMPLETE_EVENT', + HCI_LE_READ_REMOTE_FEATURES_COMPLETE_EVENT: 'HCI_LE_READ_REMOTE_FEATURES_COMPLETE_EVENT', + HCI_LE_LONG_TERM_KEY_REQUEST_EVENT: 'HCI_LE_LONG_TERM_KEY_REQUEST_EVENT', + HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_EVENT: 'HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_EVENT', + HCI_LE_DATA_LENGTH_CHANGE_EVENT: 'HCI_LE_DATA_LENGTH_CHANGE_EVENT', + HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMPLETE_EVENT: 'HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMPLETE_EVENT', + HCI_LE_GENERATE_DHKEY_COMPLETE_EVENT: 'HCI_LE_GENERATE_DHKEY_COMPLETE_EVENT', + HCI_LE_ENHANCED_CONNECTION_COMPLETE_EVENT: 'HCI_LE_ENHANCED_CONNECTION_COMPLETE_EVENT', + HCI_LE_DIRECTED_ADVERTISING_REPORT_EVENT: 'HCI_LE_DIRECTED_ADVERTISING_REPORT_EVENT', + HCI_LE_PHY_UPDATE_COMPLETE_EVENT: 'HCI_LE_PHY_UPDATE_COMPLETE_EVENT', + HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT: 'HCI_LE_EXTENDED_ADVERTISING_REPORT_EVENT', + HCI_LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHED_EVENT: 'HCI_LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHED_EVENT', + HCI_LE_PERIODIC_ADVERTISING_REPORT_EVENT: 'HCI_LE_PERIODIC_ADVERTISING_REPORT_EVENT', + HCI_LE_PERIODIC_ADVERTISING_SYNC_LOST_EVENT: 'HCI_LE_PERIODIC_ADVERTISING_SYNC_LOST_EVENT', + HCI_LE_SCAN_TIMEOUT_EVENT: 'HCI_LE_SCAN_TIMEOUT_EVENT', + HCI_LE_ADVERTISING_SET_TERMINATED_EVENT: 'HCI_LE_ADVERTISING_SET_TERMINATED_EVENT', + HCI_LE_SCAN_REQUEST_RECEIVED_EVENT: 'HCI_LE_SCAN_REQUEST_RECEIVED_EVENT', + HCI_LE_CHANNEL_SELECTION_ALGORITHM_EVENT: 'HCI_LE_CHANNEL_SELECTION_ALGORITHM_EVENT' +} + +# HCI Command +HCI_INQUIRY_COMMAND = hci_command_op_code(0x01, 0x0001) +HCI_INQUIRY_CANCEL_COMMAND = hci_command_op_code(0x01, 0x0002) +HCI_CREATE_CONNECTION_COMMAND = hci_command_op_code(0x01, 0x0005) +HCI_DISCONNECT_COMMAND = hci_command_op_code(0x01, 0x0006) +HCI_ACCEPT_CONNECTION_REQUEST_COMMAND = hci_command_op_code(0x01, 0x0009) +HCI_LINK_KEY_REQUEST_REPLY_COMMAND = hci_command_op_code(0x01, 0x000B) +HCI_LINK_KEY_REQUEST_NEGATIVE_REPLY_COMMAND = hci_command_op_code(0x01, 0x000C) +HCI_PIN_CODE_REQUEST_NEGATIVE_REPLY_COMMAND = hci_command_op_code(0x01, 0x000E) +HCI_CHANGE_CONNECTION_PACKET_TYPE_COMMAND = hci_command_op_code(0x01, 0x000F) +HCI_AUTHENTICATION_REQUESTED_COMMAND = hci_command_op_code(0x01, 0x0011) +HCI_SET_CONNECTION_ENCRYPTION_COMMAND = hci_command_op_code(0x01, 0x0013) +HCI_REMOTE_NAME_REQUEST_COMMAND = hci_command_op_code(0x01, 0x0019) +HCI_READ_REMOTE_SUPPORTED_FEATURES_COMMAND = hci_command_op_code(0x01, 0x001B) +HCI_READ_REMOTE_EXTENDED_FEATURES_COMMAND = hci_command_op_code(0x01, 0x001C) +HCI_READ_REMOTE_VERSION_INFORMATION_COMMAND = hci_command_op_code(0x01, 0x001D) +HCI_READ_CLOCK_OFFSET_COMMAND = hci_command_op_code(0x01, 0x001F) +HCI_IO_CAPABILITY_REQUEST_REPLY_COMMAND = hci_command_op_code(0x01, 0x002B) +HCI_USER_CONFIRMATION_REQUEST_REPLY_COMMAND = hci_command_op_code(0x01, 0x002C) +HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND = hci_command_op_code(0x01, 0x003D) +HCI_SNIFF_MODE_COMMAND = hci_command_op_code(0x02, 0x0003) +HCI_EXIT_SNIFF_MODE_COMMAND = hci_command_op_code(0x02, 0x0004) +HCI_SWITCH_ROLE_COMMAND = hci_command_op_code(0x02, 0x000B) +HCI_WRITE_LINK_POLICY_SETTINGS_COMMAND = hci_command_op_code(0x02, 0x000D) +HCI_WRITE_DEFAULT_LINK_POLICY_SETTINGS_COMMAND = hci_command_op_code(0x02, 0x000F) +HCI_SNIFF_SUBRATING_COMMAND = hci_command_op_code(0x02, 0x0011) +HCI_SET_EVENT_MASK_COMMAND = hci_command_op_code(0x03, 0x0001) +HCI_RESET_COMMAND = hci_command_op_code(0x03, 0x0003) +HCI_SET_EVENT_FILTER_COMMAND = hci_command_op_code(0x03, 0x0005) +HCI_READ_STORED_LINK_KEY_COMMAND = hci_command_op_code(0x03, 0x000D) +HCI_DELETE_STORED_LINK_KEY_COMMAND = hci_command_op_code(0x03, 0x0012) +HCI_WRITE_LOCAL_NAME_COMMAND = hci_command_op_code(0x03, 0x0013) +HCI_READ_LOCAL_NAME_COMMAND = hci_command_op_code(0x03, 0x0014) +HCI_WRITE_CONNECTION_ACCEPT_TIMEOUT_COMMAND = hci_command_op_code(0x03, 0x0016) +HCI_WRITE_PAGE_TIMEOUT_COMMAND = hci_command_op_code(0x03, 0x0018) +HCI_WRITE_SCAN_ENABLE_COMMAND = hci_command_op_code(0x03, 0x001A) +HCI_READ_PAGE_SCAN_ACTIVITY_COMMAND = hci_command_op_code(0x03, 0x001B) +HCI_WRITE_PAGE_SCAN_ACTIVITY_COMMAND = hci_command_op_code(0x03, 0x001C) +HCI_WRITE_INQUIRY_SCAN_ACTIVITY_COMMAND = hci_command_op_code(0x03, 0x001E) +HCI_READ_CLASS_OF_DEVICE_COMMAND = hci_command_op_code(0x03, 0x0023) +HCI_WRITE_CLASS_OF_DEVICE_COMMAND = hci_command_op_code(0x03, 0x0024) +HCI_READ_VOICE_SETTING_COMMAND = hci_command_op_code(0x03, 0x0025) +HCI_WRITE_VOICE_SETTING_COMMAND = hci_command_op_code(0x03, 0x0026) +HCI_READ_SYNCHRONOUS_FLOW_CONTROL_ENABLE_COMMAND = hci_command_op_code(0x03, 0x002E) +HCI_WRITE_SYNCHRONOUS_FLOW_CONTROL_ENABLE_COMMAND = hci_command_op_code(0x03, 0x002F) +HCI_HOST_BUFFER_SIZE_COMMAND = hci_command_op_code(0x03, 0x0033) +HCI_WRITE_LINK_SUPERVISION_TIMEOUT_COMMAND = hci_command_op_code(0x03, 0x0037) +HCI_READ_NUMBER_OF_SUPPORTED_IAC_COMMAND = hci_command_op_code(0x03, 0x0038) +HCI_READ_CURRENT_IAC_LAP_COMMAND = hci_command_op_code(0x03, 0x0039) +HCI_WRITE_INQUIRY_SCAN_TYPE_COMMAND = hci_command_op_code(0x03, 0x0043) +HCI_WRITE_INQUIRY_MODE_COMMAND = hci_command_op_code(0x03, 0x0045) +HCI_READ_PAGE_SCAN_TYPE_COMMAND = hci_command_op_code(0x03, 0x0046) +HCI_WRITE_PAGE_SCAN_TYPE_COMMAND = hci_command_op_code(0x03, 0x0047) +HCI_WRITE_EXTENDED_INQUIRY_RESPONSE_COMMAND = hci_command_op_code(0x03, 0x0052) +HCI_WRITE_SIMPLE_PAIRING_MODE_COMMAND = hci_command_op_code(0x03, 0x0056) +HCI_READ_INQUIRY_RESPONSE_TRANSMIT_POWER_LEVEL_COMMAND = hci_command_op_code(0x03, 0x0058) +HCI_SET_EVENT_MASK_PAGE_2_COMMAND = hci_command_op_code(0x03, 0x0063) +HCI_READ_DEFAULT_ERRONEOUS_DATA_REPORTING_COMMAND = hci_command_op_code(0x03, 0x005A) +HCI_READ_LE_HOST_SUPPORT_COMMAND = hci_command_op_code(0x03, 0x006C) +HCI_WRITE_LE_HOST_SUPPORT_COMMAND = hci_command_op_code(0x03, 0x006D) +HCI_WRITE_SECURE_CONNECTIONS_HOST_SUPPORT_COMMAND = hci_command_op_code(0x03, 0x007A) +HCI_WRITE_AUTHENTICATED_PAYLOAD_TIMEOUT_COMMAND = hci_command_op_code(0x03, 0x007C) +HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND = hci_command_op_code(0x04, 0x0001) +HCI_READ_LOCAL_SUPPORTED_COMMANDS_COMMAND = hci_command_op_code(0x04, 0x0002) +HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND = hci_command_op_code(0x04, 0x0003) +HCI_READ_LOCAL_EXTENDED_FEATURES_COMMAND = hci_command_op_code(0x04, 0x0004) +HCI_READ_BUFFER_SIZE_COMMAND = hci_command_op_code(0x04, 0x0005) +HCI_READ_BD_ADDR_COMMAND = hci_command_op_code(0x04, 0x0009) +HCI_READ_LOCAL_SUPPORTED_CODECS_COMMAND = hci_command_op_code(0x04, 0x000B) +HCI_READ_ENCRYPTION_KEY_SIZE_COMMAND = hci_command_op_code(0x05, 0x0008) +HCI_LE_SET_EVENT_MASK_COMMAND = hci_command_op_code(0x08, 0x0001) +HCI_LE_READ_BUFFER_SIZE_COMMAND = hci_command_op_code(0x08, 0x0002) +HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND = hci_command_op_code(0x08, 0x0003) +HCI_LE_SET_RANDOM_ADDRESS_COMMAND = hci_command_op_code(0x08, 0x0005) +HCI_LE_SET_ADVERTISING_PARAMETERS_COMMAND = hci_command_op_code(0x08, 0x0006) +HCI_LE_READ_ADVERTISING_CHANNEL_TX_POWER_COMMAND = hci_command_op_code(0x08, 0x0007) +HCI_LE_SET_ADVERTISING_DATA_COMMAND = hci_command_op_code(0x08, 0x0008) +HCI_LE_SET_SCAN_RESPONSE_DATA_COMMAND = hci_command_op_code(0x08, 0x0009) +HCI_LE_SET_ADVERTISING_ENABLE_COMMAND = hci_command_op_code(0x08, 0x000A) +HCI_LE_SET_SCAN_PARAMETERS_COMMAND = hci_command_op_code(0x08, 0x000B) +HCI_LE_SET_SCAN_ENABLE_COMMAND = hci_command_op_code(0x08, 0x000C) +HCI_LE_CREATE_CONNECTION_COMMAND = hci_command_op_code(0x08, 0x000D) +HCI_LE_CREATE_CONNECTION_CANCEL_COMMAND = hci_command_op_code(0x08, 0x000E) +HCI_LE_READ_WHITE_LIST_SIZE_COMMAND = hci_command_op_code(0x08, 0x000F) +HCI_LE_CLEAR_WHITE_LIST_COMMAND = hci_command_op_code(0x08, 0x0010) +HCI_LE_ADD_DEVICE_TO_WHITE_LIST_COMMAND = hci_command_op_code(0x08, 0x0011) +HCI_LE_REMOVE_DEVICE_FROM_WHITE_LIST_COMMAND = hci_command_op_code(0x08, 0x0012) +HCI_LE_CONNECTION_UPDATE_COMMAND = hci_command_op_code(0x08, 0x0013) +HCI_LE_SET_HOST_CHANNEL_CLASSIFICATION_COMMAND = hci_command_op_code(0x08, 0x0014) +HCI_LE_READ_CHANNEL_MAP_COMMAND = hci_command_op_code(0x08, 0x0015) +HCI_LE_READ_REMOTE_FEATURES_COMMAND = hci_command_op_code(0x08, 0x0016) +HCI_LE_ENCRYPT_COMMAND = hci_command_op_code(0x08, 0x0017) +HCI_LE_RAND_COMMAND = hci_command_op_code(0x08, 0x0018) +HCI_LE_START_ENCRYPTION_COMMAND = hci_command_op_code(0x08, 0x0019) +HCI_LE_LONG_TERM_KEY_REQUEST_REPLY_COMMAND = hci_command_op_code(0x08, 0x001A) +HCI_LE_LONG_TERM_KEY_REQUEST_NEGATIVE_REPLY_COMMAND = hci_command_op_code(0x08, 0x001B) +HCI_LE_READ_SUPPORTED_STATES_COMMAND = hci_command_op_code(0x08, 0x001C) +HCI_LE_RECEIVER_TEST_COMMAND = hci_command_op_code(0x08, 0x001D) +HCI_LE_TRANSMITTER_TEST_COMMAND = hci_command_op_code(0x08, 0x001E) +HCI_LE_TEST_END_COMMAND = hci_command_op_code(0x08, 0x001F) +HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_REPLY_COMMAND = hci_command_op_code(0x08, 0x0020) +HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_NEGATIVE_REPLY_COMMAND = hci_command_op_code(0x08, 0x0021) +HCI_LE_SET_DATA_LENGTH_COMMAND = hci_command_op_code(0x08, 0x0022) +HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND = hci_command_op_code(0x08, 0x0023) +HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND = hci_command_op_code(0x08, 0x0024) +HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMMAND = hci_command_op_code(0x08, 0x0025) +HCI_LE_GENERATE_DHKEY_COMMAND = hci_command_op_code(0x08, 0x0026) +HCI_LE_ADD_DEVICE_TO_RESOLVING_LIST_COMMAND = hci_command_op_code(0x08, 0x0027) +HCI_LE_REMOVE_DEVICE_FROM_RESOLVING_LIST_COMMAND = hci_command_op_code(0x08, 0x0028) +HCI_LE_CLEAR_RESOLVING_LIST_COMMAND = hci_command_op_code(0x08, 0x0029) +HCI_LE_READ_RESOLVING_LIST_SIZE_COMMAND = hci_command_op_code(0x08, 0x002A) +HCI_LE_READ_PEER_RESOLVABLE_ADDRESS_COMMAND = hci_command_op_code(0x08, 0x002B) +HCI_LE_READ_LOCAL_RESOLVABLE_ADDRESS_COMMAND = hci_command_op_code(0x08, 0x002C) +HCI_LE_SET_ADDRESS_RESOLUTION_ENABLE_COMMAND = hci_command_op_code(0x08, 0x002D) +HCI_LE_SET_RESOLVABLE_PRIVATE_ADDRESS_TIMEOUT_COMMAND = hci_command_op_code(0x08, 0x002E) +HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND = hci_command_op_code(0x08, 0x002F) +HCI_LE_READ_PHY_COMMAND = hci_command_op_code(0x08, 0x0030) +HCI_LE_SET_DEFAULT_PHY_COMMAND = hci_command_op_code(0x08, 0x0031) +HCI_LE_SET_PHY_COMMAND = hci_command_op_code(0x08, 0x0032) +HCI_LE_ENHANCED_RECEIVER_TEST_COMMAND = hci_command_op_code(0x08, 0x0033) +HCI_LE_ENHANCED_TRANSMITTER_TEST_COMMAND = hci_command_op_code(0x08, 0x0034) +HCI_LE_SET_ADVERTISING_SET_RANDOM_ADDRESS_COMMAND = hci_command_op_code(0x08, 0x0035) +HCI_LE_SET_EXTENDED_ADVERTISING_PARAMETERS_COMMAND = hci_command_op_code(0x08, 0x0036) +HCI_LE_SET_EXTENDED_ADVERTISING_DATA_COMMAND = hci_command_op_code(0x08, 0x0037) +HCI_LE_SET_EXTENDED_SCAN_RESPONSE_DATA_COMMAND = hci_command_op_code(0x08, 0x0038) +HCI_LE_SET_EXTENDED_ADVERTISING_ENABLE_COMMAND = hci_command_op_code(0x08, 0x0039) +HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND = hci_command_op_code(0x08, 0x003A) +HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERETISING_SETS_COMMAND = hci_command_op_code(0x08, 0x003B) +HCI_LE_REMOVE_ADVERTISING_SET_COMMAND = hci_command_op_code(0x08, 0x003C) +HCI_LE_CLEAR_ADVERTISING_SETS_COMMAND = hci_command_op_code(0x08, 0x003D) +HCI_LE_SET_PERIODIC_ADVERTISING_PARAMETERS_COMMAND = hci_command_op_code(0x08, 0x003E) +HCI_LE_SET_PERIODIC_ADVERTISING_DATA_COMMAND = hci_command_op_code(0x08, 0x003F) +HCI_LE_SET_PERIODIC_ADVERTISING_ENABLE_COMMAND = hci_command_op_code(0x08, 0x0040) +HCI_LE_SET_EXTENDED_SCAN_PARAMETERS_COMMAND = hci_command_op_code(0x08, 0x0041) +HCI_LE_SET_EXTENDED_SCAN_ENABLE_COMMAND = hci_command_op_code(0x08, 0x0042) +HCI_LE_SET_EXTENDED_CREATE_CONNECTION_COMMAND = hci_command_op_code(0x08, 0x0043) +HCI_LE_PERIODIC_ADVERTISING_CREATE_SYNC_COMMAND = hci_command_op_code(0x08, 0x0044) +HCI_LE_PERIODIC_ADVERTISING_CREATE_SYNC_CANCEL_COMMAND = hci_command_op_code(0x08, 0x0045) +HCI_LE_PERIODIC_ADVERTISING_TERMINATE_SYNC_COMMAND = hci_command_op_code(0x08, 0x0046) +HCI_LE_ADD_DEVICE_TO_PERIODIC_ADVERTISER_LIST_COMMAND = hci_command_op_code(0x08, 0x0047) +HCI_LE_REMOVE_DEVICE_FROM_PERIODIC_ADVERTISER_LIST_COMMAND = hci_command_op_code(0x08, 0x0048) +HCI_LE_CLEAR_PERIODIC_ADVERTISER_LIST_COMMAND = hci_command_op_code(0x08, 0x0049) +HCI_LE_READ_PERIODIC_ADVERTISER_LIST_SIZE_COMMAND = hci_command_op_code(0x08, 0x004A) +HCI_LE_READ_TRANSMIT_POWER_COMMAND = hci_command_op_code(0x08, 0x004B) +HCI_LE_READ_RF_PATH_COMPENSATION_COMMAND = hci_command_op_code(0x08, 0x004C) +HCI_LE_WRITE_RF_PATH_COMPENSATION_COMMAND = hci_command_op_code(0x08, 0x004D) +HCI_LE_SET_PRIVACY_MODE_COMMAND = hci_command_op_code(0x08, 0x004E) + + +HCI_COMMAND_NAMES = { + HCI_INQUIRY_COMMAND: 'HCI_INQUIRY_COMMAND', + HCI_INQUIRY_CANCEL_COMMAND: 'HCI_INQUIRY_CANCEL_COMMAND', + HCI_CREATE_CONNECTION_COMMAND: 'HCI_CREATE_CONNECTION_COMMAND', + HCI_DISCONNECT_COMMAND: 'HCI_DISCONNECT_COMMAND', + HCI_ACCEPT_CONNECTION_REQUEST_COMMAND: 'HCI_ACCEPT_CONNECTION_REQUEST_COMMAND', + HCI_LINK_KEY_REQUEST_REPLY_COMMAND: 'HCI_LINK_KEY_REQUEST_REPLY_COMMAND', + HCI_LINK_KEY_REQUEST_NEGATIVE_REPLY_COMMAND: 'HCI_LINK_KEY_REQUEST_NEGATIVE_REPLY_COMMAND', + HCI_PIN_CODE_REQUEST_NEGATIVE_REPLY_COMMAND: 'HCI_PIN_CODE_REQUEST_NEGATIVE_REPLY_COMMAND', + HCI_CHANGE_CONNECTION_PACKET_TYPE_COMMAND: 'HCI_CHANGE_CONNECTION_PACKET_TYPE_COMMAND', + HCI_AUTHENTICATION_REQUESTED_COMMAND: 'HCI_AUTHENTICATION_REQUESTED_COMMAND', + HCI_SET_CONNECTION_ENCRYPTION_COMMAND: 'HCI_SET_CONNECTION_ENCRYPTION_COMMAND', + HCI_REMOTE_NAME_REQUEST_COMMAND: 'HCI_REMOTE_NAME_REQUEST_COMMAND', + HCI_READ_REMOTE_SUPPORTED_FEATURES_COMMAND: 'HCI_READ_REMOTE_SUPPORTED_FEATURES_COMMAND', + HCI_READ_REMOTE_EXTENDED_FEATURES_COMMAND: 'HCI_READ_REMOTE_EXTENDED_FEATURES_COMMAND', + HCI_READ_REMOTE_VERSION_INFORMATION_COMMAND: 'HCI_READ_REMOTE_VERSION_INFORMATION_COMMAND', + HCI_READ_CLOCK_OFFSET_COMMAND: 'HCI_READ_CLOCK_OFFSET_COMMAND', + HCI_IO_CAPABILITY_REQUEST_REPLY_COMMAND: 'HCI_IO_CAPABILITY_REQUEST_REPLY_COMMAND', + HCI_USER_CONFIRMATION_REQUEST_REPLY_COMMAND: 'HCI_USER_CONFIRMATION_REQUEST_REPLY_COMMAND', + HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND: 'HCI_ENHANCED_SETUP_SYNCHRONOUS_CONNECTION_COMMAND', + HCI_SNIFF_MODE_COMMAND: 'HCI_SNIFF_MODE_COMMAND', + HCI_EXIT_SNIFF_MODE_COMMAND: 'HCI_EXIT_SNIFF_MODE_COMMAND', + HCI_SWITCH_ROLE_COMMAND: 'HCI_SWITCH_ROLE_COMMAND', + HCI_WRITE_LINK_POLICY_SETTINGS_COMMAND: 'HCI_WRITE_LINK_POLICY_SETTINGS_COMMAND', + HCI_WRITE_DEFAULT_LINK_POLICY_SETTINGS_COMMAND: 'HCI_WRITE_DEFAULT_LINK_POLICY_SETTINGS_COMMAND', + HCI_SNIFF_SUBRATING_COMMAND: 'HCI_SNIFF_SUBRATING_COMMAND', + HCI_SET_EVENT_MASK_COMMAND: 'HCI_SET_EVENT_MASK_COMMAND', + HCI_RESET_COMMAND: 'HCI_RESET_COMMAND', + HCI_SET_EVENT_FILTER_COMMAND: 'HCI_SET_EVENT_FILTER_COMMAND', + HCI_READ_STORED_LINK_KEY_COMMAND: 'HCI_READ_STORED_LINK_KEY_COMMAND', + HCI_DELETE_STORED_LINK_KEY_COMMAND: 'HCI_DELETE_STORED_LINK_KEY_COMMAND', + HCI_WRITE_LOCAL_NAME_COMMAND: 'HCI_WRITE_LOCAL_NAME_COMMAND', + HCI_READ_LOCAL_NAME_COMMAND: 'HCI_READ_LOCAL_NAME_COMMAND', + HCI_WRITE_CONNECTION_ACCEPT_TIMEOUT_COMMAND: 'HCI_WRITE_CONNECTION_ACCEPT_TIMEOUT_COMMAND', + HCI_WRITE_PAGE_TIMEOUT_COMMAND: 'HCI_WRITE_PAGE_TIMEOUT_COMMAND', + HCI_WRITE_SCAN_ENABLE_COMMAND: 'HCI_WRITE_SCAN_ENABLE_COMMAND', + HCI_READ_PAGE_SCAN_ACTIVITY_COMMAND: 'HCI_READ_PAGE_SCAN_ACTIVITY_COMMAND', + HCI_WRITE_PAGE_SCAN_ACTIVITY_COMMAND: 'HCI_WRITE_PAGE_SCAN_ACTIVITY_COMMAND', + HCI_WRITE_INQUIRY_SCAN_ACTIVITY_COMMAND: 'HCI_WRITE_INQUIRY_SCAN_ACTIVITY_COMMAND', + HCI_READ_CLASS_OF_DEVICE_COMMAND: 'HCI_READ_CLASS_OF_DEVICE_COMMAND', + HCI_WRITE_CLASS_OF_DEVICE_COMMAND: 'HCI_WRITE_CLASS_OF_DEVICE_COMMAND', + HCI_READ_VOICE_SETTING_COMMAND: 'HCI_READ_VOICE_SETTING_COMMAND', + HCI_WRITE_VOICE_SETTING_COMMAND: 'HCI_WRITE_VOICE_SETTING_COMMAND', + HCI_READ_SYNCHRONOUS_FLOW_CONTROL_ENABLE_COMMAND: 'HCI_READ_SYNCHRONOUS_FLOW_CONTROL_ENABLE_COMMAND', + HCI_WRITE_SYNCHRONOUS_FLOW_CONTROL_ENABLE_COMMAND: 'HCI_WRITE_SYNCHRONOUS_FLOW_CONTROL_ENABLE_COMMAND', + HCI_HOST_BUFFER_SIZE_COMMAND: 'HCI_HOST_BUFFER_SIZE_COMMAND', + HCI_WRITE_LINK_SUPERVISION_TIMEOUT_COMMAND: 'HCI_WRITE_LINK_SUPERVISION_TIMEOUT_COMMAND', + HCI_READ_NUMBER_OF_SUPPORTED_IAC_COMMAND: 'HCI_READ_NUMBER_OF_SUPPORTED_IAC_COMMAND', + HCI_READ_CURRENT_IAC_LAP_COMMAND: 'HCI_READ_CURRENT_IAC_LAP_COMMAND', + HCI_WRITE_INQUIRY_SCAN_TYPE_COMMAND: 'HCI_WRITE_INQUIRY_SCAN_TYPE_COMMAND', + HCI_WRITE_INQUIRY_MODE_COMMAND: 'HCI_WRITE_INQUIRY_MODE_COMMAND', + HCI_READ_PAGE_SCAN_TYPE_COMMAND: 'HCI_READ_PAGE_SCAN_TYPE_COMMAND', + HCI_WRITE_PAGE_SCAN_TYPE_COMMAND: 'HCI_WRITE_PAGE_SCAN_TYPE_COMMAND', + HCI_WRITE_EXTENDED_INQUIRY_RESPONSE_COMMAND: 'HCI_WRITE_EXTENDED_INQUIRY_RESPONSE_COMMAND', + HCI_WRITE_SIMPLE_PAIRING_MODE_COMMAND: 'HCI_WRITE_SIMPLE_PAIRING_MODE_COMMAND', + HCI_READ_INQUIRY_RESPONSE_TRANSMIT_POWER_LEVEL_COMMAND: 'HCI_READ_INQUIRY_RESPONSE_TRANSMIT_POWER_LEVEL_COMMAND', + HCI_SET_EVENT_MASK_PAGE_2_COMMAND: 'HCI_SET_EVENT_MASK_PAGE_2_COMMAND', + HCI_READ_DEFAULT_ERRONEOUS_DATA_REPORTING_COMMAND: 'HCI_READ_DEFAULT_ERRONEOUS_DATA_REPORTING_COMMAND', + HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND: 'HCI_READ_LOCAL_VERSION_INFORMATION_COMMAND', + HCI_READ_LOCAL_SUPPORTED_COMMANDS_COMMAND: 'HCI_READ_LOCAL_SUPPORTED_COMMANDS_COMMAND', + HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND: 'HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND', + HCI_READ_LOCAL_EXTENDED_FEATURES_COMMAND: 'HCI_READ_LOCAL_EXTENDED_FEATURES_COMMAND', + HCI_READ_BUFFER_SIZE_COMMAND: 'HCI_READ_BUFFER_SIZE_COMMAND', + HCI_READ_LE_HOST_SUPPORT_COMMAND: 'HCI_READ_LE_HOST_SUPPORT_COMMAND', + HCI_WRITE_LE_HOST_SUPPORT_COMMAND: 'HCI_WRITE_LE_HOST_SUPPORT_COMMAND', + HCI_WRITE_SECURE_CONNECTIONS_HOST_SUPPORT_COMMAND: 'HCI_WRITE_SECURE_CONNECTIONS_HOST_SUPPORT_COMMAND', + HCI_WRITE_AUTHENTICATED_PAYLOAD_TIMEOUT_COMMAND: 'HCI_WRITE_AUTHENTICATED_PAYLOAD_TIMEOUT_COMMAND', + HCI_READ_BD_ADDR_COMMAND: 'HCI_READ_BD_ADDR_COMMAND', + HCI_READ_LOCAL_SUPPORTED_CODECS_COMMAND: 'HCI_READ_LOCAL_SUPPORTED_CODECS_COMMAND', + HCI_READ_ENCRYPTION_KEY_SIZE_COMMAND: 'HCI_READ_ENCRYPTION_KEY_SIZE_COMMAND', + HCI_LE_SET_EVENT_MASK_COMMAND: 'HCI_LE_SET_EVENT_MASK_COMMAND', + HCI_LE_READ_BUFFER_SIZE_COMMAND: 'HCI_LE_READ_BUFFER_SIZE_COMMAND', + HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND: 'HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND', + HCI_LE_SET_RANDOM_ADDRESS_COMMAND: 'HCI_LE_SET_RANDOM_ADDRESS_COMMAND', + HCI_LE_SET_ADVERTISING_PARAMETERS_COMMAND: 'HCI_LE_SET_ADVERTISING_PARAMETERS_COMMAND', + HCI_LE_READ_ADVERTISING_CHANNEL_TX_POWER_COMMAND: 'HCI_LE_READ_ADVERTISING_CHANNEL_TX_POWER_COMMAND', + HCI_LE_SET_ADVERTISING_DATA_COMMAND: 'HCI_LE_SET_ADVERTISING_DATA_COMMAND', + HCI_LE_SET_SCAN_RESPONSE_DATA_COMMAND: 'HCI_LE_SET_SCAN_RESPONSE_DATA_COMMAND', + HCI_LE_SET_ADVERTISING_ENABLE_COMMAND: 'HCI_LE_SET_ADVERTISING_ENABLE_COMMAND', + HCI_LE_SET_SCAN_PARAMETERS_COMMAND: 'HCI_LE_SET_SCAN_PARAMETERS_COMMAND', + HCI_LE_SET_SCAN_ENABLE_COMMAND: 'HCI_LE_SET_SCAN_ENABLE_COMMAND', + HCI_LE_CREATE_CONNECTION_COMMAND: 'HCI_LE_CREATE_CONNECTION_COMMAND', + HCI_LE_CREATE_CONNECTION_CANCEL_COMMAND: 'HCI_LE_CREATE_CONNECTION_CANCEL_COMMAND', + HCI_LE_READ_WHITE_LIST_SIZE_COMMAND: 'HCI_LE_READ_WHITE_LIST_SIZE_COMMAND', + HCI_LE_CLEAR_WHITE_LIST_COMMAND: 'HCI_LE_CLEAR_WHITE_LIST_COMMAND', + HCI_LE_ADD_DEVICE_TO_WHITE_LIST_COMMAND: 'HCI_LE_ADD_DEVICE_TO_WHITE_LIST_COMMAND', + HCI_LE_REMOVE_DEVICE_FROM_WHITE_LIST_COMMAND: 'HCI_LE_REMOVE_DEVICE_FROM_WHITE_LIST_COMMAND', + HCI_LE_CONNECTION_UPDATE_COMMAND: 'HCI_LE_CONNECTION_UPDATE_COMMAND', + HCI_LE_SET_HOST_CHANNEL_CLASSIFICATION_COMMAND: 'HCI_LE_SET_HOST_CHANNEL_CLASSIFICATION_COMMAND', + HCI_LE_READ_CHANNEL_MAP_COMMAND: 'HCI_LE_READ_CHANNEL_MAP_COMMAND', + HCI_LE_READ_REMOTE_FEATURES_COMMAND: 'HCI_LE_READ_REMOTE_FEATURES_COMMAND', + HCI_LE_ENCRYPT_COMMAND: 'HCI_LE_ENCRYPT_COMMAND', + HCI_LE_RAND_COMMAND: 'HCI_LE_RAND_COMMAND', + HCI_LE_START_ENCRYPTION_COMMAND: 'HCI_LE_START_ENCRYPTION_COMMAND', + HCI_LE_LONG_TERM_KEY_REQUEST_REPLY_COMMAND: 'HCI_LE_LONG_TERM_KEY_REQUEST_REPLY_COMMAND', + HCI_LE_LONG_TERM_KEY_REQUEST_NEGATIVE_REPLY_COMMAND: 'HCI_LE_LONG_TERM_KEY_REQUEST_NEGATIVE_REPLY_COMMAND', + HCI_LE_READ_SUPPORTED_STATES_COMMAND: 'HCI_LE_READ_SUPPORTED_STATES_COMMAND', + HCI_LE_RECEIVER_TEST_COMMAND: 'HCI_LE_RECEIVER_TEST_COMMAND', + HCI_LE_TRANSMITTER_TEST_COMMAND: 'HCI_LE_TRANSMITTER_TEST_COMMAND', + HCI_LE_TEST_END_COMMAND: 'HCI_LE_TEST_END_COMMAND', + HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_REPLY_COMMAND: 'HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_REPLY_COMMAND', + HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_NEGATIVE_REPLY_COMMAND: 'HCI_LE_REMOTE_CONNECTION_PARAMETER_REQUEST_NEGATIVE_REPLY_COMMAND', + HCI_LE_SET_DATA_LENGTH_COMMAND: 'HCI_LE_SET_DATA_LENGTH_COMMAND', + HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND: 'HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND', + HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND: 'HCI_LE_WRITE_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND', + HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMMAND: 'HCI_LE_READ_LOCAL_P_256_PUBLIC_KEY_COMMAND', + HCI_LE_GENERATE_DHKEY_COMMAND: 'HCI_LE_GENERATE_DHKEY_COMMAND', + HCI_LE_ADD_DEVICE_TO_RESOLVING_LIST_COMMAND: 'HCI_LE_ADD_DEVICE_TO_RESOLVING_LIST_COMMAND', + HCI_LE_REMOVE_DEVICE_FROM_RESOLVING_LIST_COMMAND: 'HCI_LE_REMOVE_DEVICE_FROM_RESOLVING_LIST_COMMAND', + HCI_LE_CLEAR_RESOLVING_LIST_COMMAND: 'HCI_LE_CLEAR_RESOLVING_LIST_COMMAND', + HCI_LE_READ_RESOLVING_LIST_SIZE_COMMAND: 'HCI_LE_READ_RESOLVING_LIST_SIZE_COMMAND', + HCI_LE_READ_PEER_RESOLVABLE_ADDRESS_COMMAND: 'HCI_LE_READ_PEER_RESOLVABLE_ADDRESS_COMMAND', + HCI_LE_READ_LOCAL_RESOLVABLE_ADDRESS_COMMAND: 'HCI_LE_READ_LOCAL_RESOLVABLE_ADDRESS_COMMAND', + HCI_LE_SET_ADDRESS_RESOLUTION_ENABLE_COMMAND: 'HCI_LE_SET_ADDRESS_RESOLUTION_ENABLE_COMMAND', + HCI_LE_SET_RESOLVABLE_PRIVATE_ADDRESS_TIMEOUT_COMMAND: 'HCI_LE_SET_RESOLVABLE_PRIVATE_ADDRESS_TIMEOUT_COMMAND', + HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND: 'HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND', + HCI_LE_READ_PHY_COMMAND: 'HCI_LE_READ_PHY_COMMAND', + HCI_LE_SET_DEFAULT_PHY_COMMAND: 'HCI_LE_SET_DEFAULT_PHY_COMMAND', + HCI_LE_SET_PHY_COMMAND: 'HCI_LE_SET_PHY_COMMAND', + HCI_LE_ENHANCED_RECEIVER_TEST_COMMAND: 'HCI_LE_ENHANCED_RECEIVER_TEST_COMMAND', + HCI_LE_ENHANCED_TRANSMITTER_TEST_COMMAND: 'HCI_LE_ENHANCED_TRANSMITTER_TEST_COMMAND', + HCI_LE_SET_ADVERTISING_SET_RANDOM_ADDRESS_COMMAND: 'HCI_LE_SET_ADVERTISING_SET_RANDOM_ADDRESS_COMMAND', + HCI_LE_SET_EXTENDED_ADVERTISING_PARAMETERS_COMMAND: 'HCI_LE_SET_EXTENDED_ADVERTISING_PARAMETERS_COMMAND', + HCI_LE_SET_EXTENDED_ADVERTISING_DATA_COMMAND: 'HCI_LE_SET_EXTENDED_ADVERTISING_DATA_COMMAND', + HCI_LE_SET_EXTENDED_SCAN_RESPONSE_DATA_COMMAND: 'HCI_LE_SET_EXTENDED_SCAN_RESPONSE_DATA_COMMAND', + HCI_LE_SET_EXTENDED_ADVERTISING_ENABLE_COMMAND: 'HCI_LE_SET_EXTENDED_ADVERTISING_ENABLE_COMMAND', + HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND: 'HCI_LE_READ_MAXIMUM_ADVERTISING_DATA_LENGTH_COMMAND', + HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERETISING_SETS_COMMAND: 'HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERETISING_SETS_COMMAND', + HCI_LE_REMOVE_ADVERTISING_SET_COMMAND: 'HCI_LE_REMOVE_ADVERTISING_SET_COMMAND', + HCI_LE_CLEAR_ADVERTISING_SETS_COMMAND: 'HCI_LE_CLEAR_ADVERTISING_SETS_COMMAND', + HCI_LE_SET_PERIODIC_ADVERTISING_PARAMETERS_COMMAND: 'HCI_LE_SET_PERIODIC_ADVERTISING_PARAMETERS_COMMAND', + HCI_LE_SET_PERIODIC_ADVERTISING_DATA_COMMAND: 'HCI_LE_SET_PERIODIC_ADVERTISING_DATA_COMMAND', + HCI_LE_SET_PERIODIC_ADVERTISING_ENABLE_COMMAND: 'HCI_LE_SET_PERIODIC_ADVERTISING_ENABLE_COMMAND', + HCI_LE_SET_EXTENDED_SCAN_PARAMETERS_COMMAND: 'HCI_LE_SET_EXTENDED_SCAN_PARAMETERS_COMMAND', + HCI_LE_SET_EXTENDED_SCAN_ENABLE_COMMAND: 'HCI_LE_SET_EXTENDED_SCAN_ENABLE_COMMAND', + HCI_LE_SET_EXTENDED_CREATE_CONNECTION_COMMAND: 'HCI_LE_SET_EXTENDED_CREATE_CONNECTION_COMMAND', + HCI_LE_PERIODIC_ADVERTISING_CREATE_SYNC_COMMAND: 'HCI_LE_PERIODIC_ADVERTISING_CREATE_SYNC_COMMAND', + HCI_LE_PERIODIC_ADVERTISING_CREATE_SYNC_CANCEL_COMMAND: 'HCI_LE_PERIODIC_ADVERTISING_CREATE_SYNC_CANCEL_COMMAND', + HCI_LE_PERIODIC_ADVERTISING_TERMINATE_SYNC_COMMAND: 'HCI_LE_PERIODIC_ADVERTISING_TERMINATE_SYNC_COMMAND', + HCI_LE_ADD_DEVICE_TO_PERIODIC_ADVERTISER_LIST_COMMAND: 'HCI_LE_ADD_DEVICE_TO_PERIODIC_ADVERTISER_LIST_COMMAND', + HCI_LE_REMOVE_DEVICE_FROM_PERIODIC_ADVERTISER_LIST_COMMAND: 'HCI_LE_REMOVE_DEVICE_FROM_PERIODIC_ADVERTISER_LIST_COMMAND', + HCI_LE_CLEAR_PERIODIC_ADVERTISER_LIST_COMMAND: 'HCI_LE_CLEAR_PERIODIC_ADVERTISER_LIST_COMMAND', + HCI_LE_READ_PERIODIC_ADVERTISER_LIST_SIZE_COMMAND: 'HCI_LE_READ_PERIODIC_ADVERTISER_LIST_SIZE_COMMAND', + HCI_LE_READ_TRANSMIT_POWER_COMMAND: 'HCI_LE_READ_TRANSMIT_POWER_COMMAND', + HCI_LE_READ_RF_PATH_COMPENSATION_COMMAND: 'HCI_LE_READ_RF_PATH_COMPENSATION_COMMAND', + HCI_LE_WRITE_RF_PATH_COMPENSATION_COMMAND: 'HCI_LE_WRITE_RF_PATH_COMPENSATION_COMMAND', + HCI_LE_SET_PRIVACY_MODE_COMMAND: 'HCI_LE_SET_PRIVACY_MODE_COMMAND' +} + + +# HCI Error Codes +# See Bluetooth spec Vol 2, Part D - 1.3 LIST OF ERROR CODES +HCI_SUCCESS = 0x00 +HCI_UNKNOWN_HCI_COMMAND_ERROR = 0x01 +HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR = 0x02 +HCI_HARDWARE_FAILURE_ERROR = 0x03 +HCI_PAGE_TIMEOUT_ERROR = 0x04 +HCI_AUTHENTICATION_FAILURE_ERROR = 0x05 +HCI_PIN_OR_KEY_MISSING_ERROR = 0x06 +HCI_MEMORY_CAPACITY_EXCEEDED_ERROR = 0x07 +HCI_CONNECTION_TIMEOUT_ERROR = 0x08 +HCI_CONNECTION_LIMIT_EXCEEDED_ERROR = 0x09 +HCI_SYNCHRONOUS_CONNECTION_LIMIT_TO_A_DEVICE_EXCEEDED_ERROR = 0x0A +HCI_CONNECTION_ALREADY_EXISTS_ERROR = 0x0B +HCI_COMMAND_DISALLOWED_ERROR = 0x0C +HCI_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES_ERROR = 0x0D +HCI_CONNECTION_REJECTED_DUE_TO_SECURITY_REASONS_ERROR = 0x0E +HCI_CONNECTION_REJECTED_DUE_TO_UNACCEPTABLE_BD_ADDR_ERROR = 0x0F +HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR = 0x10 +HCI_UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE_ERROR = 0x11 +HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR = 0x12 +HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR = 0x13 +HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR = 0x14 +HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_POWER_OFF_ERROR = 0x15 +HCI_CONNECTION_TERMINATED_BY_LOCAL_HOST_ERROR = 0x16 +HCI_UNACCEPTABLE_CONNECTION_PARAMETERS_ERROR = 0x3B +HCI_CONNECTION_FAILED_TO_BE_ESTABLISHED_ERROR = 0x3E +# TODO: more error codes + +HCI_ERROR_NAMES = { + HCI_SUCCESS: 'HCI_SUCCESS', + HCI_UNKNOWN_HCI_COMMAND_ERROR: 'HCI_UNKNOWN_HCI_COMMAND_ERROR', + HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR: 'HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR', + HCI_HARDWARE_FAILURE_ERROR: 'HCI_HARDWARE_FAILURE_ERROR', + HCI_PAGE_TIMEOUT_ERROR: 'HCI_PAGE_TIMEOUT_ERROR', + HCI_AUTHENTICATION_FAILURE_ERROR: 'HCI_AUTHENTICATION_FAILURE_ERROR', + HCI_PIN_OR_KEY_MISSING_ERROR: 'HCI_PIN_OR_KEY_MISSING_ERROR', + HCI_MEMORY_CAPACITY_EXCEEDED_ERROR: 'HCI_MEMORY_CAPACITY_EXCEEDED_ERROR', + HCI_CONNECTION_TIMEOUT_ERROR: 'HCI_CONNECTION_TIMEOUT_ERROR', + HCI_CONNECTION_LIMIT_EXCEEDED_ERROR: 'HCI_CONNECTION_LIMIT_EXCEEDED_ERROR', + HCI_SYNCHRONOUS_CONNECTION_LIMIT_TO_A_DEVICE_EXCEEDED_ERROR: 'HCI_SYNCHRONOUS_CONNECTION_LIMIT_TO_A_DEVICE_EXCEEDED_ERROR', + HCI_CONNECTION_ALREADY_EXISTS_ERROR: 'HCI_CONNECTION_ALREADY_EXISTS_ERROR', + HCI_COMMAND_DISALLOWED_ERROR: 'HCI_COMMAND_DISALLOWED_ERROR', + HCI_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES_ERROR: 'HCI_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES_ERROR', + HCI_CONNECTION_REJECTED_DUE_TO_SECURITY_REASONS_ERROR: 'HCI_CONNECTION_REJECTED_DUE_TO_SECURITY_REASONS_ERROR', + HCI_CONNECTION_REJECTED_DUE_TO_UNACCEPTABLE_BD_ADDR_ERROR: 'HCI_CONNECTION_REJECTED_DUE_TO_UNACCEPTABLE_BD_ADDR_ERROR', + HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR: 'HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR', + HCI_UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE_ERROR: 'HCI_UNSUPPORTED_FEATURE_OR_PARAMETER_VALUE_ERROR', + HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR: 'HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR', + HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR: 'HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR', + HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR: 'HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR', + HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_POWER_OFF_ERROR: 'HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_POWER_OFF_ERROR', + HCI_CONNECTION_TERMINATED_BY_LOCAL_HOST_ERROR: 'HCI_CONNECTION_TERMINATED_BY_LOCAL_HOST_ERROR', + HCI_UNACCEPTABLE_CONNECTION_PARAMETERS_ERROR: 'HCI_UNACCEPTABLE_CONNECTION_PARAMETERS_ERROR', + HCI_CONNECTION_FAILED_TO_BE_ESTABLISHED_ERROR: 'HCI_CONNECTION_FAILED_TO_BE_ESTABLISHED_ERROR' +} + +# Command Status codes +HCI_COMMAND_STATUS_PENDING = 0 + +# LE Event Masks +LE_CONNECTION_COMPLETE_EVENT_MASK = (1 << 0) +LE_ADVERTISING_REPORT_EVENT_MASK = (1 << 1) +LE_CONNECTION_UPDATE_COMPLETE_EVENT_MASK = (1 << 2) +LE_READ_REMOTE_FEATURES_COMPLETE_EVENT_MASK = (1 << 3) +LE_LONG_TERM_KEY_REQUEST_EVENT_MASK = (1 << 4) +LE_REMOTE_CONNECTION_PARAMETER_REQUEST_EVENT_MASK = (1 << 5) +LE_DATA_LENGTH_CHANGE_EVENT_MASK = (1 << 6) +LE_READ_LOCAL_P_256_PUBLIC_KEY_COMPLETE_EVENT_MASK = (1 << 7) +LE_GENERATE_DHKEY_COMPLETE_EVENT_MASK = (1 << 8) +LE_ENHANCED_CONNECTION_COMPLETE_EVENT_MASK = (1 << 9) +LE_DIRECTED_ADVERTISING_REPORT_EVENT_MASK = (1 << 10) +LE_PHY_UPDATE_COMPLETE_EVENT_MASK = (1 << 11) +LE_EXTENDED_ADVERTISING_REPORT_EVENT_MASK = (1 << 12) +LE_PERIODIC_ADVERTISING_SYNC_ESTABLISHED_EVENT_MASK = (1 << 13) +LE_PERIODIC_ADVERTISING_REPORT_EVENT_MASK = (1 << 14) +LE_PERIODIC_ADVERTISING_SYNC_LOST_EVENT_MASK = (1 << 15) +LE_EXTENDED_SCAN_TIMEOUT_EVENT_MASK = (1 << 16) +LE_EXTENDED_ADVERTISING_SET_TERMINATED_EVENT_MASK = (1 << 17) +LE_SCAN_REQUEST_RECEIVED_EVENT_MASK = (1 << 18) +LE_CHANNEL_SELECTION_ALGORITHM_EVENT_MASK = (1 << 19) + +# ACL +HCI_ACL_PB_FIRST_NON_FLUSHABLE = 0 +HCI_ACL_PB_CONTINUATION = 1 +HCI_ACL_PB_FIRST_FLUSHABLE = 2 +HCI_ACK_PB_COMPLETE_L2CAP = 3 + +# Roles +HCI_CENTRAL_ROLE = 0 +HCI_PERIPHERAL_ROLE = 1 + +HCI_ROLE_NAMES = { + HCI_CENTRAL_ROLE: 'CENTRAL', + HCI_PERIPHERAL_ROLE: 'PERIPHERAL' +} + +# LE PHY Types +HCI_LE_1M_PHY = 1 +HCI_LE_2M_PHY = 2 +HCI_LE_CODED_PHY = 3 + +HCI_LE_PHY_NAMES = { + HCI_LE_1M_PHY: 'LE 1M', + HCI_LE_2M_PHY: 'L2 2M', + HCI_LE_CODED_PHY: 'LE Coded' +} + +# Connection Parameters +HCI_CONNECTION_INTERVAL_MS_PER_UNIT = 1.25 +HCI_CONNECTION_LATENCY_MS_PER_UNIT = 1.25 +HCI_SUPERVISION_TIMEOUT_MS_PER_UNIT = 10 + +# Inquiry LAP +HCI_LIMITED_DEDICATED_INQUIRY_LAP = 0x9E8B00 +HCI_GENERAL_INQUIRY_LAP = 0x9E8B33 +HCI_INQUIRY_LAP_NAMES = { + HCI_LIMITED_DEDICATED_INQUIRY_LAP: 'Limited Dedicated Inquiry', + HCI_GENERAL_INQUIRY_LAP: 'General Inquiry' +} + +# Inquiry Mode +HCI_STANDARD_INQUIRY_MODE = 0x00 +HCI_INQUIRY_WITH_RSSI_MODE = 0x01 +HCI_EXTENDED_INQUIRY_MODE = 0x02 + +# Page Scan Repetition Mode +HCI_R0_PAGE_SCAN_REPETITION_MODE = 0x00 +HCI_R1_PAGE_SCAN_REPETITION_MODE = 0x01 +HCI_R2_PAGE_SCAN_REPETITION_MODE = 0x02 + +# IO Capability +HCI_DISPLAY_ONLY_IO_CAPABILITY = 0x00 +HCI_DISPLAY_YES_NO_IO_CAPABILITY = 0x01 +HCI_KEYBOARD_ONLY_IO_CAPABILITY = 0x02 +HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY = 0x03 + +HCI_IO_CAPABILITY_NAMES = { + HCI_DISPLAY_ONLY_IO_CAPABILITY: 'HCI_DISPLAY_ONLY_IO_CAPABILITY', + HCI_DISPLAY_YES_NO_IO_CAPABILITY: 'HCI_DISPLAY_YES_NO_IO_CAPABILITY', + HCI_KEYBOARD_ONLY_IO_CAPABILITY: 'HCI_KEYBOARD_ONLY_IO_CAPABILITY', + HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: 'HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY' +} + +# Authentication Requirements +HCI_MITM_NOT_REQUIRED_NO_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x00 +HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x01 +HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x02 +HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x03 +HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS = 0x04 +HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS = 0x05 + +HCI_AUTHENTICATION_REQUIREMENTS_NAMES = { + HCI_MITM_NOT_REQUIRED_NO_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_NO_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS', + HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_NO_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS', + HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_DEDICATED_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS', + HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_DEDICATED_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS', + HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_NUMERIC_COMPARISON_AUTHENTICATION_REQUIREMENTS', + HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS: 'HCI_MITM_REQUIRED_GENERAL_BONDING_USE_IO_CAPABILITIES_AUTHENTICATION_REQUIREMENTS' +} + +# Link Key Types +HCI_COMBINATION_KEY_TYPE = 0X00 +HCI_LOCAL_UNIT_KEY_TYPE = 0X01 +HCI_REMOTE_UNIT_KEY_TYPE = 0X02 +HCI_DEBUG_COMBINATION_KEY_TYPE = 0X03 +HCI_UNAUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE = 0X04 +HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE = 0X05 +HCI_CHANGED_COMBINATION_KEY_TYPE = 0X06 +HCI_UNAUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE = 0X07 +HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE = 0X08 + +HCI_LINK_TYPE_NAMES = { + HCI_COMBINATION_KEY_TYPE: 'HCI_COMBINATION_KEY_TYPE', + HCI_LOCAL_UNIT_KEY_TYPE: 'HCI_LOCAL_UNIT_KEY_TYPE', + HCI_REMOTE_UNIT_KEY_TYPE: 'HCI_REMOTE_UNIT_KEY_TYPE', + HCI_DEBUG_COMBINATION_KEY_TYPE: 'HCI_DEBUG_COMBINATION_KEY_TYPE', + HCI_UNAUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE: 'HCI_UNAUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE', + HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE: 'HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE', + HCI_CHANGED_COMBINATION_KEY_TYPE: 'HCI_CHANGED_COMBINATION_KEY_TYPE', + HCI_UNAUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE: 'HCI_UNAUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE', + HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE: 'HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE' +} + +# Address types +HCI_PUBLIC_DEVICE_ADDRESS_TYPE = 0x0 +HCI_RANDOM_DEVICE_ADDRESS_TYPE = 0x01 +HCI_PUBLIC_IDENTITY_ADDRESS_TYPE = 0x02 +HCI_RANDOM_IDENTITY_ADDRESS_TYPE = 0x03 + +# ----------------------------------------------------------------------------- +STATUS_SPEC = {'size': 1, 'mapper': lambda x: HCI_Constant.status_name(x)} + + +# ----------------------------------------------------------------------------- +class HCI_Constant: + @staticmethod + def status_name(status): + return HCI_ERROR_NAMES.get(status, f'0x{status:02X}') + + @staticmethod + def error_name(status): + return HCI_ERROR_NAMES.get(status, f'0x{status:02X}') + + @staticmethod + def role_name(role): + return HCI_ROLE_NAMES.get(role, str(role)) + + @staticmethod + def le_phy_name(phy): + return HCI_LE_PHY_NAMES.get(phy, str(phy)) + + @staticmethod + def inquiry_lap_name(lap): + return HCI_INQUIRY_LAP_NAMES.get(lap, f'0x{lap:06X}') + + @staticmethod + def io_capability_name(io_capability): + return HCI_IO_CAPABILITY_NAMES.get(io_capability, f'0x{io_capability:02X}') + + @staticmethod + def authentication_requirements_name(authentication_requirements): + return HCI_AUTHENTICATION_REQUIREMENTS_NAMES.get( + authentication_requirements, + f'0x{authentication_requirements:02X}' + ) + + @staticmethod + def link_key_type_name(link_key_type): + return HCI_LINK_TYPE_NAMES.get(link_key_type, f'0x{link_key_type:02X}') + + +# ----------------------------------------------------------------------------- +class HCI_Error(ProtocolError): + def __init__(self, error_code): + super().__init__(error_code, 'hci', HCI_Constant.error_name(error_code)) + + +# ----------------------------------------------------------------------------- +# Generic HCI object +# ----------------------------------------------------------------------------- +class HCI_Object: + @staticmethod + def init_from_fields(object, fields, values): + if type(values) is dict: + for field_name, _ in fields: + setattr(object, field_name, values[field_name]) + else: + for field_name, field_value in zip(fields, values): + setattr(object, field_name, field_value) + + @staticmethod + def init_from_bytes(object, data, offset, fields): + parsed = HCI_Object.dict_from_bytes(data, offset, fields) + HCI_Object.init_from_fields(object, parsed.keys(), parsed.values()) + + @staticmethod + def dict_from_bytes(data, offset, fields): + result = collections.OrderedDict() + for (field_name, field_type) in fields: + # The field_type may be a dictionnary with a mapper, parser, and/or size + if type(field_type) is dict: + if 'size' in field_type: + field_type = field_type['size'] + elif 'parser' in field_type: + field_type = field_type['parser'] + + # Parse the field + if field_type == '*': + # The rest of the bytes + field_value = data[offset:] + offset += len(field_value) + elif field_type == 1: + # 8-bit unsigned + field_value = data[offset] + offset += 1 + elif field_type == -1: + # 8-bit signed + field_value = struct.unpack_from('b', data, offset)[0] + offset += 1 + elif field_type == 2: + # 16-bit unsigned + field_value = struct.unpack_from('2': + # 16-bit unsigned big-endian + field_value = struct.unpack_from('>H', data, offset)[0] + offset += 2 + elif field_type == -2: + # 16-bit signed + field_value = struct.unpack_from('4': + # 32-bit unsigned big-endian + field_value = struct.unpack_from('>I', data, offset)[0] + offset += 4 + elif type(field_type) is int and field_type > 4 and field_type <= 256: + # Byte array (from 5 up to 256 bytes) + field_value = data[offset:offset + field_type] + offset += field_type + elif callable(field_type): + offset, field_value = field_type(data, offset) + else: + raise ValueError(f'unknown field type {field_type}') + + result[field_name] = field_value + + return result + + @staticmethod + def dict_to_bytes(object, fields): + result = bytearray() + for (field_name, field_type) in fields: + # The field_type may be a dictionnary with a mapper, parser, serializer, and/or size + serializer = None + if type(field_type) is dict: + if 'serializer' in field_type: + serializer = field_type['serializer'] + if 'size' in field_type: + field_type = field_type['size'] + + # Serialize the field + field_value = object[field_name] + if serializer: + field_bytes = serializer(field_value) + elif field_type == 1: + # 8-bit unsigned + field_bytes = bytes([field_value]) + elif field_type == -1: + # 8-bit signed + field_bytes = struct.pack('b', field_value) + elif field_type == 2: + # 16-bit unsigned + field_bytes = struct.pack('2': + # 16-bit unsigned big-endian + field_bytes = struct.pack('>H', field_value) + elif field_type == -2: + # 16-bit signed + field_bytes = struct.pack('4': + # 32-bit unsigned big-endian + field_bytes = struct.pack('>I', field_value) + elif field_type == '*': + if type(field_value) is int: + if field_value >= 0 and field_value <= 255: + field_bytes = bytes([field_value]) + else: + raise ValueError('value too large for *-typed field') + else: + field_bytes = bytes(field_value) + elif type(field_value) is bytes or type(field_value) is bytearray or hasattr(field_value, 'to_bytes'): + field_bytes = bytes(field_value) + if type(field_type) is int and field_type > 4 and field_type <= 256: + # Truncate or Pad with zeros if the field is too long or too short + if len(field_bytes) < field_type: + field_bytes += bytes(field_type - len(field_bytes)) + elif len(field_bytes) > field_type: + field_bytes = field_bytes[:field_type] + else: + raise ValueError(f"don't know how to serialize type {type(field_value)}") + + result += field_bytes + + return bytes(result) + + @staticmethod + def from_bytes(data, offset, fields): + return HCI_Object(fields, **HCI_Object.dict_from_bytes(data, offset, fields)) + + def to_bytes(self): + return HCI_Object.dict_to_bytes(self.__dict__, self.fields) + + @staticmethod + def parse_length_prefixed_bytes(data, offset): + length = data[offset] + return offset + 1 + length, data[offset + 1:offset + 1 + length] + + @staticmethod + def serialize_length_prefixed_bytes(data, padded_size=0): + prefixed_size = 1 + len(data) + padding = bytes(padded_size - prefixed_size) if prefixed_size < padded_size else b'' + return bytes([len(data)]) + data + padding + + @staticmethod + def format_field_value(value, indentation): + if type(value) is bytes: + return value.hex() + elif isinstance(value, HCI_Object): + return '\n' + value.to_string(indentation) + else: + return str(value) + + @staticmethod + def format_fields(object, keys, indentation='', value_mappers={}): + if not keys: + return '' + + # Measure the widest field name + max_field_name_length = max([len(key[0] if type(key) is tuple else key) for key in keys]) + + # Build array of formatted key:value pairs + fields = [] + for key in keys: + value_mapper = None + if type(key) is tuple: + # The key has an associated specifier + key, specifier = key + + # Get the value mapper from the specifier + if type(specifier) is dict: + value_mapper = specifier.get('mapper') + + # Get the value for the field + value = object[key] + + # Map the value if needed + value_mapper = value_mappers.get(key, value_mapper) + if value_mapper is not None: + value = value_mapper(value) + + # Get the string representation of the value + value_str = HCI_Object.format_field_value(value, indentation = indentation + ' ') + + # Add the field to the formatted result + key_str = color(f'{key + ":":{1 + max_field_name_length}}', 'cyan') + fields.append(f'{indentation}{key_str} {value_str}') + + return '\n'.join(fields) + + def __bytes__(self): + return self.to_bytes() + + def __init__(self, fields, **kwargs): + self.fields = fields + self.init_from_fields(self, fields, kwargs) + + def to_string(self, indentation='', value_mappers={}): + return HCI_Object.format_fields(self.__dict__, self.fields, indentation, value_mappers) + + def __str__(self): + return self.to_string() + + +# ----------------------------------------------------------------------------- +# Bluetooth Address +# ----------------------------------------------------------------------------- +class Address: + ''' + Bluetooth Address (see Bluetooth spec Vol 6, Part B - 1.3 DEVICE ADDRESS) + NOTE: the address bytes are stored in little-endian byte order here, so + address[0] is the LSB of the address, address[5] is the MSB. + ''' + + PUBLIC_DEVICE_ADDRESS = 0x00 + RANDOM_DEVICE_ADDRESS = 0x01 + PUBLIC_IDENTITY_ADDRESS = 0x02 + RANDOM_IDENTITY_ADDRESS = 0x03 + + ADDRESS_TYPE_NAMES = { + PUBLIC_DEVICE_ADDRESS: 'PUBLIC_DEVICE_ADDRESS', + RANDOM_DEVICE_ADDRESS: 'RANDOM_DEVICE_ADDRESS', + PUBLIC_IDENTITY_ADDRESS: 'PUBLIC_IDENTITY_ADDRESS', + RANDOM_IDENTITY_ADDRESS: 'RANDOM_IDENTITY_ADDRESS' + } + + ADDRESS_TYPE_SPEC = {'size': 1, 'mapper': lambda x: Address.address_type_name(x)} + + @staticmethod + def address_type_name(address_type): + return name_or_number(Address.ADDRESS_TYPE_NAMES, address_type) + + @staticmethod + def parse_address(data, offset): + # Fix the type to a default value. This is used for parsing type-less Classic addresses + return Address.parse_address_with_type(data, offset, Address.PUBLIC_DEVICE_ADDRESS) + + @staticmethod + def parse_address_with_type(data, offset, address_type): + return offset + 6, Address(data[offset:offset + 6], address_type) + + @staticmethod + def parse_address_preceded_by_type(data, offset): + address_type = data[offset - 1] + return Address.parse_address_with_type(data, offset, address_type) + + def __init__(self, address, address_type = RANDOM_DEVICE_ADDRESS): + ''' + Initialize an instance. `address` may be a byte array in little-endian + format, or a hex string in big-endian format (with optional ':' + separators between the bytes). + If the address is a string suffixed with '/P', `address_type` is ignored and the type + is set to PUBLIC_DEVICE_ADDRESS. + ''' + if type(address) is bytes: + self.address_bytes = address + else: + # Check if there's a '/P' type specifier + if address.endswith('P'): + address_type = Address.PUBLIC_DEVICE_ADDRESS + address = address[:-2] + + if len(address) == 12 + 5: + # Form with ':' separators + address = address.replace(':', '') + self.address_bytes = bytes(reversed(bytes.fromhex(address))) + + if len(self.address_bytes) != 6: + raise ValueError('invalid address length') + + self.address_type = address_type + + @property + def is_public(self): + return self.address_type == self.PUBLIC_DEVICE_ADDRESS or self.address_type == self.PUBLIC_IDENTITY_ADDRESS + + @property + def is_random(self): + return not self.is_public + + @property + def is_resolved(self): + return self.address_type == self.PUBLIC_IDENTITY_ADDRESS or self.address_type == self.RANDOM_IDENTITY_ADDRESS + + @property + def is_resolvable(self): + return self.address_type == self.RANDOM_DEVICE_ADDRESS and (self.address_bytes[5] >> 6 == 1) + + @property + def is_static(self): + return self.is_random and (self.address_bytes[5] >> 6 == 3) + + def to_bytes(self): + return self.address_bytes + + def __bytes__(self): + return self.to_bytes() + + def __hash__(self): + return hash(self.address_bytes) + + def __eq__(self, other): + return self.address_bytes == other.address_bytes and self.is_public == other.is_public + + def __str__(self): + ''' + String representation of the address, MSB first + ''' + return ':'.join([f'{x:02X}' for x in reversed(self.address_bytes)]) + + +# ----------------------------------------------------------------------------- +class HCI_Packet: + ''' + Abstract Base class for HCI packets + ''' + + @staticmethod + def from_bytes(packet): + packet_type = packet[0] + if packet_type == HCI_COMMAND_PACKET: + return HCI_Command.from_bytes(packet) + elif packet_type == HCI_ACL_DATA_PACKET: + return HCI_AclDataPacket.from_bytes(packet) + elif packet_type == HCI_EVENT_PACKET: + return HCI_Event.from_bytes(packet) + else: + return HCI_CustomPacket(packet) + + def __init__(self, name): + self.name = name + + def __repr__(self) -> str: + return self.name + + +# ----------------------------------------------------------------------------- +class HCI_CustomPacket(HCI_Packet): + def __init__(self, payload): + super().__init__('HCI_CUSTOM_PACKET') + self.hci_packet_type = payload[0] + self.payload = payload + + +# ----------------------------------------------------------------------------- +class HCI_Command(HCI_Packet): + ''' + See Bluetooth spec @ Vol 2, Part E - 5.4.1 HCI Command Packet + ''' + hci_packet_type = HCI_COMMAND_PACKET + command_classes = {} + + @staticmethod + def command(fields=[], return_parameters_fields=[]): + ''' + Decorator used to declare and register subclasses + ''' + + def inner(cls): + cls.name = cls.__name__.upper() + cls.op_code = key_with_value(HCI_COMMAND_NAMES, cls.name) + if cls.op_code is None: + raise KeyError('command not found in HCI_COMMAND_NAMES') + cls.fields = fields + cls.return_parameters_fields = return_parameters_fields + + # Patch the __init__ method to fix the op_code + def init(self, parameters=None, **kwargs): + return HCI_Command.__init__(self, cls.op_code, parameters, **kwargs) + cls.__init__ = init + + # Register a factory for this class + HCI_Command.command_classes[cls.op_code] = cls + + return cls + + return inner + + @staticmethod + def from_bytes(packet): + op_code, length = struct.unpack_from('> 10:02x}, OCF=0x{op_code & 0x3FF:04x}]' + + @classmethod + def create_return_parameters(cls, **kwargs): + return HCI_Object(cls.return_parameters_fields, **kwargs) + + def __init__(self, op_code, parameters=None, **kwargs): + super().__init__(HCI_Command.command_name(op_code)) + if (fields := getattr(self, 'fields', None)) and kwargs: + HCI_Object.init_from_fields(self, fields, kwargs) + if parameters is None: + parameters = HCI_Object.dict_to_bytes(kwargs, fields) + self.op_code = op_code + self.parameters = parameters + + def to_bytes(self): + parameters = b'' if self.parameters is None else self.parameters + return struct.pack('> 12) & 3 + bc_flag = (h >> 14) & 3 + data = packet[5:] + if len(data) != data_total_length: + raise ValueError('invalid packet length') + return HCI_AclDataPacket(connection_handle, pb_flag, bc_flag, data_total_length, data) + + def to_bytes(self): + h = (self.pb_flag << 12) | (self.bc_flag << 14) | self.connection_handle + return struct.pack(' self.l2cap_pdu_length + 4: + logger.warning('!!! ACL data exceeds L2CAP PDU') + self.current_data = None + self.l2cap_pdu_length = 0 diff --git a/bumble/helpers.py b/bumble/helpers.py new file mode 100644 index 0000000..09b86ef --- /dev/null +++ b/bumble/helpers.py @@ -0,0 +1,179 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +from colors import color + +from .core import name_or_number +from .gatt import ATT_PDU, ATT_CID +from .l2cap import ( + L2CAP_PDU, + L2CAP_CONNECTION_REQUEST, + L2CAP_CONNECTION_RESPONSE, + L2CAP_SIGNALING_CID, + L2CAP_LE_SIGNALING_CID, + L2CAP_Control_Frame, + L2CAP_Connection_Response +) +from .hci import ( + HCI_EVENT_PACKET, + HCI_ACL_DATA_PACKET, + HCI_DISCONNECTION_COMPLETE_EVENT, + HCI_AclDataPacketAssembler +) +from .rfcomm import RFCOMM_Frame, RFCOMM_PSM +from .sdp import SDP_PDU, SDP_PSM +from .avdtp import ( + MessageAssembler as AVDTP_MessageAssembler, + AVDTP_PSM +) + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +PSM_NAMES = { + RFCOMM_PSM: 'RFCOMM', + SDP_PSM: 'SDP', + AVDTP_PSM: 'AVDTP' + # TODO: add more PSM values +} + + +# ----------------------------------------------------------------------------- +class PacketTracer: + class AclStream: + def __init__(self, analyzer): + self.analyzer = analyzer + self.packet_assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu) + self.avdtp_assemblers = {} # AVDTP assemblers, by source_cid + self.psms = {} # PSM, by source_cid + self.peer = None # ACL stream in the other direction + + def on_acl_pdu(self, pdu): + l2cap_pdu = L2CAP_PDU.from_bytes(pdu) + + if l2cap_pdu.cid == ATT_CID: + att_pdu = ATT_PDU.from_bytes(l2cap_pdu.payload) + self.analyzer.emit(att_pdu) + elif l2cap_pdu.cid == L2CAP_SIGNALING_CID or l2cap_pdu.cid == L2CAP_LE_SIGNALING_CID: + control_frame = L2CAP_Control_Frame.from_bytes(l2cap_pdu.payload) + self.analyzer.emit(control_frame) + + # Check if this signals a new channel + if control_frame.code == L2CAP_CONNECTION_REQUEST: + self.psms[control_frame.source_cid] = control_frame.psm + elif control_frame.code == L2CAP_CONNECTION_RESPONSE: + if control_frame.result == L2CAP_Connection_Response.CONNECTION_SUCCESSFUL: + if self.peer: + if psm := self.peer.psms.get(control_frame.source_cid): + # Found a pending connection + self.psms[control_frame.destination_cid] = psm + + # For AVDTP connections, create a packet assembler for each direction + if psm == AVDTP_PSM: + self.avdtp_assemblers[control_frame.source_cid] = AVDTP_MessageAssembler(self.on_avdtp_message) + self.peer.avdtp_assemblers[control_frame.destination_cid] = AVDTP_MessageAssembler(self.peer.on_avdtp_message) + + else: + # Try to find the PSM associated with this PDU + if self.peer and (psm := self.peer.psms.get(l2cap_pdu.cid)): + if psm == SDP_PSM: + sdp_pdu = SDP_PDU.from_bytes(l2cap_pdu.payload) + self.analyzer.emit(sdp_pdu) + elif psm == RFCOMM_PSM: + rfcomm_frame = RFCOMM_Frame.from_bytes(l2cap_pdu.payload) + self.analyzer.emit(rfcomm_frame) + elif psm == AVDTP_PSM: + self.analyzer.emit(f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, PSM=AVDTP]: {l2cap_pdu.payload.hex()}') + assembler = self.avdtp_assemblers.get(l2cap_pdu.cid) + if assembler: + assembler.on_pdu(l2cap_pdu.payload) + else: + psm_string = name_or_number(PSM_NAMES, psm) + self.analyzer.emit(f'{color("L2CAP", "green")} [CID={l2cap_pdu.cid}, PSM={psm_string}]: {l2cap_pdu.payload.hex()}') + else: + self.analyzer.emit(l2cap_pdu) + + def on_avdtp_message(self, transaction_label, message): + self.analyzer.emit(f'{color("AVDTP", "green")} [{transaction_label}] {message}') + + def feed_packet(self, packet): + self.packet_assembler.feed_packet(packet) + + class Analyzer: + def __init__(self, label, emit_message): + self.label = label + self.emit_message = emit_message + self.acl_streams = {} # ACL streams, by connection handle + self.peer = None # Analyzer in the other direction + + def start_acl_stream(self, connection_handle): + logger.info(f'[{self.label}] +++ Creating ACL stream for connection 0x{connection_handle:04X}') + stream = PacketTracer.AclStream(self) + self.acl_streams[connection_handle] = stream + + # Associate with a peer stream if we can + if peer_stream := self.peer.acl_streams.get(connection_handle): + stream.peer = peer_stream + peer_stream.peer = stream + + return stream + + def end_acl_stream(self, connection_handle): + if connection_handle in self.acl_streams: + logger.info(f'[{self.label}] --- Removing ACL stream for connection 0x{connection_handle:04X}') + del self.acl_streams[connection_handle] + + # Let the other forwarder know so it can cleanup its stream as well + self.peer.end_acl_stream(connection_handle) + + def on_packet(self, packet): + self.emit(packet) + + if packet.hci_packet_type == HCI_ACL_DATA_PACKET: + # Look for an existing stream for this handle, create one if it is the + # first ACL packet for that connection handle + if (stream := self.acl_streams.get(packet.connection_handle)) is None: + stream = self.start_acl_stream(packet.connection_handle) + stream.feed_packet(packet) + elif packet.hci_packet_type == HCI_EVENT_PACKET: + if packet.event_code == HCI_DISCONNECTION_COMPLETE_EVENT: + self.end_acl_stream(packet.connection_handle) + + def emit(self, message): + self.emit_message(f'[{self.label}] {message}') + + def trace(self, packet, direction=0): + if direction == 0: + self.host_to_controller_analyzer.on_packet(packet) + else: + self.controller_to_host_analyzer.on_packet(packet) + + def __init__( + self, + host_to_controller_label=color('HOST->CONTROLLER', 'blue'), + controller_to_host_label=color('CONTROLLER->HOST', 'cyan'), + emit_message=logger.info + ): + self.host_to_controller_analyzer = PacketTracer.Analyzer(host_to_controller_label, emit_message) + self.controller_to_host_analyzer = PacketTracer.Analyzer(controller_to_host_label, emit_message) + self.host_to_controller_analyzer.peer = self.controller_to_host_analyzer + self.controller_to_host_analyzer.peer = self.host_to_controller_analyzer diff --git a/bumble/hfp.py b/bumble/hfp.py new file mode 100644 index 0000000..6eeb0d9 --- /dev/null +++ b/bumble/hfp.py @@ -0,0 +1,94 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import asyncio +import collections +from colors import color + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Protocol Support +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +class HfpProtocol: + def __init__(self, dlc): + self.dlc = dlc + self.buffer = '' + self.lines = collections.deque() + self.lines_available = asyncio.Event() + + dlc.sink = self.feed + + def feed(self, data): + # Convert the data to a string if needed + if type(data) == bytes: + data = data.decode('utf-8') + + logger.debug(f'<<< Data received: {data}') + + # Add to the buffer and look for lines + self.buffer += data + while (separator := self.buffer.find('\r')) >= 0: + line = self.buffer[:separator].strip() + self.buffer = self.buffer[separator + 1:] + if len(line) > 0: + self.on_line(line) + + def on_line(self, line): + self.lines.append(line) + self.lines_available.set() + + def send_command_line(self, line): + logger.debug(color(f'>>> {line}', 'yellow')) + self.dlc.write(line + '\r') + + def send_response_line(self, line): + logger.debug(color(f'>>> {line}', 'yellow')) + self.dlc.write('\r\n' + line + '\r\n') + + async def next_line(self): + await self.lines_available.wait() + line = self.lines.popleft() + if not self.lines: + self.lines_available.clear() + logger.debug(color(f'<<< {line}', 'green')) + return line + + async def initialize_service(self): + # Perform Service Level Connection Initialization + self.send_command_line('AT+BRSF=2072') # Retrieve Supported Features + line = await(self.next_line()) + line = await(self.next_line()) + + self.send_command_line('AT+CIND=?') + line = await(self.next_line()) + line = await(self.next_line()) + + self.send_command_line('AT+CIND?') + line = await(self.next_line()) + line = await(self.next_line()) + + self.send_command_line('AT+CMER=3,0,0,1') + line = await(self.next_line()) diff --git a/bumble/host.py b/bumble/host.py new file mode 100644 index 0000000..68859ca --- /dev/null +++ b/bumble/host.py @@ -0,0 +1,604 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import logging +from pyee import EventEmitter +from colors import color + +from .hci import * +from .l2cap import * +from .att import * +from .gatt import * +from .smp import * +from .core import ConnectionParameters + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +HOST_DEFAULT_HC_LE_ACL_DATA_PACKET_LENGTH = 27 +HOST_HC_TOTAL_NUM_LE_ACL_DATA_PACKETS = 1 +HOST_DEFAULT_HC_ACL_DATA_PACKET_LENGTH = 27 +HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS = 1 + + +# ----------------------------------------------------------------------------- +class Connection: + def __init__(self, host, handle, role, peer_address): + self.host = host + self.handle = handle + self.role = role + self.peer_address = peer_address + self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu) + + def on_hci_acl_data_packet(self, packet): + self.assembler.feed_packet(packet) + + def on_acl_pdu(self, pdu): + l2cap_pdu = L2CAP_PDU.from_bytes(pdu) + + if l2cap_pdu.cid == ATT_CID: + self.host.on_gatt_pdu(self, l2cap_pdu.payload) + elif l2cap_pdu.cid == SMP_CID: + self.host.on_smp_pdu(self, l2cap_pdu.payload) + else: + self.host.on_l2cap_pdu(self, l2cap_pdu.cid, l2cap_pdu.payload) + + +# ----------------------------------------------------------------------------- +class Host(EventEmitter): + def __init__(self, controller_source = None, controller_sink = None): + super().__init__() + + self.hci_sink = None + self.ready = False # True when we can accept incoming packets + self.connections = {} # Connections, by connection handle + self.pending_command = None + self.pending_response = None + self.hc_le_acl_data_packet_length = HOST_DEFAULT_HC_LE_ACL_DATA_PACKET_LENGTH + self.hc_total_num_le_acl_data_packets = HOST_HC_TOTAL_NUM_LE_ACL_DATA_PACKETS + self.hc_acl_data_packet_length = HOST_DEFAULT_HC_ACL_DATA_PACKET_LENGTH + self.hc_total_num_acl_data_packets = HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS + self.acl_packet_queue = collections.deque() + self.acl_packets_in_flight = 0 + self.local_supported_commands = bytes(64) + self.command_semaphore = asyncio.Semaphore(1) + self.long_term_key_provider = None + self.link_key_provider = None + + # Connect to the source and sink if specified + if controller_source: + controller_source.set_packet_sink(self) + if controller_sink: + self.set_packet_sink(controller_sink) + + async def reset(self): + await self.send_command(HCI_Reset_Command()) + self.ready = True + + response = await self.send_command(HCI_Read_Local_Supported_Commands_Command()) + if response.return_parameters.status != HCI_SUCCESS: + raise ProtocolError(response.return_parameters.status, 'hci') + self.local_supported_commands = response.return_parameters.supported_commands + + await self.send_command(HCI_Set_Event_Mask_Command(event_mask = bytes.fromhex('FFFFFFFFFFFFFFFF'))) + await self.send_command(HCI_LE_Set_Event_Mask_Command(le_event_mask = bytes.fromhex('FFFFF00000000000'))) + await self.send_command(HCI_Read_Local_Version_Information_Command()) + await self.send_command(HCI_Write_LE_Host_Support_Command(le_supported_host = 1, simultaneous_le_host = 0)) + + response = await self.send_command(HCI_LE_Read_Buffer_Size_Command()) + if response.return_parameters.status == HCI_SUCCESS: + self.hc_le_acl_data_packet_length = response.return_parameters.hc_le_acl_data_packet_length + self.hc_total_num_le_acl_data_packets = response.return_parameters.hc_total_num_le_acl_data_packets + logger.debug(f'HCI LE ACL flow control: hc_le_acl_data_packet_length={response.return_parameters.hc_le_acl_data_packet_length}, hc_total_num_le_acl_data_packets={response.return_parameters.hc_total_num_le_acl_data_packets}') + else: + logger.warn(f'HCI_LE_Read_Buffer_Size_Command failed: {response.return_parameters.status}') + if response.return_parameters.hc_le_acl_data_packet_length == 0 or response.return_parameters.hc_total_num_le_acl_data_packets == 0: + # Read the non-LE-specific values + response = await self.send_command(HCI_Read_Buffer_Size_Command()) + if response.return_parameters.status == HCI_SUCCESS: + self.hc_acl_data_packet_length = response.return_parameters.hc_le_acl_data_packet_length + self.hc_le_acl_data_packet_length = self.hc_le_acl_data_packet_length or self.hc_acl_data_packet_length + self.hc_total_num_acl_data_packets = response.return_parameters.hc_total_num_le_acl_data_packets + self.hc_total_num_le_acl_data_packets = self.hc_total_num_le_acl_data_packets or self.hc_total_num_acl_data_packets + logger.debug(f'HCI LE ACL flow control: hc_le_acl_data_packet_length={self.hc_le_acl_data_packet_length}, hc_total_num_le_acl_data_packets={self.hc_total_num_le_acl_data_packets}') + else: + logger.warn(f'HCI_Read_Buffer_Size_Command failed: {response.return_parameters.status}') + + self.reset_done = True + + @property + def controller(self): + return self.hci_sink + + @controller.setter + def controller(self, controller): + self.set_packet_sink(controller) + if controller: + controller.set_packet_sink(self) + + def set_packet_sink(self, sink): + self.hci_sink = sink + + def send_hci_packet(self, packet): + self.hci_sink.on_packet(packet.to_bytes()) + + async def send_command(self, command): + logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {command}') + + # Wait until we can send (only one pending command at a time) + async with self.command_semaphore: + assert(self.pending_command is None) + assert(self.pending_response is None) + + # Create a future value to hold the eventual response + self.pending_response = asyncio.get_running_loop().create_future() + self.pending_command = command + + try: + self.send_hci_packet(command) + response = await self.pending_response + # TODO: check error values + return response + except Exception as error: + logger.warning(f'{color("!!! Exception while sending HCI packet:", "red")} {error}') + # raise error + finally: + self.pending_command = None + self.pending_response = None + + # Use this method to send a command from a task + def send_command_sync(self, command): + async def send_command(command): + await self.send_command(command) + + asyncio.create_task(send_command(command)) + + def send_l2cap_pdu(self, connection_handle, cid, pdu): + l2cap_pdu = L2CAP_PDU(cid, pdu).to_bytes() + + # Send the data to the controller via ACL packets + bytes_remaining = len(l2cap_pdu) + offset = 0 + pb_flag = 0 + while bytes_remaining: + data_total_length = min(bytes_remaining, self.hc_le_acl_data_packet_length) + acl_packet = HCI_AclDataPacket( + connection_handle = connection_handle, + pb_flag = pb_flag, + bc_flag = 0, + data_total_length = data_total_length, + data = l2cap_pdu[offset:offset + data_total_length] + ) + logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: (CID={cid}) {acl_packet}') + self.queue_acl_packet(acl_packet) + pb_flag = 1 + offset += data_total_length + bytes_remaining -= data_total_length + + def queue_acl_packet(self, acl_packet): + self.acl_packet_queue.appendleft(acl_packet) + self.check_acl_packet_queue() + + if len(self.acl_packet_queue): + logger.debug(f'{self.acl_packets_in_flight} ACL packets in flight, {len(self.acl_packet_queue)} in queue') + + def check_acl_packet_queue(self): + # Send all we can + while len(self.acl_packet_queue) > 0 and self.acl_packets_in_flight < self.hc_total_num_le_acl_data_packets: + packet = self.acl_packet_queue.pop() + self.send_hci_packet(packet) + self.acl_packets_in_flight += 1 + + # Packet Sink protocol (packets coming from the controller via HCI) + def on_packet(self, packet): + hci_packet = HCI_Packet.from_bytes(packet) + if self.ready or ( + hci_packet.hci_packet_type == HCI_EVENT_PACKET and + hci_packet.event_code == HCI_COMMAND_COMPLETE_EVENT and + hci_packet.command_opcode == HCI_RESET_COMMAND + ): + self.on_hci_packet(hci_packet) + else: + logger.debug('reset not done, ignoring packet from controller') + + def on_hci_packet(self, packet): + logger.debug(f'{color("### CONTROLLER -> HOST", "green")}: {packet}') + + # If the packet is a command, invoke the handler for this packet + if packet.hci_packet_type == HCI_COMMAND_PACKET: + self.on_hci_command_packet(packet) + elif packet.hci_packet_type == HCI_EVENT_PACKET: + self.on_hci_event_packet(packet) + elif packet.hci_packet_type == HCI_ACL_DATA_PACKET: + self.on_hci_acl_data_packet(packet) + else: + logger.warning(f'!!! unknown packet type {packet.hci_packet_type}') + + def on_hci_command_packet(self, command): + logger.warning(f'!!! unexpected command packet: {command}') + + def on_hci_event_packet(self, event): + handler_name = f'on_{event.name.lower()}' + handler = getattr(self, handler_name, self.on_hci_event) + handler(event) + + def on_hci_acl_data_packet(self, packet): + # Look for the connection to which this data belongs + if connection := self.connections.get(packet.connection_handle): + connection.on_hci_acl_data_packet(packet) + + def on_gatt_pdu(self, connection, pdu): + self.emit('gatt_pdu', connection.handle, pdu) + + def on_smp_pdu(self, connection, pdu): + self.emit('smp_pdu', connection.handle, pdu) + + def on_l2cap_pdu(self, connection, cid, pdu): + self.emit('l2cap_pdu', connection.handle, cid, pdu) + + def on_command_processed(self, event): + if self.pending_response: + # Check that it is what we were expecting + if self.pending_command.op_code != event.command_opcode: + logger.warning(f'!!! command result mismatch, expected 0x{self.pending_command.op_code:X} but got 0x{event.command_opcode:X}') + + self.pending_response.set_result(event) + else: + logger.warning('!!! no pending response future to set') + + ############################################################ + # HCI handlers + ############################################################ + def on_hci_event(self, event): + logger.warning(f'{color(f"--- Ignoring event {event}", "red")}') + + def on_hci_command_complete_event(self, event): + if event.command_opcode == 0: + # This is used just for the Num_HCI_Command_Packets field, not related to an actual command + logger.debug('no-command event') + else: + return self.on_command_processed(event) + + def on_hci_command_status_event(self, event): + return self.on_command_processed(event) + + def on_hci_number_of_completed_packets_event(self, event): + total_packets = sum(event.num_completed_packets) + if total_packets <= self.acl_packets_in_flight: + self.acl_packets_in_flight -= total_packets + self.check_acl_packet_queue() + else: + logger.warning(color(f'!!! {total_packets} completed but only {self.acl_packets_in_flight} in flight')) + self.acl_packets_in_flight = 0 + + # Classic only + def on_hci_connection_request_event(self, event): + # For now, just accept everything + # TODO: delegate the decision + self.send_command_sync( + HCI_Accept_Connection_Request_Command( + bd_addr = event.bd_addr, + role = 0x01 # Remain the peripheral + ) + ) + + def on_hci_le_connection_complete_event(self, event): + # Check if this is a cancellation + if event.status == HCI_SUCCESS: + # Create/update the connection + logger.debug(f'### CONNECTION: [0x{event.connection_handle:04X}] {event.peer_address} as {HCI_Constant.role_name(event.role)}') + + connection = self.connections.get(event.connection_handle) + if connection is None: + connection = Connection(self, event.connection_handle, event.role, event.peer_address) + self.connections[event.connection_handle] = connection + + # Notify the client + connection_parameters = ConnectionParameters( + event.conn_interval, + event.conn_latency, + event.supervision_timeout + ) + self.emit( + 'connection', + event.connection_handle, + BT_LE_TRANSPORT, + event.peer_address, + None, + event.role, + connection_parameters + ) + else: + logger.debug(f'### CONNECTION FAILED: {event.status}') + + # Notify the listeners + self.emit('connection_failure', event.status) + + def on_hci_le_enhanced_connection_complete_event(self, event): + # Just use the same implementation as for the non-enhanced event for now + self.on_hci_le_connection_complete_event(event) + + def on_hci_connection_complete_event(self, event): + if event.status == HCI_SUCCESS: + # Create/update the connection + logger.debug(f'### BR/EDR CONNECTION: [0x{event.connection_handle:04X}] {event.bd_addr}') + + connection = self.connections.get(event.connection_handle) + if connection is None: + connection = Connection(self, event.connection_handle, BT_CENTRAL_ROLE, event.bd_addr) + self.connections[event.connection_handle] = connection + + # Notify the client + self.emit( + 'connection', + event.connection_handle, + BT_BR_EDR_TRANSPORT, + event.bd_addr, + None, + BT_CENTRAL_ROLE, + None + ) + else: + logger.debug(f'### BR/EDR CONNECTION FAILED: {event.status}') + + # Notify the client + self.emit('connection_failure', event.status) + + def on_hci_disconnection_complete_event(self, event): + # Find the connection + if (connection := self.connections.get(event.connection_handle)) is None: + logger.warning('!!! DISCONNECTION COMPLETE: unknown handle') + return + + if event.status == HCI_SUCCESS: + logger.debug(f'### DISCONNECTION: [0x{event.connection_handle:04X}] {connection.peer_address} as {HCI_Constant.role_name(connection.role)}, reason={event.reason}') + del self.connections[event.connection_handle] + + # Notify the listeners + self.emit('disconnection', event.connection_handle, event.reason) + else: + logger.debug(f'### DISCONNECTION FAILED: {event.status}') + + # Notify the listeners + self.emit('disconnection_failure', event.status) + + def on_hci_le_connection_update_complete_event(self, event): + if (connection := self.connections.get(event.connection_handle)) is None: + logger.warning('!!! CONNECTION PARAMETERS UPDATE COMPLETE: unknown handle') + return + + # Notify the client + if event.status == HCI_SUCCESS: + connection_parameters = ConnectionParameters( + event.conn_interval, + event.conn_latency, + event.supervision_timeout + ) + self.emit('connection_parameters_update', connection.handle, connection_parameters) + else: + self.emit('connection_parameters_update_failure', connection.handle, event.status) + + def on_hci_le_phy_update_complete_event(self, event): + if (connection := self.connections.get(event.connection_handle)) is None: + logger.warning('!!! CONNECTION PHY UPDATE COMPLETE: unknown handle') + return + + # Notify the client + if event.status == HCI_SUCCESS: + connection_phy = ConnectionPHY(event.tx_phy, event.rx_phy) + self.emit('connection_phy_update', connection.handle, connection_phy) + else: + self.emit('connection_phy_update_failure', connection.handle, event.status) + + def on_hci_le_advertising_report_event(self, event): + for report in event.reports: + self.emit( + 'advertising_report', + report.address, + report.data, + report.rssi, + report.event_type + ) + + def on_hci_le_remote_connection_parameter_request_event(self, event): + if event.connection_handle not in self.connections: + logger.warning('!!! REMOTE CONNECTION PARAMETER REQUEST: unknown handle') + return + + # For now, just accept everything + # TODO: delegate the decision + self.send_command_sync( + HCI_LE_Remote_Connection_Parameter_Request_Reply_Command( + connection_handle = event.connection_handle, + interval_min = event.interval_min, + interval_max = event.interval_max, + latency = event.latency, + timeout = event.timeout, + minimum_ce_length = 0, + maximum_ce_length = 0 + ) + ) + + def on_hci_le_long_term_key_request_event(self, event): + if (connection := self.connections.get(event.connection_handle)) is None: + logger.warning('!!! LE LONG TERM KEY REQUEST: unknown handle') + return + + async def send_long_term_key(): + if self.long_term_key_provider is None: + logger.debug('no long term key provider') + long_term_key = None + else: + long_term_key = await self.long_term_key_provider( + connection.handle, + event.random_number, + event.encryption_diversifier + ) + if long_term_key: + response = HCI_LE_Long_Term_Key_Request_Reply_Command( + connection_handle = event.connection_handle, + long_term_key = long_term_key + ) + else: + response = HCI_LE_Long_Term_Key_Request_Negative_Reply_Command( + connection_handle = event.connection_handle + ) + + await self.send_command(response) + + asyncio.create_task(send_long_term_key()) + + def on_hci_synchronous_connection_complete_event(self, event): + pass + + def on_hci_synchronous_connection_changed_event(self, event): + pass + + def on_hci_role_change_event(self, event): + if event.status == HCI_SUCCESS: + logger.debug(f'role change for {event.bd_addr}: {HCI_Constant.role_name(event.new_role)}') + # TODO: lookup the connection and update the role + else: + logger.debug(f'role change for {event.bd_addr} failed: {HCI_Constant.error_name(event.status)}') + + def on_hci_le_data_length_change_event(self, event): + self.emit( + 'connection_data_length_change', + event.connection_handle, + event.max_tx_octets, + event.max_tx_time, + event.max_rx_octets, + event.max_rx_time + ) + + def on_hci_authentication_complete_event(self, event): + # Notify the client + if event.status == HCI_SUCCESS: + self.emit('connection_authentication_complete', event.connection_handle) + else: + self.emit('connection_authentication_failure', event.connection_handle, event.status) + + def on_hci_encryption_change_event(self, event): + # Notify the client + if event.status == HCI_SUCCESS: + self.emit('connection_encryption_change', event.connection_handle, event.encryption_enabled) + else: + self.emit('connection_encryption_failure', event.connection_handle, event.status) + + def on_hci_encryption_key_refresh_complete_event(self, event): + # Notify the client + if event.status == HCI_SUCCESS: + self.emit('connection_encryption_key_refresh', event.connection_handle) + else: + self.emit('connection_encryption_key_refresh_failure', event.connection_handle, event.status) + + def on_hci_link_supervision_timeout_changed_event(self, event): + pass + + def on_hci_max_slots_change_event(self, event): + pass + + def on_hci_page_scan_repetition_mode_change_event(self, event): + pass + + def on_hci_link_key_notification_event(self, event): + logger.debug(f'link key for {event.bd_addr}: {event.link_key.hex()}, type={HCI_Constant.link_key_type_name(event.key_type)}') + self.emit('link_key', event.bd_addr, event.link_key, event.key_type) + + def on_hci_simple_pairing_complete_event(self, event): + logger.debug(f'simple pairing complete for {event.bd_addr}: status={HCI_Constant.status_name(event.status)}') + + def on_hci_pin_code_request_event(self, event): + # For now, just refuse all requests + # TODO: delegate the decision + self.send_command_sync( + HCI_PIN_Code_Request_Negative_Reply_Command( + bd_addr = event.bd_addr + ) + ) + + def on_hci_link_key_request_event(self, event): + async def send_link_key(): + if self.link_key_provider is None: + logger.debug('no link key provider') + link_key = None + else: + link_key = await self.link_key_provider(event.bd_addr) + if link_key: + response = HCI_Link_Key_Request_Reply_Command( + bd_addr = event.bd_addr, + link_key = link_key + ) + else: + response = HCI_Link_Key_Request_Negative_Reply_Command( + bd_addr = event.bd_addr + ) + + await self.send_command(response) + + asyncio.create_task(send_link_key()) + + def on_hci_io_capability_request_event(self, event): + # For now, just return NoInputNoOutput and no MITM + # TODO: delegate the decision + self.send_command_sync( + HCI_IO_Capability_Request_Reply_Command( + bd_addr = event.bd_addr, + io_capability = HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY, + oob_data_present = 0x00, + authentication_requirements = 0x00 # 0x02 # FIXME: testing only + ) + ) + + def on_hci_io_capability_response_event(self, event): + pass + + def on_hci_user_confirmation_request_event(self, event): + # For now, just confirm everything + # TODO: delegate the decision + self.send_command_sync( + HCI_User_Confirmation_Request_Reply_Command(bd_addr = event.bd_addr) + ) + + def on_hci_inquiry_complete_event(self, event): + self.emit('inquiry_complete') + + def on_hci_inquiry_result_with_rssi_event(self, event): + for response in event.responses: + self.emit( + 'inquiry_result', + response.bd_addr, + response.class_of_device, + b'', + response.rssi + ) + + def on_hci_extended_inquiry_result_event(self, event): + self.emit( + 'inquiry_result', + event.bd_addr, + event.class_of_device, + event.extended_inquiry_response, + event.rssi + ) diff --git a/bumble/keys.py b/bumble/keys.py new file mode 100644 index 0000000..f51cfe6 --- /dev/null +++ b/bumble/keys.py @@ -0,0 +1,273 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Keys and Key Storage +# +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import os +import json +from colors import color + +from .hci import Address + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +class PairingKeys: + class Key: + def __init__(self, value, authenticated=False, ediv=None, rand=None): + self.value = value + self.authenticated = authenticated + self.ediv = ediv + self.rand = rand + + @classmethod + def from_dict(cls, key_dict): + value = bytes.fromhex(key_dict['value']) + authenticated = key_dict.get('authenticated', False) + ediv = key_dict.get('ediv') + rand = key_dict.get('rand') + if rand is not None: + rand = bytes.fromhex(rand) + + return cls(value, authenticated, ediv, rand) + + def to_dict(self): + key_dict = {'value': self.value.hex(), 'authenticated': self.authenticated} + if self.ediv is not None: + key_dict['ediv'] = self.ediv + if self.rand is not None: + key_dict['rand'] = self.rand.hex() + + return key_dict + + def __init__(self): + self.address_type = None + self.ltk = None + self.ltk_central = None + self.ltk_peripheral = None + self.irk = None + self.csrk = None + self.link_key = None # Classic + + @staticmethod + def key_from_dict(keys_dict, key_name): + key_dict = keys_dict.get(key_name) + if key_dict is not None: + return PairingKeys.Key.from_dict(key_dict) + + @staticmethod + def from_dict(keys_dict): + keys = PairingKeys() + + keys.address_type = keys_dict.get('address_type') + keys.ltk = PairingKeys.key_from_dict(keys_dict, 'ltk') + keys.ltk_central = PairingKeys.key_from_dict(keys_dict, 'ltk_central') + keys.ltk_peripheral = PairingKeys.key_from_dict(keys_dict, 'ltk_peripheral') + keys.irk = PairingKeys.key_from_dict(keys_dict, 'irk') + keys.csrk = PairingKeys.key_from_dict(keys_dict, 'csrk') + keys.link_key = PairingKeys.key_from_dict(keys_dict, 'link_key') + + return keys + + def to_dict(self): + keys = {} + + if self.address_type is not None: + keys['address_type'] = self.address_type + + if self.ltk is not None: + keys['ltk'] = self.ltk.to_dict() + + if self.ltk_central is not None: + keys['ltk_central'] = self.ltk_central.to_dict() + + if self.ltk_peripheral is not None: + keys['ltk_peripheral'] = self.ltk_peripheral.to_dict() + + if self.irk is not None: + keys['irk'] = self.irk.to_dict() + + if self.csrk is not None: + keys['csrk'] = self.csrk.to_dict() + + if self.link_key is not None: + keys['link_key'] = self.link_key.to_dict() + + return keys + + def print(self, prefix=''): + keys_dict = self.to_dict() + for (property, value) in keys_dict.items(): + if type(value) is dict: + print(f'{prefix}{color(property, "cyan")}:') + for (key_property, key_value) in value.items(): + print(f'{prefix} {color(key_property, "green")}: {key_value}') + else: + print(f'{prefix}{color(property, "cyan")}: {value}') + + +# ----------------------------------------------------------------------------- +class KeyStore: + async def delete(self, name): + pass + + async def update(self, name, keys): + pass + + async def get(self, name): + return PairingKeys() + + async def get_all(self): + return [] + + async def get_resolving_keys(self): + all_keys = await self.get_all() + resolving_keys = [] + for (name, keys) in all_keys: + if keys.irk is not None: + if keys.address_type is None: + address_type = Address.RANDOM_DEVICE_ADDRESS + else: + address_type = keys.address_type + resolving_keys.append((keys.irk.value, Address(name, address_type))) + + return resolving_keys + + async def print(self, prefix=''): + entries = await self.get_all() + separator = '' + for (name, keys) in entries: + print(separator + prefix + color(name, 'yellow')) + keys.print(prefix = prefix + ' ') + separator = '\n' + + @staticmethod + def create_for_device(device_config): + if device_config.keystore is None: + return None + + keystore_type = device_config.keystore.split(':', 1)[0] + if keystore_type == 'JsonKeyStore': + return JsonKeyStore.from_device_config(device_config) + + return None + + +# ----------------------------------------------------------------------------- +class JsonKeyStore(KeyStore): + APP_NAME = 'Bumble' + APP_AUTHOR = 'Google' + KEYS_DIR = 'Pairing' + DEFAULT_NAMESPACE = '__DEFAULT__' + + def __init__(self, namespace, filename=None): + self.namespace = namespace if namespace is not None else self.DEFAULT_NAMESPACE + + if filename is None: + # Use a default for the current user + import appdirs + self.directory_name = os.path.join( + appdirs.user_data_dir(self.APP_NAME, self.APP_AUTHOR), + self.KEYS_DIR + ) + json_filename = f'{self.namespace}.json'.lower().replace(':', '-') + self.filename = os.path.join(self.directory_name, json_filename) + else: + self.filename = filename + self.directory_name = os.path.dirname(os.path.abspath(self.filename)) + + logger.debug(f'JSON keystore: {self.filename}') + + @staticmethod + def from_device_config(device_config): + params = device_config.keystore.split(':', 1)[1:] + namespace = str(device_config.address) + if params: + filename = params[1] + else: + filename = None + + return JsonKeyStore(namespace, filename) + + async def load(self): + try: + with open(self.filename, 'r') as json_file: + return json.load(json_file) + except FileNotFoundError: + return {} + + async def save(self, db): + # Create the directory if it doesn't exist + if not os.path.exists(self.directory_name): + os.makedirs(self.directory_name, exist_ok=True) + + # Save to a temporary file + temp_filename = self.filename + '.tmp' + with open(temp_filename, 'w') as output: + json.dump(db, output, sort_keys=True, indent=4) + + # Atomically replace the previous file + os.rename(temp_filename, self.filename) + + async def delete(self, name): + db = await self.load() + + namespace = db.get(self.namespace) + if namespace is None: + raise KeyError(name) + + del namespace[name] + await self.save(db) + + async def update(self, name, keys): + db = await self.load() + + namespace = db.setdefault(self.namespace, {}) + namespace[name] = keys.to_dict() + + await self.save(db) + + async def get_all(self): + db = await self.load() + + namespace = db.get(self.namespace) + if namespace is None: + return [] + + return [(name, PairingKeys.from_dict(keys)) for (name, keys) in namespace.items()] + + async def get(self, name): + db = await self.load() + + namespace = db.get(self.namespace) + if namespace is None: + return None + + keys = namespace.get(name) + if keys is None: + return None + + return PairingKeys.from_dict(keys) diff --git a/bumble/l2cap.py b/bumble/l2cap.py new file mode 100644 index 0000000..f506483 --- /dev/null +++ b/bumble/l2cap.py @@ -0,0 +1,1079 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import logging +import struct + +from colors import color + +from .core import BT_CENTRAL_ROLE, InvalidStateError, ProtocolError +from .hci import (HCI_LE_Connection_Update_Command, HCI_Object, key_with_value, + name_or_number) +from .utils import EventEmitter + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +L2CAP_SIGNALING_CID = 0x01 +L2CAP_LE_SIGNALING_CID = 0x05 + +L2CAP_MIN_LE_MTU = 23 +L2CAP_MIN_BR_EDR_MTU = 48 + +L2CAP_DEFAULT_MTU = 2048 # Default value for the MTU we are willing to accept + +# See Bluetooth spec @ Vol 3, Part A - Table 2.1: CID name space on ACL-U, ASB-U, and AMP-U logical links +L2CAP_ACL_U_DYNAMIC_CID_RANGE_START = 0x0040 +L2CAP_ACL_U_DYNAMIC_CID_RANGE_END = 0xFFFF + +# See Bluetooth spec @ Vol 3, Part A - Table 2.2: CID name space on LE-U logical link +L2CAP_LE_U_DYNAMIC_CID_RANGE_START = 0x0040 +L2CAP_LE_U_DYNAMIC_CID_RANGE_START = 0x007F + +# Frame types +L2CAP_COMMAND_REJECT = 0x01 +L2CAP_CONNECTION_REQUEST = 0x02 +L2CAP_CONNECTION_RESPONSE = 0x03 +L2CAP_CONFIGURE_REQUEST = 0x04 +L2CAP_CONFIGURE_RESPONSE = 0x05 +L2CAP_DISCONNECTION_REQUEST = 0x06 +L2CAP_DISCONNECTION_RESPONSE = 0x07 +L2CAP_ECHO_REQUEST = 0x08 +L2CAP_ECHO_RESPONSE = 0x09 +L2CAP_INFORMATION_REQUEST = 0x0A +L2CAP_INFORMATION_RESPONSE = 0x0B +L2CAP_CREATE_CHANNEL_REQUEST = 0x0C +L2CAP_CREATE_CHANNEL_RESPONSE = 0x0D +L2CAP_MOVE_CHANNEL_REQUEST = 0x0E +L2CAP_MOVE_CHANNEL_RESPONSE = 0x0F +L2CAP_MOVE_CHANNEL_CONFIRMATION = 0x10 +L2CAP_MOVE_CHANNEL_CONFIRMATION_RESPONSE = 0x11 +L2CAP_CONNECTION_PARAMETER_UPDATE_REQUEST = 0x12 +L2CAP_CONNECTION_PARAMETER_UPDATE_RESPONSE = 0x13 +L2CAP_LE_CREDIT_BASED_CONNECTION_REQUEST = 0x14 +L2CAP_LE_CREDIT_BASED_CONNECTION_RESPONSE = 0x15 +L2CAP_LE_FLOW_CONTROL_CREDIT = 0x16 + +L2CAP_CONTROL_FRAME_NAMES = { + L2CAP_COMMAND_REJECT: 'L2CAP_COMMAND_REJECT', + L2CAP_CONNECTION_REQUEST: 'L2CAP_CONNECTION_REQUEST', + L2CAP_CONNECTION_RESPONSE: 'L2CAP_CONNECTION_RESPONSE', + L2CAP_CONFIGURE_REQUEST: 'L2CAP_CONFIGURE_REQUEST', + L2CAP_CONFIGURE_RESPONSE: 'L2CAP_CONFIGURE_RESPONSE', + L2CAP_DISCONNECTION_REQUEST: 'L2CAP_DISCONNECTION_REQUEST', + L2CAP_DISCONNECTION_RESPONSE: 'L2CAP_DISCONNECTION_RESPONSE', + L2CAP_ECHO_REQUEST: 'L2CAP_ECHO_REQUEST', + L2CAP_ECHO_RESPONSE: 'L2CAP_ECHO_RESPONSE', + L2CAP_INFORMATION_REQUEST: 'L2CAP_INFORMATION_REQUEST', + L2CAP_INFORMATION_RESPONSE: 'L2CAP_INFORMATION_RESPONSE', + L2CAP_CREATE_CHANNEL_REQUEST: 'L2CAP_CREATE_CHANNEL_REQUEST', + L2CAP_CREATE_CHANNEL_RESPONSE: 'L2CAP_CREATE_CHANNEL_RESPONSE', + L2CAP_MOVE_CHANNEL_REQUEST: 'L2CAP_MOVE_CHANNEL_REQUEST', + L2CAP_MOVE_CHANNEL_RESPONSE: 'L2CAP_MOVE_CHANNEL_RESPONSE', + L2CAP_MOVE_CHANNEL_CONFIRMATION: 'L2CAP_MOVE_CHANNEL_CONFIRMATION', + L2CAP_MOVE_CHANNEL_CONFIRMATION_RESPONSE: 'L2CAP_MOVE_CHANNEL_CONFIRMATION_RESPONSE', + L2CAP_CONNECTION_PARAMETER_UPDATE_REQUEST: 'L2CAP_CONNECTION_PARAMETER_UPDATE_REQUEST', + L2CAP_CONNECTION_PARAMETER_UPDATE_RESPONSE: 'L2CAP_CONNECTION_PARAMETER_UPDATE_RESPONSE', + L2CAP_LE_CREDIT_BASED_CONNECTION_REQUEST: 'L2CAP_LE_CREDIT_BASED_CONNECTION_REQUEST', + L2CAP_LE_CREDIT_BASED_CONNECTION_RESPONSE: 'L2CAP_LE_CREDIT_BASED_CONNECTION_RESPONSE', + L2CAP_LE_FLOW_CONTROL_CREDIT: 'L2CAP_LE_FLOW_CONTROL_CREDIT' +} + +L2CAP_CONNECTION_PARAMETERS_ACCEPTED_RESULT = 0x0000 +L2CAP_CONNECTION_PARAMETERS_REJECTED_RESULT = 0x0001 + +L2CAP_COMMAND_NOT_UNDERSTOOD_REASON = 0x0000 +L2CAP_SIGNALING_MTU_EXCEEDED_REASON = 0x0001 +L2CAP_INVALID_CID_IN_REQUEST_REASON = 0x0002 + +L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU = 2048 +L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2048 + +L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE = 0x01 + +L2CAP_MTU_CONFIGURATION_PARAMETER_TYPE = 0x01 + + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- +class L2CAP_PDU: + ''' + See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT + ''' + + @staticmethod + def from_bytes(data): + # Sanity check + if len(data) < 4: + raise ValueError('not enough data for L2CAP header') + + _, l2cap_pdu_cid = struct.unpack_from('= 2: + type = data[0] + length = data[1] + value = data[2:2 + length] + data = data[2 + length:] + options.append((type, value)) + + return options + + @staticmethod + def encode_configuration_options(options): + return b''.join([bytes([option[0], len(option[1])]) + option[1] for option in options]) + + @staticmethod + def subclass(fields): + def inner(cls): + cls.name = cls.__name__.upper() + cls.code = key_with_value(L2CAP_CONTROL_FRAME_NAMES, cls.name) + if cls.code is None: + raise KeyError(f'Control Frame name {cls.name} not found in L2CAP_CONTROL_FRAME_NAMES') + cls.fields = fields + + # Register a factory for this class + L2CAP_Control_Frame.classes[cls.code] = cls + + return cls + + return inner + + def __init__(self, pdu=None, **kwargs): + self.identifier = kwargs.get('identifier', 0) + if hasattr(self, 'fields') and kwargs: + HCI_Object.init_from_fields(self, self.fields, kwargs) + if pdu is None: + data = HCI_Object.dict_to_bytes(kwargs, self.fields) + pdu = bytes([self.code, self.identifier]) + struct.pack(' 1: + result += f': {self.pdu.hex()}' + return result + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('reason', {'size': 2, 'mapper': lambda x: L2CAP_Command_Reject.map_reason(x)}), + ('data', '*') +]) +class L2CAP_Command_Reject(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.1 COMMAND REJECT + ''' + + COMMAND_NOT_UNDERSTOOD = 0x0000 + SIGNALING_MTU_EXCEEDED = 0x0001 + INVALID_CID_IN_REQUEST = 0x0002 + + REASON_NAMES = { + COMMAND_NOT_UNDERSTOOD: 'COMMAND_NOT_UNDERSTOOD', + SIGNALING_MTU_EXCEEDED: 'SIGNALING_MTU_EXCEEDED', + INVALID_CID_IN_REQUEST: 'INVALID_CID_IN_REQUEST' + } + + @staticmethod + def map_reason(reason): + return name_or_number(L2CAP_Command_Reject.REASON_NAMES, reason) + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('psm', 2), + ('source_cid', 2) +]) +class L2CAP_Connection_Request(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.2 CONNECTION REQUEST + ''' + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('destination_cid', 2), + ('source_cid', 2), + ('result', {'size': 2, 'mapper': lambda x: L2CAP_Connection_Response.result_name(x)}), + ('status', 2) +]) +class L2CAP_Connection_Response(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.3 CONNECTION RESPONSE + ''' + + CONNECTION_SUCCESSFUL = 0x0000 + CONNECTION_PENDING = 0x0001 + CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED = 0x0002 + CONNECTION_REFUSED_SECURITY_BLOCK = 0x0003 + CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE = 0x0004 + CONNECTION_REFUSED_INVALID_SOURCE_CID = 0x0006 + CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x0007 + CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B + + CONNECTION_RESULT_NAMES = { + CONNECTION_SUCCESSFUL: 'CONNECTION_SUCCESSFUL', + CONNECTION_PENDING: 'CONNECTION_PENDING', + CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED: 'CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED', + CONNECTION_REFUSED_SECURITY_BLOCK: 'CONNECTION_REFUSED_SECURITY_BLOCK', + CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE: 'CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE', + CONNECTION_REFUSED_INVALID_SOURCE_CID: 'CONNECTION_REFUSED_INVALID_SOURCE_CID', + CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED: 'CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED', + CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS: 'CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS' + } + + @staticmethod + def result_name(result): + return name_or_number(L2CAP_Connection_Response.CONNECTION_RESULT_NAMES, result) + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('destination_cid', 2), + ('flags', 2), + ('options', '*') +]) +class L2CAP_Configure_Request(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.4 CONFIGURATION REQUEST + ''' + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('source_cid', 2), + ('flags', 2), + ('result', {'size': 2, 'mapper': lambda x: L2CAP_Configure_Response.map_result(x)}), + ('options', '*') +]) +class L2CAP_Configure_Response(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.5 CONFIGURATION RESPONSE + ''' + + SUCCESS = 0x0000 + FAILURE_UNACCEPTABLE_PARAMETERS = 0x0001 + FAILURE_REJECTED = 0x0002 + FAILURE_UNKNOWN_OPTIONS = 0x0003 + PENDING = 0x0004 + FAILURE_FLOW_SPEC_REJECTED = 0x0005 + + RESULT_NAMES = { + SUCCESS: 'SUCCESS', + FAILURE_UNACCEPTABLE_PARAMETERS: 'FAILURE_UNACCEPTABLE_PARAMETERS', + FAILURE_REJECTED: 'FAILURE_REJECTED', + FAILURE_UNKNOWN_OPTIONS: 'FAILURE_UNKNOWN_OPTIONS', + PENDING: 'PENDING', + FAILURE_FLOW_SPEC_REJECTED: 'FAILURE_FLOW_SPEC_REJECTED' + } + + @staticmethod + def map_result(result): + return name_or_number(L2CAP_Configure_Response.RESULT_NAMES, result) + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('destination_cid', 2), + ('source_cid', 2) +]) +class L2CAP_Disconnection_Request(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.6 DISCONNECTION REQUEST + ''' + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('destination_cid', 2), + ('source_cid', 2) +]) +class L2CAP_Disconnection_Response(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.7 DISCONNECTION RESPONSE + ''' + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('data', '*') +]) +class L2CAP_Echo_Request(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.8 ECHO REQUEST + ''' + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('data', '*') +]) +class L2CAP_Echo_Response(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.9 ECHO RESPONSE + ''' + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('info_type', 2) +]) +class L2CAP_Information_Request(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.10 INFORMATION REQUEST + ''' + + SUCCESS = 0x00 + NOT_SUPPORTED = 0x01 + + CONNECTIONLESS_MTU = 0x0001 + EXTENDED_FEATURES_SUPPORTED = 0x0002 + FIXED_CHANNELS_SUPPORTED = 0x0003 + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('info_type', 2), + ('result', 2), + ('data', '*') +]) +class L2CAP_Information_Response(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.11 INFORMATION RESPONSE + ''' + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('interval_min', 2), + ('interval_max', 2), + ('slave_latency', 2), + ('timeout_multiplier', 2) +]) +class L2CAP_Connection_Parameter_Update_Request(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.20 CONNECTION PARAMETER UPDATE REQUEST + ''' + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('result', 2) +]) +class L2CAP_Connection_Parameter_Update_Response(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.21 CONNECTION PARAMETER UPDATE RESPONSE + ''' + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('le_psm', 2), + ('source_cid', 2), + ('mtu', 2), + ('mps', 2), + ('initial_credits', 2) +]) +class L2CAP_LE_Credit_Based_Connection_Request(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.22 LE CREDIT BASED CONNECTION REQUEST (CODE 0x14) + ''' + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('destination_cid', 2), + ('mtu', 2), + ('mps', 2), + ('initial_credits', 2), + ('result', {'size': 2, 'mapper': lambda x: L2CAP_LE_Credit_Based_Connection_Response.map_result(x)}) +]) +class L2CAP_LE_Credit_Based_Connection_Response(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.23 LE CREDIT BASED CONNECTION RESPONSE (CODE 0x15) + ''' + + CONNECTION_SUCCESSFUL = 0x0000 + CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED = 0x0002 + CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE = 0x0004 + CONNECTION_REFUSED_INSUFFICIENT_AUTHENTICATION = 0x0005 + CONNECTION_REFUSED_INSUFFICIENT_AUTHORIZATION = 0x0006 + CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION_KEY_SIZE = 0x0007 + CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION = 0x0008 + CONNECTION_REFUSED_INVALID_SOURCE_CID = 0x0009 + CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED = 0x000A + CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS = 0x000B + + CONNECTION_RESULT_NAMES = { + CONNECTION_SUCCESSFUL: 'CONNECTION_SUCCESSFUL', + CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED: 'CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED', + CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE: 'CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE', + CONNECTION_REFUSED_INSUFFICIENT_AUTHENTICATION: 'CONNECTION_REFUSED_INSUFFICIENT_AUTHENTICATION', + CONNECTION_REFUSED_INSUFFICIENT_AUTHORIZATION: 'CONNECTION_REFUSED_INSUFFICIENT_AUTHORIZATION', + CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION_KEY_SIZE: 'CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION_KEY_SIZE', + CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION: 'CONNECTION_REFUSED_INSUFFICIENT_ENCRYPTION', + CONNECTION_REFUSED_INVALID_SOURCE_CID: 'CONNECTION_REFUSED_INVALID_SOURCE_CID', + CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED: 'CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED', + CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS: 'CONNECTION_REFUSED_UNACCEPTABLE_PARAMETERS' + } + + @staticmethod + def map_result(result): + return name_or_number(L2CAP_LE_Credit_Based_Connection_Response.CONNECTION_RESULT_NAMES, result) + + +# ----------------------------------------------------------------------------- +@L2CAP_Control_Frame.subclass([ + ('cid', 2), + ('credits', 2) +]) +class L2CAP_LE_Flow_Control_Credit(L2CAP_Control_Frame): + ''' + See Bluetooth spec @ Vol 3, Part A - 4.24 LE FLOW CONTROL CREDIT (CODE 0x16) + ''' + + +# ----------------------------------------------------------------------------- +class Channel(EventEmitter): + # States + CLOSED = 0x00 + WAIT_CONNECT = 0x01 + WAIT_CONNECT_RSP = 0x02 + OPEN = 0x03 + WAIT_DISCONNECT = 0x04 + WAIT_CREATE = 0x05 + WAIT_CREATE_RSP = 0x06 + WAIT_MOVE = 0x07 + WAIT_MOVE_RSP = 0x08 + WAIT_MOVE_CONFIRM = 0x09 + WAIT_CONFIRM_RSP = 0x0A + + # CONFIG substates + WAIT_CONFIG = 0x10 + WAIT_SEND_CONFIG = 0x11 + WAIT_CONFIG_REQ_RSP = 0x12 + WAIT_CONFIG_RSP = 0x13 + WAIT_CONFIG_REQ = 0x14 + WAIT_IND_FINAL_RSP = 0x15 + WAIT_FINAL_RSP = 0x16 + WAIT_CONTROL_IND = 0x17 + + STATE_NAMES = { + CLOSED: 'CLOSED', + WAIT_CONNECT: 'WAIT_CONNECT', + WAIT_CONNECT_RSP: 'WAIT_CONNECT_RSP', + OPEN: 'OPEN', + WAIT_DISCONNECT: 'WAIT_DISCONNECT', + WAIT_CREATE: 'WAIT_CREATE', + WAIT_CREATE_RSP: 'WAIT_CREATE_RSP', + WAIT_MOVE: 'WAIT_MOVE', + WAIT_MOVE_RSP: 'WAIT_MOVE_RSP', + WAIT_MOVE_CONFIRM: 'WAIT_MOVE_CONFIRM', + WAIT_CONFIRM_RSP: 'WAIT_CONFIRM_RSP', + + WAIT_CONFIG: 'WAIT_CONFIG', + WAIT_SEND_CONFIG: 'WAIT_SEND_CONFIG', + WAIT_CONFIG_REQ_RSP: 'WAIT_CONFIG_REQ_RSP', + WAIT_CONFIG_RSP: 'WAIT_CONFIG_RSP', + WAIT_CONFIG_REQ: 'WAIT_CONFIG_REQ', + WAIT_IND_FINAL_RSP: 'WAIT_IND_FINAL_RSP', + WAIT_FINAL_RSP: 'WAIT_FINAL_RSP', + WAIT_CONTROL_IND: 'WAIT_CONTROL_IND' + } + + def __init__(self, manager, connection, signaling_cid, psm, source_cid, mtu): + super().__init__() + self.manager = manager + self.connection = connection + self.signaling_cid = signaling_cid + self.state = Channel.CLOSED + self.mtu = mtu + self.psm = psm + self.source_cid = source_cid + self.destination_cid = 0 + self.response = None + self.connection_result = None + self.sink = None + + def change_state(self, new_state): + logger.debug(f'{self} state change -> {color(Channel.STATE_NAMES[new_state], "cyan")}') + self.state = new_state + + def send_pdu(self, pdu): + self.manager.send_pdu(self.connection, self.destination_cid, pdu) + + async def send_request(self, request): + # Check that there isn't already a request pending + if self.response: + raise InvalidStateError('request already pending') + if self.state != Channel.OPEN: + raise InvalidStateError('channel not open') + + self.response = asyncio.get_running_loop().create_future() + self.send_pdu(request) + return await self.response + + def on_pdu(self, pdu): + if self.response: + self.response.set_result(pdu) + self.response = None + elif self.sink: + self.sink(pdu) + else: + logger.warn(color('received pdu without a pending request or sink', 'red')) + + def send_control_frame(self, frame): + self.manager.send_control_frame(self.connection, self.signaling_cid, frame) + + async def connect(self): + if self.state != Channel.CLOSED: + raise InvalidStateError('invalid state') + + self.change_state(Channel.WAIT_CONNECT_RSP) + self.send_control_frame( + L2CAP_Connection_Request( + identifier = self.manager.next_identifier(self.connection), + psm = self.psm, + source_cid = self.source_cid + ) + ) + + # Create a future to wait for the state machine to get to a success or error state + self.connection_result = asyncio.get_running_loop().create_future() + return await self.connection_result + + async def disconnect(self): + if self.state != Channel.OPEN: + raise InvalidStateError('invalid state') + + self.change_state(Channel.WAIT_DISCONNECT) + self.send_control_frame( + L2CAP_Disconnection_Request( + identifier = self.manager.next_identifier(self.connection), + destination_cid = self.destination_cid, + source_cid = self.source_cid + ) + ) + + # Create a future to wait for the state machine to get to a success or error state + self.disconnection_result = asyncio.get_running_loop().create_future() + return await self.disconnection_result + + def send_configure_request(self): + options = L2CAP_Control_Frame.encode_configuration_options([( + L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE, + struct.pack('{self.destination_cid}, PSM={self.psm}, MTU={self.mtu}, state={Channel.STATE_NAMES[self.state]})' + + +# ----------------------------------------------------------------------------- +class ChannelManager: + def __init__(self): + self.host = None + self.channels = {} # Channels, mapped by connection and cid + self.identifiers = {} # Incrementing identifier values by connection + self.servers = {} # Servers accepting connections, by PSM + + def find_channel(self, connection_handle, cid): + if connection_channels := self.channels.get(connection_handle): + return connection_channels.get(cid) + + @staticmethod + def find_free_br_edr_cid(channels): + # Pick the smallest valid CID that's not already in the list + # (not necessarily the most efficient algorithm, but the list of CID is + # very small in practice) + for cid in range(L2CAP_ACL_U_DYNAMIC_CID_RANGE_START, L2CAP_ACL_U_DYNAMIC_CID_RANGE_END + 1): + if cid not in channels: + return cid + + def next_identifier(self, connection): + identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256 + self.identifiers[connection.handle] = identifier + return identifier + + def register_server(self, psm, server): + self.servers[psm] = server + + def send_pdu(self, connection, cid, pdu): + pdu_str = pdu.hex() if type(pdu) is bytes else str(pdu) + logger.debug(f'{color(">>> Sending L2CAP PDU", "blue")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}: {pdu_str}') + self.host.send_l2cap_pdu(connection.handle, cid, bytes(pdu)) + + def on_pdu(self, connection, cid, pdu): + if cid == L2CAP_SIGNALING_CID or cid == L2CAP_LE_SIGNALING_CID: + # Parse the L2CAP payload into a Control Frame object + control_frame = L2CAP_Control_Frame.from_bytes(pdu) + + self.on_control_frame(connection, cid, control_frame) + else: + if (channel := self.find_channel(connection.handle, cid)) is None: + logger.warn(color(f'channel not found for 0x{connection.handle:04X}:{cid}', 'red')) + return + + channel.on_pdu(pdu) + + def send_control_frame(self, connection, cid, control_frame): + logger.debug(f'{color(">>> Sending L2CAP Signaling Control Frame", "blue")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}:\n{control_frame}') + self.host.send_l2cap_pdu(connection.handle, cid, bytes(control_frame)) + + def on_control_frame(self, connection, cid, control_frame): + logger.debug(f'{color("<<< Received L2CAP Signaling Control Frame", "green")} on connection [0x{connection.handle:04X}] (CID={cid}) {connection.peer_address}:\n{control_frame}') + + # Find the handler method + handler_name = f'on_{control_frame.name.lower()}' + handler = getattr(self, handler_name, None) + if handler: + try: + handler(connection, cid, control_frame) + except Exception as error: + logger.warning(f'{color("!!! Exception in handler:", "red")} {error}') + self.send_control_frame( + connection, + cid, + L2CAP_Command_Reject( + identifier = control_frame.identifier, + reason = L2CAP_COMMAND_NOT_UNDERSTOOD_REASON, + data = b'' + ) + ) + raise error + else: + logger.error(color('Channel Manager command not handled???', 'red')) + self.send_control_frame( + connection, + cid, + L2CAP_Command_Reject( + identifier = control_frame.identifier, + reason = L2CAP_COMMAND_NOT_UNDERSTOOD_REASON, + data = b'' + ) + ) + + def on_l2cap_command_reject(self, connection, cid, packet): + logger.warning(f'{color("!!! Command rejected:", "red")} {packet.reason}') + pass + + def on_l2cap_connection_request(self, connection, cid, request): + # Check if there's a server for this PSM + server = self.servers.get(request.psm) + if server: + # Find a free CID for this new channel + connection_channels = self.channels.setdefault(connection.handle, {}) + source_cid = self.find_free_br_edr_cid(connection_channels) + if source_cid is None: # Should never happen! + self.send_control_frame( + connection, + cid, + L2CAP_Connection_Response( + identifier = request.identifier, + destination_cid = request.source_cid, + source_cid = 0, + result = L2CAP_Connection_Response.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE, + status = 0x0000 + ) + ) + return + + # Create a new channel + logger.debug(f'creating server channel with cid={source_cid} for psm {request.psm}') + channel = Channel(self, connection, cid, request.psm, source_cid, L2CAP_MIN_BR_EDR_MTU) + connection_channels[source_cid] = channel + + # Notify + server(channel) + channel.on_connection_request(request) + else: + logger.warn(f'No server for connection 0x{connection.handle:04X} on PSM {request.psm}') + self.send_control_frame( + connection, + cid, + L2CAP_Connection_Response( + identifier = request.identifier, + destination_cid = request.source_cid, + source_cid = 0, + result = L2CAP_Connection_Response.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED, + status = 0x0000 + ) + ) + + def on_l2cap_connection_response(self, connection, cid, response): + if (channel := self.find_channel(connection.handle, response.source_cid)) is None: + logger.warn(color(f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}', 'red')) + return + + channel.on_connection_response(response) + + def on_l2cap_configure_request(self, connection, cid, request): + if (channel := self.find_channel(connection.handle, request.destination_cid)) is None: + logger.warn(color(f'channel {request.destination_cid} not found for 0x{connection.handle:04X}:{cid}', 'red')) + return + + channel.on_configure_request(request) + + def on_l2cap_configure_response(self, connection, cid, response): + if (channel := self.find_channel(connection.handle, response.source_cid)) is None: + logger.warn(color(f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}', 'red')) + return + + channel.on_configure_response(response) + + def on_l2cap_disconnection_request(self, connection, cid, request): + if (channel := self.find_channel(connection.handle, request.destination_cid)) is None: + logger.warn(color(f'channel {request.destination_cid} not found for 0x{connection.handle:04X}:{cid}', 'red')) + return + + channel.on_disconnection_request(request) + + def on_l2cap_disconnection_response(self, connection, cid, response): + if (channel := self.find_channel(connection.handle, response.source_cid)) is None: + logger.warn(color(f'channel {response.source_cid} not found for 0x{connection.handle:04X}:{cid}', 'red')) + return + + channel.on_disconnection_response(response) + + def on_l2cap_echo_request(self, connection, cid, request): + logger.debug(f'<<< Echo request: data={request.data.hex()}') + self.send_control_frame( + connection, + cid, + L2CAP_Echo_Response( + identifier = request.identifier, + data = request.data + ) + ) + + def on_l2cap_echo_response(self, connection, cid, response): + logger.debug(f'<<< Echo response: data={response.data.hex()}') + # TODO notify listeners + + def on_l2cap_information_request(self, connection, cid, request): + if request.info_type == L2CAP_Information_Request.CONNECTIONLESS_MTU: + result = L2CAP_Information_Request.SUCCESS + data = struct.pack(' {le_create_connection_command.peer_address}') + self.pending_connection = (central_address, le_create_connection_command) + asyncio.get_running_loop().call_soon(self.on_connection_complete) + + def on_disconnection_complete(self, central_address, peripheral_address, disconnect_command): + # Find the controller that initiated the disconnection + if not (central_controller := self.find_controller(central_address)): + logger.warning('!!! Initiating controller not found') + return + + # Disconnect from the first controller with a matching address + if peripheral_controller := self.find_controller(peripheral_address): + peripheral_controller.on_link_central_disconnected(central_address, disconnect_command.reason) + + central_controller.on_link_peripheral_disconnection_complete(disconnect_command, HCI_SUCCESS) + + def disconnect(self, central_address, peripheral_address, disconnect_command): + logger.debug(f'$$$ DISCONNECTION {central_address} -> {peripheral_address}: reason = {disconnect_command.reason}') + args = [central_address, peripheral_address, disconnect_command] + asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args) + + def on_connection_encrypted(self, central_address, peripheral_address, rand, ediv, ltk): + logger.debug(f'*** ENCRYPTION {central_address} -> {peripheral_address}') + + if central_controller := self.find_controller(central_address): + central_controller.on_link_encrypted(peripheral_address, rand, ediv, ltk) + + if peripheral_controller := self.find_controller(peripheral_address): + peripheral_controller.on_link_encrypted(central_address, rand, ediv, ltk) + + +# ----------------------------------------------------------------------------- +class RemoteLink: + ''' + A Link implementation that communicates with other virtual controllers via a + WebSocket relay + ''' + def __init__(self, uri): + self.controller = None + self.uri = uri + self.execution_queue = asyncio.Queue() + self.websocket = asyncio.get_running_loop().create_future() + self.rpc_result = None + self.pending_connection = None + self.central_connections = set() # List of addresses that we have connected to + self.peripheral_connections = set() # List of addresses that have connected to us + + # Connect and run asynchronously + asyncio.create_task(self.run_connection()) + asyncio.create_task(self.run_executor_loop()) + + def add_controller(self, controller): + if self.controller: + raise ValueError('controller already set') + self.controller = controller + + def remove_controller(self, controller): + if self.controller != controller: + raise ValueError('controller mismatch') + self.controller = None + + def get_pending_connection(self): + return self.pending_connection + + async def wait_until_connected(self): + await self.websocket + + def execute(self, async_function): + self.execution_queue.put_nowait(async_function()) + + async def run_executor_loop(self): + logger.debug('executor loop starting') + while True: + item = await self.execution_queue.get() + try: + await item + except Exception as error: + logger.warning(f'{color("!!! Exception in async handler:", "red")} {error}') + + async def run_connection(self): + # Connect to the relay + logger.debug(f'connecting to {self.uri}') + websocket = await websockets.connect(self.uri) + self.websocket.set_result(websocket) + logger.debug(f'connected to {self.uri}') + + while True: + message = await websocket.recv() + logger.debug(f'received message: {message}') + keyword, *payload = message.split(':', 1) + + handler_name = f'on_{keyword}_received' + handler = getattr(self, handler_name, None) + if handler: + await handler(payload[0] if payload else None) + + def close(self): + if self.websocket.done(): + logger.debug('closing websocket') + websocket = self.websocket.result() + asyncio.create_task(websocket.close()) + + async def on_result_received(self, result): + if self.rpc_result: + self.rpc_result.set_result(result) + + async def on_left_received(self, address): + if address in self.central_connections: + self.controller.on_link_peripheral_disconnected(Address(address)) + self.central_connections.remove(address) + + if address in self.peripheral_connections: + self.controller.on_link_central_disconnected(address, HCI_CONNECTION_TIMEOUT_ERROR) + self.peripheral_connections.remove(address) + + async def on_unreachable_received(self, target): + await self.on_left_received(target) + + async def on_message_received(self, message): + sender, *payload = message.split('/', 1) + if payload: + keyword, *payload = payload[0].split(':', 1) + handler_name = f'on_{keyword}_message_received' + handler = getattr(self, handler_name, None) + if handler: + await handler(sender, payload[0] if payload else None) + + async def on_advertisement_message_received(self, sender, advertisement): + try: + self.controller.on_link_advertising_data(Address(sender), bytes.fromhex(advertisement)) + except Exception: + logger.exception('exception') + + async def on_acl_message_received(self, sender, acl_data): + try: + self.controller.on_link_acl_data(Address(sender), bytes.fromhex(acl_data)) + except Exception: + logger.exception('exception') + + async def on_connect_message_received(self, sender, _): + # Remember the connection + self.peripheral_connections.add(sender) + + # Notify the controller + logger.debug(f'connection from central {sender}') + self.controller.on_link_central_connected(Address(sender)) + + # Accept the connection by responding to it + await self.send_targetted_message(sender, 'connected') + + async def on_connected_message_received(self, sender, _): + if not self.pending_connection: + logger.warn('received a connection ack, but no connection is pending') + return + + # Remember the connection + self.central_connections.add(sender) + + # Notify the controller + logger.debug(f'connected to peripheral {self.pending_connection.peer_address}') + self.controller.on_link_peripheral_connection_complete(self.pending_connection, HCI_SUCCESS) + + async def on_disconnect_message_received(self, sender, message): + # Notify the controller + params = parse_parameters(message) + reason = int(params.get('reason', str(HCI_CONNECTION_TIMEOUT_ERROR))) + self.controller.on_link_central_disconnected(Address(sender), reason) + + # Forget the connection + if sender in self.peripheral_connections: + self.peripheral_connections.remove(sender) + + async def on_encrypted_message_received(self, sender, message): + # TODO parse params to get real args + self.controller.on_link_encrypted(Address(sender), bytes(8), 0, bytes(16)) + + async def send_rpc_command(self, command): + # Ensure we have a connection + websocket = await self.websocket + + # Create a future value to hold the eventual result + assert(self.rpc_result is None) + self.rpc_result = asyncio.get_running_loop().create_future() + + # Send the command + await websocket.send(command) + + # Wait for the result + rpc_result = await self.rpc_result + self.rpc_result = None + logger.debug(f'rpc_result: {rpc_result}') + + # TODO: parse the result + + async def send_targetted_message(self, target, message): + # Ensure we have a connection + websocket = await self.websocket + + # Send the message + await websocket.send(f'@{target} {message}') + + async def notify_address_changed(self): + await self.send_rpc_command(f'/set-address {self.controller.random_address}') + + def on_address_changed(self, controller): + logger.info(f'address changed for {controller}: {controller.random_address}') + + # Notify the relay of the change + self.execute(self.notify_address_changed) + + async def send_advertising_data_to_relay(self, data): + await self.send_targetted_message('*', f'advertisement:{data.hex()}') + + def send_advertising_data(self, sender_address, data): + self.execute(partial(self.send_advertising_data_to_relay, data)) + + async def send_acl_data_to_relay(self, peer_address, data): + await self.send_targetted_message(peer_address, f'acl:{data.hex()}') + + def send_acl_data(self, sender_address, peer_address, data): + self.execute(partial(self.send_acl_data_to_relay, peer_address, data)) + + async def send_connection_request_to_relay(self, peer_address): + await self.send_targetted_message(peer_address, 'connect') + + def connect(self, central_address, le_create_connection_command): + if self.pending_connection: + logger.warn('connection already pending') + return + self.pending_connection = le_create_connection_command + self.execute(partial(self.send_connection_request_to_relay, str(le_create_connection_command.peer_address))) + + def on_disconnection_complete(self, disconnect_command): + self.controller.on_link_peripheral_disconnection_complete(disconnect_command, HCI_SUCCESS) + + def disconnect(self, central_address, peripheral_address, disconnect_command): + logger.debug(f'disconnect {central_address} -> {peripheral_address}: reason = {disconnect_command.reason}') + self.execute(partial(self.send_targetted_message, peripheral_address, f'disconnect:reason={disconnect_command.reason}')) + asyncio.get_running_loop().call_soon(self.on_disconnection_complete, disconnect_command) + + def on_connection_encrypted(self, central_address, peripheral_address, rand, ediv, ltk): + asyncio.get_running_loop().call_soon(self.controller.on_link_encrypted, peripheral_address, rand, ediv, ltk) + self.execute(partial(self.send_targetted_message, peripheral_address, f'encrypted:ltk={ltk.hex()}')) diff --git a/bumble/rfcomm.py b/bumble/rfcomm.py new file mode 100644 index 0000000..527eaf1 --- /dev/null +++ b/bumble/rfcomm.py @@ -0,0 +1,840 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import asyncio +from colors import color + +from .utils import EventEmitter +from .core import InvalidStateError, ProtocolError, ConnectionError + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +RFCOMM_PSM = 0x0003 + + +# Frame types +RFCOMM_SABM_FRAME = 0x2F # Control field [1,1,1,1,_,1,0,0] LSB-first +RFCOMM_UA_FRAME = 0x63 # Control field [0,1,1,0,_,0,1,1] LSB-first +RFCOMM_DM_FRAME = 0x0F # Control field [1,1,1,1,_,0,0,0] LSB-first +RFCOMM_DISC_FRAME = 0x43 # Control field [0,1,0,_,0,0,1,1] LSB-first +RFCOMM_UIH_FRAME = 0xEF # Control field [1,1,1,_,1,1,1,1] LSB-first +RFCOMM_UI_FRAME = 0x03 # Control field [0,0,0,_,0,0,1,1] LSB-first + +RFCOMM_FRAME_TYPE_NAMES = { + RFCOMM_SABM_FRAME: 'SABM', + RFCOMM_UA_FRAME: 'UA', + RFCOMM_DM_FRAME: 'DM', + RFCOMM_DISC_FRAME: 'DISC', + RFCOMM_UIH_FRAME: 'UIH', + RFCOMM_UI_FRAME: 'UI' +} + +# MCC Types +RFCOMM_MCC_PN_TYPE = 0x20 +RFCOMM_MCC_MSC_TYPE = 0x38 + +# FCS CRC +CRC_TABLE = bytes([ + 0X00, 0X91, 0XE3, 0X72, 0X07, 0X96, 0XE4, 0X75, + 0X0E, 0X9F, 0XED, 0X7C, 0X09, 0X98, 0XEA, 0X7B, + 0X1C, 0X8D, 0XFF, 0X6E, 0X1B, 0X8A, 0XF8, 0X69, + 0X12, 0X83, 0XF1, 0X60, 0X15, 0X84, 0XF6, 0X67, + 0X38, 0XA9, 0XDB, 0X4A, 0X3F, 0XAE, 0XDC, 0X4D, + 0X36, 0XA7, 0XD5, 0X44, 0X31, 0XA0, 0XD2, 0X43, + 0X24, 0XB5, 0XC7, 0X56, 0X23, 0XB2, 0XC0, 0X51, + 0X2A, 0XBB, 0XC9, 0X58, 0X2D, 0XBC, 0XCE, 0X5F, + 0X70, 0XE1, 0X93, 0X02, 0X77, 0XE6, 0X94, 0X05, + 0X7E, 0XEF, 0X9D, 0X0C, 0X79, 0XE8, 0X9A, 0X0B, + 0X6C, 0XFD, 0X8F, 0X1E, 0X6B, 0XFA, 0X88, 0X19, + 0X62, 0XF3, 0X81, 0X10, 0X65, 0XF4, 0X86, 0X17, + 0X48, 0XD9, 0XAB, 0X3A, 0X4F, 0XDE, 0XAC, 0X3D, + 0X46, 0XD7, 0XA5, 0X34, 0X41, 0XD0, 0XA2, 0X33, + 0X54, 0XC5, 0XB7, 0X26, 0X53, 0XC2, 0XB0, 0X21, + 0X5A, 0XCB, 0XB9, 0X28, 0X5D, 0XCC, 0XBE, 0X2F, + 0XE0, 0X71, 0X03, 0X92, 0XE7, 0X76, 0X04, 0X95, + 0XEE, 0X7F, 0X0D, 0X9C, 0XE9, 0X78, 0X0A, 0X9B, + 0XFC, 0X6D, 0X1F, 0X8E, 0XFB, 0X6A, 0X18, 0X89, + 0XF2, 0X63, 0X11, 0X80, 0XF5, 0X64, 0X16, 0X87, + 0XD8, 0X49, 0X3B, 0XAA, 0XDF, 0X4E, 0X3C, 0XAD, + 0XD6, 0X47, 0X35, 0XA4, 0XD1, 0X40, 0X32, 0XA3, + 0XC4, 0X55, 0X27, 0XB6, 0XC3, 0X52, 0X20, 0XB1, + 0XCA, 0X5B, 0X29, 0XB8, 0XCD, 0X5C, 0X2E, 0XBF, + 0X90, 0X01, 0X73, 0XE2, 0X97, 0X06, 0X74, 0XE5, + 0X9E, 0X0F, 0X7D, 0XEC, 0X99, 0X08, 0X7A, 0XEB, + 0X8C, 0X1D, 0X6F, 0XFE, 0X8B, 0X1A, 0X68, 0XF9, + 0X82, 0X13, 0X61, 0XF0, 0X85, 0X14, 0X66, 0XF7, + 0XA8, 0X39, 0X4B, 0XDA, 0XAF, 0X3E, 0X4C, 0XDD, + 0XA6, 0X37, 0X45, 0XD4, 0XA1, 0X30, 0X42, 0XD3, + 0XB4, 0X25, 0X57, 0XC6, 0XB3, 0X22, 0X50, 0XC1, + 0XBA, 0X2B, 0X59, 0XC8, 0XBD, 0X2C, 0X5E, 0XCF +]) + +RFCOMM_DEFAULT_INITIAL_RX_CREDITS = 7 +RFCOMM_DEFAULT_PREFERRED_MTU = 1280 + +RFCOMM_DYNAMIC_CHANNEL_NUMBER_START = 1 +RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30 + + +# ----------------------------------------------------------------------------- +def fcs(buffer): + fcs = 0xFF + for byte in buffer: + fcs = CRC_TABLE[fcs ^ byte] + return 0xFF - fcs + + +# ----------------------------------------------------------------------------- +class RFCOMM_Frame: + def __init__(self, type, c_r, dlci, p_f, information = b'', with_credits = False): + self.type = type + self.c_r = c_r + self.dlci = dlci + self.p_f = p_f + self.information = information + length = len(information) + if with_credits: + length -= 1 + if length > 0x7F: + # 2-byte length indicator + self.length = bytes([(length & 0x7F) << 1, (length >> 7) & 0xFF]) + else: + # 1-byte length indicator + self.length = bytes([(length << 1) | 1]) + self.address = (dlci << 2) | (c_r << 1) | 1 + self.control = type | (p_f << 4) + if type == RFCOMM_UIH_FRAME: + self.fcs = fcs(bytes([self.address, self.control])) + else: + self.fcs = fcs(bytes([self.address, self.control]) + self.length) + + def type_name(self): + return RFCOMM_FRAME_TYPE_NAMES[self.type] + + @staticmethod + def parse_mcc(data): + type = data[0] >> 2 + c_r = (data[0] >> 1) & 1 + length = data[1] + if data[1] & 1: + length >>= 1 + value = data[2:] + else: + length = (data[3] << 7) & (length >> 1) + value = data[3:3 + length] + + return (type, c_r, value) + + @staticmethod + def make_mcc(type, c_r, data): + return bytes([(type << 2 | c_r << 1 | 1) & 0xFF, (len(data) & 0x7F) << 1 | 1]) + data + + @staticmethod + def sabm(c_r, dlci): + return RFCOMM_Frame(RFCOMM_SABM_FRAME, c_r, dlci, 1) + + @staticmethod + def ua(c_r, dlci): + return RFCOMM_Frame(RFCOMM_UA_FRAME, c_r, dlci, 1) + + @staticmethod + def dm(c_r, dlci): + return RFCOMM_Frame(RFCOMM_DM_FRAME, c_r, dlci, 1) + + @staticmethod + def disc(c_r, dlci): + return RFCOMM_Frame(RFCOMM_DISC_FRAME, c_r, dlci, 1) + + @staticmethod + def uih(c_r, dlci, information, p_f = 0): + return RFCOMM_Frame(RFCOMM_UIH_FRAME, c_r, dlci, p_f, information, with_credits = (p_f == 1)) + + @staticmethod + def from_bytes(data): + # Extract fields + dlci = (data[0] >> 2) & 0x3F + c_r = (data[0] >> 1) & 0x01 + type = data[1] & 0xEF + p_f = (data[1] >> 4) & 0x01 + length = data[2] + if length & 0x01: + length >>= 1 + information = data[3:-1] + else: + length = (data[3] << 7) & (length >> 1) + information = data[4:-1] + fcs = data[-1] + + # Construct the frame and check the CRC + frame = RFCOMM_Frame(type, c_r, dlci, p_f, information) + if frame.fcs != fcs: + logger.warn(f'FCS mismatch: got {fcs:02X}, expected {frame.fcs:02X}') + raise ValueError('fcs mismatch') + + return frame + + def __bytes__(self): + return bytes([self.address, self.control]) + self.length + self.information + bytes([self.fcs]) + + def __str__(self): + return f'{color(self.type_name(), "yellow")}(c/r={self.c_r},dlci={self.dlci},p/f={self.p_f},length={len(self.information)},fcs=0x{self.fcs:02X})' + + +# ----------------------------------------------------------------------------- +class RFCOMM_MCC_PN: + def __init__(self, dlci, cl, priority, ack_timer, max_frame_size, max_retransmissions, window_size): + self.dlci = dlci + self.cl = cl + self.priority = priority + self.ack_timer = ack_timer + self.max_frame_size = max_frame_size + self.max_retransmissions = max_retransmissions + self.window_size = window_size + + @staticmethod + def from_bytes(data): + return RFCOMM_MCC_PN( + dlci = data[0], + cl = data[1], + priority = data[2], + ack_timer = data[3], + max_frame_size = data[4] | data[5] << 8, + max_retransmissions = data[6], + window_size = data[7] + ) + + def __bytes__(self): + return bytes([ + self.dlci & 0xFF, + self.cl & 0xFF, + self.priority & 0xFF, + self.ack_timer & 0xFF, + self.max_frame_size & 0xFF, + (self.max_frame_size >> 8) & 0xFF, + self.max_retransmissions & 0xFF, + self.window_size & 0xFF + ]) + + def __str__(self): + return f'PN(dlci={self.dlci},cl={self.cl},priority={self.priority},ack_timer={self.ack_timer},max_frame_size={self.max_frame_size},max_retransmissions={self.max_retransmissions},window_size={self.window_size})' + + +# ----------------------------------------------------------------------------- +class RFCOMM_MCC_MSC: + def __init__(self, dlci, fc, rtc, rtr, ic, dv): + self.dlci = dlci + self.fc = fc + self.rtc = rtc + self.rtr = rtr + self.ic = ic + self.dv = dv + + @staticmethod + def from_bytes(data): + return RFCOMM_MCC_MSC( + dlci = data[0] >> 2, + fc = data[1] >> 1 & 1, + rtc = data[1] >> 2 & 1, + rtr = data[1] >> 3 & 1, + ic = data[1] >> 6 & 1, + dv = data[1] >> 7 & 1 + ) + + def __bytes__(self): + return bytes([ + (self.dlci << 2) | 3, + 1 | self.fc << 1 | self.rtc << 2 | self.rtr << 3 | self.ic << 6 | self.dv << 7 + ]) + + def __str__(self): + return f'MSC(dlci={self.dlci},fc={self.fc},rtc={self.rtc},rtr={self.rtr},ic={self.ic},dv={self.dv})' + + +# ----------------------------------------------------------------------------- +class DLC(EventEmitter): + # States + INIT = 0x00 + CONNECTING = 0x01 + CONNECTED = 0x02 + DISCONNECTING = 0x03 + DISCONNECTED = 0x04 + RESET = 0x05 + + STATE_NAMES = { + INIT: 'INIT', + CONNECTING: 'CONNECTING', + CONNECTED: 'CONNECTED', + DISCONNECTING: 'DISCONNECTING', + DISCONNECTED: 'DISCONNECTED', + RESET: 'RESET' + } + + def __init__(self, multiplexer, dlci, max_frame_size, initial_tx_credits): + super().__init__() + self.multiplexer = multiplexer + self.dlci = dlci + self.rx_credits = RFCOMM_DEFAULT_INITIAL_RX_CREDITS + self.rx_threshold = self.rx_credits // 2 + self.tx_credits = initial_tx_credits + self.tx_buffer = b'' + self.state = DLC.INIT + self.role = multiplexer.role + self.c_r = 1 if self.role == Multiplexer.INITIATOR else 0 + self.sink = None + + # Compute the MTU + max_overhead = 4 + 1 # header with 2-byte length + fcs + self.mtu = min(max_frame_size, self.multiplexer.l2cap_channel.mtu - max_overhead) + + @staticmethod + def state_name(state): + return DLC.STATE_NAMES[state] + + def change_state(self, new_state): + logger.debug(f'{self} state change -> {color(self.state_name(new_state), "magenta")}') + self.state = new_state + + def send_frame(self, frame): + self.multiplexer.send_frame(frame) + + def on_frame(self, frame): + handler = getattr(self, f'on_{frame.type_name()}_frame'.lower()) + handler(frame) + + def on_sabm_frame(self, frame): + if self.state != DLC.CONNECTING: + logger.warn(color('!!! received SABM when not in CONNECTING state', 'red')) + return + + self.send_frame(RFCOMM_Frame.ua(c_r = 1 - self.c_r, dlci = self.dlci)) + + # Exchange the modem status with the peer + msc = RFCOMM_MCC_MSC( + dlci = self.dlci, + fc = 0, + rtc = 1, + rtr = 1, + ic = 0, + dv = 1 + ) + mcc = RFCOMM_Frame.make_mcc(type = RFCOMM_MCC_MSC_TYPE, c_r = 1, data = bytes(msc)) + logger.debug(f'>>> MCC MSC Command: {msc}') + self.send_frame( + RFCOMM_Frame.uih( + c_r = self.c_r, + dlci = 0, + information = mcc + ) + ) + + self.change_state(DLC.CONNECTED) + self.emit('open') + + def on_ua_frame(self, frame): + if self.state != DLC.CONNECTING: + logger.warn(color('!!! received SABM when not in CONNECTING state', 'red')) + return + + # Exchange the modem status with the peer + msc = RFCOMM_MCC_MSC( + dlci = self.dlci, + fc = 0, + rtc = 1, + rtr = 1, + ic = 0, + dv = 1 + ) + mcc = RFCOMM_Frame.make_mcc(type = RFCOMM_MCC_MSC_TYPE, c_r = 1, data = bytes(msc)) + logger.debug(f'>>> MCC MSC Command: {msc}') + self.send_frame( + RFCOMM_Frame.uih( + c_r = self.c_r, + dlci = 0, + information = mcc + ) + ) + + self.change_state(DLC.CONNECTED) + self.multiplexer.on_dlc_open_complete(self) + + def on_dm_frame(self, frame): + # TODO: handle all states + pass + + def on_disc_frame(self, frame): + # TODO: handle all states + self.send_frame(RFCOMM_Frame.ua(c_r = 1 - self.c_r, dlci = self.dlci)) + + def on_uih_frame(self, frame): + data = frame.information + if frame.p_f == 1: + # With credits + credits = frame.information[0] + self.tx_credits += credits + + logger.debug(f'<<< Credits [{self.dlci}]: received {credits}, total={self.tx_credits}') + data = data[1:] + + logger.debug(f'{color("<<< Data", "yellow")} [{self.dlci}] {len(data)} bytes, rx_credits={self.rx_credits}: {data.hex()}') + if len(data) and self.sink: + self.sink(data) + + # Update the credits + if self.rx_credits > 0: + self.rx_credits -= 1 + else: + logger.warn(color('!!! received frame with no rx credits', 'red')) + + # Check if there's anything to send (including credits) + self.process_tx() + + def on_ui_frame(self, frame): + pass + + def on_mcc_msc(self, c_r, msc): + if c_r: + # Command + logger.debug(f'<<< MCC MSC Command: {msc}') + msc = RFCOMM_MCC_MSC( + dlci = self.dlci, + fc = 0, + rtc = 1, + rtr = 1, + ic = 0, + dv = 1 + ) + mcc = RFCOMM_Frame.make_mcc(type = RFCOMM_MCC_MSC_TYPE, c_r = 0, data = bytes(msc)) + logger.debug(f'>>> MCC MSC Response: {msc}') + self.send_frame( + RFCOMM_Frame.uih( + c_r = self.c_r, + dlci = 0, + information = mcc + ) + ) + else: + # Response + logger.debug(f'<<< MCC MSC Response: {msc}') + + def connect(self): + if not self.state == DLC.INIT: + raise InvalidStateError('invalid state') + + self.change_state(DLC.CONNECTING) + self.connection_result = asyncio.get_running_loop().create_future() + self.send_frame( + RFCOMM_Frame.sabm( + c_r = self.c_r, + dlci = self.dlci + ) + ) + + def accept(self): + if not self.state == DLC.INIT: + raise InvalidStateError('invalid state') + + pn = RFCOMM_MCC_PN( + dlci = self.dlci, + cl = 0xE0, + priority = 7, + ack_timer = 0, + max_frame_size = RFCOMM_DEFAULT_PREFERRED_MTU, + max_retransmissions = 0, + window_size = RFCOMM_DEFAULT_INITIAL_RX_CREDITS + ) + mcc = RFCOMM_Frame.make_mcc(type = RFCOMM_MCC_PN_TYPE, c_r = 0, data = bytes(pn)) + logger.debug(f'>>> PN Response: {pn}') + self.send_frame( + RFCOMM_Frame.uih( + c_r = self.c_r, + dlci = 0, + information = mcc + ) + ) + self.change_state(DLC.CONNECTING) + + def rx_credits_needed(self): + if self.rx_credits <= self.rx_threshold: + return RFCOMM_DEFAULT_INITIAL_RX_CREDITS - self.rx_credits + else: + return 0 + + def process_tx(self): + # Send anything we can (or an empty frame if we need to send rx credits) + rx_credits_needed = self.rx_credits_needed() + while (self.tx_buffer and self.tx_credits > 0) or rx_credits_needed > 0: + # Get the next chunk, up to MTU size + if rx_credits_needed > 0: + chunk = bytes([rx_credits_needed]) + self.tx_buffer[:self.mtu - 1] + self.tx_buffer = self.tx_buffer[len(chunk) - 1:] + self.rx_credits += rx_credits_needed + tx_credit_spent = (len(chunk) > 1) + else: + chunk = self.tx_buffer[:self.mtu] + self.tx_buffer = self.tx_buffer[len(chunk):] + tx_credit_spent = True + + # Update the tx credits + # (no tx credit spent for empty frames that only contain rx credits) + if tx_credit_spent: + self.tx_credits -= 1 + + # Send the frame + logger.debug(f'>>> sending {len(chunk)} bytes with {rx_credits_needed} credits, rx_credits={self.rx_credits}, tx_credits={self.tx_credits}') + self.send_frame( + RFCOMM_Frame.uih( + c_r = self.c_r, + dlci = self.dlci, + information = chunk, + p_f = 1 if rx_credits_needed > 0 else 0 + ) + ) + + rx_credits_needed = 0 + + # Stream protocol + def write(self, data): + # We can only send bytes + if type(data) != bytes: + if type(data) == str: + # Automatically convert strings to bytes using UTF-8 + data = data.encode('utf-8') + else: + raise ValueError('write only accept bytes or strings') + + self.tx_buffer += data + self.process_tx() + + def drain(self): + # TODO + pass + + def __str__(self): + return f'DLC(dlci={self.dlci},state={self.state_name(self.state)})' + + +# ----------------------------------------------------------------------------- +class Multiplexer(EventEmitter): + # Roles + INITIATOR = 0x00 + RESPONDER = 0x01 + + # States + INIT = 0x00 + CONNECTING = 0x01 + CONNECTED = 0x02 + OPENING = 0x03 + DISCONNECTING = 0x04 + DISCONNECTED = 0x05 + RESET = 0x06 + + STATE_NAMES = { + INIT: 'INIT', + CONNECTING: 'CONNECTING', + CONNECTED: 'CONNECTED', + OPENING: 'OPENING', + DISCONNECTING: 'DISCONNECTING', + DISCONNECTED: 'DISCONNECTED', + RESET: 'RESET' + } + + def __init__(self, l2cap_channel, role): + super().__init__() + self.role = role + self.l2cap_channel = l2cap_channel + self.state = Multiplexer.INIT + self.dlcs = {} # DLCs, by DLCI + self.connection_result = None + self.disconnection_result = None + self.open_result = None + self.acceptor = None + + # Become a sink for the L2CAP channel + l2cap_channel.sink = self.on_pdu + + @staticmethod + def state_name(state): + return Multiplexer.STATE_NAMES[state] + + def change_state(self, new_state): + logger.debug(f'{self} state change -> {color(self.state_name(new_state), "cyan")}') + self.state = new_state + + def send_frame(self, frame): + logger.debug(f'>>> Multiplexer sending {frame}') + self.l2cap_channel.send_pdu(frame) + + def on_pdu(self, pdu): + frame = RFCOMM_Frame.from_bytes(pdu) + logger.debug(f'<<< Multiplexer received {frame}') + + # Dispatch to this multiplexer or to a dlc, depending on the address + if frame.dlci == 0: + self.on_frame(frame) + else: + if frame.type == RFCOMM_DM_FRAME: + # DM responses are for a DLCI, but since we only create the dlc when we receive + # a PN response (because we need the parameters), we handle DM frames at the Multiplexer + # level + self.on_dm_frame(frame) + else: + dlc = self.dlcs.get(frame.dlci) + if dlc is None: + logger.warn(f'no dlc for DLCI {frame.dlci}') + return + dlc.on_frame(frame) + + def on_frame(self, frame): + handler = getattr(self, f'on_{frame.type_name()}_frame'.lower()) + handler(frame) + + def on_sabm_frame(self, frame): + if self.state != Multiplexer.INIT: + logger.debug('not in INIT state, ignoring SABM') + return + self.change_state(Multiplexer.CONNECTED) + self.send_frame(RFCOMM_Frame.ua(c_r = 1, dlci = 0)) + + def on_ua_frame(self, frame): + if self.state == Multiplexer.CONNECTING: + self.change_state(Multiplexer.CONNECTED) + if self.connection_result: + self.connection_result.set_result(0) + self.connection_result = None + elif self.state == Multiplexer.DISCONNECTING: + self.change_state(Multiplexer.DISCONNECTED) + if self.disconnection_result: + self.disconnection_result.set_result(None) + self.disconnection_result = None + + def on_dm_frame(self, frame): + if self.state == Multiplexer.OPENING: + self.change_state(Multiplexer.CONNECTED) + if self.open_result: + self.open_result.set_exception(ConnectionError(ConnectionError.CONNECTION_REFUSED)) + else: + logger.warn(f'unexpected state for DM: {self}') + + def on_disc_frame(self, frame): + self.change_state(Multiplexer.DISCONNECTED) + self.send_frame(RFCOMM_Frame.ua(c_r = 0 if self.role == Multiplexer.INITIATOR else 1, dlci = 0)) + + def on_uih_frame(self, frame): + (type, c_r, value) = RFCOMM_Frame.parse_mcc(frame.information) + + if type == RFCOMM_MCC_PN_TYPE: + pn = RFCOMM_MCC_PN.from_bytes(value) + self.on_mcc_pn(c_r, pn) + elif type == RFCOMM_MCC_MSC_TYPE: + mcs = RFCOMM_MCC_MSC.from_bytes(value) + self.on_mcc_msc(c_r, mcs) + + def on_ui_frame(self, frame): + pass + + def on_mcc_pn(self, c_r, pn): + if c_r == 1: + # Command + logger.debug(f'<<< PN Command: {pn}') + + # Check with the multiplexer if there's an acceptor for this channel + if pn.dlci & 1: + # Not expected, this is an initiator-side number + # TODO: error out + logger.warn(f'invalid DLCI: {pn.dlci}') + else: + if self.acceptor: + channel_number = pn.dlci >> 1 + if self.acceptor(channel_number): + # Create a new DLC + dlc = DLC(self, pn.dlci, pn.max_frame_size, pn.window_size) + self.dlcs[pn.dlci] = dlc + + # Re-emit the handshake completion event + dlc.on('open', lambda: self.emit('dlc', dlc)) + + # Respond to complete the handshake + dlc.accept() + else: + # No acceptor, we're in Disconnected Mode + self.send_frame(RFCOMM_Frame.dm(c_r = 1, dlci = pn.dlci)) + else: + # No acceptor?? shouldn't happen + logger.warn(color('!!! no acceptor registered', 'red')) + else: + # Response + logger.debug(f'>>> PN Response: {pn}') + if self.state == Multiplexer.OPENING: + dlc = DLC(self, pn.dlci, pn.max_frame_size, pn.window_size) + self.dlcs[pn.dlci] = dlc + dlc.connect() + else: + logger.warn('ignoring PN response') + + def on_mcc_msc(self, c_r, msc): + dlc = self.dlcs.get(msc.dlci) + if dlc is None: + logger.warn(f'no dlc for DLCI {msc.dlci}') + return + dlc.on_mcc_msc(c_r, msc) + + async def connect(self): + if self.state != Multiplexer.INIT: + raise InvalidStateError('invalid state') + + self.change_state(Multiplexer.CONNECTING) + self.connection_result = asyncio.get_running_loop().create_future() + self.send_frame(RFCOMM_Frame.sabm(c_r = 1, dlci = 0)) + return await self.connection_result + + async def disconnect(self): + if self.state != Multiplexer.CONNECTED: + return + + self.disconnection_result = asyncio.get_running_loop().create_future() + self.change_state(Multiplexer.DISCONNECTING) + self.send_frame(RFCOMM_Frame.disc(c_r = 1 if self.role == Multiplexer.INITIATOR else 0, dlci = 0)) + await self.disconnection_result + + async def open_dlc(self, channel): + if self.state != Multiplexer.CONNECTED: + if self.state == Multiplexer.OPENING: + raise InvalidStateError('open already in progress') + else: + raise InvalidStateError('not connected') + + pn = RFCOMM_MCC_PN( + dlci = channel << 1, + cl = 0xF0, + priority = 7, + ack_timer = 0, + max_frame_size = RFCOMM_DEFAULT_PREFERRED_MTU, + max_retransmissions = 0, + window_size = RFCOMM_DEFAULT_INITIAL_RX_CREDITS + ) + mcc = RFCOMM_Frame.make_mcc(type = RFCOMM_MCC_PN_TYPE, c_r = 1, data = bytes(pn)) + logger.debug(f'>>> Sending MCC: {pn}') + self.open_result = asyncio.get_running_loop().create_future() + self.change_state(Multiplexer.OPENING) + self.send_frame( + RFCOMM_Frame.uih( + c_r = 1 if self.role == Multiplexer.INITIATOR else 0, + dlci = 0, + information = mcc + ) + ) + result = await self.open_result + self.open_result = None + return result + + def on_dlc_open_complete(self, dlc): + logger.debug(f'DLC [{dlc.dlci}] open complete') + self.change_state(Multiplexer.CONNECTED) + if self.open_result: + self.open_result.set_result(dlc) + + def __str__(self): + return f'Multiplexer(state={self.state_name(self.state)})' + + +# ----------------------------------------------------------------------------- +class Client: + def __init__(self, device, connection): + self.device = device + self.connection = connection + self.l2cap_channel = None + self.multiplexer = None + + async def start(self): + # Create a new L2CAP connection + try: + self.l2cap_channel = await self.device.l2cap_channel_manager.connect(self.connection, RFCOMM_PSM) + except ProtocolError as error: + logger.warn(f'L2CAP connection failed: {error}') + raise + + # Create a mutliplexer to manage DLCs with the server + self.multiplexer = Multiplexer(self.l2cap_channel, Multiplexer.INITIATOR) + + # Connect the multiplexer + await self.multiplexer.connect() + + return self.multiplexer + + async def shutdown(self): + # Disconnect the multiplexer + await self.multiplexer.disconnect() + self.multiplexer = None + + # Close the L2CAP channel + # TODO + + +# ----------------------------------------------------------------------------- +class Server(EventEmitter): + def __init__(self, device): + super().__init__() + self.device = device + self.multiplexer = None + self.acceptors = {} + + # Register ourselves with the L2CAP channel manager + device.register_l2cap_server(RFCOMM_PSM, self.on_connection) + + def listen(self, acceptor): + # Find a free channel number + for channel in range(RFCOMM_DYNAMIC_CHANNEL_NUMBER_START, RFCOMM_DYNAMIC_CHANNEL_NUMBER_END + 1): + if channel not in self.acceptors: + self.acceptors[channel] = acceptor + return channel + + # All channels used... + return 0 + + def on_connection(self, l2cap_channel): + logger.debug(f'+++ new L2CAP connection: {l2cap_channel}') + l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel)) + + def on_l2cap_channel_open(self, l2cap_channel): + logger.debug(f'$$$ L2CAP channel open: {l2cap_channel}') + + # Create a new multiplexer for the channel + multiplexer = Multiplexer(l2cap_channel, Multiplexer.RESPONDER) + multiplexer.acceptor = self.accept_dlc + multiplexer.on('dlc', self.on_dlc) + + # Notify + self.emit('start', multiplexer) + + def accept_dlc(self, channel_number): + return channel_number in self.acceptors + + def on_dlc(self, dlc): + logger.debug(f'@@@ new DLC connected: {dlc}') + + # Let the acceptor know + acceptor = self.acceptors.get(dlc.dlci >> 1) + if acceptor: + acceptor(dlc) diff --git a/bumble/sdp.py b/bumble/sdp.py new file mode 100644 index 0000000..935561e --- /dev/null +++ b/bumble/sdp.py @@ -0,0 +1,1021 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import struct +from colors import color +import colors + +from . import core +from .core import InvalidStateError +from .hci import HCI_Object, name_or_number, key_with_value + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +SDP_CONTINUATION_WATCHDOG = 64 # Maximum number of continuations we're willing to do + +SDP_PSM = 0x0001 + +SDP_ERROR_RESPONSE = 0x01 +SDP_SERVICE_SEARCH_REQUEST = 0x02 +SDP_SERVICE_SEARCH_RESPONSE = 0x03 +SDP_SERVICE_ATTRIBUTE_REQUEST = 0x04 +SDP_SERVICE_ATTRIBUTE_RESPONSE = 0x05 +SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST = 0x06 +SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE = 0x07 + +SDP_PDU_NAMES = { + SDP_ERROR_RESPONSE: 'SDP_ERROR_RESPONSE', + SDP_SERVICE_SEARCH_REQUEST: 'SDP_SERVICE_SEARCH_REQUEST', + SDP_SERVICE_SEARCH_RESPONSE: 'SDP_SERVICE_SEARCH_RESPONSE', + SDP_SERVICE_ATTRIBUTE_REQUEST: 'SDP_SERVICE_ATTRIBUTE_REQUEST', + SDP_SERVICE_ATTRIBUTE_RESPONSE: 'SDP_SERVICE_ATTRIBUTE_RESPONSE', + SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST: 'SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST', + SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE: 'SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE' +} + +SDP_INVALID_SDP_VERSION_ERROR = 0x0001 +SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR = 0x0002 +SDP_INVALID_REQUEST_SYNTAX_ERROR = 0x0003 +SDP_INVALID_PDU_SIZE_ERROR = 0x0004 +SDP_INVALID_CONTINUATION_STATE_ERROR = 0x0005 +SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR = 0x0006 + +SDP_ERROR_NAMES = { + SDP_INVALID_SDP_VERSION_ERROR: 'SDP_INVALID_SDP_VERSION_ERROR', + SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR: 'SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR', + SDP_INVALID_REQUEST_SYNTAX_ERROR: 'SDP_INVALID_REQUEST_SYNTAX_ERROR', + SDP_INVALID_PDU_SIZE_ERROR: 'SDP_INVALID_PDU_SIZE_ERROR', + SDP_INVALID_CONTINUATION_STATE_ERROR: 'SDP_INVALID_CONTINUATION_STATE_ERROR', + SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR: 'SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR' +} + +SDP_SERVICE_NAME_ATTRIBUTE_ID_OFFSET = 0x0000 +SDP_SERVICE_DESCRIPTION_ATTRIBUTE_ID_OFFSET = 0x0001 +SDP_PROVIDER_NAME_ATTRIBUTE_ID_OFFSET = 0x0002 + +SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID = 0X0000 +SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID = 0X0001 +SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID = 0X0002 +SDP_SERVICE_ID_ATTRIBUTE_ID = 0X0003 +SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0X0004 +SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID = 0X0005 +SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID = 0X0006 +SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID = 0X0007 +SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID = 0X0008 +SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0X0009 +SDP_DOCUMENTATION_URL_ATTRIBUTE_ID = 0X000A +SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID = 0X000B +SDP_ICON_URL_ATTRIBUTE_ID = 0X000C +SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID = 0X000D + +SDP_ATTRIBUTE_ID_NAMES = { + SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID: 'SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID', + SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID: 'SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID', + SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID: 'SDP_SERVICE_RECORD_STATE_ATTRIBUTE_ID', + SDP_SERVICE_ID_ATTRIBUTE_ID: 'SDP_SERVICE_ID_ATTRIBUTE_ID', + SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID: 'SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID', + SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID: 'SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID', + SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID: 'SDP_LANGUAGE_BASE_ATTRIBUTE_ID_LIST_ATTRIBUTE_ID', + SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID: 'SDP_SERVICE_INFO_TIME_TO_LIVE_ATTRIBUTE_ID', + SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID: 'SDP_SERVICE_AVAILABILITY_ATTRIBUTE_ID', + SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID: 'SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID', + SDP_DOCUMENTATION_URL_ATTRIBUTE_ID: 'SDP_DOCUMENTATION_URL_ATTRIBUTE_ID', + SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID: 'SDP_CLIENT_EXECUTABLE_URL_ATTRIBUTE_ID', + SDP_ICON_URL_ATTRIBUTE_ID: 'SDP_ICON_URL_ATTRIBUTE_ID', + SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID: 'SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID' +} + +SDP_PUBLIC_BROWSE_ROOT = core.UUID.from_16_bits(0x1002, 'PublicBrowseRoot') + +# To be used in searches where an attribute ID list allows a range to be specified +SDP_ALL_ATTRIBUTES_RANGE = (0x0000FFFF, 4) # Express this as tuple so we can convey the desired encoding size + + +# ----------------------------------------------------------------------------- +class DataElement: + NIL = 0 + UNSIGNED_INTEGER = 1 + SIGNED_INTEGER = 2 + UUID = 3 + TEXT_STRING = 4 + BOOLEAN = 5 + SEQUENCE = 6 + ALTERNATIVE = 7 + URL = 8 + + TYPE_NAMES = { + NIL: 'NIL', + UNSIGNED_INTEGER: 'UNSIGNED_INTEGER', + SIGNED_INTEGER: 'SIGNED_INTEGER', + UUID: 'UUID', + TEXT_STRING: 'TEXT_STRING', + BOOLEAN: 'BOOLEAN', + SEQUENCE: 'SEQUENCE', + ALTERNATIVE: 'ALTERNATIVE', + URL: 'URL' + } + + type_constructors = { + NIL: lambda x: DataElement(DataElement.NIL, None), + UNSIGNED_INTEGER: lambda x, y: DataElement(DataElement.UNSIGNED_INTEGER, DataElement.unsigned_integer_from_bytes(x), value_size=y), + SIGNED_INTEGER: lambda x, y: DataElement(DataElement.SIGNED_INTEGER, DataElement.signed_integer_from_bytes(x), value_size=y), + UUID: lambda x: DataElement(DataElement.UUID, core.UUID.from_bytes(bytes(reversed(x)))), + TEXT_STRING: lambda x: DataElement(DataElement.TEXT_STRING, x.decode('utf8')), + BOOLEAN: lambda x: DataElement(DataElement.BOOLEAN, x[0] == 1), + SEQUENCE: lambda x: DataElement(DataElement.SEQUENCE, DataElement.list_from_bytes(x)), + ALTERNATIVE: lambda x: DataElement(DataElement.ALTERNATIVE, DataElement.list_from_bytes(x)), + URL: lambda x: DataElement(DataElement.URL, x.decode('utf8')) + } + + def __init__(self, type, value, value_size=None): + self.type = type + self.value = value + self.value_size = value_size + self.bytes = None # Used a cache when parsing from bytes so we can emit a byte-for-byte replica + if type == DataElement.UNSIGNED_INTEGER or type == DataElement.SIGNED_INTEGER: + if value_size is None: + raise ValueError('integer types must have a value size specified') + + @staticmethod + def nil(): + return DataElement(DataElement.NIL, None) + + @staticmethod + def unsigned_integer(value, value_size): + return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size) + + @staticmethod + def unsigned_integer_8(value): + return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=1) + + @staticmethod + def unsigned_integer_16(value): + return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=2) + + @staticmethod + def unsigned_integer_32(value): + return DataElement(DataElement.UNSIGNED_INTEGER, value, value_size=4) + + @staticmethod + def signed_integer(value, value_size): + return DataElement(DataElement.SIGNED_INTEGER, value, value_size) + + @staticmethod + def signed_integer_8(value): + return DataElement(DataElement.SIGNED_INTEGER, value, value_size=1) + + @staticmethod + def signed_integer_16(value): + return DataElement(DataElement.SIGNED_INTEGER, value, value_size=2) + + @staticmethod + def signed_integer_32(value): + return DataElement(DataElement.SIGNED_INTEGER, value, value_size=4) + + @staticmethod + def uuid(value): + return DataElement(DataElement.UUID, value) + + @staticmethod + def text_string(value): + return DataElement(DataElement.TEXT_STRING, value) + + @staticmethod + def boolean(value): + return DataElement(DataElement.BOOLEAN, value) + + @staticmethod + def sequence(value): + return DataElement(DataElement.SEQUENCE, value) + + @staticmethod + def alternative(value): + return DataElement(DataElement.ALTERNATIVE, value) + + @staticmethod + def url(value): + return DataElement(DataElement.URL, value) + + @staticmethod + def unsigned_integer_from_bytes(data): + if len(data) == 1: + return data[0] + elif len(data) == 2: + return struct.unpack('>H', data)[0] + elif len(data) == 4: + return struct.unpack('>I', data)[0] + elif len(data) == 8: + return struct.unpack('>Q', data)[0] + else: + raise ValueError(f'invalid integer length {len(data)}') + + @staticmethod + def signed_integer_from_bytes(data): + if len(data) == 1: + return struct.unpack('b', data)[0] + elif len(data) == 2: + return struct.unpack('>h', data)[0] + elif len(data) == 4: + return struct.unpack('>i', data)[0] + elif len(data) == 8: + return struct.unpack('>q', data)[0] + else: + raise ValueError(f'invalid integer length {len(data)}') + + @staticmethod + def list_from_bytes(data): + elements = [] + while data: + element = DataElement.from_bytes(data) + elements.append(element) + data = data[len(bytes(element)):] + return elements + + @staticmethod + def parse_from_bytes(data, offset): + element = DataElement.from_bytes(data[offset:]) + return offset + len(bytes(element)), element + + @staticmethod + def from_bytes(data): + type = data[0] >> 3 + size_index = data[0] & 7 + value_offset = 0 + if size_index == 0: + if type == DataElement.NIL: + value_size = 0 + else: + value_size = 1 + elif size_index == 1: + value_size = 2 + elif size_index == 2: + value_size = 4 + elif size_index == 3: + value_size = 8 + elif size_index == 4: + value_size = 16 + elif size_index == 5: + value_size = data[1] + value_offset = 1 + elif size_index == 6: + value_size = struct.unpack('>H', data[1:3])[0] + value_offset = 2 + else: # size_index == 7 + value_size = struct.unpack('>I', data[1:5])[0] + value_offset = 4 + + value_data = data[1 + value_offset:1 + value_offset + value_size] + constructor = DataElement.type_constructors.get(type) + if constructor: + if type == DataElement.UNSIGNED_INTEGER or type == DataElement.SIGNED_INTEGER: + result = constructor(value_data, value_size) + else: + result = constructor(value_data) + else: + result = DataElement(type, value_data) + result.bytes = data[:1 + value_offset + value_size] # Keep a copy so we can re-serialize to an exact replica + return result + + def to_bytes(self): + return bytes(self) + + def __bytes__(self): + # Return early if we have a cache + if self.bytes: + return self.bytes + + if self.type == DataElement.NIL: + data = b'' + elif self.type == DataElement.UNSIGNED_INTEGER: + if self.value < 0: + raise ValueError('UNSIGNED_INTEGER cannot be negative') + elif self.value_size == 1: + data = struct.pack('B', self.value) + elif self.value_size == 2: + data = struct.pack('>H', self.value) + elif self.value_size == 4: + data = struct.pack('>I', self.value) + elif self.value_size == 8: + data = struct.pack('>Q', self.value) + else: + raise ValueError('invalid value_size') + elif self.type == DataElement.SIGNED_INTEGER: + if self.value_size == 1: + data = struct.pack('b', self.value) + elif self.value_size == 2: + data = struct.pack('>h', self.value) + elif self.value_size == 4: + data = struct.pack('>i', self.value) + elif self.value_size == 8: + data = struct.pack('>q', self.value) + else: + raise ValueError('invalid value_size') + elif self.type == DataElement.UUID: + data = bytes(reversed(bytes(self.value))) + elif self.type == DataElement.TEXT_STRING or self.type == DataElement.URL: + data = self.value.encode('utf8') + elif self.type == DataElement.BOOLEAN: + data = bytes([1 if self.value else 0]) + elif self.type == DataElement.SEQUENCE or self.type == DataElement.ALTERNATIVE: + data = b''.join([bytes(element) for element in self.value]) + else: + data = self.value + + size = len(data) + size_bytes = b'' + if self.type == DataElement.NIL: + if size != 0: + raise ValueError('NIL must be empty') + size_index = 0 + elif (self.type == DataElement.UNSIGNED_INTEGER or + self.type == DataElement.SIGNED_INTEGER or + self.type == DataElement.UUID): + if size <= 1: + size_index = 0 + elif size == 2: + size_index = 1 + elif size == 4: + size_index = 2 + elif size == 8: + size_index = 3 + elif size == 16: + size_index = 4 + else: + raise ValueError('invalid data size') + elif (self.type == DataElement.TEXT_STRING or + self.type == DataElement.SEQUENCE or + self.type == DataElement.ALTERNATIVE or + self.type == DataElement.URL): + if size <= 0xFF: + size_index = 5 + size_bytes = bytes([size]) + elif size <= 0xFFFF: + size_index = 6 + size_bytes = struct.pack('>H', size) + elif size <= 0xFFFFFFFF: + size_index = 7 + size_bytes = struct.pack('>I', size) + else: + raise ValueError('invalid data size') + elif self.type == DataElement.BOOLEAN: + if size != 1: + raise ValueError('boolean must be 1 byte') + size_index = 0 + + self.bytes = bytes([self.type << 3 | size_index]) + size_bytes + data + return self.bytes + + def to_string(self, pretty=False, indentation=0): + prefix = ' ' * indentation + type_name = name_or_number(self.TYPE_NAMES, self.type) + if self.type == DataElement.NIL: + value_string = '' + elif self.type == DataElement.SEQUENCE or self.type == DataElement.ALTERNATIVE: + container_separator = '\n' if pretty else '' + element_separator = '\n' if pretty else ',' + value_string = f'[{container_separator}{element_separator.join([element.to_string(pretty, indentation + 1 if pretty else 0) for element in self.value])}{container_separator}{prefix}]' + elif self.type == DataElement.UNSIGNED_INTEGER or self.type == DataElement.SIGNED_INTEGER: + value_string = f'{self.value}#{self.value_size}' + elif isinstance(self.value, DataElement): + value_string = self.value.to_string(pretty, indentation) + else: + value_string = str(self.value) + return f'{prefix}{type_name}({value_string})' + + def __str__(self): + return self.to_string() + + +# ----------------------------------------------------------------------------- +class ServiceAttribute: + def __init__(self, id, value): + self.id = id + self.value = value + + @staticmethod + def list_from_data_elements(elements): + attribute_list = [] + for i in range(0, len(elements) // 2): + attribute_id, attribute_value = elements[2 * i:2 * (i + 1)] + if attribute_id.type != DataElement.UNSIGNED_INTEGER: + logger.warn('attribute ID element is not an integer') + continue + attribute_list.append(ServiceAttribute(attribute_id.value, attribute_value)) + + return attribute_list + + @staticmethod + def find_attribute_in_list(attribute_list, attribute_id): + return next((attribute.value for attribute in attribute_list if attribute.id == attribute_id), None) + + @staticmethod + def id_name(id): + return name_or_number(SDP_ATTRIBUTE_ID_NAMES, id) + + @staticmethod + def is_uuid_in_value(uuid, value): + # Find if a uuid matches a value, either directly or recursing into sequences + if value.type == DataElement.UUID: + return value.value == uuid + elif value.type == DataElement.SEQUENCE: + for element in value.value: + if ServiceAttribute.is_uuid_in_value(uuid, element): + return True + return False + else: + return False + + def to_string(self, color=False): + if color: + return f'Attribute(id={colors.color(self.id_name(self.id),"magenta")},value={self.value})' + else: + return f'Attribute(id={self.id_name(self.id)},value={self.value})' + + def __str__(self): + return self.to_string() + + +# ----------------------------------------------------------------------------- +class SDP_PDU: + ''' + See Bluetooth spec @ Vol 3, Part B - 4.2 PROTOCOL DATA UNIT FORMAT + ''' + sdp_pdu_classes = {} + + @staticmethod + def from_bytes(pdu): + pdu_id, transaction_id, parameters_length = struct.unpack_from('>BHH', pdu, 0) + + cls = SDP_PDU.sdp_pdu_classes.get(pdu_id) + if cls is None: + instance = SDP_PDU(pdu) + instance.name = SDP_PDU.pdu_name(pdu_id) + instance.pdu_id = pdu_id + instance.transaction_id = transaction_id + return instance + self = cls.__new__(cls) + SDP_PDU.__init__(self, pdu, transaction_id) + if hasattr(self, 'fields'): + self.init_from_bytes(pdu, 5) + return self + + @staticmethod + def parse_service_record_handle_list_preceded_by_count(data, offset): + count = struct.unpack_from('>H', data, offset - 2)[0] + handle_list = [struct.unpack_from('>I', data, offset + x * 4)[0] for x in range(count)] + return offset + count * 4, handle_list + + @staticmethod + def parse_bytes_preceded_by_length(data, offset): + length = struct.unpack_from('>H', data, offset - 2)[0] + return offset + length, data[offset:offset + length] + + @staticmethod + def error_name(error_code): + return name_or_number(SDP_ERROR_NAMES, error_code) + + @staticmethod + def pdu_name(code): + return name_or_number(SDP_PDU_NAMES, code) + + @staticmethod + def subclass(fields): + def inner(cls): + name = cls.__name__ + + # add a _ character before every uppercase letter, except the SDP_ prefix + location = len(name) - 1 + while location > 4: + if not name[location].isupper(): + location -= 1 + continue + name = name[:location] + '_' + name[location:] + location -= 1 + + cls.name = name.upper() + cls.pdu_id = key_with_value(SDP_PDU_NAMES, cls.name) + if cls.pdu_id is None: + raise KeyError(f'PDU name {cls.name} not found in SDP_PDU_NAMES') + cls.fields = fields + + # Register a factory for this class + SDP_PDU.sdp_pdu_classes[cls.pdu_id] = cls + + return cls + + return inner + + def __init__(self, pdu=None, transaction_id=0, **kwargs): + if hasattr(self, 'fields') and kwargs: + HCI_Object.init_from_fields(self, self.fields, kwargs) + if pdu is None: + parameters = HCI_Object.dict_to_bytes(kwargs, self.fields) + pdu = struct.pack('>BHH', self.pdu_id, transaction_id, len(parameters)) + parameters + self.pdu = pdu + self.transaction_id = transaction_id + + def init_from_bytes(self, pdu, offset): + return HCI_Object.init_from_bytes(self, pdu, offset, self.fields) + + def to_bytes(self): + return self.pdu + + def __bytes__(self): + return self.to_bytes() + + def __str__(self): + result = f'{color(self.name, "blue")} [TID={self.transaction_id}]' + if fields := getattr(self, 'fields', None): + result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, ' ') + elif len(self.pdu) > 1: + result += f': {self.pdu.hex()}' + return result + + +# ----------------------------------------------------------------------------- +@SDP_PDU.subclass([ + ('error_code', {'size': 2, 'mapper': SDP_PDU.error_name}) +]) +class SDP_ErrorResponse(SDP_PDU): + ''' + See Bluetooth spec @ Vol 3, Part B - 4.4.1 SDP_ErrorResponse PDU + ''' + + +# ----------------------------------------------------------------------------- +@SDP_PDU.subclass([ + ('service_search_pattern', DataElement.parse_from_bytes), + ('maximum_service_record_count', '>2'), + ('continuation_state', '*') +]) +class SDP_ServiceSearchRequest(SDP_PDU): + ''' + See Bluetooth spec @ Vol 3, Part B - 4.5.1 SDP_ServiceSearchRequest PDU + ''' + + +# ----------------------------------------------------------------------------- +@SDP_PDU.subclass([ + ('total_service_record_count', '>2'), + ('current_service_record_count', '>2'), + ('service_record_handle_list', SDP_PDU.parse_service_record_handle_list_preceded_by_count), + ('continuation_state', '*') +]) +class SDP_ServiceSearchResponse(SDP_PDU): + ''' + See Bluetooth spec @ Vol 3, Part B - 4.5.2 SDP_ServiceSearchResponse PDU + ''' + + +# ----------------------------------------------------------------------------- +@SDP_PDU.subclass([ + ('service_record_handle', '>4'), + ('maximum_attribute_byte_count', '>2'), + ('attribute_id_list', DataElement.parse_from_bytes), + ('continuation_state', '*') +]) +class SDP_ServiceAttributeRequest(SDP_PDU): + ''' + See Bluetooth spec @ Vol 3, Part B - 4.6.1 SDP_ServiceAttributeRequest PDU + ''' + + +# ----------------------------------------------------------------------------- +@SDP_PDU.subclass([ + ('attribute_list_byte_count', '>2'), + ('attribute_list', SDP_PDU.parse_bytes_preceded_by_length), + ('continuation_state', '*') +]) +class SDP_ServiceAttributeResponse(SDP_PDU): + ''' + See Bluetooth spec @ Vol 3, Part B - 4.6.2 SDP_ServiceAttributeResponse PDU + ''' + + +# ----------------------------------------------------------------------------- +@SDP_PDU.subclass([ + ('service_search_pattern', DataElement.parse_from_bytes), + ('maximum_attribute_byte_count', '>2'), + ('attribute_id_list', DataElement.parse_from_bytes), + ('continuation_state', '*') +]) +class SDP_ServiceSearchAttributeRequest(SDP_PDU): + ''' + See Bluetooth spec @ Vol 3, Part B - 4.7.1 SDP_ServiceSearchAttributeRequest PDU + ''' + + +# ----------------------------------------------------------------------------- +@SDP_PDU.subclass([ + ('attribute_lists_byte_count', '>2'), + ('attribute_lists', SDP_PDU.parse_bytes_preceded_by_length), + ('continuation_state', '*') +]) +class SDP_ServiceSearchAttributeResponse(SDP_PDU): + ''' + See Bluetooth spec @ Vol 3, Part B - 4.7.2 SDP_ServiceSearchAttributeResponse PDU + ''' + + +# ----------------------------------------------------------------------------- +class Client: + def __init__(self, device): + self.device = device + self.pending_request = None + self.channel = None + + async def connect(self, connection): + result = await self.device.l2cap_channel_manager.connect(connection, SDP_PSM) + self.channel = result + + async def disconnect(self): + if self.channel: + await self.channel.disconnect() + self.channel = None + + async def search_services(self, uuids): + if self.pending_request is not None: + raise InvalidStateError('request already pending') + + service_search_pattern = DataElement.sequence([DataElement.uuid(uuid) for uuid in uuids]) + + # Request and accumulate until there's no more continuation + service_record_handle_list = [] + continuation_state = bytes([0]) + watchdog = SDP_CONTINUATION_WATCHDOG + while watchdog > 0: + response_pdu = await self.channel.send_request( + SDP_ServiceSearchRequest( + transaction_id = 0, # Transaction ID TODO: pick a real value + service_search_pattern = service_search_pattern, + maximum_service_record_count = 0xFFFF, + continuation_state = continuation_state + ) + ) + response = SDP_PDU.from_bytes(response_pdu) + logger.debug(f'<<< Response: {response}') + service_record_handle_list += response.service_record_handle_list + continuation_state = response.continuation_state + if len(continuation_state) == 1 and continuation_state[0] == 0: + break + logger.debug(f'continuation: {continuation_state.hex()}') + watchdog -= 1 + + return service_record_handle_list + + async def search_attributes(self, uuids, attribute_ids): + if self.pending_request is not None: + raise InvalidStateError('request already pending') + + service_search_pattern = DataElement.sequence([DataElement.uuid(uuid) for uuid in uuids]) + attribute_id_list = DataElement.sequence( + [ + DataElement.unsigned_integer(attribute_id[0], value_size=attribute_id[1]) + if type(attribute_id) is tuple + else DataElement.unsigned_integer_16(attribute_id) + for attribute_id in attribute_ids + ] + ) + + # Request and accumulate until there's no more continuation + accumulator = b'' + continuation_state = bytes([0]) + watchdog = SDP_CONTINUATION_WATCHDOG + while watchdog > 0: + response_pdu = await self.channel.send_request( + SDP_ServiceSearchAttributeRequest( + transaction_id = 0, # Transaction ID TODO: pick a real value + service_search_pattern = service_search_pattern, + maximum_attribute_byte_count = 0xFFFF, + attribute_id_list = attribute_id_list, + continuation_state = continuation_state + ) + ) + response = SDP_PDU.from_bytes(response_pdu) + logger.debug(f'<<< Response: {response}') + accumulator += response.attribute_lists + continuation_state = response.continuation_state + if len(continuation_state) == 1 and continuation_state[0] == 0: + break + logger.debug(f'continuation: {continuation_state.hex()}') + watchdog -= 1 + + # Parse the result into attribute lists + attribute_lists_sequences = DataElement.from_bytes(accumulator) + if attribute_lists_sequences.type != DataElement.SEQUENCE: + logger.warn('unexpected data type') + return [] + + return [ + ServiceAttribute.list_from_data_elements(sequence.value) + for sequence in attribute_lists_sequences.value + if sequence.type == DataElement.SEQUENCE + ] + + async def get_attributes(self, service_record_handle, attribute_ids): + if self.pending_request is not None: + raise InvalidStateError('request already pending') + + attribute_id_list = DataElement.sequence( + [ + DataElement.unsigned_integer(attribute_id[0], value_size=attribute_id[1]) + if type(attribute_id) is tuple + else DataElement.unsigned_integer_16(attribute_id) + for attribute_id in attribute_ids + ] + ) + + # Request and accumulate until there's no more continuation + accumulator = b'' + continuation_state = bytes([0]) + watchdog = SDP_CONTINUATION_WATCHDOG + while watchdog > 0: + response_pdu = await self.channel.send_request( + SDP_ServiceAttributeRequest( + transaction_id = 0, # Transaction ID TODO: pick a real value + service_record_handle = service_record_handle, + maximum_attribute_byte_count = 0xFFFF, + attribute_id_list = attribute_id_list, + continuation_state = continuation_state + ) + ) + response = SDP_PDU.from_bytes(response_pdu) + logger.debug(f'<<< Response: {response}') + accumulator += response.attribute_list + continuation_state = response.continuation_state + if len(continuation_state) == 1 and continuation_state[0] == 0: + break + logger.debug(f'continuation: {continuation_state.hex()}') + watchdog -= 1 + + # Parse the result into a list of attributes + attribute_list_sequence = DataElement.from_bytes(accumulator) + if attribute_list_sequence.type != DataElement.SEQUENCE: + logger.warn('unexpected data type') + return [] + + return ServiceAttribute.list_from_data_elements(attribute_list_sequence.value) + + +# ----------------------------------------------------------------------------- +class Server: + CONTINUATION_STATE = bytes([0x01, 0x43]) + + def __init__(self, device): + self.device = device + self.service_records = {} # Service records maps, by record handle + self.current_response = None + + def register(self, l2cap_channel_manager): + l2cap_channel_manager.register_server(SDP_PSM, self.on_connection) + + def send_response(self, response): + logger.debug(f'{color(">>> Sending SDP Response", "blue")}: {response}') + self.channel.send_pdu(response) + + def match_services(self, search_pattern): + # Find the services for which the attributes in the pattern is a subset of the + # service's attribute values (NOTE: the value search recurses into sequences) + matching_services = {} + for handle, service in self.service_records.items(): + for uuid in search_pattern.value: + found = False + for attribute in service: + if ServiceAttribute.is_uuid_in_value(uuid.value, attribute.value): + found = True + break + if found: + matching_services[handle] = service + break + + return matching_services + + def on_connection(self, channel): + self.channel = channel + self.channel.sink = self.on_pdu + + def on_pdu(self, pdu): + try: + sdp_pdu = SDP_PDU.from_bytes(pdu) + except Exception as error: + logger.warn(color(f'failed to parse SDP Request PDU: {error}', 'red')) + self.send_response( + SDP_ErrorResponse( + transaction_id = 0, + error_code = SDP_INVALID_REQUEST_SYNTAX_ERROR + ) + ) + + logger.debug(f'{color("<<< Received SDP Request", "green")}: {sdp_pdu}') + + # Find the handler method + handler_name = f'on_{sdp_pdu.name.lower()}' + handler = getattr(self, handler_name, None) + if handler: + try: + handler(sdp_pdu) + except Exception as error: + logger.warning(f'{color("!!! Exception in handler:", "red")} {error}') + self.send_response( + SDP_ErrorResponse( + transaction_id = sdp_pdu.transaction_id, + error_code = SDP_INSUFFICIENT_RESOURCES_TO_SATISFY_REQUEST_ERROR + ) + ) + else: + logger.error(color('SDP Request not handled???', 'red')) + self.send_response( + SDP_ErrorResponse( + transaction_id = sdp_pdu.transaction_id, + error_code = SDP_INVALID_REQUEST_SYNTAX_ERROR + ) + ) + + def get_next_response_payload(self, maximum_size): + if len(self.current_response) > maximum_size: + payload = self.current_response[:maximum_size] + continuation_state = Server.CONTINUATION_STATE + self.current_response = self.current_response[maximum_size:] + else: + payload = self.current_response + continuation_state = bytes([0]) + self.current_response = None + + return (payload, continuation_state) + + @staticmethod + def get_service_attributes(service, attribute_ids): + attributes = [] + for attribute_id in attribute_ids: + if attribute_id.value_size == 4: + # Attribute ID range + id_range_start = attribute_id.value >> 16 + id_range_end = attribute_id.value & 0xFFFF + else: + id_range_start = attribute_id.value + id_range_end = attribute_id.value + attributes += [ + attribute for attribute in service + if attribute.id >= id_range_start and attribute.id <= id_range_end + ] + + # Return the maching attributes, sorted by attribute id + attributes.sort(key = lambda x: x.id) + attribute_list = DataElement.sequence([]) + for attribute in attributes: + attribute_list.value.append(DataElement.unsigned_integer_16(attribute.id)) + attribute_list.value.append(attribute.value) + + return attribute_list + + def on_sdp_service_search_request(self, request): + # Check if this is a continuation + if len(request.continuation_state) > 1: + if not self.current_response: + self.send_response( + SDP_ErrorResponse( + transaction_id = request.transaction_id, + error_code = SDP_INVALID_CONTINUATION_STATE_ERROR + ) + ) + return + else: + # Cleanup any partial response leftover + self.current_response = None + + # Find the matching services + matching_services = self.match_services(request.service_search_pattern) + service_record_handles = list(matching_services.keys()) + + # Only return up to the maximum requested + service_record_handles_subset = service_record_handles[:request.maximum_service_record_count] + + # Serialize to a byte array, and remember the total count + logger.debug(f'Service Record Handles: {service_record_handles}') + self.current_response = ( + len(service_record_handles), + service_record_handles_subset + ) + + # Respond, keeping any unsent handles for later + service_record_handles = self.current_response[1][:request.maximum_service_record_count] + self.current_response = ( + self.current_response[0], + self.current_response[1][request.maximum_service_record_count:] + ) + continuation_state = Server.CONTINUATION_STATE if self.current_response[1] else bytes([0]) + service_record_handle_list = b''.join([struct.pack('>I', handle) for handle in service_record_handles]) + self.send_response( + SDP_ServiceSearchResponse( + transaction_id = request.transaction_id, + total_service_record_count = self.current_response[0], + current_service_record_count = len(service_record_handles), + service_record_handle_list = service_record_handle_list, + continuation_state = continuation_state + ) + ) + + def on_sdp_service_attribute_request(self, request): + # Check if this is a continuation + if len(request.continuation_state) > 1: + if not self.current_response: + self.send_response( + SDP_ErrorResponse( + transaction_id = request.transaction_id, + error_code = SDP_INVALID_CONTINUATION_STATE_ERROR + ) + ) + return + else: + # Cleanup any partial response leftover + self.current_response = None + + # Check that the service exists + service = self.service_records.get(request.service_record_handle) + if service is None: + self.send_response( + SDP_ErrorResponse( + transaction_id = request.transaction_id, + error_code = SDP_INVALID_SERVICE_RECORD_HANDLE_ERROR + ) + ) + return + + # Get the attributes for the service + attribute_list = Server.get_service_attributes(service, request.attribute_id_list.value) + + # Serialize to a byte array + logger.debug(f'Attributes: {attribute_list}') + self.current_response = bytes(attribute_list) + + # Respond, keeping any pending chunks for later + attribute_list, continuation_state = self.get_next_response_payload(request.maximum_attribute_byte_count) + self.send_response( + SDP_ServiceAttributeResponse( + transaction_id = request.transaction_id, + attribute_list_byte_count = len(attribute_list), + attribute_list = attribute_list, + continuation_state = continuation_state + ) + ) + + def on_sdp_service_search_attribute_request(self, request): + # Check if this is a continuation + if len(request.continuation_state) > 1: + if not self.current_response: + self.send_response( + SDP_ErrorResponse( + transaction_id = request.transaction_id, + error_code = SDP_INVALID_CONTINUATION_STATE_ERROR + ) + ) + else: + # Cleanup any partial response leftover + self.current_response = None + + # Find the matching services + matching_services = self.match_services(request.service_search_pattern).values() + + # Filter the required attributes + attribute_lists = DataElement.sequence([]) + for service in matching_services: + attribute_list = Server.get_service_attributes(service, request.attribute_id_list.value) + if attribute_list.value: + attribute_lists.value.append(attribute_list) + + # Serialize to a byte array + logger.debug(f'Search response: {attribute_lists}') + self.current_response = bytes(attribute_lists) + + # Respond, keeping any pending chunks for later + attribute_lists, continuation_state = self.get_next_response_payload(request.maximum_attribute_byte_count) + self.send_response( + SDP_ServiceSearchAttributeResponse( + transaction_id = request.transaction_id, + attribute_lists_byte_count = len(attribute_lists), + attribute_lists = attribute_lists, + continuation_state = continuation_state + ) + ) diff --git a/bumble/smp.py b/bumble/smp.py new file mode 100644 index 0000000..7efb8f5 --- /dev/null +++ b/bumble/smp.py @@ -0,0 +1,1514 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# SMP - Security Manager Protocol +# +# See Bluetooth spec @ Vol 3, Part H +# +# ----------------------------------------------------------------------------- + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import asyncio +import secrets +from pyee import EventEmitter +from colors import color + +from .core import * +from .hci import * +from .keys import PairingKeys +from . import crypto + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +# Constants +# ----------------------------------------------------------------------------- +SMP_CID = 0x06 + +SMP_PAIRING_REQUEST_COMMAND = 0x01 +SMP_PAIRING_RESPONSE_COMMAND = 0x02 +SMP_PAIRING_CONFIRM_COMMAND = 0x03 +SMP_PAIRING_RANDOM_COMMAND = 0x04 +SMP_PAIRING_FAILED_COMMAND = 0x05 +SMP_ENCRYPTION_INFORMATION_COMMAND = 0x06 +SMP_MASTER_IDENTIFICATION_COMMAND = 0x07 +SMP_IDENTITY_INFORMATION_COMMAND = 0x08 +SMP_IDENTITY_ADDRESS_INFORMATION_COMMAND = 0x09 +SMP_SIGNING_INFORMATION_COMMAND = 0x0A +SMP_SECURITY_REQUEST_COMMAND = 0x0B +SMP_PAIRING_PUBLIC_KEY_COMMAND = 0x0C +SMP_PAIRING_DHKEY_CHECK_COMMAND = 0x0D +SMP_PAIRING_KEYPRESS_NOTIFICATION_COMMAND = 0x0E + +SMP_COMMAND_NAMES = { + SMP_PAIRING_REQUEST_COMMAND: 'SMP_PAIRING_REQUEST_COMMAND', + SMP_PAIRING_RESPONSE_COMMAND: 'SMP_PAIRING_RESPONSE_COMMAND', + SMP_PAIRING_CONFIRM_COMMAND: 'SMP_PAIRING_CONFIRM_COMMAND', + SMP_PAIRING_RANDOM_COMMAND: 'SMP_PAIRING_RANDOM_COMMAND', + SMP_PAIRING_FAILED_COMMAND: 'SMP_PAIRING_FAILED_COMMAND', + SMP_ENCRYPTION_INFORMATION_COMMAND: 'SMP_ENCRYPTION_INFORMATION_COMMAND', + SMP_MASTER_IDENTIFICATION_COMMAND: 'SMP_MASTER_IDENTIFICATION_COMMAND', + SMP_IDENTITY_INFORMATION_COMMAND: 'SMP_IDENTITY_INFORMATION_COMMAND', + SMP_IDENTITY_ADDRESS_INFORMATION_COMMAND: 'SMP_IDENTITY_ADDRESS_INFORMATION_COMMAND', + SMP_SIGNING_INFORMATION_COMMAND: 'SMP_SIGNING_INFORMATION_COMMAND', + SMP_SECURITY_REQUEST_COMMAND: 'SMP_SECURITY_REQUEST_COMMAND', + SMP_PAIRING_PUBLIC_KEY_COMMAND: 'SMP_PAIRING_PUBLIC_KEY_COMMAND', + SMP_PAIRING_DHKEY_CHECK_COMMAND: 'SMP_PAIRING_DHKEY_CHECK_COMMAND', + SMP_PAIRING_KEYPRESS_NOTIFICATION_COMMAND: 'SMP_PAIRING_KEYPRESS_NOTIFICATION_COMMAND' +} + +SMP_DISPLAY_ONLY_IO_CAPABILITY = 0x00 +SMP_DISPLAY_YES_NO_IO_CAPABILITY = 0x01 +SMP_KEYBOARD_ONLY_IO_CAPABILITY = 0x02 +SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY = 0x03 +SMP_KEYBOARD_DISPLAY_IO_CAPABILITY = 0x04 + +SMP_IO_CAPABILITY_NAMES = { + SMP_DISPLAY_ONLY_IO_CAPABILITY: 'SMP_DISPLAY_ONLY_IO_CAPABILITY', + SMP_DISPLAY_YES_NO_IO_CAPABILITY: 'SMP_DISPLAY_YES_NO_IO_CAPABILITY', + SMP_KEYBOARD_ONLY_IO_CAPABILITY: 'SMP_KEYBOARD_ONLY_IO_CAPABILITY', + SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: 'SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY', + SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: 'SMP_KEYBOARD_DISPLAY_IO_CAPABILITY' +} + +SMP_PASSKEY_ENTRY_FAILED_ERROR = 0x01 +SMP_OOB_NOT_AVAILABLE_ERROR = 0x02 +SMP_AUTHENTICATION_REQUIREMENTS_ERROR = 0x03 +SMP_CONFIRM_VALUE_FAILED_ERROR = 0x04 +SMP_PAIRING_NOT_SUPPORTED_ERROR = 0x05 +SMP_ENCRYPTION_KEY_SIZE_ERROR = 0x06 +SMP_COMMAND_NOT_SUPPORTED_ERROR = 0x07 +SMP_UNSPECIFIED_REASON_ERROR = 0x08 +SMP_REPEATED_ATTEMPTS_ERROR = 0x09 +SMP_INVALID_PARAMETERS_ERROR = 0x0A +SMP_DHKEY_CHECK_FAILED_ERROR = 0x0B +SMP_NUMERIC_COMPARISON_FAILED_ERROR = 0x0C +SMP_BD_EDR_PAIRING_IN_PROGRESS_ERROR = 0x0D +SMP_CROSS_TRANSPORT_KEY_DERIVATION_NOT_ALLOWED_ERROR = 0x0E + +SMP_ERROR_NAMES = { + SMP_PASSKEY_ENTRY_FAILED_ERROR: 'SMP_PASSKEY_ENTRY_FAILED_ERROR', + SMP_OOB_NOT_AVAILABLE_ERROR: 'SMP_OOB_NOT_AVAILABLE_ERROR', + SMP_AUTHENTICATION_REQUIREMENTS_ERROR: 'SMP_AUTHENTICATION_REQUIREMENTS_ERROR', + SMP_CONFIRM_VALUE_FAILED_ERROR: 'SMP_CONFIRM_VALUE_FAILED_ERROR', + SMP_PAIRING_NOT_SUPPORTED_ERROR: 'SMP_PAIRING_NOT_SUPPORTED_ERROR', + SMP_ENCRYPTION_KEY_SIZE_ERROR: 'SMP_ENCRYPTION_KEY_SIZE_ERROR', + SMP_COMMAND_NOT_SUPPORTED_ERROR: 'SMP_COMMAND_NOT_SUPPORTED_ERROR', + SMP_UNSPECIFIED_REASON_ERROR: 'SMP_UNSPECIFIED_REASON_ERROR', + SMP_REPEATED_ATTEMPTS_ERROR: 'SMP_REPEATED_ATTEMPTS_ERROR', + SMP_INVALID_PARAMETERS_ERROR: 'SMP_INVALID_PARAMETERS_ERROR', + SMP_DHKEY_CHECK_FAILED_ERROR: 'SMP_DHKEY_CHECK_FAILED_ERROR', + SMP_NUMERIC_COMPARISON_FAILED_ERROR: 'SMP_NUMERIC_COMPARISON_FAILED_ERROR', + SMP_BD_EDR_PAIRING_IN_PROGRESS_ERROR: 'SMP_BD_EDR_PAIRING_IN_PROGRESS_ERROR', + SMP_CROSS_TRANSPORT_KEY_DERIVATION_NOT_ALLOWED_ERROR: 'SMP_CROSS_TRANSPORT_KEY_DERIVATION_NOT_ALLOWED_ERROR' +} + +SMP_PASSKEY_ENTRY_STARTED_KEYPRESS_NOTIFICATION_TYPE = 0 +SMP_PASSKEY_DIGIT_ENTERED_KEYPRESS_NOTIFICATION_TYPE = 1 +SMP_PASSKEY_DIGIT_ERASED_KEYPRESS_NOTIFICATION_TYPE = 2 +SMP_PASSKEY_CLEARED_KEYPRESS_NOTIFICATION_TYPE = 3 +SMP_PASSKEY_ENTRY_COMPLETED_KEYPRESS_NOTIFICATION_TYPE = 4 + +SMP_KEYPRESS_NOTIFICATION_TYPE_NAMES = { + SMP_PASSKEY_ENTRY_STARTED_KEYPRESS_NOTIFICATION_TYPE: 'SMP_PASSKEY_ENTRY_STARTED_KEYPRESS_NOTIFICATION_TYPE', + SMP_PASSKEY_DIGIT_ENTERED_KEYPRESS_NOTIFICATION_TYPE: 'SMP_PASSKEY_DIGIT_ENTERED_KEYPRESS_NOTIFICATION_TYPE', + SMP_PASSKEY_DIGIT_ERASED_KEYPRESS_NOTIFICATION_TYPE: 'SMP_PASSKEY_DIGIT_ERASED_KEYPRESS_NOTIFICATION_TYPE', + SMP_PASSKEY_CLEARED_KEYPRESS_NOTIFICATION_TYPE: 'SMP_PASSKEY_CLEARED_KEYPRESS_NOTIFICATION_TYPE', + SMP_PASSKEY_ENTRY_COMPLETED_KEYPRESS_NOTIFICATION_TYPE: 'SMP_PASSKEY_ENTRY_COMPLETED_KEYPRESS_NOTIFICATION_TYPE' +} + +# Bit flags for key distribution/generation +SMP_ENC_KEY_DISTRIBUTION_FLAG = 0b0001 +SMP_ID_KEY_DISTRIBUTION_FLAG = 0b0010 +SMP_SIGN_KEY_DISTRIBUTION_FLAG = 0b0100 +SMP_LINK_KEY_DISTRIBUTION_FLAG = 0b1000 + +# AuthReq fields +SMP_BONDING_AUTHREQ = 0b00000001 +SMP_MITM_AUTHREQ = 0b00000100 +SMP_SC_AUTHREQ = 0b00001000 +SMP_KEYPRESS_AUTHREQ = 0b00010000 +SMP_CT2_AUTHREQ = 0b00100000 + + +# ----------------------------------------------------------------------------- +# Utils +# ----------------------------------------------------------------------------- +def error_name(error_code): + return name_or_number(SMP_ERROR_NAMES, error_code) + + +# ----------------------------------------------------------------------------- +# Classes +# ----------------------------------------------------------------------------- +class SMP_Command: + ''' + See Bluetooth spec @ Vol 3, Part H - 3 SECURITY MANAGER PROTOCOL + ''' + smp_classes = {} + code = 0 + + @staticmethod + def from_bytes(pdu): + code = pdu[0] + + cls = SMP_Command.smp_classes.get(code) + if cls is None: + instance = SMP_Command(pdu) + instance.name = SMP_Command.command_name(code) + instance.code = code + return instance + self = cls.__new__(cls) + SMP_Command.__init__(self, pdu) + if hasattr(self, 'fields'): + self.init_from_bytes(pdu, 1) + return self + + @staticmethod + def command_name(code): + return name_or_number(SMP_COMMAND_NAMES, code) + + @staticmethod + def auth_req_str(value): + bonding_flags = value & 3 + mitm = (value >> 2) & 1 + sc = (value >> 3) & 1 + keypress = (value >> 4) & 1 + ct2 = (value >> 5) & 1 + + return f'bonding_flags={bonding_flags}, MITM={mitm}, sc={sc}, keypress={keypress}, ct2={ct2}' + + @staticmethod + def io_capability_name(io_capability): + return name_or_number(SMP_IO_CAPABILITY_NAMES, io_capability) + + @staticmethod + def key_distribution_str(value): + key_types = [] + if value & SMP_ENC_KEY_DISTRIBUTION_FLAG: + key_types.append('ENC') + if value & SMP_ID_KEY_DISTRIBUTION_FLAG: + key_types.append('ID') + if value & SMP_SIGN_KEY_DISTRIBUTION_FLAG: + key_types.append('SIGN') + if value & SMP_LINK_KEY_DISTRIBUTION_FLAG: + key_types.append('LINK') + return ','.join(key_types) + + @staticmethod + def keypress_notification_type_name(notification_type): + return name_or_number(SMP_KEYPRESS_NOTIFICATION_TYPE_NAMES, notification_type) + + @staticmethod + def subclass(fields): + def inner(cls): + cls.name = cls.__name__.upper() + cls.code = key_with_value(SMP_COMMAND_NAMES, cls.name) + if cls.code is None: + raise KeyError(f'Command name {cls.name} not found in SMP_COMMAND_NAMES') + cls.fields = fields + + # Register a factory for this class + SMP_Command.smp_classes[cls.code] = cls + + return cls + + return inner + + def __init__(self, pdu=None, **kwargs): + if hasattr(self, 'fields') and kwargs: + HCI_Object.init_from_fields(self, self.fields, kwargs) + if pdu is None: + pdu = bytes([self.code]) + HCI_Object.dict_to_bytes(kwargs, self.fields) + self.pdu = pdu + + def init_from_bytes(self, pdu, offset): + return HCI_Object.init_from_bytes(self, pdu, offset, self.fields) + + def to_bytes(self): + return self.pdu + + def __bytes__(self): + return self.to_bytes() + + def __str__(self): + result = color(self.name, 'yellow') + if fields := getattr(self, 'fields', None): + result += ':\n' + HCI_Object.format_fields(self.__dict__, fields, ' ') + else: + if len(self.pdu) > 1: + result += f': {self.pdu.hex()}' + return result + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('io_capability', {'size': 1, 'mapper': SMP_Command.io_capability_name}), + ('oob_data_flag', 1), + ('auth_req', {'size': 1, 'mapper': SMP_Command.auth_req_str}), + ('maximum_encryption_key_size', 1), + ('initiator_key_distribution', {'size': 1, 'mapper': SMP_Command.key_distribution_str}), + ('responder_key_distribution', {'size': 1, 'mapper': SMP_Command.key_distribution_str}) +]) +class SMP_Pairing_Request_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.5.1 Pairing Request + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('io_capability', {'size': 1, 'mapper': SMP_Command.io_capability_name}), + ('oob_data_flag', 1), + ('auth_req', {'size': 1, 'mapper': SMP_Command.auth_req_str}), + ('maximum_encryption_key_size', 1), + ('initiator_key_distribution', {'size': 1, 'mapper': SMP_Command.key_distribution_str}), + ('responder_key_distribution', {'size': 1, 'mapper': SMP_Command.key_distribution_str}) +]) +class SMP_Pairing_Response_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.5.2 Pairing Response + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('confirm_value', 16) +]) +class SMP_Pairing_Confirm_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.5.3 Pairing Confirm + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('random_value', 16) +]) +class SMP_Pairing_Random_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.5.4 Pairing Random + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('reason', {'size': 1, 'mapper': error_name}) +]) +class SMP_Pairing_Failed_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.5.5 Pairing Failed + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('public_key_x', 32), + ('public_key_y', 32) +]) +class SMP_Pairing_Public_Key_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.5.6 Pairing Public Key + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('dhkey_check', 16), +]) +class SMP_Pairing_DHKey_Check_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.5.7 Pairing DHKey Check + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('notification_type', {'size': 1, 'mapper': SMP_Command.keypress_notification_type_name}), +]) +class SMP_Pairing_Keypress_Notification_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.5.8 Keypress Notification + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('long_term_key', 16) +]) +class SMP_Encryption_Information_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.6.2 Encryption Information + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('ediv', 2), + ('rand', 8) +]) +class SMP_Master_Identification_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.6.3 Master Identification + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('identity_resolving_key', 16) +]) +class SMP_Identity_Information_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.6.4 Identity Information + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('addr_type', Address.ADDRESS_TYPE_SPEC), + ('bd_addr', Address.parse_address_preceded_by_type) +]) +class SMP_Identity_Address_Information_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.6.5 Identity Address Information + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('signature_key', 16) +]) +class SMP_Signing_Information_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.6.6 Signing Information + ''' + + +# ----------------------------------------------------------------------------- +@SMP_Command.subclass([ + ('auth_req', {'size': 1, 'mapper': SMP_Command.auth_req_str}), +]) +class SMP_Security_Request_Command(SMP_Command): + ''' + See Bluetooth spec @ Vol 3, Part H - 3.6.7 Security Request + ''' + + +# ----------------------------------------------------------------------------- +def smp_auth_req(bonding, mitm, sc, keypress, ct2): + value = 0 + if bonding: + value |= SMP_BONDING_AUTHREQ + if mitm: + value |= SMP_MITM_AUTHREQ + if sc: + value |= SMP_SC_AUTHREQ + if keypress: + value |= SMP_KEYPRESS_AUTHREQ + if ct2: + value |= SMP_CT2_AUTHREQ + return value + + +# ----------------------------------------------------------------------------- +class AddressResolver: + def __init__(self, resolving_keys): + self.resolving_keys = resolving_keys + + def resolve(self, address): + address_bytes = bytes(address) + hash = address_bytes[0:3] + prand = address_bytes[3:6] + for (irk, resolved_address) in self.resolving_keys: + local_hash = crypto.ah(irk, prand) + if local_hash == hash: + # Match! + if resolved_address.address_type == Address.PUBLIC_DEVICE_ADDRESS: + resolved_address_type = Address.PUBLIC_IDENTITY_ADDRESS + else: + resolved_address_type = Address.RANDOM_IDENTITY_ADDRESS + return Address(address=str(resolved_address), address_type=resolved_address_type) + + +# ----------------------------------------------------------------------------- +class PairingDelegate: + NO_OUTPUT_NO_INPUT = SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY + KEYBOARD_INPUT_ONLY = SMP_KEYBOARD_ONLY_IO_CAPABILITY + DISPLAY_OUTPUT_ONLY = SMP_DISPLAY_ONLY_IO_CAPABILITY + DISPLAY_OUTPUT_AND_YES_NO_INPUT = SMP_DISPLAY_YES_NO_IO_CAPABILITY + DISPLAY_OUTPUT_AND_KEYBOARD_INPUT = SMP_KEYBOARD_DISPLAY_IO_CAPABILITY + + def __init__(self, io_capability = NO_OUTPUT_NO_INPUT): + self.io_capability = io_capability + + async def accept(self): + return True + + async def compare_numbers(self, number): + return True + + async def get_number(self): + return 0 + + async def display_number(self, number): + pass + + +# ----------------------------------------------------------------------------- +class PairingConfig: + def __init__(self, sc=True, mitm=True, bonding=True, delegate=None): + self.sc = sc + self.mitm = mitm + self.bonding = bonding + self.delegate = delegate or PairingDelegate() + + def __str__(self): + io_capability_str = SMP_Command.io_capability_name(self.delegate.io_capability) + return f'PairingConfig(sc={self.sc}, mitm={self.mitm}, bonding={self.bonding}, delegate[{io_capability_str}])' + + +# ----------------------------------------------------------------------------- +class Session: + # Pairing methods + JUST_WORKS = 0 + NUMERIC_COMPARISON = 1 + PASSKEY = 2 + OOB = 3 + + PAIRING_METHOD_NAMES = { + JUST_WORKS: 'JUST_WORKS', + NUMERIC_COMPARISON: 'NUMERIC_COMPARISON', + PASSKEY: 'PASSKEY', + OOB: 'OOB' + } + + # I/O Capability to pairing method decision matrix + # + # See Bluetooth spec @ Vol 3, part H - Table 2.8: Mapping of IO Capabilities to Key Generation Method + # + # Map: initiator -> responder -> + # where may be a simple entry or a 2-element tuple, with the first element for legacy + # pairing and the second for secure connections, when the two are different. + # Each entry is either a method name, or, for PASSKEY, a tuple: + # (method, initiator_displays, responder_displays) + # to specify if the initiator and responder should display (True) or input a code (False). + PAIRING_METHODS = { + SMP_DISPLAY_ONLY_IO_CAPABILITY: { + SMP_DISPLAY_ONLY_IO_CAPABILITY: JUST_WORKS, + SMP_DISPLAY_YES_NO_IO_CAPABILITY: JUST_WORKS, + SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PASSKEY, True, False), + SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: JUST_WORKS, + SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: (PASSKEY, True, False), + }, + SMP_DISPLAY_YES_NO_IO_CAPABILITY: { + SMP_DISPLAY_ONLY_IO_CAPABILITY: JUST_WORKS, + SMP_DISPLAY_YES_NO_IO_CAPABILITY: (JUST_WORKS, NUMERIC_COMPARISON), + SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PASSKEY, True, False), + SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: JUST_WORKS, + SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: ((PASSKEY, True, False), NUMERIC_COMPARISON) + }, + SMP_KEYBOARD_ONLY_IO_CAPABILITY: { + SMP_DISPLAY_ONLY_IO_CAPABILITY: (PASSKEY, False, True), + SMP_DISPLAY_YES_NO_IO_CAPABILITY: (PASSKEY, False, True), + SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PASSKEY, False, False), + SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: JUST_WORKS, + SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: (PASSKEY, False, True), + }, + SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: { + SMP_DISPLAY_ONLY_IO_CAPABILITY: JUST_WORKS, + SMP_DISPLAY_YES_NO_IO_CAPABILITY: JUST_WORKS, + SMP_KEYBOARD_ONLY_IO_CAPABILITY: JUST_WORKS, + SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: JUST_WORKS, + SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: JUST_WORKS + }, + SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: { + SMP_DISPLAY_ONLY_IO_CAPABILITY: (PASSKEY, False, True), + SMP_DISPLAY_YES_NO_IO_CAPABILITY: ((PASSKEY, False, True), NUMERIC_COMPARISON), + SMP_KEYBOARD_ONLY_IO_CAPABILITY: (PASSKEY, True, False), + SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: JUST_WORKS, + SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: ((PASSKEY, True, False), NUMERIC_COMPARISON) + } + } + + def __init__(self, manager, connection, pairing_config): + self.manager = manager + self.connection = connection + self.tk = bytes(16) + self.r = bytes(16) + self.stk = None + self.ltk = None + self.ltk_ediv = 0 + self.ltk_rand = bytes(8) + self.initiator_key_distribution = 0 + self.responder_key_distribution = 0 + self.peer_random_value = None + self.peer_public_key_x = bytes(32) + self.peer_public_key_y = bytes(32) + self.peer_ltk = None + self.peer_ediv = None + self.peer_rand = None + self.peer_identity_resolving_key = None + self.peer_bd_addr = None + self.peer_signature_key = None + self.peer_expected_distributions = [] + self.dh_key = None + self.passkey = 0 + self.passkey_step = 0 + self.passkey_display = False + self.pairing_method = 0 + self.pairing_config = pairing_config + self.wait_before_continuing = None + self.completed = False + + # Decide if we're the initiator or the responder + self.is_initiator = (connection.role == BT_CENTRAL_ROLE) + self.is_responder = not self.is_initiator + + # Listen for connection events + connection.on('disconnection', self.on_disconnection) + connection.on('connection_encryption_change', self.on_connection_encryption_change) + connection.on('connection_encryption_key_refresh', self.on_connection_encryption_key_refresh) + + # Create a future that can be used to wait for the session to complete + if self.is_initiator: + self.pairing_result = asyncio.get_running_loop().create_future() + else: + self.pairing_result = None + + # Key Distribution (default values before negotiation) + self.initiator_key_distribution = ( + SMP_ENC_KEY_DISTRIBUTION_FLAG | + SMP_ID_KEY_DISTRIBUTION_FLAG # |SMP_SIGN_KEY_DISTRIBUTION_FLAG + ) + self.responder_key_distribution = self.initiator_key_distribution + + # Authentication Requirements Flags - Vol 3, Part H, Figure 3.3 + self.bonding = pairing_config.bonding + self.sc = pairing_config.sc + self.mitm = pairing_config.mitm + self.keypress = False + self.ct2 = False + + # I/O Capabilities + self.io_capability = pairing_config.delegate.io_capability + self.peer_io_capability = SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY + + # OOB (not supported yet) + self.oob = False + + # Set up addresses + peer_address = connection.peer_resolvable_address or connection.peer_address + if self.is_initiator: + self.ia = bytes(manager.address) + self.iat = 1 if manager.address.is_random else 0 + self.ra = bytes(peer_address) + self.rat = 1 if peer_address.is_random else 0 + else: + self.ra = bytes(manager.address) + self.rat = 1 if manager.address.is_random else 0 + self.ia = bytes(peer_address) + self.iat = 1 if peer_address.is_random else 0 + + @property + def pkx(self): + return ( + bytes(reversed(self.manager.ecc_key.x)), + self.peer_public_key_x + ) + + @property + def pka(self): + return self.pkx[0 if self.is_initiator else 1] + + @property + def pkb(self): + return self.pkx[0 if self.is_responder else 1] + + @property + def nx(self): + return ( + self.r, + self.peer_random_value + ) + + @property + def na(self): + return self.nx[0 if self.is_initiator else 1] + + @property + def nb(self): + return self.nx[0 if self.is_responder else 1] + + @property + def auth_req(self): + return smp_auth_req(self.bonding, self.mitm, self.sc, self.keypress, self.ct2) + + def get_long_term_key(self, rand, ediv): + if not self.sc and not self.completed: + if rand == self.ltk_rand and ediv == self.ltk_ediv: + return self.stk + else: + return self.ltk + + def decide_pairing_method(self, auth_req, initiator_io_capability, responder_io_capability): + if (not self.mitm) and (auth_req & SMP_MITM_AUTHREQ == 0): + self.pairing_method = self.JUST_WORKS + return + + details = self.PAIRING_METHODS[initiator_io_capability][responder_io_capability] + if type(details) is tuple and len(details) == 2: + # One entry for legacy pairing and one for secure connections + details = details[1 if self.sc else 0] + if type(details) is int: + # Just a method ID + self.pairing_method = details + else: + # PASSKEY method, with a method ID and display/input flags + self.pairing_method = details[0] + self.passkey_display = details[1 if self.is_initiator else 2] + + def check_expected_value(self, expected, received, error): + logger.debug(f'expected={expected.hex()} got={received.hex()}') + if expected != received: + logger.info(color('pairing confirm/check mismatch', 'red')) + self.send_pairing_failed(error) + return False + return True + + def prompt_user_for_numeric_comparison(self, code, next_steps): + async def prompt(): + logger.debug(f'verification code: {code}') + try: + response = await self.pairing_config.delegate.compare_numbers(code) + if response: + next_steps() + return + except Exception as error: + logger.warn(f'exception while prompting: {error}') + + self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR) + + asyncio.create_task(prompt()) + + def prompt_user_for_number(self, next_steps): + async def prompt(): + logger.debug('prompting user for passkey') + try: + passkey = await self.pairing_config.delegate.get_number() + logger.debug(f'user input: {passkey}') + next_steps(passkey) + except Exception as error: + logger.warn(f'exception while prompting: {error}') + self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR) + + asyncio.create_task(prompt()) + + def display_passkey(self): + # Generate random Passkey/PIN code + self.passkey = secrets.randbelow(1000000) + logger.debug(f'Pairing PIN CODE: {self.passkey:06}') + + # The value of TK is computed from the PIN code + if not self.sc: + self.tk = self.passkey.to_bytes(16, byteorder='little') + logger.debug(f'TK from passkey = {self.tk.hex()}') + + asyncio.create_task(self.pairing_config.delegate.display_number(self.passkey)) + + def input_passkey(self, next_steps=None): + # Prompt the user for the passkey displayed on the peer + def after_input(passkey): + self.passkey = passkey + + if not self.sc: + self.tk = passkey.to_bytes(16, byteorder='little') + logger.debug(f'TK from passkey = {self.tk.hex()}') + + if next_steps is not None: + next_steps() + self.prompt_user_for_number(after_input) + + def display_or_input_passkey(self, next_steps=None): + if self.passkey_display: + self.display_passkey() + if next_steps is not None: + next_steps() + else: + self.input_passkey(next_steps) + + def send_command(self, command): + self.manager.send_command(self.connection, command) + + def send_pairing_failed(self, error): + self.send_command(SMP_Pairing_Failed_Command(reason = error)) + self.on_pairing_failure(error) + + def send_pairing_request_command(self): + self.manager.on_session_start(self) + + command = SMP_Pairing_Request_Command( + io_capability = self.io_capability, + oob_data_flag = 0, + auth_req = self.auth_req, + maximum_encryption_key_size = 16, + initiator_key_distribution = self.initiator_key_distribution, + responder_key_distribution = self.responder_key_distribution + ) + self.preq = bytes(command) + self.send_command(command) + + def send_pairing_response_command(self): + response = SMP_Pairing_Response_Command( + io_capability = self.io_capability, + oob_data_flag = 0, + auth_req = self.auth_req, + maximum_encryption_key_size = 16, + initiator_key_distribution = self.initiator_key_distribution, + responder_key_distribution = self.responder_key_distribution + ) + self.pres = bytes(response) + self.send_command(response) + + def send_pairing_confirm_command(self): + self.r = crypto.r() + logger.debug(f'generated random: {self.r.hex()}') + + if self.sc: + if self.pairing_method == self.JUST_WORKS or self.pairing_method == self.NUMERIC_COMPARISON: + z = 0 + elif self.pairing_method == self.PASSKEY: + z = 0x80 + ((self.passkey >> self.passkey_step) & 1) + else: + return + + if self.is_initiator: + confirm_value = crypto.f4( + self.pka, + self.pkb, + self.r, + bytes([z]) + ) + else: + confirm_value = crypto.f4( + self.pkb, + self.pka, + self.r, + bytes([z]) + ) + else: + confirm_value = crypto.c1( + self.tk, + self.r, + self.preq, + self.pres, + self.iat, + self.rat, + self.ia, + self.ra + ) + + self.send_command(SMP_Pairing_Confirm_Command(confirm_value = confirm_value)) + + def send_pairing_random_command(self): + self.send_command(SMP_Pairing_Random_Command(random_value = self.r)) + + def send_public_key_command(self): + self.send_command( + SMP_Pairing_Public_Key_Command( + public_key_x = bytes(reversed(self.manager.ecc_key.x)), + public_key_y = bytes(reversed(self.manager.ecc_key.y)) + ) + ) + + def send_pairing_dhkey_check_command(self): + self.send_command( + SMP_Pairing_DHKey_Check_Command( + dhkey_check = self.ea if self.is_initiator else self.eb + ) + ) + + def start_encryption(self, key): + # We can now encrypt the connection with the short term key, so that we can + # distribute the long term and/or other keys over an encrypted connection + asyncio.create_task( + self.manager.device.host.send_command( + HCI_LE_Start_Encryption_Command( + connection_handle = self.connection.handle, + random_number = bytes(8), + encrypted_diversifier = 0, + long_term_key = key + ) + ) + ) + + def distribute_keys(self): + # Distribute the keys as required + if self.is_initiator: + if not self.sc: + # Distribute the LTK, EDIV and RAND + if self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG: + self.send_command(SMP_Encryption_Information_Command(long_term_key=self.ltk)) + self.send_command(SMP_Master_Identification_Command(ediv=self.ltk_ediv, rand=self.ltk_rand)) + + # Distribute IRK + if self.initiator_key_distribution & SMP_ID_KEY_DISTRIBUTION_FLAG: + self.send_command( + SMP_Identity_Information_Command(identity_resolving_key=self.manager.device.irk) + ) + + # Distribute BD ADDR + self.send_command(SMP_Identity_Address_Information_Command( + addr_type = self.manager.address.address_type, + bd_addr = self.manager.address + )) + + # Distribute CSRK + csrk = bytes(16) # FIXME: testing + if self.initiator_key_distribution & SMP_SIGN_KEY_DISTRIBUTION_FLAG: + self.send_command(SMP_Signing_Information_Command(signature_key=csrk)) + else: + # Distribute the LTK + if not self.sc: + if self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG: + self.send_command(SMP_Encryption_Information_Command(long_term_key=self.ltk)) + + # Distribute EDIV and RAND + self.send_command(SMP_Master_Identification_Command(ediv=self.ltk_ediv, rand=self.ltk_rand)) + + # Distribute IRK + if self.responder_key_distribution & SMP_ID_KEY_DISTRIBUTION_FLAG: + self.send_command( + SMP_Identity_Information_Command(identity_resolving_key=self.manager.device.irk) + ) + + # Distribute BD ADDR + self.send_command(SMP_Identity_Address_Information_Command( + addr_type = self.manager.address.address_type, + bd_addr = self.manager.address + )) + + # Distribute CSRK + csrk = bytes(16) # FIXME: testing + if self.responder_key_distribution & SMP_SIGN_KEY_DISTRIBUTION_FLAG: + self.send_command(SMP_Signing_Information_Command(signature_key=csrk)) + + def compute_peer_expected_distributions(self, key_distribution_flags): + # Set our expectations for what to wait for in the key distribution phase + self.peer_expected_distributions = [] + if not self.sc: + if (key_distribution_flags & SMP_ENC_KEY_DISTRIBUTION_FLAG != 0): + self.peer_expected_distributions.append(SMP_Encryption_Information_Command) + self.peer_expected_distributions.append(SMP_Master_Identification_Command) + if (key_distribution_flags & SMP_ID_KEY_DISTRIBUTION_FLAG != 0): + self.peer_expected_distributions.append(SMP_Identity_Information_Command) + self.peer_expected_distributions.append(SMP_Identity_Address_Information_Command) + if (key_distribution_flags & SMP_SIGN_KEY_DISTRIBUTION_FLAG != 0): + self.peer_expected_distributions.append(SMP_Signing_Information_Command) + logger.debug(f'expecting distributions: {[c.__name__ for c in self.peer_expected_distributions]}') + + def check_key_distribution(self, command_class): + # First, check that the connection is encrypted + if not self.connection.is_encrypted: + logger.warn(color('received key distribution on a non-encrypted connection', 'red')) + self.send_pairing_failed(SMP_UNSPECIFIED_REASON_ERROR) + return + + # Check that this command class is expected + if command_class in self.peer_expected_distributions: + self.peer_expected_distributions.remove(command_class) + logger.debug(f'remaining distributions: {[c.__name__ for c in self.peer_expected_distributions]}') + if not self.peer_expected_distributions: + # The initiator can now send its keys + if self.is_initiator: + self.distribute_keys() + + # Nothing left to expect, we're done + self.on_pairing() + else: + logger.warn(color('!!! unexpected key distribution command', 'red')) + self.send_pairing_failed(SMP_UNSPECIFIED_REASON_ERROR) + + async def pair(self): + # Start pairing as an initiator + # TODO: check that this session isn't already active + + # Send the pairing request to start the process + self.send_pairing_request_command() + + # Wait for the pairing process to finish + await self.pairing_result + + def on_disconnection(self, reason): + self.connection.remove_listener('disconnection', self.on_disconnection) + self.connection.remove_listener('connection_encryption_change', self.on_connection_encryption_change) + self.connection.remove_listener('connection_encryption_key_refresh', self.on_connection_encryption_key_refresh) + self.manager.on_session_end(self) + + def on_connection_encryption_change(self): + if self.connection.is_encrypted: + if self.is_responder: + # The responder distributes its keys first, the initiator later + self.distribute_keys() + + def on_connection_encryption_key_refresh(self): + # Do as if the connection had just been encrypted + self.on_connection_encryption_change() + + def on_pairing(self): + logger.debug('pairing complete') + + if self.completed: + return + else: + self.completed = True + + if self.pairing_result is not None and not self.pairing_result.done(): + self.pairing_result.set_result(None) + + # Use the peer address from the pairing protocol or the connection + if self.peer_bd_addr: + peer_address = self.peer_bd_addr + else: + peer_address = self.connection.peer_address + + # Create an object to hold the keys + keys = PairingKeys() + keys.address_type = peer_address.address_type + authenticated = self.pairing_method != self.JUST_WORKS + if self.sc: + keys.ltk = PairingKeys.Key( + value = self.ltk, + authenticated = authenticated + ) + else: + our_ltk_key = PairingKeys.Key( + value = self.ltk, + authenticated = authenticated, + ediv = self.ltk_ediv, + rand = self.ltk_rand + ) + peer_ltk_key = PairingKeys.Key( + value = self.peer_ltk, + authenticated = authenticated, + ediv = self.peer_ediv, + rand = self.peer_rand + ) + if self.is_initiator: + keys.ltk_central = peer_ltk_key + keys.ltk_peripheral = our_ltk_key + else: + keys.ltk_central = our_ltk_key + keys.ltk_peripheral = peer_ltk_key + if self.peer_identity_resolving_key is not None: + keys.irk = PairingKeys.Key( + value = self.peer_identity_resolving_key, + authenticated = authenticated + ) + if self.peer_signature_key is not None: + keys.csrk = PairingKeys.Key( + value = self.peer_signature_key, + authenticated = authenticated + ) + + self.manager.on_pairing(self, peer_address, keys) + + def on_pairing_failure(self, reason): + logger.warn(f'pairing failure ({error_name(reason)})') + + if self.completed: + return + else: + self.completed = True + + error = ProtocolError(reason, 'smp', error_name(reason)) + if self.pairing_result is not None and not self.pairing_result.done(): + self.pairing_result.set_exception(error) + self.manager.on_pairing_failure(self, reason) + + def on_smp_command(self, command): + # Find the handler method + handler_name = f'on_{command.name.lower()}' + handler = getattr(self, handler_name, None) + if handler is not None: + try: + handler(command) + except Exception as error: + logger.warning(f'{color("!!! Exception in handler:", "red")} {error}') + response = SMP_Pairing_Failed_Command(reason = SMP_UNSPECIFIED_REASON_ERROR) + self.send_command(response) + else: + logger.error(color('SMP command not handled???', 'red')) + + def on_smp_pairing_request_command(self, command): + asyncio.create_task(self.on_smp_pairing_request_command_async(command)) + + async def on_smp_pairing_request_command_async(self, command): + # Check if the request should proceed + accepted = await self.pairing_config.delegate.accept() + if not accepted: + logger.debug('pairing rejected by delegate') + self.send_pairing_failed(SMP_PAIRING_NOT_SUPPORTED_ERROR) + return + + # Save the request + self.preq = bytes(command) + + # Bonding and SC require both sides to request/support it + self.bonding = self.bonding and (command.auth_req & SMP_BONDING_AUTHREQ != 0) + self.sc = self.sc and (command.auth_req & SMP_SC_AUTHREQ != 0) + + # Check for OOB + if command.oob_data_flag != 0: + self.terminate(SMP_OOB_NOT_AVAILABLE_ERROR) + return + + # Decide which pairing method to use + self.decide_pairing_method( + command.auth_req, + command.io_capability, + self.io_capability + ) + logger.debug(f'pairing method: {self.PAIRING_METHOD_NAMES[self.pairing_method]}') + + # Key distribution + self.initiator_key_distribution &= command.initiator_key_distribution + self.responder_key_distribution &= command.responder_key_distribution + self.compute_peer_expected_distributions(self.initiator_key_distribution) + + # The pairing is now starting + self.manager.on_session_start(self) + + # Display a passkey if we need to + if not self.sc: + if self.pairing_method == self.PASSKEY and self.passkey_display: + self.display_passkey() + + # Respond + self.send_pairing_response_command() + + def on_smp_pairing_response_command(self, command): + if self.is_responder: + logger.warn(color('received pairing response as a responder', 'red')) + return + + # Save the response + self.pres = bytes(command) + self.peer_io_capability = command.io_capability + + # Bonding and SC require both sides to request/support it + self.bonding = self.bonding and (command.auth_req & SMP_BONDING_AUTHREQ != 0) + self.sc = self.sc and (command.auth_req & SMP_SC_AUTHREQ != 0) + + # Check for OOB + if self.sc and command.oob_data_flag: + self.send_pairing_failed(SMP_OOB_NOT_AVAILABLE_ERROR) + return + + # Decide which pairing method to use + self.decide_pairing_method( + command.auth_req, + self.io_capability, + command.io_capability + ) + logger.debug(f'pairing method: {self.PAIRING_METHOD_NAMES[self.pairing_method]}') + + # Key distribution + if (command.initiator_key_distribution & ~self.initiator_key_distribution != 0) or \ + (command.responder_key_distribution & ~self.responder_key_distribution != 0): + # The response isn't a subset of the request + self.send_pairing_failed(SMP_INVALID_PARAMETERS_ERROR) + return + self.initiator_key_distribution = command.initiator_key_distribution + self.responder_key_distribution = command.responder_key_distribution + self.compute_peer_expected_distributions(self.responder_key_distribution) + + # Start phase 2 + if self.sc: + if self.pairing_method == self.PASSKEY and self.passkey_display: + self.display_passkey() + + self.send_public_key_command() + else: + if self.pairing_method == self.PASSKEY: + self.display_or_input_passkey(self.send_pairing_confirm_command) + else: + self.send_pairing_confirm_command() + + def on_smp_pairing_confirm_command_legacy(self, command): + if self.is_initiator: + self.send_pairing_random_command() + else: + # If the method is PASSKEY, now is the time to input the code + if self.pairing_method == self.PASSKEY and not self.passkey_display: + self.input_passkey(self.send_pairing_confirm_command) + else: + self.send_pairing_confirm_command() + + def on_smp_pairing_confirm_command_secure_connections(self, command): + if self.pairing_method == self.JUST_WORKS or self.pairing_method == self.NUMERIC_COMPARISON: + if self.is_initiator: + self.r = crypto.r() + self.send_pairing_random_command() + elif self.pairing_method == self.PASSKEY: + if self.is_initiator: + self.send_pairing_random_command() + else: + self.send_pairing_confirm_command() + + def on_smp_pairing_confirm_command(self, command): + self.confirm_value = command.confirm_value + if self.sc: + self.on_smp_pairing_confirm_command_secure_connections(command) + else: + self.on_smp_pairing_confirm_command_legacy(command) + + def on_smp_pairing_random_command_legacy(self, command): + # Check that the confirmation values match + confirm_verifier = crypto.c1( + self.tk, + command.random_value, + self.preq, + self.pres, + self.iat, + self.rat, + self.ia, + self.ra + ) + if not self.check_expected_value( + self.confirm_value, + confirm_verifier, + SMP_CONFIRM_VALUE_FAILED_ERROR + ): + return + + # Compute STK + if self.is_initiator: + mrand = self.r + srand = command.random_value + else: + srand = self.r + mrand = command.random_value + stk = crypto.s1(self.tk, srand, mrand) + logger.debug(f'STK = {stk.hex()}') + + # Generate LTK + self.ltk = crypto.r() + + if self.is_initiator: + self.start_encryption(stk) + else: + self.send_pairing_random_command() + + def on_smp_pairing_random_command_secure_connections(self, command): + if self.is_initiator: + if self.pairing_method == self.JUST_WORKS or self.pairing_method == self.NUMERIC_COMPARISON: + # Check that the random value matches what was committed to earlier + confirm_verifier = crypto.f4( + self.pkb, + self.pka, + command.random_value, + bytes([0]) + ) + if not self.check_expected_value( + self.confirm_value, + confirm_verifier, + SMP_CONFIRM_VALUE_FAILED_ERROR + ): + return + elif self.pairing_method == self.PASSKEY: + # Check that the random value matches what was committed to earlier + confirm_verifier = crypto.f4( + self.pkb, + self.pka, + command.random_value, + bytes([0x80 + ((self.passkey >> self.passkey_step) & 1)]) + ) + if not self.check_expected_value( + self.confirm_value, + confirm_verifier, + SMP_CONFIRM_VALUE_FAILED_ERROR + ): + return + + # Move on to the next iteration + self.passkey_step += 1 + logger.debug(f'passkey finished step {self.passkey_step} of 20') + if self.passkey_step < 20: + self.send_pairing_confirm_command() + return + else: + return + else: + if self.pairing_method == self.JUST_WORKS or self.pairing_method == self.NUMERIC_COMPARISON: + self.send_pairing_random_command() + elif self.pairing_method == self.PASSKEY: + # Check that the random value matches what was committed to earlier + confirm_verifier = crypto.f4( + self.pka, + self.pkb, + command.random_value, + bytes([0x80 + ((self.passkey >> self.passkey_step) & 1)]) + ) + if not self.check_expected_value( + self.confirm_value, + confirm_verifier, + SMP_CONFIRM_VALUE_FAILED_ERROR + ): + return + + self.send_pairing_random_command() + + # Move on to the next iteration + self.passkey_step += 1 + logger.debug(f'passkey finished step {self.passkey_step} of 20') + if self.passkey_step < 20: + self.r = crypto.r() + return + else: + return + + # Compute the MacKey and LTK + a = self.ia + bytes([self.iat]) + b = self.ra + bytes([self.rat]) + (mac_key, self.ltk) = crypto.f5(self.dh_key, self.na, self.nb, a, b) + + # Compute the DH Key checks + if self.pairing_method == self.JUST_WORKS or self.pairing_method == self.NUMERIC_COMPARISON: + ra = bytes(16) + rb = ra + elif self.pairing_method == self.PASSKEY: + ra = self.passkey.to_bytes(16, byteorder='little') + rb = ra + else: + # OOB not implemented yet + return + + io_cap_a = self.preq[1:4] + io_cap_b = self.pres[1:4] + self.ea = crypto.f6(mac_key, self.na, self.nb, rb, io_cap_a, a, b) + self.eb = crypto.f6(mac_key, self.nb, self.na, ra, io_cap_b, b, a) + + # Next steps to be performed after possible user confirmation + def next_steps(): + # The initiator sends the DH Key check to the responder + if self.is_initiator: + self.send_pairing_dhkey_check_command() + else: + if self.wait_before_continuing: + self.wait_before_continuing.set_result(None) + + # Prompt the user for confirmation if needed + if self.pairing_method == self.JUST_WORKS or self.pairing_method == self.NUMERIC_COMPARISON: + # Compute the 6-digit code + code = crypto.g2(self.pka, self.pkb, self.na, self.nb) % 1000000 + + if self.pairing_method == self.NUMERIC_COMPARISON: + # Ask for user confirmation + self.wait_before_continuing = asyncio.get_running_loop().create_future() + self.prompt_user_for_numeric_comparison(code, next_steps) + else: + next_steps() + else: + next_steps() + + def on_smp_pairing_random_command(self, command): + self.peer_random_value = command.random_value + if self.sc: + self.on_smp_pairing_random_command_secure_connections(command) + else: + self.on_smp_pairing_random_command_legacy(command) + + def on_smp_pairing_public_key_command(self, command): + # Store the public key so that we can compute the confirmation value later + self.peer_public_key_x = command.public_key_x + self.peer_public_key_y = command.public_key_y + + # Compute the DH key + self.dh_key = bytes(reversed(self.manager.ecc_key.dh( + bytes(reversed(command.public_key_x)), + bytes(reversed(command.public_key_y)) + ))) + logger.debug(f'DH key: {self.dh_key.hex()}') + + if self.is_initiator: + if self.pairing_method == self.PASSKEY: + if self.passkey_display: + self.send_pairing_confirm_command() + else: + self.input_passkey(self.send_pairing_confirm_command) + else: + # Send our public key back to the initiator + if self.pairing_method == self.PASSKEY: + self.display_or_input_passkey(self.send_public_key_command) + else: + self.send_public_key_command() + + if self.pairing_method == self.JUST_WORKS or self.pairing_method == self.NUMERIC_COMPARISON: + # We can now send the confirmation value + self.send_pairing_confirm_command() + + def on_smp_pairing_dhkey_check_command(self, command): + # Check that what we received matches what we computed earlier + expected = self.eb if self.is_initiator else self.ea + if not self.check_expected_value( + expected, + command.dhkey_check, + SMP_DHKEY_CHECK_FAILED_ERROR + ): + return + + if self.is_responder: + if self.wait_before_continuing is not None: + async def next_steps(): + await self.wait_before_continuing + self.wait_before_continuing = None + self.send_pairing_dhkey_check_command() + + asyncio.create_task(next_steps()) + else: + self.send_pairing_dhkey_check_command() + else: + self.start_encryption(self.ltk) + + def on_smp_pairing_failed_command(self, command): + self.on_pairing_failure(command.reason) + + def on_smp_encryption_information_command(self, command): + self.peer_ltk = command.long_term_key + self.check_key_distribution(SMP_Encryption_Information_Command) + + def on_smp_master_identification_command(self, command): + self.peer_ediv = command.ediv + self.peer_rand = command.rand + self.check_key_distribution(SMP_Master_Identification_Command) + + def on_smp_identity_information_command(self, command): + self.peer_identity_resolving_key = command.identity_resolving_key + self.check_key_distribution(SMP_Identity_Information_Command) + + def on_smp_identity_address_information_command(self, command): + self.peer_bd_addr = command.bd_addr + self.check_key_distribution(SMP_Identity_Address_Information_Command) + + def on_smp_signing_information_command(self, command): + self.peer_signature_key = command.signature_key + self.check_key_distribution(SMP_Signing_Information_Command) + + +# ----------------------------------------------------------------------------- +class Manager(EventEmitter): + ''' + Implements the Initiator and Responder roles of the Security Manager Protocol + ''' + + def __init__(self, device, address): + super().__init__() + self.device = device + self.address = address + self.sessions = {} + self._ecc_key = None + self.pairing_config_factory = lambda connection: PairingConfig() + + def send_command(self, connection, command): + logger.debug(f'>>> Sending SMP Command on connection [0x{connection.handle:04X}] {connection.peer_address}: {command}') + connection.send_l2cap_pdu(SMP_CID, command.to_bytes()) + + def on_smp_pdu(self, connection, pdu): + # Look for a session with this connection, and create one if none exists + if not (session := self.sessions.get(connection.handle)): + pairing_config = self.pairing_config_factory(connection) + if pairing_config is None: + # Pairing disabled + self.send_command( + connection, + SMP_Pairing_Failed_Command( + reason = SMP_PAIRING_NOT_SUPPORTED_ERROR + ) + ) + return + session = Session(self, connection, pairing_config) + self.sessions[connection.handle] = session + + # Parse the L2CAP payload into an SMP Command object + command = SMP_Command.from_bytes(pdu) + logger.debug(f'<<< Received SMP Command on connection [0x{connection.handle:04X}] {connection.peer_address}: {command}') + + # Delegate the handling of the command to the session + session.on_smp_command(command) + + @property + def ecc_key(self): + if self._ecc_key is None: + self._ecc_key = crypto.EccKey.generate() + return self._ecc_key + + async def pair(self, connection): + # TODO: check if there's already a session for this connection + pairing_config = self.pairing_config_factory(connection) + if pairing_config is None: + raise ValueError('pairing config must not be None when initiating') + session = Session(self, connection, pairing_config) + self.sessions[connection.handle] = session + return await session.pair() + + def request_pairing(self, connection): + pairing_config = self.pairing_config_factory(connection) + if pairing_config: + auth_req = smp_auth_req( + pairing_config.bonding, + pairing_config.mitm, + pairing_config.sc, + False, + False + ) + else: + auth_req = 0 + self.send_command(connection, SMP_Security_Request_Command(auth_req=auth_req)) + + def on_session_start(self, session): + self.device.on_pairing_start(session.connection.handle) + + def on_pairing(self, session, identity_address, keys): + # Store the keys in the key store + if self.device.keystore and identity_address is not None: + async def store_keys(): + try: + await self.device.keystore.update(str(identity_address), keys) + except Exception as error: + logger.warn(f'!!! error while storing keys: {error}') + asyncio.create_task(store_keys()) + + # Notify the device + self.device.on_pairing(session.connection.handle, keys) + + def on_pairing_failure(self, session, reason): + self.device.on_pairing_failure(session.connection.handle, reason) + + def on_session_end(self, session): + logger.debug(f'session end for connection 0x{session.connection.handle:04X}') + if session.connection.handle in self.sessions: + del self.sessions[session.connection.handle] + + def get_long_term_key(self, connection, rand, ediv): + if session := self.sessions.get(connection.handle): + return session.get_long_term_key(rand, ediv) diff --git a/bumble/transport/__init__.py b/bumble/transport/__init__.py new file mode 100644 index 0000000..c3bd5f8 --- /dev/null +++ b/bumble/transport/__init__.py @@ -0,0 +1,95 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging + +from .common import Transport, AsyncPipeSink +from ..link import RemoteLink +from ..controller import Controller + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_transport(name): + ''' + Open a transport by name. + The name must be : + Where depend on the type (and may be empty for some types). + The supported types are: serial,udp,tcp,pty,usb + ''' + scheme, *spec = name.split(':', 1) + if scheme == 'serial' and spec: + from .serial import open_serial_transport + return await open_serial_transport(spec[0]) + elif scheme == 'udp' and spec: + from .udp import open_udp_transport + return await open_udp_transport(spec[0]) + elif scheme == 'tcp-client' and spec: + from .tcp_client import open_tcp_client_transport + return await open_tcp_client_transport(spec[0]) + elif scheme == 'tcp-server' and spec: + from .tcp_server import open_tcp_server_transport + return await open_tcp_server_transport(spec[0]) + elif scheme == 'ws-client' and spec: + from .ws_client import open_ws_client_transport + return await open_ws_client_transport(spec[0]) + elif scheme == 'ws-server' and spec: + from .ws_server import open_ws_server_transport + return await open_ws_server_transport(spec[0]) + elif scheme == 'pty': + from .pty import open_pty_transport + return await open_pty_transport(spec[0] if spec else None) + elif scheme == 'file': + from .file import open_file_transport + return await open_file_transport(spec[0] if spec else None) + elif scheme == 'vhci': + from .vhci import open_vhci_transport + return await open_vhci_transport(spec[0] if spec else None) + elif scheme == 'hci-socket': + from .hci_socket import open_hci_socket_transport + return await open_hci_socket_transport(spec[0] if spec else None) + elif scheme == 'usb': + from .usb import open_usb_transport + return await open_usb_transport(spec[0] if spec else None) + elif scheme == 'pyusb': + from .pyusb import open_pyusb_transport + return await open_pyusb_transport(spec[0] if spec else None) + elif scheme == 'android-emulator': + from .android_emulator import open_android_emulator_transport + return await open_android_emulator_transport(spec[0] if spec else None) + else: + raise ValueError('unknown transport scheme') + + +# ----------------------------------------------------------------------------- +async def open_transport_or_link(name): + if name.startswith('link-relay:'): + link = RemoteLink(name[11:]) + await link.wait_until_connected() + controller = Controller('remote', link = link) + + class LinkTransport(Transport): + async def close(self): + link.close() + + return LinkTransport(controller, AsyncPipeSink(controller)) + else: + return await open_transport(name) diff --git a/bumble/transport/android_emulator.py b/bumble/transport/android_emulator.py new file mode 100644 index 0000000..d27aef6 --- /dev/null +++ b/bumble/transport/android_emulator.py @@ -0,0 +1,107 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import grpc + +from .common import PumpedTransport, PumpedPacketSource, PumpedPacketSink +from .emulated_bluetooth_pb2_grpc import EmulatedBluetoothServiceStub +from .emulated_bluetooth_packets_pb2 import HCIPacket +from .emulated_bluetooth_vhci_pb2_grpc import VhciForwardingServiceStub + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_android_emulator_transport(spec): + ''' + Open a transport connection to an Android emulator via its gRPC interface. + The parameter string has this syntax: + [:][,mode=] + The : part is optional, it defaults to localhost:8554 + The mode= part is optional, it defaults to mode=host + When the mode is set to 'controller', the connection is for a controller (i.e the + Android Bluetooth stack will use the connected endpoint as its controller). When + the mode is set to 'host', the connection is to the 'Root Canal' virtual controller + that runs as part of the emulator, and used by the Android Bluetooth stack. + + Examples: + (empty string) --> connect as a host to the emulator on localhost:8554 + localhost:8555 --> connect as a host to the emulator on localhost:8555 + mode=controller --> connect as a controller to the emulator on localhost:8554 + ''' + + # Wrapper for I/O operations + class HciDevice: + def __init__(self, hci_device): + self.hci_device = hci_device + + async def read(self): + packet = await self.hci_device.read() + return bytes([packet.type]) + packet.packet + + async def write(self, packet): + await self.hci_device.write( + HCIPacket( + type = packet[0], + packet = packet[1:] + ) + ) + + # Parse the parameters + mode = 'host' + server_host = 'localhost' + server_port = 8554 + if spec is not None: + params = spec.split(',') + for param in params: + if param.startswith('mode='): + mode = param.split('=')[1] + elif ':' in param: + server_host, server_port = param.split(':') + else: + raise ValueError('invalid parameter') + + # Connect to the gRPC server + server_address = f'{server_host}:{server_port}' + logger.debug(f'connecting to gRPC server at {server_address}') + channel = grpc.aio.insecure_channel(server_address) + + if mode == 'host': + # Connect as a host + service = EmulatedBluetoothServiceStub(channel) + hci_device = HciDevice(service.registerHCIDevice()) + elif mode == 'controller': + # Connect as a controller + service = VhciForwardingServiceStub(channel) + hci_device = HciDevice(service.attachVhci()) + else: + raise ValueError('invalid mode') + + # Create the transport object + transport = PumpedTransport( + PumpedPacketSource(hci_device.read), + PumpedPacketSink(hci_device.write), + channel.close + ) + transport.start() + + return transport diff --git a/bumble/transport/common.py b/bumble/transport/common.py new file mode 100644 index 0000000..d5c1ae9 --- /dev/null +++ b/bumble/transport/common.py @@ -0,0 +1,326 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import struct +import asyncio +import logging +from colors import color + +from .. import hci + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + +# ----------------------------------------------------------------------------- +# Information needed to parse HCI packets with a generic parser: +# For each packet type, the info represents: +# (length-size, length-offset, unpack-type) +HCI_PACKET_INFO = { + hci.HCI_COMMAND_PACKET: (1, 2, 'B'), + hci.HCI_ACL_DATA_PACKET: (2, 2, 'H'), + hci.HCI_SYNCHRONOUS_DATA_PACKET: (1, 2, 'B'), + hci.HCI_EVENT_PACKET: (1, 1, 'B') +} + + +# ----------------------------------------------------------------------------- +class PacketPump: + ''' + Pump HCI packets from a reader to a sink + ''' + + def __init__(self, reader, sink): + self.reader = reader + self.sink = sink + + async def run(self): + while True: + try: + # Get a packet from the source + packet = hci.HCI_Packet.from_bytes(await self.reader.next_packet()) + + # Deliver the packet to the sink + self.sink.on_packet(packet) + except Exception as error: + logger.warning(f'!!! {error}') + + +# ----------------------------------------------------------------------------- +class PacketParser: + ''' + In-line parser that accepts data and emits 'on_packet' when a full packet has been parsed + ''' + NEED_TYPE = 0 + NEED_LENGTH = 1 + NEED_BODY = 2 + + def __init__(self, sink = None): + self.sink = sink + self.extended_packet_info = {} + self.reset() + + def reset(self): + self.state = PacketParser.NEED_TYPE + self.bytes_needed = 1 + self.packet = bytearray() + self.packet_info = None + + def feed_data(self, data): + data_offset = 0 + data_left = len(data) + while data_left and self.bytes_needed: + consumed = min(self.bytes_needed, data_left) + self.packet.extend(data[data_offset:data_offset + consumed]) + data_offset += consumed + data_left -= consumed + self.bytes_needed -= consumed + + if self.bytes_needed == 0: + if self.state == PacketParser.NEED_TYPE: + packet_type = self.packet[0] + self.packet_info = HCI_PACKET_INFO.get(packet_type) or self.extended_packet_info.get(packet_type) + if self.packet_info is None: + raise ValueError(f'invalid packet type {packet_type}') + self.state = PacketParser.NEED_LENGTH + self.bytes_needed = self.packet_info[0] + self.packet_info[1] + elif self.state == PacketParser.NEED_LENGTH: + body_length = struct.unpack_from(self.packet_info[2], self.packet, 1 + self.packet_info[1])[0] + self.bytes_needed = body_length + self.state = PacketParser.NEED_BODY + + # Emit a packet if one is complete + if self.state == PacketParser.NEED_BODY and not self.bytes_needed: + if self.sink: + try: + self.sink.on_packet(bytes(self.packet)) + except Exception as error: + logger.warning(color(f'!!! Exception in on_packet: {error}', 'red')) + self.reset() + + def set_packet_sink(self, sink): + self.sink = sink + + +# ----------------------------------------------------------------------------- +class PacketReader: + ''' + Reader that reads HCI packets from a sync source + ''' + + def __init__(self, source): + self.source = source + + def next_packet(self): + # Get the packet type + packet_type = self.source.read(1) + if len(packet_type) != 1: + return None + + # Get the packet info based on its type + packet_info = HCI_PACKET_INFO.get(packet_type[0]) + if packet_info is None: + raise ValueError(f'invalid packet type {packet_type} found') + + # Read the header (that includes the length) + header_size = packet_info[0] + packet_info[1] + header = self.source.read(header_size) + if len(header) != header_size: + raise ValueError('packet too short') + + # Read the body + body_length = struct.unpack_from(packet_info[2], header, packet_info[1])[0] + body = self.source.read(body_length) + if len(body) != body_length: + raise ValueError('packet too short') + + return packet_type + header + body + + +# ----------------------------------------------------------------------------- +class AsyncPacketReader: + ''' + Reader that reads HCI packets from an async source + ''' + + def __init__(self, source): + self.source = source + + async def next_packet(self): + # Get the packet type + packet_type = await self.source.readexactly(1) + + # Get the packet info based on its type + packet_info = HCI_PACKET_INFO.get(packet_type[0]) + if packet_info is None: + raise ValueError(f'invalid packet type {packet_type} found') + + # Read the header (that includes the length) + header_size = packet_info[0] + packet_info[1] + header = await self.source.readexactly(header_size) + + # Read the body + body_length = struct.unpack_from(packet_info[2], header, packet_info[1])[0] + body = await self.source.readexactly(body_length) + + return packet_type + header + body + + +# ----------------------------------------------------------------------------- +class AsyncPipeSink: + ''' + Sink that forwards packets asynchronously to another sink + ''' + def __init__(self, sink): + self.sink = sink + self.loop = asyncio.get_running_loop() + + def on_packet(self, packet): + self.loop.call_soon(self.sink.on_packet, packet) + + +# ----------------------------------------------------------------------------- +class ParserSource: + """ + Base class designed to be subclassed by transport-specific source classes + """ + + def __init__(self): + self.parser = PacketParser() + self.terminated = asyncio.get_running_loop().create_future() + + def set_packet_sink(self, sink): + self.parser.set_packet_sink(sink) + + async def wait_for_termination(self): + return await self.terminated + + def close(self): + pass + + +# ----------------------------------------------------------------------------- +class StreamPacketSource(asyncio.Protocol, ParserSource): + def data_received(self, data): + self.parser.feed_data(data) + + +# ----------------------------------------------------------------------------- +class StreamPacketSink: + def __init__(self, transport): + self.transport = transport + + def on_packet(self, packet): + self.transport.write(packet) + + def close(self): + self.transport.close() + + +# ----------------------------------------------------------------------------- +class Transport: + def __init__(self, source, sink): + self.source = source + self.sink = sink + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + await self.close() + + def __iter__(self): + return iter((self.source, self.sink)) + + async def close(self): + self.source.close() + self.sink.close() + + +# ----------------------------------------------------------------------------- +class PumpedPacketSource(ParserSource): + def __init__(self, receive): + super().__init__() + self.receive_function = receive + self.pump_task = None + + def start(self): + async def pump_packets(): + while True: + try: + packet = await self.receive_function() + self.parser.feed_data(packet) + except asyncio.exceptions.CancelledError: + logger.debug('source pump task done') + break + except Exception as error: + logger.warn(f'exception while waiting for packet: {error}') + self.terminated.set_result(error) + break + + self.pump_task = asyncio.get_running_loop().create_task(pump_packets()) + + def close(self): + if self.pump_task: + self.pump_task.cancel() + + +# ----------------------------------------------------------------------------- +class PumpedPacketSink: + def __init__(self, send): + self.send_function = send + self.packet_queue = asyncio.Queue() + self.pump_task = None + + def on_packet(self, packet): + self.packet_queue.put_nowait(packet) + + def start(self): + async def pump_packets(): + while True: + try: + packet = await self.packet_queue.get() + await self.send_function(packet) + except asyncio.exceptions.CancelledError: + logger.debug('sink pump task done') + break + except Exception as error: + logger.warn(f'exception while sending packet: {error}') + break + + self.pump_task = asyncio.get_running_loop().create_task(pump_packets()) + + def close(self): + if self.pump_task: + self.pump_task.cancel() + + +# ----------------------------------------------------------------------------- +class PumpedTransport(Transport): + def __init__(self, source, sink, close_function): + super().__init__(source, sink) + self.close_function = close_function + + def start(self): + self.source.start() + self.sink.start() + + async def close(self): + await super().close() + await self.close_function() diff --git a/bumble/transport/emulated_bluetooth_packets_pb2.py b/bumble/transport/emulated_bluetooth_packets_pb2.py new file mode 100644 index 0000000..9d3591d --- /dev/null +++ b/bumble/transport/emulated_bluetooth_packets_pb2.py @@ -0,0 +1,52 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: emulated_bluetooth_packets.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n emulated_bluetooth_packets.proto\x12\x1b\x61ndroid.emulation.bluetooth\"\xfb\x01\n\tHCIPacket\x12?\n\x04type\x18\x01 \x01(\x0e\x32\x31.android.emulation.bluetooth.HCIPacket.PacketType\x12\x0e\n\x06packet\x18\x02 \x01(\x0c\"\x9c\x01\n\nPacketType\x12\x1b\n\x17PACKET_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n\x17PACKET_TYPE_HCI_COMMAND\x10\x01\x12\x13\n\x0fPACKET_TYPE_ACL\x10\x02\x12\x13\n\x0fPACKET_TYPE_SCO\x10\x03\x12\x15\n\x11PACKET_TYPE_EVENT\x10\x04\x12\x13\n\x0fPACKET_TYPE_ISO\x10\x05\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3') + + + +_HCIPACKET = DESCRIPTOR.message_types_by_name['HCIPacket'] +_HCIPACKET_PACKETTYPE = _HCIPACKET.enum_types_by_name['PacketType'] +HCIPacket = _reflection.GeneratedProtocolMessageType('HCIPacket', (_message.Message,), { + 'DESCRIPTOR' : _HCIPACKET, + '__module__' : 'emulated_bluetooth_packets_pb2' + # @@protoc_insertion_point(class_scope:android.emulation.bluetooth.HCIPacket) + }) +_sym_db.RegisterMessage(HCIPacket) + +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth' + _HCIPACKET._serialized_start=66 + _HCIPACKET._serialized_end=317 + _HCIPACKET_PACKETTYPE._serialized_start=161 + _HCIPACKET_PACKETTYPE._serialized_end=317 +# @@protoc_insertion_point(module_scope) diff --git a/bumble/transport/emulated_bluetooth_pb2.py b/bumble/transport/emulated_bluetooth_pb2.py new file mode 100644 index 0000000..4da12d5 --- /dev/null +++ b/bumble/transport/emulated_bluetooth_pb2.py @@ -0,0 +1,53 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: emulated_bluetooth.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18\x65mulated_bluetooth.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto\"\x19\n\x07RawData\x12\x0e\n\x06packet\x18\x01 \x01(\x0c\x32\xcb\x02\n\x18\x45mulatedBluetoothService\x12\x64\n\x12registerClassicPhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12`\n\x0eregisterBlePhy\x12$.android.emulation.bluetooth.RawData\x1a$.android.emulation.bluetooth.RawData(\x01\x30\x01\x12g\n\x11registerHCIDevice\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x42\"\n\x1e\x63om.android.emulator.bluetoothP\x01\x62\x06proto3') + + + +_RAWDATA = DESCRIPTOR.message_types_by_name['RawData'] +RawData = _reflection.GeneratedProtocolMessageType('RawData', (_message.Message,), { + 'DESCRIPTOR' : _RAWDATA, + '__module__' : 'emulated_bluetooth_pb2' + # @@protoc_insertion_point(class_scope:android.emulation.bluetooth.RawData) + }) +_sym_db.RegisterMessage(RawData) + +_EMULATEDBLUETOOTHSERVICE = DESCRIPTOR.services_by_name['EmulatedBluetoothService'] +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\036com.android.emulator.bluetoothP\001' + _RAWDATA._serialized_start=91 + _RAWDATA._serialized_end=116 + _EMULATEDBLUETOOTHSERVICE._serialized_start=119 + _EMULATEDBLUETOOTHSERVICE._serialized_end=450 +# @@protoc_insertion_point(module_scope) diff --git a/bumble/transport/emulated_bluetooth_pb2_grpc.py b/bumble/transport/emulated_bluetooth_pb2_grpc.py new file mode 100644 index 0000000..cc0ce37 --- /dev/null +++ b/bumble/transport/emulated_bluetooth_pb2_grpc.py @@ -0,0 +1,207 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2 +from . import emulated_bluetooth_pb2 as emulated__bluetooth__pb2 + + +class EmulatedBluetoothServiceStub(object): + """An Emulated Bluetooth Service exposes the emulated bluetooth chip from the + android emulator. It allows you to register emulated bluetooth devices and + control the packets that are exchanged between the device and the world. + + This service enables you to establish a "virtual network" of emulated + bluetooth devices that can interact with each other. + + Note: This is not yet finalized, it is likely that these definitions will + evolve. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.registerClassicPhy = channel.stream_stream( + '/android.emulation.bluetooth.EmulatedBluetoothService/registerClassicPhy', + request_serializer=emulated__bluetooth__pb2.RawData.SerializeToString, + response_deserializer=emulated__bluetooth__pb2.RawData.FromString, + ) + self.registerBlePhy = channel.stream_stream( + '/android.emulation.bluetooth.EmulatedBluetoothService/registerBlePhy', + request_serializer=emulated__bluetooth__pb2.RawData.SerializeToString, + response_deserializer=emulated__bluetooth__pb2.RawData.FromString, + ) + self.registerHCIDevice = channel.stream_stream( + '/android.emulation.bluetooth.EmulatedBluetoothService/registerHCIDevice', + request_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString, + response_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString, + ) + + +class EmulatedBluetoothServiceServicer(object): + """An Emulated Bluetooth Service exposes the emulated bluetooth chip from the + android emulator. It allows you to register emulated bluetooth devices and + control the packets that are exchanged between the device and the world. + + This service enables you to establish a "virtual network" of emulated + bluetooth devices that can interact with each other. + + Note: This is not yet finalized, it is likely that these definitions will + evolve. + """ + + def registerClassicPhy(self, request_iterator, context): + """Connect device to link layer. This will establish a direct connection + to the emulated bluetooth chip and configure the following: + + - Each connection creates a new device and attaches it to the link layer + - Link Layer packets are transmitted directly to the phy + + This should be used for classic connections. + + This is used to directly connect various android emulators together. + For example a wear device can connect to an android emulator through + this. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def registerBlePhy(self, request_iterator, context): + """Connect device to link layer. This will establish a direct connection + to root canal and execute the following: + + - Each connection creates a new device and attaches it to the link layer + - Link Layer packets are transmitted directly to the phy + + This should be used for BLE connections. + + This is used to directly connect various android emulators together. + For example a wear device can connect to an android emulator through + this. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def registerHCIDevice(self, request_iterator, context): + """Connect the device to the emulated bluetooth chip. The device will + participate in the network. You can configure the chip to scan, advertise + and setup connections with other devices that are connected to the + network. + + This is usually used when you have a need for an emulated bluetooth chip + and have a bluetooth stack that can interpret and handle the packets + correctly. + + For example the apache nimble stack can use this endpoint as the + transport layer. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_EmulatedBluetoothServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'registerClassicPhy': grpc.stream_stream_rpc_method_handler( + servicer.registerClassicPhy, + request_deserializer=emulated__bluetooth__pb2.RawData.FromString, + response_serializer=emulated__bluetooth__pb2.RawData.SerializeToString, + ), + 'registerBlePhy': grpc.stream_stream_rpc_method_handler( + servicer.registerBlePhy, + request_deserializer=emulated__bluetooth__pb2.RawData.FromString, + response_serializer=emulated__bluetooth__pb2.RawData.SerializeToString, + ), + 'registerHCIDevice': grpc.stream_stream_rpc_method_handler( + servicer.registerHCIDevice, + request_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString, + response_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'android.emulation.bluetooth.EmulatedBluetoothService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class EmulatedBluetoothService(object): + """An Emulated Bluetooth Service exposes the emulated bluetooth chip from the + android emulator. It allows you to register emulated bluetooth devices and + control the packets that are exchanged between the device and the world. + + This service enables you to establish a "virtual network" of emulated + bluetooth devices that can interact with each other. + + Note: This is not yet finalized, it is likely that these definitions will + evolve. + """ + + @staticmethod + def registerClassicPhy(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerClassicPhy', + emulated__bluetooth__pb2.RawData.SerializeToString, + emulated__bluetooth__pb2.RawData.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def registerBlePhy(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerBlePhy', + emulated__bluetooth__pb2.RawData.SerializeToString, + emulated__bluetooth__pb2.RawData.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def registerHCIDevice(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.EmulatedBluetoothService/registerHCIDevice', + emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString, + emulated__bluetooth__packets__pb2.HCIPacket.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/bumble/transport/emulated_bluetooth_vhci_pb2.py b/bumble/transport/emulated_bluetooth_vhci_pb2.py new file mode 100644 index 0000000..a638439 --- /dev/null +++ b/bumble/transport/emulated_bluetooth_vhci_pb2.py @@ -0,0 +1,43 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: emulated_bluetooth_vhci.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1d\x65mulated_bluetooth_vhci.proto\x12\x1b\x61ndroid.emulation.bluetooth\x1a emulated_bluetooth_packets.proto2y\n\x15VhciForwardingService\x12`\n\nattachVhci\x12&.android.emulation.bluetooth.HCIPacket\x1a&.android.emulation.bluetooth.HCIPacket(\x01\x30\x01\x42J\n\x1f\x63om.android.emulation.bluetoothP\x01\xf8\x01\x01\xa2\x02\x03\x41\x45\x42\xaa\x02\x1b\x41ndroid.Emulation.Bluetoothb\x06proto3') + + + +_VHCIFORWARDINGSERVICE = DESCRIPTOR.services_by_name['VhciForwardingService'] +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\037com.android.emulation.bluetoothP\001\370\001\001\242\002\003AEB\252\002\033Android.Emulation.Bluetooth' + _VHCIFORWARDINGSERVICE._serialized_start=96 + _VHCIFORWARDINGSERVICE._serialized_end=217 +# @@protoc_insertion_point(module_scope) diff --git a/bumble/transport/emulated_bluetooth_vhci_pb2_grpc.py b/bumble/transport/emulated_bluetooth_vhci_pb2_grpc.py new file mode 100644 index 0000000..94140d7 --- /dev/null +++ b/bumble/transport/emulated_bluetooth_vhci_pb2_grpc.py @@ -0,0 +1,114 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from . import emulated_bluetooth_packets_pb2 as emulated__bluetooth__packets__pb2 + + +class VhciForwardingServiceStub(object): + """This is a service which allows you to directly intercept the VHCI packets + that are coming and going to the device before they are delivered to + the rootcanal service described below. + + This service is usually not available on the emulator, and must be explictly + requested from the commandline. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.attachVhci = channel.stream_stream( + '/android.emulation.bluetooth.VhciForwardingService/attachVhci', + request_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString, + response_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString, + ) + + +class VhciForwardingServiceServicer(object): + """This is a service which allows you to directly intercept the VHCI packets + that are coming and going to the device before they are delivered to + the rootcanal service described below. + + This service is usually not available on the emulator, and must be explictly + requested from the commandline. + """ + + def attachVhci(self, request_iterator, context): + """This attach directly to /dev/vhci inside the android guest if available + + - This will disable root canal. + - You will have to provide your own virtual bluetooth chip. + + Some things to be aware of: + - Only one client can be active. + - Registering when bluetooth is active in android can result in + undefined behavior. + - If a client disconnects, rootcanal will be activated again + + Status codes: + - FAILED_PRECONDITION (code 9) If another client is controlling /dev/vhci. + + This is an internal testing only interface, and is NOT publicly + supported. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_VhciForwardingServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'attachVhci': grpc.stream_stream_rpc_method_handler( + servicer.attachVhci, + request_deserializer=emulated__bluetooth__packets__pb2.HCIPacket.FromString, + response_serializer=emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'android.emulation.bluetooth.VhciForwardingService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class VhciForwardingService(object): + """This is a service which allows you to directly intercept the VHCI packets + that are coming and going to the device before they are delivered to + the rootcanal service described below. + + This service is usually not available on the emulator, and must be explictly + requested from the commandline. + """ + + @staticmethod + def attachVhci(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream(request_iterator, target, '/android.emulation.bluetooth.VhciForwardingService/attachVhci', + emulated__bluetooth__packets__pb2.HCIPacket.SerializeToString, + emulated__bluetooth__packets__pb2.HCIPacket.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/bumble/transport/file.py b/bumble/transport/file.py new file mode 100644 index 0000000..841c62a --- /dev/null +++ b/bumble/transport/file.py @@ -0,0 +1,60 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import io +import logging + +from .common import Transport, StreamPacketSource, StreamPacketSink + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_file_transport(spec): + ''' + Open a File transport (typically not for a real file, but for a PTY or other unix virtual files). + The parameter string is the path of the file to open + ''' + + # Open the file + file = io.open(spec, 'r+b', buffering=0) + + # Setup reading + read_transport, packet_source = await asyncio.get_running_loop().connect_read_pipe( + lambda: StreamPacketSource(), + file + ) + + # Setup writing + write_transport, _ = await asyncio.get_running_loop().connect_write_pipe( + lambda: asyncio.BaseProtocol(), + file + ) + packet_sink = StreamPacketSink(write_transport) + + class FileTransport(Transport): + async def close(self): + read_transport.close() + write_transport.close() + file.close() + + return FileTransport(packet_source, packet_sink) + diff --git a/bumble/transport/hci_socket.py b/bumble/transport/hci_socket.py new file mode 100644 index 0000000..f74a535 --- /dev/null +++ b/bumble/transport/hci_socket.py @@ -0,0 +1,146 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import logging +import struct +import os +import socket +import ctypes +import collections + +from .common import Transport, ParserSource + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_hci_socket_transport(spec): + ''' + Open an HCI Socket (only available on some platforms). + The parameter string is either empty (to use the first/default Bluetooth adapter) + or a 0-based integer to indicate the adapter number. + ''' + + HCI_CHANNEL_USER = 1 + + # Create a raw HCI socket + try: + hci_socket = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_RAW | socket.SOCK_NONBLOCK, socket.BTPROTO_HCI) + except AttributeError: + # Not supported on this platform + logger.info("HCI sockets not supported on this platform") + raise Exception('Bluetooth HCI sockets not supported on this platform') + + # Compute the adapter index + if spec is None: + adapter_index = 0 + else: + adapter_index = int(spec) + + # Bind the socket + # NOTE: since Python doesn't support binding with the required address format (yet), + # we need to go directly to the C runtime... + try: + ctypes.cdll.LoadLibrary('libc.so.6') + libc = ctypes.CDLL('libc.so.6', use_errno=True) + except OSError: + logger.info("HCI sockets not supported on this platform") + raise Exception('Bluetooth HCI sockets not supported on this platform') + libc.bind.argtypes = (ctypes.c_int, ctypes.POINTER(ctypes.c_char), ctypes.c_int) + libc.bind.restype = ctypes.c_int + bind_address = struct.pack(' or : + With as the 0-based index to select amongst all the devices that appear + to be supporting Bluetooth HCI (0 being the first one), or + Where and are the vendor ID and product ID in hexadecimal. + + Examples: + 0 --> the first BT USB dongle + 04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901 + ''' + + USB_RECIPIENT_DEVICE = 0x00 + USB_REQUEST_TYPE_CLASS = 0x01 << 5 + USB_ENDPOINT_EVENTS_IN = 0x81 + USB_ENDPOINT_ACL_IN = 0x82 + USB_ENDPOINT_SCO_IN = 0x83 + USB_ENDPOINT_ACL_OUT = 0x02 + # USB_ENDPOINT_SCO_OUT = 0x03 + USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0 + USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01 + USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER = 0x01 + + READ_SIZE = 1024 + READ_TIMEOUT = 1000 + + class UsbPacketSink: + def __init__(self, device): + self.device = device + self.thread = threading.Thread(target=self.run) + self.loop = asyncio.get_running_loop() + self.stop_event = None + + def on_packet(self, packet): + # TODO: don't block here, just queue for the write thread + if len(packet) == 0: + logger.warning('packet too short') + return + + packet_type = packet[0] + try: + if packet_type == hci.HCI_ACL_DATA_PACKET: + self.device.write(USB_ENDPOINT_ACL_OUT, packet[1:]) + elif packet_type == hci.HCI_COMMAND_PACKET: + self.device.ctrl_transfer(USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS, 0, 0, 0, packet[1:]) + else: + logger.warning(color(f'unsupported packet type {packet_type}', 'red')) + except usb.core.USBTimeoutError: + logger.warning('USB Write Timeout') + except usb.core.USBError as error: + logger.warning(f'USB write error: {error}') + time.sleep(1) # Sleep one second to avoid busy looping + + def start(self): + self.thread.start() + + async def stop(self): + # Create stop events and wait for them to be signaled + self.stop_event = asyncio.Event() + await self.stop_event.wait() + + def run(self): + while self.stop_event is None: + time.sleep(1) + self.loop.call_soon_threadsafe(lambda: self.stop_event.set()) + + class UsbPacketSource(asyncio.Protocol, ParserSource): + def __init__(self, device, sco_enabled): + super().__init__() + self.device = device + self.loop = asyncio.get_running_loop() + self.queue = asyncio.Queue() + self.event_thread = threading.Thread( + target=self.run, + args=(USB_ENDPOINT_EVENTS_IN, hci.HCI_EVENT_PACKET) + ) + self.event_thread.stop_event = None + self.acl_thread = threading.Thread( + target=self.run, + args=(USB_ENDPOINT_ACL_IN, hci.HCI_ACL_DATA_PACKET) + ) + self.acl_thread.stop_event = None + + # SCO support is optional + self.sco_enabled = sco_enabled + if sco_enabled: + self.sco_thread = threading.Thread( + target=self.run, + args=(USB_ENDPOINT_SCO_IN, hci.HCI_SYNCHRONOUS_DATA_PACKET) + ) + self.sco_thread.stop_event = None + + def data_received(self, packet): + self.parser.feed_data(packet) + + def enqueue(self, packet): + self.queue.put_nowait(packet) + + async def dequeue(self): + while True: + try: + data = await self.queue.get() + except asyncio.CancelledError: + return + self.data_received(data) + + def start(self): + self.dequeue_task = self.loop.create_task(self.dequeue()) + self.event_thread.start() + self.acl_thread.start() + if self.sco_enabled: + self.sco_thread.start() + + async def stop(self): + # Stop the dequeuing task + self.dequeue_task.cancel() + + # Create stop events and wait for them to be signaled + self.event_thread.stop_event = asyncio.Event() + self.acl_thread.stop_event = asyncio.Event() + await self.event_thread.stop_event.wait() + await self.acl_thread.stop_event.wait() + if self.sco_enabled: + await self.sco_thread.stop_event.wait() + + def run(self, endpoint, packet_type): + # Read until asked to stop + current_thread = threading.current_thread() + while current_thread.stop_event is None: + try: + # Read, with a timeout of 1 second + data = self.device.read(endpoint, READ_SIZE, timeout=READ_TIMEOUT) + packet = bytes([packet_type]) + data.tobytes() + self.loop.call_soon_threadsafe(self.enqueue, packet) + except usb.core.USBTimeoutError: + continue + except usb.core.USBError: + # Don't log this: because pyusb doesn't really support multiple threads + # reading at the same time, we can get occasional USBError(errno=5) + # Input/Output errors reported, but they seem to be harmless. + # Until support for async or multi-thread support is added to pyusb, + # we'll just live with this as is... + # logger.warning(f'USB read error: {error}') + time.sleep(1) # Sleep one second to avoid busy looping + + stop_event = current_thread.stop_event + self.loop.call_soon_threadsafe(lambda: stop_event.set()) + + class UsbTransport(Transport): + def __init__(self, device, source, sink): + super().__init__(source, sink) + self.device = device + + async def close(self): + await self.source.stop() + await self.sink.stop() + usb.util.release_interface(self.device, 0) + + # Find the device according to the spec moniker + if ':' in spec: + vendor_id, product_id = spec.split(':') + device = usb.core.find(idVendor=int(vendor_id, 16), idProduct=int(product_id, 16)) + else: + device_index = int(spec) + devices = list(usb.core.find( + find_all = 1, + bDeviceClass = USB_DEVICE_CLASS_WIRELESS_CONTROLLER, + bDeviceSubClass = USB_DEVICE_SUBCLASS_RF_CONTROLLER, + bDeviceProtocol = USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER + )) + if len(devices) > device_index: + device = devices[device_index] + else: + device = None + + if device is None: + raise ValueError('device not found') + logger.debug(f'USB Device: {device}') + + # Detach the kernel driver if needed + if device.is_kernel_driver_active(0): + logger.debug("detaching kernel driver") + device.detach_kernel_driver(0) + + # Set configuration, if needed + try: + configuration = device.get_active_configuration() + except usb.core.USBError: + device.set_configuration() + configuration = device.get_active_configuration() + interface = configuration[(0, 0)] + logger.debug(f'USB Interface: {interface}') + usb.util.claim_interface(device, 0) + + # Select an alternate setting for SCO, if available + sco_enabled = False + # NOTE: this is disabled for now, because SCO with alternate settings is broken, + # see: https://github.com/libusb/libusb/issues/36 + # + # best_packet_size = 0 + # best_interface = None + # sco_enabled = False + # for interface in configuration: + # iso_in_endpoint = None + # iso_out_endpoint = None + # for endpoint in interface: + # if (endpoint.bEndpointAddress == USB_ENDPOINT_SCO_IN and + # usb.util.endpoint_direction(endpoint.bEndpointAddress) == usb.util.ENDPOINT_IN and + # usb.util.endpoint_type(endpoint.bmAttributes) == usb.util.ENDPOINT_TYPE_ISO): + # iso_in_endpoint = endpoint + # continue + # if (endpoint.bEndpointAddress == USB_ENDPOINT_SCO_OUT and + # usb.util.endpoint_direction(endpoint.bEndpointAddress) == usb.util.ENDPOINT_OUT and + # usb.util.endpoint_type(endpoint.bmAttributes) == usb.util.ENDPOINT_TYPE_ISO): + # iso_out_endpoint = endpoint + + # if iso_in_endpoint is not None and iso_out_endpoint is not None: + # if iso_out_endpoint.wMaxPacketSize > best_packet_size: + # best_packet_size = iso_out_endpoint.wMaxPacketSize + # best_interface = interface + + # if best_interface is not None: + # logger.debug(f'SCO enabled, selecting alternate setting (wMaxPacketSize={best_packet_size}): {best_interface}') + # sco_enabled = True + # try: + # device.set_interface_altsetting( + # interface = best_interface.bInterfaceNumber, + # alternate_setting = best_interface.bAlternateSetting + # ) + # except usb.USBError: + # logger.warning('failed to set alternate setting') + + packet_source = UsbPacketSource(device, sco_enabled) + packet_sink = UsbPacketSink(device) + packet_source.start() + packet_sink.start() + + return UsbTransport(device, packet_source, packet_sink) \ No newline at end of file diff --git a/bumble/transport/serial.py b/bumble/transport/serial.py new file mode 100644 index 0000000..b760a29 --- /dev/null +++ b/bumble/transport/serial.py @@ -0,0 +1,72 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import logging +import serial_asyncio + +from .common import Transport, StreamPacketSource, StreamPacketSink + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_serial_transport(spec): + ''' + Open a serial port transport. + The parameter string has this syntax: + [,][,rtscts][,dsrdtr] + When is omitted, the default value of 1000000 is used + When "rtscts" is specified, RTS/CTS hardware flow control is enabled + When "dsrdtr" is specified, DSR/DTR hardware flow control is enabled + + Examples: + /dev/tty.usbmodem0006839912172 + /dev/tty.usbmodem0006839912172,1000000 + /dev/tty.usbmodem0006839912172,rtscts + ''' + + speed = 1000000 + rtscts = False + dsrdtr = False + if ',' in spec: + parts = spec.split(',') + device = parts[0] + for part in parts[1:]: + if part == 'rtscts': + rtscts = True + elif part == 'dsrdtr': + dsrdtr = True + elif part.isnumeric(): + speed = int(part) + else: + device = spec + serial_transport, packet_source = await serial_asyncio.create_serial_connection( + asyncio.get_running_loop(), + lambda: StreamPacketSource(), + device, + baudrate=speed, + rtscts=rtscts, + dsrdtr=dsrdtr + ) + packet_sink = StreamPacketSink(serial_transport) + + return Transport(packet_source, packet_sink) + diff --git a/bumble/transport/tcp_client.py b/bumble/transport/tcp_client.py new file mode 100644 index 0000000..e250f25 --- /dev/null +++ b/bumble/transport/tcp_client.py @@ -0,0 +1,52 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import logging + +from .common import Transport, StreamPacketSource, StreamPacketSink + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_tcp_client_transport(spec): + ''' + Open a TCP client transport. + The parameter string has this syntax: + : + + Example: 127.0.0.1:9001 + ''' + + class TcpPacketSource(StreamPacketSource): + def connection_lost(self, error): + logger.debug(f'connection lost: {error}') + self.terminated.set_result(error) + + remote_host, remote_port = spec.split(':') + tcp_transport, packet_source = await asyncio.get_running_loop().create_connection( + lambda: TcpPacketSource(), + host=remote_host, + port=int(remote_port), + ) + packet_sink = StreamPacketSink(tcp_transport) + + return Transport(packet_source, packet_sink) diff --git a/bumble/transport/tcp_server.py b/bumble/transport/tcp_server.py new file mode 100644 index 0000000..6806683 --- /dev/null +++ b/bumble/transport/tcp_server.py @@ -0,0 +1,88 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import logging + +from .common import Transport, StreamPacketSource + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_tcp_server_transport(spec): + ''' + Open a TCP server transport. + The parameter string has this syntax: + : + Where may be the address of a local network interface, or '_' + to accept connections on all local network interfaces. + + Example: _:9001 + ''' + + class TcpServerTransport(Transport): + async def close(self): + await super().close() + + class TcpServerProtocol: + def __init__(self, packet_source, packet_sink): + self.packet_source = packet_source + self.packet_sink = packet_sink + + # Called when a new connection is established + def connection_made(self, transport): + peername = transport.get_extra_info('peername') + logger.debug('connection from {}'.format(peername)) + self.packet_sink.transport = transport + + # Called when the client is disconnected + def connection_lost(self, error): + logger.debug(f'connection lost: {error}') + self.packet_sink.transport = None + + def eof_received(self): + logger.debug('connection end') + self.packet_sink.transport = None + + # Called when data is received on the socket + def data_received(self, data): + self.packet_source.data_received(data) + + class TcpServerPacketSink: + def __init__(self): + self.transport = None + + def on_packet(self, packet): + if self.transport: + self.transport.write(packet) + else: + logger.debug('no client, dropping packet') + + local_host, local_port = spec.split(':') + packet_source = StreamPacketSource() + packet_sink = TcpServerPacketSink() + await asyncio.get_running_loop().create_server( + lambda: TcpServerProtocol(packet_source, packet_sink), + host=local_host if local_host != '_' else None, + port=int(local_port), + ) + + return TcpServerTransport(packet_source, packet_sink) diff --git a/bumble/transport/udp.py b/bumble/transport/udp.py new file mode 100644 index 0000000..f4c59ea --- /dev/null +++ b/bumble/transport/udp.py @@ -0,0 +1,63 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import logging + +from .common import Transport, ParserSource + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_udp_transport(spec): + ''' + Open a UDP transport. + The parameter string has this syntax: + :,: + + Example: 0.0.0.0:9000,127.0.0.1:9001 + ''' + + class UdpPacketSource(asyncio.DatagramProtocol, ParserSource): + def datagram_received(self, data, addr): + self.parser.feed_data(data) + + class UdpPacketSink: + def __init__(self, transport): + self.transport = transport + + def on_packet(self, packet): + self.transport.sendto(packet) + + def close(self): + self.transport.close() + + local, remote = spec.split(',') + local_host, local_port = local.split(':') + remote_host, remote_port = remote.split(':') + udp_transport, packet_source = await asyncio.get_running_loop().create_datagram_endpoint( + lambda: UdpPacketSource(), + local_addr=(local_host, int(local_port)), + remote_addr=(remote_host, int(remote_port)) + ) + packet_sink = UdpPacketSink(udp_transport) + + return Transport(packet_source, packet_sink) diff --git a/bumble/transport/usb.py b/bumble/transport/usb.py new file mode 100644 index 0000000..5e1d506 --- /dev/null +++ b/bumble/transport/usb.py @@ -0,0 +1,324 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import logging +import usb1 +import threading +import collections +from colors import color + +from .common import Transport, ParserSource +from .. import hci + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_usb_transport(spec): + ''' + Open a USB transport. + The parameter string has this syntax: + either or : + With as the 0-based index to select amongst all the devices that appear + to be supporting Bluetooth HCI (0 being the first one), or + Where and are the vendor ID and product ID in hexadecimal. + + Examples: + 0 --> the first BT USB dongle + 04b4:f901 --> the BT USB dongle with vendor=04b4 and product=f901 + ''' + + USB_RECIPIENT_DEVICE = 0x00 + USB_REQUEST_TYPE_CLASS = 0x01 << 5 + USB_ENDPOINT_EVENTS_IN = 0x81 + USB_ENDPOINT_ACL_IN = 0x82 + USB_ENDPOINT_ACL_OUT = 0x02 + USB_DEVICE_CLASS_WIRELESS_CONTROLLER = 0xE0 + USB_DEVICE_SUBCLASS_RF_CONTROLLER = 0x01 + USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER = 0x01 + + READ_SIZE = 1024 + + class UsbPacketSink: + def __init__(self, device): + self.device = device + self.transfer = device.getTransfer() + self.packets = collections.deque() # Queue of packets waiting to be sent + self.loop = asyncio.get_running_loop() + self.cancel_done = self.loop.create_future() + self.closed = False + + def start(self): + pass + + def on_packet(self, packet): + # Ignore packets if we're closed + if self.closed: + return + + if len(packet) == 0: + logger.warning('packet too short') + return + + # Queue the packet + self.packets.append(packet) + if len(self.packets) == 1: + # The queue was previously empty, re-prime the pump + self.process_queue() + + def on_packet_sent(self, transfer): + status = transfer.getStatus() + # logger.debug(f'<<< USB out transfer callback: status={status}') + + if status == usb1.TRANSFER_COMPLETED: + self.loop.call_soon_threadsafe(self.on_packet_sent_) + elif status == usb1.TRANSFER_CANCELLED: + self.loop.call_soon_threadsafe(self.cancel_done.set_result, None) + else: + logger.warning(color(f'!!! out transfer not completed: status={status}', 'red')) + + def on_packet_sent_(self): + if self.packets: + self.packets.popleft() + self.process_queue() + + def process_queue(self): + if len(self.packets) == 0: + return # Nothing to do + + packet = self.packets[0] + packet_type = packet[0] + if packet_type == hci.HCI_ACL_DATA_PACKET: + self.transfer.setBulk( + USB_ENDPOINT_ACL_OUT, + packet[1:], + callback=self.on_packet_sent + ) + logger.debug('submit ACL') + self.transfer.submit() + elif packet_type == hci.HCI_COMMAND_PACKET: + self.transfer.setControl( + USB_RECIPIENT_DEVICE | USB_REQUEST_TYPE_CLASS, 0, 0, 0, + packet[1:], + callback=self.on_packet_sent + ) + logger.debug('submit COMMAND') + self.transfer.submit() + else: + logger.warning(color(f'unsupported packet type {packet_type}', 'red')) + + async def close(self): + self.closed = True + + # Empty the packet queue so that we don't send any more data + self.packets.clear() + + # If we have a transfer in flight, cancel it + if self.transfer.isSubmitted(): + # Try to cancel the transfer, but that may fail because it may have already completed + try: + self.transfer.cancel() + + logger.debug('waiting for OUT transfer cancellation to be done...') + await self.cancel_done + logger.debug('OUT transfer cancellation done') + except usb1.USBError: + logger.debug('OUT transfer likely already completed') + + class UsbPacketSource(asyncio.Protocol, ParserSource): + def __init__(self, context, device): + super().__init__() + self.context = context + self.device = device + self.loop = asyncio.get_running_loop() + self.queue = asyncio.Queue() + self.closed = False + self.event_loop_done = self.loop.create_future() + self.cancel_done = { + hci.HCI_EVENT_PACKET: self.loop.create_future(), + hci.HCI_ACL_DATA_PACKET: self.loop.create_future() + } + + # Create a thread to process events + self.event_thread = threading.Thread(target=self.run) + + def start(self): + # Set up transfer objects for input + self.events_in_transfer = device.getTransfer() + self.events_in_transfer.setInterrupt( + USB_ENDPOINT_EVENTS_IN, + READ_SIZE, + callback=self.on_packet_received, + user_data=hci.HCI_EVENT_PACKET + ) + self.events_in_transfer.submit() + + self.acl_in_transfer = device.getTransfer() + self.acl_in_transfer.setBulk( + USB_ENDPOINT_ACL_IN, + READ_SIZE, + callback=self.on_packet_received, + user_data=hci.HCI_ACL_DATA_PACKET + ) + self.acl_in_transfer.submit() + + self.dequeue_task = self.loop.create_task(self.dequeue()) + self.event_thread.start() + + def on_packet_received(self, transfer): + packet_type = transfer.getUserData() + status = transfer.getStatus() + # logger.debug(f'<<< USB IN transfer callback: status={status} packet_type={packet_type}') + + if status == usb1.TRANSFER_COMPLETED: + packet = bytes([packet_type]) + transfer.getBuffer()[:transfer.getActualLength()] + self.loop.call_soon_threadsafe(self.queue.put_nowait, packet) + elif status == usb1.TRANSFER_CANCELLED: + self.loop.call_soon_threadsafe(self.cancel_done[packet_type].set_result, None) + return + else: + logger.warning(color(f'!!! transfer not completed: status={status}', 'red')) + + # Re-submit the transfer so we can receive more data + transfer.submit() + + async def dequeue(self): + while not self.closed: + try: + packet = await self.queue.get() + except asyncio.CancelledError: + return + self.parser.feed_data(packet) + + def run(self): + logger.debug('starting USB event loop') + while self.events_in_transfer.isSubmitted() or self.acl_in_transfer.isSubmitted(): + try: + self.context.handleEvents() + except usb1.USBErrorInterrupted: + pass + + logger.debug('USB event loop done') + self.event_loop_done.set_result(None) + + async def close(self): + self.closed = True + self.dequeue_task.cancel() + + # Cancel the transfers + for transfer in (self.events_in_transfer, self.acl_in_transfer): + if transfer.isSubmitted(): + # Try to cancel the transfer, but that may fail because it may have already completed + packet_type = transfer.getUserData() + try: + transfer.cancel() + logger.debug(f'waiting for IN[{packet_type}] transfer cancellation to be done...') + await self.cancel_done[packet_type] + logger.debug(f'IN[{packet_type}] transfer cancellation done') + except usb1.USBError: + logger.debug(f'IN[{packet_type}] transfer likely already completed') + + # Wait for the thread to terminate + await self.event_loop_done + + class UsbTransport(Transport): + def __init__(self, context, device, interface, source, sink): + super().__init__(source, sink) + self.context = context + self.device = device + self.interface = interface + + # Get exclusive access + device.claimInterface(interface) + + # The source and sink can now start + source.start() + sink.start() + + async def close(self): + await self.source.close() + await self.sink.close() + self.device.releaseInterface(self.interface) + self.device.close() + self.context.close() + + # Find the device according to the spec moniker + context = usb1.USBContext() + context.open() + try: + found = None + if ':' in spec: + vendor_id, product_id = spec.split(':') + found = context.getByVendorIDAndProductID(int(vendor_id, 16), int(product_id, 16), skip_on_error=True) + else: + device_index = int(spec) + device_iterator = context.getDeviceIterator(skip_on_error=True) + try: + for device in device_iterator: + if device.getDeviceClass() == USB_DEVICE_CLASS_WIRELESS_CONTROLLER and \ + device.getDeviceSubClass() == USB_DEVICE_SUBCLASS_RF_CONTROLLER and \ + device.getDeviceProtocol() == USB_DEVICE_PROTOCOL_BLUETOOTH_PRIMARY_CONTROLLER: + if device_index == 0: + found = device + break + device_index -= 1 + device.close() + finally: + device_iterator.close() + + if found is None: + context.close() + raise ValueError('device not found') + + logger.debug(f'USB Device: {found}') + device = found.open() + + # Set the configuration if needed + try: + configuration = device.getConfiguration() + logger.debug(f'current configuration = {configuration}') + except usb1.USBError: + try: + logger.debug('setting configuration 1') + device.setConfiguration(1) + except usb1.USBError: + logger.debug('failed to set configuration 1') + + # Use the first interface + interface = 0 + + # Detach the kernel driver if supported and needed + if usb1.hasCapability(usb1.CAP_SUPPORTS_DETACH_KERNEL_DRIVER): + try: + if device.kernelDriverActive(interface): + logger.debug("detaching kernel driver") + device.detachKernelDriver(interface) + except usb1.USBError: + pass + + source = UsbPacketSource(context, device) + sink = UsbPacketSink(device) + return UsbTransport(context, device, interface, source, sink) + except usb1.USBError as error: + logger.warning(color(f'!!! failed to open USB device: {error}', 'red')) + context.close() + raise diff --git a/bumble/transport/vhci.py b/bumble/transport/vhci.py new file mode 100644 index 0000000..572c31d --- /dev/null +++ b/bumble/transport/vhci.py @@ -0,0 +1,59 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging + +from .file import open_file_transport + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_vhci_transport(spec): + ''' + Open a VHCI transport (only available on some platforms). + The parameter string is either empty (to use the default VHCI device + path at /dev/vhci), or the path of a VHCI device + ''' + + HCI_VENDOR_PKT = 0xff + HCI_BREDR = 0x00 # Controller type + + # Open the VHCI device + transport = await open_file_transport(spec or '/dev/vhci') + + # Override the source's `data_received` method so that we can + # filter out the vendor packet that is received just after the + # initial open + def vhci_data_received(data): + if len(data) > 0 and data[0] == HCI_VENDOR_PKT: + if len(data) == 4: + hci_index = data[2] << 8 | data[3] + logger.info(f'HCI index {hci_index}') + else: + transport.source.parser.feed_data(data) + + transport.source.data_received = vhci_data_received + + # Write the initial config + transport.sink.on_packet(bytes([HCI_VENDOR_PKT, HCI_BREDR])) + + return transport + diff --git a/bumble/transport/ws_client.py b/bumble/transport/ws_client.py new file mode 100644 index 0000000..9ee7e49 --- /dev/null +++ b/bumble/transport/ws_client.py @@ -0,0 +1,49 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import logging +import websockets + +from .common import PumpedPacketSource, PumpedPacketSink, PumpedTransport + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_ws_client_transport(spec): + ''' + Open a WebSocket client transport. + The parameter string has this syntax: + : + + Example: 127.0.0.1:9001 + ''' + + remote_host, remote_port = spec.split(':') + uri = f'ws://{remote_host}:{remote_port}' + websocket = await websockets.connect(uri) + + transport = PumpedTransport( + PumpedPacketSource(websocket.recv), + PumpedPacketSink(websocket.send), + websocket.close + ) + transport.start() + return transport diff --git a/bumble/transport/ws_server.py b/bumble/transport/ws_server.py new file mode 100644 index 0000000..3b2d15e --- /dev/null +++ b/bumble/transport/ws_server.py @@ -0,0 +1,81 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import logging +import websockets + +from .common import Transport, ParserSource, PumpedPacketSink + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +async def open_ws_server_transport(spec): + ''' + Open a WebSocket server transport. + The parameter string has this syntax: + : + Where may be the address of a local network interface, or '_' + to accept connections on all local network interfaces. + + Example: _:9001 + ''' + + class WsServerTransport(Transport): + def __init__(self): + source = ParserSource() + sink = PumpedPacketSink(self.send_packet) + self.connection = asyncio.get_running_loop().create_future() + + super().__init__(source, sink) + + async def serve(self, local_host, local_port): + self.sink.start() + self.server = await websockets.serve( + ws_handler = self.on_connection, + host = local_host if local_host != '_' else None, + port = int(local_port) + ) + logger.debug(f'websocket server ready on port {local_port}') + + async def on_connection(self, connection): + logger.debug(f'new connection on {connection.local_address} from {connection.remote_address}') + self.connection.set_result(connection) + try: + async for packet in connection: + if type(packet) is bytes: + self.source.parser.feed_data(packet) + else: + logger.warn('discarding packet: not a BINARY frame') + except websockets.WebSocketException as error: + logger.debug(f'exception while receiving packet: {error}') + + # Wait for a new connection + self.connection = asyncio.get_running_loop().create_future() + + async def send_packet(self, packet): + connection = await self.connection + return await connection.send(packet) + + local_host, local_port = spec.split(':') + transport = WsServerTransport() + await transport.serve(local_host, local_port) + return transport diff --git a/bumble/utils.py b/bumble/utils.py new file mode 100644 index 0000000..1ab3fd7 --- /dev/null +++ b/bumble/utils.py @@ -0,0 +1,142 @@ +# Copyright 2021-2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ----------------------------------------------------------------------------- +# Imports +# ----------------------------------------------------------------------------- +import asyncio +import logging +import traceback +from functools import wraps +from colors import color +from pyee import EventEmitter + + +# ----------------------------------------------------------------------------- +# Logging +# ----------------------------------------------------------------------------- +logger = logging.getLogger(__name__) + + +# ----------------------------------------------------------------------------- +def setup_event_forwarding(emitter, forwarder, event_name): + def emit(*args, **kwargs): + forwarder.emit(event_name, *args, **kwargs) + emitter.on(event_name, emit) + + +# ----------------------------------------------------------------------------- +def composite_listener(cls): + """ + Decorator that adds a `register` and `deregister` method to a class, which + registers/deregisters all methods named `on_` as a listener for + the event with an emitter. + """ + def register(self, emitter): + for method_name in dir(cls): + if method_name.startswith('on_'): + emitter.on(method_name[3:], getattr(self, method_name)) + + def deregister(self, emitter): + for method_name in dir(cls): + if method_name.startswith('on_'): + emitter.remove_listener(method_name[3:], getattr(self, method_name)) + + cls._bumble_register_composite = register + cls._bumble_deregister_composite = deregister + return cls + + +# ----------------------------------------------------------------------------- +class CompositeEventEmitter(EventEmitter): + def __init__(self): + super().__init__() + self._listener = None + + @property + def listener(self): + return self._listener + + @listener.setter + def listener(self, listener): + if self._listener: + # Call the deregistration methods for each base class that has them + for cls in self._listener.__class__.mro(): + if hasattr(cls, '_bumble_register_composite'): + cls._bumble_deregister_composite(listener, self) + self._listener = listener + if listener: + # Call the registration methods for each base class that has them + for cls in listener.__class__.mro(): + if hasattr(cls, '_bumble_deregister_composite'): + cls._bumble_register_composite(listener, self) + + +# ----------------------------------------------------------------------------- +class AsyncRunner: + class WorkQueue: + def __init__(self, create_task=True): + self.queue = None + self.task = None + self.create_task = create_task + + def enqueue(self, coroutine): + # Create a task now if we need to and haven't done so already + if self.create_task and self.task is None: + self.task = asyncio.create_task(self.run()) + + # Lazy-create the coroutine queue + if self.queue is None: + self.queue = asyncio.Queue() + + # Enqueue the work + self.queue.put_nowait(coroutine) + + async def run(self): + while True: + item = await self.queue.get() + try: + await item + except Exception as error: + logger.warning(f'{color("!!! Exception in work queue:", "red")} {error}') + + # Shared default queue + default_queue = WorkQueue() + + @staticmethod + def run_in_task(queue=None): + """ + Function decorator used to adapt an async function into a sync function + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + coroutine = func(*args, **kwargs) + if queue is None: + # Create a task to run the coroutine + async def run(): + try: + await coroutine + except Exception: + logger.warning(f'{color("!!! Exception in wrapper:", "red")} {traceback.format_exc()}') + + asyncio.create_task(run()) + else: + # Queue the coroutine to be awaited by the work queue + queue.enqueue(coroutine) + + return wrapper + + return decorator diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ca7d4c0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,22 @@ +Bumble Documentation +==================== + +The documentation consists of a collection of markdown text files, with the root of the file +hierarchy at `docs/mkdocs/src`, starting with `docs/mkdocs/src/index.md`. +You can read the documentation as text, with any text viewer or your favorite markdown viewer, +or generate a static HTML "site" using `mkdocs`, which you can then open with any browser. + +# Static HTML With MkDocs + +[MkDocs](https://www.mkdocs.org/) is used to generate a static HTML documentation site. +The `mkdocs` directory contains all the data (actual documentation) and metadata (configuration) for the site. +`mkdocs/requirements.txt` includes the list of Python packages needed to build the site. +`mkdocs/mkdocs.yml` contains the site configuration. +`mkdocs/src/` is the directory where the actual documentation text, in markdown format, is located. + +To build, from the project's root directory: +``` +$ mkdocs build -f docs/mkdocs/mkdocs.yml +``` + +You can then open `docs/mkdocs/site/index.html` with any web browser. diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..665d878bd9d42a0ba5bdabd16e6ec4e3e7d6549d GIT binary patch literal 24591 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/logo.vectornator/Artboard0.json b/docs/images/logo.vectornator/Artboard0.json new file mode 100644 index 0000000..eddf68a --- /dev/null +++ b/docs/images/logo.vectornator/Artboard0.json @@ -0,0 +1 @@ +{"layers":[{"elements":[{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[-5.882939210360405,25.513785542788241],"opacity":1,"blur":0,"isLocked":false,"gid":56,"smootheningRate":0,"initialPoint":[-5.882939210360405,25.513785542788241],"creationPoints":[],"group":{"elements":[{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[225.48442074400293,105.07566425478205],"opacity":1,"blur":0,"isLocked":false,"gid":27,"smootheningRate":0,"initialPoint":[225.48442074400293,105.07566425478205],"creationPoints":[],"group":{"elements":[{"elementDescription":"(rectangle)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[225.48442074400293,180.0751680461143],"reflectionModeOverride":0,"anchorPoint":[225.48442074400293,180.0751680461143],"cornerRadius":0,"prevPoint":[-2.4497734478039774,-990.41770428277562],"inPoint":[225.48442074400293,180.0751680461143],"nextPoint":[-2.4497734478039774,-990.41770428277562]},{"outPoint":[375.4834254415108,180.0751680461143],"reflectionModeOverride":0,"anchorPoint":[375.4834254415108,180.0751680461143],"cornerRadius":0,"prevPoint":[-2.4497734478039774,-990.41770428277562],"inPoint":[375.4834254415108,180.0751680461143],"nextPoint":[-2.4497734478039774,-990.41770428277562]},{"outPoint":[375.4834254415108,430.07351680205772],"reflectionModeOverride":0,"anchorPoint":[375.4834254415108,430.07351680205772],"cornerRadius":0,"prevPoint":[-2.4497734478039774,-990.41770428277562],"inPoint":[375.4834254415108,430.07351680205772],"nextPoint":[-2.4497734478039774,-990.41770428277562]},{"outPoint":[225.48442074400293,430.07351680205772],"reflectionModeOverride":0,"anchorPoint":[225.48442074400293,430.07351680205772],"cornerRadius":0,"prevPoint":[-2.4497734478039774,-990.41770428277562],"inPoint":[225.48442074400293,430.07351680205772],"nextPoint":[-2.4497734478039774,-990.41770428277562]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.13822251558303833,"a":1},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":20,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[375.4834254415108,430.07351680205772],"opacity":1,"blur":0,"isLocked":false,"gid":22,"smootheningRate":0,"initialPoint":[225.48442074400293,180.0751680461143],"creationPoints":[],"name":"(rectangle)"},{"elementDescription":"(oval)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[225.48442074400293,138.65412011223214],"reflectionModeOverride":0,"anchorPoint":[225.48442074400293,180.07518887113827],"cornerRadius":0,"prevPoint":[-235.35384472404451,-574.6867431949596],"inPoint":[225.48442074400293,221.49625763004531],"nextPoint":[-235.35384472404451,-574.6867431949596]},{"outPoint":[341.90501411926641,105.07566425478205],"reflectionModeOverride":0,"anchorPoint":[300.48394536035983,105.07566425478205],"cornerRadius":0,"prevPoint":[-235.35384472404451,-574.6867431949596],"inPoint":[259.06287660145324,105.07566425478205],"nextPoint":[-235.35384472404451,-574.6867431949596]},{"outPoint":[375.4834230115174,221.49625763004531],"reflectionModeOverride":0,"anchorPoint":[375.4834230115174,180.07518887113827],"cornerRadius":0,"prevPoint":[-235.35384472404451,-574.6867431949596],"inPoint":[375.4834230115174,138.65412011223214],"nextPoint":[-235.35384472404451,-574.6867431949596]},{"outPoint":[259.06287660145324,255.0746665222963],"reflectionModeOverride":0,"anchorPoint":[300.48394536035983,255.0746665222963],"cornerRadius":0,"prevPoint":[-235.35384472404451,-574.6867431949596],"inPoint":[341.90501411926641,255.0746665222963],"nextPoint":[-235.35384472404451,-574.6867431949596]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.13822251558303833,"a":1},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":20,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[-235.35384472404451,-574.6867431949596],"opacity":1,"blur":0,"isLocked":false,"gid":24,"smootheningRate":0,"initialPoint":[-235.35384472404451,-574.6867431949596],"creationPoints":[],"name":"(oval)"},{"elementDescription":"(oval)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[225.48442074400293,388.65246608333962],"reflectionModeOverride":0,"anchorPoint":[225.48442074400293,430.07353484224666],"cornerRadius":0,"prevPoint":[-235.35384472404451,-324.68839722385167],"inPoint":[225.48442074400293,471.49460360115279],"nextPoint":[-235.35384472404451,-324.68839722385167]},{"outPoint":[341.90501411926641,355.07401022588954],"reflectionModeOverride":0,"anchorPoint":[300.48394536035983,355.07401022588954],"cornerRadius":0,"prevPoint":[-235.35384472404451,-324.68839722385167],"inPoint":[259.06287660145324,355.07401022588954],"nextPoint":[-235.35384472404451,-324.68839722385167]},{"outPoint":[375.4834230115174,471.49460360115279],"reflectionModeOverride":0,"anchorPoint":[375.4834230115174,430.07353484224666],"cornerRadius":0,"prevPoint":[-235.35384472404451,-324.68839722385167],"inPoint":[375.4834230115174,388.65246608333962],"nextPoint":[-235.35384472404451,-324.68839722385167]},{"outPoint":[259.06287660145324,505.07301249340378],"reflectionModeOverride":0,"anchorPoint":[300.48394536035983,505.07301249340378],"cornerRadius":0,"prevPoint":[-235.35384472404451,-324.68839722385167],"inPoint":[341.90501411926641,505.07301249340378],"nextPoint":[-235.35384472404451,-324.68839722385167]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.13822251558303833,"a":1},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":20,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[-235.35384472404451,-324.68839722385167],"opacity":1,"blur":0,"isLocked":false,"gid":25,"smootheningRate":0,"initialPoint":[-235.35384472404451,-324.68839722385167],"creationPoints":[],"name":"(oval)"},{"elementDescription":"(rectangle)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[235.48435458284723,179.89237563632139],"reflectionModeOverride":0,"anchorPoint":[235.48435458284723,179.89237563632139],"cornerRadius":0,"prevPoint":[37.941384471822971,-990.60049669256784],"inPoint":[235.48435458284723,179.89237563632139],"nextPoint":[37.941384471822971,-990.60049669256784]},{"outPoint":[365.48349317943928,179.89237563632139],"reflectionModeOverride":0,"anchorPoint":[365.48349317943928,179.89237563632139],"cornerRadius":0,"prevPoint":[37.941384471822971,-990.60049669256784],"inPoint":[365.48349317943928,179.89237563632139],"nextPoint":[37.941384471822971,-990.60049669256784]},{"outPoint":[365.48349317943928,429.89072439226481],"reflectionModeOverride":0,"anchorPoint":[365.48349317943928,429.89072439226481],"cornerRadius":0,"prevPoint":[37.941384471822971,-990.60049669256784],"inPoint":[365.48349317943928,429.89072439226481],"nextPoint":[37.941384471822971,-990.60049669256784]},{"outPoint":[235.48435458284723,429.89072439226481],"reflectionModeOverride":0,"anchorPoint":[235.48435458284723,429.89072439226481],"cornerRadius":0,"prevPoint":[37.941384471822971,-990.60049669256784],"inPoint":[235.48435458284723,429.89072439226481],"nextPoint":[37.941384471822971,-990.60049669256784]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.13822251558303833,"a":1},"strokeStyle":{"color":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[365.48349317943928,429.89072439226481],"opacity":1,"blur":0,"isLocked":false,"gid":26,"smootheningRate":0,"initialPoint":[235.48435458284723,179.89237563632139],"creationPoints":[],"name":"(rectangle)"}]},"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[35.772159785141525,199.98637434514319],"reflectionModeOverride":0,"anchorPoint":[35.772159785141525,199.98637434514319],"cornerRadius":0,"prevPoint":[16.704525477792117,37.513706149401855],"inPoint":[35.772159785141525,199.98637434514319],"nextPoint":[16.704525477792117,37.513706149401855]},{"outPoint":[159.56661941832897,334.72204728763415],"reflectionModeOverride":0,"anchorPoint":[159.56661941832897,334.72204728763415],"cornerRadius":0,"prevPoint":[16.704525477792117,37.513706149401855],"inPoint":[159.56661941832897,334.72204728763415],"nextPoint":[16.704525477792117,37.513706149401855]},{"outPoint":[299.28226312627601,200.42272477566019],"reflectionModeOverride":0,"anchorPoint":[299.28226312627601,200.42272477566019],"cornerRadius":0,"prevPoint":[16.704525477792117,37.513706149401855],"inPoint":[299.28226312627601,200.42272477566019],"nextPoint":[16.704525477792117,37.513706149401855]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[16.704525477792117,37.513706149401855],"opacity":1,"blur":0,"isLocked":false,"gid":29,"smootheningRate":0,"initialPoint":[16.704525477792117,37.513706149401855],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[316.02230657990947,197.89539868947611],"reflectionModeOverride":0,"anchorPoint":[316.02230657990947,197.89539868947611],"cornerRadius":0,"prevPoint":[296.695700377354,37.472065800512951],"inPoint":[316.02230657990947,197.89539868947611],"nextPoint":[296.695700377354,37.472065800512951]},{"outPoint":[441.49811195900963,330.9315945770885],"reflectionModeOverride":0,"anchorPoint":[441.49811195900963,330.9315945770885],"cornerRadius":0,"prevPoint":[296.695700377354,37.472065800512951],"inPoint":[441.49811195900963,330.9315945770885],"nextPoint":[296.695700377354,37.472065800512951]},{"outPoint":[583.11133899889126,198.32624525061453],"reflectionModeOverride":0,"anchorPoint":[583.11133899889126,198.32624525061453],"cornerRadius":0,"prevPoint":[296.695700377354,37.472065800512951],"inPoint":[583.11133899889126,198.32624525061453],"nextPoint":[296.695700377354,37.472065800512951]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[296.695700377354,37.472065800512951],"opacity":1,"blur":0,"isLocked":false,"gid":30,"smootheningRate":0,"initialPoint":[296.695700377354,37.472065800512951],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[454.65560248608688,45.627293553562936],"reflectionModeOverride":0,"anchorPoint":[454.65560248608688,45.627293553562936],"cornerRadius":0,"prevPoint":[971.93140466738657,-224.79785180257943],"inPoint":[454.65560248608688,45.627293553562936],"nextPoint":[971.93140466738657,-224.79785180257943]},{"outPoint":[162.17037499378046,339.70097131047794],"reflectionModeOverride":0,"anchorPoint":[162.17037499378046,339.70097131047794],"cornerRadius":0,"prevPoint":[971.93140466738657,-224.79785180257943],"inPoint":[162.17037499378046,339.70097131047794],"nextPoint":[971.93140466738657,-224.79785180257943]},{"outPoint":[16.450758016910754,190.92333427643769],"reflectionModeOverride":0,"anchorPoint":[16.450758016910754,190.92333427643769],"cornerRadius":0,"prevPoint":[971.93140466738657,-224.79785180257943],"inPoint":[16.450758016910754,190.92333427643769],"nextPoint":[971.93140466738657,-224.79785180257943]},{"outPoint":[586.27726302866802,193.75471648932478],"reflectionModeOverride":0,"anchorPoint":[586.27726302866802,193.75471648932478],"cornerRadius":0,"prevPoint":[971.93140466738657,-224.79785180257943],"inPoint":[586.27726302866802,193.75471648932478],"nextPoint":[971.93140466738657,-224.79785180257943]},{"outPoint":[444.36970577266027,335.60394507941498],"reflectionModeOverride":0,"anchorPoint":[444.36970577266027,335.60394507941498],"cornerRadius":0,"prevPoint":[971.93140466738657,-224.79785180257943],"inPoint":[444.36970577266027,335.60394507941498],"nextPoint":[971.93140466738657,-224.79785180257943]},{"outPoint":[155.71179749272039,46.932845447085015],"reflectionModeOverride":0,"anchorPoint":[155.71179749272039,46.932845447085015],"cornerRadius":0,"prevPoint":[971.93140466738657,-224.79785180257943],"inPoint":[155.71179749272039,46.932845447085015],"nextPoint":[971.93140466738657,-224.79785180257943]}],"closed":false,"reversed":false}},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":20,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":90,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[1132.6259120879768,-222.31407294998928],"opacity":1,"blur":0,"isLocked":false,"gid":21,"smootheningRate":0,"initialPoint":[1132.6259120879768,-222.31407294998928],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(line)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[228.4683004292159,309.55668062155007],"reflectionModeOverride":0,"anchorPoint":[228.4683004292159,309.55668062155007],"cornerRadius":0,"prevPoint":[16.704525477792117,37.513706149401855],"inPoint":[228.4683004292159,309.55668062155007],"nextPoint":[16.704525477792117,37.513706149401855]},{"outPoint":[371.51081106842264,309.55668062155007],"reflectionModeOverride":0,"anchorPoint":[371.51081106842264,309.55668062155007],"cornerRadius":0,"prevPoint":[16.704525477792117,37.513706149401855],"inPoint":[371.51081106842264,309.55668062155007],"nextPoint":[16.704525477792117,37.513706149401855]}],"closed":false,"reversed":false}},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":61.869819641113281,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[16.704525477792117,37.513706149401855],"opacity":1,"blur":0,"isLocked":false,"gid":31,"smootheningRate":0,"initialPoint":[16.704525477792117,37.513706149401855],"creationPoints":[],"name":"(line)"},{"elementDescription":"(line)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[228.4683004292159,412.55599916164635],"reflectionModeOverride":0,"anchorPoint":[228.4683004292159,412.55599916164635],"cornerRadius":0,"prevPoint":[16.704525477792117,140.51302468949859],"inPoint":[228.4683004292159,412.55599916164635],"nextPoint":[16.704525477792117,140.51302468949859]},{"outPoint":[371.51081106842264,412.55599916164635],"reflectionModeOverride":0,"anchorPoint":[371.51081106842264,412.55599916164635],"cornerRadius":0,"prevPoint":[16.704525477792117,140.51302468949859],"inPoint":[371.51081106842264,412.55599916164635],"nextPoint":[16.704525477792117,140.51302468949859]}],"closed":false,"reversed":false}},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":61.869819641113281,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[16.704525477792117,140.51302468949859],"opacity":1,"blur":0,"isLocked":false,"gid":32,"smootheningRate":0,"initialPoint":[16.704525477792117,140.51302468949859],"creationPoints":[],"name":"(line)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[2.7206907461103356,25.513785542788241],"opacity":1,"blur":0,"isLocked":false,"gid":55,"smootheningRate":0,"initialPoint":[2.7206907461103356,25.513785542788241],"creationPoints":[],"group":{"elements":[{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[300.38008332937886,557.84279577418272],"reflectionModeOverride":0,"anchorPoint":[300.38008332937886,557.84279577418272],"cornerRadius":0,"prevPoint":[4.8049994992771872,-1.0129103054042616],"inPoint":[300.87435644597588,529.50877422158635],"nextPoint":[4.8049994992771872,-1.0129103054042616]},{"outPoint":[300.43161232869249,507.2021517541192],"reflectionModeOverride":0,"anchorPoint":[300.43161232869249,507.2021517541192],"cornerRadius":0,"prevPoint":[-12.365577443621419,-27.342871697380019],"inPoint":[300.43161232869249,507.2021517541192],"nextPoint":[-12.365577443621419,-27.342871697380019]},{"outPoint":[315.7481814828169,517.26662181075551],"reflectionModeOverride":0,"anchorPoint":[336.28774305214836,507.15563112854068],"cornerRadius":0,"prevPoint":[2.7206907461103356,24.978316814837285],"inPoint":[336.28774305214836,507.15563112854068],"nextPoint":[2.7206907461103356,24.978316814837285]}],"closed":true,"reversed":false}},"fillColor":{"b":0,"s":0,"h":0,"a":1},"strokeStyle":{"color":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[4.9686415433105822,12.610834591390244],"opacity":1,"blur":0,"isLocked":false,"gid":48,"smootheningRate":0,"initialPoint":[4.9686415433105822,12.610834591390244],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[300.44667602585685,557.84279577418272],"reflectionModeOverride":0,"anchorPoint":[300.44667602585685,557.84279577418272],"cornerRadius":0,"prevPoint":[596.02175985595829,-1.0129103054042616],"inPoint":[299.95240290925983,529.50877422158635],"nextPoint":[596.02175985595829,-1.0129103054042616]},{"outPoint":[300.39514702654321,507.2021517541192],"reflectionModeOverride":0,"anchorPoint":[300.39514702654321,507.2021517541192],"cornerRadius":0,"prevPoint":[613.1923367988569,-27.342871697380019],"inPoint":[300.39514702654321,507.2021517541192],"nextPoint":[613.1923367988569,-27.342871697380019]},{"outPoint":[285.07857787241835,517.26662181075551],"reflectionModeOverride":0,"anchorPoint":[264.53901630308735,507.15563112854068],"cornerRadius":0,"prevPoint":[598.10606860912537,24.978316814837285],"inPoint":[264.53901630308735,507.15563112854068],"nextPoint":[598.10606860912537,24.978316814837285]}],"closed":true,"reversed":false}},"fillColor":{"b":0,"s":0,"h":0,"a":1},"strokeStyle":{"color":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[595.85811781192467,12.818230389370797],"opacity":1,"blur":0,"isLocked":false,"gid":54,"smootheningRate":0,"initialPoint":[595.85811781192467,12.818230389370797],"creationPoints":[],"name":"(curve)"}]},"name":"(curve)"}]},"name":"(curve)"}],"isExpanded":false,"isLocked":false,"isVisible":true,"opacity":1,"gid":4,"name":"Layer 1"}],"frame":{"y":0,"x":0,"width":600,"height":600},"title":"Mac App icon","activeLayerIndex":0,"settings":{"gridSpacing":20,"gridAngle":45,"backgroundColor":{"b":1,"s":0,"h":0,"a":1},"gridMode":0,"isGridVisible":false},"guideLayer":{"isExpanded":false,"elements":[],"isLocked":false,"defaultName":"Guides","isVisible":true,"opacity":1,"name":"Guides","gid":5},"gid":3} \ No newline at end of file diff --git a/docs/images/logo.vectornator/Document.json b/docs/images/logo.vectornator/Document.json new file mode 100644 index 0000000..ac7714f --- /dev/null +++ b/docs/images/logo.vectornator/Document.json @@ -0,0 +1 @@ +{"date":644900643.85054696,"appVersion":"4.1.5","drawing":{"modificationDate":644894800.328192,"activeArtboardIndex":0,"settings":{"outlineMode":false,"isolateActiveLayer":false,"snapToEdges":false,"snapToPoints":false,"guidesVisible":true,"snapToGrid":false,"units":"Pixels","dimensionsVisible":true,"dynamicGuides":false,"isCMYKColorPreviewEnabled":false,"undoHistoryDisabled":false,"snapToGuides":true,"drawOnlyUsingPencil":false,"whiteBackground":false,"rulersVisible":true,"isTimeLapseWatermarkDisabled":false},"artboardPaths":["Artboard0.json"],"documentVersion":"unknown"}} \ No newline at end of file diff --git a/docs/images/logo.vectornator/Manifest.json b/docs/images/logo.vectornator/Manifest.json new file mode 100644 index 0000000..0f80b78 --- /dev/null +++ b/docs/images/logo.vectornator/Manifest.json @@ -0,0 +1 @@ +{"documentJSONFilename":"Document.json","undoHistoryJSONFilename":"UndoHistory.json","fileFormatVersion":0,"thumbnailImageFilename":"Thumbnail.png"} \ No newline at end of file diff --git a/docs/images/logo.vectornator/Thumbnail.png b/docs/images/logo.vectornator/Thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..5db5e3838c4d0ac64d1406af4a8f9c5038068ff5 GIT binary patch literal 42419 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 z64 literal 0 HcmV?d00001 diff --git a/docs/images/logo.vectornator/UndoHistory.json b/docs/images/logo.vectornator/UndoHistory.json new file mode 100644 index 0000000..ead621d --- /dev/null +++ b/docs/images/logo.vectornator/UndoHistory.json @@ -0,0 +1 @@ +{"cacheElements":[{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[320,664.0506297595],"reflectionModeOverride":0,"anchorPoint":[320,640],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[320,580],"nextPoint":[0,0]},{"outPoint":[344.050612449646,595.89893977911322],"reflectionModeOverride":0,"anchorPoint":[337.34381007033926,612.48211711168926],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[327.23039949003834,637.48844227310212],"nextPoint":[0,0]},{"outPoint":[380,580],"reflectionModeOverride":0,"anchorPoint":[360,580],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[352.025306224823,580],"nextPoint":[0,0]},{"outPoint":[260,580],"reflectionModeOverride":0,"anchorPoint":[280,580],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[300,580],"nextPoint":[0,0]}],"closed":true,"reversed":false}},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":2,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[0,0],"opacity":1,"blur":0,"isLocked":false,"gid":34,"smootheningRate":0,"initialPoint":[0,0],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"fillColor":{"b":0.98823529481887817,"s":1,"h":0.58068782582120682,"a":1},"maskedElements":[],"abstractPath":{"fillRule":0,"compoundPathData":{"subpaths":[{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[221.67257107604084,316.56452785377144],"reflectionModeOverride":0,"anchorPoint":[221.67257107604084,316.56452785377144],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[221.67257107604084,316.56452785377144],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[222.78002147975192,316.58196755365179],"reflectionModeOverride":0,"anchorPoint":[222.50257105935154,316.56452785377144],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[222.50257105935154,316.56452785377144],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[223.46754319256974,316.15798118973765],"reflectionModeOverride":0,"anchorPoint":[223.27257099819641,316.3245278149393],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[223.05416436926282,316.49652037792839],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[223.56257095468965,315.05452779244973],"reflectionModeOverride":0,"anchorPoint":[223.56257095468965,315.65452781629159],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[223.57458948209043,315.9106673969701],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[222.50257101191011,314.72452780913903],"reflectionModeOverride":0,"anchorPoint":[222.50257101191011,314.72452780913903],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[223.18257095945802,314.72452780913903],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[221.67257102859941,314.72452780913903],"reflectionModeOverride":0,"anchorPoint":[221.67257102859941,314.72452780913903],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[221.67257102859941,314.72452780913903],"nextPoint":[112.85257138121662,191.97453151588078]}],"closed":true,"reversed":false}},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[112.85257138121662,191.97453151588078],"opacity":1,"blur":0,"isLocked":false,"gid":7,"smootheningRate":0,"initialPoint":[112.85257138121662,191.97453151588078],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[223.39257110465107,318.92452774886726],"reflectionModeOverride":0,"anchorPoint":[223.39257110465107,318.92452774886726],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[223.39257110465107,318.92452774886726],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[222.33257116187153,317.04452775363563],"reflectionModeOverride":0,"anchorPoint":[222.33257116187153,317.04452775363563],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[222.33257116187153,317.04452775363563],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[221.67257113564548,317.04452775363563],"reflectionModeOverride":0,"anchorPoint":[221.67257113564548,317.04452775363563],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[221.67257113564548,317.04452775363563],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[221.67257113564548,318.97453151588081],"reflectionModeOverride":0,"anchorPoint":[221.67257113564548,318.97453151588081],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[221.67257113564548,318.97453151588081],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[221.06257112134037,318.97453151588081],"reflectionModeOverride":0,"anchorPoint":[221.06257112134037,318.97453151588081],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[221.06257112134037,318.97453151588081],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[221.06257112134037,314.29453168754219],"reflectionModeOverride":0,"anchorPoint":[221.06257112134037,314.29453168754219],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[221.06257112134037,314.29453168754219],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[222.85351610118363,314.2910670771638],"reflectionModeOverride":0,"anchorPoint":[222.64257116425571,314.29453168754219],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[222.64257116425571,314.29453168754219],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[223.44257068590508,314.46434348341825],"reflectionModeOverride":0,"anchorPoint":[223.26257121864279,314.39453162176318],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[223.06341253016012,314.32492145971571],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[223.8867055322342,314.82297152263595],"reflectionModeOverride":0,"anchorPoint":[223.75257125112995,314.69453165532451],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[223.60854628757448,314.56596123813961],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[224.15455094852729,315.31612333025106],"reflectionModeOverride":0,"anchorPoint":[224.07257117822328,315.14453165999419],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[223.99528738469252,314.97566482247669],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[224.19825083117667,315.9922372888982],"reflectionModeOverride":0,"anchorPoint":[224.19257122020886,315.69453168706491],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[224.19562660185056,315.50438689028209],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[223.7338072115908,316.75555373263109],"reflectionModeOverride":0,"anchorPoint":[223.91257119347284,316.51453168318102],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[224.09914577717603,316.28247388344346],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[223.19257120773227,317.01453168185151],"reflectionModeOverride":0,"anchorPoint":[223.19257120773227,317.01453168185151],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[223.48085346525613,316.9312160768892],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[222.99257120475204,317.08453168214953],"reflectionModeOverride":0,"anchorPoint":[222.99257120475204,317.08453168214953],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[222.99257120475204,317.08453168214953],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[224.0925712285939,318.97453166784442],"reflectionModeOverride":0,"anchorPoint":[224.0925712285939,318.97453166784442],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[224.0925712285939,318.97453166784442],"nextPoint":[112.85257138121662,191.97453151588078]}],"closed":true,"reversed":false}},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[112.85257138121662,191.97453151588078],"opacity":1,"blur":0,"isLocked":false,"gid":8,"smootheningRate":0,"initialPoint":[112.85257138121662,191.97453151588078],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[221.88165496610824,312.75214941635204],"reflectionModeOverride":0,"anchorPoint":[222.39257110465107,312.75452767257332],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[222.39257110465107,312.75452767257332],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[220.45378029794426,313.23854645592189],"reflectionModeOverride":0,"anchorPoint":[220.90257124445034,313.04452773196982],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[221.37529974656684,312.85070177480884],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[219.35876943156495,314.2208600575953],"reflectionModeOverride":0,"anchorPoint":[219.70257152159786,313.86452769906913],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[220.04640821090715,313.51691728007791],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[218.51848057480817,316.05954140545026],"reflectionModeOverride":0,"anchorPoint":[218.89257170245423,315.09452765536599],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[219.0841152815558,314.63792794299417],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[219.07885049250976,318.55391401613247],"reflectionModeOverride":0,"anchorPoint":[218.89257166857527,318.09452771720584],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[218.51848023468915,317.12951359608263],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[220.04640881460426,319.67213836278347],"reflectionModeOverride":0,"anchorPoint":[219.70257171508007,319.32452783266035],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[219.35412998721458,318.97193143333453],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[222.32887552639454,320.75534631866788],"reflectionModeOverride":0,"anchorPoint":[220.90257185775954,320.14452762388811],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[220.45378073087409,319.9505090873252],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[225.42747093525361,318.97720769048311],"reflectionModeOverride":0,"anchorPoint":[225.07257197531021,319.32452740981233],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[223.98375014972524,320.42992729161517],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[226.25666363710593,317.12951355179308],"reflectionModeOverride":0,"anchorPoint":[225.8825720147413,318.09452742555698],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[225.70369149262439,318.55776181404121],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[225.69870344883279,314.63387871118448],"reflectionModeOverride":0,"anchorPoint":[225.88257204862026,315.09452736371713],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[226.25666342066654,316.05954129932087],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[224.73054756988128,313.51475021332715],"reflectionModeOverride":0,"anchorPoint":[225.07257201185996,313.86452742080394],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[225.42312528062655,314.21540813724465],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[223.40302999066401,312.851739523118],"reflectionModeOverride":0,"anchorPoint":[223.87257194075443,313.04452740721484],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[224.32272320141846,313.23607014436448],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[222.39257194847221,312.754527354262],"reflectionModeOverride":0,"anchorPoint":[222.39257194847221,312.754527354262],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[222.90014945704277,312.7532021443493],"nextPoint":[112.85257138121662,191.97453151588078]}],"closed":false,"reversed":false}},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[112.85257138121662,191.97453151588078],"opacity":1,"blur":0,"isLocked":false,"gid":9,"smootheningRate":0,"initialPoint":[112.85257138121662,191.97453151588078],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[221.79793545229796,321.06845412941806],"reflectionModeOverride":0,"anchorPoint":[222.39257194847221,321.06452777387869],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[222.39257194847221,321.06452777387869],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[220.13630955687603,320.47368746402753],"reflectionModeOverride":0,"anchorPoint":[220.66257205810552,320.71452796827054],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[221.20891937561655,320.94928924957026],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[218.84008207185138,319.31458146286332],"reflectionModeOverride":0,"anchorPoint":[219.26257159607349,319.71452784560978],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[219.66107353858615,320.13423302542299],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[217.79637500761001,317.20177600727493],"reflectionModeOverride":0,"anchorPoint":[218.26257166453621,318.30452834140976],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[218.50031051375157,318.83550383670706],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[218.71657507260346,313.79480682341318],"reflectionModeOverride":0,"anchorPoint":[218.26257150938892,314.85452883246262],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[217.79637484257967,315.95728089190948],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[221.15964890699888,312.25319776201661],"reflectionModeOverride":0,"anchorPoint":[220.61257125382446,312.48452913388269],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[219.5567350056233,312.94749659064405],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[222.93948006388655,312.14028017121217],"reflectionModeOverride":0,"anchorPoint":[222.34257146546875,312.14452894709979],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[221.74863705025209,312.1374427227563],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[224.61118094730551,312.72639164873863],"reflectionModeOverride":0,"anchorPoint":[224.08257153232208,312.48452915920495],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[223.53116831317226,312.25589748063879],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[225.90916737355408,313.88955714821628],"reflectionModeOverride":0,"anchorPoint":[225.49257153005306,313.48452928232075],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[225.08951592668194,313.06563634089633],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[226.95876793869749,315.99728178436908],"reflectionModeOverride":0,"anchorPoint":[226.49257128177129,314.89452945023424],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[226.24808810980261,314.36743571685838],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[226.25070868333646,318.87313890226199],"reflectionModeOverride":0,"anchorPoint":[226.49257117287002,318.34452948727852],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[226.95876813673391,317.24177716378313],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[224.65955409863321,320.59420064219239],"reflectionModeOverride":0,"anchorPoint":[225.49257104975428,319.75452948500953],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[225.91146402567489,319.35147381264608],"nextPoint":[112.85257138121662,191.97453151588078]},{"outPoint":[222.34257072457945,321.06452950412722],"reflectionModeOverride":0,"anchorPoint":[222.34257072457945,321.06452950412722],"cornerRadius":0,"prevPoint":[112.85257138121662,191.97453151588078],"inPoint":[223.52534831488276,321.06588609892162],"nextPoint":[112.85257138121662,191.97453151588078]}],"closed":false,"reversed":false}},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[112.85257138121662,191.97453151588078],"opacity":1,"blur":0,"isLocked":false,"gid":10,"smootheningRate":0,"initialPoint":[112.85257138121662,191.97453151588078],"creationPoints":[],"name":"(curve)"}]}}},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[112.85257138121662,191.97453151588078],"opacity":1,"blur":0,"isLocked":false,"gid":6,"smootheningRate":0,"initialPoint":[112.85257138121662,191.97453151588078],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"fillColor":{"b":1,"s":0,"h":0,"a":1},"maskedElements":[],"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[189.49257363188801,253.97453151588081],"reflectionModeOverride":0,"anchorPoint":[189.49257363188801,253.97453151588081],"cornerRadius":0,"prevPoint":[112.85257138121662,152.97453151588078],"inPoint":[189.49257363188801,253.97453151588081],"nextPoint":[112.85257138121662,152.97453151588078]},{"outPoint":[198.97257317412434,244.48453174476265],"reflectionModeOverride":0,"anchorPoint":[198.97257317412434,244.48453174476265],"cornerRadius":0,"prevPoint":[112.85257138121662,152.97453151588078],"inPoint":[198.97257317412434,244.48453174476265],"nextPoint":[112.85257138121662,152.97453151588078]},{"outPoint":[189.49257363188801,235.01453147773384],"reflectionModeOverride":0,"anchorPoint":[189.49257363188801,235.01453147773384],"cornerRadius":0,"prevPoint":[112.85257138121662,152.97453151588078],"inPoint":[189.49257363188801,235.01453147773384],"nextPoint":[112.85257138121662,152.97453151588078]}],"closed":true,"reversed":false}}},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[112.85257138121662,152.97453151588078],"opacity":1,"blur":0,"isLocked":false,"gid":16,"smootheningRate":0,"initialPoint":[112.85257138121662,152.97453151588078],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[300,640],"reflectionModeOverride":0,"anchorPoint":[300,640],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[300,640],"nextPoint":[0,0]},{"outPoint":[380,580],"reflectionModeOverride":0,"anchorPoint":[340,580],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[300,580],"nextPoint":[0,0]}],"closed":false,"reversed":false}},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":2,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[0,0],"opacity":1,"blur":0,"isLocked":false,"gid":36,"smootheningRate":0,"initialPoint":[0,0],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[292.2196979967656,487.32457590003901],"reflectionModeOverride":0,"anchorPoint":[270.70254252087443,481.42866614307741],"cornerRadius":0,"prevPoint":[0,1.3634276556741725],"inPoint":[246.45876716860445,474.78563603599895],"nextPoint":[0,1.3634276556741725]},{"outPoint":[308.32019835237998,560.47255779544446],"reflectionModeOverride":0,"anchorPoint":[307.80961189523731,532.13830093176216],"cornerRadius":0,"prevPoint":[9.9345758586960642,16.47175369678871],"inPoint":[307.29902543809465,503.80404406807986],"nextPoint":[9.9345758586960642,16.47175369678871]}],"closed":false,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.13822251558303833,"a":1},"strokeStyle":{"color":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[9.9345758586960642,15.108326041114537],"opacity":1,"blur":0,"isLocked":false,"gid":44,"smootheningRate":0,"initialPoint":[9.9345758586960642,15.108326041114537],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(polygon)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[368.75294547403814,729.44134214300266],"reflectionModeOverride":0,"anchorPoint":[368.75294547403814,729.44134214300266],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[368.75294547403814,729.44134214300266],"nextPoint":[221.86289110820167,-279.40236718212691]},{"outPoint":[58.884741476317231,550.53885452571922],"reflectionModeOverride":0,"anchorPoint":[58.884741476317231,550.53885452571922],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[58.884741476317231,550.53885452571922],"nextPoint":[221.86289110820167,-279.40236718212691]},{"outPoint":[58.884771993693448,192.73377248033557],"reflectionModeOverride":0,"anchorPoint":[58.884771993693448,192.73377248033557],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[58.884771993693448,192.73377248033557],"nextPoint":[221.86289110820167,-279.40236718212691]},{"outPoint":[368.75296538097359,13.831330639116459],"reflectionModeOverride":0,"anchorPoint":[368.75296538097359,13.831330639116459],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[368.75296538097359,13.831330639116459],"nextPoint":[221.86289110820167,-279.40236718212691]},{"outPoint":[678.62124178681802,192.73392506721666],"reflectionModeOverride":0,"anchorPoint":[678.62124178681802,192.73392506721666],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[678.62124178681802,192.73392506721666],"nextPoint":[221.86289110820167,-279.40236718212691]},{"outPoint":[678.62118075206558,550.53885452571922],"reflectionModeOverride":0,"anchorPoint":[678.62118075206558,550.53885452571922],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[678.62118075206558,550.53885452571922],"nextPoint":[221.86289110820167,-279.40236718212691]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":0.3074892778331435},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":18.646402359008789,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[368.75296111419152,729.44134214300266],"opacity":1,"blur":0,"isLocked":false,"gid":57,"smootheningRate":0,"initialPoint":[368.75296111419152,371.63633639105956],"creationPoints":[],"name":"(polygon)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[340,700],"reflectionModeOverride":0,"anchorPoint":[340,660],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[340,620],"nextPoint":[0,0]},{"outPoint":[340,600],"reflectionModeOverride":0,"anchorPoint":[380,600],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[420,600],"nextPoint":[0,0]}],"closed":false,"reversed":false}},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":2,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[0,0],"opacity":1,"blur":0,"isLocked":false,"gid":35,"smootheningRate":0,"initialPoint":[0,0],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[345.33038784778722,480.48121812716067],"reflectionModeOverride":0,"anchorPoint":[345.33038784778722,480.48121812716067],"cornerRadius":0,"prevPoint":[86.563617484933189,-170.37985441312253],"inPoint":[308.36370636737951,480.48121812716067],"nextPoint":[86.563617484933189,-170.37985441312253]},{"outPoint":[271.39702488697174,480.48121812716067],"reflectionModeOverride":0,"anchorPoint":[271.39702488697174,480.48121812716067],"cornerRadius":0,"prevPoint":[86.563617484933189,-170.37985441312253],"inPoint":[271.39702488697174,480.48121812716067],"nextPoint":[86.563617484933189,-170.37985441312253]},{"outPoint":[345.33038784778722,480.48121812716067],"reflectionModeOverride":0,"anchorPoint":[345.33038784778722,480.48121812716067],"cornerRadius":0,"prevPoint":[86.563617484933189,-170.37985441312253],"inPoint":[345.33038784778722,480.48121812716067],"nextPoint":[86.563617484933189,-170.37985441312253]},{"outPoint":[308.36370636737951,480.48121812716067],"reflectionModeOverride":0,"anchorPoint":[271.39702488697174,480.48121812716067],"cornerRadius":0,"prevPoint":[86.563617484933189,-170.37985441312253],"inPoint":[271.39702488697174,480.48121812716067],"nextPoint":[86.563617484933189,-170.37985441312253]},{"outPoint":[308.36370636737951,547.81167390718997],"reflectionModeOverride":0,"anchorPoint":[308.36370636737951,547.81167390718997],"cornerRadius":0,"prevPoint":[86.563617484933189,-170.37985441312253],"inPoint":[308.36370636737951,547.81167390718997],"nextPoint":[86.563617484933189,-170.37985441312253]}],"closed":true,"reversed":true}},"fillColor":{"b":0,"s":0,"h":0,"a":1},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":2,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[579.21777967228218,-165.50118646031109],"opacity":1,"blur":0,"isLocked":false,"gid":37,"smootheningRate":0,"initialPoint":[579.21777967228218,-165.50118646031109],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(line)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[13.974435927889729,160.80268978822852],"reflectionModeOverride":0,"anchorPoint":[13.974435927889729,160.80268978822852],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[13.974435927889729,160.80268978822852],"nextPoint":[0,0]},{"outPoint":[144.25556161242727,291.78142890790133],"reflectionModeOverride":0,"anchorPoint":[144.25556161242727,291.78142890790133],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[144.25556161242727,291.78142890790133],"nextPoint":[0,0]}],"closed":false,"reversed":false}},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":20,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[0,0],"opacity":1,"blur":0,"isLocked":false,"gid":28,"smootheningRate":0,"initialPoint":[0,0],"creationPoints":[],"name":"(line)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[323.38754763944246,487.32457590003901],"reflectionModeOverride":0,"anchorPoint":[344.90470311533363,481.42866614307741],"cornerRadius":0,"prevPoint":[615.60724563620806,1.3634276556741725],"inPoint":[369.14847846760358,474.78563603599895],"nextPoint":[615.60724563620806,1.3634276556741725]},{"outPoint":[307.28704728382809,560.47255779544446],"reflectionModeOverride":0,"anchorPoint":[307.79763374097075,532.13830093176216],"cornerRadius":0,"prevPoint":[605.67266977751206,16.47175369678871],"inPoint":[308.30822019811342,503.80404406807986],"nextPoint":[605.67266977751206,16.47175369678871]}],"closed":false,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.13822251558303833,"a":1},"strokeStyle":{"color":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[605.67266977751206,16.47175369678871],"opacity":1,"blur":0,"isLocked":false,"gid":45,"smootheningRate":0,"initialPoint":[605.67266977751206,16.47175369678871],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[307.98765662154068,497.22766759942954],"reflectionModeOverride":0,"anchorPoint":[307.98765662154068,526.40806463936553],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[307.98765662154068,555.58846167930153],"nextPoint":[0,0]},{"outPoint":[347.07124677007465,468.5633046399771],"reflectionModeOverride":0,"anchorPoint":[331.56894097295356,478.9361833057668],"cornerRadius":0,"prevPoint":[0,0],"inPoint":[316.06663517583246,489.3090619715565],"nextPoint":[0,0]}],"closed":false,"reversed":false}},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":2,"width":20,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[0,0],"opacity":1,"blur":0,"isLocked":false,"gid":33,"smootheningRate":0,"initialPoint":[0,0],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"compoundPathData":{"subpaths":[{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[367.7690644381571,667.02363476289884],"reflectionModeOverride":0,"anchorPoint":[367.7690644381571,667.02363476289884],"cornerRadius":0,"prevPoint":[72.193980608055426,108.16792868331186],"inPoint":[368.26333755475412,638.68961321030247],"nextPoint":[72.193980608055426,108.16792868331186]},{"outPoint":[367.82059343747073,616.38299074283532],"reflectionModeOverride":0,"anchorPoint":[367.82059343747073,616.38299074283532],"cornerRadius":0,"prevPoint":[55.02340366515682,81.837967291336099],"inPoint":[367.82059343747073,616.38299074283532],"nextPoint":[55.02340366515682,81.837967291336099]},{"outPoint":[383.13716259159514,626.44746079947163],"reflectionModeOverride":0,"anchorPoint":[403.6767241609266,616.33647011725679],"cornerRadius":0,"prevPoint":[70.109671854888575,134.1591558035534],"inPoint":[403.6767241609266,616.33647011725679],"nextPoint":[70.109671854888575,134.1591558035534]}],"closed":true,"reversed":false}},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[72.357622652088821,121.79167358010636],"opacity":1,"blur":0,"isLocked":false,"gid":48,"smootheningRate":0,"initialPoint":[72.357622652088821,121.79167358010636],"creationPoints":[],"name":"(curve)"}]}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.13822251558303833,"a":1},"strokeStyle":{"color":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[-2.4740749822735211,26.734403557988003],"opacity":1,"blur":0,"isLocked":false,"gid":46,"smootheningRate":0,"initialPoint":[-2.4740749822735211,26.734403557988003],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"fillColor":{"b":0.98823529481887817,"s":1,"h":0.58068782582120682,"a":1},"maskedElements":[],"abstractPath":{"fillRule":0,"compoundPathData":{"subpaths":[{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[-17.790763092914176,455.94130474918654],"reflectionModeOverride":0,"anchorPoint":[-17.790763092914176,455.94130474918654],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[-17.790763092914176,455.94130474918654],"nextPoint":[-343.22817622315347,103.6410041510419]},{"outPoint":[-118.14742861878335,355.38893101990755],"reflectionModeOverride":0,"anchorPoint":[-118.14742861878335,355.38893101990755],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[-118.14742861878335,355.38893101990755],"nextPoint":[-343.22817622315347,103.6410041510419]},{"outPoint":[-89.082653539453304,326.03057263672883],"reflectionModeOverride":0,"anchorPoint":[-89.082653539453304,326.03057263672883],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[-89.082653539453304,326.03057263672883],"nextPoint":[-343.22817622315347,103.6410041510419]},{"outPoint":[-9.0811227455444623,406.22781863362536],"reflectionModeOverride":0,"anchorPoint":[-9.0811227455444623,406.22781863362536],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[-9.0811227455444623,406.22781863362536],"nextPoint":[-343.22817622315347,103.6410041510419]},{"outPoint":[-9.0811227455444623,214.66453204993843],"reflectionModeOverride":0,"anchorPoint":[-9.0811227455444623,214.66453204993843],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[-9.0811227455444623,214.66453204993843],"nextPoint":[-343.22817622315347,103.6410041510419]},{"outPoint":[136.29167736876548,359.98840044701058],"reflectionModeOverride":0,"anchorPoint":[136.29167736876548,359.98840044701058],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[136.29167736876548,359.98840044701058],"nextPoint":[-343.22817622315347,103.6410041510419]},{"outPoint":[40.289832016581158,455.94130474918654],"reflectionModeOverride":0,"anchorPoint":[40.289832016581158,455.94130474918654],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[40.289832016581158,455.94130474918654],"nextPoint":[-343.22817622315347,103.6410041510419]},{"outPoint":[136.29167736876548,551.89420905136228],"reflectionModeOverride":0,"anchorPoint":[136.29167736876548,551.89420905136228],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[136.29167736876548,551.89420905136228],"nextPoint":[-343.22817622315347,103.6410041510419]},{"outPoint":[-9.0322003610774573,697.21808678120522],"reflectionModeOverride":0,"anchorPoint":[-9.0322003610774573,697.21808678120522],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[-9.0322003610774573,697.21808678120522],"nextPoint":[-343.22817622315347,103.6410041510419]},{"outPoint":[-9.0322003610774573,505.65480953028907],"reflectionModeOverride":0,"anchorPoint":[-9.0322003610774573,505.65480953028907],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[-9.0322003610774573,505.65480953028907],"nextPoint":[-343.22817622315347,103.6410041510419]},{"outPoint":[-88.78907023560464,585.60738760822585],"reflectionModeOverride":0,"anchorPoint":[-88.78907023560464,585.60738760822585],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[-88.78907023560464,585.60738760822585],"nextPoint":[-343.22817622315347,103.6410041510419]},{"outPoint":[-117.85384531493469,556.24902922504714],"reflectionModeOverride":0,"anchorPoint":[-117.85384531493469,556.24902922504714],"cornerRadius":0,"prevPoint":[-343.22817622315347,103.6410041510419],"inPoint":[-117.85384531493469,556.24902922504714],"nextPoint":[-343.22817622315347,103.6410041510419]}],"closed":true,"reversed":false}},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[-343.22817622315347,103.6410041510419],"opacity":1,"blur":0,"isLocked":false,"gid":13,"smootheningRate":0,"initialPoint":[-343.22817622315347,103.6410041510419],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[-87.50742636811205,292.97453151588081],"reflectionModeOverride":0,"anchorPoint":[-87.50742636811205,292.97453151588081],"cornerRadius":0,"prevPoint":[-164.14742861878338,191.97453151588081],"inPoint":[-87.50742636811205,292.97453151588081],"nextPoint":[-164.14742861878338,191.97453151588081]},{"outPoint":[-78.027426825875665,283.48453174476265],"reflectionModeOverride":0,"anchorPoint":[-78.027426825875665,283.48453174476265],"cornerRadius":0,"prevPoint":[-164.14742861878338,191.97453151588081],"inPoint":[-78.027426825875665,283.48453174476265],"nextPoint":[-164.14742861878338,191.97453151588081]},{"outPoint":[-87.507426368111936,274.01453147773384],"reflectionModeOverride":0,"anchorPoint":[-87.507426368111936,274.01453147773384],"cornerRadius":0,"prevPoint":[-164.14742861878338,191.97453151588081],"inPoint":[-87.507426368111936,274.01453147773384],"nextPoint":[-164.14742861878338,191.97453151588081]}],"closed":true,"reversed":false}},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[-164.14742861878338,191.97453151588081],"opacity":1,"blur":0,"isLocked":false,"gid":14,"smootheningRate":0,"initialPoint":[-164.14742861878338,191.97453151588081],"creationPoints":[],"name":"(curve)"}]}}},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[-164.14742861878338,191.97453151588078],"opacity":1,"blur":0,"isLocked":false,"gid":11,"smootheningRate":0,"initialPoint":[-164.14742861878338,191.97453151588078],"creationPoints":[],"name":"(curve)"}],"cacheLayers":[],"cacheArtboardPaths":[],"undoStack":[[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":6}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -20, -20]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11,6]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -29.209948435277369, -232.41660739687057]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[12]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[12,13,14,15]}","argumentGID":0}],"methodSignature":"setSubpaths:","targetGID":11},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11}],"methodSignature":"setSuperpath:","targetGID":12},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":12},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":12},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setShadow:","targetGID":12},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11}],"methodSignature":"setSuperpath:","targetGID":13},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":13},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":13},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setShadow:","targetGID":13},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11}],"methodSignature":"setSuperpath:","targetGID":14},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":14},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":14},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setShadow:","targetGID":14}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":6},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":16}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -20, -20]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[16]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":16},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":16}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":14}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":16}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":16}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.01450315572447696,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.93007444840156717,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.014503091068591101,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.93451308072143913,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.014503091068591101,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.93910701763028559,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.014503091068591101,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.94055809677531532,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.014503091068591101,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.94999337125602745,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.013467045153601698,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.95297621548706413,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.013467045153601698,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.95790068402842177,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.011044777045815676,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.96266052064980523,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.0088583736096398309,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.96911575815444173,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.0077646546444650423,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.97217928832645584,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.0032726223185911016,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.98016728703982747,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.0010851843882415254,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.98662352066719217,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.99307006473711057,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":16}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[0.20437110010339629, -0, -0, 0.20437110010339629, 126.38769661378409, 170.79330548171168]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[13]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[12,13,14]}","argumentGID":0}],"methodSignature":"setSubpaths:","targetGID":11},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11}],"methodSignature":"setSuperpath:","targetGID":13},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":13},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":13},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setShadow:","targetGID":13},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11}],"methodSignature":"setSuperpath:","targetGID":14},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":14},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":14},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setShadow:","targetGID":14}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":21}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":185.15633048630471,\"AnchorPoint_Y\":354.17149931944061,\"inPoint_Y\":354.17149931944061,\"OutPoint_X\":185.15633048630471,\"AnchorPoint_X\":185.15633048630471,\"OutPoint_Y\":354.17149931944061},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":185.15633048630471,\"AnchorPoint_Y\":354.17149931944061,\"inPoint_Y\":354.17149931944061,\"OutPoint_X\":185.15633048630471,\"AnchorPoint_X\":185.15633048630471,\"OutPoint_Y\":354.17149931944061},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":386.50447364412418,\"AnchorPoint_Y\":554.43203524141848,\"inPoint_Y\":554.43203524141848,\"OutPoint_X\":386.50447364412418,\"AnchorPoint_X\":386.50447364412418,\"OutPoint_Y\":554.43203524141848},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":185.15633048630471,\"AnchorPoint_Y\":354.17149931944061,\"inPoint_Y\":354.17149931944061,\"OutPoint_X\":185.15633048630471,\"AnchorPoint_X\":185.15633048630471,\"OutPoint_Y\":354.17149931944061},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":386.50447364412418,\"AnchorPoint_Y\":554.43203524141848,\"inPoint_Y\":554.43203524141848,\"OutPoint_X\":386.50447364412418,\"AnchorPoint_X\":386.50447364412418,\"OutPoint_Y\":554.43203524141848},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":284.63851065196616,\"AnchorPoint_Y\":654.20422348213651,\"inPoint_Y\":654.20422348213651,\"OutPoint_X\":284.63851065196616,\"AnchorPoint_X\":284.63851065196616,\"OutPoint_Y\":654.20422348213651},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":185.15633048630471,\"AnchorPoint_Y\":354.17149931944061,\"inPoint_Y\":354.17149931944061,\"OutPoint_X\":185.15633048630471,\"AnchorPoint_X\":185.15633048630471,\"OutPoint_Y\":354.17149931944061},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":386.50447364412418,\"AnchorPoint_Y\":554.43203524141848,\"inPoint_Y\":554.43203524141848,\"OutPoint_X\":386.50447364412418,\"AnchorPoint_X\":386.50447364412418,\"OutPoint_Y\":554.43203524141848},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":284.63851065196616,\"AnchorPoint_Y\":654.20422348213651,\"inPoint_Y\":654.20422348213651,\"OutPoint_X\":284.63851065196616,\"AnchorPoint_X\":284.63851065196616,\"OutPoint_Y\":654.20422348213651},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":286.57710140084737,\"AnchorPoint_Y\":264.05198470231699,\"inPoint_Y\":264.05198470231699,\"OutPoint_X\":286.57710140084737,\"AnchorPoint_X\":286.57710140084737,\"OutPoint_Y\":264.05198470231699},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":185.15633048630471,\"AnchorPoint_Y\":354.17149931944061,\"inPoint_Y\":354.17149931944061,\"OutPoint_X\":185.15633048630471,\"AnchorPoint_X\":185.15633048630471,\"OutPoint_Y\":354.17149931944061},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":386.50447364412418,\"AnchorPoint_Y\":554.43203524141848,\"inPoint_Y\":554.43203524141848,\"OutPoint_X\":386.50447364412418,\"AnchorPoint_X\":386.50447364412418,\"OutPoint_Y\":554.43203524141848},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":284.63851065196616,\"AnchorPoint_Y\":654.20422348213651,\"inPoint_Y\":654.20422348213651,\"OutPoint_X\":284.63851065196616,\"AnchorPoint_X\":284.63851065196616,\"OutPoint_Y\":654.20422348213651},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":286.57710140084737,\"AnchorPoint_Y\":264.05198470231699,\"inPoint_Y\":264.05198470231699,\"OutPoint_X\":286.57710140084737,\"AnchorPoint_X\":286.57710140084737,\"OutPoint_Y\":264.05198470231699},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":383.69928863669242,\"AnchorPoint_Y\":361.21410026752545,\"inPoint_Y\":361.21410026752545,\"OutPoint_X\":383.69928863669242,\"AnchorPoint_X\":383.69928863669242,\"OutPoint_Y\":361.21410026752545},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":1}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":1,\"WDHueKey\":0.5806878306878307,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":1}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":1,\"WDHueKey\":0.5806878306878307,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":37.148059844970703}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":1,\"WDHueKey\":0.5806878306878307,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":2,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":37.148059844970703}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":185.15633048630471,\"AnchorPoint_Y\":354.17149931944061,\"inPoint_Y\":354.17149931944061,\"OutPoint_X\":185.15633048630471,\"AnchorPoint_X\":185.15633048630471,\"OutPoint_Y\":354.17149931944061},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":386.50447364412418,\"AnchorPoint_Y\":554.43203524141848,\"inPoint_Y\":554.43203524141848,\"OutPoint_X\":386.50447364412418,\"AnchorPoint_X\":386.50447364412418,\"OutPoint_Y\":554.43203524141848},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":284.63851065196616,\"AnchorPoint_Y\":654.20422348213651,\"inPoint_Y\":654.20422348213651,\"OutPoint_X\":284.63851065196616,\"AnchorPoint_X\":284.63851065196616,\"OutPoint_Y\":654.20422348213651},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":286.57710140084737,\"AnchorPoint_Y\":264.05198470231699,\"inPoint_Y\":264.05198470231699,\"OutPoint_X\":286.57710140084737,\"AnchorPoint_X\":286.57710140084737,\"OutPoint_Y\":264.05198470231699},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":383.69928863669242,\"AnchorPoint_Y\":361.21410026752545,\"inPoint_Y\":361.21410026752545,\"OutPoint_X\":383.69928863669242,\"AnchorPoint_X\":383.69928863669242,\"OutPoint_Y\":361.21410026752545},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":186.05023255601679,\"AnchorPoint_Y\":558.85414169234264,\"inPoint_Y\":558.85414169234264,\"OutPoint_X\":186.05023255601679,\"AnchorPoint_X\":186.05023255601679,\"OutPoint_Y\":558.85414169234264},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":185.15633048630471,\"AnchorPoint_Y\":354.17149931944061,\"inPoint_Y\":354.17149931944061,\"OutPoint_X\":185.15633048630471,\"AnchorPoint_X\":185.15633048630471,\"OutPoint_Y\":354.17149931944061},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":386.50447364412418,\"AnchorPoint_Y\":554.43203524141848,\"inPoint_Y\":554.43203524141848,\"OutPoint_X\":386.50447364412418,\"AnchorPoint_X\":386.50447364412418,\"OutPoint_Y\":554.43203524141848},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":284.63851065196616,\"AnchorPoint_Y\":654.20422348213651,\"inPoint_Y\":654.20422348213651,\"OutPoint_X\":284.63851065196616,\"AnchorPoint_X\":284.63851065196616,\"OutPoint_Y\":654.20422348213651},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":286.57710140084737,\"AnchorPoint_Y\":264.05198470231699,\"inPoint_Y\":264.05198470231699,\"OutPoint_X\":286.57710140084737,\"AnchorPoint_X\":286.57710140084737,\"OutPoint_Y\":264.05198470231699},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":383.69928863669242,\"AnchorPoint_Y\":361.21410026752545,\"inPoint_Y\":361.21410026752545,\"OutPoint_X\":383.69928863669242,\"AnchorPoint_X\":383.69928863669242,\"OutPoint_Y\":361.21410026752545},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":1,\"NodePoints\":{\"inPoint_X\":235.46249740746129,\"AnchorPoint_Y\":558.85414169234264,\"inPoint_Y\":509.44413050490067,\"OutPoint_X\":136.6379677045723,\"AnchorPoint_X\":186.05023255601679,\"OutPoint_Y\":608.26415287978466},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782582120682,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.98823529481887817,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[11]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":16},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":1,\"WDHueKey\":0.5806878306878307,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":37.148059844970703}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":1,\"WDHueKey\":0.5806878306878307,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":37.148059844970703}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0.03408561318607653,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.016189711030229925,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":37.148059844970703}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0.029926881951800856,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.016189711030229925,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":37.148059844970703}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0.021585238181938561,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.012961911164688478,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":37.148059844970703}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0.011895648503707629,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.007872629590133573,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":37.148059844970703}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0.005957393322960804,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":37.148059844970703}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":37.148059844970703}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":37.148059844970703}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":21.344829559326172}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":21.344829559326172}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":20}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[-4.3711390001862419e-08, -0.99999999999999922, 0.99999999999999922, -4.3711390001862419e-08, -173.29768953296772, 744.95852622656867]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 8.2190355245806472, 176.90598115888827]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":11},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":22}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 0.55538688884063114, -0, 182.49084631957811]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[22]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":24}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 150.32620239929324, 36.081797695525552]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[24]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[24]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0.12249755187863798, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[24]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[0.64978701032960984, -0, -0, 0.64978701032960984, 52.531948450558538, 142.05406077476505]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[24]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -55.171276915352394, 196.69205141995428]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[24]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[0.65616231221472443, -0, -0, 0.65616231221472443, 51.425334356867019, 141.12771095949628]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[22]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -55.53221494113933, 129.60740108742715]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[22]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 205.17127691535239, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[24]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 205.09503537570964, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[22]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 208.92985571850454]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[24]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 130.84123782858848]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[22]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 75]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[22]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":25}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -20, -20]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[25]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 4.5418914390142788, -306.56258907512347]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[25]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[0.96223302635797447, -0, -0, 0.96223302635797447, -0, 2.8325230231519103]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[22]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1.0392493219135692, -0, -0, 1.0392493219135692, 0, -2.9436991435176969]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[22]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 0.96223302635797447, -0, 2.8325230231519103]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[22]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 15.458108560985721, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[25]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 26.562589075123469]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[25]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 25]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[25]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 50]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[25]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -25]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[25]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":22}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":26}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -20, -20]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":20}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":0.10000000149011612}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":1,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":0.10000000149011612}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 9.881426012744555, 0.91199734232554874]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -77.745954011856824, 28.270796276850916]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 67.864527999112269, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 10, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1.1538461432654479, -0, -0, 1, -1.5384614326544779, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[26]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -136.98574299882625, 7.8612196478959504]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":27}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":25},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":3}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":24},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":26},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":3}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":22},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setElements:","targetGID":27},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -53.141264363887032, -43.604121250556744]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -70.052055171480333, -2.5049969673447094]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 219.77475934149726, 163.57319520938375]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":27},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":27}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[0.6846814845770105, -0, -0, 0.6846814845770105, -0.080018108436871196, 3.1890182924268196]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -85.587957051898314, -21.453286888159028]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":20}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setShadow:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDShadowOffsetKey\":10,\"WDShadowAngleKey\":1.5707999467849731,\"WDShadowRadiusKey\":10,\"WDShadowColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":0.33300000000000002,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"$class\":\"WDShadow\"}}","argumentGID":0}],"methodSignature":"setShadow:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setShadow:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDShadowOffsetKey\":10,\"WDShadowAngleKey\":1.5707999467849731,\"WDShadowRadiusKey\":10,\"WDShadowColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":0.33300000000000002,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"$class\":\"WDShadow\"}}","argumentGID":0}],"methodSignature":"setShadow:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":20}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":21}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":28}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":28},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":29}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":29}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":19.067760461856238,\"AnchorPoint_Y\":162.4737431408027,\"inPoint_Y\":162.4737431408027,\"OutPoint_X\":19.067760461856238,\"AnchorPoint_X\":19.067760461856238,\"OutPoint_Y\":162.4737431408027},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":29}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":19.067760461856238,\"AnchorPoint_Y\":162.4737431408027,\"inPoint_Y\":162.4737431408027,\"OutPoint_X\":19.067760461856238,\"AnchorPoint_X\":19.067760461856238,\"OutPoint_Y\":162.4737431408027},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":142.86303913891427,\"AnchorPoint_Y\":297.21030751597522,\"inPoint_Y\":297.21030751597522,\"OutPoint_X\":142.86303913891427,\"AnchorPoint_X\":142.86303913891427,\"OutPoint_Y\":297.21030751597522},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":29}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":3,"argumentEncodedJsonString":"{\"Argument\":false}","argumentGID":0}],"methodSignature":"setClosed:","targetGID":29}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":29}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782582120682,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.98823529481887817,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":29}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":30}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -20, -20]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -258.8309537310077, 14.790477788784642]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":21},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":21}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":21},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":3}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":21}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":3}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":1,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":20}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":30}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":1,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":20}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":29}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":3}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":3}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":1}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[0.98660023945785613, -0, -0, 1.0127745463822073, 7.5897658853896965, -3.7483052243093384]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[30]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":3}","argumentGID":0},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0}],"methodSignature":"exchangeObjectAtIndex:withObjectAtIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":31}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":1,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":0.10000000149011612}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":31}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":1,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":27.568212509155273}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":31}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":27.568212509155273}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":31}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":1,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":29}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782582120682,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.98823529481887817,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782582120682,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.98823529481887817,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782582120682,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.98823529481887817,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782582120682,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.98823529481887817,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11623311042785645,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11623311042785645,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11623311042785645,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11623311042785645,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11518210172653198,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11518210172653198,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11518210172653198,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11518210172653198,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11062517762184143,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11062517762184143,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11062517762184143,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11062517762184143,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10827497392892838,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10827497392892838,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10827497392892838,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10827497392892838,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10599262267351151,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10599262267351151,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10599262267351151,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10599262267351151,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10487814247608185,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10487814247608185,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10487814247608185,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10487814247608185,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10140901058912277,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10140901058912277,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10140901058912277,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10140901058912277,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10024447739124298,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10024447739124298,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10024447739124298,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10024447739124298,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.0991511270403862,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.0991511270403862,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.0991511270403862,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.0991511270403862,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.095819912850856781,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.095819912850856781,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.095819912850856781,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.095819912850856781,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.093592062592506436,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.093592062592506436,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.093592062592506436,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.093592062592506436,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.091305255889892578,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.091305255889892578,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.091305255889892578,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.091305255889892578,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.086611531674861908,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.086611531674861908,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.086611531674861908,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.086611531674861908,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.084330290555953952,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.084330290555953952,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.084330290555953952,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.084330290555953952,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.082062393426895142,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.082062393426895142,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.082062393426895142,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.082062393426895142,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.080980166792869596,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.080980166792869596,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.080980166792869596,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.080980166792869596,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.078774556517601013,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.078774556517601013,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.078774556517601013,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.078774556517601013,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.079864569008350372,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.079864569008350372,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.079864569008350372,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.079864569008350372,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.084498241543769864,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.084498241543769864,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.084498241543769864,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.084498241543769864,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.090882599353790255,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.090882599353790255,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.090882599353790255,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.090882599353790255,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.09557187557220459,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.09557187557220459,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.09557187557220459,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.09557187557220459,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.0990910604596138,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.0990910604596138,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.0990910604596138,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.0990910604596138,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10261469334363937,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10261469334363937,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10261469334363937,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10261469334363937,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10601820796728134,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10601820796728134,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10601820796728134,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10601820796728134,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10719386488199234,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10719386488199234,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10719386488199234,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.10719386488199234,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11685270816087723,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11685270816087723,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11685270816087723,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.11685270816087723,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.12441384047269821,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.12441384047269821,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.12441384047269821,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.12441384047269821,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.12794080376625061,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.12794080376625061,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.12794080376625061,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.12794080376625061,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13263343274593353,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13263343274593353,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13263343274593353,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13263343274593353,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13615038990974426,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13615038990974426,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13615038990974426,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13615038990974426,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13955500721931458,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13955500721931458,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13955500721931458,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13955500721931458,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14536766707897186,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14536766707897186,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14536766707897186,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14536766707897186,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15242382884025574,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15242382884025574,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15242382884025574,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15242382884025574,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15580509603023529,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15580509603023529,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15580509603023529,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15580509603023529,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15815529227256775,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15815529227256775,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15815529227256775,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15815529227256775,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.16035088896751404,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.16035088896751404,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.16035088896751404,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.16035088896751404,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15694738924503326,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15694738924503326,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15694738924503326,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15694738924503326,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15200117230415344,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15200117230415344,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15200117230415344,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.15200117230415344,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14730300009250641,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14730300009250641,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14730300009250641,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14730300009250641,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14378604292869568,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14378604292869568,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14378604292869568,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14378604292869568,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14143472909927368,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14143472909927368,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14143472909927368,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14143472909927368,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14034359157085419,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14034359157085419,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14034359157085419,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.14034359157085419,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":26}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":1,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":30}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":32}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -20, -20]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[32]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[27,21,30,32,29,31]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[21]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":33}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":33}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":307.98765662154068,\"AnchorPoint_Y\":526.40806463936553,\"inPoint_Y\":555.58846167930153,\"OutPoint_X\":307.98765662154068,\"AnchorPoint_X\":307.98765662154068,\"OutPoint_Y\":497.22766759942954},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":33}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":1,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":20}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":33}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":33},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":9223372036854775807}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":34}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":34}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":320,\"AnchorPoint_Y\":640,\"inPoint_Y\":680,\"OutPoint_X\":320,\"AnchorPoint_X\":320,\"OutPoint_Y\":600},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":34}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":320,\"AnchorPoint_Y\":640,\"inPoint_Y\":680,\"OutPoint_X\":320,\"AnchorPoint_X\":320,\"OutPoint_Y\":600},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":340,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":380,\"AnchorPoint_X\":360,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":34}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":320,\"AnchorPoint_Y\":640,\"inPoint_Y\":680,\"OutPoint_X\":320,\"AnchorPoint_X\":320,\"OutPoint_Y\":600},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":340,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":380,\"AnchorPoint_X\":360,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":260,\"AnchorPoint_X\":280,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":34},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":3,"argumentEncodedJsonString":"{\"Argument\":false}","argumentGID":0}],"methodSignature":"setClosed:","targetGID":34}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":320,\"AnchorPoint_Y\":640,\"inPoint_Y\":580,\"OutPoint_X\":320,\"AnchorPoint_X\":320,\"OutPoint_Y\":680.00002878904479},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":340,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":380,\"AnchorPoint_X\":360,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":260,\"AnchorPoint_X\":280,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":34}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":34},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":6}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":35}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":35}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":340,\"AnchorPoint_Y\":660,\"inPoint_Y\":620,\"OutPoint_X\":340,\"AnchorPoint_X\":340,\"OutPoint_Y\":700},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":35}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":35},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":6}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":36}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":36}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":640,\"inPoint_Y\":640,\"OutPoint_X\":300,\"AnchorPoint_X\":300,\"OutPoint_Y\":640},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":36}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":37}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -20, -20]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 20, 20]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[-1, -0, 0, 1, 640, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"KeepNodesAfterTransformTransformOptionKey\":true}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 39, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":640,\"inPoint_Y\":640,\"OutPoint_X\":300,\"AnchorPoint_X\":300,\"OutPoint_Y\":640},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{600, 0}\",\"VNPrevPointForRadiusKey\":\"{600, 0}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":220,\"AnchorPoint_X\":260,\"OutPoint_Y\":580},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{600, 0}\",\"VNPrevPointForRadiusKey\":\"{600, 0}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":37}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":640,\"inPoint_Y\":640,\"OutPoint_X\":300,\"AnchorPoint_X\":300,\"OutPoint_Y\":640},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{600, 0}\",\"VNPrevPointForRadiusKey\":\"{600, 0}\",\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":260,\"AnchorPoint_X\":260,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":37}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":640,\"inPoint_Y\":640,\"OutPoint_X\":300,\"AnchorPoint_X\":300,\"OutPoint_Y\":640},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{600, 0}\",\"VNPrevPointForRadiusKey\":\"{600, 0}\",\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":260,\"AnchorPoint_X\":260,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":340,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":340,\"AnchorPoint_X\":340,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":37}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":640,\"inPoint_Y\":640,\"OutPoint_X\":300,\"AnchorPoint_X\":300,\"OutPoint_Y\":640},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{600, 0}\",\"VNPrevPointForRadiusKey\":\"{600, 0}\",\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":260,\"AnchorPoint_X\":260,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":340,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":340,\"AnchorPoint_X\":340,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":260,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":260,\"AnchorPoint_X\":260,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":37}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":640,\"inPoint_Y\":640,\"OutPoint_X\":300,\"AnchorPoint_X\":300,\"OutPoint_Y\":640},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{600, 0}\",\"VNPrevPointForRadiusKey\":\"{600, 0}\",\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":260,\"AnchorPoint_X\":260,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":340,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":340,\"AnchorPoint_X\":340,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":260,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":260,\"AnchorPoint_X\":260,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":340,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":340,\"AnchorPoint_X\":340,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":37}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 60, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":240,\"AnchorPoint_Y\":640,\"inPoint_Y\":640,\"OutPoint_X\":240,\"AnchorPoint_X\":240,\"OutPoint_Y\":640},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{540, 0}\",\"VNPrevPointForRadiusKey\":\"{540, 0}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":240,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":200,\"AnchorPoint_X\":200,\"OutPoint_Y\":580},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-60, 0}\",\"VNPrevPointForRadiusKey\":\"{-60, 0}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":280,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":280,\"AnchorPoint_X\":280,\"OutPoint_Y\":580},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-60, 0}\",\"VNPrevPointForRadiusKey\":\"{-60, 0}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":200,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":200,\"AnchorPoint_X\":200,\"OutPoint_Y\":580},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-60, 0}\",\"VNPrevPointForRadiusKey\":\"{-60, 0}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":280,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":300,\"AnchorPoint_X\":280,\"OutPoint_Y\":600},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-60, 0}\",\"VNPrevPointForRadiusKey\":\"{-60, 0}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":37},{"actionTypeRawValue":"proxy","methodArguments":[],"methodSignature":"reversePathDirection","targetGID":37}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":300,\"AnchorPoint_Y\":580,\"inPoint_Y\":600,\"OutPoint_X\":280,\"AnchorPoint_X\":280,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":200,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":200,\"AnchorPoint_X\":200,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":280,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":280,\"AnchorPoint_X\":280,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":200,\"AnchorPoint_Y\":580,\"inPoint_Y\":580,\"OutPoint_X\":240,\"AnchorPoint_X\":200,\"OutPoint_Y\":580},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":240,\"AnchorPoint_Y\":640,\"inPoint_Y\":640,\"OutPoint_X\":240,\"AnchorPoint_X\":240,\"OutPoint_Y\":640},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":37},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":3,"argumentEncodedJsonString":"{\"Argument\":false}","argumentGID":0}],"methodSignature":"setClosed:","targetGID":37}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":36},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":6}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -60, 100]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -8.3360061810856791, -0.18596575427056905]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":37}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0.12624915594960839, -0.84513498021431133]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1.0820554726071352, -0, -0, 1, -28.572516179113212, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 3.7693360211719664, 0.58007166688469169]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 0.8911271920692454, -0, 58.840421083655528]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0.018101184397323777, -6.1834757589472247]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0.5616683932951787, -14.721591768371582]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1.4335340527209723, 13.544422687729025]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[37]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":37},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":6}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":43}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.10440918421369,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":598.11903819000008},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":314.21352490073997,\"AnchorPoint_Y\":480.08137659705397,\"inPoint_Y\":479.94813303079826,\"OutPoint_X\":366.0442444460744,\"AnchorPoint_X\":340.12888467340719,\"OutPoint_Y\":480.21462016330969},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":314.21352490073997,\"AnchorPoint_Y\":480.08137659705397,\"inPoint_Y\":479.94813303079826,\"OutPoint_X\":340.12888467340719,\"AnchorPoint_X\":340.12888467340719,\"OutPoint_Y\":480.08137659705397},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":314.21352490073997,\"AnchorPoint_Y\":480.08137659705397,\"inPoint_Y\":479.94813303079826,\"OutPoint_X\":340.12888467340719,\"AnchorPoint_X\":340.12888467340719,\"OutPoint_Y\":480.08137659705397},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":337.32713106884489,\"AnchorPoint_Y\":505.14299981511687,\"inPoint_Y\":505.14299981511687,\"OutPoint_X\":337.32713106884489,\"AnchorPoint_X\":337.32713106884489,\"OutPoint_Y\":505.14299981511687},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":314.21352490073997,\"AnchorPoint_Y\":480.08137659705397,\"inPoint_Y\":479.94813303079826,\"OutPoint_X\":340.12888467340719,\"AnchorPoint_X\":340.12888467340719,\"OutPoint_Y\":480.08137659705397},\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":268.41762507168443,\"AnchorPoint_Y\":478.86160216124927,\"inPoint_Y\":478.86160216124927,\"OutPoint_X\":268.41762507168443,\"AnchorPoint_X\":268.41762507168443,\"OutPoint_Y\":478.86160216124927},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-68.909505997160466, -26.2813976538676}\",\"VNPrevPointForRadiusKey\":\"{-68.909505997160466, -26.2813976538676}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":268.41762507168443,\"AnchorPoint_Y\":478.86160216124927,\"inPoint_Y\":478.86160216124927,\"OutPoint_X\":268.41762507168443,\"AnchorPoint_X\":268.41762507168443,\"OutPoint_Y\":478.86160216124927},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-68.909505997160466, -26.2813976538676}\",\"VNPrevPointForRadiusKey\":\"{-68.909505997160466, -26.2813976538676}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":282.38947067174871,\"AnchorPoint_Y\":502.68576551434728,\"inPoint_Y\":502.68576551434728,\"OutPoint_X\":282.38947067174871,\"AnchorPoint_X\":282.38947067174871,\"OutPoint_Y\":502.68576551434728},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.41762507168443,\"AnchorPoint_Y\":478.86160216124927,\"inPoint_Y\":478.86160216124927,\"OutPoint_X\":268.41762507168443,\"AnchorPoint_X\":268.41762507168443,\"OutPoint_Y\":478.86160216124927},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":333.80634908853523,\"AnchorPoint_Y\":479.14166624396699,\"inPoint_Y\":479.14166624396699,\"OutPoint_X\":333.80634908853523,\"AnchorPoint_X\":333.80634908853523,\"OutPoint_Y\":479.14166624396699},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{51.416878416786517, -23.544099270380286}\",\"VNPrevPointForRadiusKey\":\"{51.416878416786517, -23.544099270380286}\",\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.41762507168443,\"AnchorPoint_Y\":478.86160216124927,\"inPoint_Y\":478.86160216124927,\"OutPoint_X\":268.41762507168443,\"AnchorPoint_X\":268.41762507168443,\"OutPoint_Y\":478.86160216124927},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":342.99265545324346,\"AnchorPoint_Y\":479.14166624396699,\"inPoint_Y\":494.80574311033359,\"OutPoint_X\":324.620042723827,\"AnchorPoint_X\":333.80634908853523,\"OutPoint_Y\":463.4775893776004},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.41762507168443,\"AnchorPoint_Y\":478.86160216124927,\"inPoint_Y\":478.86160216124927,\"OutPoint_X\":268.41762507168443,\"AnchorPoint_X\":268.41762507168443,\"OutPoint_Y\":478.86160216124927},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":333.80634908853523,\"AnchorPoint_Y\":479.14166624396699,\"inPoint_Y\":479.14166624396699,\"OutPoint_X\":333.80634908853523,\"AnchorPoint_X\":333.80634908853523,\"OutPoint_Y\":479.14166624396699},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.41762507168443,\"AnchorPoint_Y\":478.86160216124927,\"inPoint_Y\":478.86160216124927,\"OutPoint_X\":268.41762507168443,\"AnchorPoint_X\":268.41762507168443,\"OutPoint_Y\":478.86160216124927},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":3,\"NodePoints\":{\"inPoint_X\":342.99265545324346,\"AnchorPoint_Y\":479.14166624396699,\"inPoint_Y\":494.80574311033359,\"OutPoint_X\":324.620042723827,\"AnchorPoint_X\":333.80634908853523,\"OutPoint_Y\":463.4775893776004},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.41762507168443,\"AnchorPoint_Y\":478.86160216124927,\"inPoint_Y\":478.86160216124927,\"OutPoint_X\":268.41762507168443,\"AnchorPoint_X\":268.41762507168443,\"OutPoint_Y\":478.86160216124927},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":3,\"NodePoints\":{\"inPoint_X\":342.99265545324346,\"AnchorPoint_Y\":479.14166624396699,\"inPoint_Y\":494.80574311033359,\"OutPoint_X\":333.79302393751607,\"AnchorPoint_X\":333.80634908853523,\"OutPoint_Y\":478.98295741265042},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.41762507168443,\"AnchorPoint_Y\":478.86160216124927,\"inPoint_Y\":478.86160216124927,\"OutPoint_X\":268.41762507168443,\"AnchorPoint_X\":268.41762507168443,\"OutPoint_Y\":478.86160216124927},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.38209900930354,\"AnchorPoint_Y\":478.82499281784192,\"inPoint_Y\":478.82499281784192,\"OutPoint_X\":268.38209900930354,\"AnchorPoint_X\":268.38209900930354,\"OutPoint_Y\":478.82499281784192},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.22129242932607,\"AnchorPoint_Y\":541.51791009776923,\"inPoint_Y\":484.91678200553838,\"OutPoint_X\":305.16285080676988,\"AnchorPoint_X\":305.16285080676988,\"OutPoint_Y\":541.51791009776923},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":3,\"NodePoints\":{\"inPoint_X\":303.99512941310888,\"AnchorPoint_Y\":479.14166624396699,\"inPoint_Y\":487.18557523329599,\"OutPoint_X\":333.79302393751607,\"AnchorPoint_X\":333.80634908853523,\"OutPoint_Y\":478.98295741265042},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":268.41762507168443,\"AnchorPoint_Y\":478.86160216124927,\"inPoint_Y\":478.86160216124927,\"OutPoint_X\":268.41762507168443,\"AnchorPoint_X\":268.41762507168443,\"OutPoint_Y\":478.86160216124927},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":43}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":44}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":44}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":249.48159496463126,\"AnchorPoint_Y\":480.06523848740324,\"inPoint_Y\":466.59075022213392,\"OutPoint_X\":291.92349007711761,\"AnchorPoint_X\":270.70254252087443,\"OutPoint_Y\":493.53972675267255},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":44}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":249.48159496463126,\"AnchorPoint_Y\":480.06523848740324,\"inPoint_Y\":466.59075022213392,\"OutPoint_X\":291.92349007711761,\"AnchorPoint_X\":270.70254252087443,\"OutPoint_Y\":493.53972675267255},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":297.36444957939858,\"AnchorPoint_Y\":515.66654723497345,\"inPoint_Y\":487.33229037129115,\"OutPoint_X\":298.38562249368391,\"AnchorPoint_X\":297.87503603654125,\"OutPoint_Y\":544.00080409865575},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":44}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":246.45876716860445,\"AnchorPoint_Y\":480.06523848740324,\"inPoint_Y\":473.42220838032478,\"OutPoint_X\":292.2196979967656,\"AnchorPoint_X\":270.70254252087443,\"OutPoint_Y\":485.96114824436484},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":297.36444957939858,\"AnchorPoint_Y\":515.66654723497345,\"inPoint_Y\":487.33229037129115,\"OutPoint_X\":298.38562249368391,\"AnchorPoint_X\":297.87503603654125,\"OutPoint_Y\":544.00080409865575},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":44}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":45}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":246.45876716860445,\"AnchorPoint_Y\":480.06523848740324,\"inPoint_Y\":473.42220838032478,\"OutPoint_X\":292.2196979967656,\"AnchorPoint_X\":270.70254252087443,\"OutPoint_Y\":485.96114824436484},\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":307.29902543809465,\"AnchorPoint_Y\":530.77487327608799,\"inPoint_Y\":502.44061641240569,\"OutPoint_X\":308.32019835237998,\"AnchorPoint_X\":307.80961189523731,\"OutPoint_Y\":559.10913013977029},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{9.9345758586960642, 15.108326041114537}\",\"VNPrevPointForRadiusKey\":\"{9.9345758586960642, 15.108326041114537}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":45}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[-1, -0, 0, 1, 618.5121544161118, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[45]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"KeepNodesAfterTransformTransformOptionKey\":true}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -17.095091220096265, 18.636572344325828]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[45]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":246.45876716860445,\"AnchorPoint_Y\":480.06523848740324,\"inPoint_Y\":473.42220838032478,\"OutPoint_X\":292.2196979967656,\"AnchorPoint_X\":270.70254252087443,\"OutPoint_Y\":485.96114824436484},\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":307.29902543809465,\"AnchorPoint_Y\":530.77487327608799,\"inPoint_Y\":502.44061641240569,\"OutPoint_X\":308.32019835237998,\"AnchorPoint_X\":307.80961189523731,\"OutPoint_Y\":559.10913013977029},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{9.9345758586960642, 15.108326041114537}\",\"VNPrevPointForRadiusKey\":\"{9.9345758586960642, 15.108326041114537}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":44}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":46}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":45},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":8}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":44},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":6}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 2.4740749822735211, -26.734403557988003]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[46]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":50}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":50}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":305.84902480507895,\"AnchorPoint_Y\":558.859403557988,\"inPoint_Y\":530.52519454298988,\"OutPoint_X\":305.33842501772648,\"AnchorPoint_X\":305.33842501772648,\"OutPoint_Y\":558.859403557988},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-2.4740749822735211, 26.734403557988003}\",\"VNPrevPointForRadiusKey\":\"{-2.4740749822735211, 26.734403557988003}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":342.43217501772648,\"AnchorPoint_Y\":508.171903557988,\"inPoint_Y\":508.171903557988,\"OutPoint_X\":320.91501841129599,\"AnchorPoint_X\":342.43217501772648,\"OutPoint_Y\":514.06781336097845},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-2.4740749822735211, 26.734403557988003}\",\"VNPrevPointForRadiusKey\":\"{-2.4740749822735211, 26.734403557988003}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":48}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":305.84902480507895,\"AnchorPoint_Y\":558.859403557988,\"inPoint_Y\":530.52519454298988,\"OutPoint_X\":305.33842501772648,\"AnchorPoint_X\":305.33842501772648,\"OutPoint_Y\":558.859403557988},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":323.12940602919167,\"AnchorPoint_Y\":534.54856008655031,\"inPoint_Y\":534.54856008655031,\"OutPoint_X\":323.12940602919167,\"AnchorPoint_X\":323.12940602919167,\"OutPoint_Y\":534.54856008655031},\"$class\":\"WDBezierNode\"},{\"NodeTypeOverride\":0,\"NodePoints\":{\"inPoint_X\":342.43217501772648,\"AnchorPoint_Y\":508.171903557988,\"inPoint_Y\":508.171903557988,\"OutPoint_X\":320.91501841129599,\"AnchorPoint_X\":342.43217501772648,\"OutPoint_Y\":514.06781336097845},\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":48}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[47,48]}","argumentGID":0}],"methodSignature":"setSubpaths:","targetGID":46},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":46}],"methodSignature":"setSuperpath:","targetGID":48},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":48},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":48},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setShadow:","targetGID":48},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":46}],"methodSignature":"setSuperpath:","targetGID":48},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":48},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setFill:","targetGID":48},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setShadow:","targetGID":48},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":48}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":46},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":7}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.13822251558303833,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":48}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -2.1531569701203352, 25.991399082341331]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[48]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":308.00218177519929,\"AnchorPoint_Y\":532.86800447564667,\"inPoint_Y\":504.53379546064855,\"OutPoint_X\":307.49158198784681,\"AnchorPoint_X\":307.49158198784681,\"OutPoint_Y\":532.86800447564667},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{2.1531569701203352, -25.991399082341331}\",\"VNPrevPointForRadiusKey\":\"{2.1531569701203352, -25.991399082341331}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":307.54481307650724,\"AnchorPoint_Y\":482.22702540901258,\"inPoint_Y\":482.22702540901258,\"OutPoint_X\":307.54481307650724,\"AnchorPoint_X\":307.54481307650724,\"OutPoint_Y\":482.22702540901258},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-15.584592952684432, -52.321534677537727}\",\"VNPrevPointForRadiusKey\":\"{-15.584592952684432, -52.321534677537727}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":344.58533198784681,\"AnchorPoint_Y\":482.18050447564667,\"inPoint_Y\":482.18050447564667,\"OutPoint_X\":323.06817538141632,\"AnchorPoint_X\":344.58533198784681,\"OutPoint_Y\":488.07641427863712},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{2.1531569701203352, -25.991399082341331}\",\"VNPrevPointForRadiusKey\":\"{2.1531569701203352, -25.991399082341331}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":48}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[48]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[48]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[48]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[48]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 0.34003453224391933]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[48]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 0.1954377384541317]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[48]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":54}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -20, -20]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[54]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[-1, -0, 0, 1, 692.07691397569363, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[54]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"KeepNodesAfterTransformTransformOptionKey\":true}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 57.024957634971202, 19.792602829855639]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[54]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":307.04977456552314,\"AnchorPoint_Y\":532.53992937509292,\"inPoint_Y\":504.20572036009486,\"OutPoint_X\":307.56037435287561,\"AnchorPoint_X\":307.56037435287561,\"OutPoint_Y\":532.53992937509292},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{612.89879937060209, -26.319474182895021}\",\"VNPrevPointForRadiusKey\":\"{612.89879937060209, -26.319474182895021}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":307.50714326421519,\"AnchorPoint_Y\":481.89895030845889,\"inPoint_Y\":481.89895030845889,\"OutPoint_X\":307.50714326421519,\"AnchorPoint_X\":307.50714326421519,\"OutPoint_Y\":481.89895030845889},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{630.6365492934068, -52.649609778091417}\",\"VNPrevPointForRadiusKey\":\"{630.6365492934068, -52.649609778091417}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":270.46662435287561,\"AnchorPoint_Y\":481.85242937509298,\"inPoint_Y\":481.85242937509298,\"OutPoint_X\":291.68464211968808,\"AnchorPoint_X\":270.46662435287561,\"OutPoint_Y\":491.96348695323337},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{615.05195634072243, -0.32807510055368994}\",\"VNPrevPointForRadiusKey\":\"{615.05195634072243, -0.32807510055368994}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":54}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":55}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":54},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":8}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":48},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":6}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setElements:","targetGID":55},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":48},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":54}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1.0330248439984473, -0, -0, 0.99999999999999989, -11.379876833051835, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[55]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":56}],"methodSignature":"removeObject:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":55},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":7}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":21},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":3}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":30},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":2}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":32},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":3}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":27},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":29},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":31},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setElements:","targetGID":56},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":27},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":29},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":30},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":21},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":31},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":32},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":0}],"methodSignature":"setGroup:","targetGID":55}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":57}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -290.115478515625, 62.302734375]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.042222879700741525,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.89467432944640213,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.042222879700741525,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.90097236067321962,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.031593452065677957,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.91739226661025597,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.019517801575741518,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.93616537665754818,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.0091232041181144082,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.94328004740819738,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0.0018620895127118644,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.95006183205443251,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.95531556443573817,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9587083584477003,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.96669617604784863,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.96977202191904677,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.97283138648692502,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.97731792325435829,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9818318081185089,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9863455118694362,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.99094089768406901,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.99553628349870182,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":1,\"WDHueKey\":0.5806878306878307,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":0.10000000149011612}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"$class\":\"StrokeStyle\",\"WDColorKey\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":0,\"$class\":\"Vectornator.Color\"},\"WDEndArrowKey\":\"\",\"WDCapKey\":0,\"WDStartArrowKey\":\"\",\"WDJoinKey\":1,\"WDWeightKey\":0.10000000149011612}}","argumentGID":0}],"methodSignature":"setStrokeStyle:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -138.240478515625, -358.288818359375]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":56},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":56}],"methodSignature":"removeObject:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 10.2197265625, 7.887451171875]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1.73681640625, -1.3956298828125]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":0,\"WDHueKey\":0,\"WDAlphaKey\":1,\"WDBrightnessKey\":1,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.5806878306878307,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.5806878306878307,\"WDAlphaKey\":1,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.96954748896640086,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.96362693977790437,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.95841712343394081,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.954131593465262,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.93934857275056949,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.83357840083997725,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.80253639308086555,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.76741239856207288,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.7565612097807517,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.73508462058656032,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.69529915290432798,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.68285743522209563,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.66214385499715267,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.64970324957289294,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.63305608449601369,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.62061547907175396,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.60169597095671978,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.59555519468963558,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.5870419721668565,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.57588046341116172,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.56733832218109337,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.5457271497722096,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.51220146996013671,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.45939702270785876,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.41278562784738043,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.38011749893223234,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.35006206399487472,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.33336373505125283,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.31040673049544421,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.29577720138097952,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.27499243664578588,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.25639993237471526,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.24782776017938496,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.23669294561503418,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.22722206897779043,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.22344372864464693,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.21991675861332574,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.21874110193621868,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.2197966347522779,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.23113499252562641,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.24180488325740318,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.2529452591116173,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.28146132901480636,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.29042946504840544,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.29303437322038722,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.30319262528473806,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.31050016016514809,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.31284924900341687,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.31402379342255127,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.31192273811218679,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.31087054207004555,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.30975717183940776,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"WDSaturationKey\":1,\"WDHueKey\":0.58068782091140747,\"WDAlphaKey\":0.3074892778331435,\"WDBrightnessKey\":0.9882352941176471,\"$class\":\"Vectornator.Color\"}}","argumentGID":0}],"methodSignature":"setFill:","targetGID":57}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1.0000066161593433, -0, -0, 1.0000066161593433, -0.00084115369224984499, -0.0015278888873493538]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -0, 1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 106.25150911801597, 215.10157288799655]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {1024, 1024}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {1018.1834573966, 1022.316502396953}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {1008.5644489681886, 1015.9218488910001}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {1003.3383988868811, 1012.188740268123}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {959.56182404754463, 968.37466824766148}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {930.99873639690077, 938.04865943448704}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {883.88375480634159, 898.86742705675647}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {851.39277451811745, 876.13749149913815}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {807.42735481491377, 847.80421878677203}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {780.8131139470363, 833.66058405713125}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {758.18676120653936, 823.6793880913026}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {745.42543866806363, 820.26800024929958}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {738.45307691385142, 820.26800024929958}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {738.07015268029545, 820.26800024929958}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {738.07015268029545, 820.65313908147255}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {735.7456294412641, 820.1359296408807}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {734.20024226104533, 819.105537302637}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {730.82720724054207, 815.73250228213374}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {722.51078545309133, 807.41391622785272}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {715.22999050970566, 800.13291995732015}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {710.02528110597814, 794.3195482564854}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {708.99086222479468, 792.76802059828378}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {705.03881032957679, 786.83843280185465}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {703.71729893680003, 785.04591654871797}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {702.86608775936838, 784.19455437592615}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {701.16326275021129, 782.49188036212945}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {700.77188277648224, 782.10024872946656}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {699.97704320020512, 781.30550981676288}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {699.12865060283116, 780.87985389626033}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {698.70304501411556, 780.45414764397106}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {695.54100084364723, 776.8204946317021}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {693.70127337454369, 774.98056583545167}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {691.86235121402819, 772.67134345959084}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {689.97672115541332, 769.84279770809485}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {689.03491276184081, 768.90119064166925}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {686.67817717929256, 766.07264489017348}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {683.90469440249581, 763.76603976722345}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {682.53607245732564, 762.86897633206718}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {681.17026909221318, 762.39756881741346}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {678.95083862390538, 761.54620664462163}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {676.30580256688177, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {675.07408308165805, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {673.28025820206608, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {671.48603066818009, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {670.54341696601955, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {669.17600298373122, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {667.39103649860635, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {666.44842279644581, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {666.02281720772999, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {665.226367014277, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {663.57830298909857, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.7291050831368, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.92701772956843, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.54691207607016, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.15190821369538, 761.07479912996791}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.15190821369538, 760.69429082217562}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.15190821369538, 760.30406847954168}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.15190821369538, 759.91817467056762}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.15190821369538, 759.52795232793369}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.15190821369538, 759.12605301077474}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.15190821369538, 758.73814593033103}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.15190821369538, 758.35446671997374}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.55134127330371, 758.35446671997374}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.55134127330371, 757.96107347477482}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.55134127330371, 757.57593464260208}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.97694686201953, 757.57593464260208}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.3896675133285, 757.18274272455005}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.3896675133285, 756.35493582795493}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 755.94518475206382}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 755.5517915068649}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 754.78614436689986}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 754.3960730196261}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 754.00237778370661}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 753.60918586565458}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 753.21065877820774}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 752.81701387407497}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 752.41430924832821}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 749.69714774089903}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 748.93285955917622}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 747.99004452986878}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 747.13868235707696}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.79916693028599, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.41906127678772, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.65522608114543, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.27149653900142, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {660.88132452815421, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {660.48793128295529, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {659.69429966956, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {659.30453031300658, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {659.68503862079888, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {660.0695734715307, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {660.47263041978476, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {660.86843959074758, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.25498771294906, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {661.63670398362319, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.02566803158879, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {662.83158060094979, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {664.08383545516563, 745.8865784982213}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {665.7862578100287, 746.7378903392264}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {667.06307457617595, 747.16359659151567}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {669.70851328749336, 748.01490843252077}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {670.98533005364084, 748.44061468481004}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {672.66399580516031, 748.84437627807847}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {673.94081257130756, 749.26907589463281}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {675.19387273411121, 749.69478214692208}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {675.58525270784025, 749.69478214692208}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {675.20474440004818, 749.69478214692208}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {673.93235683113426, 749.26907589463281}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {672.1381292972485, 747.52296554887494}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {670.87500277709569, 747.09725929658589}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {670.47194582884163, 746.2471050866759}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {669.64368594616599, 745.41864387685314}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {669.24787677520317, 745.02308636482417}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {669.24787677520317, 744.6337699943515}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":287.2857717617394,\"AnchorPoint_Y\":289.25600703483042,\"inPoint_Y\":289.25600703483042,\"OutPoint_X\":287.2857717617394,\"AnchorPoint_X\":287.2857717617394,\"OutPoint_Y\":289.25600703483042},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{59.351577569932488, -881.2368652940595}\",\"VNPrevPointForRadiusKey\":\"{59.351577569932488, -881.2368652940595}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":437.28477645924727,\"AnchorPoint_Y\":289.25600703483042,\"inPoint_Y\":289.25600703483042,\"OutPoint_X\":437.28477645924727,\"AnchorPoint_X\":437.28477645924727,\"OutPoint_Y\":289.25600703483042},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{59.351577569932488, -881.2368652940595}\",\"VNPrevPointForRadiusKey\":\"{59.351577569932488, -881.2368652940595}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":437.28477645924727,\"AnchorPoint_Y\":539.25435579077384,\"inPoint_Y\":539.25435579077384,\"OutPoint_X\":437.28477645924727,\"AnchorPoint_X\":437.28477645924727,\"OutPoint_Y\":539.25435579077384},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{59.351577569932488, -881.2368652940595}\",\"VNPrevPointForRadiusKey\":\"{59.351577569932488, -881.2368652940595}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":287.2857717617394,\"AnchorPoint_Y\":539.25435579077384,\"inPoint_Y\":539.25435579077384,\"OutPoint_X\":287.2857717617394,\"AnchorPoint_X\":287.2857717617394,\"OutPoint_Y\":539.25435579077384},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{59.351577569932488, -881.2368652940595}\",\"VNPrevPointForRadiusKey\":\"{59.351577569932488, -881.2368652940595}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":22},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":287.2857717617394,\"AnchorPoint_Y\":289.25602785985438,\"inPoint_Y\":330.67709661876142,\"OutPoint_X\":287.2857717617394,\"AnchorPoint_X\":287.2857717617394,\"OutPoint_Y\":247.83495910094825},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-173.55249370630804, -465.50590420624349}\",\"VNPrevPointForRadiusKey\":\"{-173.55249370630804, -465.50590420624349}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":320.86422761918971,\"AnchorPoint_Y\":214.25650324349817,\"inPoint_Y\":214.25650324349817,\"OutPoint_X\":403.70636513700288,\"AnchorPoint_X\":362.28529637809629,\"OutPoint_Y\":214.25650324349817},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-173.55249370630804, -465.50590420624349}\",\"VNPrevPointForRadiusKey\":\"{-173.55249370630804, -465.50590420624349}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":437.28477402925387,\"AnchorPoint_Y\":289.25602785985438,\"inPoint_Y\":247.83495910094825,\"OutPoint_X\":437.28477402925387,\"AnchorPoint_X\":437.28477402925387,\"OutPoint_Y\":330.67709661876142},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-173.55249370630804, -465.50590420624349}\",\"VNPrevPointForRadiusKey\":\"{-173.55249370630804, -465.50590420624349}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":403.70636513700288,\"AnchorPoint_Y\":364.25550551101242,\"inPoint_Y\":364.25550551101242,\"OutPoint_X\":320.86422761918971,\"AnchorPoint_X\":362.28529637809629,\"OutPoint_Y\":364.25550551101242},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-173.55249370630804, -465.50590420624349}\",\"VNPrevPointForRadiusKey\":\"{-173.55249370630804, -465.50590420624349}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":24},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":287.2857717617394,\"AnchorPoint_Y\":539.25437383096278,\"inPoint_Y\":580.67544258986891,\"OutPoint_X\":287.2857717617394,\"AnchorPoint_X\":287.2857717617394,\"OutPoint_Y\":497.83330507205574},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-173.55249370630804, -215.50755823513555}\",\"VNPrevPointForRadiusKey\":\"{-173.55249370630804, -215.50755823513555}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":320.86422761918971,\"AnchorPoint_Y\":464.25484921460566,\"inPoint_Y\":464.25484921460566,\"OutPoint_X\":403.70636513700288,\"AnchorPoint_X\":362.28529637809629,\"OutPoint_Y\":464.25484921460566},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-173.55249370630804, -215.50755823513555}\",\"VNPrevPointForRadiusKey\":\"{-173.55249370630804, -215.50755823513555}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":437.28477402925387,\"AnchorPoint_Y\":539.25437383096278,\"inPoint_Y\":497.83330507205574,\"OutPoint_X\":437.28477402925387,\"AnchorPoint_X\":437.28477402925387,\"OutPoint_Y\":580.67544258986891},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-173.55249370630804, -215.50755823513555}\",\"VNPrevPointForRadiusKey\":\"{-173.55249370630804, -215.50755823513555}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":403.70636513700288,\"AnchorPoint_Y\":614.2538514821199,\"inPoint_Y\":614.2538514821199,\"OutPoint_X\":320.86422761918971,\"AnchorPoint_X\":362.28529637809629,\"OutPoint_Y\":614.2538514821199},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{-173.55249370630804, -215.50755823513555}\",\"VNPrevPointForRadiusKey\":\"{-173.55249370630804, -215.50755823513555}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":25},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":297.2857056005837,\"AnchorPoint_Y\":289.07321462503751,\"inPoint_Y\":289.07321462503751,\"OutPoint_X\":297.2857056005837,\"AnchorPoint_X\":297.2857056005837,\"OutPoint_Y\":289.07321462503751},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{99.742735489559436, -881.41965770385173}\",\"VNPrevPointForRadiusKey\":\"{99.742735489559436, -881.41965770385173}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":427.28484419717574,\"AnchorPoint_Y\":289.07321462503751,\"inPoint_Y\":289.07321462503751,\"OutPoint_X\":427.28484419717574,\"AnchorPoint_X\":427.28484419717574,\"OutPoint_Y\":289.07321462503751},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{99.742735489559436, -881.41965770385173}\",\"VNPrevPointForRadiusKey\":\"{99.742735489559436, -881.41965770385173}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":427.28484419717574,\"AnchorPoint_Y\":539.07156338098093,\"inPoint_Y\":539.07156338098093,\"OutPoint_X\":427.28484419717574,\"AnchorPoint_X\":427.28484419717574,\"OutPoint_Y\":539.07156338098093},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{99.742735489559436, -881.41965770385173}\",\"VNPrevPointForRadiusKey\":\"{99.742735489559436, -881.41965770385173}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":297.2857056005837,\"AnchorPoint_Y\":539.07156338098093,\"inPoint_Y\":539.07156338098093,\"OutPoint_X\":297.2857056005837,\"AnchorPoint_X\":297.2857056005837,\"OutPoint_Y\":539.07156338098093},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{99.742735489559436, -881.41965770385173}\",\"VNPrevPointForRadiusKey\":\"{99.742735489559436, -881.41965770385173}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":26},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":97.57351080287799,\"AnchorPoint_Y\":309.16721333385931,\"inPoint_Y\":309.16721333385931,\"OutPoint_X\":97.57351080287799,\"AnchorPoint_X\":97.57351080287799,\"OutPoint_Y\":309.16721333385931},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{78.505876495528582, 146.69454513811797}\",\"VNPrevPointForRadiusKey\":\"{78.505876495528582, 146.69454513811797}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":221.36797043606543,\"AnchorPoint_Y\":443.90288627635027,\"inPoint_Y\":443.90288627635027,\"OutPoint_X\":221.36797043606543,\"AnchorPoint_X\":221.36797043606543,\"OutPoint_Y\":443.90288627635027},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{78.505876495528582, 146.69454513811797}\",\"VNPrevPointForRadiusKey\":\"{78.505876495528582, 146.69454513811797}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":361.08361414401247,\"AnchorPoint_Y\":309.6035637643763,\"inPoint_Y\":309.6035637643763,\"OutPoint_X\":361.08361414401247,\"AnchorPoint_X\":361.08361414401247,\"OutPoint_Y\":309.6035637643763},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{78.505876495528582, 146.69454513811797}\",\"VNPrevPointForRadiusKey\":\"{78.505876495528582, 146.69454513811797}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":29},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":377.82365759764593,\"AnchorPoint_Y\":307.07623767819223,\"inPoint_Y\":307.07623767819223,\"OutPoint_X\":377.82365759764593,\"AnchorPoint_X\":377.82365759764593,\"OutPoint_Y\":307.07623767819223},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{358.49705139509047, 146.65290478922907}\",\"VNPrevPointForRadiusKey\":\"{358.49705139509047, 146.65290478922907}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":503.2994629767461,\"AnchorPoint_Y\":440.11243356580462,\"inPoint_Y\":440.11243356580462,\"OutPoint_X\":503.2994629767461,\"AnchorPoint_X\":503.2994629767461,\"OutPoint_Y\":440.11243356580462},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{358.49705139509047, 146.65290478922907}\",\"VNPrevPointForRadiusKey\":\"{358.49705139509047, 146.65290478922907}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":644.91269001662772,\"AnchorPoint_Y\":307.50708423933065,\"inPoint_Y\":307.50708423933065,\"OutPoint_X\":644.91269001662772,\"AnchorPoint_X\":644.91269001662772,\"OutPoint_Y\":307.50708423933065},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{358.49705139509047, 146.65290478922907}\",\"VNPrevPointForRadiusKey\":\"{358.49705139509047, 146.65290478922907}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":30},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":516.45695350382334,\"AnchorPoint_Y\":154.80813254227905,\"inPoint_Y\":154.80813254227905,\"OutPoint_X\":516.45695350382334,\"AnchorPoint_X\":516.45695350382334,\"OutPoint_Y\":154.80813254227905},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"VNPrevPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":223.97172601151692,\"AnchorPoint_Y\":448.88181029919406,\"inPoint_Y\":448.88181029919406,\"OutPoint_X\":223.97172601151692,\"AnchorPoint_X\":223.97172601151692,\"OutPoint_Y\":448.88181029919406},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"VNPrevPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":78.252109034647219,\"AnchorPoint_Y\":300.1041732651538,\"inPoint_Y\":300.1041732651538,\"OutPoint_X\":78.252109034647219,\"AnchorPoint_X\":78.252109034647219,\"OutPoint_Y\":300.1041732651538},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"VNPrevPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":648.07861404640448,\"AnchorPoint_Y\":302.9355554780409,\"inPoint_Y\":302.9355554780409,\"OutPoint_X\":648.07861404640448,\"AnchorPoint_X\":648.07861404640448,\"OutPoint_Y\":302.9355554780409},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"VNPrevPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":506.17105679039673,\"AnchorPoint_Y\":444.7847840681311,\"inPoint_Y\":444.7847840681311,\"OutPoint_X\":506.17105679039673,\"AnchorPoint_X\":506.17105679039673,\"OutPoint_Y\":444.7847840681311},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"VNPrevPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":217.51314851045686,\"AnchorPoint_Y\":156.11368443580113,\"inPoint_Y\":156.11368443580113,\"OutPoint_X\":217.51314851045686,\"AnchorPoint_X\":217.51314851045686,\"OutPoint_Y\":156.11368443580113},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"VNPrevPointForRadiusKey\":\"{1033.732755685123, -115.61701281386331}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":21},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":290.26965144695237,\"AnchorPoint_Y\":418.73751961026619,\"inPoint_Y\":418.73751961026619,\"OutPoint_X\":290.26965144695237,\"AnchorPoint_X\":290.26965144695237,\"OutPoint_Y\":418.73751961026619},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{78.505876495528582, 146.69454513811797}\",\"VNPrevPointForRadiusKey\":\"{78.505876495528582, 146.69454513811797}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":433.31216208615911,\"AnchorPoint_Y\":418.73751961026619,\"inPoint_Y\":418.73751961026619,\"OutPoint_X\":433.31216208615911,\"AnchorPoint_X\":433.31216208615911,\"OutPoint_Y\":418.73751961026619},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{78.505876495528582, 146.69454513811797}\",\"VNPrevPointForRadiusKey\":\"{78.505876495528582, 146.69454513811797}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":31},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":290.26965144695237,\"AnchorPoint_Y\":521.73683815036247,\"inPoint_Y\":521.73683815036247,\"OutPoint_X\":290.26965144695237,\"AnchorPoint_X\":290.26965144695237,\"OutPoint_Y\":521.73683815036247},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{78.505876495528582, 249.69386367821471}\",\"VNPrevPointForRadiusKey\":\"{78.505876495528582, 249.69386367821471}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":433.31216208615911,\"AnchorPoint_Y\":521.73683815036247,\"inPoint_Y\":521.73683815036247,\"OutPoint_X\":433.31216208615911,\"AnchorPoint_X\":433.31216208615911,\"OutPoint_Y\":521.73683815036247},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{78.505876495528582, 249.69386367821471}\",\"VNPrevPointForRadiusKey\":\"{78.505876495528582, 249.69386367821471}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":32},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":362.67570746371234,\"AnchorPoint_Y\":667.02363476289884,\"inPoint_Y\":638.68961321030247,\"OutPoint_X\":362.18143434711533,\"AnchorPoint_X\":362.18143434711533,\"OutPoint_Y\":667.02363476289884},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{66.606350517013652, 108.16792868331186}\",\"VNPrevPointForRadiusKey\":\"{66.606350517013652, 108.16792868331186}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":362.23296334642896,\"AnchorPoint_Y\":616.38299074283532,\"inPoint_Y\":616.38299074283532,\"OutPoint_X\":362.23296334642896,\"AnchorPoint_X\":362.23296334642896,\"OutPoint_Y\":616.38299074283532},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{49.435773574115046, 81.837967291336099}\",\"VNPrevPointForRadiusKey\":\"{49.435773574115046, 81.837967291336099}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":398.08909406988482,\"AnchorPoint_Y\":616.33647011725679,\"inPoint_Y\":616.33647011725679,\"OutPoint_X\":377.54953250055337,\"AnchorPoint_X\":398.08909406988482,\"OutPoint_Y\":626.44746079947163},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{64.522041763846801, 134.1591558035534}\",\"VNPrevPointForRadiusKey\":\"{64.522041763846801, 134.1591558035534}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":48},{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":[{\"NodePoints\":{\"inPoint_X\":361.7537539269963,\"AnchorPoint_Y\":667.02363476289884,\"inPoint_Y\":638.68961321030247,\"OutPoint_X\":362.24802704359331,\"AnchorPoint_X\":362.24802704359331,\"OutPoint_Y\":667.02363476289884},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{657.82311087369476, 108.16792868331186}\",\"VNPrevPointForRadiusKey\":\"{657.82311087369476, 108.16792868331186}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":362.19649804427968,\"AnchorPoint_Y\":616.38299074283532,\"inPoint_Y\":616.38299074283532,\"OutPoint_X\":362.19649804427968,\"AnchorPoint_X\":362.19649804427968,\"OutPoint_Y\":616.38299074283532},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{674.99368781659336, 81.837967291336099}\",\"VNPrevPointForRadiusKey\":\"{674.99368781659336, 81.837967291336099}\",\"$class\":\"WDBezierNode\"},{\"NodePoints\":{\"inPoint_X\":326.34036732082382,\"AnchorPoint_Y\":616.33647011725679,\"inPoint_Y\":616.33647011725679,\"OutPoint_X\":346.87992889015482,\"AnchorPoint_X\":326.34036732082382,\"OutPoint_Y\":626.44746079947163},\"NodeTypeOverride\":0,\"VNNextPointForRadiusKey\":\"{659.90741962686184, 134.1591558035534}\",\"VNPrevPointForRadiusKey\":\"{659.90741962686184, 134.1591558035534}\",\"$class\":\"WDBezierNode\"}]}","argumentGID":0}],"methodSignature":"setNodes:","targetGID":54}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, -1, 0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 1, -0]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[57,56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":\"Mac App icon\"}","argumentGID":0}],"methodSignature":"title","targetGID":3}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":4,"argumentGID":57},{"argumentTypeRawValue":1,"argumentEncodedJsonString":"{\"Argument\":0}","argumentGID":0}],"methodSignature":"insertObject:atIndex:","targetGID":4}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 51.701018329683393, 116.18082292683278]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {740, 744.6337699943515}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {736.35253901673036, 740.35218585345365}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {726.62032261171248, 732.07890244371663}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {706.29497285008392, 715.91557216276851}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {691.48129050959142, 702.64569380957585}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {676.51521301916046, 688.7168285032127}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {672.9666932377902, 683.82682021816697}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {667.51490451806353, 675.17444423582754}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {659.05376131978483, 661.73639796317264}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {652.798107458392, 653.69283161018325}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {646.24074717873964, 645.29847372318682}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {639.28068335784974, 635.32950515648088}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {636.62597526532704, 630.02008897143548}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {635.24953610331818, 624.67241338007113}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 622.6021329778589}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 621.53096597223009}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 620.05992231684661}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 617.99269367248519}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 616.51850188795038}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 616.24683119169595}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 615.70516023506332}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 615.17627453763725}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 614.91966988803563}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 614.41860337270202}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 614.17012603611374}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.94885764559763, 613.92274090759827}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {635.49213479057289, 613.12141354351331}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {635.76223142225149, 612.8513490356014}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {636.57611917917507, 612.03746127867794}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {637.11913933401547, 611.49444112383765}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {637.39077790650299, 610.95251317707016}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {637.39077790650299, 609.88031821090215}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {637.39077790650299, 609.63566360256891}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {637.39077790650299, 609.14548704419769}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {637.39077790650299, 605.7312446082625}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {636.5843428634887, 599.34404392161696}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {636.5843428634887, 593.09060660013677}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {636.5843428634887, 589.48773340525565}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {636.5843428634887, 588.67320317299516}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {636.5843428634887, 588.41560268662124}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {636.5843428634887, 588.17014498411686}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {636.5843428634887, 587.37090566487711}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {636.5843428634887, 585.89687449917653}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {637.75570389792438, 583.82575887902624}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {638.35654683309554, 582.08108497761054}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {638.89982397807057, 581.53800057523642}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {638.89982397807057, 581.26632987898211}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {638.89982397807057, 580.49002692927365}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {638.89982397807057, 579.01760195191559}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {638.89982397807057, 577.54289618711107}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {638.89982397807057, 576.39787664149299}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {638.89982397807057, 574.98324232069888}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {637.49819978285086, 574.14796013505304}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {636.68559697660157, 573.33526095750301}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {635.54147689645515, 572.22030925765694}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {634.72784612966643, 571.9486064376357}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {633.91472934314743, 571.67857405349071}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {632.17002331796471, 571.67857405349071}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {630.76017543843136, 571.67857405349071}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {630.21689829345632, 571.67857405349071}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {629.95939417838304, 571.67857405349071}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {629.16606563224377, 571.67857405349071}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {627.41930368598264, 572.55198715038807}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {626.22070169725794, 573.15125602098351}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {624.65229090455068, 574.07519980313668}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {622.58506226018949, 575.54563310695005}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {622.31316669756711, 575.81730380320448}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {621.51598329940589, 576.35817166566585}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {621.75935295706495, 576.11476988423976}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {622.01171726944222, 576.11476988423976}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {622.79887805234603, 575.59915130252296}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {623.61379376980881, 574.78423558506029}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {625.35849979499153, 574.21346049566114}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {626.4990220132504, 573.64088647531833}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {627.02790771067657, 573.36995462570155}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {627.02790771067657, 573.12317984875619}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {627.02790771067657, 572.87576259647392}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {627.02790771067657, 572.62320554149551}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {627.02790771067657, 572.10858279655099}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {627.02790771067657, 571.85628273170744}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {626.48437357556668, 571.58695707043307}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {625.96371156245436, 571.06635930485447}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {625.71160424021173, 571.06635930485447}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {625.46360876012614, 571.06635930485447}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {625.21124444774887, 570.81418773507824}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {624.70805776380325, 570.81418773507824}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {624.18174196772497, 570.28806468160133}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {623.92449484278654, 570.01636186157998}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {623.11163504640217, 569.74465904155875}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {622.57632459560614, 569.20931646699569}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {622.31933446080234, 569.20931646699569}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3},{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {622.04743889817996, 568.93899496894903}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {622.04743889817996, 568.68393226015633}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {621.80252729971198, 568.68393226015633}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {621.80252729971198, 570.51884182265508}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"dynamicProxy","methodArguments":[{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"NS.special\":3,\"NS.rectval\":\"{{0, 0}, {600, 570.51884182265508}}\",\"$class\":\"NSValue\"}}","argumentGID":0}],"methodSignature":"frame","targetGID":3}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 15.687962779094846, 1.6061883343354566e-05]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":1,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}],[{"actionTypeRawValue":"proxy","methodArguments":[{"argumentTypeRawValue":6,"argumentEncodedJsonString":"{\"Argument\":\"[1, -0, -0, 1, 0, -1]\"}","argumentGID":0},{"argumentTypeRawValue":5,"argumentEncodedJsonString":"{\"Argument\":[56]}","argumentGID":0},{"argumentTypeRawValue":4,"argumentEncodedJsonString":"{\"Argument\":{\"UpdateAngleAfterTransformTransformOptionKey\":0,\"TransformingNodesTransformOptionKey\":false}}","argumentGID":0}],"methodSignature":"applyTransform:forElements:options:","targetGID":0}]],"isGroupingUndo":false,"currentUndoGroup":[],"currentRedoGroup":[],"isGroupingRedo":false,"redoStack":[],"nextGID":57} \ No newline at end of file diff --git a/docs/images/logo_framed.png b/docs/images/logo_framed.png new file mode 100644 index 0000000000000000000000000000000000000000..d6704da4f20aa98150f9e558ad0e4e1912402b44 GIT binary patch literal 49038 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 literal 0 HcmV?d00001 diff --git a/docs/images/logo_framed.svg b/docs/images/logo_framed.svg new file mode 100644 index 0000000..70ef7a9 --- /dev/null +++ b/docs/images/logo_framed.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/logo_framed.vectornator/Artboard0.json b/docs/images/logo_framed.vectornator/Artboard0.json new file mode 100644 index 0000000..7a365f7 --- /dev/null +++ b/docs/images/logo_framed.vectornator/Artboard0.json @@ -0,0 +1 @@ +{"layers":[{"elements":[{"elementDescription":"(polygon)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[368.75294547403814,729.44134214300266],"reflectionModeOverride":0,"anchorPoint":[368.75294547403814,729.44134214300266],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[368.75294547403814,729.44134214300266],"nextPoint":[221.86289110820167,-279.40236718212691]},{"outPoint":[58.884741476317231,550.53885452571922],"reflectionModeOverride":0,"anchorPoint":[58.884741476317231,550.53885452571922],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[58.884741476317231,550.53885452571922],"nextPoint":[221.86289110820167,-279.40236718212691]},{"outPoint":[58.884771993693448,192.73377248033557],"reflectionModeOverride":0,"anchorPoint":[58.884771993693448,192.73377248033557],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[58.884771993693448,192.73377248033557],"nextPoint":[221.86289110820167,-279.40236718212691]},{"outPoint":[368.75296538097359,13.831330639116459],"reflectionModeOverride":0,"anchorPoint":[368.75296538097359,13.831330639116459],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[368.75296538097359,13.831330639116459],"nextPoint":[221.86289110820167,-279.40236718212691]},{"outPoint":[678.62124178681802,192.73392506721666],"reflectionModeOverride":0,"anchorPoint":[678.62124178681802,192.73392506721666],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[678.62124178681802,192.73392506721666],"nextPoint":[221.86289110820167,-279.40236718212691]},{"outPoint":[678.62118075206558,550.53885452571922],"reflectionModeOverride":0,"anchorPoint":[678.62118075206558,550.53885452571922],"cornerRadius":0,"prevPoint":[221.86289110820167,-279.40236718212691],"inPoint":[678.62118075206558,550.53885452571922],"nextPoint":[221.86289110820167,-279.40236718212691]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":0.3074892778331435},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":18.646402359008789,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[368.75296111419152,729.44134214300266],"opacity":1,"blur":0,"isLocked":false,"gid":57,"smootheningRate":0,"initialPoint":[368.75296111419152,371.63633639105956],"creationPoints":[],"name":"(polygon)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[61.506041898417834,134.69462453150436],"opacity":1,"blur":0,"isLocked":false,"gid":56,"smootheningRate":0,"initialPoint":[61.506041898417834,134.69462453150436],"creationPoints":[],"group":{"elements":[{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[292.87340185278117,214.25650324349817],"opacity":1,"blur":0,"isLocked":false,"gid":27,"smootheningRate":0,"initialPoint":[292.87340185278117,214.25650324349817],"creationPoints":[],"group":{"elements":[{"elementDescription":"(rectangle)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[292.87340185278117,289.25600703483042],"reflectionModeOverride":0,"anchorPoint":[292.87340185278117,289.25600703483042],"cornerRadius":0,"prevPoint":[64.939207660974262,-881.2368652940595],"inPoint":[292.87340185278117,289.25600703483042],"nextPoint":[64.939207660974262,-881.2368652940595]},{"outPoint":[442.87240655028904,289.25600703483042],"reflectionModeOverride":0,"anchorPoint":[442.87240655028904,289.25600703483042],"cornerRadius":0,"prevPoint":[64.939207660974262,-881.2368652940595],"inPoint":[442.87240655028904,289.25600703483042],"nextPoint":[64.939207660974262,-881.2368652940595]},{"outPoint":[442.87240655028904,539.25435579077384],"reflectionModeOverride":0,"anchorPoint":[442.87240655028904,539.25435579077384],"cornerRadius":0,"prevPoint":[64.939207660974262,-881.2368652940595],"inPoint":[442.87240655028904,539.25435579077384],"nextPoint":[64.939207660974262,-881.2368652940595]},{"outPoint":[292.87340185278117,539.25435579077384],"reflectionModeOverride":0,"anchorPoint":[292.87340185278117,539.25435579077384],"cornerRadius":0,"prevPoint":[64.939207660974262,-881.2368652940595],"inPoint":[292.87340185278117,539.25435579077384],"nextPoint":[64.939207660974262,-881.2368652940595]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.13822251558303833,"a":1},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":20,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[442.87240655028904,539.25435579077384],"opacity":1,"blur":0,"isLocked":false,"gid":22,"smootheningRate":0,"initialPoint":[292.87340185278117,289.25600703483042],"creationPoints":[],"name":"(rectangle)"},{"elementDescription":"(oval)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[292.87340185278117,247.83495910094825],"reflectionModeOverride":0,"anchorPoint":[292.87340185278117,289.25602785985438],"cornerRadius":0,"prevPoint":[-167.96486361526627,-465.50590420624349],"inPoint":[292.87340185278117,330.67709661876142],"nextPoint":[-167.96486361526627,-465.50590420624349]},{"outPoint":[409.29399522804465,214.25650324349817],"reflectionModeOverride":0,"anchorPoint":[367.87292646913806,214.25650324349817],"cornerRadius":0,"prevPoint":[-167.96486361526627,-465.50590420624349],"inPoint":[326.45185771023148,214.25650324349817],"nextPoint":[-167.96486361526627,-465.50590420624349]},{"outPoint":[442.87240412029564,330.67709661876142],"reflectionModeOverride":0,"anchorPoint":[442.87240412029564,289.25602785985438],"cornerRadius":0,"prevPoint":[-167.96486361526627,-465.50590420624349],"inPoint":[442.87240412029564,247.83495910094825],"nextPoint":[-167.96486361526627,-465.50590420624349]},{"outPoint":[326.45185771023148,364.25550551101242],"reflectionModeOverride":0,"anchorPoint":[367.87292646913806,364.25550551101242],"cornerRadius":0,"prevPoint":[-167.96486361526627,-465.50590420624349],"inPoint":[409.29399522804465,364.25550551101242],"nextPoint":[-167.96486361526627,-465.50590420624349]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.13822251558303833,"a":1},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":20,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[-167.96486361526627,-465.50590420624349],"opacity":1,"blur":0,"isLocked":false,"gid":24,"smootheningRate":0,"initialPoint":[-167.96486361526627,-465.50590420624349],"creationPoints":[],"name":"(oval)"},{"elementDescription":"(oval)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[292.87340185278117,497.83330507205574],"reflectionModeOverride":0,"anchorPoint":[292.87340185278117,539.25437383096278],"cornerRadius":0,"prevPoint":[-167.96486361526627,-215.50755823513555],"inPoint":[292.87340185278117,580.67544258986891],"nextPoint":[-167.96486361526627,-215.50755823513555]},{"outPoint":[409.29399522804465,464.25484921460566],"reflectionModeOverride":0,"anchorPoint":[367.87292646913806,464.25484921460566],"cornerRadius":0,"prevPoint":[-167.96486361526627,-215.50755823513555],"inPoint":[326.45185771023148,464.25484921460566],"nextPoint":[-167.96486361526627,-215.50755823513555]},{"outPoint":[442.87240412029564,580.67544258986891],"reflectionModeOverride":0,"anchorPoint":[442.87240412029564,539.25437383096278],"cornerRadius":0,"prevPoint":[-167.96486361526627,-215.50755823513555],"inPoint":[442.87240412029564,497.83330507205574],"nextPoint":[-167.96486361526627,-215.50755823513555]},{"outPoint":[326.45185771023148,614.2538514821199],"reflectionModeOverride":0,"anchorPoint":[367.87292646913806,614.2538514821199],"cornerRadius":0,"prevPoint":[-167.96486361526627,-215.50755823513555],"inPoint":[409.29399522804465,614.2538514821199],"nextPoint":[-167.96486361526627,-215.50755823513555]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.13822251558303833,"a":1},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":20,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[-167.96486361526627,-215.50755823513555],"opacity":1,"blur":0,"isLocked":false,"gid":25,"smootheningRate":0,"initialPoint":[-167.96486361526627,-215.50755823513555],"creationPoints":[],"name":"(oval)"},{"elementDescription":"(rectangle)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[302.87333569162547,289.07321462503751],"reflectionModeOverride":0,"anchorPoint":[302.87333569162547,289.07321462503751],"cornerRadius":0,"prevPoint":[105.33036558060121,-881.41965770385173],"inPoint":[302.87333569162547,289.07321462503751],"nextPoint":[105.33036558060121,-881.41965770385173]},{"outPoint":[432.87247428821752,289.07321462503751],"reflectionModeOverride":0,"anchorPoint":[432.87247428821752,289.07321462503751],"cornerRadius":0,"prevPoint":[105.33036558060121,-881.41965770385173],"inPoint":[432.87247428821752,289.07321462503751],"nextPoint":[105.33036558060121,-881.41965770385173]},{"outPoint":[432.87247428821752,539.07156338098093],"reflectionModeOverride":0,"anchorPoint":[432.87247428821752,539.07156338098093],"cornerRadius":0,"prevPoint":[105.33036558060121,-881.41965770385173],"inPoint":[432.87247428821752,539.07156338098093],"nextPoint":[105.33036558060121,-881.41965770385173]},{"outPoint":[302.87333569162547,539.07156338098093],"reflectionModeOverride":0,"anchorPoint":[302.87333569162547,539.07156338098093],"cornerRadius":0,"prevPoint":[105.33036558060121,-881.41965770385173],"inPoint":[302.87333569162547,539.07156338098093],"nextPoint":[105.33036558060121,-881.41965770385173]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.13822251558303833,"a":1},"strokeStyle":{"color":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[432.87247428821752,539.07156338098093],"opacity":1,"blur":0,"isLocked":false,"gid":26,"smootheningRate":0,"initialPoint":[302.87333569162547,289.07321462503751],"creationPoints":[],"name":"(rectangle)"}]},"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[103.16114089391976,309.16721333385931],"reflectionModeOverride":0,"anchorPoint":[103.16114089391976,309.16721333385931],"cornerRadius":0,"prevPoint":[84.093506586570356,146.69454513811797],"inPoint":[103.16114089391976,309.16721333385931],"nextPoint":[84.093506586570356,146.69454513811797]},{"outPoint":[226.95560052710721,443.90288627635027],"reflectionModeOverride":0,"anchorPoint":[226.95560052710721,443.90288627635027],"cornerRadius":0,"prevPoint":[84.093506586570356,146.69454513811797],"inPoint":[226.95560052710721,443.90288627635027],"nextPoint":[84.093506586570356,146.69454513811797]},{"outPoint":[366.67124423505425,309.6035637643763],"reflectionModeOverride":0,"anchorPoint":[366.67124423505425,309.6035637643763],"cornerRadius":0,"prevPoint":[84.093506586570356,146.69454513811797],"inPoint":[366.67124423505425,309.6035637643763],"nextPoint":[84.093506586570356,146.69454513811797]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[84.093506586570356,146.69454513811797],"opacity":1,"blur":0,"isLocked":false,"gid":29,"smootheningRate":0,"initialPoint":[84.093506586570356,146.69454513811797],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[383.41128768868771,307.07623767819223],"reflectionModeOverride":0,"anchorPoint":[383.41128768868771,307.07623767819223],"cornerRadius":0,"prevPoint":[364.08468148613224,146.65290478922907],"inPoint":[383.41128768868771,307.07623767819223],"nextPoint":[364.08468148613224,146.65290478922907]},{"outPoint":[508.88709306778787,440.11243356580462],"reflectionModeOverride":0,"anchorPoint":[508.88709306778787,440.11243356580462],"cornerRadius":0,"prevPoint":[364.08468148613224,146.65290478922907],"inPoint":[508.88709306778787,440.11243356580462],"nextPoint":[364.08468148613224,146.65290478922907]},{"outPoint":[650.50032010766949,307.50708423933065],"reflectionModeOverride":0,"anchorPoint":[650.50032010766949,307.50708423933065],"cornerRadius":0,"prevPoint":[364.08468148613224,146.65290478922907],"inPoint":[650.50032010766949,307.50708423933065],"nextPoint":[364.08468148613224,146.65290478922907]}],"closed":true,"reversed":false}},"fillColor":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[364.08468148613224,146.65290478922907],"opacity":1,"blur":0,"isLocked":false,"gid":30,"smootheningRate":0,"initialPoint":[364.08468148613224,146.65290478922907],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[522.04458359486512,154.80813254227905],"reflectionModeOverride":0,"anchorPoint":[522.04458359486512,154.80813254227905],"cornerRadius":0,"prevPoint":[1039.3203857761648,-115.61701281386331],"inPoint":[522.04458359486512,154.80813254227905],"nextPoint":[1039.3203857761648,-115.61701281386331]},{"outPoint":[229.5593561025587,448.88181029919406],"reflectionModeOverride":0,"anchorPoint":[229.5593561025587,448.88181029919406],"cornerRadius":0,"prevPoint":[1039.3203857761648,-115.61701281386331],"inPoint":[229.5593561025587,448.88181029919406],"nextPoint":[1039.3203857761648,-115.61701281386331]},{"outPoint":[83.839739125688993,300.1041732651538],"reflectionModeOverride":0,"anchorPoint":[83.839739125688993,300.1041732651538],"cornerRadius":0,"prevPoint":[1039.3203857761648,-115.61701281386331],"inPoint":[83.839739125688993,300.1041732651538],"nextPoint":[1039.3203857761648,-115.61701281386331]},{"outPoint":[653.66624413744626,302.9355554780409],"reflectionModeOverride":0,"anchorPoint":[653.66624413744626,302.9355554780409],"cornerRadius":0,"prevPoint":[1039.3203857761648,-115.61701281386331],"inPoint":[653.66624413744626,302.9355554780409],"nextPoint":[1039.3203857761648,-115.61701281386331]},{"outPoint":[511.75868688143851,444.7847840681311],"reflectionModeOverride":0,"anchorPoint":[511.75868688143851,444.7847840681311],"cornerRadius":0,"prevPoint":[1039.3203857761648,-115.61701281386331],"inPoint":[511.75868688143851,444.7847840681311],"nextPoint":[1039.3203857761648,-115.61701281386331]},{"outPoint":[223.10077860149863,156.11368443580113],"reflectionModeOverride":0,"anchorPoint":[223.10077860149863,156.11368443580113],"cornerRadius":0,"prevPoint":[1039.3203857761648,-115.61701281386331],"inPoint":[223.10077860149863,156.11368443580113],"nextPoint":[1039.3203857761648,-115.61701281386331]}],"closed":false,"reversed":false}},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":20,"endArrow":"","startArrow":"","cap":1},"maskedElements":[]},"angle":90,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[1200.0148931967549,-113.13323396127316],"opacity":1,"blur":0,"isLocked":false,"gid":21,"smootheningRate":0,"initialPoint":[1200.0148931967549,-113.13323396127316],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(line)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[295.85728153799414,418.73751961026619],"reflectionModeOverride":0,"anchorPoint":[295.85728153799414,418.73751961026619],"cornerRadius":0,"prevPoint":[84.093506586570356,146.69454513811797],"inPoint":[295.85728153799414,418.73751961026619],"nextPoint":[84.093506586570356,146.69454513811797]},{"outPoint":[438.89979217720088,418.73751961026619],"reflectionModeOverride":0,"anchorPoint":[438.89979217720088,418.73751961026619],"cornerRadius":0,"prevPoint":[84.093506586570356,146.69454513811797],"inPoint":[438.89979217720088,418.73751961026619],"nextPoint":[84.093506586570356,146.69454513811797]}],"closed":false,"reversed":false}},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":61.869819641113281,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[84.093506586570356,146.69454513811797],"opacity":1,"blur":0,"isLocked":false,"gid":31,"smootheningRate":0,"initialPoint":[84.093506586570356,146.69454513811797],"creationPoints":[],"name":"(line)"},{"elementDescription":"(line)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[295.85728153799414,521.73683815036247],"reflectionModeOverride":0,"anchorPoint":[295.85728153799414,521.73683815036247],"cornerRadius":0,"prevPoint":[84.093506586570356,249.69386367821471],"inPoint":[295.85728153799414,521.73683815036247],"nextPoint":[84.093506586570356,249.69386367821471]},{"outPoint":[438.89979217720088,521.73683815036247],"reflectionModeOverride":0,"anchorPoint":[438.89979217720088,521.73683815036247],"cornerRadius":0,"prevPoint":[84.093506586570356,249.69386367821471],"inPoint":[438.89979217720088,521.73683815036247],"nextPoint":[84.093506586570356,249.69386367821471]}],"closed":false,"reversed":false}},"strokeStyle":{"color":{"b":0,"s":0,"h":0,"a":1},"dashPattern":[],"join":1,"width":61.869819641113281,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[84.093506586570356,249.69386367821471],"opacity":1,"blur":0,"isLocked":false,"gid":32,"smootheningRate":0,"initialPoint":[84.093506586570356,249.69386367821471],"creationPoints":[],"name":"(line)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[70.109671854888575,134.69462453150436],"opacity":1,"blur":0,"isLocked":false,"gid":55,"smootheningRate":0,"initialPoint":[70.109671854888575,134.69462453150436],"creationPoints":[],"group":{"elements":[{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[367.7690644381571,667.02363476289884],"reflectionModeOverride":0,"anchorPoint":[367.7690644381571,667.02363476289884],"cornerRadius":0,"prevPoint":[72.193980608055426,108.16792868331186],"inPoint":[368.26333755475412,638.68961321030247],"nextPoint":[72.193980608055426,108.16792868331186]},{"outPoint":[367.82059343747073,616.38299074283532],"reflectionModeOverride":0,"anchorPoint":[367.82059343747073,616.38299074283532],"cornerRadius":0,"prevPoint":[55.02340366515682,81.837967291336099],"inPoint":[367.82059343747073,616.38299074283532],"nextPoint":[55.02340366515682,81.837967291336099]},{"outPoint":[383.13716259159514,626.44746079947163],"reflectionModeOverride":0,"anchorPoint":[403.6767241609266,616.33647011725679],"cornerRadius":0,"prevPoint":[70.109671854888575,134.1591558035534],"inPoint":[403.6767241609266,616.33647011725679],"nextPoint":[70.109671854888575,134.1591558035534]}],"closed":true,"reversed":false}},"fillColor":{"b":0,"s":0,"h":0,"a":1},"strokeStyle":{"color":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[72.357622652088821,121.79167358010636],"opacity":1,"blur":0,"isLocked":false,"gid":48,"smootheningRate":0,"initialPoint":[72.357622652088821,121.79167358010636],"creationPoints":[],"name":"(curve)"},{"elementDescription":"(curve)","category":0,"blendMode":0,"creationViewScale":0,"isFreeHandCurve":false,"styleable":{"abstractPath":{"fillRule":0,"pathData":{"nodes":[{"outPoint":[367.83565713463508,667.02363476289884],"reflectionModeOverride":0,"anchorPoint":[367.83565713463508,667.02363476289884],"cornerRadius":0,"prevPoint":[663.41074096473653,108.16792868331186],"inPoint":[367.34138401803807,638.68961321030247],"nextPoint":[663.41074096473653,108.16792868331186]},{"outPoint":[367.78412813532145,616.38299074283532],"reflectionModeOverride":0,"anchorPoint":[367.78412813532145,616.38299074283532],"cornerRadius":0,"prevPoint":[680.58131790763514,81.837967291336099],"inPoint":[367.78412813532145,616.38299074283532],"nextPoint":[680.58131790763514,81.837967291336099]},{"outPoint":[352.46755898119659,626.44746079947163],"reflectionModeOverride":0,"anchorPoint":[331.92799741186559,616.33647011725679],"cornerRadius":0,"prevPoint":[665.49504971790361,134.1591558035534],"inPoint":[331.92799741186559,616.33647011725679],"nextPoint":[665.49504971790361,134.1591558035534]}],"closed":true,"reversed":false}},"fillColor":{"b":0,"s":0,"h":0,"a":1},"strokeStyle":{"color":{"b":0.9882352941176471,"s":1,"h":0.5806878306878307,"a":1},"dashPattern":[],"join":1,"width":0.10000000149011612,"endArrow":"","startArrow":"","cap":0},"maskedElements":[]},"angle":0,"smootheningRateRaw":0,"isHidden":false,"endPointFIX":[663.24709892070291,121.99906937808692],"opacity":1,"blur":0,"isLocked":false,"gid":54,"smootheningRate":0,"initialPoint":[663.24709892070291,121.99906937808692],"creationPoints":[],"name":"(curve)"}]},"name":"(curve)"}]},"name":"(curve)"}],"isExpanded":false,"isLocked":false,"isVisible":true,"opacity":1,"gid":4,"name":"Layer 1"}],"frame":{"y":0,"x":0,"width":745,"height":744.6337699943515},"title":"Mac App icon","activeLayerIndex":0,"settings":{"gridSpacing":20,"gridAngle":45,"backgroundColor":{"b":1,"s":0,"h":0,"a":1},"gridMode":0,"isGridVisible":false},"guideLayer":{"isExpanded":false,"elements":[],"isLocked":false,"defaultName":"Guides","isVisible":true,"opacity":1,"name":"Guides","gid":5},"gid":3} \ No newline at end of file diff --git a/docs/images/logo_framed.vectornator/Document.json b/docs/images/logo_framed.vectornator/Document.json new file mode 100644 index 0000000..974df2c --- /dev/null +++ b/docs/images/logo_framed.vectornator/Document.json @@ -0,0 +1 @@ +{"date":644900741.09290397,"appVersion":"4.1.5","drawing":{"modificationDate":644894800.328192,"activeArtboardIndex":0,"settings":{"outlineMode":false,"isolateActiveLayer":false,"snapToEdges":false,"snapToPoints":false,"guidesVisible":true,"snapToGrid":false,"units":"Pixels","dimensionsVisible":true,"dynamicGuides":false,"isCMYKColorPreviewEnabled":false,"undoHistoryDisabled":false,"snapToGuides":true,"drawOnlyUsingPencil":false,"whiteBackground":false,"rulersVisible":true,"isTimeLapseWatermarkDisabled":false},"artboardPaths":["Artboard0.json"],"documentVersion":"unknown"}} \ No newline at end of file diff --git a/docs/images/logo_framed.vectornator/Manifest.json b/docs/images/logo_framed.vectornator/Manifest.json new file mode 100644 index 0000000..0f80b78 --- /dev/null +++ b/docs/images/logo_framed.vectornator/Manifest.json @@ -0,0 +1 @@ +{"documentJSONFilename":"Document.json","undoHistoryJSONFilename":"UndoHistory.json","fileFormatVersion":0,"thumbnailImageFilename":"Thumbnail.png"} \ No newline at end of file diff --git a/docs/images/logo_framed.vectornator/Thumbnail.png b/docs/images/logo_framed.vectornator/Thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..e3eb759a22a5dce97d1954a4f00c114cd0374d2c GIT binary patch literal 59364 zcmZU51yoeq`!+B%$RN@n9TJjCC>@eYOG!71fYi_(0+P~F0s$yi|Q8cMX>k7X=07nu5HHItmJm4til@g5OLyU|NGW zR3~*gDU^~v>fhi4!a_&E^5H`icJMPc3OXt!3I_BQ@Df9%`tRolsJBpHSHDL?K?$`% zLI3AFkH9_o{p=yXrtt5q1s@O(QPILibC=+1|J~r9ub^aO{ka95AqI^I!q3R}oP><)Npo8$eEwG= zN8vvY7SixTnNy}O$%jUvqw24e#5LOn7jJ)B+7QzI@`kk9>y?hKE-VaO;^s1wHiyM8 zBzWv?((nF^*^PwGueOM;Nd+1@Pb}Ku)^t~vWFR3SArklAI`aA{|E*it;DKE^a?#4U zDDdDv^Q?O8hw}Dg>!dg|_hvfW4~7ktzlZbPaB0dVnHU(*z)(L*M#X*0{qvLt@RXHb zurqIHiTwI-sf8R+n3WPWuYs@UdL8cwdF&PR$+cx(O+giP41B$NO8OZsbax3xjEkdr zxbyEJjF&{rFQ~-Uc^EdyevcH9l^E2$Q!DzU|7YEKKBJ)%<{Z*ZTunu_uz9{Kn%ATi zJt!y$fiJR-VtR>>hu2nRGmPB(t&vbXzw~F7ja@OY=`6QQXs#Yc7lM{1U?=tCgYI1z z{J!5$aYjbQa=VFY^4l}wf7ZKs6_-%r!zKz1H1$N)nWOFb0?op>(dIS+x`TeoyS6jE zDg228saOhsjA5HY0_CLU_11vn%IyOK}a^bop7jJm&7e}+kCB!zq;XUg+RnkW=j5fSR$<^HY+GVa{(QLoImxhVD*J5uA2)!w> zp80zyukF275priy3Ga{-o(|pH_&FHZ(xws3O|Zf&S`w4XY6YtF!+uv6Fe|>EIj3jY z=YmIES>nGwf~h{!8jNQ>QC)1)aG$V%7QbSy(hDT~vDr6l5-5)E2uC$gfx$4T z&a?z#k>9l?MuWj_!)RTxad2>2Y8~g1D}#(U9sv*knZWl9ely-m~$G04{G6487wj#)puZiLIcB)Tp}%>3vY5$mJKsRqyall`^w@AH@H z(C7>N*1=`wTlCe@;5lD!As2dXD8CW9o}HcTNc!u50>$S7#;g#}(vf83^AZ@3ZYQgV z>eY3Fz(&X$u*Q7AC*^DjO-9lIPGPI%Z;SF3O|@(Fd#*N373|4jv^4WDIfb;C#7k=C(F ztGrDofh$`vb^-hO*bOBbS`!1}TK8R+lYRjpVr|Q)MLTk%Kjka-s;7<%cMtu1b5FLZRw9ED|69q+m938YDAj8@lH zPRoS%3@zcUMP4jSOw)z#*jnGqM)y};j8_wqFF`?-*ʳuMmqfyQmr*dE8M6vOh6 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