diff --git a/src/codes/Somecodes.kt b/src/codes/Somecodes.kt
index 8795e2c..f689679 100644
--- a/src/codes/Somecodes.kt
+++ b/src/codes/Somecodes.kt
@@ -16,6 +16,8 @@ import oshi.SystemInfo
import oshi.hardware.CentralProcessor
import oshi.hardware.GlobalMemory
import oshi.hardware.NetworkIF
+import oshi.hardware.Sensors
+import oshi.software.os.OperatingSystem
import java.nio.file.Files
import java.nio.file.Path
import java.time.LocalDateTime
@@ -37,6 +39,8 @@ class Somecodes {
val processor: CentralProcessor = si.hardware.processor
val memory : GlobalMemory = si.hardware.memory
val NetworkInfoMap = mutableMapOf()
+ val sensor : Sensors = si.hardware.sensors
+ val os : OperatingSystem = si.operatingSystem
val datetimeformat1: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss")
val dateformat1: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
@@ -492,6 +496,35 @@ class Somecodes {
sb.append(".wav")
return sb.toString()
}
+
+ /**
+ * Get sensors information using OSHI library.
+ * @return A string representing the CPU temperature, fan speeds, and CPU voltage, or an empty string if not available.
+ */
+ fun GetSensorsInfo() : String {
+ val cputemp = sensor.cpuTemperature
+ val cpuvolt = sensor.cpuVoltage
+ val fanspeed = sensor.fanSpeeds
+ return if (cpuvolt>0 && cputemp > 0 && fanspeed.isNotEmpty()){
+ String.format("CPU Temp: %.1f °C\nFan Speeds: %s RPM\nCPU Voltage: %.2f V",
+ sensor.cpuTemperature,
+ sensor.fanSpeeds.joinToString("/"),
+ sensor.cpuVoltage
+ )
+ } else ""
+
+ }
+
+ fun GetUptime() : String {
+ val value = os.systemUptime
+ return if (value>0){
+ // number of seconds since system boot
+ val hours = value / 3600
+ val minutes = (value % 3600) / 60
+ val seconds = value % 60
+ String.format("%02d:%02d:%02d", hours, minutes, seconds)
+ } else ""
+ }
}
diff --git a/src/toa/Vx3K.kt b/src/toa/Vx3K.kt
new file mode 100644
index 0000000..fe713fd
--- /dev/null
+++ b/src/toa/Vx3K.kt
@@ -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){
+ 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){
+ 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){
+ 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){
+ 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()
+ }
+}
\ No newline at end of file
diff --git a/src/toa/Vx3K_BroadcastZone.kt b/src/toa/Vx3K_BroadcastZone.kt
new file mode 100644
index 0000000..aab2722
--- /dev/null
+++ b/src/toa/Vx3K_BroadcastZone.kt
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/web/WebApp.kt b/src/web/WebApp.kt
index 92bc722..32cd3e3 100644
--- a/src/web/WebApp.kt
+++ b/src/web/WebApp.kt
@@ -2,6 +2,8 @@ package web
import StreamerOutputs
import codes.Somecodes
+import codes.Somecodes.Companion.GetSensorsInfo
+import codes.Somecodes.Companion.GetUptime
import codes.Somecodes.Companion.ListAudioFiles
import codes.Somecodes.Companion.ValiDateForLogHtml
import codes.Somecodes.Companion.ValidFile
@@ -104,16 +106,23 @@ class WebApp(val listenPort: Int, val userlist: List>) {
objectmapper.readValue(wsMessageContext.message(), WebsocketCommand::class.java)
when (cmd.command) {
"getSystemTime" -> {
+ val systemtime = LocalDateTime.now().format(Somecodes.datetimeformat1)
+ val uptime = GetUptime()
SendReply(
wsMessageContext,
cmd.command,
- LocalDateTime.now().format(Somecodes.datetimeformat1)
+ if (uptime.isNotEmpty()) "Date & Time : $systemtime\nSystem Uptime : $uptime" else "Date & Time : $systemtime"
)
}
"getCPUStatus" -> {
Somecodes.getCPUUsage { vv ->
- SendReply(wsMessageContext, cmd.command, vv)
+ val sv = GetSensorsInfo()
+ if (sv.isNotEmpty()){
+ SendReply(wsMessageContext, cmd.command, vv+"\n"+sv)
+ } else {
+ SendReply(wsMessageContext, cmd.command, vv)
+ }
}
}