commit 18/10/2025 sync changes from master
This commit is contained in:
31
html/webpage/assets/js/dragdrop.js
vendored
Normal file
31
html/webpage/assets/js/dragdrop.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
const dropArea = document.getElementById("drop-area");
|
||||
const fileInput = document.getElementById("file-input");
|
||||
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
dropArea.addEventListener(eventName, e => e.preventDefault());
|
||||
dropArea.addEventListener(eventName, e => e.stopPropagation());
|
||||
});
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
dropArea.addEventListener(eventName, () => dropArea.classList.add('highlight'));
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
dropArea.addEventListener(eventName, () => dropArea.classList.remove('highlight'));
|
||||
});
|
||||
|
||||
|
||||
dropArea.addEventListener('click', () => fileInput.click());
|
||||
|
||||
dropArea.addEventListener('drop', e => {
|
||||
const files = e.dataTransfer.files;
|
||||
handleFiles(files);
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', e => {
|
||||
handleFiles(e.target.files);
|
||||
});
|
||||
|
||||
function handleFiles(files) {
|
||||
console.log("file dropped");
|
||||
}
|
||||
@@ -195,9 +195,9 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-sm-1">No</th>
|
||||
<th class="col-sm-3">Description</th>
|
||||
<th class="col">IP Address</th>
|
||||
<th class="class05">No</th>
|
||||
<th class="class75">Description</th>
|
||||
<th class="class20">IP Address</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="soundchanneltablebody"></tbody>
|
||||
@@ -236,11 +236,11 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-sm-1">No</th>
|
||||
<th class="col-sm-2">Description</th>
|
||||
<th class="col-sm-2">SoundChannel</th>
|
||||
<th class="col-sm-2">ID</th>
|
||||
<th class="col">BP</th>
|
||||
<th class="class05">No</th>
|
||||
<th class="class40">Description</th>
|
||||
<th class="class20">SoundChannel</th>
|
||||
<th class="class05">ID</th>
|
||||
<th class="class30">BP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="broadcastzonetablebody"></tbody>
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
</div>
|
||||
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
||||
<script src="assets/js/bs-init.js"></script>
|
||||
<script src="assets/js/dragdrop.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
266
src/toa/Vx3K.kt
Normal file
266
src/toa/Vx3K.kt
Normal file
@@ -0,0 +1,266 @@
|
||||
package toa
|
||||
|
||||
import codes.Somecodes
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.tinylog.Logger
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.function.BiConsumer
|
||||
|
||||
/**
|
||||
* VX3K Protocol
|
||||
* @param ipaddress IP address of the VX3K device, default to 192.168.14.1
|
||||
* @param port Port number of the VX3K device, from 50050-50053 default to 50053
|
||||
*/
|
||||
class Vx3K(val ipaddress : String = "192.168.14.1", val port : Int = 50053) {
|
||||
private val remotesocket : InetSocketAddress
|
||||
init{
|
||||
if (port !in 50050..50053){
|
||||
throw IllegalArgumentException("Port number must be between 50050 and 50053")
|
||||
}
|
||||
try{
|
||||
val inet = Inet4Address.getByName(ipaddress)
|
||||
remotesocket = InetSocketAddress(inet, port)
|
||||
} catch (_ : Exception){
|
||||
throw IllegalArgumentException("Invalid IP address: $ipaddress")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the VX3K device
|
||||
* @param timeout Connection timeout in milliseconds, default to 30000 ms
|
||||
*/
|
||||
fun Connect(timeout: Int = 30000){
|
||||
try{
|
||||
val socket = Socket()
|
||||
// read timeout 5 seconds
|
||||
socket.soTimeout = 5000
|
||||
socket.connect(remotesocket, timeout)
|
||||
} catch (e : Exception){
|
||||
Logger.error { "Failed to connect with ${remotesocket.hostName}:${remotesocket.port}, Message: ${e.message}" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual Contact Input (Commmand 0x1001)
|
||||
* @param ID : Device ID for VX3K, range 0 - 31
|
||||
* @param CIN : Contact Input Number, range 0 - 15 for normal terminal, 16 for emergency contact input1, 17 for emergency contact input2
|
||||
* @param isON : true for ON, false for OFF
|
||||
* @param cb : Callback function with parameters (success: Boolean, message: String)
|
||||
*/
|
||||
fun VirtualCIN(ID: Short, CIN: Short, isON: Boolean, cb : BiConsumer<Boolean, String>){
|
||||
val commandID = 0x1001.toShort()
|
||||
if (ID !in 0..31){
|
||||
cb.accept(false, "ID must be between 0 and 31")
|
||||
return
|
||||
}
|
||||
if (CIN !in 0..17){
|
||||
cb.accept(false, "CIN must be between 0 and 17")
|
||||
return
|
||||
}
|
||||
val payload = ByteBuffer.allocate(6).order(ByteOrder.BIG_ENDIAN)
|
||||
payload.putShort(ID)
|
||||
payload.putShort(CIN)
|
||||
payload.putShort(if (isON) 1 else 0)
|
||||
val command = Make_Request_Command(commandID, payload.array())
|
||||
Send_Receive(command,8){
|
||||
success, reply ->
|
||||
if (success){
|
||||
val bb = ByteBuffer.wrap(reply).order(ByteOrder.BIG_ENDIAN)
|
||||
val resp_commandID = bb.short
|
||||
val resp_code = bb.short
|
||||
if (resp_commandID==commandID){
|
||||
if (resp_code.toInt() == 0){
|
||||
cb.accept(true, "Virtual CIN command sent successfully to ${remotesocket.hostName}:${remotesocket.port}")
|
||||
} else {
|
||||
cb.accept(false, "Virtual CIN command failed with response code $resp_code from ${remotesocket.hostName}:${remotesocket.port}")
|
||||
}
|
||||
} else {
|
||||
cb.accept(false, "Invalid response command ID $resp_commandID from ${remotesocket.hostName}:${remotesocket.port}")
|
||||
}
|
||||
} else {
|
||||
cb.accept(false, "Failed to send Virtual CIN command to ${remotesocket.hostName}:${remotesocket.port}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open / Close Audio Input in Broadcast Pattern (Command 0x1003 for old firmware, 0x1101 for new firmware)
|
||||
* @param NewFirmware Set to true if using new firmware version
|
||||
* @param ID Device ID for VX3K, range 0 - 39 for new firmware, 0 - 31 for old firmware
|
||||
* @param Channel Audio Input Channel, range 0 - 3
|
||||
* @param BroadcastZones Vx3K_BroadcastZone object with selected broadcast zones
|
||||
* @param cb Callback function with parameters (success: Boolean, message: String)
|
||||
*/
|
||||
fun AudioInput_BroadcastPattern(NewFirmware : Boolean, ID: Short, Channel: Short, BroadcastZones: Vx3K_BroadcastZone, cb: BiConsumer<Boolean, String>){
|
||||
val CommandID = if (NewFirmware) 0x1101.toShort() else 0x1003.toShort()
|
||||
if (NewFirmware){
|
||||
if (ID !in 0..39){
|
||||
cb.accept(false, "ID must be between 0 and 39")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (ID !in 0..31){
|
||||
cb.accept(false, "ID must be between 0 and 31")
|
||||
return
|
||||
}
|
||||
}
|
||||
if (Channel !in 0..3){
|
||||
cb.accept(false, "Channel must be between 0 and 3")
|
||||
return
|
||||
}
|
||||
|
||||
val payload = ByteBuffer.allocate(if (NewFirmware) 86 else 70).order(ByteOrder.BIG_ENDIAN)
|
||||
// Audio Input = 1
|
||||
payload.putShort(1)
|
||||
// VX3K ID
|
||||
payload.putShort(ID)
|
||||
// Audio Input Channel
|
||||
payload.putShort(Channel)
|
||||
// Broadcast Pattern Zones
|
||||
payload.put(BroadcastZones.payload)
|
||||
val command = Make_Request_Command(CommandID, payload.array())
|
||||
Send_Receive(command,8){
|
||||
success, reply ->
|
||||
if (success){
|
||||
val bb = ByteBuffer.wrap(reply).order(ByteOrder.BIG_ENDIAN)
|
||||
val resp_commandID = bb.short
|
||||
val resp_code = bb.short
|
||||
if (resp_commandID==CommandID){
|
||||
if (resp_code.toInt() == 0){
|
||||
cb.accept(true, "Open Audio Input Broadcast command sent successfully to ${remotesocket.hostName}:${remotesocket.port}")
|
||||
} else {
|
||||
cb.accept(false, "Open Audio Input Broadcast command failed with response code $resp_code from ${remotesocket.hostName}:${remotesocket.port}")
|
||||
}
|
||||
} else {
|
||||
cb.accept(false, "Invalid response command ID $resp_commandID from ${remotesocket.hostName}:${remotesocket.port}")
|
||||
}
|
||||
} else {
|
||||
cb.accept(false, "Failed to send Open Audio Input Broadcast command to ${remotesocket.hostName}:${remotesocket.port}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open / Close Network Broadcast Pattern (Command 0x1102 for old firmware, 0x1104 for new firmware)
|
||||
* @param NewFirmware Set to true if using new firmware version
|
||||
* @param multicastIP Multicast IP address in string format, from 224.0.0.0 ~ 239.255.255.255, default to 224.0.0.1
|
||||
* @param port UDP Port number, port 5000-5255 is invalid, default to 5300
|
||||
* @param priority Broadcast Priority value, range 1 - 1024, default to 512 (to be checked later)
|
||||
* @param isBGM true for BGM, false for Paging
|
||||
* @param SSRC SSRC value, range 1 - 65535, default to 1 (to be checked later)
|
||||
* @param payloadType RTP Payload Type, range 0 - 127, default to 0 (to be checked later)
|
||||
* @param payloadSize RTP Payload Size, range 0 - 1500, default to 1000 (to be checked later)
|
||||
* @param BroadcastZones Vx3K_BroadcastZone object with selected broadcast zones
|
||||
* @param cb Callback function with parameters (success: Boolean, message: String)
|
||||
*/
|
||||
fun Network_BroadcastPattern(NewFirmware: Boolean,multicastIP: String = "224.0.0.1", port: Int = 5300, priority: Int = 512, isBGM: Boolean, SSRC: UShort = 1u, payloadType: Short = 0, payloadSize: Short = 1000, BroadcastZones: Vx3K_BroadcastZone, cb:BiConsumer<Boolean, String>){
|
||||
val CommandID = if (NewFirmware) 0x1104.toShort() else 0x1102.toShort()
|
||||
if (Somecodes.ValidIPV4(multicastIP)){
|
||||
val inet = Inet4Address.getByName(multicastIP)
|
||||
if (inet.isMulticastAddress){
|
||||
if (port in 5256..65535){
|
||||
if (priority in 1..1024){
|
||||
if (SSRC in 1u..65535u){
|
||||
if (payloadType in 0..127){
|
||||
if (payloadSize in 0..1500){
|
||||
val jitterbuffer = 1000 //TODO 32 signed integer, to be checked later
|
||||
val payload = ByteBuffer.allocate(if (NewFirmware) 100 else 84)
|
||||
payload.put(inet.address)
|
||||
payload.putShort(port.toShort())
|
||||
payload.putShort(priority.toShort())
|
||||
payload.putShort(if (isBGM) 1 else 0)
|
||||
payload.putShort(SSRC.toShort())
|
||||
payload.putShort(payloadType)
|
||||
payload.putShort(payloadSize)
|
||||
payload.putInt(jitterbuffer)
|
||||
payload.put(BroadcastZones.payload)
|
||||
val command = Make_Request_Command(CommandID, payload.array())
|
||||
Send_Receive(command,10){
|
||||
success, reply ->
|
||||
if (success){
|
||||
val bb = ByteBuffer.wrap(reply).order(ByteOrder.BIG_ENDIAN)
|
||||
val resp_commandID = bb.short
|
||||
val resp_code = bb.short
|
||||
// buang 2 short
|
||||
bb.short
|
||||
bb.short
|
||||
val sound_source_number = bb.short
|
||||
if (resp_commandID==CommandID){
|
||||
if (resp_code.toInt() == 0){
|
||||
cb.accept(true, "Open Network Broadcast command sent successfully to ${remotesocket.hostName}:${remotesocket.port}, Sound Source Number: $sound_source_number")
|
||||
} else {
|
||||
cb.accept(false, "Open Network Broadcast command failed with response code $resp_code from ${remotesocket.hostName}:${remotesocket.port}")
|
||||
}
|
||||
} else {
|
||||
cb.accept(false, "Invalid response command ID $resp_commandID from ${remotesocket.hostName}:${remotesocket.port}")
|
||||
}
|
||||
} else {
|
||||
cb.accept(false, "Failed to send Open Network Broadcast command to ${remotesocket.hostName}:${remotesocket.port}")
|
||||
}
|
||||
}
|
||||
} else cb.accept(false, "Payload Size must be between 0 and 1500")
|
||||
} else cb.accept(false, "Payload Type must be between 0 and 127")
|
||||
} else cb.accept(false, "SSRC must be between 1 and 65535")
|
||||
} else cb.accept(false, "Priority must be between 1 and 1024")
|
||||
} else cb.accept(false, "Port must be greater than 5255 and less than 65536")
|
||||
} else cb.accept(false, "multicastIP must between 224.0.0.0 ~ 239.255.255.255")
|
||||
} else cb.accept(false, "Multicast IP address invalid")
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Send command and receive reply from VX3K device
|
||||
* @param command Command byte array to send
|
||||
* @param expectedlength Expected length of the reply byte array
|
||||
* @param cb Callback function with parameters (success: Boolean, reply: ByteArray)
|
||||
*/
|
||||
private fun Send_Receive(command: ByteArray, expectedlength: Int, cb : BiConsumer<Boolean, ByteArray>){
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try{
|
||||
val tcp = Socket()
|
||||
tcp.soTimeout = 5000
|
||||
tcp.connect(remotesocket, 30000)
|
||||
val outstream = tcp.getOutputStream()
|
||||
val instream = tcp.getInputStream()
|
||||
outstream.write(command)
|
||||
outstream.flush()
|
||||
val reply = ByteArray(expectedlength)
|
||||
instream.read(reply)
|
||||
outstream.close()
|
||||
instream.close()
|
||||
tcp.close()
|
||||
cb.accept(true, reply)
|
||||
} catch (_: Exception){
|
||||
cb.accept(false, ByteArray(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap payload into VX3K Request Command format
|
||||
* @param commandID Command ID
|
||||
* @param Payload Payload data
|
||||
* @return ByteArray of the complete command
|
||||
*/
|
||||
private fun Make_Request_Command(commandID: Short, Payload: ByteArray) : ByteArray {
|
||||
val result = ByteBuffer.allocate(8+Payload.size).order(ByteOrder.BIG_ENDIAN)
|
||||
// command ID (2 bytes)
|
||||
result.putShort(commandID)
|
||||
// Response code (2 bytes), set 0 for Request
|
||||
result.putShort(0)
|
||||
// command length = command header (8 bytes) + payload size
|
||||
result.putShort((Payload.size+8).toShort())
|
||||
// flag and reserver = 0 (2 bytes)
|
||||
result.putShort(0)
|
||||
result.put(Payload)
|
||||
// read to byte array
|
||||
return result.array()
|
||||
}
|
||||
}
|
||||
67
src/toa/Vx3K_BroadcastZone.kt
Normal file
67
src/toa/Vx3K_BroadcastZone.kt
Normal file
@@ -0,0 +1,67 @@
|
||||
package toa
|
||||
|
||||
import kotlin.experimental.and
|
||||
import kotlin.experimental.or
|
||||
|
||||
/**
|
||||
* VX3K Broadcast Zone Configuration
|
||||
* Old Firmware: Broadcast Zone 1 - 512
|
||||
* New Firmware: Broadcast Zone 1 - 640
|
||||
* @param NewFirmware Set to true if using new firmware version
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class Vx3K_BroadcastZone(val NewFirmware: Boolean = false) {
|
||||
val payload: ByteArray = if (NewFirmware) ByteArray(80) else ByteArray(64)
|
||||
|
||||
/**
|
||||
* Set a zone as active
|
||||
* @param zonenumber Zone number to set (1 to 512 for old firmware, 1 to 640 for new firmware)
|
||||
*/
|
||||
fun SetZone(zonenumber: Int){
|
||||
if (zonenumber<1) throw Exception("Minimum zone number is 1")
|
||||
if (NewFirmware){
|
||||
if (zonenumber>640) throw Exception("Maximum zone number is 640 for new firmware")
|
||||
} else {
|
||||
if (zonenumber>512) throw Exception("Maximum zone number is 512 for old firmware")
|
||||
}
|
||||
|
||||
val byteIndex = (zonenumber - 1) / 8
|
||||
val bitIndex = (zonenumber - 1) % 8
|
||||
payload[byteIndex] = payload[byteIndex] or (1 shl bitIndex).toByte()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a zone (set as inactive)
|
||||
* @param zonenumber Zone number to clear (1 to 512 for old firmware, 1 to 640 for new firmware)
|
||||
*/
|
||||
fun ClearZone(zonenumber: Int){
|
||||
if (zonenumber<1) throw Exception("Minimum zone number is 1")
|
||||
if (NewFirmware){
|
||||
if (zonenumber>640) throw Exception("Maximum zone number is 640 for new firmware")
|
||||
} else {
|
||||
if (zonenumber>512) throw Exception("Maximum zone number is 512 for old firmware")
|
||||
}
|
||||
|
||||
val byteIndex = (zonenumber - 1) / 8
|
||||
val bitIndex = (zonenumber - 1) % 8
|
||||
payload[byteIndex] = payload[byteIndex] and ((1 shl bitIndex).inv().toByte())
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all zones as active
|
||||
*/
|
||||
fun SetAllZones(){
|
||||
for (i in payload.indices){
|
||||
payload[i] = 0xFF.toByte()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all zones (set all as inactive)
|
||||
*/
|
||||
fun ClearAllZones(){
|
||||
for (i in payload.indices){
|
||||
payload[i] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user