Commit 06/08/2025

This commit is contained in:
2025-08-06 09:29:14 +07:00
parent 8290418100
commit 158f3ea4c2
12 changed files with 345 additions and 23 deletions

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectInspectionProfilesVisibleTreeState"> <component name="ProjectInspectionProfilesVisibleTreeState">
<entry key="Project Default"> <entry key="Project Default">

16
config.json Normal file
View File

@@ -0,0 +1,16 @@
{
"ZelloUsername": "gtcdevice01",
"ZelloPassword": "GtcDev2025",
"ZelloChannel": "GtcDev2025",
"ZelloServer": "community",
"ZelloWorkNetworkName": "",
"ZelloEnterpriseServerDomain": "",
"M1": "",
"M2": "",
"M3": "",
"M4": "",
"M5": "",
"M6": "",
"M7": "",
"M8": ""
}

View File

@@ -9,7 +9,7 @@ $(document).ready(function() {
$('#indicatorDisconnected').addClass('visually-hidden'); $('#indicatorDisconnected').addClass('visually-hidden');
$('#indicatorConnected').removeClass('visually-hidden'); $('#indicatorConnected').removeClass('visually-hidden');
setInterval(function() { setInterval(function() {
ws.send(JSON.stringify({ command: "getZelloStatus" })); sendCommand({ command: "getZelloStatus" });
}, 5000); }, 5000);
}; };
@@ -39,4 +39,12 @@ $(document).ready(function() {
ws.onerror = function(error) { ws.onerror = function(error) {
console.error('WebSocket error:', 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);
}
}
}); });

View File

@@ -79,7 +79,7 @@ $(document).ready(function() {
function sendCommand(cmd) { function sendCommand(cmd) {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
ws.send(cmd); ws.send(JSON.stringify(cmd));
} else { } else {
console.error('WebSocket is not open. Unable to send command:', JSON.stringify(cmd)); console.error('WebSocket is not open. Unable to send command:', JSON.stringify(cmd));
} }

View File

@@ -40,23 +40,23 @@ $(document).ready(function() {
return; return;
} }
if (msg.reply === "getConfig" && msg.data !== undefined ) { if (msg.reply === "getConfig" && msg.data !== undefined ) {
const configData = msg.data; const configData = JSON.parse(msg.data);
console.log('Config Data:', configData); console.log('Config Data:', configData);
$('#zelloUsername').val(configData.ZelloUsername || ''); $('#zelloUsername').val(configData.zelloUsername || '');
$('#zelloPassword').val(configData.ZelloPassword || ''); $('#zelloPassword').val(configData.zelloPassword || '');
$('#zelloChannel').val(configData.ZelloChannel || ''); $('#zelloChannel').val(configData.zelloChannel || '');
if ("community" === configData.ZelloServer) { if ("community" === configData.zelloServer) {
$('#zellocommunity').prop('checked', true); $('#zellocommunity').prop('checked', true);
$('#zelloWorkNetworkName').val('').prop('disabled', true); $('#zelloWorkNetworkName').val('').prop('disabled', true);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true); $('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
} else if ("work" === configData.ZelloServer) { } else if ("work" === configData.zelloServer) {
$('#zellowork').prop('checked', true); $('#zellowork').prop('checked', true);
$('#zelloWorkNetworkName').val(configData.ZelloWorkNetworkName || '').prop('disabled', false); $('#zelloWorkNetworkName').val(configData.zelloWorkNetworkName || '').prop('disabled', false);
$('#zelloEnterpriseServerDomain').val('').prop('disabled', true); $('#zelloEnterpriseServerDomain').val('').prop('disabled', true);
} else if ("enterprise" === configData.ZelloServer) { } else if ("enterprise" === configData.zelloServer) {
$('#zelloenterprise').prop('checked', true); $('#zelloenterprise').prop('checked', true);
$('#zelloWorkNetworkName').val('').prop('disabled', 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++) { for (let i = 1; i <= 8; i++) {
@@ -72,8 +72,8 @@ $(document).ready(function() {
dropdownMenu.append(item); dropdownMenu.append(item);
}); });
// Set button text to selected message if present // Set button text to selected message if present
if (configData[`M${i}`]) { if (configData[`m${i}`]) {
dropdownButton.text(configData[`M${i}`]); dropdownButton.text(configData[`m${i}`]);
} else { } else {
dropdownButton.text(''); dropdownButton.text('');
} }
@@ -176,7 +176,7 @@ $(document).ready(function() {
function sendCommand(cmd) { function sendCommand(cmd) {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
ws.send(cmd); ws.send(JSON.stringify(cmd));
} else { } else {
console.error('WebSocket is not open. Unable to send command:', JSON.stringify(cmd)); console.error('WebSocket is not open. Unable to send command:', JSON.stringify(cmd));
} }

View File

@@ -1,13 +1,28 @@
import audio.AudioFilePlayer
import audio.AudioUtility import audio.AudioUtility
import audio.OpusStreamReceiver import audio.OpusStreamReceiver
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import somecodes.Codes.Companion.ValidString import somecodes.Codes.Companion.ValidString
import somecodes.configFile
import web.WsReply
import web.webApp import web.webApp
import zello.ZelloClient import zello.ZelloClient
import zello.ZelloEvent 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 <b>Run</b> code, press <shortcut actionId="Run"/> or //TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter. // click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
fun main() { fun main() {
val logger = LoggerFactory.getLogger("Main")
val objectMapper = ObjectMapper()
val cfg = configFile()
cfg.Load()
val au = AudioUtility() val au = AudioUtility()
var audioID = 0 var audioID = 0
val preferedAudioDevice = "Speakers" val preferedAudioDevice = "Speakers"
@@ -23,8 +38,117 @@ fun main() {
} }
val o = OpusStreamReceiver(audioID) 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<Map<String, String>>() {})
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<Map<String, String>>() {})
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() w.Start()
val z = ZelloClient.fromConsumerZello("gtcdevice01","GtcDev2025") val z = ZelloClient.fromConsumerZello("gtcdevice01","GtcDev2025")
@@ -97,4 +221,6 @@ fun main() {
} }
}) })
} }

View File

@@ -7,9 +7,10 @@ import java.util.function.Consumer
* Supported extensions : .wav, .mp3 * Supported extensions : .wav, .mp3
*/ */
@Suppress("unused") @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 val bass: Bass = Bass.Instance
var filehandle = 0 var filehandle = 0
var isPlaying = false
init{ init{
if (bass.BASS_SetDevice(deviceID)){ if (bass.BASS_SetDevice(deviceID)){
filehandle = bass.BASS_StreamCreateFile(false, filename, 0, 0, 0) 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") } 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<Any> ) : Boolean{ fun Play(finished: Consumer<Any> ) : Boolean{
if (bass.BASS_ChannelPlay(filehandle, false)){ if (bass.BASS_ChannelPlay(filehandle, false)){
val thread = Thread{ val thread = Thread{
isPlaying = true
while(true){ while(true){
Thread.sleep(1000) Thread.sleep(1000)
if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){ if (bass.BASS_ChannelIsActive(filehandle)!= Bass.BASS_ACTIVE_PLAYING){
// finished playing // finished playing
break break
} }
} }
isPlaying = false
bass.BASS_StreamFree(filehandle)
filehandle = 0
finished.accept(true) finished.accept(true)
} }
thread.name = "AudioFilePlayer $filename" thread.name = "AudioFilePlayer $filename"

View File

@@ -1,9 +1,23 @@
package somecodes package somecodes
import com.fasterxml.jackson.databind.ObjectMapper
import java.io.File
@Suppress("unused") @Suppress("unused")
class Codes { class Codes {
private val objectMapper = ObjectMapper()
companion object{ 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 { fun ValidString(s : String?) : Boolean {
return s != null && s.isNotEmpty() && s.isNotBlank() return s != null && s.isNotEmpty() && s.isNotBlank()
} }

View File

@@ -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}")
}
}
}

10
src/web/WsCommand.kt Normal file
View File

@@ -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')"
}
}

11
src/web/WsReply.kt Normal file
View File

@@ -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')"
}
}

View File

@@ -1,5 +1,6 @@
package web; package web;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.javalin.Javalin; import io.javalin.Javalin;
import javafx.util.Pair; import javafx.util.Pair;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -7,16 +8,19 @@ import org.slf4j.LoggerFactory;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.BiFunction;
import static io.javalin.apibuilder.ApiBuilder.*; import static io.javalin.apibuilder.ApiBuilder.*;
@SuppressWarnings("unused")
public class webApp { public class webApp {
private final Javalin app; private final Javalin app;
private final String listenAddress; private final String listenAddress;
private final int listenPort; private final int listenPort;
private final ObjectMapper objectMapper = new ObjectMapper();
private final Logger logger = LoggerFactory.getLogger(webApp.class); private final Logger logger = LoggerFactory.getLogger(webApp.class);
private final Map<String, String> usermap = new HashMap<>(); private final Map<String, String> usermap = new HashMap<>();
public webApp(String listenAddress, int listenPort, Pair<String, String>... users) { public webApp(String listenAddress, int listenPort, BiFunction<String,WsCommand, WsReply> callback, Pair<String, String>... users ) {
this.listenAddress = listenAddress; this.listenAddress = listenAddress;
this.listenPort = listenPort; this.listenPort = listenPort;
if (users != null){ if (users != null){
@@ -60,7 +64,14 @@ public class webApp {
ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> { ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> {
// Handle incoming WebSocket messages // Handle incoming WebSocket messages
String message = wsMessageContext.message(); 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", ()->{ path("/prerecordedbroadcast.html", ()->{
@@ -72,7 +83,14 @@ public class webApp {
ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> { ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> {
// Handle incoming WebSocket messages // Handle incoming WebSocket messages
String message = wsMessageContext.message(); 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", () ->{ path("/setting.html", () ->{
@@ -84,7 +102,17 @@ public class webApp {
ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> { ws("/ws", wshandler -> wshandler.onMessage(wsMessageContext -> {
// Handle incoming WebSocket messages // Handle incoming WebSocket messages
String message = wsMessageContext.message(); 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());
}
})); }));
}); });