├── .gitattributes ├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── examples └── get_single_frame.py ├── pyphantom ├── __init__.py ├── cli │ ├── __init__.py │ └── pfs_cam.py ├── discover.py ├── fakecam │ ├── __init__.py │ ├── fakecam-ph7.py │ ├── fakecam.png │ ├── fakecam.py │ ├── fakecam.spec │ ├── fakecam_data.py │ ├── takes-ph7 │ │ ├── 0.data │ │ └── 0.raw │ ├── takes │ │ ├── 0.data │ │ ├── 0.raw │ │ ├── 1.data │ │ ├── 1.raw │ │ ├── 2.data │ │ ├── 2.raw │ │ ├── 3.data │ │ └── 3.raw │ └── ximg_send.py ├── flex.py ├── frame.py ├── network.py ├── structures.py └── utils.py ├── pyproject.toml ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── test_fakecam.py └── test_flex.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb linguist-documentation 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | .cache 4 | .coverage 5 | .DS_Store 6 | .eggs 7 | .idea 8 | .ipynb_checkpoints 9 | build 10 | build_debug 11 | dist 12 | venv 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | pyphantom = {editable = true, path = "."} 8 | 9 | [dev-packages] 10 | "pytest" = "*" 11 | "pcapy" = "*" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | 16 | [scripts] 17 | test = "py.test -v" 18 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7656605695a281bbbfeee97d12a1a9b3fec6313a99b2d03baf5db39af91b7b72" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "cached-property": { 20 | "hashes": [ 21 | "sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f", 22 | "sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504" 23 | ], 24 | "version": "==1.5.1" 25 | }, 26 | "click": { 27 | "hashes": [ 28 | "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", 29 | "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" 30 | ], 31 | "version": "==7.1.1" 32 | }, 33 | "colorama": { 34 | "hashes": [ 35 | "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", 36 | "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" 37 | ], 38 | "version": "==0.4.3" 39 | }, 40 | "netifaces": { 41 | "hashes": [ 42 | "sha256:078986caf4d6a602a4257d3686afe4544ea74362b8928e9f4389b5cd262bc215", 43 | "sha256:0c4304c6d5b33fbd9b20fdc369f3a2fef1a8bbacfb6fd05b9708db01333e9e7b", 44 | "sha256:2dee9ffdd16292878336a58d04a20f0ffe95555465fee7c9bd23b3490ef2abf3", 45 | "sha256:3095218b66d359092b82f07c5422293c2f6559cf8d36b96b379cc4cdc26eeffa", 46 | "sha256:30ed89ab8aff715caf9a9d827aa69cd02ad9f6b1896fd3fb4beb998466ed9a3c", 47 | "sha256:4921ed406386246b84465950d15a4f63480c1458b0979c272364054b29d73084", 48 | "sha256:563a1a366ee0fb3d96caab79b7ac7abd2c0a0577b157cc5a40301373a0501f89", 49 | "sha256:5b3167f923f67924b356c1338eb9ba275b2ba8d64c7c2c47cf5b5db49d574994", 50 | "sha256:6d84e50ec28e5d766c9911dce945412dc5b1ce760757c224c71e1a9759fa80c2", 51 | "sha256:755050799b5d5aedb1396046f270abfc4befca9ccba3074f3dbbb3cb34f13aae", 52 | "sha256:75d3a4ec5035db7478520ac547f7c176e9fd438269e795819b67223c486e5cbe", 53 | "sha256:7a25a8e28281504f0e23e181d7a9ed699c72f061ca6bdfcd96c423c2a89e75fc", 54 | "sha256:7cc6fd1eca65be588f001005446a47981cbe0b2909f5be8feafef3bf351a4e24", 55 | "sha256:86b8a140e891bb23c8b9cb1804f1475eb13eea3dbbebef01fcbbf10fbafbee42", 56 | "sha256:ad10acab2ef691eb29a1cc52c3be5ad1423700e993cc035066049fa72999d0dc", 57 | "sha256:b2ff3a0a4f991d2da5376efd3365064a43909877e9fabfa801df970771161d29", 58 | "sha256:b47e8f9ff6846756be3dc3fb242ca8e86752cd35a08e06d54ffc2e2a2aca70ea", 59 | "sha256:da298241d87bcf468aa0f0705ba14572ad296f24c4fda5055d6988701d6fd8e1", 60 | "sha256:db881478f1170c6dd524175ba1c83b99d3a6f992a35eca756de0ddc4690a1940", 61 | "sha256:f0427755c68571df37dc58835e53a4307884a48dec76f3c01e33eb0d4a3a81d7", 62 | "sha256:f8885cc48c8c7ad51f36c175e462840f163cb4687eeb6c6d7dfaf7197308e36b", 63 | "sha256:f911b7f0083d445c8d24cfa5b42ad4996e33250400492080f5018a28c026db2b" 64 | ], 65 | "version": "==0.10.9" 66 | }, 67 | "psutil": { 68 | "hashes": [ 69 | "sha256:1413f4158eb50e110777c4f15d7c759521703bd6beb58926f1d562da40180058", 70 | "sha256:298af2f14b635c3c7118fd9183843f4e73e681bb6f01e12284d4d70d48a60953", 71 | "sha256:60b86f327c198561f101a92be1995f9ae0399736b6eced8f24af41ec64fb88d4", 72 | "sha256:685ec16ca14d079455892f25bd124df26ff9137664af445563c1bd36629b5e0e", 73 | "sha256:73f35ab66c6c7a9ce82ba44b1e9b1050be2a80cd4dcc3352cc108656b115c74f", 74 | "sha256:75e22717d4dbc7ca529ec5063000b2b294fc9a367f9c9ede1f65846c7955fd38", 75 | "sha256:a02f4ac50d4a23253b68233b07e7cdb567bd025b982d5cf0ee78296990c22d9e", 76 | "sha256:d008ddc00c6906ec80040d26dc2d3e3962109e40ad07fd8a12d0284ce5e0e4f8", 77 | "sha256:d84029b190c8a66a946e28b4d3934d2ca1528ec94764b180f7d6ea57b0e75e26", 78 | "sha256:e2d0c5b07c6fe5a87fa27b7855017edb0d52ee73b71e6ee368fae268605cc3f5", 79 | "sha256:f344ca230dd8e8d5eee16827596f1c22ec0876127c28e800d7ae20ed44c4b310" 80 | ], 81 | "version": "==5.7.0" 82 | }, 83 | "pyphantom": { 84 | "editable": true, 85 | "path": "." 86 | }, 87 | "pyyaml": { 88 | "hashes": [ 89 | "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", 90 | "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", 91 | "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", 92 | "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", 93 | "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", 94 | "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", 95 | "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", 96 | "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", 97 | "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", 98 | "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", 99 | "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" 100 | ], 101 | "version": "==5.3.1" 102 | } 103 | }, 104 | "develop": { 105 | "attrs": { 106 | "hashes": [ 107 | "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", 108 | "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72" 109 | ], 110 | "version": "==19.3.0" 111 | }, 112 | "importlib-metadata": { 113 | "hashes": [ 114 | "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f", 115 | "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e" 116 | ], 117 | "markers": "python_version < '3.8'", 118 | "version": "==1.6.0" 119 | }, 120 | "more-itertools": { 121 | "hashes": [ 122 | "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c", 123 | "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507" 124 | ], 125 | "version": "==8.2.0" 126 | }, 127 | "packaging": { 128 | "hashes": [ 129 | "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3", 130 | "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752" 131 | ], 132 | "version": "==20.3" 133 | }, 134 | "pcapy": { 135 | "hashes": [ 136 | "sha256:aa239913678d7ba116e66057a37f914de7726aecd11d00db470127df115c4e78" 137 | ], 138 | "index": "pypi", 139 | "version": "==0.11.4" 140 | }, 141 | "pluggy": { 142 | "hashes": [ 143 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 144 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 145 | ], 146 | "version": "==0.13.1" 147 | }, 148 | "py": { 149 | "hashes": [ 150 | "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", 151 | "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" 152 | ], 153 | "version": "==1.8.1" 154 | }, 155 | "pyparsing": { 156 | "hashes": [ 157 | "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", 158 | "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" 159 | ], 160 | "version": "==2.4.6" 161 | }, 162 | "pytest": { 163 | "hashes": [ 164 | "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172", 165 | "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970" 166 | ], 167 | "index": "pypi", 168 | "version": "==5.4.1" 169 | }, 170 | "six": { 171 | "hashes": [ 172 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 173 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 174 | ], 175 | "version": "==1.14.0" 176 | }, 177 | "wcwidth": { 178 | "hashes": [ 179 | "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", 180 | "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1" 181 | ], 182 | "version": "==0.1.9" 183 | }, 184 | "zipp": { 185 | "hashes": [ 186 | "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", 187 | "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96" 188 | ], 189 | "version": "==3.1.0" 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyphantom 2 | 3 | [![PyPI version](https://badge.fury.io/py/pyphantom.svg)](https://pypi.org/project/pyphantom) 4 | [![GitHub license](https://img.shields.io/github/license/OTTOMATIC-IO/pycine.svg)](https://github.com/OTTOMATIC-IO/pycine/blob/master/LICENSE) 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) 6 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/24b61435a9ca4b3ea56d1b02fb80e731)](https://app.codacy.com/app/OTTOMATIC/pyphantom?utm_source=github.com&utm_medium=referral&utm_content=OTTOMATIC-IO/pyphantom&utm_campaign=Badge_Grade_Dashboard) 7 | 8 | 9 | Communicate with Vision Research Phantom® cameras with python 10 | 11 | 12 | ## Installation 13 | 14 | ### Release Version 15 | 16 | ``` 17 | pip install pyphantom 18 | ``` 19 | 20 | 21 | ### Development version 22 | 23 | ``` 24 | pip install git+https://github.com/ottomatic-io/pyphantom.git 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /examples/get_single_frame.py: -------------------------------------------------------------------------------- 1 | from pyphantom.discover import discover 2 | from pyphantom.flex import Phantom 3 | from pyphantom.frame import get_frames 4 | from pyphantom.network import get_networks 5 | 6 | 7 | def get_single_frame(take_number: int, frame_number: int): 8 | networks = get_networks() 9 | cameras = discover(networks) 10 | c = cameras[0] 11 | 12 | camera = Phantom(c.ip, c.port, c.protocol) 13 | camera.connect() 14 | 15 | frame_index = int(camera.takes[take_number]["firstfr"]) + frame_number 16 | frame_data = get_frames(camera=camera, take=take_number, frame_index=frame_index, from_ram=False, count=1)[0] 17 | 18 | # TODO: do something useful with the frame data 19 | 20 | 21 | if __name__ == "__main__": 22 | get_single_frame(take_number=0, frame_number=0) 23 | -------------------------------------------------------------------------------- /pyphantom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomatic-io/pyphantom/54978decce8c998d69a2fb4150094b1d8c7862a8/pyphantom/__init__.py -------------------------------------------------------------------------------- /pyphantom/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomatic-io/pyphantom/54978decce8c998d69a2fb4150094b1d8c7862a8/pyphantom/cli/__init__.py -------------------------------------------------------------------------------- /pyphantom/cli/pfs_cam.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | import click 5 | from pyphantom.flex import Phantom 6 | 7 | 8 | @click.group() 9 | @click.version_option() 10 | def cli(): 11 | pass 12 | 13 | 14 | @cli.command() 15 | def info(): 16 | from pyphantom.discover import discover 17 | from pyphantom.network import get_networks 18 | 19 | networks = get_networks() 20 | cameras = discover(networks) 21 | 22 | try: 23 | c = cameras[0] 24 | except IndexError: 25 | click.secho("No camera found", fg="red") 26 | sys.exit() 27 | 28 | with Phantom(c.ip, c.port, c.protocol) as cam: 29 | cam.connect() 30 | cam_info = cam.structures.info 31 | click.secho("Connected to a {} at {}".format(cam_info.model, c.ip), fg="green") 32 | for key in dir(cam_info): 33 | click.echo(key + ": ", nl=False) 34 | click.secho(str(getattr(cam_info, key)), bold=False, fg="yellow") 35 | 36 | 37 | if __name__ == "__main__": 38 | cli() 39 | -------------------------------------------------------------------------------- /pyphantom/discover.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | import socket 3 | import time 4 | import logging 5 | from collections import namedtuple 6 | from threading import Thread 7 | 8 | from pyphantom.flex import Phantom 9 | from pyphantom.network import get_networks 10 | 11 | logger = logging.getLogger() 12 | 13 | CameraInfo = namedtuple("CameraInfo", ["ip", "port", "protocol", "hardware_version", "serial", "name"]) 14 | 15 | 16 | def discover(networks): 17 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 18 | s.bind(("", 0)) 19 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 20 | s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 21 | s.setblocking(False) 22 | 23 | cameras = list() 24 | 25 | for _, ipv4 in networks.items(): 26 | try: 27 | s.sendto(b"phantom?", (ipv4["broadcast"], 7380)) 28 | logger.debug("Sent discovery packet to {}".format(ipv4["broadcast"])) 29 | except socket.error as e: 30 | logger.warning("Could not send discovery packet to {}: {}".format(ipv4["broadcast"], e)) 31 | 32 | time.sleep(0.6) 33 | 34 | while True: 35 | try: 36 | data, address = s.recvfrom(1024) 37 | try: 38 | protocol, port, hardware_version, serial, name = shlex.split( 39 | data.decode("ascii", errors="ignore").rstrip("\0") 40 | ) 41 | name = name.strip('"') 42 | except ValueError: 43 | # PH7 44 | protocol, port = shlex.split(data.decode("ascii", errors="ignore")) 45 | hardware_version = "" 46 | serial = "" 47 | name = "" 48 | 49 | cameras.append(CameraInfo(address[0], port, protocol, hardware_version, serial, name)) 50 | 51 | except socket.error: 52 | break 53 | 54 | return cameras 55 | 56 | 57 | class Cameras(Thread): 58 | """ 59 | Keeps an updated list of Cameras / CineStations and keeps them connected 60 | """ 61 | 62 | def __init__(self, daemon=True): 63 | super(Cameras, self).__init__() 64 | 65 | self.daemon = daemon 66 | 67 | self.networks = [] 68 | self.cameras = {} 69 | 70 | self.start() 71 | 72 | def run(self): 73 | while True: 74 | networks = get_networks() 75 | if networks != self.networks: 76 | logger.info("New network config: %s", networks) 77 | self.networks = networks 78 | 79 | cameras = discover(self.networks) 80 | logger.debug("Discovered %d cameras", len(cameras)) 81 | 82 | for camera_info in cameras.copy(): 83 | if camera_info not in self.cameras: 84 | logger.info("Connecting to %s", camera_info) 85 | camera = Phantom(camera_info.ip, camera_info.port, camera_info.protocol) 86 | try: 87 | camera.connect() 88 | self.cameras[camera_info] = camera 89 | except Exception as e: 90 | logger.warning(e) 91 | 92 | for camera_info, camera in self.cameras.copy().items(): 93 | try: 94 | _ = camera.mag_state 95 | time.sleep(0.4) 96 | except Exception as e: 97 | logger.error("Connection dead. %s", e) 98 | camera.disconnect() 99 | del self.cameras[camera_info] 100 | 101 | logger.debug("Got %d connected cameras", len(self.cameras)) 102 | 103 | time.sleep(0.1) 104 | 105 | def __len__(self): 106 | return len(self.cameras) 107 | -------------------------------------------------------------------------------- /pyphantom/fakecam/__init__.py: -------------------------------------------------------------------------------- 1 | from .fakecam import run, get, state, load_takes, logger 2 | -------------------------------------------------------------------------------- /pyphantom/fakecam/fakecam-ph7.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # FIXME: Merge with PH16 fakecam 3 | 4 | import glob 5 | import logging 6 | import os 7 | import socket 8 | import sys 9 | import time 10 | from threading import Thread 11 | 12 | import yaml 13 | 14 | from .fakecam_data import state, answers 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | camthread = None 19 | 20 | 21 | def get(keystring): 22 | sub = keystring.split(".") 23 | out = state 24 | for key in sub: 25 | out = out[key] 26 | if isinstance(out, dict): 27 | out = "{}: {}".format(key, out) 28 | return out 29 | 30 | 31 | def threaded(fn): 32 | def wrapper(*args, **kwargs): 33 | t = Thread(target=fn, args=args, kwargs=kwargs) 34 | t.daemon = True 35 | t.start() 36 | 37 | return wrapper 38 | 39 | 40 | @threaded 41 | def send_frame(socket, cine, count=1): 42 | script_path = os.path.dirname(os.path.realpath(sys.argv[0])) 43 | raw_path = os.path.join(script_path, "./takes-ph7/{}.raw".format(cine)) 44 | with open(raw_path, 'rb') as f: 45 | logger.debug("sending {}.raw".format(cine)) 46 | socket.sendall(f.read() * count) 47 | 48 | 49 | @threaded 50 | def save(fsave): 51 | state["fstat"]["in_progress"] = fsave["lastframe"] - fsave["firstframe"] 52 | while state["fstat"]["in_progress"]: 53 | state["fstat"]["in_progress"] -= 1 54 | time.sleep(0.001) 55 | 56 | 57 | @threaded 58 | def ferase(): 59 | state["fstat"]["in_progress"] = 100 60 | 61 | state["mag"]["state"] = 8 62 | logger.info("CineMag erasing") 63 | 64 | while state["fstat"]["in_progress"]: 65 | state["fstat"]["in_progress"] -= 1 66 | time.sleep(0.1) 67 | 68 | state["mag"]["takes"] = 0 69 | 70 | state["mag"]["state"] = 2 71 | logger.info("CineMag initialising") 72 | 73 | time.sleep(1) 74 | 75 | state["mag"]["state"] = 3 76 | logger.info("CineMag scanning") 77 | 78 | time.sleep(1) 79 | 80 | state["mag"]["state"] = 4 81 | logger.info("CineMag ready") 82 | 83 | 84 | def responder(clientsocket, address): 85 | logger.info("connection from {}".format(address)) 86 | while True: 87 | command = clientsocket.recv(1024).decode('ascii') 88 | answer = None 89 | img = "" 90 | if command: 91 | logger.debug("got command: {}".format(command)) 92 | try: 93 | if command == "rec 1\n": 94 | state["c1"]["state"] = {"WTR"} 95 | 96 | elif command == "trig\n": 97 | state["c1"]["state"] = {"RDY"} 98 | 99 | elif command.startswith("get"): 100 | answer = get(command.replace("get ", "").strip()) 101 | 102 | elif command.startswith("img"): 103 | clean = " ".join(command.lstrip("img ").split()).replace("\\", "") 104 | img = yaml.safe_load(clean) 105 | answer = "Ok! {{ cine: {cine}, res: {res}, fmt: P10 }}".format( 106 | cine=img["cine"], res=state["fc{}".format(img["cine"])]["res"] 107 | ) 108 | 109 | elif command.startswith("set"): 110 | option, value = command.strip().lstrip("set ").split(": ") 111 | split = option.split(".") 112 | if len(split) == 2: 113 | key, subkey = split 114 | state[key][subkey] = value 115 | elif len(split) == 3: 116 | key, subkey, subsubkey = split 117 | state[key][subkey][subsubkey] = value 118 | answer = "Ok!" 119 | 120 | elif command.startswith("vplay"): 121 | clean = " ".join(command.lstrip("vplay ").split()).replace("\\", "") 122 | vplay = yaml.safe_load(clean) 123 | try: 124 | for key, value in vplay.items(): 125 | state["video"]["play"][key] = value 126 | except: 127 | logger.warning("vplay: {}".format(vplay)) 128 | answer = "Ok!" 129 | 130 | elif command.startswith("fsave"): 131 | clean = " ".join(command.lstrip("fsave ").split()).replace("\\", "") 132 | fsave = yaml.safe_load(clean) 133 | save(fsave) 134 | answer = "Ok!" 135 | 136 | elif command.startswith("attach"): 137 | command = "attach" 138 | 139 | elif command.startswith("startdata"): 140 | clean = " ".join(command.lstrip("startdata ").split()).replace("\\", "") 141 | startdata = yaml.safe_load(clean) 142 | print(startdata) 143 | data_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 144 | data_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 145 | data_socket.connect((address[0], startdata["port"])) 146 | answer = "Ok!" 147 | 148 | elif command.startswith("ferase"): 149 | ferase() 150 | answer = "Ok!" 151 | 152 | if answer is None: 153 | answer = str(answers[command.strip()]) 154 | 155 | logger.debug(repr(answer)) 156 | 157 | # uncomment to simulate slow connection 158 | # import time; time.sleep(0.6) 159 | 160 | clientsocket.send(answer.encode('ascii') + b"\r\n") 161 | 162 | if img: 163 | send_frame(data_socket, img["cine"], img["cnt"]) 164 | 165 | except KeyError: 166 | logger.error("command not implemented: {}".format(command)) 167 | clientsocket.send(b"command not implemented..\r\n") 168 | raise 169 | 170 | else: 171 | logger.error("connection lost") 172 | break 173 | 174 | 175 | def discover(discoversocket): 176 | while True: 177 | try: 178 | data, addr = discoversocket.recvfrom(1024).decode('ascii') 179 | if data == b"phantom?": 180 | logger.info("hello phantom :P") 181 | discoversocket.sendto(b"PH7 7115", addr) 182 | except socket.error: 183 | pass 184 | 185 | 186 | def delete_takes(): 187 | for key in list(state.keys()): 188 | if key.startswith("fc"): 189 | del state[key] 190 | 191 | 192 | def load_takes(): 193 | delete_takes() 194 | script_path = os.path.dirname(os.path.realpath(sys.argv[0])) 195 | 196 | takes = 0 197 | for yaml_file in glob.glob("{}/takes-ph7/*.data".format(script_path)): 198 | take_index = os.path.splitext(os.path.basename(yaml_file))[0] 199 | 200 | if os.path.exists("{}/takes-ph7/{}.raw".format(script_path, take_index)): 201 | with open(yaml_file) as y: 202 | clean = " ".join(y.read().split()).replace("\\", "") 203 | take_info = yaml.safe_load(clean) 204 | # use first key of take_info because we renumber the takes 205 | state["fc{}".format(take_index)] = take_info[list(take_info.keys())[0]] 206 | logger.info("Take {} loaded".format(take_index)) 207 | takes += 1 208 | 209 | state["mag"]["takes"] = takes 210 | 211 | 212 | @threaded 213 | def run(): 214 | try: 215 | discoversocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 216 | discoversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 217 | discoversocket.bind(("", 7380)) 218 | # discoversocket.setblocking(False) 219 | 220 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 221 | serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 222 | serversocket.bind(("", 7115)) 223 | serversocket.listen(5) 224 | 225 | d = Thread(target=discover, args=(discoversocket,)) 226 | d.daemon = True 227 | d.start() 228 | 229 | while True: 230 | (clientsocket, address) = serversocket.accept() 231 | t = Thread(target=responder, args=(clientsocket, address)) 232 | t.daemon = True 233 | t.start() 234 | 235 | except KeyboardInterrupt: 236 | logger.error("Keyboard Interrupt: stopping server") 237 | 238 | finally: 239 | discoversocket.close() 240 | serversocket.close() 241 | 242 | 243 | load_takes() 244 | 245 | if __name__ == "__main__": 246 | FORMAT = "%(asctime)s %(name)-12s %(levelname)-8s %(message)s" 247 | logging.basicConfig(format=FORMAT, level=logging.INFO) 248 | logger.debug("Starting FakeCam") 249 | 250 | run() 251 | while True: 252 | time.sleep(100) 253 | -------------------------------------------------------------------------------- /pyphantom/fakecam/fakecam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomatic-io/pyphantom/54978decce8c998d69a2fb4150094b1d8c7862a8/pyphantom/fakecam/fakecam.png -------------------------------------------------------------------------------- /pyphantom/fakecam/fakecam.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import glob 3 | import logging 4 | import os 5 | import socket 6 | import sys 7 | import time 8 | from copy import deepcopy 9 | from io import StringIO 10 | from threading import Thread, Lock 11 | 12 | import yaml 13 | 14 | from pyphantom.fakecam import ximg_send 15 | from pyphantom.fakecam.fakecam_data import state, answers 16 | 17 | logger = logging.getLogger(__name__) 18 | FORMAT = "%(asctime)s %(name)-12s %(levelname)-8s %(message)s" 19 | logging.basicConfig(format=FORMAT, level=logging.INFO) 20 | 21 | camthread = None 22 | 23 | takes_path = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), "takes") 24 | 25 | if not os.path.isdir(takes_path): 26 | takes_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "takes") 27 | 28 | data_lock = Lock() 29 | 30 | 31 | def threaded(fn): 32 | def wrapper(*args, **kwargs): 33 | t = Thread(target=fn, args=args, kwargs=kwargs) 34 | t.daemon = True 35 | t.start() 36 | 37 | return wrapper 38 | 39 | 40 | def parse_simple(response): 41 | clean = response.replace(" ", "").split("{")[1].split("}")[0].split(",") 42 | 43 | out = {} 44 | for x in clean: 45 | try: 46 | key, value = x.split(":") 47 | except ValueError: 48 | split = x.split(":") 49 | key, value = split[0], ":".join(split[1:]) 50 | out[key] = value 51 | 52 | return out 53 | 54 | 55 | def phantom_format(key, value, stream=None): 56 | if stream is None: 57 | stream = StringIO() 58 | stream.write("{} : ".format(key)) 59 | if isinstance(value, (int, float)): 60 | stream.write(str(value)) 61 | elif isinstance(value, str): 62 | stream.write('"{}"'.format(value)) 63 | elif isinstance(value, list): 64 | stream.write("{") 65 | for item in value: 66 | stream.write(" {}".format(item)) 67 | stream.write(" }") 68 | elif isinstance(value, dict): 69 | phantom_dictformat(value, stream) 70 | 71 | return stream.getvalue() 72 | 73 | 74 | def phantom_dictformat(mydict, stream): 75 | if not isinstance(mydict, dict): 76 | raise AssertionError() 77 | 78 | stream.write("{ ") 79 | first = True 80 | for key, value in mydict.items(): 81 | if not first: 82 | stream.write(", ") 83 | phantom_format(key, value, stream) 84 | 85 | first = False 86 | stream.write(" }") 87 | 88 | 89 | def get(state, keystring): 90 | if keystring == "*": 91 | return phantom_format("*", "NOT IMPLEMENTED") 92 | sub = keystring.split(".") 93 | out = state 94 | for key in sub: 95 | out = out[key] 96 | 97 | return phantom_format(sub[-1], out) 98 | 99 | 100 | @threaded 101 | def send_frame(socket, cine, count=1): 102 | with data_lock: 103 | if cine == -1: 104 | cine = 0 105 | raw_path = os.path.join(takes_path, f"./{cine}.raw") 106 | with open(raw_path, "rb") as f: 107 | logger.info(f"sending {cine}.raw") 108 | logger.info(f"with size {os.path.getsize(raw_path)}") 109 | socket.sendall(f.read() * count) 110 | logger.info(f"sent {cine}.raw") 111 | 112 | 113 | @threaded 114 | def save(fsave): 115 | state["mag"]["progress"] = fsave["lastframe"] - fsave["firstframe"] 116 | while state["mag"]["progress"]: 117 | state["mag"]["progress"] -= 1 118 | time.sleep(0.001) 119 | 120 | 121 | @threaded 122 | def ferase(): 123 | state["mag"]["progress"] = 100 124 | 125 | state["mag"]["state"] = 8 126 | logger.info("CineMag erasing") 127 | 128 | while state["mag"]["progress"]: 129 | state["mag"]["progress"] -= 1 130 | time.sleep(0.1) 131 | 132 | state["mag"]["takes"] = 0 133 | 134 | state["mag"]["state"] = 2 135 | logger.info("CineMag initialising") 136 | 137 | time.sleep(1) 138 | 139 | state["mag"]["state"] = 3 140 | logger.info("CineMag scanning") 141 | 142 | time.sleep(1) 143 | 144 | state["mag"]["state"] = 4 145 | logger.info("CineMag ready") 146 | 147 | 148 | def responder(clientsocket, address, clientsocket_data, address_data): 149 | ssrc = 0 150 | 151 | logger.info("connection from {}".format(address)) 152 | while True: 153 | command = clientsocket.recv(1024).decode("ascii").strip() 154 | answer = None 155 | img = "" 156 | ximg = "" 157 | if command: 158 | logger.debug("got command: {}".format(command)) 159 | try: 160 | if command == "rec 1": 161 | state["c1"]["state"] = ["WTR"] 162 | 163 | elif command == "trig": 164 | state["c1"]["state"] = ["RDY"] 165 | 166 | elif command.startswith("get"): 167 | keystring = command.replace("get ", "").strip() 168 | answer = get(state, keystring) 169 | 170 | elif command.startswith("img"): 171 | img = parse_simple(command) 172 | answer = "Ok! {{ cine: {cine}, res: {res}, fmt: {fmt} }}".format( 173 | cine=img["cine"], 174 | res=state[f"fc{img['cine']}"]["res"], 175 | fmt=state[f"fc{img['cine']}"]["format"], 176 | ) 177 | 178 | elif command.startswith("ximg"): 179 | ximg = parse_simple(command) 180 | answer = "Ok! {{ cine: {cine}, res: {res}, fmt: {fmt}}, ssrc: {ssrc} }}".format( 181 | cine=ximg["cine"], 182 | res=state["fc{}".format(ximg["cine"])]["res"], 183 | fmt=state[f"fc{img['cine']}"]["format"], 184 | ssrc=ssrc, 185 | ) 186 | ximg["ssrc"] = ssrc 187 | 188 | ssrc += 1 189 | 190 | elif command.startswith("setrtc"): 191 | answer = "Ok!" 192 | 193 | elif command.startswith("set"): 194 | option, value = command.strip().lstrip("set ").split() 195 | try: 196 | value = int(value) 197 | except ValueError: 198 | pass 199 | 200 | split = option.split(".") 201 | if len(split) == 2: 202 | key, subkey = split 203 | state[key][subkey] = value 204 | elif len(split) == 3: 205 | key, subkey, subsubkey = split 206 | state[key][subkey][subsubkey] = value 207 | answer = "Ok!" 208 | 209 | elif command.startswith("vplay"): 210 | clean = " ".join(command.lstrip("vplay ").split()).replace("\\", "") 211 | vplay = yaml.safe_load(clean) 212 | try: 213 | for key, value in vplay.items(): 214 | try: 215 | state["video"]["play"][key] = int(value) 216 | except ValueError: 217 | state["video"]["play"][key] = value 218 | except Exception as e: 219 | logger.warning(e) 220 | logger.warning("vplay: {}".format(vplay)) 221 | answer = "Ok!" 222 | 223 | elif command.startswith("fsave"): 224 | clean = " ".join(command.lstrip("fsave ").split()).replace("\\", "") 225 | fsave = yaml.safe_load(clean) 226 | save(fsave) 227 | answer = "Ok!" 228 | 229 | elif command.startswith("attach"): 230 | command = "attach" 231 | 232 | elif command.startswith("ferase"): 233 | ferase() 234 | answer = "Ok!" 235 | 236 | if answer is None: 237 | answer = str(answers[command.strip()]) 238 | 239 | logger.debug(repr(answer)) 240 | 241 | # uncomment to simulate slow connection 242 | # import time; time.sleep(0.6) 243 | 244 | clientsocket.send(answer.encode("ascii") + b"\r\n") 245 | 246 | if ximg: 247 | ximg_send.send_frame(int(ximg["cine"]), int(ximg["cnt"]), ximg["dest"], ximg["ssrc"]) 248 | if img: 249 | send_frame(clientsocket_data, int(img["cine"]), int(img["cnt"])) 250 | 251 | except KeyError: 252 | logger.error("command not implemented: {}".format(command)) 253 | clientsocket.send(b"command not implemented..\r\n") 254 | raise 255 | 256 | else: 257 | logger.error("connection lost") 258 | break 259 | 260 | 261 | def discover(discoversocket): 262 | while True: 263 | try: 264 | data, addr = discoversocket.recvfrom(1024) 265 | if data == b"phantom?": 266 | logger.debug("hello phantom :P") 267 | discoversocket.sendto(b'PH16 7115 4001 16001 "FAKE CAMERA"\0', addr) 268 | except socket.error: 269 | pass 270 | 271 | 272 | def delete_takes(): 273 | for key in list(state.keys()): 274 | if key.startswith("fc"): 275 | del state[key] 276 | 277 | 278 | def load_takes(): 279 | delete_takes() 280 | 281 | takes = 0 282 | for yaml_file in glob.glob("{}/*.data".format(takes_path)): 283 | take_index = os.path.splitext(os.path.basename(yaml_file))[0] 284 | 285 | if os.path.exists("{}/{}.raw".format(takes_path, take_index)): 286 | with open(yaml_file) as y: 287 | clean = " ".join(y.read().split()).replace("\\", "") 288 | take_info = yaml.safe_load(clean) 289 | # use first key of take_info because we renumber the takes 290 | state["fc{}".format(take_index)] = take_info[list(take_info.keys())[0]] 291 | logger.info("Take {} loaded".format(take_index)) 292 | takes += 1 293 | 294 | state["mag"]["takes"] = takes 295 | state["fc-1"] = deepcopy(state["fc0"]) 296 | state["c0"] = deepcopy(state["fc0"]) 297 | state["c1"] = deepcopy(state["fc1"]) 298 | 299 | 300 | @threaded 301 | def start_playback(): 302 | while True: 303 | if state["video"]["play"]["step"] != 0: 304 | state["video"]["play"]["fn"] += 1 305 | if state["video"]["play"]["fn"] > state["video"]["play"]["out"]: 306 | state["video"]["play"]["fn"] = state["video"]["play"]["in"] 307 | time.sleep(1 / 25) 308 | 309 | 310 | @threaded 311 | def run(): 312 | logger.info("Starting FakeCam") 313 | 314 | start_playback() 315 | 316 | try: 317 | discoversocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 318 | discoversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 319 | discoversocket.bind(("", 7380)) 320 | # discoversocket.setblocking(0) 321 | 322 | port = 7115 323 | 324 | while True: 325 | try: 326 | serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 327 | serversocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 328 | serversocket.bind(("", port)) 329 | serversocket.listen(5) 330 | 331 | serversocket_data = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 332 | serversocket_data.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 333 | serversocket_data.bind(("", port + 1)) 334 | serversocket_data.listen(5) 335 | 336 | break 337 | 338 | except: 339 | raise 340 | 341 | d = Thread(target=discover, args=(discoversocket,)) 342 | d.daemon = True 343 | d.start() 344 | 345 | while True: 346 | (clientsocket, address) = serversocket.accept() 347 | (clientsocket_data, address_data) = serversocket_data.accept() 348 | t = Thread(target=responder, args=(clientsocket, address, clientsocket_data, address_data)) 349 | t.daemon = True 350 | t.start() 351 | 352 | except KeyboardInterrupt: 353 | logger.error("Keyboard Interrupt: stopping server") 354 | 355 | except: 356 | raise 357 | 358 | finally: 359 | try: 360 | discoversocket.close() 361 | serversocket.close() 362 | serversocket_data.close() 363 | except UnboundLocalError: 364 | pass 365 | 366 | 367 | load_takes() 368 | 369 | if __name__ == "__main__": 370 | try: 371 | if sys.argv[1] in ["--ximg", "-x"]: 372 | logger.info("Simulating 10GbE connection") 373 | state["info"]["features"] += " ximg" 374 | except IndexError: 375 | pass 376 | 377 | run() 378 | while True: 379 | time.sleep(100) 380 | -------------------------------------------------------------------------------- /pyphantom/fakecam/fakecam.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python -*- 2 | 3 | block_cipher = None 4 | 5 | added_files = [('*.kv', ''), ('*.png', ''), ('takes', 'takes'), ] 6 | 7 | a = Analysis(['fakecam_gui.py'], 8 | pathex=['/Users/ben/Projects/PHANTOMfuse/fakecam'], 9 | binaries=None, 10 | datas=added_files, 11 | hiddenimports=['uuid'], 12 | hookspath=[], 13 | runtime_hooks=[], 14 | excludes=['enchant', '_tkinter', 'Tkinter', 'twisted', 'cv2', 15 | 'gi.repository.Gst', 'gi.repository.GLib', 16 | 'gi.repository.GObject', 'numpy'], 17 | win_no_prefer_redirects=False, 18 | win_private_assemblies=False, 19 | cipher=block_cipher) 20 | pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) 21 | exe = EXE(pyz, 22 | a.scripts, 23 | exclude_binaries=True, 24 | name='FakeCam', 25 | debug=False, 26 | strip=False, 27 | upx=True, 28 | console=False) 29 | coll = COLLECT(exe, 30 | a.binaries, 31 | a.zipfiles, 32 | a.datas, 33 | strip=False, 34 | upx=True, 35 | name='FakeCam') 36 | app = BUNDLE(coll, 37 | name='FakeCam.app', 38 | icon='fakecam/fakecam.icns', 39 | bundle_identifier=None, 40 | info_plist={ 41 | 'NSHighResolutionCapable': 'True', 42 | 'NSHumanReadableCopyright': 43 | 'Copyright 2016, Ben Hagen <ben@kamerawerk.ch>, ' 44 | 'All Rights Reserved', 45 | }) 46 | -------------------------------------------------------------------------------- /pyphantom/fakecam/fakecam_data.py: -------------------------------------------------------------------------------- 1 | state = { 2 | "play": {"cine": 1, "fn": 0, "frcount": 0, "in": -278, "live": 1, "mag": 0, "out": 0, "speed": 1, "step": 1}, 3 | "defc": { 4 | "rate": 150, 5 | "res": "2048 x 1152", 6 | "exp": 3333332, 7 | "edrexp": 0, 8 | "ptframes": 1, 9 | "shoff": 0, 10 | "ramp": "", 11 | "bcount": 0, 12 | "bperiod": 10416317, 13 | "hqenable": 0, 14 | "decimation": 1, 15 | "frcount": 5000, 16 | "frsize": 2949120, 17 | "aexpmode": 0, 18 | "aexpcomp": 0, 19 | "meta": { 20 | "ox": 0, 21 | "oy": 0, 22 | "w": 1920, 23 | "h": 1080, 24 | "ow": 0, 25 | "oh": 0, 26 | "crop": 1, 27 | }, 28 | }, 29 | "c1": {"firstfr": -4735, "lastfr": 0, "in": -3300, "out": -256, "frcount": 4735, "state": ["RDY"]}, 30 | "mag": { 31 | "progress": 0, 32 | "takes": 2, 33 | "state": 4, 34 | "protect": 0, 35 | "size": 1046478848, 36 | "used": 333447168, 37 | "version": 0, 38 | "runstop": 0, 39 | "type": 100, 40 | }, 41 | "cam": {"timezone": -7200, "cines": 1}, 42 | "video": { 43 | "play": { 44 | "cine": 1, 45 | "fn": -512, 46 | "frcount": 0, 47 | "in": -1000, 48 | "live": 0, 49 | "mag": 0, 50 | "out": -256, 51 | "speed": 0, 52 | "step": 0, 53 | }, 54 | "adj": {}, 55 | }, 56 | "info": { 57 | "battv": 12800, 58 | "battstate": 3, 59 | "features": "attach", 60 | "model": "FakeCam 4K", 61 | "fver": 42, 62 | "name": "Cam Name", 63 | }, 64 | "hw": { 65 | "colorcal": " 0.44 0.41 1.68862 -0.720569 0.0793544 -0.111587 1.05738 1.30908 -0.0465662 -0.771453 6.11653", # FIXME: Get colorcal of KW-Flex 66 | # flex2k 67 | "rtctime": {"tz": -3600}, 68 | }, 69 | "fstat": {"in_progress": 0}, 70 | } 71 | 72 | 73 | answers = {"attach": "Ok! attach", "rec 1": "Ok!", "trig": "Ok!"} 74 | -------------------------------------------------------------------------------- /pyphantom/fakecam/takes-ph7/0.data: -------------------------------------------------------------------------------- 1 | fc0 : { \ 2 | res : 2560 x 1440, \ 3 | rate : 150, \ 4 | exp : 3333333, \ 5 | shoff : 0, \ 6 | edrexp : 0, \ 7 | frdelay : 0, \ 8 | ptframes : 1, \ 9 | bcount : 0, \ 10 | bperiod : 5128205, \ 11 | hqenable : 0, \ 12 | frcount : 1955, \ 13 | ramp : "", \ 14 | autoexp : { \ 15 | rect : { \ 16 | x : 0, \ 17 | y : 0, \ 18 | w : 2, \ 19 | h : 2 \ 20 | }, \ 21 | axlevel : 0, \ 22 | head : 0, \ 23 | lockmode : 0, \ 24 | enable : 0 \ 25 | }, \ 26 | state : { STR }, \ 27 | start : 00A21C00, \ 28 | len : 1F584000, \ 29 | frsize : 0, \ 30 | frspace : 0, \ 31 | trigtime : { \ 32 | secs : 1453818803, \ 33 | frac : 949154 \ 34 | }, \ 35 | trigoff : 1954, \ 36 | firstfr : -1255, \ 37 | lastfr : 0, \ 38 | format : 266, \ 39 | i3trigpos : 0, \ 40 | autotrig : 0, \ 41 | decimation : 7, \ 42 | serial : 11884, \ 43 | in : -1255, \ 44 | out : 0, \ 45 | cam : { \ 46 | syncimg : 0, \ 47 | master : 0, \ 48 | locktoirig : 0, \ 49 | tcmode : 0, \ 50 | modirig : 0, \ 51 | trigpol : 0, \ 52 | trigfilt : 32, \ 53 | frdelay : 0, \ 54 | startonacq : 0, \ 55 | irigouten : 0, \ 56 | memgateen : 0, \ 57 | membpp : 12, \ 58 | tsformat : 1, \ 59 | heads : 0 \ 60 | }, \ 61 | profile : { \ 62 | atrig : 0, \ 63 | p0cnt : 0, \ 64 | p0rate : 0, \ 65 | p0exp : 0, \ 66 | p1cnt : 0, \ 67 | p1rate : 0, \ 68 | p1exp : 0, \ 69 | p2cnt : 0, \ 70 | p2rate : 0, \ 71 | p2exp : 0, \ 72 | p3cnt : 0, \ 73 | p3rate : 0, \ 74 | p3exp : 0 \ 75 | }, \ 76 | info : { \ 77 | hwver : 135, \ 78 | model : "Phantom flex", \ 79 | sensor : 130, \ 80 | snsversion : 0, \ 81 | snsserial : 0, \ 82 | kernel : 16140, \ 83 | swver : 797, \ 84 | xver : 573244, \ 85 | serial : 11884, \ 86 | cfa : 3, \ 87 | filter : 0, \ 88 | name : "11884", \ 89 | mdepths : 00001000, \ 90 | tone1 : "medium 0.125 0.0859 0.25 0.218 0.5 0.5 0.75 0.768", \ 91 | tone2 : "strong 0.125 0.0625 0.25 0.195 0.5 0.5 0.75 0.79", \ 92 | tone3 : "highs1 0.18 0.25 0.375 0.70 0.5 0.860 0.75 0.95", \ 93 | tone4 : "", \ 94 | matrix1 : "daylight -1.45 0.27 -0.15 -0.39 -0.07 -0.80", \ 95 | matrix2 : "", \ 96 | matrix3 : "", \ 97 | matrix4 : "", \ 98 | genlockstatus : 0, \ 99 | snstemp : 30, \ 100 | camtemp : 38, \ 101 | features : "bref shtr blk4 v444 hqmode smpte vsync " \ 102 | }, \ 103 | adj : { \ 104 | red : 2048, \ 105 | green : 1024, \ 106 | blue : 1566, \ 107 | gamma : 2275, \ 108 | rgamma : 0, \ 109 | bgamma : 0, \ 110 | gain : 1024, \ 111 | offset : 0, \ 112 | flare : 0, \ 113 | hue : 0, \ 114 | sat : 1024, \ 115 | rped : 0, \ 116 | gped : 0, \ 117 | bped : 0, \ 118 | tone : 4, \ 119 | chroma : 1024, \ 120 | matrix : 0, \ 121 | mmhue : "", \ 122 | mmsat : "", \ 123 | sdimin : 0, \ 124 | sdimax : 1000 \ 125 | }, \ 126 | meta : { \ 127 | name : "", \ 128 | uuid : "00000000-2e6c-4000-d6a7-83b3000e7ba2", \ 129 | system : 21, \ 130 | pbrate : 25 , \ 131 | tcrate : 25 , \ 132 | trigtc : "15:20:23.17", \ 133 | pax : 0, \ 134 | pay : 1440, \ 135 | paox : 0, \ 136 | paoy : 0, \ 137 | vox : 0, \ 138 | voy : 0, \ 139 | vw : 2560, \ 140 | vh : 1440, \ 141 | vow : 1920, \ 142 | voh : 1080, \ 143 | ox : 0, \ 144 | oy : 0, \ 145 | w : 0, \ 146 | h : 0, \ 147 | ow : 0, \ 148 | oh : 0, \ 149 | crop : 0, \ 150 | resize : 0, \ 151 | lens : "", \ 152 | fstop : 0 , \ 153 | flen : 0 , \ 154 | comment : "" \ 155 | } \ 156 | } 157 | -------------------------------------------------------------------------------- /pyphantom/fakecam/takes-ph7/0.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomatic-io/pyphantom/54978decce8c998d69a2fb4150094b1d8c7862a8/pyphantom/fakecam/takes-ph7/0.raw -------------------------------------------------------------------------------- /pyphantom/fakecam/takes/0.data: -------------------------------------------------------------------------------- 1 | fc51 : { \ 2 | res : 2048 x 1152, \ 3 | rate : 150, \ 4 | exp : 3333332, \ 5 | shoff : 0, \ 6 | edrexp : 0, \ 7 | ptframes : 1, \ 8 | bcount : 0, \ 9 | bperiod : 10416317, \ 10 | hqenable : 0, \ 11 | frcount : 1346, \ 12 | ramp : "", \ 13 | state : { STR }, \ 14 | start : 0x1840000, \ 15 | len : 0x3E74A000, \ 16 | frsize : 2949120, \ 17 | frspace : 1047830528, \ 18 | firstfr : -1345, \ 19 | lastfr : 0, \ 20 | format : 266, \ 21 | decimation : 1, \ 22 | in : -1345, \ 23 | out : 0, \ 24 | aexpmode : 0, \ 25 | aexpcomp : 0, \ 26 | audiopktsize : 0, \ 27 | trigtime : { \ 28 | secs : 1444399007, \ 29 | frac : 740800 \ 30 | }, \ 31 | cam : { \ 32 | syncimg : 0, \ 33 | master : 0, \ 34 | tcmode : 1, \ 35 | trigpol : 0, \ 36 | trigfilt : 32, \ 37 | frdelay : 0, \ 38 | startonacq : 0, \ 39 | aux1mode : 2, \ 40 | aux2mode : 2, \ 41 | aux3mode : 0, \ 42 | memgateen : 0, \ 43 | membpp : 12, \ 44 | tsformat : 1 \ 45 | }, \ 46 | info : { \ 47 | hwver : 4001, \ 48 | sver : 1028, \ 49 | fver : 38, \ 50 | pver : 16, \ 51 | model : "Phantom Flex4K", \ 52 | sensor : 4000, \ 53 | snsversion : 100, \ 54 | snsserial : 0, \ 55 | kernel : 563, \ 56 | swver : 14755, \ 57 | xver : 977545, \ 58 | serial : 16001, \ 59 | cfa : 3, \ 60 | filter : 0, \ 61 | name : "KW_FLEX", \ 62 | mdepths : 0x1000, \ 63 | genlockstatus : 0, \ 64 | snstemp : 33, \ 65 | camtemp : 51, \ 66 | features : "bref blk4 shtr attach earlyimg notify log dualp aux2 wide mm24 v4k genlock mag" \ 67 | }, \ 68 | adj : { \ 69 | red : 1, \ 70 | green : 1, \ 71 | blue : 1, \ 72 | wbtemp : 5600, \ 73 | wbcc : 0, \ 74 | wbred : 1.079672124, \ 75 | wbblue : 1.60720448, \ 76 | edgeg : 0, \ 77 | edger : 0, \ 78 | edgeth : 0, \ 79 | gamma : 2.2, \ 80 | rgamma : 0, \ 81 | bgamma : 0, \ 82 | toe : 1, \ 83 | gain : 1, \ 84 | offset : 0, \ 85 | flare : 0, \ 86 | hue : 0, \ 87 | sat : 1, \ 88 | rped : 0, \ 89 | gped : 0, \ 90 | bped : 0, \ 91 | tone : "0.024 0.100 0.056 0.200 0.134 0.400 0.224 0.600 0.271 0.700 0.600 0.960", \ 92 | mmsat : "", \ 93 | mmhue : "", \ 94 | chroma : 1, \ 95 | matrix : 1, \ 96 | log : 1, \ 97 | sdimin : 32, \ 98 | sdimax : 960, \ 99 | cmatrix : "cal 1.61481 -0.51633 0.033238 -0.10671 0.757674 0.548315 -0.0445308 -0.552791 2.56194", \ 100 | umatrix : "userMatrix 1.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 1.000000", \ 101 | filter : "" \ 102 | }, \ 103 | meta : { \ 104 | name : "A265", \ 105 | uuid : "00000000-3e81-4000-d617-c79f000b4dc0", \ 106 | system : 53, \ 107 | pbrate : 25, \ 108 | tcrate : 25, \ 109 | trigtc : "11:40:46.11", \ 110 | pax : 0, \ 111 | pay : 0, \ 112 | paox : -2048, \ 113 | paoy : -1152, \ 114 | vox : 0, \ 115 | voy : 0, \ 116 | vw : 2048, \ 117 | vh : 1152, \ 118 | vow : 1920, \ 119 | voh : 1080, \ 120 | ox : 0, \ 121 | oy : 0, \ 122 | w : 1920, \ 123 | h : 1080, \ 124 | ow : 1920, \ 125 | oh : 1080, \ 126 | crop : 1, \ 127 | resize : 0, \ 128 | lens : "", \ 129 | fstop : 0, \ 130 | flen : 0, \ 131 | gps : "", \ 132 | comment : "", \ 133 | xset : "" \ 134 | } \ 135 | } 136 | -------------------------------------------------------------------------------- /pyphantom/fakecam/takes/0.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomatic-io/pyphantom/54978decce8c998d69a2fb4150094b1d8c7862a8/pyphantom/fakecam/takes/0.raw -------------------------------------------------------------------------------- /pyphantom/fakecam/takes/1.data: -------------------------------------------------------------------------------- 1 | fc67 : { \ 2 | res : 4096 x 2304, \ 3 | rate : 25, \ 4 | exp : 20000000, \ 5 | shoff : 0, \ 6 | edrexp : 0, \ 7 | ptframes : 1, \ 8 | bcount : 0, \ 9 | bperiod : 33323389, \ 10 | hqenable : 0, \ 11 | frcount : 134, \ 12 | ramp : "", \ 13 | state : { STR }, \ 14 | start : 0x1840000, \ 15 | len : 0x3E74A000, \ 16 | frsize : 11796480, \ 17 | frspace : 1047830528, \ 18 | firstfr : -133, \ 19 | lastfr : 0, \ 20 | format : 266, \ 21 | decimation : 1, \ 22 | in : -133, \ 23 | out : 0, \ 24 | aexpmode : 0, \ 25 | aexpcomp : 0, \ 26 | audiopktsize : 0, \ 27 | trigtime : { \ 28 | secs : 1446198035, \ 29 | frac : 434457 \ 30 | }, \ 31 | cam : { \ 32 | syncimg : 0, \ 33 | master : 0, \ 34 | tcmode : 1, \ 35 | trigpol : 0, \ 36 | trigfilt : 32, \ 37 | frdelay : 0, \ 38 | startonacq : 0, \ 39 | aux1mode : 2, \ 40 | aux2mode : 2, \ 41 | aux3mode : 0, \ 42 | memgateen : 0, \ 43 | membpp : 12, \ 44 | tsformat : 1 \ 45 | }, \ 46 | info : { \ 47 | hwver : 4001, \ 48 | sver : 1029, \ 49 | fver : 53, \ 50 | pver : 16, \ 51 | model : "Phantom Flex4K", \ 52 | sensor : 4000, \ 53 | snsversion : 100, \ 54 | snsserial : 0, \ 55 | kernel : 563, \ 56 | swver : 167360191, \ 57 | xver : 1200700, \ 58 | serial : 16001, \ 59 | cfa : 3, \ 60 | filter : 0, \ 61 | name : "KW_FLEX", \ 62 | mdepths : 0x1000, \ 63 | genlockstatus : 0, \ 64 | snstemp : 31, \ 65 | camtemp : 50, \ 66 | features : "bref blk4 shtr attach earlyimg notify log dualp aux2 wide mm24 v4k genlock audio prores mag" \ 67 | }, \ 68 | adj : { \ 69 | red : 1, \ 70 | green : 1, \ 71 | blue : 1, \ 72 | wbtemp : 5600, \ 73 | wbcc : 0, \ 74 | wbred : 1.079672124, \ 75 | wbblue : 1.60720448, \ 76 | edgeg : 0, \ 77 | edger : 0, \ 78 | edgeth : 0, \ 79 | gamma : 2.2, \ 80 | rgamma : 0, \ 81 | bgamma : 0, \ 82 | toe : 1, \ 83 | gain : 1, \ 84 | offset : 0, \ 85 | flare : 0, \ 86 | hue : 0, \ 87 | sat : 1, \ 88 | rped : 0, \ 89 | gped : 0, \ 90 | bped : 0, \ 91 | tone : "0.024 0.100 0.056 0.200 0.134 0.400 0.224 0.600 0.271 0.700 0.600 0.960", \ 92 | mmsat : "", \ 93 | mmhue : "", \ 94 | chroma : 1, \ 95 | matrix : 1, \ 96 | log : 1, \ 97 | sdimin : 32, \ 98 | sdimax : 960, \ 99 | cmatrix : "cal 1.61481 -0.51633 0.033238 -0.10671 0.757674 0.548315 -0.0445308 -0.552791 2.56194", \ 100 | umatrix : "userMatrix 1.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 1.000000", \ 101 | filter : "" \ 102 | }, \ 103 | meta : { \ 104 | name : "A299", \ 105 | uuid : "00000000-3e81-4000-d633-3b130006a119", \ 106 | system : 53, \ 107 | pbrate : 25, \ 108 | tcrate : 25, \ 109 | trigtc : "09:40:35.10", \ 110 | pax : 0, \ 111 | pay : 0, \ 112 | paox : -2048, \ 113 | paoy : -1152, \ 114 | vox : 0, \ 115 | voy : 0, \ 116 | vw : 4096, \ 117 | vh : 2304, \ 118 | vow : 1911, \ 119 | voh : 1075, \ 120 | ox : 0, \ 121 | oy : 0, \ 122 | w : 0, \ 123 | h : 0, \ 124 | ow : 0, \ 125 | oh : 0, \ 126 | crop : 0, \ 127 | resize : 0, \ 128 | lens : "", \ 129 | fstop : 0, \ 130 | flen : 0, \ 131 | gps : "", \ 132 | comment : "", \ 133 | xset : "" \ 134 | } \ 135 | } 136 | -------------------------------------------------------------------------------- /pyphantom/fakecam/takes/1.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomatic-io/pyphantom/54978decce8c998d69a2fb4150094b1d8c7862a8/pyphantom/fakecam/takes/1.raw -------------------------------------------------------------------------------- /pyphantom/fakecam/takes/2.data: -------------------------------------------------------------------------------- 1 | fc22 : { \ 2 | res : 4096 x 2304, \ 3 | rate : 25, \ 4 | exp : 20000000, \ 5 | shoff : 0, \ 6 | edrexp : 0, \ 7 | ptframes : 1, \ 8 | bcount : 0, \ 9 | bperiod : 33323389, \ 10 | hqenable : 0, \ 11 | frcount : 80, \ 12 | ramp : "", \ 13 | state : { STR }, \ 14 | start : 0x1840000, \ 15 | len : 0x3E74A000, \ 16 | frsize : 11796480, \ 17 | frspace : 1047830528, \ 18 | firstfr : -79, \ 19 | lastfr : 0, \ 20 | format : 266, \ 21 | decimation : 1, \ 22 | in : -79, \ 23 | out : 0, \ 24 | aexpmode : 0, \ 25 | aexpcomp : 0, \ 26 | audiopktsize : 0, \ 27 | trigtime : { \ 28 | secs : 1452185951, \ 29 | frac : 5787 \ 30 | }, \ 31 | cam : { \ 32 | syncimg : 0, \ 33 | master : 0, \ 34 | tcmode : 1, \ 35 | trigpol : 0, \ 36 | trigfilt : 32, \ 37 | frdelay : 0, \ 38 | startonacq : 0, \ 39 | aux1mode : 2, \ 40 | aux2mode : 2, \ 41 | aux3mode : 0, \ 42 | memgateen : 0, \ 43 | membpp : 12, \ 44 | tsformat : 1 \ 45 | }, \ 46 | info : { \ 47 | hwver : 4001, \ 48 | sver : 1029, \ 49 | fver : 53, \ 50 | pver : 16, \ 51 | model : "Phantom Flex4K", \ 52 | sensor : 4000, \ 53 | snsversion : 100, \ 54 | snsserial : 0, \ 55 | kernel : 563, \ 56 | swver : 167360191, \ 57 | xver : 1200700, \ 58 | serial : 16001, \ 59 | cfa : 3, \ 60 | filter : 0, \ 61 | name : "KW_FLEX", \ 62 | mdepths : 0x1000, \ 63 | genlockstatus : 0, \ 64 | snstemp : 30, \ 65 | camtemp : 50, \ 66 | features : "bref blk4 shtr attach earlyimg notify log dualp aux2 wide mm24 v4k genlock audio prores mag" \ 67 | }, \ 68 | adj : { \ 69 | red : 1, \ 70 | green : 1, \ 71 | blue : 1, \ 72 | wbtemp : 5600, \ 73 | wbcc : 0, \ 74 | wbred : 1.079672124, \ 75 | wbblue : 1.60720448, \ 76 | edgeg : 0, \ 77 | edger : 0, \ 78 | edgeth : 0, \ 79 | gamma : 2.2, \ 80 | rgamma : 0, \ 81 | bgamma : 0, \ 82 | toe : 1, \ 83 | gain : 1, \ 84 | offset : 0, \ 85 | flare : 0, \ 86 | hue : 0, \ 87 | sat : 1, \ 88 | rped : 0, \ 89 | gped : 0, \ 90 | bped : 0, \ 91 | tone : "EI1000 0.030 0.100 0.068 0.200 0.159 0.400 0.260 0.600 0.313 0.700 0.700 0.970", \ 92 | mmsat : "", \ 93 | mmhue : "", \ 94 | chroma : 1, \ 95 | matrix : 1, \ 96 | log : 1, \ 97 | sdimin : 32, \ 98 | sdimax : 960, \ 99 | cmatrix : "cal 1.61481 -0.51633 0.033238 -0.10671 0.757674 0.548315 -0.0445308 -0.552791 2.56194", \ 100 | umatrix : "userMatrix 1.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 1.000000", \ 101 | filter : "" \ 102 | }, \ 103 | meta : { \ 104 | name : "A364", \ 105 | uuid : "00000000-3e81-4000-d68e-995f0000169b", \ 106 | system : 53, \ 107 | pbrate : 25, \ 108 | tcrate : 25, \ 109 | trigtc : "16:59:11.00", \ 110 | pax : 0, \ 111 | pay : 0, \ 112 | paox : -2048, \ 113 | paoy : -1152, \ 114 | vox : 0, \ 115 | voy : 0, \ 116 | vw : 4096, \ 117 | vh : 2304, \ 118 | vow : 1911, \ 119 | voh : 1075, \ 120 | ox : 0, \ 121 | oy : 0, \ 122 | w : 4096, \ 123 | h : 2304, \ 124 | ow : 1920, \ 125 | oh : 1080, \ 126 | crop : 0, \ 127 | resize : 1, \ 128 | lens : "", \ 129 | fstop : 0, \ 130 | flen : 0, \ 131 | gps : "", \ 132 | comment : "", \ 133 | xset : "" \ 134 | } \ 135 | } 136 | -------------------------------------------------------------------------------- /pyphantom/fakecam/takes/2.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomatic-io/pyphantom/54978decce8c998d69a2fb4150094b1d8c7862a8/pyphantom/fakecam/takes/2.raw -------------------------------------------------------------------------------- /pyphantom/fakecam/takes/3.data: -------------------------------------------------------------------------------- 1 | fc29 : { \ 2 | res : 4096 x 2304, \ 3 | rate : 938, \ 4 | exp : 133262, \ 5 | shoff : 0, \ 6 | edrexp : 0, \ 7 | ptframes : 1, \ 8 | bcount : 0, \ 9 | bperiod : 10416317, \ 10 | hqenable : 0, \ 11 | frcount : 4736, \ 12 | ramp : "", \ 13 | state : { STR }, \ 14 | start : 0x1840000, \ 15 | len : 0x3E74A000, \ 16 | frsize : 4812800, \ 17 | frspace : 1047830528, \ 18 | firstfr : -4735, \ 19 | lastfr : 0, \ 20 | format : 516, \ 21 | decimation : 1, \ 22 | in : -4735, \ 23 | out : 0, \ 24 | aexpmode : 0, \ 25 | aexpcomp : 0, \ 26 | audiopktsize : 0, \ 27 | trigtime : { \ 28 | secs : 1480928960, \ 29 | frac : 459813 \ 30 | }, \ 31 | cam : { \ 32 | syncimg : 0, \ 33 | master : 0, \ 34 | tcmode : 0, \ 35 | trigpol : 0, \ 36 | trigfilt : 32, \ 37 | frdelay : 0, \ 38 | startonacq : 0, \ 39 | aux1mode : 0, \ 40 | aux2mode : 0, \ 41 | aux3mode : 0, \ 42 | memgateen : 0, \ 43 | membpp : 12, \ 44 | tsformat : 1 \ 45 | }, \ 46 | info : { \ 47 | hwver : 4001, \ 48 | sver : 1032, \ 49 | fver : 65, \ 50 | pver : 16, \ 51 | model : "Phantom Flex4K", \ 52 | sensor : 4000, \ 53 | snsversion : 100, \ 54 | snsserial : 0, \ 55 | kernel : 587, \ 56 | swver : 167360237, \ 57 | xver : 1200700, \ 58 | serial : 16001, \ 59 | cfa : 3, \ 60 | filter : 0, \ 61 | name : "16001", \ 62 | mdepths : 0x1000, \ 63 | genlockstatus : 0, \ 64 | snstemp : 19, \ 65 | camtemp : 25, \ 66 | features : "bref blk4 shtr attach earlyimg notify log dualp aux2 wide mm24 v4k genlock audio prores mag" \ 67 | }, \ 68 | adj : { \ 69 | red : 1, \ 70 | green : 1, \ 71 | blue : 1, \ 72 | wbtemp : 5600, \ 73 | wbcc : 0, \ 74 | wbred : 1.098809019, \ 75 | wbblue : 1.229431919, \ 76 | edgeg : 0, \ 77 | edger : 0, \ 78 | edgeth : 0, \ 79 | gamma : 2.22, \ 80 | rgamma : 0, \ 81 | bgamma : 0, \ 82 | toe : 1, \ 83 | gain : 1, \ 84 | offset : 0, \ 85 | flare : 0, \ 86 | hue : 0, \ 87 | sat : 1, \ 88 | rped : 0, \ 89 | gped : 0, \ 90 | bped : 0, \ 91 | tone : "tone", \ 92 | mmsat : "", \ 93 | mmhue : "", \ 94 | chroma : 1, \ 95 | matrix : 1, \ 96 | log : 1, \ 97 | sdimin : 32, \ 98 | sdimax : 960, \ 99 | cmatrix : "cal 1.51198 -0.430469 0.0669433 -0.115077 0.759451 0.424494 -0.0514928 -0.570455 1.98838", \ 100 | umatrix : "userMatrix 1.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 1.000000", \ 101 | filter : "" \ 102 | }, \ 103 | meta : { \ 104 | name : "", \ 105 | uuid : "00000000-3e81-4000-d845-2ec000070425", \ 106 | system : 21, \ 107 | pbrate : 25, \ 108 | tcrate : 25, \ 109 | trigtc : "07:31:16.11", \ 110 | pax : 0, \ 111 | pay : 0, \ 112 | paox : 0, \ 113 | paoy : 0, \ 114 | vox : 0, \ 115 | voy : 0, \ 116 | vw : 4096, \ 117 | vh : 2304, \ 118 | vow : 1911, \ 119 | voh : 1075, \ 120 | ox : 0, \ 121 | oy : 0, \ 122 | w : 0, \ 123 | h : 0, \ 124 | ow : 0, \ 125 | oh : 0, \ 126 | crop : 0, \ 127 | resize : 0, \ 128 | lens : "", \ 129 | fstop : 0, \ 130 | flen : 0, \ 131 | gps : "", \ 132 | comment : "", \ 133 | xset : "" \ 134 | } \ 135 | } 136 | -------------------------------------------------------------------------------- /pyphantom/fakecam/takes/3.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomatic-io/pyphantom/54978decce8c998d69a2fb4150094b1d8c7862a8/pyphantom/fakecam/takes/3.raw -------------------------------------------------------------------------------- /pyphantom/fakecam/ximg_send.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import logging 3 | import os 4 | import pcapy 5 | import struct 6 | 7 | DEFAULT_TIMEOUT = 3 8 | BPF_START = 0 9 | 10 | logger = logging.getLogger(__name__) 11 | frame_cache = {} 12 | 13 | pc = pcapy.create("lo0") 14 | pc.set_snaplen(1504) 15 | pc.set_promisc(False) 16 | pc.set_timeout(1) # in millisecods 17 | pc.set_buffer_size(32 * 1024 * 1024) 18 | pc.activate() 19 | 20 | 21 | def send_frame(cine, count, dest, ssrc): 22 | global frame_cache 23 | 24 | script_path = os.path.dirname(__file__) 25 | raw_path = os.path.join(script_path, "./takes/{}.raw".format(cine)) 26 | 27 | try: 28 | frame_bytes = frame_cache[raw_path] 29 | except KeyError: 30 | frame_bytes = open(raw_path, "rb").read() 31 | frame_cache[raw_path] = frame_bytes 32 | 33 | to_mac = bytes.fromhex(dest) 34 | from_mac = bytes.fromhex("feedfacebeef") 35 | protocol = b"\x88\xb7" 36 | version = b"\x01" 37 | sequence_number = 512 38 | timestamp = 0 # TODO: don't we want to use this somehow? 39 | unused = 0 40 | length = len(frame_bytes) 41 | 42 | for _ in range(count): 43 | view = memoryview(frame_bytes) 44 | start = b"\x80" 45 | 46 | while len(view): 47 | header = struct.pack( 48 | ">6s 6s 2s c c H I I H I", 49 | to_mac, 50 | from_mac, 51 | protocol, 52 | version, 53 | start, 54 | sequence_number, 55 | timestamp, 56 | ssrc, 57 | unused, 58 | length, 59 | ) 60 | 61 | ether_frame = header + view[:1468].tobytes() 62 | 63 | pc.sendpacket(ether_frame) 64 | view = view[1468:] 65 | sequence_number += 1 66 | if sequence_number > 65535: 67 | sequence_number = 0 68 | start = b"\x00" 69 | -------------------------------------------------------------------------------- /pyphantom/flex.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import logging 3 | import socket 4 | import time 5 | from multiprocessing import Lock 6 | from threading import current_thread 7 | 8 | import yaml 9 | from cached_property import cached_property_with_ttl, cached_property 10 | 11 | from pyphantom.network import get_mac, get_interface_of_ip, get_mac_of_interface 12 | from pyphantom.structures import PhantomStructures 13 | from pyphantom.utils import threaded 14 | 15 | logger = logging.getLogger() 16 | 17 | 18 | def angle2exp(rate, angle): 19 | return 1.0 / rate * 1000000000 / (360.0 / angle) 20 | 21 | 22 | def exp2angle(rate, exp): 23 | return int(round(360 / (1 / float(rate) * 1000000000 / float(exp)))) 24 | 25 | 26 | class CameraError(Exception): 27 | pass 28 | 29 | 30 | class ConnectionError(Exception): 31 | pass 32 | 33 | 34 | class FrameOutsideRangeError(Exception): 35 | pass 36 | 37 | 38 | def parse_simple(response): 39 | clean = response.replace(" ", "").split("{")[1].split("}")[0].split(",") 40 | 41 | out = {} 42 | for x in clean: 43 | try: 44 | key, value = x.split(":") 45 | except ValueError: 46 | split = x.split(":") 47 | key, value = split[0], ":".join(split[1:]) 48 | out[key] = value 49 | 50 | return out 51 | 52 | 53 | def parse_flag_list(response): 54 | return response.split("{")[1].split("}")[0].strip().split() 55 | 56 | 57 | def parse_response(response): 58 | # if response.startswith('state'): 59 | # return response 60 | 61 | brackets = response.count("{") 62 | 63 | if brackets == 1 and "\t" not in response: 64 | if response.count(":") > 1: 65 | return parse_simple(response) 66 | else: 67 | return parse_flag_list(response) 68 | 69 | elif brackets: 70 | clean = " ".join(response.lstrip("Ok!").split()).replace("\\", "") 71 | 72 | # Remove key and replace by X as a marker so it can easily be removed. 73 | # I am sure there is a cleaner way to do this. 74 | clean = ":".join(clean.split(":")[1:]) 75 | clean = "X :" + clean 76 | 77 | try: 78 | return yaml.safe_load(clean)["X"] 79 | except yaml.parser.ParserError: 80 | raise 81 | 82 | elif response.startswith("Ok!"): 83 | return response 84 | 85 | elif response.startswith("ERR: start+count frame outside range"): 86 | raise FrameOutsideRangeError() 87 | 88 | elif response.startswith("ERR"): 89 | raise CameraError(response.replace("ERR: ", "").capitalize()) 90 | 91 | else: 92 | try: 93 | return response.split(" : ")[1].strip().replace('"', "") 94 | except: 95 | return response.strip() 96 | 97 | 98 | class Phantom(object): 99 | def __init__(self, ip: str, port: int = 7115, protocol: str = "PH16"): 100 | self.ip: str = ip 101 | self.port: int = int(port) 102 | self.protocol: str = protocol 103 | self.interface = None 104 | self.interface_mac = None 105 | 106 | self.connected = False 107 | self.alive = False 108 | 109 | self.socket: socket.socket = None 110 | self.socket_data: socket.socket = None 111 | 112 | self.lock = Lock() 113 | self.connect_lock = Lock() 114 | self.data_lock = Lock() 115 | 116 | self.last_message = 0 117 | 118 | fake_ssrc_counter = 0 119 | 120 | @property 121 | def structures(self): 122 | return PhantomStructures(self) 123 | 124 | def get_fake_ssrc(self): 125 | with self.lock: 126 | self.fake_ssrc_counter += 1 127 | if self.fake_ssrc_counter > 65535: 128 | self.fake_ssrc_counter = 0 129 | return self.fake_ssrc_counter 130 | 131 | @cached_property 132 | def mac(self): 133 | try: 134 | return get_mac(self.ip) 135 | except ValueError: 136 | logger.warning("Could not get mac address for {}".format(self.ip)) 137 | return "feedfacebeef" 138 | 139 | @cached_property 140 | def model(self): 141 | return self.ask("get info.model") 142 | 143 | @cached_property_with_ttl(ttl=1) 144 | def takes(self): 145 | return Takes(self) 146 | 147 | @cached_property_with_ttl(ttl=1) 148 | def ram_takes(self): 149 | return RamTakes(self) 150 | 151 | @cached_property_with_ttl(ttl=0.03) 152 | def recstatus(self): 153 | return "WTR" in self.ask("get c1.state") 154 | 155 | @cached_property_with_ttl(ttl=0.5) 156 | def c1(self): 157 | return self.get_takeinfo("c1") 158 | 159 | @cached_property_with_ttl(ttl=0.5) 160 | def defc(self): 161 | return self.ask("get defc") 162 | 163 | @cached_property_with_ttl(ttl=0.5) 164 | def shutter_angle(self): 165 | return exp2angle(self.defc["rate"], self.defc["exp"]) 166 | 167 | @property 168 | def progress(self): 169 | try: 170 | return int(self.ask("get mag.progress")) 171 | except ValueError: 172 | return 0 173 | 174 | @property 175 | def mag_state(self): 176 | try: 177 | return int(self.ask("get mag.state")) 178 | except TypeError: 179 | return 180 | 181 | @cached_property_with_ttl(ttl=0.5) 182 | def battv(self): 183 | return round(float(self.ask("get info.battv")) / 1000, 1) 184 | 185 | @cached_property_with_ttl(ttl=0.5) 186 | def vcdina(self): 187 | try: 188 | return round(float(self.ask("get info.vcdina")), 1) 189 | except CameraError: 190 | return 0 191 | 192 | @cached_property_with_ttl(ttl=0.5) 193 | def vcdinb(self): 194 | try: 195 | return round(float(self.ask("get info.vcdinb")), 1) 196 | except CameraError: 197 | return 0 198 | 199 | @cached_property_with_ttl(ttl=0.5) 200 | def battstate(self): 201 | return int(self.ask("get info.battstate")) 202 | 203 | @property 204 | def video_play(self): 205 | return self.ask("get video.play") 206 | 207 | @property 208 | def resolution(self): 209 | if int(self.defc["meta"]["crop"]) or int(self.defc["meta"]["resize"]): 210 | return "{}x{}".format(self.defc["meta"]["ow"], self.defc["meta"]["oh"]) 211 | else: 212 | return self.defc["res"] 213 | 214 | def connect(self): 215 | with self.connect_lock: 216 | if not self.connected: 217 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 218 | self.socket.settimeout(4.0) 219 | self.socket.connect((self.ip, self.port)) 220 | 221 | if self.protocol == "PH16": 222 | self.socket_data = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 223 | self.socket_data.settimeout(4.0) 224 | data_port = self.port + 1 225 | self.socket_data.connect((self.ip, data_port)) 226 | socket_data_port = self.socket_data.getsockname()[1] 227 | 228 | self.ask("attach {}".format(socket_data_port)).strip() 229 | 230 | elif self.protocol == "PH7": 231 | data_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 232 | data_server.bind(("", 0)) 233 | data_server.listen(5) 234 | data_server_port = data_server.getsockname()[1] 235 | 236 | self.ask("startdata {{port: {}}}".format(data_server_port)).strip() 237 | self.socket_data, address = data_server.accept() 238 | 239 | self.connected = True 240 | 241 | # sync camera clock to this systems clock 242 | self.set_rtc() 243 | 244 | self.interface = get_interface_of_ip(self.ip) 245 | self.interface_mac = get_mac_of_interface(self.interface) 246 | 247 | logger.info("Connected to a {} at {} on interface {}".format(self.model, self.ip, self.interface)) 248 | if self.protocol == "PH16": 249 | cinestation_firmware_version = self.ask("get info.fver") 250 | cam_firmware_version = self.ask("get fc0.info.fver") 251 | elif self.protocol == "PH7": 252 | cinestation_firmware_version = self.ask("get info.swver") 253 | cam_firmware_version = self.ask("get fc0.info.swver") 254 | 255 | logger.info("Cinestation firmware version: {}".format(cinestation_firmware_version)) 256 | logger.info("Camera firmware version: {}".format(cam_firmware_version)) 257 | 258 | @threaded 259 | def check_alive(self, check_interval=0.4): 260 | while True: 261 | if not current_thread().parent_thread.is_alive(): 262 | logger.warning("Parent thread died. Stopping check_alive") 263 | break 264 | 265 | if self.connected: 266 | try: 267 | _ = self.mag_state 268 | time.sleep(check_interval) 269 | except Exception as e: 270 | logger.warning("Connection dead. %s", e) 271 | self.disconnect() 272 | else: 273 | break 274 | 275 | def disconnect(self): 276 | self.alive = False 277 | self.connected = False 278 | if self.socket: 279 | self.socket.close() 280 | if self.socket_data: 281 | self.socket_data.close() 282 | logger.info("Disconnected") 283 | 284 | def __enter__(self): 285 | return self 286 | 287 | def __exit__(self, exception_type, exception_value, traceback): 288 | self.disconnect() 289 | 290 | def toggle(self): 291 | if self.alive: 292 | commands = ["rec 1", "trig"] 293 | self.ask(commands[int(self.recstatus)]) 294 | 295 | def get_takeinfo(self, take, keys=None): 296 | keys = keys or ["firstfr", "lastfr", "in", "out", "frcount", "state"] 297 | takeinfo = {} 298 | for key in keys: 299 | takeinfo[key] = self.ask("get {}.{}".format(take, key)) 300 | 301 | return takeinfo 302 | 303 | def recvall(self, total_length: int) -> bytes: 304 | data = b"" 305 | while len(data) < total_length: 306 | packet = self.socket_data.recv(total_length - len(data)) 307 | if not packet: 308 | # TODO: Raise a better exception 309 | raise ValueError 310 | data += packet 311 | return data 312 | 313 | def recv_frame(self, frame_size, frame_header): 314 | data = bytearray(frame_size + len(frame_header)) 315 | data[: len(frame_header)] = frame_header 316 | view = memoryview(data) 317 | view = view[len(frame_header) :] 318 | to_read = frame_size 319 | while to_read: 320 | nbytes = self.socket_data.recv_into(view, to_read) 321 | view = view[nbytes:] 322 | to_read -= nbytes 323 | return data 324 | 325 | @staticmethod 326 | def recv_end(the_socket: socket.socket): 327 | end = "\r\n" 328 | escaped_crlf = "\\\r\n" 329 | 330 | total_data = [] 331 | 332 | while True: 333 | data = the_socket.recv(8192).decode("ascii", errors="ignore") 334 | if not data: 335 | logger.warning("No data..") 336 | break 337 | total_data.append(data) 338 | 339 | # In case last received packet is only one byte we need to check the last two packets 340 | last_2_packets = "".join(total_data[-2:]) 341 | if last_2_packets.endswith(end) and not last_2_packets.endswith(escaped_crlf): 342 | break 343 | 344 | return "".join(total_data) 345 | 346 | def ask(self, command: str): 347 | try: 348 | with self.lock: 349 | # logger.debug('Command: {}'.format(command)) 350 | self.socket.sendall(command.encode("ascii") + b"\n") 351 | response = self.recv_end(self.socket) 352 | 353 | self.alive = True 354 | self.last_message = time.time() 355 | 356 | return parse_response(response) 357 | 358 | except AttributeError: 359 | logger.warning("Trying to send data without connection") 360 | 361 | except socket.timeout: 362 | self.disconnect() 363 | raise ConnectionError("Socket timeout") 364 | 365 | except socket.error as serr: 366 | self.disconnect() 367 | 368 | if serr.errno == errno.EBADF: 369 | raise ConnectionError("bad file descriptor") 370 | elif serr.errno == errno.ECONNREFUSED: 371 | raise ConnectionError("connection refused") 372 | elif serr.errno == errno.EPIPE: 373 | raise ConnectionError("broken pipe") 374 | elif serr.errno == errno.ECONNRESET: 375 | raise ConnectionError("connection reset") 376 | elif serr.errno == errno.ENETUNREACH: 377 | raise ConnectionError("network unreachable") 378 | else: 379 | raise ConnectionError(serr) 380 | 381 | except CameraError: 382 | raise 383 | 384 | except Exception as e: 385 | logger.exception(e) 386 | self.disconnect() 387 | raise 388 | 389 | def ask_raw(self, command: str): 390 | with self.lock: 391 | self.socket.sendall(command.encode("ascii") + b"\n") 392 | response = self.recv_end(self.socket) 393 | 394 | return response 395 | 396 | def live(self): 397 | self.ask("set video.play.live 1") 398 | self.ask("set video.play.step 0") 399 | 400 | def play(self, cine=1, source="ram"): 401 | try: 402 | if self.video_play["step"]: 403 | self.ask("vplay {{cine: {}, step: 0, from: {}}}".format(cine, source)) 404 | else: 405 | self.ask("vplay {{cine: {}, step: 1, speed: 1, from: {}}}".format(cine, source)) 406 | except CameraError as e: 407 | logger.warning(e) 408 | 409 | def set_playhead(self, frame): 410 | if self.protocol == "PH16": 411 | if frame != self.video_play["fn"] and frame in range( 412 | int(self.video_play["in"]), int(self.video_play["out"]) 413 | ): 414 | self.ask("vplay {{fn: {}}}".format(frame)) 415 | 416 | def set_in(self, frame): 417 | if int(frame) < int(self.c1["out"]): 418 | if self.protocol == "PH16": 419 | self.ask("vplay {{in: {}}}".format(frame)) 420 | elif self.protocol == "PH7": 421 | self.ask("vplay {{firstframe: {}}}".format(frame)) 422 | 423 | self.ask("set c1.in {}".format(frame)) 424 | 425 | def set_out(self, frame): 426 | if int(frame) > int(self.c1["in"]): 427 | if self.protocol == "PH16": 428 | self.ask("vplay {{out: {}}}".format(frame)) 429 | elif self.protocol == "PH7": 430 | self.ask("vplay {{lastframe: {}}}".format(frame)) 431 | 432 | self.ask("set c1.out {}".format(frame)) 433 | 434 | def set_value(self, key, value): 435 | self.ask("set {} {}".format(key, value)) 436 | 437 | def save(self): 438 | self.ask("fsave {{cine: 1, firstframe: {}, lastframe: {}}}".format(self.c1["in"], self.c1["out"])) 439 | 440 | def set_rtc(self, timestamp=None): 441 | if not timestamp: 442 | timestamp = time.time() 443 | if self.protocol == "PH16": 444 | self.ask("setrtc {{ value: {} }}".format(int(timestamp))) 445 | elif self.protocol == "PH7": 446 | self.ask("set hw.rtctime.secs {}".format(int(timestamp))) 447 | 448 | 449 | class Takes(object): 450 | def __init__(self, camera): 451 | self.camera = camera 452 | 453 | def __len__(self): 454 | return int(self.camera.ask("get mag.takes")) 455 | 456 | def __getitem__(self, index): 457 | if index >= len(self): 458 | raise IndexError 459 | return self.camera.get_takeinfo( 460 | "fc{}".format(index), ["firstfr", "lastfr", "res", "rate", "trigtime.secs", "format"] 461 | ) 462 | 463 | 464 | class RamTakes(object): 465 | def __init__(self, camera): 466 | self.camera = camera 467 | 468 | def __len__(self): 469 | if self.camera.protocol == "PH16": 470 | return int(self.camera.ask("get cam.cines")) 471 | elif self.camera.protocol == "PH7": 472 | # disable ram cines for now 473 | return 0 474 | 475 | def __getitem__(self, index): 476 | if index >= len(self): 477 | raise IndexError 478 | return self.camera.get_takeinfo( 479 | "c{}".format(index + 1), 480 | ["state", "firstfr", "lastfr", "res", "rate", "trigtime.secs", "format", "info.serial", "info.name"], 481 | ) 482 | -------------------------------------------------------------------------------- /pyphantom/frame.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pyphantom.flex import Phantom 4 | 5 | 6 | def get_frames(camera: Phantom, take: int, frame_index: int, from_ram: bool = False, count: int = 1) -> List[bytes]: 7 | if camera.protocol == "PH16": 8 | img_cmd = ( 9 | f"img {{ cine: {take}, start: {frame_index}, " 10 | f"cnt: {count}, fmt: P10, from: {'ram' if from_ram else 'mag'} }}" 11 | ) 12 | elif camera.protocol == "PH7": 13 | img_cmd = f"img {{ cine: {take}, start: {frame_index}, cnt: {count}, fmt: {'P10' if from_ram else 'flash'} }}" 14 | else: 15 | raise NotImplementedError 16 | 17 | frame_info = camera.ask(img_cmd) 18 | 19 | if frame_info["fmt"] in ["P10", "266"]: 20 | width, height = [int(x) for x in frame_info["res"].split("x")] 21 | frame_size = width * height * 10 // 8 22 | 23 | elif frame_info["fmt"] in ["513", "514", "515", "516", "517"]: 24 | frame_size = int(camera.ask("get fc{}.frsize".format(take))) 25 | 26 | else: 27 | raise NotImplementedError 28 | 29 | frames = camera.recvall(frame_size * count) 30 | return [frames[i : i + frame_size] for i in range(0, len(frames), frame_size)] 31 | -------------------------------------------------------------------------------- /pyphantom/network.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import netifaces 3 | import platform 4 | import subprocess 5 | 6 | logger = logging.getLogger() 7 | 8 | 9 | def get_mac(ip): 10 | try: 11 | if platform.system() != "Windows": 12 | output = subprocess.check_output(["arp", "-n", ip], universal_newlines=True) 13 | mac_raw = output.split()[3] 14 | mac = "".join(["{:02x}".format(int(x, 16)) for x in mac_raw.split(":")]) 15 | return mac 16 | else: 17 | output = subprocess.check_output(["arp", "-a", ip]) 18 | return output.splitlines()[3].split()[1].replace(b"-", b"").decode() 19 | except subprocess.CalledProcessError: 20 | return None 21 | 22 | 23 | def get_interface_of_ip(ip): 24 | if platform.system() != "Windows": 25 | output = subprocess.check_output(["/sbin/route", "-n", "get", ip]) 26 | 27 | for line in output.splitlines(): 28 | if b"interface" in line: 29 | return line.split()[1].decode() 30 | else: 31 | output = subprocess.check_output(["pathping", "-n", "-w", "1", "-h", "1", "-q", "1", ip]) 32 | interface_ip = output.splitlines()[3].split()[1].decode() 33 | for name, config in get_networks().items(): 34 | if config["addr"] == interface_ip: 35 | return name 36 | 37 | 38 | def get_mac_of_interface(interface): 39 | try: 40 | return get_networks()[interface]["mac"] 41 | except KeyError: 42 | return None 43 | 44 | 45 | def get_networks(): 46 | networks = {} 47 | for interface in netifaces.interfaces(): 48 | try: 49 | if_addresses = netifaces.ifaddresses(interface) 50 | config = if_addresses[netifaces.AF_INET][0] 51 | config["mac"] = if_addresses[netifaces.AF_LINK][0]["addr"].replace(":", "") 52 | logger.debug( 53 | "{}: ip={}, netmask={}, broadcast={}, mac={}".format( 54 | interface, config["addr"], config["netmask"], config["broadcast"], config["mac"] 55 | ) 56 | ) 57 | networks[interface] = config 58 | 59 | except KeyError: 60 | pass 61 | 62 | return networks 63 | -------------------------------------------------------------------------------- /pyphantom/structures.py: -------------------------------------------------------------------------------- 1 | class PhantomStructures(object): 2 | path = "" 3 | 4 | def __init__(self, camera, path=""): 5 | super(PhantomStructures, self).__setattr__("camera", camera) 6 | if path: 7 | super(PhantomStructures, self).__setattr__("path", self.path + path) 8 | 9 | def __getattr__(self, item): 10 | if self.path: 11 | return PhantomStructures(self.camera, self.path + "." + item) 12 | else: 13 | return PhantomStructures(self.camera, item) 14 | 15 | def __setattr__(self, key, value): 16 | cmd = "set {}.{} {}".format(self.path, key, value) 17 | self.camera.ask(cmd) 18 | 19 | def __dir__(self): 20 | try: 21 | return self._get().keys() 22 | except AttributeError: 23 | return dir(self._get()) 24 | 25 | def _get(self): 26 | cmd = "get {}".format(self.path if self.path else "*") 27 | return self.camera.ask(cmd) 28 | 29 | def __int__(self): 30 | return int(self._get()) 31 | 32 | def __float__(self): 33 | return float(self._get()) 34 | 35 | def __str__(self): 36 | return str(self._get()) 37 | 38 | def __repr__(self): 39 | return self.__str__() 40 | -------------------------------------------------------------------------------- /pyphantom/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import platform 4 | from multiprocessing import Process 5 | from threading import Thread, current_thread 6 | 7 | import psutil 8 | 9 | logger = logging.getLogger() 10 | 11 | 12 | class ChildThread(Thread): 13 | def __init__(self, *args, **kwargs): 14 | self.parent_thread = current_thread() 15 | Thread.__init__(self, *args, **kwargs) 16 | 17 | 18 | def threaded(fn): 19 | def wrapper(*args, **kwargs): 20 | t = ChildThread(target=fn, args=args, kwargs=kwargs) 21 | t.daemon = True 22 | t.start() 23 | 24 | return wrapper 25 | 26 | 27 | def processed(fn): 28 | def wrapper(*args, **kwargs): 29 | p = Process(target=fn, args=args, kwargs=kwargs) 30 | p.daemon = True 31 | p.start() 32 | 33 | return wrapper 34 | 35 | 36 | def check_pid(pid): 37 | """ Check For the existence of a unix pid. """ 38 | try: 39 | os.kill(pid, 0) 40 | except OSError: 41 | return False 42 | else: 43 | return True 44 | 45 | 46 | def get_sys_info(): 47 | mac_os_version = platform.mac_ver()[0] 48 | memory = psutil.virtual_memory().total / 1024.0 ** 3 49 | cores = psutil.cpu_count(logical=False) 50 | 51 | return locals() 52 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | author="Ben Hagen", 8 | author_email="ben@ottomatic.io", 9 | description="Phantom® highspeed camera control", 10 | entry_points={"console_scripts": ["pfs_cam = pyphantom.cli.pfs_cam:cli"]}, 11 | install_requires=["psutil", "PyYAML", "netifaces", "cached_property", "click", "colorama"], 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | name="pyphantom", 15 | packages=find_packages(), 16 | include_package_data=True, 17 | scripts=[], 18 | setup_requires=["pytest-runner", "setuptools_scm"], 19 | tests_require=["pytest", "tempdir", "pcapy"], 20 | url="https://github.com/ottomatic-io/pyphantom", 21 | use_scm_version=True, 22 | classifiers=[ 23 | "Programming Language :: Python :: 3 :: Only", 24 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 25 | "Operating System :: OS Independent", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ottomatic-io/pyphantom/54978decce8c998d69a2fb4150094b1d8c7862a8/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | from pyphantom import fakecam 5 | 6 | fakecam.logger.setLevel(logging.INFO) 7 | 8 | 9 | @pytest.fixture(scope="session", autouse=True) 10 | def run_fakecam(request): 11 | fakecam.run() 12 | -------------------------------------------------------------------------------- /tests/test_fakecam.py: -------------------------------------------------------------------------------- 1 | from pyphantom import fakecam 2 | 3 | state = { 4 | "string": "some string", 5 | "integer": 2, 6 | "taglist": ["RDY", "ACT"], 7 | "subdict": {"string": "another string", "integer": 3, "subsubdict": {"substring": "blaa"}}, 8 | } 9 | 10 | 11 | def test_simple(): 12 | assert fakecam.get(state, "string") == 'string : "some string"' 13 | assert fakecam.get(state, "integer") == "integer : 2" 14 | assert fakecam.get(state, "taglist") == "taglist : { RDY ACT }" 15 | 16 | 17 | def test_subdict_direct(): 18 | assert fakecam.get(state, "subdict.string") == 'string : "another string"' 19 | assert fakecam.get(state, "subdict.integer") == "integer : 3" 20 | 21 | 22 | def test_subdict_whole(): 23 | assert ( 24 | fakecam.get(state, "subdict") 25 | == 'subdict : { string : "another string", integer : 3, subsubdict : { substring : "blaa" } }' 26 | ) 27 | -------------------------------------------------------------------------------- /tests/test_flex.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyphantom import flex 4 | 5 | 6 | @pytest.fixture(scope="module") 7 | def cam(request): 8 | c = flex.Phantom("127.0.0.1", 7115, "PH16") 9 | c.connect() 10 | 11 | def fin(): 12 | c.disconnect() 13 | 14 | request.addfinalizer(fin) 15 | 16 | return c 17 | 18 | 19 | # FIXME: Find a nicer way to test structures than calling `str()` on them 20 | def test_flag(cam): 21 | assert cam.ask("get c1.state") == ["RDY"] 22 | assert str(cam.structures.c1.state) == str(["RDY"]) 23 | 24 | 25 | def test_simple(cam): 26 | assert cam.ask("get fc0.res") == "2048 x 1152" 27 | assert str(cam.structures.fc0.res) == "2048 x 1152" 28 | 29 | 30 | def test_simple_with_colon(cam): 31 | assert cam.ask("get fc0.meta.trigtc") == "11:40:46.11" 32 | assert str(cam.structures.fc0.meta.trigtc) == "11:40:46.11" 33 | 34 | 35 | def test_dict(cam): 36 | assert cam.ask("get defc") == { 37 | "exp": 1250000, 38 | "meta": {"crop": 0, "oh": 0, "ow": 0, "resize": 0}, 39 | "rate": 400, 40 | "res": "4096x2304", 41 | } 42 | assert str(cam.structures.defc) == str( 43 | {"rate": 400, "res": "4096x2304", "exp": 1250000, "meta": {"crop": 0, "resize": 0, "ow": 0, "oh": 0}} 44 | ) 45 | --------------------------------------------------------------------------------