Compare commits
1 Commits
17b4485e69
...
feature-we
| Author | SHA1 | Date | |
|---|---|---|---|
| cdcb02e976 |
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">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="col-sm-1">No</th>
|
<th class="class05">No</th>
|
||||||
<th class="col-sm-3">Description</th>
|
<th class="class75">Description</th>
|
||||||
<th class="col">IP Address</th>
|
<th class="class20">IP Address</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="soundchanneltablebody"></tbody>
|
<tbody id="soundchanneltablebody"></tbody>
|
||||||
@@ -236,11 +236,11 @@
|
|||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="col-sm-1">No</th>
|
<th class="class05">No</th>
|
||||||
<th class="col-sm-2">Description</th>
|
<th class="class40">Description</th>
|
||||||
<th class="col-sm-2">SoundChannel</th>
|
<th class="class20">SoundChannel</th>
|
||||||
<th class="col-sm-2">ID</th>
|
<th class="class05">ID</th>
|
||||||
<th class="col">BP</th>
|
<th class="class30">BP</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="broadcastzonetablebody"></tbody>
|
<tbody id="broadcastzonetablebody"></tbody>
|
||||||
|
|||||||
@@ -80,6 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
||||||
<script src="assets/js/bs-init.js"></script>
|
<script src="assets/js/bs-init.js"></script>
|
||||||
|
<script src="assets/js/dragdrop.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</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