├── __init__.py ├── mpf-vpcom-bridge.txt ├── VPX Demo ├── start.cmd ├── data │ ├── machine_vars.yaml │ └── audits.yaml ├── MPF_VPX_Demo_0053.7z ├── modes │ ├── base │ │ ├── shows │ │ │ ├── 1_on.yaml │ │ │ ├── 2_on.yaml │ │ │ ├── 3_on.yaml │ │ │ ├── 4_on.yaml │ │ │ ├── hole_wait_show.yaml │ │ │ └── base_display_loop.yaml │ │ └── config │ │ │ └── base.yaml │ ├── attract │ │ ├── config │ │ │ └── attract.yaml │ │ └── shows │ │ │ └── attract_display_loop.yaml │ └── rolloverlit │ │ └── config │ │ └── rolloverlit.yaml └── config │ └── config.yaml ├── mpf_vpcom_bridge ├── __init__.py ├── __main__.py └── main.py ├── Start Test.cmd ├── test_config ├── modes │ └── base │ │ └── config │ │ └── base.yaml └── config │ └── config.yaml ├── setup.py ├── README.md ├── test_VpCom.py └── register_vpcom.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mpf-vpcom-bridge.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /VPX Demo/start.cmd: -------------------------------------------------------------------------------- 1 | mpf both -------------------------------------------------------------------------------- /mpf_vpcom_bridge/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Start Test.cmd: -------------------------------------------------------------------------------- 1 | python -m unittest 2 | pause -------------------------------------------------------------------------------- /VPX Demo/data/machine_vars.yaml: -------------------------------------------------------------------------------- 1 | player1_score: 2 | expire: null 3 | value: 10750 4 | -------------------------------------------------------------------------------- /mpf_vpcom_bridge/__main__.py: -------------------------------------------------------------------------------- 1 | from .main import main 2 | 3 | 4 | if __name__=='__main__': 5 | main() 6 | -------------------------------------------------------------------------------- /test_config/modes/base/config/base.yaml: -------------------------------------------------------------------------------- 1 | #config_version=5 2 | mode: 3 | start_events: ball_starting 4 | -------------------------------------------------------------------------------- /VPX Demo/MPF_VPX_Demo_0053.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/missionpinball/mpf-vpcom-bridge/HEAD/VPX Demo/MPF_VPX_Demo_0053.7z -------------------------------------------------------------------------------- /VPX Demo/modes/base/shows/1_on.yaml: -------------------------------------------------------------------------------- 1 | #show_version=5 2 | 3 | - duration: 1 4 | lights: 5 | light_spinner1: on 6 | light_spinner4: off 7 | -------------------------------------------------------------------------------- /VPX Demo/modes/base/shows/2_on.yaml: -------------------------------------------------------------------------------- 1 | #show_version=5 2 | 3 | - duration: 1 4 | lights: 5 | light_spinner2: on 6 | light_spinner1: off 7 | -------------------------------------------------------------------------------- /VPX Demo/modes/base/shows/3_on.yaml: -------------------------------------------------------------------------------- 1 | #show_version=5 2 | 3 | - duration: 1 4 | lights: 5 | light_spinner2: off 6 | light_spinner3: on 7 | -------------------------------------------------------------------------------- /VPX Demo/modes/base/shows/4_on.yaml: -------------------------------------------------------------------------------- 1 | #show_version=5 2 | 3 | - duration: 1 4 | lights: 5 | light_spinner4: on 6 | light_spinner3: off 7 | -------------------------------------------------------------------------------- /VPX Demo/modes/attract/config/attract.yaml: -------------------------------------------------------------------------------- 1 | #config_version=5 2 | show_player: 3 | mode_attract_started: attract_display_loop 4 | mode_attract_stopped: 5 | attract_display_loop: 6 | action: stop 7 | -------------------------------------------------------------------------------- /VPX Demo/modes/attract/shows/attract_display_loop.yaml: -------------------------------------------------------------------------------- 1 | #show_version=5 2 | - duration: 200ms 3 | slides: 4 | awesome_slide: 5 | widgets: 6 | - type: text 7 | text: GAME OVER 8 | z: 10 9 | y: 15 10 | style: point_display 11 | -------------------------------------------------------------------------------- /VPX Demo/modes/base/shows/hole_wait_show.yaml: -------------------------------------------------------------------------------- 1 | #show_version=5 2 | - duration: 1s 3 | flashers: light_hole 4 | - duration: 750ms 5 | flashers: light_hole 6 | - duration: 500ms 7 | flashers: light_hole 8 | - duration: 250ms 9 | flashers: light_hole 10 | - duration: 50ms 11 | flashers: light_hole 12 | -------------------------------------------------------------------------------- /VPX Demo/modes/rolloverlit/config/rolloverlit.yaml: -------------------------------------------------------------------------------- 1 | #config_version=5 2 | mode: 3 | start_events: rollovers_lit_complete 4 | stop_events: target_hit 5 | priority: 110 6 | 7 | show_player: 8 | mode_rolloverlit_started: target_lit_loop 9 | 10 | shows: 11 | target_lit_loop: 12 | - duration: 1s 13 | lights: 14 | light_target5k: ff 15 | 16 | variable_player: 17 | target_hit: 18 | score: 5000 19 | -------------------------------------------------------------------------------- /VPX Demo/data/audits.yaml: -------------------------------------------------------------------------------- 1 | events: {} 2 | player: {} 3 | shots: 4 | bumper: 135 5 | dtC: 4 6 | dtM: 4 7 | hole: 20 8 | linlane: 11 9 | lsling: 50 10 | rinlane: 25 11 | rollover_a: 36 12 | rollover_b: 77 13 | rollover_c: 35 14 | rsling: 60 15 | target: 39 16 | switches: 17 | s_bumper: 135 18 | s_drop_target_1: 172 19 | s_drop_target_2: 171 20 | s_enter_playfield: 101 21 | s_launch: 0 22 | s_left_inlane: 11 23 | s_leftflipper: 319 24 | s_leftsling: 50 25 | s_plunger_lane: 103 26 | s_right_hole: 20 27 | s_right_inlane: 25 28 | s_rightflipper: 347 29 | s_rightsling: 60 30 | s_spinner: 352 31 | s_standup_target: 39 32 | s_startgame: 14 33 | s_top_rollover_a: 36 34 | s_top_rollover_b: 77 35 | s_top_rollover_c: 35 36 | s_trough_1: 103 37 | s_trough_2: 131 38 | s_trough_3: 139 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | DEPENDENCIES = ["mpf"] 6 | 7 | setup( 8 | name="mpf_vpcom_bridge", 9 | version="0.1.1", 10 | author="The Mission Pinball Framework Team", 11 | author_email="brian@missionpinball.com", 12 | packages=['mpf_vpcom_bridge'], 13 | entry_points={ 14 | "console_scripts": 15 | ["mpf_vpcom_bridge=mpf_vpcom_bridge.main:main"] 16 | }, 17 | install_requires=DEPENDENCIES, 18 | python_requires=">=3.9", 19 | classifiers=[ 20 | 'Development Status :: 3 - Alpha', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: MIT License', 23 | 'Programming Language :: Python :: 3.6', 24 | 'Programming Language :: Python :: 3.7', 25 | 'Programming Language :: Python :: 3.8', 26 | 'Programming Language :: Python :: 3.9', 27 | 'Natural Language :: English', 28 | 'Operating System :: MacOS :: MacOS X', 29 | 'Operating System :: Microsoft :: Windows', 30 | 'Operating System :: POSIX :: Linux', 31 | 'Topic :: Artistic Software', 32 | 'Topic :: Games/Entertainment :: Arcade' 33 | 34 | ], 35 | keywords=["pinball"] 36 | ) -------------------------------------------------------------------------------- /test_config/config/config.yaml: -------------------------------------------------------------------------------- 1 | #config_version=5 2 | 3 | hardware: 4 | platform: virtual_pinball 5 | 6 | switches: 7 | s_start: 8 | number: 0 9 | tags: start 10 | s_test00: 11 | number: 1 12 | s_test37: 13 | number: 2 14 | s_test77_nc: 15 | number: 3 16 | type: 'NC' 17 | s_top_a: 18 | number: swa 19 | 20 | coils: 21 | c_test: 22 | number: 0 23 | c_test_allow_enable: 24 | number: 1 25 | default_hold_power: 1.0 26 | c_trough_eject: 27 | number: coil2 28 | default_pulse_ms: 3ms 29 | 30 | lights: 31 | test_light: 32 | number: lamp3 33 | test_light2: 34 | number: 15 35 | subtype: matrix 36 | test_gi: 37 | number: 15 38 | subtype: gi 39 | 40 | autofire_coils: 41 | jet: 42 | coil: c_test 43 | switch: s_test00 44 | 45 | playfields: 46 | playfield: 47 | default_source_device: bd_trough 48 | tags: default 49 | 50 | ball_devices: 51 | bd_trough: 52 | tags: trough, home, drain 53 | ball_switches: s_test00 54 | eject_coil: c_trough_eject 55 | eject_targets: playfield 56 | entrance_count_delay: 300ms 57 | 58 | modes: 59 | - base -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bridge Installation: 2 | 3 | 1. Register mpf-visual-pinball bridge (run cmd shell as Administrator): 4 | python register_vpcom.py –register 5 | 6 | 2. Create Your MPF table folder. In Your config.yaml enter: 7 | hardware: 8 | platform: virtual_pinball 9 | 10 | 3. In VPX edit script and replace the controller with: 11 | ``` 12 | Set Controller = CreateObject(“MPF.Controller”) 13 | ``` 14 | 15 | 4. If your mpf instance is running on a different machine than the machine running Visual Pinball then tell the bridge where to connect using the Controller.Run() command parameters 16 | - Default BCP server will listen on the loopbak/localhost interface so In order for this to work you need to tell your bcp server to listen on external network interface by altering your config.yaml file as following 17 | ``` 18 | bcp: 19 | servers: 20 | url_style: 21 | ip: 0.0.0.0 22 | ``` 23 | - Update your table script to specify the address and/or port of your server in the 24 | ``` 25 | #Ex 1 : Different machine on port 5051 26 | Controller.Run "mypinball" 27 | 28 | #Ex 2 : Different machine and different port 29 | Controller.Run "mypinball", 1337 30 | ``` 31 | 32 | 33 | 34 | Instructions for MPF installed via `pipx`: 35 | The latest official installation instructions for MPF utilize `pipx` to create a virtual environment (venv) for MPF to run in. To install the mpf-visual-pinball bridge, it must be injected into the MPF venv. 36 | 1. Open a command prompt and change directories (cd) to the folder containing the mpf_vpcom_bridge source code. 37 | 38 | 2. Run the following command to inject mpf_vpcom_bridge into the MPF venv. 39 | ``` 40 | pipx inject mpf ./ 41 | ``` 42 | 3. To register the bridge with Windows as a COM object, the following exe must be run as Administrator. 43 | ``` 44 | mpf_vpcom_bridge.exe 45 | ``` 46 | NOTE: The exe may not be included in the PATH for Windows by default. It may be located in the Scripts folder of the MPF venv, for example: C:\Users\Me\\.local\pipx\venvs\mpf\Scripts\mpf_vpcom_bridge.exe where Me is the Windows username for the session. 47 | 48 | 4. If no longer needed, the bridge can be unregistered by passing the following argument to the exe as Administrator: 49 | ``` 50 | mpf_vpcom_bridge.exe --unregister 51 | ``` 52 | 53 | To run a game: 54 | 55 | 1. start MPF (mpf both), wait until the display has been initialized 56 | 2. start VPX (as Administrator). 57 | 3. Run VPX table. 58 | 59 | Remarks: 60 | The setting of cGamename in VPX is not used to establish or check the connection. 61 | The VPX Bridge connects to the running MPF. 62 | 63 | To run a test: 64 | 65 | 1. run "Start Test.cmd" or run shell command "python -m unittest" 66 | -------------------------------------------------------------------------------- /VPX Demo/modes/base/config/base.yaml: -------------------------------------------------------------------------------- 1 | #config_version=5 2 | 3 | mode: 4 | start_events: ball_starting 5 | 6 | show_player: 7 | mode_base_started: base_display_loop 8 | start_hole_wait_show: 9 | hole_wait_show: 10 | loops: 0 11 | events_when_completed: hole_wait_show_ended 12 | advance_lane{count==1}: 13 | 1_on: play 14 | 4_on: stop 15 | advance_lane{count==2}: 16 | 2_on: play 17 | 1_on: stop 18 | advance_lane{count==3}: 19 | 3_on: play 20 | 2_on: stop 21 | advance_lane{count==4}: 22 | 4_on: play 23 | 3_on: stop 24 | 25 | queue_relay_player: 26 | balldevice_bd_right_hole_ball_eject_attempt: 27 | post: start_hole_wait_show 28 | wait_for: hole_wait_show_ended 29 | 30 | variable_player: 31 | bumper_hit: 32 | score: 500 33 | rsling_hit: 34 | score: 100 35 | lsling_hit: 36 | score: 100 37 | rollover_a_unlit_hit: 38 | score: 50 39 | rollover_a_lit_hit: 40 | score: 1000 41 | rollover_b_unlit_hit: 42 | score: 50 43 | rollover_b_lit_hit: 44 | score: 1000 45 | rollover_c_unlit_hit: 46 | score: 50 47 | rollover_c_lit_hit: 48 | score: 1000 49 | rinlane_hit: 50 | score: 200 51 | linlane_hit: 52 | score: 200 53 | hole_hit: 54 | score: 2500 55 | target_hit: 56 | score: 500 57 | s_drop_target_1_hit: 58 | score: 100 59 | s_drop_target_2_hit: 60 | score: 100 61 | s_spinner_active: 62 | score: 10 63 | drop_target_bank_dt_bank_down: 64 | score: 1000 65 | 66 | counters: 67 | spinner_rotations: 68 | count_events: s_spinner_active 69 | count_complete_value: 4 70 | starting_count: 0 71 | direction: up 72 | persist_state: true 73 | events_when_hit: advance_lane 74 | reset_on_complete: true 75 | disable_on_complete: false 76 | 77 | shots: 78 | bumper: 79 | switch: s_bumper 80 | lsling: 81 | switch: s_leftsling 82 | rsling: 83 | switch: s_rightsling 84 | rollover_a: 85 | switch: s_top_rollover_a 86 | profile: rollovers 87 | show_tokens: 88 | light: light_a 89 | rollover_b: 90 | switch: s_top_rollover_b 91 | profile: rollovers 92 | show_tokens: 93 | light: light_b 94 | rollover_c: 95 | switch: s_top_rollover_c 96 | profile: rollovers 97 | show_tokens: 98 | light: light_c 99 | linlane: 100 | switch: s_left_inlane 101 | rinlane: 102 | switch: s_right_inlane 103 | target: 104 | switch: s_standup_target 105 | hole: 106 | switch: s_right_hole 107 | 108 | shot_groups: 109 | rollovers: 110 | shots: rollover_a, rollover_b , rollover_c 111 | enable_events: ball_started 112 | disable_events: ball_ending 113 | reset_events: target_hit 114 | rotate_left_events: s_leftflipper_active 115 | rotate_right_events: s_rightflipper_active 116 | 117 | shot_profiles: 118 | rollovers: 119 | states: 120 | - name: unlit 121 | show: off 122 | - name: lit 123 | show: on 124 | loop: True 125 | -------------------------------------------------------------------------------- /VPX Demo/modes/base/shows/base_display_loop.yaml: -------------------------------------------------------------------------------- 1 | #show_version=5 2 | - duration: 200ms 3 | slides: 4 | awesome_slide: 5 | widgets: 6 | # - type: image 7 | # image: Idle1 8 | - type: text 9 | text: (score) 10 | number_grouping: true 11 | min_digits: 2 12 | z: 10 13 | y: 20 14 | style: point_display 15 | 16 | - type: text 17 | text: PLAYER (number) 18 | z: 10 19 | y: 0 20 | x: 1 21 | style: status_display 22 | anchor_x: left 23 | anchor_y: bottom 24 | - type: text 25 | text: BALL (ball) 26 | z: 10 27 | y: 0 28 | x: right 29 | style: status_display 30 | anchor_x: right 31 | anchor_y: bottom 32 | lights: 33 | light1: ff 34 | light2: 0 35 | light3: 0 36 | 37 | - duration: 200ms 38 | slides: 39 | press_start: 40 | widgets: 41 | # - type: image 42 | # image: Idle2 43 | - type: text 44 | text: (score) 45 | number_grouping: true 46 | min_digits: 2 47 | z: 10 48 | y: 20 49 | style: point_display 50 | - type: text 51 | text: PLAYER (number) 52 | z: 10 53 | y: 0 54 | x: 1 55 | style: status_display 56 | anchor_x: left 57 | anchor_y: bottom 58 | - type: text 59 | text: BALL (ball) 60 | z: 10 61 | y: 0 62 | x: right 63 | style: status_display 64 | anchor_x: right 65 | anchor_y: bottom 66 | lights: 67 | light1: 0 68 | light2: ff 69 | light3: 0 70 | 71 | - duration: 200ms 72 | slides: 73 | mission_pinball: 74 | widgets: 75 | # - type: image 76 | # image: Idle3 77 | - type: text 78 | text: (score) 79 | number_grouping: true 80 | min_digits: 2 81 | z: 10 82 | y: 20 83 | style: point_display 84 | - type: text 85 | text: PLAYER (number) 86 | z: 10 87 | y: 0 88 | x: 1 89 | style: status_display 90 | anchor_x: left 91 | anchor_y: bottom 92 | - type: text 93 | text: BALL (ball) 94 | z: 10 95 | y: 0 96 | x: right 97 | style: status_display 98 | anchor_x: right 99 | anchor_y: bottom 100 | lights: 101 | light1: 0 102 | light2: 0 103 | light3: ff 104 | 105 | - duration: 200ms 106 | slides: 107 | mission_pinball2: 108 | widgets: 109 | # - type: image 110 | # image: Idle4 111 | - type: text 112 | text: (score) 113 | number_grouping: true 114 | min_digits: 2 115 | z: 10 116 | y: 20 117 | style: point_display 118 | - type: text 119 | text: PLAYER (number) 120 | z: 10 121 | y: 0 122 | x: 1 123 | style: status_display 124 | anchor_x: left 125 | anchor_y: bottom 126 | - type: text 127 | text: BALL (ball) 128 | z: 10 129 | y: 0 130 | x: right 131 | style: status_display 132 | anchor_x: right 133 | anchor_y: bottom 134 | lights: 135 | light1: 0 136 | light2: ff 137 | light3: 0 138 | -------------------------------------------------------------------------------- /test_VpCom.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from unittest.mock import patch, MagicMock 4 | 5 | from mpf.core.utility_functions import Util 6 | from mpf.tests.MpfBcpTestCase import MpfBcpTestCase 7 | 8 | class TestVPCom(MpfBcpTestCase): 9 | 10 | def get_platform(self): 11 | return False 12 | 13 | def getConfigFile(self): 14 | return 'config.yaml' 15 | 16 | def getMachinePath(self): 17 | def getconfigFile(self): 18 | return 'config.yaml' 19 | return os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test_config') 20 | 21 | def _initialise_machine(self): 22 | self.init = Util.ensure_future(self.machine.initialise(), loop=self.machine.clock.loop) 23 | self.advance_time_and_run(1) 24 | 25 | with self.assertRaises(asyncio.futures.InvalidStateError): 26 | self.init.result() 27 | 28 | def setUp(self): 29 | super().setUp() 30 | # we are connected as anonymous client 31 | self._bcp_client.name = None 32 | 33 | def _error(self, desc): 34 | raise AssertionError(desc) 35 | 36 | @patch("register_vpcom.pythoncom") 37 | def test_vpcom(self, pythoncom): 38 | 39 | import register_vpcom 40 | vpcom = register_vpcom.Controller(loop=self.loop) 41 | vpcom._connect = MagicMock() 42 | vpcom._raise_error = self._error 43 | 44 | vpcom.bcp_client = self._bcp_external_client 45 | 46 | vpcom.Run() 47 | self.advance_time_and_run() 48 | self.init.result() 49 | 50 | # Switch Test 51 | # s_test00: 1 52 | # s_test37: 2 53 | # s_test77_nc: 3 54 | # s_top_a: swa 55 | 56 | vpcom.SetSwitch(2, False) 57 | self.assertFalse(vpcom.Switch(2)) 58 | self.assertFalse(vpcom.GetSwitch(2)) 59 | self.assertSwitchState("s_test37", False) 60 | vpcom.SetSwitch(2, True) 61 | self.assertTrue(vpcom.Switch(2)) 62 | self.assertTrue(vpcom.GetSwitch(2)) 63 | self.assertSwitchState("s_test37", True) 64 | vpcom.SetSwitch(2, False) 65 | self.assertFalse(vpcom.Switch(2)) 66 | self.assertFalse(vpcom.GetSwitch(2)) 67 | self.assertSwitchState("s_test37", False) 68 | 69 | vpcom.SetSwitch(1, False) 70 | self.assertSwitchState("s_test00", False) 71 | self.assertSwitchState("s_test37", False) 72 | vpcom.SetSwitch(1, True) 73 | self.assertSwitchState("s_test00", True) 74 | self.assertSwitchState("s_test37", False) 75 | 76 | #String type numbers 77 | vpcom.SetSwitch("swa", False) 78 | self.assertSwitchState("s_top_a", False) 79 | self.assertFalse(vpcom.Switch("swa")) 80 | vpcom.SetSwitch("swa", True) 81 | self.assertSwitchState("s_top_a", True) 82 | self.assertTrue(vpcom.GetSwitch("swa")) 83 | vpcom.PulseSW("swa") 84 | self.assertSwitchState("s_top_a", False) 85 | self.assertFalse(vpcom.Switch("swa")) 86 | 87 | #Switch(3) is NC 88 | vpcom.SetSwitch(3, False) 89 | self.assertTrue(vpcom.Switch(3)) 90 | self.assertTrue(vpcom.GetSwitch(3)) 91 | self.assertSwitchState("s_test77_nc", True) 92 | vpcom.SetSwitch(3, True) 93 | self.assertFalse(vpcom.Switch(3)) 94 | self.assertFalse(vpcom.GetSwitch(3)) 95 | self.assertSwitchState("s_test77_nc", False) 96 | vpcom.SetSwitch(3, False) 97 | self.assertTrue(vpcom.Switch(3)) 98 | self.assertTrue(vpcom.GetSwitch(3)) 99 | self.assertSwitchState("s_test77_nc", True) 100 | 101 | #Solenoid Test 102 | # c_test: 0 103 | # c_test_allow_enable: 1 104 | # c_trough_eject: coil2 105 | 106 | #Pulse and Enable, both should be ON 107 | self.assertFalse(vpcom.ChangedSolenoids()) 108 | self.machine.coils["c_test"].pulse() 109 | self.machine.coils["c_trough_eject"].pulse() 110 | self.machine.coils["c_test_allow_enable"].enable() 111 | self.assertCountEqual([("0", True),("1", True),("coil2", True)], vpcom.ChangedSolenoids()) 112 | 113 | #After a short while the pulsed coils should be OFF 114 | self.advance_time_and_run(.1) 115 | self.assertCountEqual([("0", False),("coil2", False)], vpcom.ChangedSolenoids()) 116 | 117 | #Disable to get hold coil OFF 118 | self.machine.coils["c_test_allow_enable"].disable() 119 | self.assertCountEqual([("1", False)], vpcom.ChangedSolenoids()) 120 | 121 | #Lights Test 122 | # test_light: lamp3 123 | # test_light2: 15 124 | # test_gi: 15 125 | 126 | self.assertFalse(vpcom.ChangedLamps()) 127 | self.assertFalse(vpcom.ChangedGIStrings()) 128 | self.machine.lights["test_light"].on() 129 | self.machine.lights["test_gi"].on() 130 | self.assertCountEqual([("lamp3", True)], vpcom.ChangedLamps()) 131 | self.assertCountEqual([("15", True)], vpcom.ChangedGIStrings()) 132 | self.machine.lights["test_light"].off() 133 | self.assertCountEqual([("lamp3", False)], vpcom.ChangedLamps()) 134 | 135 | #Hardwarerules Test 136 | # Game Over 137 | # no rules are active 138 | self.assertFalse(vpcom.HardwareRules()) 139 | self.assertFalse(vpcom.IsCoilActive("0")) 140 | 141 | # Start Game 142 | self.machine_config_patches['switches'] = dict() 143 | self.machine_config_patches['switches']['s_start'] = {"number": "", "tags": "start"} 144 | for trough in self.machine.ball_devices.items_tagged("trough"): 145 | for switch in trough.config['ball_switches']: 146 | self.hit_switch_and_run(switch.name, 0) 147 | self.advance_time_and_run(1) 148 | 149 | # Game still OFF 150 | self.assertIsNone(self.machine.game, "Expected game to have ended but game is active.") 151 | 152 | self.hit_and_release_switch("s_start") 153 | self.advance_time_and_run(1) 154 | 155 | # Game is now started 156 | self.assertIsNotNone(self.machine.game, "Expected a running game but no game is active.") 157 | self.assertEqual(1, self.machine.game.num_players) 158 | 159 | #rules for autofire_coils are active 160 | self.assertTrue(vpcom.IsCoilActive("0")) 161 | self.assertTrue(vpcom.HardwareRules()) 162 | -------------------------------------------------------------------------------- /VPX Demo/config/config.yaml: -------------------------------------------------------------------------------- 1 | #config_version=5 2 | hardware: 3 | platform: virtual_pinball 4 | 5 | switches: 6 | s_startgame: 7 | number: 0 8 | tags: start 9 | s_trough_1: 10 | number: tr1 11 | s_trough_2: 12 | number: tr2 13 | s_trough_3: 14 | number: tr3 15 | s_leftflipper: 16 | number: 2 17 | tags: left_flipper 18 | s_rightflipper: 19 | number: 3 20 | tags: right_flipper 21 | s_leftsling: 22 | number: 4 23 | s_rightsling: 24 | number: 5 25 | s_bumper: 26 | number: 6 27 | s_plunger_lane: 28 | number: 7 29 | tags: plunger_lane 30 | s_right_hole: 31 | number: hole 32 | s_top_rollover_a: 33 | number: swa 34 | s_top_rollover_b: 35 | number: swb 36 | s_top_rollover_c: 37 | number: swc 38 | s_launch: 39 | number: 8 40 | s_left_inlane: 41 | number: 9 42 | s_right_inlane: 43 | number: 10 44 | s_standup_target: 45 | number: 11 46 | s_enter_playfield: 47 | number: 12 48 | type: 'NC' 49 | s_drop_target_1: 50 | number: 21 51 | s_drop_target_2: 52 | number: 22 53 | s_spinner: 54 | number: 23 55 | 56 | coils: 57 | c_ballrelease: 58 | number: coil1 59 | default_pulse_ms: 100 60 | c_leftflipper: 61 | number: coil2 62 | default_pulse_ms: 100 63 | c_rightflipper: 64 | number: coil3 65 | default_pulse_ms: 100 66 | c_leftflipper_hold: 67 | number: 42 68 | default_hold_power: 0.25 69 | allow_enable: true 70 | c_rightflipper_hold: 71 | number: 43 72 | default_hold_power: 0.25 73 | allow_enable: true 74 | 75 | c_leftsling: 76 | number: coil4 77 | default_pulse_ms: 100 78 | c_rightsling: 79 | number: coil5 80 | default_pulse_ms: 100 81 | c_bumper: 82 | number: coil6 83 | default_pulse_ms: 100 84 | c_plunger_eject: 85 | number: coil7 86 | default_pulse_ms: 100 87 | c_right_hole: 88 | number: coil8 89 | default_pulse_ms: 100 90 | c_drop_target_reset: 91 | number: coil_9 92 | default_pulse_ms: 100 # be sure to set this to at least 100ms so that it pulses long enough on a ball loss to reset the drop targets 93 | 94 | drop_targets: # this configs each switch to a drop target 95 | dtC: 96 | switch: s_drop_target_1 97 | dtM: 98 | switch: s_drop_target_2 99 | 100 | 101 | drop_target_banks: # this assigns the drop targets to their bank and sets parameters 102 | dt_bank: 103 | drop_targets: dtC, dtM 104 | reset_coils: c_drop_target_reset 105 | reset_on_complete: 100ms # this has to be at least 100ms or it will be too short for VPX to see on a ball draiin 106 | reset_events: 107 | - drop_target_bank_dt_bank_down 108 | - ball_starting 109 | - machine_reset_phase_3 110 | 111 | lights: 112 | light1: 113 | number: l-1 114 | subtype: led 115 | light2: 116 | number: l-2 117 | subtype: led 118 | light3: 119 | number: l-3 120 | subtype: led 121 | light_a: 122 | number: l-4 123 | light_b: 124 | number: l-5 125 | light_c: 126 | number: l-6 127 | light_target5k: 128 | number: l-5k 129 | light_hole: 130 | number: 2 131 | subtype: flasher 132 | test_light2: 133 | number: 15 134 | subtype: matrix 135 | test_gi: 136 | number: gi1 137 | subtype: gi 138 | light_dt: 139 | number: l_dt 140 | light_spinner1: 141 | number: l_spinner1 142 | light_spinner2: 143 | number: l_spinner2 144 | light_spinner3: 145 | number: l_spinner3 146 | light_spinner4: 147 | number: l_spinner4 148 | 149 | 150 | 151 | playfields: 152 | playfield: 153 | default_source_device: bd_trough 154 | tags: default 155 | 156 | autofire_coils: 157 | left_slingshot: 158 | coil: c_leftsling 159 | switch: s_leftsling 160 | right_slingshot: 161 | coil: c_rightsling 162 | switch: s_rightsling 163 | jet: 164 | coil: c_bumper 165 | switch: s_bumper 166 | 167 | flippers: 168 | left_flipper: 169 | main_coil: c_leftflipper 170 | hold_coil: c_leftflipper_hold 171 | activation_switch: s_leftflipper 172 | # enable_events: machine_reset_phase_3 173 | right_flipper: 174 | main_coil: c_rightflipper 175 | hold_coil: c_rightflipper_hold 176 | activation_switch: s_rightflipper 177 | # enable_events: machine_reset_phase_3 178 | 179 | ball_devices: 180 | bd_trough: 181 | tags: trough, home, drain 182 | ball_switches: s_trough_1, s_trough_2, s_trough_3 183 | eject_coil: c_ballrelease 184 | eject_targets: bd_plunger 185 | entrance_count_delay: 300ms 186 | bd_plunger: 187 | ball_switches: s_plunger_lane 188 | entrance_count_delay: 300ms 189 | eject_timeouts: 3s 190 | eject_coil: c_plunger_eject 191 | player_controlled_eject_event: s_launch 192 | debug: true 193 | bd_right_hole: 194 | ball_switches: s_right_hole 195 | entrance_count_delay: 300ms 196 | eject_timeouts: 2s 197 | eject_coil: c_right_hole 198 | 199 | light_player: 200 | ball_starting: 201 | test_gi: ff 202 | ball_ending: 203 | test_gi: 0 204 | 205 | window: 206 | width: 774 207 | height: 200 208 | title: Mission Pinball Framework 209 | resizable: true 210 | fullscreen: false 211 | borderless: false 212 | exit_on_escape: true 213 | source_display: window 214 | 215 | displays: 216 | window: 217 | width: 774 218 | height: 200 219 | dmd: 220 | width: 128 221 | height: 32 222 | default: true 223 | 224 | slides: 225 | window_slide: 226 | - type: display 227 | width: 768 228 | height: 192 229 | source_display: dmd 230 | 231 | effects: 232 | - type: dmd 233 | dot_color: ff5500 234 | background_color: 220000 235 | dot_filter: True 236 | - type: rectangle 237 | width: 772 238 | height: 196 239 | color: orange 240 | 241 | slide_player: 242 | init_done: 243 | window_slide: 244 | target: window 245 | 246 | widget_styles: 247 | point_display: 248 | font_name: Quadrit 249 | font_size: 10 250 | adjust_top: 2 251 | adjust_bottom: 3 252 | status_display: 253 | font_name: Quadrit 254 | font_size: 5 255 | 256 | modes: 257 | - attract 258 | - base 259 | - rolloverlit 260 | -------------------------------------------------------------------------------- /register_vpcom.py: -------------------------------------------------------------------------------- 1 | """Brige between virtual pinball and MPF. 2 | 3 | Based on proc-visual-pinball of destruk, Gerry Stellenberg, Adam Preble and Michael Ocean. 4 | """ 5 | import asyncio 6 | import sys 7 | import logging 8 | 9 | try: 10 | import win32com 11 | import win32com.server.util 12 | from win32com.server.util import wrap, unwrap 13 | import pythoncom 14 | from win32com.server.exception import COMException 15 | import winerror 16 | except ImportError: 17 | win32com = None 18 | util = None 19 | wrap = None 20 | unwrap = None 21 | pythoncom = None 22 | COMException = AssertionError 23 | winerror = None 24 | 25 | from mpf.core.bcp.bcp_socket_client import AsyncioBcpClientSocket 26 | 27 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 28 | filename="mpf-vpcom-bridge.txt") 29 | 30 | 31 | class ISettings: 32 | _public_methods_ = [] 33 | _public_attrs_ = [ 'Value'] 34 | 35 | def Value(self, item, item2): 36 | del item 37 | del item2 38 | return True 39 | 40 | def SetValue(self, item, item2): 41 | del item 42 | del item2 43 | return True 44 | 45 | 46 | class IGames: 47 | _public_methods_ = [] 48 | _public_attrs_ = [ 'Settings'] 49 | 50 | def Settings(self): 51 | settings = ISettings() 52 | Settings = wrap( settings ) 53 | return Settings 54 | 55 | def SetSettings(self): 56 | settings = ISettings() 57 | Settings = wrap( settings ) 58 | return Settings 59 | 60 | 61 | class Controller: 62 | 63 | """Main Visual Pinball COM interface class.""" 64 | 65 | _public_methods_ = [ 66 | 'Run', 67 | 'Stop', 68 | 'PrintGlobal', 69 | 'PulseSW', 70 | 'IsCoilActive' 71 | ] 72 | _reg_progid_ = "MPF.Controller" # Visual MPF Controller 73 | _reg_clsid_ = "{196FF002-17F9-4714-4242-A7BD39AD413B}" # use a unique class guid for Visual MPF Controller 74 | _public_attrs_ = [ 75 | 'Version', 76 | 'GameName', 77 | 'Games', 78 | 'SplashInfoLine', 79 | 'ShowTitle', 80 | 'ShowFrame', 81 | 'ShowDMDOnly', 82 | 'HandleMechanics', 83 | 'HandleKeyboard', 84 | 'DIP', 85 | 'Switch', 86 | 'Mech', 87 | 'Pause', 88 | 'ChangedSolenoids', 89 | 'ChangedGIStrings', 90 | 'ChangedLamps', 91 | 'ChangedLEDs', 92 | 'ChangedFlashers', 93 | 'HardwareRules', 94 | 'GetMech' 95 | ] 96 | 97 | _readonly_attrs_ = [ 98 | 'Version', 99 | 'ChangedSolenoids', 100 | 'ChangedLamps', 101 | 'ChangedGIStrings', 102 | 'ChangedLEDs', 103 | 'ChangedFlashers', 104 | 'HardwareRules', 105 | 'GetMech' 106 | ] 107 | 108 | Version = "22222222" 109 | ShowTitle = None 110 | ShowFrame = False 111 | ShowDMDOnly = False 112 | HandleKeyboard = False 113 | DIP = False 114 | GameName = "Game name" 115 | 116 | Pause = None 117 | 118 | HandleMechanics = True 119 | ErrorMsg = "Python Failed -- check the log" 120 | 121 | def __init__(self, *, loop=None): 122 | self._reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER # LocalSever (no InProc) only means game reloads entirely 123 | self.bcp_client = None 124 | self.last_switch = None 125 | self.switches = {} 126 | if not loop: 127 | self.loop = asyncio.get_event_loop() 128 | else: 129 | self.loop = loop 130 | 131 | # Need to overload this method to tell that we support IID_IServerWithEvents 132 | def _query_interface_(self, iid): 133 | """ Return this main interface if the IController class is queried. """ 134 | IID_IController = pythoncom.MakeIID('{CE9ECC7C-960F-407E-B27B-62E39AB1E30F}') 135 | if iid == IID_IController: 136 | return win32com.server.util.wrap(self) 137 | 138 | def PrintGlobal(self): 139 | """Unused.""" 140 | logging.getLogger('vpcom').info("PrintGlobal called.") 141 | return True 142 | 143 | def _connect(self, addr, port): 144 | """Connect to MPF.""" 145 | try: 146 | reader, writer = self.loop.run_until_complete(asyncio.open_connection(addr, port)) 147 | self.bcp_client = AsyncioBcpClientSocket(writer, reader) 148 | except Exception as e: 149 | raise COMException(desc="Failed to connect to MPF: {}".format(e), scode=winerror.E_FAIL) 150 | 151 | def Run(self, addr="localhost", port=5051): 152 | """Connect to MPF.""" 153 | logging.getLogger('vpcom').info("Starting bridge. Connecting to {}:{}".format(addr,port)) 154 | 155 | self._connect(addr, port) 156 | 157 | self.bcp_client.send("vpcom_bridge", {"subcommand": "start"}) 158 | self.loop.run_until_complete(self.bcp_client.wait_for_response("vpcom_bridge_response")) 159 | 160 | return True 161 | 162 | def Stop(self): 163 | if self.bcp_client: 164 | self.bcp_client.send("vpcom_bridge", {"subcommand": "stop"}) 165 | self.loop.run_until_complete(self.bcp_client.wait_for_response("vpcom_bridge_response")) 166 | self.bcp_client = None 167 | 168 | sys.exit(1) 169 | 170 | def Games(self, rom_name): 171 | """Return the IGames interface, by wrapping the object.""" 172 | del rom_name 173 | games = IGames() 174 | wrapped_games = wrap (games) 175 | return wrapped_games 176 | 177 | def SetGames(self, rom_name): 178 | """Return the IGames interface, by wrapping the object.""" 179 | del rom_name 180 | games = IGames() 181 | wrapped_games = wrap (games) 182 | return wrapped_games 183 | 184 | def _raise_error(self, desc): 185 | raise COMException(desc=desc, scode=winerror.E_FAIL) 186 | 187 | def _dispatch_to_mpf(self, command, **params): 188 | """Dispatch to MPF and wait for result.""" 189 | params["subcommand"] = command 190 | try: 191 | self.bcp_client.send("vpcom_bridge", params) 192 | response = self.loop.run_until_complete(self.bcp_client.wait_for_response("vpcom_bridge_response")) 193 | response_data = response[1] 194 | if "error" in response_data: 195 | self._raise_error(desc="MPF reported an error as response to command {} ({}): {}".format( 196 | command, params, response_data["error"])) 197 | if "result" not in response_data: 198 | self._raise_error(desc="MPF did not return a result {} ({}): {}".format( 199 | command, params, response_data)) 200 | return response_data["result"] 201 | except Exception as e: 202 | self._raise_error(desc="Failed to communicate with MPF at command {} ({}): {}".format( 203 | command, params, e)) 204 | 205 | def Switch(self, number): 206 | """Return the current value of the requested switch.""" 207 | return self._dispatch_to_mpf("switch", number=number) 208 | 209 | def GetSwitch(self, number): 210 | """Return the current value of the requested switch.""" 211 | return self._dispatch_to_mpf("get_switch", number=number) 212 | 213 | def SetSwitch(self, number, value): 214 | """Set the value of the requested switch.""" 215 | return self._dispatch_to_mpf("set_switch", number=number, value=value) 216 | 217 | def PulseSW(self, number): 218 | """Pulse Switch.""" 219 | return self._dispatch_to_mpf("pulsesw", number=number) 220 | 221 | def Mech(self, number): 222 | """Currently unused.""" 223 | return self._dispatch_to_mpf("mech", number=number) 224 | 225 | def SetMech(self, number, value): 226 | """Currently unused.""" 227 | return self._dispatch_to_mpf("set_mech", number=number, value=value) 228 | 229 | def GetMech(self, number): 230 | """Currently unused.""" 231 | return self._dispatch_to_mpf("get_mech", number=number) 232 | 233 | def ChangedSolenoids(self): 234 | """Return a list of changed coils.""" 235 | return self._dispatch_to_mpf("changed_solenoids") 236 | 237 | def ChangedLamps(self): 238 | """Return a list of changed lamps.""" 239 | return self._dispatch_to_mpf("changed_lamps") 240 | 241 | def ChangedGIStrings(self): 242 | """Return a list of changed GI strings.""" 243 | return self._dispatch_to_mpf("changed_gi_strings") 244 | 245 | def ChangedLEDs(self): 246 | """Return a list of changed lamps.""" 247 | return self._dispatch_to_mpf("changed_leds") 248 | 249 | def ChangedFlashers(self): 250 | """Return a list of changed GI strings.""" 251 | return self._dispatch_to_mpf("changed_flashers") 252 | 253 | def HardwareRules(self): 254 | """Return a list of MPF Hardware Rules for autofire coils.""" 255 | return self._dispatch_to_mpf("get_hardwarerules") 256 | 257 | def IsCoilActive(self, number): 258 | """Return True if a MPF Hardware Rule for the coil(number) exists.""" 259 | return self._dispatch_to_mpf("get_coilactive", number=number) 260 | 261 | 262 | def Register(pyclass=Controller, p_game=None): 263 | """ Registration code for the Visual Pinball COM interface for pyprocgame.""" 264 | pythoncom.CoInitialize() 265 | from win32com.server.register import UseCommandLine 266 | UseCommandLine(pyclass) 267 | 268 | # Run the registration code by default. Using the commandline param 269 | # "--unregister" will unregister this COM object. 270 | if __name__=='__main__': 271 | if not win32com: 272 | raise AssertionError("Please run: pip3 install pywin32") 273 | Register(Controller) 274 | -------------------------------------------------------------------------------- /mpf_vpcom_bridge/main.py: -------------------------------------------------------------------------------- 1 | """Brige between virtual pinball and MPF. 2 | 3 | Based on proc-visual-pinball of destruk, Gerry Stellenberg, Adam Preble and Michael Ocean. 4 | """ 5 | import asyncio 6 | import sys 7 | import logging 8 | 9 | 10 | try: 11 | import win32com 12 | import win32com.server.util 13 | from win32com.server.util import wrap, unwrap 14 | import pythoncom 15 | from win32com.server.exception import COMException 16 | import winerror 17 | except ImportError: 18 | win32com = None 19 | util = None 20 | wrap = None 21 | unwrap = None 22 | pythoncom = None 23 | COMException = AssertionError 24 | winerror = None 25 | 26 | from mpf.core.bcp.bcp_socket_client import AsyncioBcpClientSocket 27 | 28 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 29 | filename="mpf-vpcom-bridge.txt") 30 | 31 | 32 | class ISettings: 33 | _public_methods_ = [] 34 | _public_attrs_ = [ 'Value'] 35 | 36 | def Value(self, item, item2): 37 | del item 38 | del item2 39 | return True 40 | 41 | def SetValue(self, item, item2): 42 | del item 43 | del item2 44 | return True 45 | 46 | 47 | class IGames: 48 | _public_methods_ = [] 49 | _public_attrs_ = [ 'Settings'] 50 | 51 | def Settings(self): 52 | settings = ISettings() 53 | Settings = wrap( settings ) 54 | return Settings 55 | 56 | def SetSettings(self): 57 | settings = ISettings() 58 | Settings = wrap( settings ) 59 | return Settings 60 | 61 | 62 | class Controller: 63 | 64 | """Main Visual Pinball COM interface class.""" 65 | 66 | _public_methods_ = [ 67 | 'Run', 68 | 'Stop', 69 | 'PrintGlobal', 70 | 'PulseSW', 71 | 'IsCoilActive' 72 | ] 73 | _reg_progid_ = "MPF.Controller" # Visual MPF Controller 74 | _reg_clsid_ = "{196FF002-17F9-4714-4242-A7BD39AD413B}" # use a unique class guid for Visual MPF Controller 75 | _public_attrs_ = [ 76 | 'Version', 77 | 'GameName', 78 | 'Games', 79 | 'SplashInfoLine', 80 | 'ShowTitle', 81 | 'ShowFrame', 82 | 'ShowDMDOnly', 83 | 'HandleMechanics', 84 | 'HandleKeyboard', 85 | 'DIP', 86 | 'Switch', 87 | 'Mech', 88 | 'Pause', 89 | 'ChangedSolenoids', 90 | 'ChangedGIStrings', 91 | 'ChangedLamps', 92 | 'ChangedLEDs', 93 | 'ChangedBrightnessLEDs', 94 | 'ChangedFlashers', 95 | 'HardwareRules', 96 | 'GetMech' 97 | ] 98 | 99 | _readonly_attrs_ = [ 100 | 'Version', 101 | 'ChangedSolenoids', 102 | 'ChangedLamps', 103 | 'ChangedGIStrings', 104 | 'ChangedLEDs', 105 | 'ChangedBrightnessLEDs', 106 | 'ChangedFlashers', 107 | 'HardwareRules', 108 | 'GetMech' 109 | ] 110 | 111 | Version = "22222222" 112 | ShowTitle = None 113 | ShowFrame = False 114 | ShowDMDOnly = False 115 | HandleKeyboard = False 116 | DIP = False 117 | GameName = "Game name" 118 | 119 | Pause = None 120 | 121 | HandleMechanics = True 122 | ErrorMsg = "Python Failed -- check the log" 123 | 124 | def __init__(self, *, loop=None): 125 | self._reg_clsctx_ = pythoncom.CLSCTX_LOCAL_SERVER # LocalSever (no InProc) only means game reloads entirely 126 | self.bcp_client = None 127 | self.last_switch = None 128 | self.switches = {} 129 | if not loop: 130 | self.loop = asyncio.get_event_loop() 131 | else: 132 | self.loop = loop 133 | 134 | # Need to overload this method to tell that we support IID_IServerWithEvents 135 | def _query_interface_(self, iid): 136 | """ Return this main interface if the IController class is queried. """ 137 | IID_IController = pythoncom.MakeIID('{CE9ECC7C-960F-407E-B27B-62E39AB1E30F}') 138 | if iid == IID_IController: 139 | return win32com.server.util.wrap(self) 140 | 141 | def PrintGlobal(self): 142 | """Unused.""" 143 | logging.getLogger('vpcom').info("PrintGlobal called.") 144 | return True 145 | 146 | def _connect(self, addr, port): 147 | """Connect to MPF.""" 148 | try: 149 | reader, writer = self.loop.run_until_complete(asyncio.open_connection(addr, port)) 150 | self.bcp_client = AsyncioBcpClientSocket(writer, reader) 151 | except Exception as e: 152 | raise COMException(desc="Failed to connect to MPF: {}".format(e), scode=winerror.E_FAIL) 153 | 154 | def Run(self, addr="localhost", port=5051): 155 | """Connect to MPF.""" 156 | logging.getLogger('vpcom').info("Starting bridge. Connecting to {}:{}".format(addr,port)) 157 | 158 | self._connect(addr, port) 159 | 160 | self.bcp_client.send("vpcom_bridge", {"subcommand": "start"}) 161 | self.loop.run_until_complete(self.bcp_client.wait_for_response("vpcom_bridge_response")) 162 | 163 | return True 164 | 165 | def Stop(self): 166 | if self.bcp_client: 167 | self.bcp_client.send("vpcom_bridge", {"subcommand": "stop"}) 168 | self.loop.run_until_complete(self.bcp_client.wait_for_response("vpcom_bridge_response")) 169 | self.bcp_client = None 170 | 171 | sys.exit(1) 172 | 173 | def Games(self, rom_name): 174 | """Return the IGames interface, by wrapping the object.""" 175 | del rom_name 176 | games = IGames() 177 | wrapped_games = wrap (games) 178 | return wrapped_games 179 | 180 | def SetGames(self, rom_name): 181 | """Return the IGames interface, by wrapping the object.""" 182 | del rom_name 183 | games = IGames() 184 | wrapped_games = wrap (games) 185 | return wrapped_games 186 | 187 | def _raise_error(self, desc): 188 | raise COMException(desc=desc, scode=winerror.E_FAIL) 189 | 190 | def _dispatch_to_mpf(self, command, **params): 191 | """Dispatch to MPF and wait for result.""" 192 | params["subcommand"] = command 193 | try: 194 | self.bcp_client.send("vpcom_bridge", params) 195 | response = self.loop.run_until_complete(self.bcp_client.wait_for_response("vpcom_bridge_response")) 196 | response_data = response[1] 197 | if "error" in response_data: 198 | self._raise_error(desc="MPF reported an error as response to command {} ({}): {}".format( 199 | command, params, response_data["error"])) 200 | if "result" not in response_data: 201 | self._raise_error(desc="MPF did not return a result {} ({}): {}".format( 202 | command, params, response_data)) 203 | return response_data["result"] 204 | except Exception as e: 205 | self._raise_error(desc="Failed to communicate with MPF at command {} ({}): {}".format( 206 | command, params, e)) 207 | 208 | def Switch(self, number): 209 | """Return the current value of the requested switch.""" 210 | return self._dispatch_to_mpf("switch", number=number) 211 | 212 | def GetSwitch(self, number): 213 | """Return the current value of the requested switch.""" 214 | return self._dispatch_to_mpf("get_switch", number=number) 215 | 216 | def SetSwitch(self, number, value): 217 | """Set the value of the requested switch.""" 218 | return self._dispatch_to_mpf("set_switch", number=number, value=value) 219 | 220 | def PulseSW(self, number): 221 | """Pulse Switch.""" 222 | return self._dispatch_to_mpf("pulsesw", number=number) 223 | 224 | def Mech(self, number): 225 | """Currently unused.""" 226 | return self._dispatch_to_mpf("mech", number=number) 227 | 228 | def SetMech(self, number, value): 229 | """Currently unused.""" 230 | return self._dispatch_to_mpf("set_mech", number=number, value=value) 231 | 232 | def GetMech(self, number): 233 | """Currently unused.""" 234 | return self._dispatch_to_mpf("get_mech", number=number) 235 | 236 | def ChangedSolenoids(self): 237 | """Return a list of changed coils.""" 238 | return self._dispatch_to_mpf("changed_solenoids") 239 | 240 | def ChangedLamps(self): 241 | """Return a list of changed lamps.""" 242 | return self._dispatch_to_mpf("changed_lamps") 243 | 244 | def ChangedGIStrings(self): 245 | """Return a list of changed GI strings.""" 246 | return self._dispatch_to_mpf("changed_gi_strings") 247 | 248 | def ChangedLEDs(self): 249 | """Return a list of changed lamps.""" 250 | return self._dispatch_to_mpf("changed_leds") 251 | 252 | def ChangedBrightnessLEDs(self): 253 | """Return a list of changed LEDs with brightness vlaues as floats.""" 254 | return self._dispatch_to_mpf("changed_brightness_leds") 255 | 256 | def ChangedFlashers(self): 257 | """Return a list of changed GI strings.""" 258 | return self._dispatch_to_mpf("changed_flashers") 259 | 260 | def HardwareRules(self): 261 | """Return a list of MPF Hardware Rules for autofire coils.""" 262 | return self._dispatch_to_mpf("get_hardwarerules") 263 | 264 | def IsCoilActive(self, number): 265 | """Return True if a MPF Hardware Rule for the coil(number) exists.""" 266 | return self._dispatch_to_mpf("get_coilactive", number=number) 267 | 268 | 269 | def Register(pyclass=Controller, p_game=None): 270 | """ Registration code for the Visual Pinball COM interface for pyprocgame.""" 271 | pythoncom.CoInitialize() 272 | from win32com.server.register import UseCommandLine 273 | UseCommandLine(pyclass) 274 | 275 | 276 | # Run the registration code by default. Using the commandline param 277 | # "--unregister" will unregister this COM object. 278 | 279 | def main(): 280 | if not win32com: 281 | raise AssertionError("Please run: pip3 install pywin32") 282 | Register(Controller) --------------------------------------------------------------------------------