Commit 12/08/2025

This commit is contained in:
2025-08-12 09:44:11 +07:00
parent f4c9fa8730
commit 60e8524c8f
33 changed files with 385 additions and 110 deletions

20
.idea/artifacts/EWS_Nanopi_Duo2.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<component name="ArtifactManager">
<artifact name="EWS Nanopi Duo2">
<output-path>$PROJECT_DIR$/out/artifacts/EWS_Nanopi_Duo2</output-path>
<root id="root">
<element id="archive" name="EWS_POC.jar">
<element id="directory" name="META-INF">
<element id="file-copy" path="$PROJECT_DIR$/meta/nanopi duo2/META-INF/MANIFEST.MF" />
</element>
<element id="module-output" name="EWS_POC" />
</element>
<element id="library" level="project" name="jetbrains.kotlinx.coroutines.core" />
<element id="library" level="project" name="java.websocket.Java.WebSocket" />
<element id="library" level="project" name="KotlinJavaRuntime" />
<element id="library" level="project" name="net.java.dev.jna" />
<element id="library" level="project" name="io.javalin.bundle" />
<element id="dir-copy" path="$PROJECT_DIR$/html" />
<element id="dir-copy" path="$PROJECT_DIR$/libs/linux-arm" />
</root>
</artifact>
</component>

View File

@@ -1,68 +0,0 @@
<component name="ArtifactManager">
<artifact type="jar" name="EWS_POC:jar">
<output-path>$PROJECT_DIR$/out/artifacts/EWS_POC_jar</output-path>
<root id="archive" name="EWS_POC.jar">
<element id="module-output" name="EWS_POC" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.2.0/kotlin-stdlib-2.2.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.10.2/kotlinx-coroutines-core-1.10.2.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlinx/kotlinx-coroutines-core-jvm/1.10.2/kotlinx-coroutines-core-jvm-1.10.2.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/annotations/23.0.0/annotations-23.0.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.1.0/kotlin-stdlib-2.1.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/net/java/dev/jna/jna/5.17.0/jna-5.17.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/io/javalin/javalin-bundle/6.7.0/javalin-bundle-6.7.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/io/javalin/javalin/6.7.0/javalin-6.7.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-server/11.0.25/jetty-server-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-http/11.0.25/jetty-http-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-jetty-server/11.0.25/websocket-jetty-server-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-servlet/11.0.25/jetty-servlet-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-security/11.0.25/jetty-security-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-webapp/11.0.25/jetty-webapp-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-xml/11.0.25/jetty-xml-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-jetty-api/11.0.25/websocket-jetty-api-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-jetty-common/11.0.25/websocket-jetty-common-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-core-common/11.0.25/websocket-core-common-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-servlet/11.0.25/websocket-servlet-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/websocket/websocket-core-server/11.0.25/websocket-core-server-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/io/javalin/javalin-context-mock/6.7.0/javalin-context-mock-6.7.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/toolchain/jetty-jakarta-servlet-api/5.0.2/jetty-jakarta-servlet-api-5.0.2.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/io/javalin/javalin-testtools/6.7.0/javalin-testtools-6.7.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/squareup/okhttp3/okhttp/4.12.0/okhttp-4.12.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/squareup/okio/okio/3.6.0/okio-3.6.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/squareup/okio/okio-jvm/3.6.0/okio-jvm-3.6.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-common/1.9.10/kotlin-stdlib-common-1.9.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/io/javalin/javalin-micrometer/6.7.0/javalin-micrometer-6.7.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/io/micrometer/micrometer-core/1.14.5/micrometer-core-1.14.5.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/io/micrometer/micrometer-commons/1.14.5/micrometer-commons-1.14.5.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/io/micrometer/micrometer-observation/1.14.5/micrometer-observation-1.14.5.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/hdrhistogram/HdrHistogram/2.2.2/HdrHistogram-2.2.2.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/latencyutils/LatencyUtils/2.0.3/LatencyUtils-2.0.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/io/micrometer/micrometer-jetty11/1.14.5/micrometer-jetty11-1.14.5.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-databind/2.18.3/jackson-databind-2.18.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-annotations/2.18.3/jackson-annotations-2.18.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/fasterxml/jackson/core/jackson-core/2.18.3/jackson-core-2.18.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/fasterxml/jackson/module/jackson-module-kotlin/2.18.3/jackson-module-kotlin-2.18.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-reflect/1.8.10/kotlin-reflect-1.8.10.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.18.3/jackson-datatype-jsr310-2.18.3.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/aayushatharva/brotli4j/brotli4j/1.18.0/brotli4j-1.18.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/aayushatharva/brotli4j/service/1.18.0/service-1.18.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/aayushatharva/brotli4j/native-windows-x86_64/1.18.0/native-windows-x86_64-1.18.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/http2/http2-server/11.0.25/http2-server-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/http2/http2-common/11.0.25/http2-common-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/http2/http2-hpack/11.0.25/http2-hpack-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-alpn-conscrypt-server/11.0.25/jetty-alpn-conscrypt-server-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/conscrypt/conscrypt-openjdk-uber/2.5.2/conscrypt-openjdk-uber-2.5.2.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-alpn-server/11.0.25/jetty-alpn-server-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-io/11.0.25/jetty-io-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/eclipse/jetty/jetty-util/11.0.25/jetty-util-11.0.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/ch/qos/logback/logback-classic/1.5.18/logback-classic-1.5.18.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/ch/qos/logback/logback-core/1.5.18/logback-core-1.5.18.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/1.9.25/kotlin-stdlib-jdk8-1.9.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/1.9.25/kotlin-stdlib-1.9.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/1.9.25/kotlin-stdlib-jdk7-1.9.25.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/java-websocket/Java-WebSocket/1.6.0/Java-WebSocket-1.6.0.jar" path-in-jar="/" />
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/slf4j/slf4j-api/2.0.13/slf4j-api-2.0.13.jar" path-in-jar="/" />
</root>
</artifact>
</component>

3
.idea/deployment.xml generated
View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="PublishConfigData" serverName="nanopi gtcdevice01" remoteFilesAllowedToDisappearOnAutoupload="false"> <component name="PublishConfigData" serverName="nanopi gtcdevice01" remoteFilesAllowedToDisappearOnAutoupload="false" confirmBeforeUploading="false">
<option name="confirmBeforeUploading" value="false" />
<serverData> <serverData>
<paths name="nanopi gtcdevice01"> <paths name="nanopi gtcdevice01">
<serverdata> <serverdata>

View File

@@ -4,6 +4,7 @@
<inspection_tool class="AiaStyle" enabled="false" level="TYPO" enabled_by_default="false" /> <inspection_tool class="AiaStyle" enabled="false" level="TYPO" enabled_by_default="false" />
<inspection_tool class="AssignedValueIsNeverRead" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="AssignedValueIsNeverRead" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="ClassName" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="ClassName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="ConstPropertyName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="DuplicatedCode" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="FunctionName" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> <inspection_tool class="FunctionName" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" /> <inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />

View File

@@ -1,3 +0,0 @@
Manifest-Version: 1.0
Main-Class: MainKt

View File

@@ -0,0 +1,27 @@
Manifest-Version: 1.0
Main-Class: MainKt
Class-Path: kotlinx-coroutines-core-1.10.2.jar kotlinx-coroutines-core-j
vm-1.10.2.jar annotations-23.0.0.jar kotlin-stdlib-2.1.0.jar Java-WebSo
cket-1.6.0.jar slf4j-api-2.0.13.jar kotlin-stdlib-2.2.0.jar annotations
-13.0.jar jna-5.17.0.jar javalin-bundle-6.7.0.jar javalin-6.7.0.jar slf
4j-api-2.0.17.jar jetty-server-11.0.25.jar jetty-http-11.0.25.jar webso
cket-jetty-server-11.0.25.jar jetty-servlet-11.0.25.jar jetty-security-
11.0.25.jar jetty-webapp-11.0.25.jar jetty-xml-11.0.25.jar websocket-je
tty-api-11.0.25.jar websocket-jetty-common-11.0.25.jar websocket-core-c
ommon-11.0.25.jar websocket-servlet-11.0.25.jar websocket-core-server-1
1.0.25.jar javalin-context-mock-6.7.0.jar jetty-jakarta-servlet-api-5.0
.2.jar javalin-testtools-6.7.0.jar okhttp-4.12.0.jar okio-3.6.0.jar oki
o-jvm-3.6.0.jar kotlin-stdlib-common-1.9.10.jar javalin-micrometer-6.7.
0.jar micrometer-core-1.14.5.jar micrometer-commons-1.14.5.jar micromet
er-observation-1.14.5.jar HdrHistogram-2.2.2.jar LatencyUtils-2.0.3.jar
micrometer-jetty11-1.14.5.jar jackson-databind-2.18.3.jar jackson-anno
tations-2.18.3.jar jackson-core-2.18.3.jar jackson-module-kotlin-2.18.3
.jar kotlin-reflect-1.8.10.jar jackson-datatype-jsr310-2.18.3.jar brotl
i4j-1.18.0.jar service-1.18.0.jar native-windows-x86_64-1.18.0.jar http
2-server-11.0.25.jar http2-common-11.0.25.jar http2-hpack-11.0.25.jar j
etty-alpn-conscrypt-server-11.0.25.jar conscrypt-openjdk-uber-2.5.2.jar
jetty-alpn-server-11.0.25.jar jetty-io-11.0.25.jar jetty-util-11.0.25.
jar logback-classic-1.5.18.jar logback-core-1.5.18.jar kotlin-stdlib-jd
k8-1.9.25.jar kotlin-stdlib-1.9.25.jar annotations-13.0.jar kotlin-stdl
ib-jdk7-1.9.25.jar

View File

@@ -14,7 +14,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import sbc.DigitalOutput
import sbc.NanopiDuo2
import somecodes.Codes import somecodes.Codes
import web.WsCommand import web.WsCommand
import java.util.function.BiFunction import java.util.function.BiFunction
@@ -24,11 +27,32 @@ import java.util.function.BiFunction
fun main() { fun main() {
val logger = LoggerFactory.getLogger("Main") val logger = LoggerFactory.getLogger("Main")
val objectMapper = jacksonObjectMapper() val objectMapper = jacksonObjectMapper()
logger.info("Application started, version 0.0.8")
val cfg = configFile() val cfg = configFile()
cfg.Load() cfg.Load()
var relay1 : DigitalOutput? = null
var relay2 : DigitalOutput? = null
var commandLED : DigitalOutput? = null
var streamingLED : DigitalOutput? = null
runBlocking {
relay1 = DigitalOutput(NanopiDuo2.CLK.linuxGpio, "Relay1", true)
relay2 = DigitalOutput(NanopiDuo2.MISO.linuxGpio, "Relay2", true)
commandLED = DigitalOutput(NanopiDuo2.TX1.linuxGpio, "CommandLED", true)
streamingLED = DigitalOutput(NanopiDuo2.RX1.linuxGpio, "StreamingLED", true)
relay1.setOFF()
relay2.setOFF()
commandLED.setOFF()
streamingLED.setOFF()
}
var audioID = 0 var audioID = 0
val preferedAudioDevice = "Speakers" val preferedAudioDevice = "USB Audio"
AudioUtility.LoadLibraries()
AudioUtility.PrintVersion()
AudioUtility.DetectPlaybackDevices().forEach { pair -> AudioUtility.DetectPlaybackDevices().forEach { pair ->
logger.info("Device ID: ${pair.first}, Name: ${pair.second}") logger.info("Device ID: ${pair.first}, Name: ${pair.second}")
@@ -36,10 +60,9 @@ fun main() {
audioID = pair.first audioID = pair.first
} }
} }
if (audioID!=0){ if (audioID==0) audioID = 1 // fallback to first device if preferred not found
val initsuccess = AudioUtility.InitDevice(audioID,44100) val initsuccess = AudioUtility.InitDevice(audioID,44100)
logger.info("Audio Device $audioID initialized: $initsuccess") logger.info("Audio Device $audioID initialized: $initsuccess")
}
// for Zello Client // for Zello Client
val o = OpusStreamReceiver(audioID) val o = OpusStreamReceiver(audioID)
@@ -113,12 +136,16 @@ fun main() {
override fun onConnected() { override fun onConnected() {
logger.info("Connected to Zello server.") logger.info("Connected to Zello server.")
relay1?.setOFF()
relay2?.setOFF()
} }
override fun onDisconnected(reason: String) { override fun onDisconnected(reason: String) {
logger.info("Disconnected from Zello Server, reason: $reason") logger.info("Disconnected from Zello Server, reason: $reason")
logger.info("Reconnecting after 10 seconds...") logger.info("Reconnecting after 10 seconds...")
z.Stop() z.Stop()
relay1?.setOFF()
relay2?.setOFF()
val e = this val e = this
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
@@ -136,8 +163,10 @@ fun main() {
// stop any previous playback // stop any previous playback
afp?.Stop() afp?.Stop()
afp = null afp = null
commandLED?.Blink()
if (o.Start()){ if (o.Start()){
relay1?.setON()
relay2?.setON()
logger.info("Opus Receiver ready for streaming from $from for $For on channel $channel") logger.info("Opus Receiver ready for streaming from $from for $For on channel $channel")
} else { } else {
logger.info("Failed to start Opus Receiver for streaming from $from for $For on channel $channel") logger.info("Failed to start Opus Receiver for streaming from $from for $For on channel $channel")
@@ -146,6 +175,9 @@ fun main() {
override fun onStopStreaming(from: String, For: String, channel: String) { override fun onStopStreaming(from: String, For: String, channel: String) {
o.Stop() o.Stop()
relay1?.setON()
relay2?.setOFF()
commandLED?.Blink()
logger.info("Opus Receiver stopped streaming from $from for $For on channel $channel") logger.info("Opus Receiver stopped streaming from $from for $For on channel $channel")
} }
@@ -156,6 +188,7 @@ fun main() {
data: ByteArray data: ByteArray
) { ) {
if (o.isPlaying) o.PushData(data) if (o.isPlaying) o.PushData(data)
streamingLED?.Blink()
} }
} }
z.Start(z_event) z.Start(z_event)
@@ -166,6 +199,7 @@ fun main() {
when (source) { when (source) {
"setting" -> when(cmd.command){ "setting" -> when(cmd.command){
"getConfig" ->{ "getConfig" ->{
commandLED?.Blink()
val data = mapOf( val data = mapOf(
"zelloUsername" to cfg.ZelloUsername, "zelloUsername" to cfg.ZelloUsername,
"zelloPassword" to cfg.ZelloPassword, "zelloPassword" to cfg.ZelloPassword,
@@ -187,6 +221,7 @@ fun main() {
WsReply(cmd.command, objectMapper.writeValueAsString(data).trim()) WsReply(cmd.command, objectMapper.writeValueAsString(data).trim())
} }
"setZelloConfig" -> { "setZelloConfig" -> {
commandLED?.Blink()
try{ try{
val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {}) val xx = objectMapper.readValue(cmd.data, object: TypeReference<Map<String, String>>() {})
var changed = false var changed = false
@@ -220,6 +255,7 @@ fun main() {
z = CreateZelloFromConfig() z = CreateZelloFromConfig()
z.Start(z_event) z.Start(z_event)
WsReply(cmd.command,"success") WsReply(cmd.command,"success")
} else WsReply(cmd.command,"No changes made") } else WsReply(cmd.command,"No changes made")
} catch (e: Exception){ } catch (e: Exception){
@@ -228,6 +264,7 @@ fun main() {
} }
"setMessageConfig"-> { "setMessageConfig"-> {
commandLED?.Blink()
try{ try{
val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {}) val xx = objectMapper.readValue(cmd.data, object : TypeReference<Map<String, String>>() {})
@@ -278,6 +315,7 @@ fun main() {
"prerecordedbroadcast" -> when(cmd.command){ "prerecordedbroadcast" -> when(cmd.command){
"getMessageConfig" ->{ "getMessageConfig" ->{
commandLED?.Blink()
val data = mapOf( val data = mapOf(
"M1" to cfg.M1, "M1" to cfg.M1,
"M2" to cfg.M2, "M2" to cfg.M2,
@@ -291,6 +329,7 @@ fun main() {
WsReply(cmd.command, objectMapper.writeValueAsString(data)) WsReply(cmd.command, objectMapper.writeValueAsString(data))
} }
"getPlaybackStatus" ->{ "getPlaybackStatus" ->{
commandLED?.Blink()
if (afp!=null && true==afp?.isPlaying){ if (afp!=null && true==afp?.isPlaying){
WsReply(cmd.command, "Playing: ${afp?.filename}, Duration: ${afp?.duration?.toInt()}, Elapsed: ${afp?.elapsed?.toInt()} seconds") WsReply(cmd.command, "Playing: ${afp?.filename}, Duration: ${afp?.duration?.toInt()}, Elapsed: ${afp?.elapsed?.toInt()} seconds")
} else { } else {
@@ -298,7 +337,7 @@ fun main() {
} }
} }
"playMessage" ->{ "playMessage" ->{
commandLED?.Blink()
afp?.Stop() afp?.Stop()
afp = null afp = null
// stop Opus Receiver if it is running // stop Opus Receiver if it is running
@@ -317,14 +356,23 @@ fun main() {
null null
} }
} }
relay1?.setOFF()
relay2?.setOFF()
if (filename!=null){ if (filename!=null){
try{ try{
afp= AudioFilePlayer(audioID, filename) afp= AudioFilePlayer(audioID, filename)
afp?.Play { _ -> afp = null} afp?.Play { _ ->
afp = null
relay1?.setOFF()
relay2?.setOFF()
}
relay1?.setON()
relay2?.setON()
WsReply(cmd.command,"success") WsReply(cmd.command,"success")
} catch (e: Exception){ } catch (e: Exception){
afp?.Stop() afp?.Stop()
afp = null afp = null
WsReply(cmd.command, "failed: ${e.message}") WsReply(cmd.command, "failed: ${e.message}")
} }
@@ -336,14 +384,18 @@ fun main() {
} }
"stopMessage" ->{ "stopMessage" ->{
commandLED?.Blink()
afp?.Stop() afp?.Stop()
afp = null afp = null
relay1?.setOFF()
relay2?.setOFF()
WsReply(cmd.command,"success") WsReply(cmd.command,"success")
} }
else -> WsReply(cmd.command,"Invalid command: ${cmd.command}") else -> WsReply(cmd.command,"Invalid command: ${cmd.command}")
} }
"pocreceiver" -> when(cmd.command){ "pocreceiver" -> when(cmd.command){
"getZelloStatus" -> { "getZelloStatus" -> {
commandLED?.Blink()
var status = "Disconnected" var status = "Disconnected"
if (z.currentChannel?.isNotBlank() == true){ if (z.currentChannel?.isNotBlank() == true){
status = "Channel: ${z.currentChannel}, Online: ${z.isOnline}, Username: ${z.username}" status = "Channel: ${z.currentChannel}, Online: ${z.isOnline}, Username: ${z.username}"

View File

@@ -1,5 +1,6 @@
package audio package audio
import audio.AudioUtility.Companion.bass
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -15,7 +16,7 @@ import kotlin.io.path.exists
*/ */
@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) {
private val bass: Bass = Bass.Instance
private var filehandle = 0 private var filehandle = 0
var isPlaying = false var isPlaying = false
val fileSize: Long val fileSize: Long
@@ -25,6 +26,7 @@ class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate:
val fullpath = Codes.audioFilePath.resolve(filename) val fullpath = Codes.audioFilePath.resolve(filename)
if (fullpath.exists()){ if (fullpath.exists()){
if (AudioUtility.InitDevice(deviceID, device_samplingrate)) { if (AudioUtility.InitDevice(deviceID, device_samplingrate)) {
AudioUtility.setVolumeOutput(deviceID,1.0f)
filehandle = bass.BASS_StreamCreateFile(false, fullpath.absolutePathString(), 0, 0, 0) filehandle = bass.BASS_StreamCreateFile(false, fullpath.absolutePathString(), 0, 0, 0)
if (filehandle!=0){ if (filehandle!=0){
fileSize = bass.BASS_ChannelGetLength(filehandle, Bass.BASS_POS_BYTE) fileSize = bass.BASS_ChannelGetLength(filehandle, Bass.BASS_POS_BYTE)
@@ -47,6 +49,7 @@ class AudioFilePlayer(deviceID: Int, val filename: String, device_samplingrate:
fun Play(finished: Consumer<Any> ) : Boolean{ fun Play(finished: Consumer<Any> ) : Boolean{
if (bass.BASS_ChannelPlay(filehandle, false)){ if (bass.BASS_ChannelPlay(filehandle, false)){
bass.BASS_ChannelSetAttribute(filehandle, Bass.BASS_ATTRIB_VOL, 1.0f)
elapsed = 0.0 elapsed = 0.0
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
isPlaying = true isPlaying = true

View File

@@ -1,15 +1,63 @@
package audio package audio
import com.sun.jna.Platform
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.isRegularFile
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
@Suppress("unused")
class AudioUtility { class AudioUtility {
companion object{
val bass : Bass = Bass.Instance
private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java) private val logger : Logger = LoggerFactory.getLogger(AudioUtility::class.java)
fun ExtractLibraries(parent: String, filename: String, targetPath: Path) : Path?{
if (targetPath.resolve(filename).isRegularFile()) return targetPath.resolve(filename) // already exists
val out = targetPath.resolve(filename)
try{
AudioUtility::class.java.getResourceAsStream("$parent/$filename").use { ins ->
requireNotNull(ins) { "Resource $parent/$filename not found" }
Files.copy(ins, out, REPLACE_EXISTING)
}
out.toFile().setReadable(true, false)
out.toFile().setExecutable(true, false)
return out
} catch (_: Exception){
return null
}
companion object{ }
private val bass = Bass.Instance
fun LoadLibraries(){
val runfolder = Path.of(System.getProperty("user.dir"))
//logger.info("Checking for BASS libraries in $runfolder")
val resourceprefix = Platform.RESOURCE_PREFIX
logger.info("Resource prefix : $resourceprefix")
val bass = ExtractLibraries(resourceprefix, System.mapLibraryName("bass"), runfolder)
val bassopus = ExtractLibraries(resourceprefix, System.mapLibraryName("bassopus"), runfolder)
if (bass!=null){
System.load(bass.toString())
}
if (bassopus!=null){
System.load(bassopus.toString())
}
}
fun PrintVersion(){
logger.info("BASS Version: ${bass.BASS_GetVersion().toHexString()}")
}
fun LoadPlugin(plugin: String){
if (bass.BASS_PluginLoad(plugin, 0)>0){
logger.info("Plugin $plugin loaded successfully")
} else {
logger.error("Failed to load plugin $plugin: ${bass.BASS_ErrorGetCode()}")
}
}
fun DetectPlaybackDevices() : List<Pair<Int, String>> { fun DetectPlaybackDevices() : List<Pair<Int, String>> {
val result = ArrayList<Pair<Int, String>>() val result = ArrayList<Pair<Int, String>>()
@@ -24,6 +72,14 @@ class AudioUtility {
return result return result
} }
fun setVolumeOutput(deviceID: Int, value: Float){
var vol = value
if (vol < 0) vol = 0f
if (vol > 1) vol = 1f
bass.BASS_SetDevice(deviceID)
bass.BASS_SetVolume(vol)
}
fun InitDevice(deviceID: Int, device_samplingrate: Int = 48000) : Boolean { fun InitDevice(deviceID: Int, device_samplingrate: Int = 48000) : Boolean {
val dev = Bass.BASS_DEVICEINFO() val dev = Bass.BASS_DEVICEINFO()
if (bass.BASS_GetDeviceInfo(deviceID, dev)) { if (bass.BASS_GetDeviceInfo(deviceID, dev)) {
@@ -54,9 +110,7 @@ class AudioUtility {
} }
} }
init{
logger.info("Bass Version = ${bass.BASS_GetVersion().toHexString()}")
}

View File

@@ -8,6 +8,7 @@ import com.sun.jna.*;
public interface Bass extends Library { public interface Bass extends Library {
Bass Instance = Native.load("bass", Bass.class); Bass Instance = Native.load("bass", Bass.class);
int BASSVERSION = 0x204; // API version int BASSVERSION = 0x204; // API version
String BASSVERSIONTEXT = "2.4"; String BASSVERSIONTEXT = "2.4";

View File

@@ -1,5 +1,6 @@
package audio package audio
import audio.AudioUtility.Companion.bass
import com.sun.jna.Memory import com.sun.jna.Memory
import com.sun.jna.Pointer import com.sun.jna.Pointer
import org.slf4j.Logger import org.slf4j.Logger
@@ -13,11 +14,11 @@ import org.slf4j.LoggerFactory
*/ */
@Suppress("unused") @Suppress("unused")
class OpusStreamReceiver(val deviceID: Int, val samplingrate: Int = 16000) { class OpusStreamReceiver(val deviceID: Int, val samplingrate: Int = 16000) {
private val bass = Bass.Instance
private val bassopus = BASSOPUS.Instance
private var filehandle = 0 private var filehandle = 0
private val logger : Logger = LoggerFactory.getLogger(OpusStreamReceiver::class.java) private val logger : Logger = LoggerFactory.getLogger(OpusStreamReceiver::class.java)
var isPlaying = false var isPlaying = false
val bassopus : BASSOPUS = BASSOPUS.Instance
@@ -34,9 +35,12 @@ class OpusStreamReceiver(val deviceID: Int, val samplingrate: Int = 16000) {
opushead.channels = 1 opushead.channels = 1
opushead.inputrate = samplingrate opushead.inputrate = samplingrate
val procpush = Pointer(-1) val procpush = Pointer(-1)
AudioUtility.setVolumeOutput(deviceID, 1.0f)
filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null) filehandle = bassopus.BASS_OPUS_StreamCreate(opushead,0, procpush, null)
if (filehandle != 0){ if (filehandle != 0){
if (bass.BASS_ChannelPlay(filehandle,false)){ if (bass.BASS_ChannelPlay(filehandle,false)){
bass.BASS_ChannelSetAttribute(filehandle, Bass.BASS_ATTRIB_VOL, 1.0f)
isPlaying = true isPlaying = true
return true return true
} else logger.error("BASS_ChannelPlay failed for filehandle $filehandle, code ${bass.BASS_ErrorGetCode()}") } else logger.error("BASS_ChannelPlay failed for filehandle $filehandle, code ${bass.BASS_ErrorGetCode()}")

157
src/sbc/DigitalOutput.kt Normal file
View File

@@ -0,0 +1,157 @@
package sbc
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import somecodes.Codes.Companion.gpioExportPath
import somecodes.Codes.Companion.gpioPath
import somecodes.Codes.Companion.gpioUnexportPath
import somecodes.Codes.Companion.haveGpioSupport
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.isDirectory
/**
* Create a digital output for a GPIO pin.
* @param linuxGpio The GPIO pin number in Linux.
* @param name The name of the digital output.
* @param activeHigh If true, the output is active high (default is true).
* @property inited Indicates whether the digital output has been initialized.
*
*/
@Suppress("unused")
class DigitalOutput(val linuxGpio: Int, val name: String, val activeHigh: Boolean = true) {
var inited = false; private set
private var valuepath: Path? = null
private val logger = LoggerFactory.getLogger("DigitalOutput $name ($linuxGpio)")
init{
if (haveGpioSupport()){
if (checkExists()){
// already exists
try{
// try to set direction to output
val dir = gpioPath.resolve("gpio$linuxGpio").resolve("direction")
Files.writeString(dir, "out")
// successfully set direction to output
inited = true
valuepath = gpioPath.resolve("gpio$linuxGpio").resolve("value")
logger.info("GPIO $name GPIO $linuxGpio already exists as output")
} catch (e : Exception){
// failed to set direction to output, log error
logger.error("Failed to set existing GPIO $name GPIO $linuxGpio as output: ${e.message}")
}
} else {
// not yet exists, export it
try{
// export the GPIO pin
Files.writeString(gpioExportPath, linuxGpio.toString())
if (checkExists()){
// successfully exported, now set direction to output
val dir = gpioPath.resolve("gpio$linuxGpio").resolve("direction")
Files.writeString(dir, "out")
inited = true
valuepath = gpioPath.resolve("gpio$linuxGpio").resolve("value")
logger.info("Initialized GPIO $name GPIO $linuxGpio as output")
} else {
// failed to export, log error
logger.error("GPIO $name GPIO $linuxGpio does not exist after export")
}
} catch (e: Exception){
// failed to export GPIO pin, log error
logger.error("Failed to export GPIO $name GPIO $linuxGpio: ${e.message}")
}
}
} else logger.error("DigitalOutput $name GPIO $linuxGpio not initialized: GPIO support not available")
}
fun checkExists(): Boolean{
return try {
val gpioPath = gpioPath.resolve("gpio$linuxGpio")
if (gpioPath.isDirectory() && gpioPath.toFile().exists()){
val dir = gpioPath.resolve("direction").toFile()
val value = gpioPath.resolve("value").toFile()
if (dir.exists() && value.exists()) {
return dir.canWrite() && value.canWrite()
}
}
false
} catch (e: Exception) {
logger.error("Error checking if GPIO $linuxGpio exists: ${e.message}")
false
}
}
/**
* Release the GPIO pin by unexporting it.
*/
fun release(){
if (inited){
try{
Files.writeString(gpioUnexportPath, linuxGpio.toString())
inited = false
valuepath = null
} catch (e : Exception){
logger.error("Failed to unexport GPIO $name GPIO ${e.message}")
}
}
}
/**
* Set Digital Output to ON
*/
fun setON() : Boolean{
if (inited){
if (valuepath!=null){
try{
Files.writeString(valuepath!!, if (activeHigh) "1" else "0")
return true
} catch (e: Exception){
logger.error("Failed to set GPIO $name GPIO $linuxGpio ON: ${e.message}")
}
}
}
return false
}
/**
* Set Digital Output to OFF
*/
fun setOFF() : Boolean{
if (inited){
if (valuepath!=null){
try{
Files.writeString(valuepath!!, if (activeHigh) "0" else "1")
return true
} catch (e : Exception){
logger.error("Failed to set GPIO $name GPIO $linuxGpio OFF: ${e.message}")
}
}
}
return false
}
/**
* Blink the Digital Output for a specified duration.
* @param ms The duration in milliseconds to keep the output ON before turning it OFF (default is 50ms).
*/
fun Blink(ms: Long = 50){
if (inited){
CoroutineScope(Dispatchers.IO).launch {
setON()
delay(ms)
setOFF()
}
}
}
}

21
src/sbc/NanopiDuo2.kt Normal file
View File

@@ -0,0 +1,21 @@
package sbc
/**
* Nanopi Duo2 pin and GPIO mapping.
* Source : https://wiki.friendlyelec.com/wiki/index.php/NanoPi_Duo2
*/
@Suppress("unused")
enum class NanopiDuo2(val pin: Int, val linuxGpio: Int) {
IRRX(9,363),
PG11(11,203),
RXD(2,5),
TXD(4,4),
SCL(8,11),
SDA(10,12),
CS(12,13),
CLK(14,14),
MISO(16,16),
MOSI(18,15),
RX1(20,199),
TX1(22,198)
}

View File

@@ -1,6 +1,9 @@
package somecodes package somecodes
import com.sun.jna.Platform
import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.nio.file.Path
import kotlin.io.path.Path import kotlin.io.path.Path
@Suppress("unused") @Suppress("unused")
@@ -8,10 +11,25 @@ class Codes {
companion object{ companion object{
val audioFilePath = Path(System.getProperty("user.dir"), "audiofile") val audioFilePath = Path(System.getProperty("user.dir"), "audiofile")
val gpioPath : Path = Path("/sys/class/gpio")
val gpioExportPath : Path = gpioPath.resolve("export")
val gpioUnexportPath : Path = gpioPath.resolve("unexport")
private val logger = LoggerFactory.getLogger("Codes")
private val validAudioExtensions = setOf("wav", "mp3") private val validAudioExtensions = setOf("wav", "mp3")
private val KB_size = 1024 // 1 KB = 1024 bytes private const val KB_size = 1024 // 1 KB = 1024 bytes
private val MB_size = 1024 * KB_size // 1 MB = 1024 KB private const val MB_size = 1024 * KB_size // 1 MB = 1024 KB
private val GB_size = 1024 * MB_size // 1 GB = 1024 MB private const val GB_size = 1024 * MB_size // 1 GB = 1024 MB
fun haveGpioSupport() : Boolean {
if (Platform.isLinux()){
if (gpioExportPath.toFile().exists() && gpioUnexportPath.toFile().exists()) {
if (gpioExportPath.toFile().canWrite() && gpioUnexportPath.toFile().canWrite()) {
return true
} else logger.error("$gpioExportPath or $gpioUnexportPath is not writable")
} else logger.error("$gpioExportPath or $gpioUnexportPath does not exist")
} else logger.error("Platform is not Linux, GPIO support is not available")
return false
}
fun SizeToString(size: Long) : String { fun SizeToString(size: Long) : String {
return when { return when {

View File

@@ -35,7 +35,7 @@ public class webApp {
} }
app = Javalin.create(config -> { app = Javalin.create(config -> {
config.useVirtualThreads = true; // Enable virtual threads for better performance config.useVirtualThreads = true; // Enable virtual threads for better performance
config.staticFiles.add("/"); config.staticFiles.add("/webpage");
config.router.apiBuilder(()->{ config.router.apiBuilder(()->{
path("/", () -> get(ctx -> { path("/", () -> get(ctx -> {
if (ctx.sessionAttribute("user") == null) { if (ctx.sessionAttribute("user") == null) {

View File

@@ -128,7 +128,7 @@ class ZelloClient(val address : URI, val username: String, val password: String,
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun onMessage(message: String?) { override fun onMessage(message: String?) {
logger.info("Message received: $message") //logger.info("Message received: $message")
val jsnode = mapper.readTree(message) val jsnode = mapper.readTree(message)
if (jsnode["seq"] != null) { if (jsnode["seq"] != null) {
when(val seq = jsnode.get("seq").asInt()){ when(val seq = jsnode.get("seq").asInt()){
@@ -229,19 +229,6 @@ class ZelloClient(val address : URI, val username: String, val password: String,
isOnline = false isOnline = false
currentChannel = null currentChannel = null
//Revisi 06/08/2025 : Change to Coroutines
// val thread = Thread {
// try {
// Thread.sleep(10000)
// connect()
// } catch (e: InterruptedException) {
// logger.error("Reconnection interrupted: ${e.message}")
// }
// }
// thread.name= "ZelloClient-ReconnectThread"
// thread.isDaemon = true
// thread.start()
} }
override fun onError(ex: Exception?) { override fun onError(ex: Exception?) {