diff --git a/.idea/misc.xml b/.idea/misc.xml index 223c63a..62b543c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/config.json b/config.json new file mode 100644 index 0000000..c3166d8 --- /dev/null +++ b/config.json @@ -0,0 +1,16 @@ +{ + "ZelloUsername": "gtcdevice01", + "ZelloPassword": "GtcDev2025", + "ZelloChannel": "GtcDev2025", + "ZelloServer": "community", + "ZelloWorkNetworkName": "", + "ZelloEnterpriseServerDomain": "", + "M1": "", + "M2": "", + "M3": "", + "M4": "", + "M5": "", + "M6": "", + "M7": "", + "M8": "" + } \ No newline at end of file diff --git a/html/assets/js/pocreceiver.js b/html/assets/js/pocreceiver.js index bece86c..d1915e5 100644 --- a/html/assets/js/pocreceiver.js +++ b/html/assets/js/pocreceiver.js @@ -9,7 +9,7 @@ $(document).ready(function() { $('#indicatorDisconnected').addClass('visually-hidden'); $('#indicatorConnected').removeClass('visually-hidden'); setInterval(function() { - ws.send(JSON.stringify({ command: "getZelloStatus" })); + sendCommand({ command: "getZelloStatus" }); }, 5000); }; @@ -39,4 +39,12 @@ $(document).ready(function() { ws.onerror = function(error) { console.error('WebSocket error:', error); }; + + function sendCommand(command) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(command)); + } else { + console.error('WebSocket is not open. Unable to send command:', command); + } + } }); \ No newline at end of file diff --git a/html/assets/js/prerecordedbroadcast.js b/html/assets/js/prerecordedbroadcast.js index 023091d..4ccc430 100644 --- a/html/assets/js/prerecordedbroadcast.js +++ b/html/assets/js/prerecordedbroadcast.js @@ -79,7 +79,7 @@ $(document).ready(function() { function sendCommand(cmd) { if (ws.readyState === WebSocket.OPEN) { - ws.send(cmd); + ws.send(JSON.stringify(cmd)); } else { console.error('WebSocket is not open. Unable to send command:', JSON.stringify(cmd)); } diff --git a/html/assets/js/setting.js b/html/assets/js/setting.js index 3ee2af6..0241a02 100644 --- a/html/assets/js/setting.js +++ b/html/assets/js/setting.js @@ -40,23 +40,23 @@ $(document).ready(function() { return; } if (msg.reply === "getConfig" && msg.data !== undefined ) { - const configData = msg.data; + const configData = JSON.parse(msg.data); console.log('Config Data:', configData); - $('#zelloUsername').val(configData.ZelloUsername || ''); - $('#zelloPassword').val(configData.ZelloPassword || ''); - $('#zelloChannel').val(configData.ZelloChannel || ''); - if ("community" === configData.ZelloServer) { + $('#zelloUsername').val(configData.zelloUsername || ''); + $('#zelloPassword').val(configData.zelloPassword || ''); + $('#zelloChannel').val(configData.zelloChannel || ''); + if ("community" === configData.zelloServer) { $('#zellocommunity').prop('checked', true); $('#zelloWorkNetworkName').val('').prop('disabled', true); $('#zelloEnterpriseServerDomain').val('').prop('disabled', true); - } else if ("work" === configData.ZelloServer) { + } else if ("work" === configData.zelloServer) { $('#zellowork').prop('checked', true); - $('#zelloWorkNetworkName').val(configData.ZelloWorkNetworkName || '').prop('disabled', false); + $('#zelloWorkNetworkName').val(configData.zelloWorkNetworkName || '').prop('disabled', false); $('#zelloEnterpriseServerDomain').val('').prop('disabled', true); - } else if ("enterprise" === configData.ZelloServer) { + } else if ("enterprise" === configData.zelloServer) { $('#zelloenterprise').prop('checked', true); $('#zelloWorkNetworkName').val('').prop('disabled', true); - $('#zelloEnterpriseServerDomain').val(configData.ZelloEnterpriseServerDomain || '').prop('disabled', false); + $('#zelloEnterpriseServerDomain').val(configData.zelloEnterpriseServerDomain || '').prop('disabled', false); } for (let i = 1; i <= 8; i++) { @@ -72,8 +72,8 @@ $(document).ready(function() { dropdownMenu.append(item); }); // Set button text to selected message if present - if (configData[`M${i}`]) { - dropdownButton.text(configData[`M${i}`]); + if (configData[`m${i}`]) { + dropdownButton.text(configData[`m${i}`]); } else { dropdownButton.text(''); } @@ -176,7 +176,7 @@ $(document).ready(function() { function sendCommand(cmd) { if (ws.readyState === WebSocket.OPEN) { - ws.send(cmd); + ws.send(JSON.stringify(cmd)); } else { console.error('WebSocket is not open. Unable to send command:', JSON.stringify(cmd)); } diff --git a/src/Main.kt b/src/Main.kt index ee06f47..d1cf58b 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -1,13 +1,28 @@ +import audio.AudioFilePlayer import audio.AudioUtility import audio.OpusStreamReceiver +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper import somecodes.Codes.Companion.ValidString +import somecodes.configFile +import web.WsReply import web.webApp import zello.ZelloClient import zello.ZelloEvent +import javafx.util.Pair +import org.slf4j.LoggerFactory +import somecodes.Codes.Companion.ValidFile +import web.WsCommand +import java.util.function.BiFunction //TIP To Run code, press or // click the icon in the gutter. fun main() { + val logger = LoggerFactory.getLogger("Main") + val objectMapper = ObjectMapper() + + val cfg = configFile() + cfg.Load() val au = AudioUtility() var audioID = 0 val preferedAudioDevice = "Speakers" @@ -23,8 +38,117 @@ fun main() { } val o = OpusStreamReceiver(audioID) + var afp: AudioFilePlayer? = null - val w = webApp("0.0.0.0",3030, javafx.util.Pair("admin","admin1234")) + val w = webApp("0.0.0.0",3030, BiFunction { + source: String, cmd: WsCommand -> + when (source) { + "setting" -> when(cmd.command){ + "getConfig" ->{ + logger.info("Get Config") + WsReply(cmd.command,objectMapper.writeValueAsString(cfg) ) + } + "setZelloConfig" -> { + try{ + val xx = objectMapper.readValue(cmd.data, object: TypeReference>() {}) + cfg.ZelloUsername = xx["ZelloUsername"] + cfg.ZelloPassword = xx["ZelloPassword"] + cfg.ZelloChannel = xx["ZelloChannel"] + cfg.ZelloServer = xx["ZelloServer"] + cfg.ZelloWorkNetworkName = xx["ZelloWorkNetworkName"] + cfg.ZelloEnterpriseServerDomain = xx["ZelloEnterpriseServerDomain"] + + WsReply(cmd.command,"success") + } catch (e: Exception){ + WsReply(cmd.command,"failed: ${e.message}") + } + + } + "setMessageConfig"-> { + try{ + + val xx = objectMapper.readValue(cmd.data, object : TypeReference>() {}) + cfg.M1 = xx["M1"] + cfg.M2 = xx["M2"] + cfg.M3 = xx["M3"] + cfg.M4 = xx["M4"] + cfg.M5 = xx["M5"] + cfg.M6 = xx["M6"] + cfg.M7 = xx["M7"] + cfg.M8 = xx["M8"] + WsReply(cmd.command,"success") + } catch (e: Exception){ + WsReply(cmd.command,"failed: ${e.message}") + } + + } + else -> WsReply(cmd.command,"Invalid command: ${cmd.command}") + } + + "prerecordedbroadcast" -> when(cmd.command){ + "getMessageConfig" ->{ + val data = mapOf( + "M1" to cfg.M1, + "M2" to cfg.M2, + "M3" to cfg.M3, + "M4" to cfg.M4, + "M5" to cfg.M5, + "M6" to cfg.M6, + "M7" to cfg.M7, + "M8" to cfg.M8 + ) + WsReply(cmd.command, objectMapper.writeValueAsString(data)) + } + "getPlaybackStatus" ->{ + if (afp!=null && true==afp?.isPlaying){ + WsReply(cmd.command, "Playing: ${afp?.filename}") + } else { + WsReply(cmd.command, "Idle") + } + } + "playMessage" ->{ + val filename = when(cmd.data){ + "M1" -> cfg.M1 + "M2" -> cfg.M2 + "M3" -> cfg.M3 + "M4" -> cfg.M4 + "M5" -> cfg.M5 + "M6" -> cfg.M6 + "M7" -> cfg.M7 + "M8" -> cfg.M8 + else -> { + null + } + } + if (ValidFile(filename)){ + try{ + val player= AudioFilePlayer(audioID, filename) + player.Play { cb -> afp = null} + afp = player + WsReply(cmd.command,"success") + + } catch (e: Exception){ + WsReply(cmd.command, "failed: ${e.message}") + } + } else WsReply(cmd.command,"Invalid file : $filename") + + + + } + "stopMessage" ->{ + afp?.Stop() + afp = null + WsReply(cmd.command,"success") + } + else -> WsReply(cmd.command,"Invalid command: ${cmd.command}") + } + "pocreceiver" -> when(cmd.command){ + else -> WsReply(cmd.command,"Invalid command: ${cmd.command}") + } + else -> WsReply(cmd.command,"Invalid source: $source") + } + + } , Pair("admin","admin1234")) w.Start() val z = ZelloClient.fromConsumerZello("gtcdevice01","GtcDev2025") @@ -97,4 +221,6 @@ fun main() { } }) -} \ No newline at end of file +} + + diff --git a/src/audio/AudioFilePlayer.kt b/src/audio/AudioFilePlayer.kt index 699828f..abba95d 100644 --- a/src/audio/AudioFilePlayer.kt +++ b/src/audio/AudioFilePlayer.kt @@ -7,9 +7,10 @@ import java.util.function.Consumer * Supported extensions : .wav, .mp3 */ @Suppress("unused") -class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate: Int = 48000) { +class AudioFilePlayer(deviceID: Int, val filename: String?, device_samplingrate: Int = 48000) { val bass: Bass = Bass.Instance var filehandle = 0 + var isPlaying = false init{ if (bass.BASS_SetDevice(deviceID)){ filehandle = bass.BASS_StreamCreateFile(false, filename, 0, 0, 0) @@ -19,17 +20,30 @@ class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate: } else throw Exception("Failed to set device $deviceID") } + fun Stop(){ + if (filehandle!=0){ + bass.BASS_ChannelStop(filehandle) + bass.BASS_StreamFree(filehandle) + filehandle = 0 + } + } + fun Play(finished: Consumer ) : Boolean{ if (bass.BASS_ChannelPlay(filehandle, false)){ val thread = Thread{ + isPlaying = true while(true){ Thread.sleep(1000) + if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){ // finished playing break } } + isPlaying = false + bass.BASS_StreamFree(filehandle) + filehandle = 0 finished.accept(true) } thread.name = "AudioFilePlayer $filename" diff --git a/src/somecodes/Codes.kt b/src/somecodes/Codes.kt index 715a027..598115f 100644 --- a/src/somecodes/Codes.kt +++ b/src/somecodes/Codes.kt @@ -1,9 +1,23 @@ package somecodes +import com.fasterxml.jackson.databind.ObjectMapper +import java.io.File + @Suppress("unused") class Codes { - + private val objectMapper = ObjectMapper() companion object{ + + fun ValidFile(s: String?) : Boolean { + if (s!=null){ + if (ValidString(s)){ + val ff = File(s) + return ff.isFile + } + } + return false + } + fun ValidString(s : String?) : Boolean { return s != null && s.isNotEmpty() && s.isNotBlank() } diff --git a/src/somecodes/configFile.kt b/src/somecodes/configFile.kt new file mode 100644 index 0000000..4323876 --- /dev/null +++ b/src/somecodes/configFile.kt @@ -0,0 +1,96 @@ +package somecodes + +import java.nio.file.Path +import kotlin.io.path.Path + +class configFile { + var ZelloUsername: String? = "gtcdevice01" + var ZelloPassword: String? = "GtcDev2025" + var ZelloChannel: String? = "GtcDev2025" + var ZelloServer: String? = "community" + var ZelloWorkNetworkName: String? = "" + var ZelloEnterpriseServerDomain: String? = "" + var M1: String? = "" + var M2: String? = "" + var M3: String? = "" + var M4: String? = "" + var M5: String? = "" + var M6: String? = "" + var M7: String? = "" + var M8: String? = "" + + private val filepath : Path = Path(System.getProperty("user.dir"), "config.json") + + fun Load(){ + if (filepath.toFile().exists()){ + // file found, then load the configuration to configFile object + try{ + val json = filepath.toFile().readText() + val configMap = json.split(",").associate { it -> + val (key, value) = it.split(":").map { it.trim().removeSurrounding("\"") } + key to value + } + + ZelloUsername = configMap["ZelloUsername"] + ZelloPassword = configMap["ZelloPassword"] + ZelloChannel = configMap["ZelloChannel"] + ZelloServer = configMap["ZelloServer"] + ZelloWorkNetworkName = configMap["ZelloWorkNetworkName"] + ZelloEnterpriseServerDomain = configMap["ZelloEnterpriseServerDomain"] + M1 = configMap["M1"] + M2 = configMap["M2"] + M3 = configMap["M3"] + M4 = configMap["M4"] + M5 = configMap["M5"] + M6 = configMap["M6"] + M7 = configMap["M7"] + M8 = configMap["M8"] + } catch (e: Exception) { + println("Error loading configuration: ${e.message}") + } + } else CreateDefaultConfig() + } + + fun CreateDefaultConfig() { + ZelloUsername = "gtcdevice01" + ZelloPassword = "GtcDev2025" + ZelloChannel = "GtcDev2025" + ZelloServer = "community" + ZelloWorkNetworkName = "" + ZelloEnterpriseServerDomain = "" + M1 = "" + M2 = "" + M3 = "" + M4 = "" + M5 = "" + M6 = "" + M7 = "" + M8 = "" + Save() + } + + fun Save(){ + try { + // Convert the configFile object to JSON and write it to the file + val json = """{ + "ZelloUsername": "$ZelloUsername", + "ZelloPassword": "$ZelloPassword", + "ZelloChannel": "$ZelloChannel", + "ZelloServer": "$ZelloServer", + "ZelloWorkNetworkName": "$ZelloWorkNetworkName", + "ZelloEnterpriseServerDomain": "$ZelloEnterpriseServerDomain", + "M1": "$M1", + "M2": "$M2", + "M3": "$M3", + "M4": "$M4", + "M5": "$M5", + "M6": "$M6", + "M7": "$M7", + "M8": "$M8" + }""" + filepath.toFile().writeText(json) + } catch (e: Exception) { + println("Error saving configuration: ${e.message}") + } + } +} \ No newline at end of file diff --git a/src/web/WsCommand.kt b/src/web/WsCommand.kt new file mode 100644 index 0000000..82e16c5 --- /dev/null +++ b/src/web/WsCommand.kt @@ -0,0 +1,10 @@ +package web + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +data class WsCommand @JsonCreator constructor(@JsonProperty("command")val command: String, @JsonProperty("data") val data: String?=null) { + override fun toString(): String { + return "WsCommand(command='$command', data='$data')" + } +} diff --git a/src/web/WsReply.kt b/src/web/WsReply.kt new file mode 100644 index 0000000..aa711c9 --- /dev/null +++ b/src/web/WsReply.kt @@ -0,0 +1,11 @@ +package web + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty + +data class WsReply @JsonCreator constructor(@JsonProperty("reply")val reply: String, @JsonProperty("data") val data: String?=null) { + + override fun toString(): String { + return "WsReply(reply='$reply', data='$data')" + } +} diff --git a/src/web/webApp.java b/src/web/webApp.java index a2ca713..396ba6a 100644 --- a/src/web/webApp.java +++ b/src/web/webApp.java @@ -1,5 +1,6 @@ package web; +import com.fasterxml.jackson.databind.ObjectMapper; import io.javalin.Javalin; import javafx.util.Pair; import org.slf4j.Logger; @@ -7,16 +8,19 @@ import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; +import java.util.function.BiFunction; import static io.javalin.apibuilder.ApiBuilder.*; +@SuppressWarnings("unused") public class webApp { private final Javalin app; private final String listenAddress; private final int listenPort; + private final ObjectMapper objectMapper = new ObjectMapper(); private final Logger logger = LoggerFactory.getLogger(webApp.class); private final Map usermap = new HashMap<>(); - public webApp(String listenAddress, int listenPort, Pair... users) { + public webApp(String listenAddress, int listenPort, BiFunction callback, Pair... users ) { this.listenAddress = listenAddress; this.listenPort = listenPort; if (users != null){ @@ -60,7 +64,14 @@ public class webApp { ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> { // Handle incoming WebSocket messages String message = wsMessageContext.message(); - // Process the message as needed + try{ + var command = objectMapper.readValue(message, WsCommand.class); + logger.info("Received command from pocreceiver.html/ws : {}", command); + var reply = callback.apply("pocreceiver", command); + wsMessageContext.send(reply); + } catch (Exception e){ + logger.error("Error processing {} from pocreceiver.html/ws: {}",message, e.getMessage()); + } })); }); path("/prerecordedbroadcast.html", ()->{ @@ -72,7 +83,14 @@ public class webApp { ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> { // Handle incoming WebSocket messages String message = wsMessageContext.message(); - // Process the message as needed + try{ + var command = objectMapper.readValue(message, WsCommand.class); + logger.info("Received command from prerecordedbroadcast.html/ws : {}", command); + var reply = callback.apply("prerecordedbroadcast", command); + wsMessageContext.send(reply); + } catch (Exception e){ + logger.error("Error processing {} from prerecordedbroadcast.html/ws: {}",message, e.getMessage()); + } })); }); path("/setting.html", () ->{ @@ -84,7 +102,17 @@ public class webApp { ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> { // Handle incoming WebSocket messages String message = wsMessageContext.message(); - // Process the message as needed + try{ + var command = objectMapper.readValue(message, WsCommand.class); + logger.info("Received command from setting.html/ws : {}", command); + var reply = callback.apply("setting", command); + logger.info("Replying to setting.html/ws : {}", reply); + wsMessageContext.send(reply); + } catch (Exception e){ + logger.error("Error processing {} from setting.html/ws: {}", message, e.getMessage()); + } + + })); });