-
Notifications
You must be signed in to change notification settings - Fork 2
/
osc.py
269 lines (217 loc) · 12.6 KB
/
osc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# This is a modified version of OSC Mode that supports sending lamp updates to the graphical
# OSC client. This has been modified from an earlier version written by Brian Madden.
# Unfortunately, this version has not been generated from Brian's latest version, and as
# such some functionality from his latest is likely to be missing in this version.
# Coupled with the latest Graphical OSC 'switchMatrixClient' you can layout lamps on an
# image of the playfield and watch lamp updates during FakePinPROC (or live) machine testing.
# This version will respond to an OSC request of /lamps/get and it will return OSC messagess
# consisting for each of the lamps and their respective state (0=off, 1=on).
# This is NOT an efficent way to do this. I will, eventually, provide an OSC message to
# specify the lamp ordering so the reply message can be a single message with the state of
# all lamps (and drastically reduce overhead), but this is enough to get people started... I hope.
# This code is released under the MIT License.
# Portions unique to this version are Copyright (c) 2015 Michael Ocean
############
# An OSC-based interface for controlling pyprocgame with OSC devices
# for pyprocgame, a Python-based pinball software development framework
# for use with P-ROC written by Adam Preble and Gerry Stellenberg
# More information is avaible at http://pyprocgame.pindev.org/
# and http://pinballcontrollers.com/
# More info on OSC at http://opensoundcontrol.org/
# This OSC interface was written by Brian Madden
# Version ?.?.? - ???
# This code is released under the MIT License.
#The MIT License (MIT)
#Copyright (c) 2013-2014 Brian Madden
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
# This code requires pyOSC, https://trac.v2.nl/wiki/pyOSC
# It was written for pyOSC 0.3.5b build 5394,
# though I would expect later versions should work
# It also requires that the "desktop" mode is enabled in pyprocgame,
# and it requires some changes to procgame/game/game.py
# See http://www.pinballcontrollers.com/forum for more information
import OSC
import socket
import threading
import pinproc
import procgame
from procgame.game import Mode
class OSC_Mode(Mode):
"""This is the awesome OSC interface. A few parameters:
game - game object
priority - game mode priority. It doesn't really matter for this mode.
serverIP - the IP address the OSC server will listen on. If you don't pass it anything it will use the default IP address of your computer which should be fine
serverPort - the UDP port the server will listen on. Default 9000
clientIP - the IP address of the client you'd like to connect to. Leave it blank and it will automatically connect to the first client that contacts it
clientPort - the client UDP port. Default is 8000
closed_switches - a list of switch names that you'd like to have set "closed" by default. Good for troughs and stuff. Maybe use some logic here so they're only set to closed with fakepinproc?
"""
def __init__(self, game, priority, serverIP=None, serverPort=9000, clientIP = None, clientPort = 8000, closed_switches=[]):
super(OSC_Mode, self).__init__(game, priority)
self.serverPort = serverPort
self.clientPort = clientPort
self.closed_switches = closed_switches
if not serverIP:
self.serverIP = socket.gethostbyname(socket.gethostname())
else:
self.serverIP = serverIP
self.clientIP = clientIP
self.client_needs_sync = False
self.do_we_have_a_client = False
def mode_started(self):
receive_address = (self.serverIP, self.serverPort) # create a tuple from the IP & UDP port
self.server = OSC.OSCServer(receive_address)
self.server.addDefaultHandlers()
self.server.addMsgHandler("/lamps/get", self.PROC_OSC_lamp_handler)
self.server.addMsgHandler("default", self.PROC_OSC_message_handler)
# start the OSC server
self.game.logger.info("OSC Server listening on %s:%s", self.serverIP, self.serverPort)
self.last_lamp_states = []
self.server_thread = threading.Thread(target=self.server.serve_forever)
self.server_thread.start()
self.set_initial_switches()
pass
def dumpLamps(self):
""" Return a list of changed lamps. """
lamps = self.getLampStates()
changedLamps = []
if len(self.last_lamp_states) > 0:
for i in range(0,len(lamps)):
if lamps[i] != self.last_lamp_states[i]:
changedLamps += [(i,lamps[i])]
self.last_lamp_states = lamps
#print("LAMPS: %s" % changedLamps)
self.delay(name="printer", delay=.03, handler=self.dumpLamps)
#return changedLamps
def getLampStates(self):
""" Gets the current state of the lamps.
Shamelessly ripped from the VPCOM project.
"""
vplamps = [False]*90
for i in range(0,64):
vpNum = (((i/8)+1)*10) + (i%8) + 1
vplamps[vpNum] = self.game.proc.drivers[i+80].curr_state
return vplamps
def mode_stopped(self):
self.OSC_shutdown()
def OSC_shutdown(self):
"""Shuts down the OSC Server thread. If you don't do this python will hang when you exit the game."""
self.server.close()
self.game.logger.info("Waiting for the OSC Server thread to finish")
self.server_thread.join()
self.game.logger.info("OSC Server thread is done.")
def PROC_OSC_lamp_handler(self, addr, tags, data, client_address):
#print("GOING THROUGH LAMP HANDLER %s " % addr)
self.sync_client_lamps()
def PROC_OSC_message_handler(self, addr, tags, data, client_address):
""" receives OSC messages and acts on them by setting switches."""
#print("GOING THROUGH DEFAULT HANDLER %s " % addr)
# print("recvd OSC message " + str(addr) +" tags:"+ str(tags) +" data:"+ str(data))
# need to add supprt for "sync" instruction which we'll call when switching tabs in touchOSC
#strip out the switch name
switchname = addr.split("/")[-1] # This is the OSC address, not the IP address. Strip out the characters after the final "/"
if switchname in self.game.switches:
switch_number = self.game.switches[switchname].number
#print("switch_number is local -> %d" % switch_number)
else:
switch_number = pinproc.decode(self.game.machine_type, switchname)
#print("switch_number is lookedup -> %d" % switch_number)
# I'm kind of cheating by using desktop.key_events here, but I guess this is ok?
if data[0] == 1.0: # close the switch
self.game.desktop.key_events.append({'type': pinproc.EventTypeSwitchClosedDebounced, 'value': switch_number})
elif data[0] == 0.0: # open the switch
self.game.desktop.key_events.append({'type': pinproc.EventTypeSwitchOpenDebounced, 'value': switch_number})
# since we just got a message from a client, let's set up a connection to it
if not self.do_we_have_a_client:
if not self.clientIP: # if a client IP wasn't specified, use the one that just communicated with us now
self.clientIP = client_address[0]
self.clientTuple = (self.clientIP, self.clientPort)
# print(self.clientTuple)
# print("OSC attempting connection back to client")
self.OSC_client = OSC.OSCClient()
self.OSC_client.connect(self.clientTuple)
self.do_we_have_a_client = True
else:
# print("We do have a client...")
pass
if(self.do_we_have_a_client):
# print("OSC sending back back to client")
OSC_message = OSC.OSCMessage()
OSC_message.setAddress(addr)
OSC_message.append("OK")
self.OSC_client.send(OSC_message)
def sync_client(self, OSC_branch=1):
""" Read through all the current switch states and updates the client to set the default states on the client.
Since we don't know whether the client has momentary or toggle switches, we just have to update all of them.
"""
for switch in self.game.switches:
status = 0.0 # set the status to 'off'
if switch.state:
status = 1.0 # if the switch.state is 'True', the switch is closed
self.update_client_switch(switch.name, status, OSC_branch)
self.client_needs_sync = False # since the sync is done we reset the flag
def sync_client_lamps(self):
""" Guess..
"""
#print("===================sync_client_lamps===================")
for lamps in self.game.lamps:
status = 0.0 # set the status to 'off'
if hasattr(self.game.proc.drivers[lamps.number], 'curr_state'):
if self.game.proc.drivers[lamps.number].curr_state:
status = 1.0 # if the switch.state is 'True', the switch is closed
elif self.game.proc.driver_get_state(lamps.number):
#self.game.proc.drivers[lamps.number].state().state:
status = 1.0
#print("/lamps/%s/%d" % (lamps.name,status))
self.update_client_switch(lamps.name,status,"lamps")
def update_client_switch(self, switch_name, status, OSC_branch=1):
"""update the client switch states.
Parameters
swtich_name - the procgame switch name
status - closed = 1, open = 0
OSC_branch - what OSC branch do you want? For TouchOSC, this defaults to the "tab"
The screen is the /1/ or /2/ or whatever part of the OSC address
"""
if self.do_we_have_a_client: # only do this if we have a client
# Let's build a message. For example OSC address "/1/switchname" with data "1"
self.OSC_message = OSC.OSCMessage()
self.OSC_message.setAddress("/" + str(OSC_branch) + "/" + switch_name)
self.OSC_message.append(status)
self.OSC_client.send(self.OSC_message)
else: # we don't have a client?
pass
self.do_we_have_a_client = False
#self.delay(name="printer", delay=.03, handler=self.sync_client)
def set_initial_switches(self):
"""sets up the initial switches that should be closed, then marks the client to sync
Should I add some logic here to only do this with fakepinproc?
"""
if ('pinproc_class' in procgame.config.values and
procgame.config.values['pinproc_class'] == 'procgame.fakepinproc.FakePinPROC'):
for switchname in self.closed_switches: # run through the list of closed_switches passed to the mode as args
if switchname in self.game.switches: # convert the names to switch numbers
switch_number = self.game.switches[switchname].number
else:
switch_number = pinproc.decode(self.game.machine_type, switchname)
self.game.desktop.key_events.append({'type': pinproc.EventTypeSwitchClosedDebounced, 'value': switch_number}) # add these switch close events to the queue
self.client_needs_sync = True # Now that this is done we set the flag to sync the client
# we use the flag because if we just did it now it's too fast. The game loop hasn't read in the new closures yet
def mode_tick(self):
"""performs a client sync if we need it"""
if self.do_we_have_a_client: # only proceed if we've establish a connection with a client
if self.client_needs_sync: # if the client is out of sync, then sync it
self.sync_client()