diff --git a/html/webpage/assets/css/bss-overrides.css b/html/webpage/assets/css/bss-overrides.css index d06cc3c..e4c6b12 100644 --- a/html/webpage/assets/css/bss-overrides.css +++ b/html/webpage/assets/css/bss-overrides.css @@ -37,3 +37,52 @@ --bs-btn-disabled-border-color: #0d6efd; } +.my-4 { + margin-top: 1.5rem!important; + margin-bottom: 1.5rem!important; +} + +.mt-0 { + margin-top: 0!important; +} + +.me-2 { + margin-right: .5rem!important; +} + +.mb-2 { + margin-bottom: .5rem!important; +} + +.mb-3 { + margin-bottom: 1rem!important; +} + +.mb-4 { + margin-bottom: 1.5rem!important; +} + +.mb-5 { + margin-bottom: 3rem!important; +} + +.mb-7 { + margin-bottom: 6rem !important; +} + +.mb-auto { + margin-bottom: auto!important; +} + +@media (min-width:768px) { + .me-md-auto { + margin-right: auto!important; + } +} + +@media (min-width:768px) { + .mb-md-0 { + margin-bottom: 0!important; + } +} + diff --git a/html/webpage/assets/css/styles.css b/html/webpage/assets/css/styles.css index 0c12988..0f45c33 100644 --- a/html/webpage/assets/css/styles.css +++ b/html/webpage/assets/css/styles.css @@ -104,7 +104,7 @@ body { } .btn-login { - border-radius: 20px; + border-radius: 8px; box-shadow: rgba(136, 165, 191, 0.48) 6px 2px 16px 0px, rgba(255, 255, 255, 0.8) -6px -2px 16px 0px; --bs-btn-hover-bg: #5780f2; background-color: #5278e1; @@ -277,3 +277,44 @@ table { border-radius: 0 !important; } +.p-login { + text-align: left; + font-weight: 600; + margin-bottom: 0; + color: #3E4C66; +} + +.h-login { + font-weight: 600; + color: #2E3A59; +} + +#file-input { + display: none; +} + +.card-setting { + border-radius: 8px; + border: white solid 3px; +} + +#drop-area { + border: 2px dashed #ccc; + border-radius: 20px; + width: 400px; + height: 200px; + display: flex; + justify-content: center; + align-items: center; + font-family: sans-serif; + color: #666; + cursor: pointer; + transition: background 0.2s, border-color 0.2s; +} + +#drop-area.highlight { + background: #f0f8ff; + border-color: #0d6efd; + color: #0d6efd; +} + diff --git a/html/webpage/assets/js/dragdrop.js b/html/webpage/assets/js/dragdrop.js new file mode 100644 index 0000000..a3082cb --- /dev/null +++ b/html/webpage/assets/js/dragdrop.js @@ -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"); +} diff --git a/html/webpage/assets/js/schedulebank.js b/html/webpage/assets/js/schedulebank.js index 0174747..41607ee 100644 --- a/html/webpage/assets/js/schedulebank.js +++ b/html/webpage/assets/js/schedulebank.js @@ -101,28 +101,30 @@ $(document).ready(function () { let $schedulehour = $schedulemodal.find('#schedulehour'); // number input 0-59 let $scheduleminute = $schedulemodal.find('#scheduleminute'); - // text input - let $schedulesoundpath = $schedulemodal.find('#schedulesoundpath'); + // select2 for message + let $schedulemessage = $schedulemodal.find('#schedulemessage'); + $schedulemessage.select2({}); // number input 0-5 let $schedulerepeat = $schedulemodal.find('#schedulerepeat'); // checkbox let $scheduleenable = $schedulemodal.find('#scheduleenable'); - // div for list of checkboxes + // select2 for broadcastzones let $schedulezones = $schedulemodal.find('#schedulezones'); + $schedulezones.select2({}); // radio button for everyday let $scheduleeveryday = $schedulemodal.find('#scheduleeveryday'); - // radio button for weekdays - let $schedulesunday = $schedulemodal.find('#schedulesunday'); - let $schedulemonday = $schedulemodal.find('#schedulemonday'); - let $scheduletuesday = $schedulemodal.find('#scheduletuesday'); - let $schedulewednesday = $schedulemodal.find('#schedulewednesday'); - let $schedulethursday = $schedulemodal.find('#schedulethursday'); - let $schedulefriday = $schedulemodal.find('#schedulefriday'); - let $schedulesaturday = $schedulemodal.find('#schedulesaturday'); + // radio button for weekly + let $scheduleweekly = $schedulemodal.find('#scheduleweekly'); + // select2 for weekly selection + let $weeklyselect = $schedulemodal.find('#weeklyselect'); + $weeklyselect.select2({}); // radio button for specific date let $schedulespecialdate = $schedulemodal.find('#schedulespecialdate'); // date input let $scheduledate = $schedulemodal.find('#scheduledate'); + // select2 for language + let $languageselect = $schedulemodal.find('#languageselect'); + $languageselect.select2({}); $schedulespecialdate.on('change', function () { if ($(this).is(':checked')) { @@ -137,17 +139,10 @@ $(document).ready(function () { $scheduledescription.val(''); $schedulehour.val('0'); $scheduleminute.val('0'); - $schedulesoundpath.val(''); $schedulerepeat.val('0'); $scheduleenable.prop('checked', true); $scheduleeveryday.prop('checked', false); - $schedulesunday.prop('checked', false); - $schedulemonday.prop('checked', false); - $scheduletuesday.prop('checked', false); - $schedulewednesday.prop('checked', false); - $schedulethursday.prop('checked', false); - $schedulefriday.prop('checked', false); - $schedulesaturday.prop('checked', false); + $schedulespecialdate.prop('checked', false); $scheduledate.prop('disabled', true).val(''); diff --git a/html/webpage/broadcastzones.html b/html/webpage/broadcastzones.html index 5a033f7..ef34350 100644 --- a/html/webpage/broadcastzones.html +++ b/html/webpage/broadcastzones.html @@ -4,7 +4,7 @@ - AAS_NewGen_08OKT25 + AAS_NewGen_17OKT25 @@ -195,9 +195,9 @@ - - - + + + @@ -236,11 +236,11 @@
NoDescriptionIP AddressNoDescriptionIP Address
- - - - - + + + + + diff --git a/html/webpage/language.html b/html/webpage/language.html index c0e37d9..eb19006 100644 --- a/html/webpage/language.html +++ b/html/webpage/language.html @@ -4,7 +4,7 @@ - AAS_NewGen_08OKT25 + AAS_NewGen_17OKT25 @@ -42,9 +42,9 @@
NoDescriptionSoundChannelIDBPNoDescriptionSoundChannelIDBP
- - - + + + diff --git a/html/webpage/log.html b/html/webpage/log.html index d942a70..b292290 100644 --- a/html/webpage/log.html +++ b/html/webpage/log.html @@ -4,7 +4,7 @@ - AAS_NewGen_08OKT25 + AAS_NewGen_17OKT25 @@ -39,11 +39,11 @@
NoTAGLanguagesNoTAGLanguages
- - - - - + + + + + diff --git a/html/webpage/login.html b/html/webpage/login.html index ba46872..d39616f 100644 --- a/html/webpage/login.html +++ b/html/webpage/login.html @@ -14,22 +14,21 @@
-
-
-

Sign In

-
-
-
+
+
NoDateTimeMachineDescriptionNoDateTimeMachineDescription
- - - - - - - + + + + + + + diff --git a/html/webpage/overview.html b/html/webpage/overview.html index 0d31f99..54e1e43 100644 --- a/html/webpage/overview.html +++ b/html/webpage/overview.html @@ -4,7 +4,7 @@ - AAS_NewGen_08OKT25 + AAS_NewGen_17OKT25 @@ -19,8 +19,8 @@
- -
+ +
@@ -102,7 +102,7 @@
-
+

Channel 05

@@ -121,7 +121,7 @@
-
+

Channel 06

@@ -140,7 +140,7 @@
-
+

Channel 07

@@ -159,7 +159,7 @@
-
+

Channel 08

@@ -180,7 +180,7 @@
-
+

Channel 09

@@ -199,7 +199,7 @@
-
+

Channel 10

@@ -218,7 +218,7 @@
-
+

Channel 11

@@ -237,7 +237,7 @@
-
+

Channel 12

@@ -258,7 +258,7 @@
-
+

Channel 13

@@ -277,7 +277,7 @@
-
+

Channel 14

@@ -296,7 +296,7 @@
-
+

Channel 15

@@ -315,7 +315,7 @@
-
+

Channel 16

@@ -336,7 +336,7 @@
-
+

Channel 17

@@ -355,7 +355,7 @@
-
+

Channel 18

@@ -374,7 +374,7 @@
-
+

Channel 19

@@ -393,7 +393,7 @@
-
+

Channel 20

@@ -414,7 +414,7 @@
-
+

Channel 21

@@ -433,7 +433,7 @@
-
+

Channel 22

@@ -452,7 +452,7 @@
-
+

Channel 23

@@ -471,7 +471,7 @@
-
+

Channel 24

@@ -492,7 +492,7 @@
-
+

Channel 25

@@ -511,7 +511,7 @@
-
+

Channel 26

@@ -530,7 +530,7 @@
-
+

Channel 27

@@ -549,7 +549,7 @@
-
+

Channel 28

@@ -570,7 +570,7 @@
-
+

Channel 29

@@ -589,7 +589,7 @@
-
+

Channel 30

@@ -608,7 +608,7 @@
-
+

Channel 31

@@ -627,7 +627,7 @@
-
+

Channel 32

@@ -648,7 +648,7 @@
-
+

Channel 33

@@ -667,7 +667,7 @@
-
+

Channel 34

@@ -686,7 +686,7 @@
-
+

Channel 35

@@ -705,7 +705,7 @@
-
+

Channel 36

@@ -726,7 +726,7 @@
-
+

Channel 37

@@ -745,7 +745,7 @@
-
+

Channel 38

@@ -764,7 +764,7 @@
-
+

Channel 39

@@ -783,7 +783,7 @@
-
+

Channel 40

@@ -804,7 +804,7 @@
-
+

Channel 41

@@ -823,7 +823,7 @@
-
+

Channel 42

@@ -842,7 +842,7 @@
-
+

Channel 43

@@ -861,7 +861,7 @@
-
+

Channel 44

@@ -882,7 +882,7 @@
-
+

Channel 45

@@ -901,7 +901,7 @@
-
+

Channel 46

@@ -920,7 +920,7 @@
-
+

Channel 47

@@ -939,7 +939,7 @@
-
+

Channel 48

@@ -960,7 +960,7 @@
-
+

Channel 49

@@ -979,7 +979,7 @@
-
+

Channel 50

@@ -998,7 +998,7 @@
-
+

Channel 51

@@ -1017,7 +1017,7 @@
-
+

Channel 52

@@ -1038,7 +1038,7 @@
-
+

Channel 53

@@ -1057,7 +1057,7 @@
-
+

Channel 54

@@ -1076,7 +1076,7 @@
-
+

Channel 55

@@ -1095,7 +1095,7 @@
-
+

Channel 56

@@ -1116,7 +1116,7 @@
-
+

Channel 57

@@ -1135,7 +1135,7 @@
-
+

Channel 58

@@ -1154,7 +1154,7 @@
-
+

Channel 59

@@ -1173,7 +1173,7 @@
-
+

Channel 60

@@ -1194,7 +1194,7 @@
-
+

Channel 61

@@ -1213,7 +1213,7 @@
-
+

Channel 62

@@ -1232,7 +1232,7 @@
-
+

Channel 63

@@ -1251,7 +1251,7 @@
-
+

Channel 64

@@ -1274,20 +1274,20 @@
- -
+ +
NoDescriptionLanguageANN IDTypeMessage DetailsMessage TagsNoDescriptionLanguageANN IDTypeMessage DetailsMessage Tags
- - - - - - + + + + + + @@ -1310,12 +1310,12 @@
IndexDate TimeSourceTypeMessageBroadcast ZonesIndexDate TimeSourceTypeMessageBroadcast Zones
- - - - - - + + + + + + diff --git a/html/webpage/setting.html b/html/webpage/setting.html index e17dcd0..006b5b5 100644 --- a/html/webpage/setting.html +++ b/html/webpage/setting.html @@ -4,7 +4,7 @@ - AAS_NewGen_08OKT25 + AAS_NewGen_17OKT25 @@ -17,8 +17,70 @@

Setting

+
+
+
+
+

Upload Soundbank

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

FIS CODE

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/html/webpage/soundbank.html b/html/webpage/soundbank.html index 6db7233..4bc18ba 100644 --- a/html/webpage/soundbank.html +++ b/html/webpage/soundbank.html @@ -4,7 +4,7 @@ - AAS_NewGen_08OKT25 + AAS_NewGen_17OKT25 @@ -42,13 +42,13 @@
IndexDate TimeSourceTypeMessageBroadcast ZonesIndexDate TimeSourceTypeMessageBroadcast Zones
- - - - - - - + + + + + + + @@ -102,7 +102,7 @@

Path

-
+
diff --git a/html/webpage/streamerstatus.html b/html/webpage/streamerstatus.html index b251933..89ff588 100644 --- a/html/webpage/streamerstatus.html +++ b/html/webpage/streamerstatus.html @@ -4,7 +4,7 @@ - AAS_NewGen_08OKT25 + AAS_NewGen_17OKT25 diff --git a/html/webpage/timer.html b/html/webpage/timer.html index 458b974..71903aa 100644 --- a/html/webpage/timer.html +++ b/html/webpage/timer.html @@ -4,7 +4,7 @@ - AAS_NewGen_08OKT25 + AAS_NewGen_17OKT25 @@ -42,14 +42,15 @@
NoDescriptionTAGCategoryLanguageTypeFilenameNoDescriptionTAGCategoryLanguageTypeFilename
- - - - - - - - + + + + + + + + + @@ -87,37 +88,13 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -134,12 +111,12 @@
-
-
+
+

(H)

-
-
+
+

(M)

@@ -147,9 +124,25 @@
-

Sound Path

+

Message

+
+
+
+
+
+

Language

+
+
+
+
+
-
@@ -167,7 +160,13 @@

Broadcast Zones

-
+
diff --git a/html/webpage/usermanagement.html b/html/webpage/usermanagement.html index 2599c2a..1a26400 100644 --- a/html/webpage/usermanagement.html +++ b/html/webpage/usermanagement.html @@ -4,7 +4,7 @@ - AAS_NewGen_08OKT25 + AAS_NewGen_17OKT25 @@ -42,13 +42,13 @@
NoDescriptionDayTimeSound PathRepeatEnableBroadcast ZonesNoDescriptionDayTimeSound PathRepeatEnableBroadcast ZonesLanguage
- - - - - - - + + + + + + + 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) + } } }
NoUsernameLocationAirlineCityMessagebankBroadcast ZonesNoUsernameLocationAirlineCityMessagebankBroadcast Zones