The sound player component will hold the sound data on-heap once you pass it some soundData property; in your case, via the runScript binding. This is exactly equivalent to using system.util.globals - it's on heap durable storage. So basically exactly the same performance characteristics. Except, I'd argue, potentially less steps involved.
I would probably approach this with something like:
Gateway
def handleMessage(payload):
from java.nio.file import Files, Path
folder = "whatever"
fileStream = Files.list(Path.of(folder))
try:
return {
str(path.fileName): system.file.readFileAsBytes(str(path))
for path in fileStream.toList()
if str(path.fileName).upper().endswith('.WAV')
}
finally:
fileStream.close()
We use a network share on the gateway to hold all of the wav files.
Then we use system.util.sendMessage from scripts on the gateway to send the file name, to the client designated as the machine to play the WAV on for that alarm area.
It is a little more complex due to us having multiple different production areas at each plant with their own alarms and what we call "voice alarms"
Scripts on the gateway will scan alarms as they come active, see if there is an assigned voice alarm for that alarm, then put it in a dataset using FIFO for the system to send the message to the clients.
The gateway watches that dataset for records, sends the record to the client(s), and deletes the row from the dataset.
So if I'm understanding, what you're mainly doing differently is that you're caching all the sounds locally (and refreshing the list "on demand" somewhere if they want to get the latest sounds), then just passing the bytes from memory rather than hitting the gateway for them?
Well, and you're using the scripted function/event to play the audio once, whereas I want ours to continuously loop until the alarm goes away.
Are the clients loading the sound file from the network share or via a sendMessage? I guess that works, but I would think the sendMessage method would be more cross-platform capable and doesn't require a network share at all (so maybe a bit more "secure" as I like to keep anything I don't really need turned off/disabled).
It sends the file name and path, the clients load them from the share on the gateway. Nothing stored in memory, the config is basically just a table with the alarm path, client to play on, wav file path and name.
Stored procedure that accepts the alarm path as the parameter, returns the client, and wav file path.
Nice little vision window for configuring it in runtime.
Keeps it simple in that it is only one location for the files.
We then also can copy the generic wav files to different plants easier.
60+ gateways I tend to try and keep it as simple as possible.
Paul's dissertation in #21 is pretty much what I'd do in this situation. I'd probably add a file watcher to that folder to auto-convert any MP3 dropped into that gateway folder, with a broadcast to clients to refresh their cache.
But then, I don't rely on any PC-based client to play alarm sounds. That's a job for dedicated hardware attached to the PLC.
I used to use alarm horns, but on all the big gas plants I work on anymore, the operators are in a cushy control room away from the plant and the only audible plant horn is for ESD/Evacuations, and tornados (although they do have a trigger for standard alarms, they always keep it turned off unless everyone's leaving the control room). Plus they tend to like some "alternative" sounds at the plants like Michael Scott from the office yelling "No God, No!", or the most annoying sound in the world from Dumb and Dumber. There are a few others, but those 2 seem to be the favorites to get their attention yet make their work lives a little more enjoyable.
The other 2 we have right now are the dying sounds from Mario Bros and PacMan, plus just a standard alarm sound that's your boring typical alarm sound.
Trying to do something very similar, but in my own twisted way.
Here's the basic steps.
Client requests the available sounds from the gateway (as a list)
Client uses this list, and for each sound that is available:
a. Get the sound file in bytes from the gateway
b. Write the sound file to the client
I've got all scripts in my project library Sound:
import os
import errno
alarmSoundPath = 'C:\SCADA\Alarm Sounds'
alarmSoundOptionsTagPath = '[client]Alarms/AlarmSoundOptions'
def alarmSoundPathExists():
'''
Checks to see if the alarmSoundPath folder exists. Creates it if it does not.
Args:
n/a
Returns
folder (bool) : true if file path exists, or was successfully created
'''
path = alarmSoundPath
#check to see that file path exists
folder = False
if not os.path.exists(path):
#create the folder if it doesn't exist
try:
os.makedirs(path)
folder = True
except OSError as e:
if errno.EEXIST != e.errno:
raise
#folder exists, continue
else:
folder = True
return folder
def getAlarmSoundList():
'''
Get a list of alarm sounds that are available at the alarmSoundPath
Args:
n/a
Returns:
fileList (string list) : list of WAV file names in the directory
'''
path = alarmSoundPath
#check to see that file path exists
folder = alarmSoundPathExists()
#create the file list for return
if folder:
fileList = [ name.split('.')[0] for name in os.listdir(path) if name.upper().endswith('.WAV') ]
else:
fileList = []
return fileList
def getAlarmSoundDataset():
'''
Get a dataset of alarm sounds that are available at the alarmSoundPath
Args:
n/a
Returns:
dataset (dataset) : file names as dataset for use with a dropdown
'''
path = alarmSoundPath
headers = ['Value','Label']
#get file list
fileList = getAlarmSoundList()
#need a list of lists to match the headers
data = [[i, name] for i, name in enumerate(fileList)]
#return dataset
return system.dataset.toDataSet(headers, data)
##################################################################################
############ functions for clients requesting sounds from the gateway ############
##################################################################################
def requestAvailableAlarmSounds():
'''
Request the list of available sounds from the gateway via message handler
Args:
n/a
Returns:
fileList (string list) : list of WAV file names in the directory
'''
return system.util.sendRequest('Project', 'SendAlarmSoundList')
def retrieveSoundBytes(fileName):
'''
Get a sound file from the gateway via message handler
Args:
fileName (string) : alarm sound name to retrieve
Returns:
soundBytes (byte array) : sound file in byte format
'''
return system.util.sendRequest('Project', 'SendAlarmSound', {'fileName':fileName})
def writeSoundBytes(filePath, soundBytes):
'''
Write sound file to the local client at the alarmSoundPath
Args:
filePath (string) : file to create
soundBytes (byte array) : sound file in byte format
Returns:
n/a
'''
#check to see that file path exists
folder = alarmSoundPathExists()
if folder:
system.file.writeFile(filePath, soundBytes)
def updateClientAlarmSounds():
'''
Update alarm sound files on the local client
'''
path = alarmSoundPath
tagPath = alarmSoundOptionsTagPath
fileList = requestAvailableAlarmSounds()
for fileName in fileList:
filePath = path + '\\' + fileName + '.wav'
soundBytes = retrieveSoundBytes(fileName)
writeSoundBytes(filePath, soundBytes)
#update dataset for options dropdown
dataset = getAlarmSoundDataset()
system.tag.writeBlocking([tagPath], [dataset])
##################################################################################
####### functions for the gateway responding to client requests for sounds #######
##################################################################################
def retrieveAlarmSoundList_MH():
'''
Message handler to retrieve the list of available sound files on the gateway
Args:
n/a
Returns:
fileList (string list) : list of WAV file names in the directory
'''
return getAlarmSoundList()
def retrieveAlarmSound_MH(payload):
'''
Message handler to retrieve sound file from the gateway
Args:
payload (dict) : payload 'fileName' for alarm sound file
Returns:
soundBytes (byte array) : sound file in byte format
'''
path = alarmSoundPath
if payload['fileName']:
filePath = path + '\\' + payload['fileName'] + '.wav'
if system.file.fileExists(filePath):
return system.file.readFileAsBytes(filePath)
Gateway Message Handlers: SendAlarmSound
return Sound.retrieveAlarmSound_MH(payload)
SendAlarmSoundList
return Sound.retrieveAlarmSoundList_MH()
Client Tag Change Script (so I can trigger it at-will):
Sound.updateClientAlarmSounds()
It's not populating. Where might I see some error messages for these?
In a few places, you're using double backslashes, but in the alarmSoundPath variable, you're not. I don't recall if you need the double backslashes, but my guess would be the inconsistencies could be breaking it. Also, I would add some logging throughout and monitor the client and server logs to see what it's seeing.
Use case in this example is one sound for an andon call (need components, call team leader, etc) and one as an alarm when a barcode doesn't match.
Two examples given for loading wav data from URLs or from file locations. Both load into the globals as a dictionary with keys 'call' and 'scan'
Starting a sound will use start('call') or start('scan')
Stopping a sound will use stop('call') or stop('scan'). Using stop() with no parameter will stop all clips.
from java.io import ByteArrayInputStream
from java.net import URL
from javax.sound.sampled import AudioSystem
from javax.sound.sampled import AudioInputStream
from javax.sound.sampled import Clip
def loadUrls():
''' Load a set of wav urls to the global dictionary '''
urlDict = {'call': 'http://192.168.140.35/music/tv/GalaxyQuest.wav',
'scan': 'http://192.168.140.35/music/tv/scan_alert_short.wav'
}
audioDict = {}
for key, value in urlDict.items():
AudioClip = AudioSystem.getClip()
audioStream = AudioSystem.getAudioInputStream(URL(value))
AudioClip.open(audioStream)
audioDict[key] = AudioClip
system.util.globals['audio'] = audioDict
def loadFiles():
''' Load a set of wav files to the global dictionary '''
fileDict = {'call': '/mnt/d/music/Andon music/GalaxyQuest.wav',
'scan': '/mnt/d/music/Andon music/scan_alert_short.wav'
}
audioDict = {}
for key, value in fileDict.items():
AudioClip = AudioSystem.getClip()
inputStream = ByteArrayInputStream(system.file.readFileAsBytes(value))
audioStream = AudioSystem.getAudioInputStream(inputStream)
AudioClip.open(audioStream)
audioDict[key] = AudioClip
system.util.globals['audio'] = audioDict
def start(sound):
system.util.globals['audio'][sound].setMicrosecondPosition(0)
system.util.globals['audio'][sound].loop(Clip.LOOP_CONTINUOUSLY)
def stop(sound = None):
if sound is not None:
system.util.globals['audio'][sound].stop()
else:
for key in system.util.globals['audio'].keys():
system.util.globals['audio'][key].stop()