mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: serial comm (#141)
This commit is contained in:
@@ -240,4 +240,8 @@ https://github.com/mixpanel/mixpanel-java/blob/master/LICENSE
|
||||
|
||||
json-20231013
|
||||
Public Domain.
|
||||
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
||||
https://github.com/stleary/JSON-java/blob/master/LICENSE
|
||||
|
||||
jSerialComm 2.11.0
|
||||
Apache License 2.0
|
||||
https://github.com/Fazecast/jSerialComm/blob/master/LICENSE-APACHE-2.0
|
||||
@@ -1,5 +1,6 @@
|
||||
import org.gradle.internal.jvm.Jvm
|
||||
import org.gradle.kotlin.dsl.support.uppercaseFirstChar
|
||||
import org.gradle.nativeplatform.platform.internal.ArchitectureInternal
|
||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||
import org.jetbrains.kotlin.org.apache.commons.io.FileUtils
|
||||
import org.jetbrains.kotlin.org.apache.commons.lang3.StringUtils
|
||||
@@ -17,7 +18,7 @@ group = "app.termora"
|
||||
version = "1.0.5"
|
||||
|
||||
val os: OperatingSystem = DefaultNativePlatform.getCurrentOperatingSystem()
|
||||
val arch: Architecture = DefaultNativePlatform.getCurrentArchitecture()
|
||||
val arch: ArchitectureInternal = DefaultNativePlatform.getCurrentArchitecture()
|
||||
|
||||
// macOS 签名信息
|
||||
val macOSSignUsername = System.getenv("TERMORA_MAC_SIGN_USER_NAME") ?: StringUtils.EMPTY
|
||||
@@ -104,6 +105,7 @@ dependencies {
|
||||
implementation(libs.bip39)
|
||||
implementation(libs.colorpicker)
|
||||
implementation(libs.mixpanel)
|
||||
implementation(libs.jSerialComm)
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -148,6 +150,8 @@ tasks.register<Copy>("copy-dependencies") {
|
||||
val jna = libs.jna.asProvider().get()
|
||||
val dylib = dir.get().dir("dylib").asFile
|
||||
val pty4j = libs.pty4j.get()
|
||||
val jSerialComm = libs.jSerialComm.get()
|
||||
|
||||
for (file in dir.get().asFile.listFiles() ?: emptyArray()) {
|
||||
if ("${jna.name}-${jna.version}" == file.nameWithoutExtension) {
|
||||
val targetDir = File(dylib, jna.name)
|
||||
@@ -172,6 +176,21 @@ tasks.register<Copy>("copy-dependencies") {
|
||||
// @formatter:on
|
||||
// 删除所有二进制类库
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "resources/*") }
|
||||
} else if ("${jSerialComm.name}-${jSerialComm.version}" == file.nameWithoutExtension) {
|
||||
val archName = if (arch.isArm) "aarch64" else "x86_64"
|
||||
val targetDir = FileUtils.getFile(dylib, jSerialComm.name, "OSX", archName)
|
||||
FileUtils.forceMkdir(targetDir)
|
||||
// @formatter:off
|
||||
exec { commandLine("unzip", "-j" , "-o", file.absolutePath, "OSX/${archName}/*", "-d", targetDir.absolutePath) }
|
||||
// @formatter:on
|
||||
// 删除所有二进制类库
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Android/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "FreeBSD/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Linux/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "OpenBSD/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "OSX/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Solaris/*") }
|
||||
exec { commandLine("zip", "-d", file.absolutePath, "Windows/*") }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ rhino = "1.7.15"
|
||||
delight-rhino-sandbox = "0.0.17"
|
||||
testcontainers = "1.20.4"
|
||||
mixpanel = "1.5.3"
|
||||
jSerialComm="2.11.0"
|
||||
|
||||
[libraries]
|
||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||
@@ -97,6 +98,7 @@ rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||
delight-rhino-sandbox = { module = "org.javadelight:delight-rhino-sandbox", version.ref = "delight-rhino-sandbox" }
|
||||
colorpicker = { module = "org.drjekyll:colorpicker", version.ref = "colorpicker" }
|
||||
mixpanel = { module = "com.mixpanel:mixpanel-java", version.ref = "mixpanel" }
|
||||
jSerialComm = { module = "com.fazecast:jSerialComm", version.ref = "jSerialComm" }
|
||||
|
||||
[plugins]
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
|
||||
@@ -22,10 +22,6 @@ class ChannelShellPtyConnector(
|
||||
output.flush()
|
||||
}
|
||||
|
||||
override fun write(buffer: String) {
|
||||
write(buffer.toByteArray(charset))
|
||||
}
|
||||
|
||||
override fun resize(rows: Int, cols: Int) {
|
||||
channel.sendWindowChange(cols, rows)
|
||||
}
|
||||
@@ -38,4 +34,8 @@ class ChannelShellPtyConnector(
|
||||
override fun close() {
|
||||
channel.close(true)
|
||||
}
|
||||
|
||||
override fun getCharset(): Charset {
|
||||
return charset
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,16 @@ class EditHostOptionsPane(private val host: Host) : HostOptionsPane() {
|
||||
}
|
||||
|
||||
jumpHostsOption.filter = { it.id != host.id }
|
||||
|
||||
val serialComm = host.options.serialComm
|
||||
if (serialComm.port.isNotBlank()) {
|
||||
serialCommOption.serialPortComboBox.selectedItem = serialComm.port
|
||||
}
|
||||
serialCommOption.baudRateComboBox.selectedItem = serialComm.baudRate
|
||||
serialCommOption.dataBitsComboBox.selectedItem = serialComm.dataBits
|
||||
serialCommOption.parityComboBox.selectedItem = serialComm.parity
|
||||
serialCommOption.stopBitsComboBox.selectedItem = serialComm.stopBits
|
||||
serialCommOption.flowControlComboBox.selectedItem = serialComm.flowControl
|
||||
}
|
||||
|
||||
override fun getHost(): Host {
|
||||
|
||||
@@ -13,6 +13,7 @@ enum class Protocol {
|
||||
Folder,
|
||||
SSH,
|
||||
Local,
|
||||
Serial
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +40,53 @@ data class Authentication(
|
||||
}
|
||||
}
|
||||
|
||||
enum class SerialCommParity {
|
||||
None,
|
||||
Even,
|
||||
Odd,
|
||||
Mark,
|
||||
Space
|
||||
}
|
||||
|
||||
enum class SerialCommFlowControl {
|
||||
None,
|
||||
RTS_CTS,
|
||||
XON_XOFF,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SerialComm(
|
||||
/**
|
||||
* 串口
|
||||
*/
|
||||
val port: String = StringUtils.EMPTY,
|
||||
|
||||
/**
|
||||
* 波特率
|
||||
*/
|
||||
val baudRate: Int = 9600,
|
||||
|
||||
/**
|
||||
* 数据位:5、6、7、8
|
||||
*/
|
||||
val dataBits: Int = 8,
|
||||
|
||||
/**
|
||||
* 停止位: 1、1.5、2
|
||||
*/
|
||||
val stopBits: String = "1",
|
||||
|
||||
/**
|
||||
* 校验位
|
||||
*/
|
||||
val parity: SerialCommParity = SerialCommParity.None,
|
||||
|
||||
/**
|
||||
* 流控
|
||||
*/
|
||||
val flowControl: SerialCommFlowControl = SerialCommFlowControl.None,
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
data class Options(
|
||||
@@ -61,7 +109,12 @@ data class Options(
|
||||
/**
|
||||
* SSH 心跳间隔
|
||||
*/
|
||||
val heartbeatInterval: Int = 30
|
||||
val heartbeatInterval: Int = 30,
|
||||
|
||||
/**
|
||||
* 串口配置
|
||||
*/
|
||||
val serialComm: SerialComm = SerialComm(),
|
||||
) {
|
||||
companion object {
|
||||
val Default = Options()
|
||||
|
||||
@@ -67,37 +67,53 @@ class HostDialog(owner: Window, host: Host? = null) : DialogWrapper(owner) {
|
||||
|
||||
private suspend fun testConnection(host: Host) {
|
||||
val owner = this
|
||||
if (host.protocol != Protocol.SSH) {
|
||||
if (host.protocol == Protocol.Local) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
OptionPane.showMessageDialog(owner, I18n.getString("termora.new-host.test-connection-successful"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (host.protocol == Protocol.SSH) {
|
||||
testSSH(host)
|
||||
} else if (host.protocol == Protocol.Serial) {
|
||||
testSerial(host)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
OptionPane.showMessageDialog(
|
||||
owner, ExceptionUtils.getMessage(e),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Swing) {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
I18n.getString("termora.new-host.test-connection-successful")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun testSSH(host: Host) {
|
||||
var client: SshClient? = null
|
||||
var session: ClientSession? = null
|
||||
try {
|
||||
client = SshClients.openClient(host)
|
||||
client.userInteraction = TerminalUserInteraction(owner)
|
||||
session = SshClients.openSession(host, client)
|
||||
withContext(Dispatchers.Swing) {
|
||||
OptionPane.showMessageDialog(
|
||||
owner,
|
||||
I18n.getString("termora.new-host.test-connection-successful")
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
OptionPane.showMessageDialog(
|
||||
owner, ExceptionUtils.getRootCauseMessage(e),
|
||||
messageType = JOptionPane.ERROR_MESSAGE
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
session?.close()
|
||||
client?.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun testSerial(host: Host) {
|
||||
Serials.openPort(host).closePort()
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
|
||||
@@ -2,11 +2,17 @@ package app.termora
|
||||
|
||||
import app.termora.keymgr.KeyManager
|
||||
import app.termora.keymgr.KeyManagerDialog
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.ui.FlatTextBorder
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.awt.*
|
||||
import java.awt.event.*
|
||||
@@ -22,6 +28,7 @@ open class HostOptionsPane : OptionsPane() {
|
||||
protected val proxyOption = ProxyOption()
|
||||
protected val terminalOption = TerminalOption()
|
||||
protected val jumpHostsOption = JumpHostsOption()
|
||||
protected val serialCommOption = SerialCommOption()
|
||||
protected val owner: Window get() = SwingUtilities.getWindowAncestor(this)
|
||||
|
||||
init {
|
||||
@@ -30,6 +37,7 @@ open class HostOptionsPane : OptionsPane() {
|
||||
addOption(tunnelingOption)
|
||||
addOption(jumpHostsOption)
|
||||
addOption(terminalOption)
|
||||
addOption(serialCommOption)
|
||||
|
||||
setContentBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8))
|
||||
}
|
||||
@@ -43,6 +51,7 @@ open class HostOptionsPane : OptionsPane() {
|
||||
var authentication = Authentication.No
|
||||
var proxy = Proxy.No
|
||||
|
||||
|
||||
if (generalOption.authenticationTypeComboBox.selectedItem == AuthenticationType.Password) {
|
||||
authentication = authentication.copy(
|
||||
type = AuthenticationType.Password,
|
||||
@@ -66,12 +75,23 @@ open class HostOptionsPane : OptionsPane() {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val serialComm = SerialComm(
|
||||
port = serialCommOption.serialPortComboBox.selectedItem?.toString() ?: StringUtils.EMPTY,
|
||||
baudRate = serialCommOption.baudRateComboBox.selectedItem?.toString()?.toIntOrNull() ?: 9600,
|
||||
dataBits = serialCommOption.dataBitsComboBox.selectedItem as Int? ?: 8,
|
||||
stopBits = serialCommOption.stopBitsComboBox.selectedItem as String? ?: "1",
|
||||
parity = serialCommOption.parityComboBox.selectedItem as SerialCommParity,
|
||||
flowControl = serialCommOption.flowControlComboBox.selectedItem as SerialCommFlowControl
|
||||
)
|
||||
|
||||
val options = Options.Default.copy(
|
||||
encoding = terminalOption.charsetComboBox.selectedItem as String,
|
||||
env = terminalOption.environmentTextArea.text,
|
||||
startupCommand = terminalOption.startupCommandTextField.text,
|
||||
heartbeatInterval = (terminalOption.heartbeatIntervalTextField.value ?: 30) as Int,
|
||||
jumpHosts = jumpHostsOption.jumpHosts.map { it.id }
|
||||
jumpHosts = jumpHostsOption.jumpHosts.map { it.id },
|
||||
serialComm = serialComm
|
||||
)
|
||||
|
||||
return Host(
|
||||
@@ -103,6 +123,12 @@ open class HostOptionsPane : OptionsPane() {
|
||||
if (validateField(generalOption.usernameTextField)) {
|
||||
return false
|
||||
}
|
||||
} else if (host.protocol == Protocol.Serial) {
|
||||
if (validateField(serialCommOption.serialPortComboBox)
|
||||
|| validateField(serialCommOption.baudRateComboBox)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (host.authentication.type == AuthenticationType.Password) {
|
||||
@@ -152,7 +178,8 @@ open class HostOptionsPane : OptionsPane() {
|
||||
* 返回 true 表示有错误
|
||||
*/
|
||||
private fun validateField(comboBox: JComboBox<*>): Boolean {
|
||||
if (comboBox.isEnabled && comboBox.selectedItem == null) {
|
||||
val selectedItem = comboBox.selectedItem
|
||||
if (comboBox.isEnabled && (selectedItem == null || (selectedItem is String && selectedItem.isBlank()))) {
|
||||
selectOptionJComponent(comboBox)
|
||||
comboBox.putClientProperty(FlatClientProperties.OUTLINE, FlatClientProperties.OUTLINE_ERROR)
|
||||
comboBox.requestFocusInWindow()
|
||||
@@ -259,6 +286,7 @@ open class HostOptionsPane : OptionsPane() {
|
||||
|
||||
protocolTypeComboBox.addItem(Protocol.SSH)
|
||||
protocolTypeComboBox.addItem(Protocol.Local)
|
||||
protocolTypeComboBox.addItem(Protocol.Serial)
|
||||
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.No)
|
||||
authenticationTypeComboBox.addItem(AuthenticationType.Password)
|
||||
@@ -328,7 +356,9 @@ open class HostOptionsPane : OptionsPane() {
|
||||
passwordTextField.isEnabled = true
|
||||
chooseKeyBtn.isEnabled = true
|
||||
|
||||
if (protocolTypeComboBox.selectedItem == Protocol.Local) {
|
||||
if (protocolTypeComboBox.selectedItem == Protocol.Local
|
||||
|| protocolTypeComboBox.selectedItem == Protocol.Serial
|
||||
) {
|
||||
hostTextField.isEnabled = false
|
||||
portTextField.isEnabled = false
|
||||
usernameTextField.isEnabled = false
|
||||
@@ -901,6 +931,127 @@ open class HostOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class SerialCommOption : JPanel(BorderLayout()), Option {
|
||||
val serialPortComboBox = OutlineComboBox<String>()
|
||||
val baudRateComboBox = OutlineComboBox<Int>()
|
||||
val dataBitsComboBox = OutlineComboBox<Int>()
|
||||
val parityComboBox = OutlineComboBox<SerialCommParity>()
|
||||
val stopBitsComboBox = OutlineComboBox<String>()
|
||||
val flowControlComboBox = OutlineComboBox<SerialCommFlowControl>()
|
||||
|
||||
|
||||
init {
|
||||
initView()
|
||||
initEvents()
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
||||
serialPortComboBox.isEditable = true
|
||||
|
||||
baudRateComboBox.isEditable = true
|
||||
baudRateComboBox.addItem(9600)
|
||||
baudRateComboBox.addItem(19200)
|
||||
baudRateComboBox.addItem(38400)
|
||||
baudRateComboBox.addItem(57600)
|
||||
baudRateComboBox.addItem(115200)
|
||||
|
||||
dataBitsComboBox.addItem(5)
|
||||
dataBitsComboBox.addItem(6)
|
||||
dataBitsComboBox.addItem(7)
|
||||
dataBitsComboBox.addItem(8)
|
||||
dataBitsComboBox.selectedItem = 8
|
||||
|
||||
parityComboBox.addItem(SerialCommParity.None)
|
||||
parityComboBox.addItem(SerialCommParity.Even)
|
||||
parityComboBox.addItem(SerialCommParity.Odd)
|
||||
parityComboBox.addItem(SerialCommParity.Mark)
|
||||
parityComboBox.addItem(SerialCommParity.Space)
|
||||
|
||||
stopBitsComboBox.addItem("1")
|
||||
stopBitsComboBox.addItem("1.5")
|
||||
stopBitsComboBox.addItem("2")
|
||||
stopBitsComboBox.selectedItem = "1"
|
||||
|
||||
flowControlComboBox.addItem(SerialCommFlowControl.None)
|
||||
flowControlComboBox.addItem(SerialCommFlowControl.RTS_CTS)
|
||||
flowControlComboBox.addItem(SerialCommFlowControl.XON_XOFF)
|
||||
|
||||
flowControlComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
val text = value?.toString() ?: StringUtils.EMPTY
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
text.replace('_', '/'),
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
add(getCenterComponent(), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
addComponentListener(object : ComponentAdapter() {
|
||||
override fun componentShown(e: ComponentEvent) {
|
||||
removeComponentListener(this)
|
||||
@Suppress("OPT_IN_USAGE")
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
for (commPort in SerialPort.getCommPorts()) {
|
||||
withContext(Dispatchers.Swing) {
|
||||
serialPortComboBox.addItem(commPort.systemPortName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getIcon(isSelected: Boolean): Icon {
|
||||
return Icons.plugin
|
||||
}
|
||||
|
||||
override fun getTitle(): String {
|
||||
return I18n.getString("termora.new-host.serial")
|
||||
}
|
||||
|
||||
override fun getJComponent(): JComponent {
|
||||
return this
|
||||
}
|
||||
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $formMargin, default:grow, $formMargin",
|
||||
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
|
||||
)
|
||||
|
||||
var rows = 1
|
||||
val step = 2
|
||||
val panel = FormBuilder.create().layout(layout)
|
||||
.add("${I18n.getString("termora.new-host.serial.port")}:").xy(1, rows)
|
||||
.add(serialPortComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.serial.baud-rate")}:").xy(1, rows)
|
||||
.add(baudRateComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.serial.data-bits")}:").xy(1, rows)
|
||||
.add(dataBitsComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.serial.parity")}:").xy(1, rows)
|
||||
.add(parityComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.serial.stop-bits")}:").xy(1, rows)
|
||||
.add(stopBitsComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.new-host.serial.flow-control")}:").xy(1, rows)
|
||||
.add(flowControlComboBox).xy(3, rows).apply { rows += step }
|
||||
.build()
|
||||
return panel
|
||||
}
|
||||
}
|
||||
|
||||
protected inner class JumpHostsOption : JPanel(BorderLayout()), Option {
|
||||
val jumpHosts = mutableListOf<Host>()
|
||||
|
||||
@@ -69,6 +69,8 @@ class HostTree : JTree(), Disposable {
|
||||
icon = if (expanded) FlatTreeOpenIcon() else FlatTreeClosedIcon()
|
||||
} else if (host.protocol == Protocol.SSH || host.protocol == Protocol.Local) {
|
||||
icon = if (sel && this@HostTree.hasFocus()) Icons.terminal.dark else Icons.terminal
|
||||
} else if (host.protocol == Protocol.Serial) {
|
||||
icon = if (sel && this@HostTree.hasFocus()) Icons.plugin.dark else Icons.plugin
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package app.termora
|
||||
object Icons {
|
||||
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") }
|
||||
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
|
||||
val plugin by lazy { DynamicIcon("icons/plugin.svg", "icons/plugin_dark.svg") }
|
||||
val moveUp by lazy { DynamicIcon("icons/moveUp.svg", "icons/moveUp_dark.svg") }
|
||||
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }
|
||||
val moveDown by lazy { DynamicIcon("icons/moveDown.svg", "icons/moveDown_dark.svg") }
|
||||
|
||||
@@ -41,4 +41,9 @@ private fun setupNativeLibraries() {
|
||||
if (pty4j.exists()) {
|
||||
System.setProperty(PtyUtil.PREFERRED_NATIVE_FOLDER_KEY, pty4j.absolutePath)
|
||||
}
|
||||
|
||||
val jSerialComm = FileUtils.getFile(dylib, "jSerialComm")
|
||||
if (jSerialComm.exists()) {
|
||||
System.setProperty("jSerialComm.library.path", jSerialComm.absolutePath)
|
||||
}
|
||||
}
|
||||
@@ -53,8 +53,12 @@ abstract class PtyHostTerminalTab(
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
delay(250.milliseconds)
|
||||
withContext(Dispatchers.Swing) {
|
||||
ptyConnector.write(host.options.startupCommand)
|
||||
ptyConnector.write(terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER)))
|
||||
val charset = ptyConnector.getCharset()
|
||||
ptyConnector.write(host.options.startupCommand.toByteArray(charset))
|
||||
ptyConnector.write(
|
||||
terminal.getKeyEncoder().encode(TerminalKeyEvent(KeyEvent.VK_ENTER))
|
||||
.toByteArray(charset)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
src/main/kotlin/app/termora/SerialPortPtyConnector.kt
Normal file
61
src/main/kotlin/app/termora/SerialPortPtyConnector.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
import com.fazecast.jSerialComm.SerialPortDataListener
|
||||
import com.fazecast.jSerialComm.SerialPortEvent
|
||||
import java.nio.charset.Charset
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SerialPortPtyConnector(
|
||||
private val serialPort: SerialPort,
|
||||
private val charset: Charset = Charsets.UTF_8
|
||||
) : PtyConnector, SerialPortDataListener {
|
||||
|
||||
private val queue = LinkedBlockingQueue<Char>()
|
||||
|
||||
init {
|
||||
serialPort.addDataListener(this)
|
||||
}
|
||||
|
||||
override fun read(buffer: CharArray): Int {
|
||||
buffer[0] = queue.poll(1, TimeUnit.SECONDS) ?: return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
override fun write(buffer: ByteArray, offset: Int, len: Int) {
|
||||
serialPort.writeBytes(buffer, len, offset)
|
||||
}
|
||||
|
||||
override fun resize(rows: Int, cols: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun waitFor(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
queue.clear()
|
||||
serialPort.closePort()
|
||||
}
|
||||
|
||||
override fun getListeningEvents(): Int {
|
||||
return SerialPort.LISTENING_EVENT_DATA_RECEIVED
|
||||
}
|
||||
|
||||
override fun serialEvent(event: SerialPortEvent) {
|
||||
if (event.eventType == SerialPort.LISTENING_EVENT_DATA_RECEIVED) {
|
||||
val data = event.receivedData
|
||||
if (data.isEmpty()) return
|
||||
for (c in String(data, charset).toCharArray()) {
|
||||
queue.add(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCharset(): Charset {
|
||||
return charset
|
||||
}
|
||||
}
|
||||
20
src/main/kotlin/app/termora/SerialTerminalTab.kt
Normal file
20
src/main/kotlin/app/termora/SerialTerminalTab.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package app.termora
|
||||
|
||||
import app.termora.terminal.PtyConnector
|
||||
import org.apache.commons.io.Charsets
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.swing.Icon
|
||||
|
||||
class SerialTerminalTab(windowScope: WindowScope, host: Host) : PtyHostTerminalTab(windowScope, host) {
|
||||
override suspend fun openPtyConnector(): PtyConnector {
|
||||
val serialPort = Serials.openPort(host)
|
||||
return SerialPortPtyConnector(
|
||||
serialPort,
|
||||
Charsets.toCharset(host.options.encoding, StandardCharsets.UTF_8)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getIcon(): Icon {
|
||||
return Icons.plugin
|
||||
}
|
||||
}
|
||||
38
src/main/kotlin/app/termora/Serials.kt
Normal file
38
src/main/kotlin/app/termora/Serials.kt
Normal file
@@ -0,0 +1,38 @@
|
||||
package app.termora
|
||||
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
|
||||
object Serials {
|
||||
fun openPort(host: Host): SerialPort {
|
||||
val serialComm = host.options.serialComm
|
||||
val serialPort = SerialPort.getCommPort(serialComm.port)
|
||||
serialPort.setBaudRate(serialComm.baudRate)
|
||||
serialPort.setNumDataBits(serialComm.dataBits)
|
||||
|
||||
when (serialComm.parity) {
|
||||
SerialCommParity.None -> serialPort.setParity(SerialPort.NO_PARITY)
|
||||
SerialCommParity.Mark -> serialPort.setParity(SerialPort.MARK_PARITY)
|
||||
SerialCommParity.Even -> serialPort.setParity(SerialPort.EVEN_PARITY)
|
||||
SerialCommParity.Odd -> serialPort.setParity(SerialPort.ODD_PARITY)
|
||||
SerialCommParity.Space -> serialPort.setParity(SerialPort.SPACE_PARITY)
|
||||
}
|
||||
|
||||
when (serialComm.stopBits) {
|
||||
"1" -> serialPort.setNumStopBits(SerialPort.ONE_STOP_BIT)
|
||||
"1.5" -> serialPort.setNumStopBits(SerialPort.ONE_POINT_FIVE_STOP_BITS)
|
||||
"2" -> serialPort.setNumStopBits(SerialPort.TWO_STOP_BITS)
|
||||
}
|
||||
|
||||
when (serialComm.flowControl) {
|
||||
SerialCommFlowControl.None -> serialPort.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED)
|
||||
SerialCommFlowControl.RTS_CTS -> serialPort.setFlowControl(SerialPort.FLOW_CONTROL_RTS_ENABLED or SerialPort.FLOW_CONTROL_CTS_ENABLED)
|
||||
SerialCommFlowControl.XON_XOFF -> serialPort.setFlowControl(SerialPort.FLOW_CONTROL_XONXOFF_IN_ENABLED or SerialPort.FLOW_CONTROL_XONXOFF_OUT_ENABLED)
|
||||
}
|
||||
|
||||
if (!serialPort.openPort()) {
|
||||
throw IllegalStateException("Open serial port [${serialComm.port}] failed")
|
||||
}
|
||||
|
||||
return serialPort
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
package app.termora.actions
|
||||
|
||||
import app.termora.LocalTerminalTab
|
||||
import app.termora.OpenHostActionEvent
|
||||
import app.termora.Protocol
|
||||
import app.termora.SSHTerminalTab
|
||||
import app.termora.*
|
||||
|
||||
class OpenHostAction : AnAction() {
|
||||
companion object {
|
||||
@@ -18,9 +15,11 @@ class OpenHostAction : AnAction() {
|
||||
val terminalTabbedManager = evt.getData(DataProviders.TerminalTabbedManager) ?: return
|
||||
val windowScope = evt.getData(DataProviders.WindowScope) ?: return
|
||||
|
||||
val tab = if (evt.host.protocol == Protocol.SSH)
|
||||
SSHTerminalTab(windowScope, evt.host)
|
||||
else LocalTerminalTab(windowScope, evt.host)
|
||||
val tab = when (evt.host.protocol) {
|
||||
Protocol.SSH -> SSHTerminalTab(windowScope, evt.host)
|
||||
Protocol.Serial -> SerialTerminalTab(windowScope, evt.host)
|
||||
else -> LocalTerminalTab(windowScope, evt.host)
|
||||
}
|
||||
|
||||
terminalTabbedManager.addTerminalTab(tab)
|
||||
tab.start()
|
||||
|
||||
@@ -485,9 +485,11 @@ class ControlSequenceIntroducerProcessor(terminal: Terminal, reader: TerminalRea
|
||||
val m = args.first()
|
||||
if (m == '6') {
|
||||
val position = terminal.getCursorModel().getPosition()
|
||||
ptyConnector.write("${ControlCharacters.ESC}[${position.y};${position.x}R")
|
||||
val bytes = "${ControlCharacters.ESC}[${position.y};${position.x}R".toByteArray(ptyConnector.getCharset())
|
||||
ptyConnector.write(bytes)
|
||||
} else if (m == '5') {
|
||||
ptyConnector.write("${ControlCharacters.ESC}[0n")
|
||||
val bytes = "${ControlCharacters.ESC}[0n".toByteArray(ptyConnector.getCharset())
|
||||
ptyConnector.write(bytes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.termora.terminal
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.Charset
|
||||
|
||||
|
||||
interface PtyConnector {
|
||||
@@ -15,15 +16,18 @@ interface PtyConnector {
|
||||
*/
|
||||
fun write(buffer: ByteArray, offset: Int, len: Int)
|
||||
|
||||
/**
|
||||
* 写入数组。
|
||||
*
|
||||
* 如果要写入 String 字符串,请通过 [getCharset] 编码。
|
||||
*/
|
||||
fun write(buffer: ByteArray) {
|
||||
write(buffer, 0, buffer.size)
|
||||
}
|
||||
|
||||
fun write(buffer: String) {
|
||||
if (buffer.isEmpty()) return
|
||||
write(buffer.toByteArray())
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入单个 Int
|
||||
*/
|
||||
fun write(buffer: Int) {
|
||||
write(ByteBuffer.allocate(Integer.BYTES).putInt(buffer).flip().array())
|
||||
}
|
||||
@@ -43,4 +47,8 @@ interface PtyConnector {
|
||||
*/
|
||||
fun close()
|
||||
|
||||
/**
|
||||
* 编码
|
||||
*/
|
||||
fun getCharset(): Charset = Charsets.UTF_8
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package app.termora.terminal
|
||||
|
||||
import java.nio.charset.Charset
|
||||
|
||||
open class PtyConnectorDelegate(
|
||||
@Volatile var ptyConnector: PtyConnector? = null
|
||||
) : PtyConnector {
|
||||
@@ -26,5 +28,7 @@ open class PtyConnectorDelegate(
|
||||
ptyConnector = null
|
||||
}
|
||||
|
||||
|
||||
override fun getCharset(): Charset {
|
||||
return ptyConnector?.getCharset() ?: super.getCharset()
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,6 @@ class PtyProcessConnector(private val process: PtyProcess, private val charset:
|
||||
output.flush()
|
||||
}
|
||||
|
||||
override fun write(buffer: String) {
|
||||
write(buffer.toByteArray(charset))
|
||||
}
|
||||
|
||||
override fun resize(rows: Int, cols: Int) {
|
||||
process.winSize = WinSize(cols, rows)
|
||||
@@ -38,5 +35,7 @@ class PtyProcessConnector(private val process: PtyProcess, private val charset:
|
||||
process.destroyForcibly()
|
||||
}
|
||||
|
||||
|
||||
override fun getCharset(): Charset {
|
||||
return charset
|
||||
}
|
||||
}
|
||||
@@ -298,7 +298,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
||||
|
||||
// 输入法提交
|
||||
if (committedCharacterCount > 0) {
|
||||
ptyConnector.write(sb.toString())
|
||||
ptyConnector.write(sb.toString().toByteArray(ptyConnector.getCharset()))
|
||||
} else {
|
||||
val breakIterator = BreakIterator.getCharacterInstance()
|
||||
val chars = mutableListOf<Char>()
|
||||
@@ -404,9 +404,15 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
|
||||
}
|
||||
|
||||
if (terminal.getTerminalModel().getData(DataKey.BracketedPasteMode, false)) {
|
||||
ptyConnector.write("${ControlCharacters.ESC}[200~${content}${ControlCharacters.ESC}[201~")
|
||||
val bytes = ptyConnector.getCharset()
|
||||
.encode("${ControlCharacters.ESC}[200~${content}${ControlCharacters.ESC}[201~")
|
||||
.array()
|
||||
ptyConnector.write(bytes)
|
||||
} else {
|
||||
ptyConnector.write(content)
|
||||
val bytes = ptyConnector.getCharset()
|
||||
.encode(content)
|
||||
.array()
|
||||
ptyConnector.write(bytes)
|
||||
}
|
||||
|
||||
terminal.getScrollingModel().scrollToRow(
|
||||
|
||||
@@ -24,7 +24,7 @@ class TerminalPanelKeyAdapter(
|
||||
}
|
||||
|
||||
terminal.getSelectionModel().clearSelection()
|
||||
ptyConnector.write("${e.keyChar}")
|
||||
ptyConnector.write("${e.keyChar}".toByteArray(ptyConnector.getCharset()))
|
||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class TerminalPanelKeyAdapter(
|
||||
|
||||
val encode = terminal.getKeyEncoder().encode(AWTTerminalKeyEvent(e))
|
||||
if (encode.isNotEmpty()) {
|
||||
ptyConnector.write(encode)
|
||||
ptyConnector.write(encode.toByteArray(ptyConnector.getCharset()))
|
||||
}
|
||||
|
||||
// https://github.com/TermoraDev/termora/issues/52
|
||||
@@ -64,7 +64,7 @@ class TerminalPanelKeyAdapter(
|
||||
terminal.getSelectionModel().clearSelection()
|
||||
// 如果不为空表示已经发送过了,所以这里为空的时候再发送
|
||||
if (encode.isEmpty()) {
|
||||
ptyConnector.write("${e.keyChar}")
|
||||
ptyConnector.write("${e.keyChar}".toByteArray(ptyConnector.getCharset()))
|
||||
}
|
||||
terminal.getScrollingModel().scrollTo(Int.MAX_VALUE)
|
||||
}
|
||||
|
||||
@@ -70,9 +70,9 @@ class TerminalPanelMouseTrackingAdapter(
|
||||
val encode = terminal.getKeyEncoder()
|
||||
.encode(TerminalKeyEvent(if (e.wheelRotation < 0) KeyEvent.VK_UP else KeyEvent.VK_DOWN))
|
||||
if (encode.isBlank()) return
|
||||
|
||||
val bytes = encode.toByteArray(ptyConnector.getCharset())
|
||||
for (i in 0 until abs(unitsToScroll)) {
|
||||
ptyConnector.write(encode)
|
||||
ptyConnector.write(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,14 @@ termora.new-host.terminal.heartbeat-interval=Heartbeat Interval
|
||||
termora.new-host.terminal.startup-commands=Startup Command
|
||||
termora.new-host.terminal.env=Environment
|
||||
|
||||
termora.new-host.serial=Serial
|
||||
termora.new-host.serial.port=Port
|
||||
termora.new-host.serial.baud-rate=Baud rate
|
||||
termora.new-host.serial.data-bits=Data bits
|
||||
termora.new-host.serial.parity=Parity
|
||||
termora.new-host.serial.stop-bits=Stop bits
|
||||
termora.new-host.serial.flow-control=Flow control
|
||||
|
||||
termora.new-host.tunneling=Tunneling
|
||||
termora.new-host.tunneling.table.name=Name
|
||||
termora.new-host.tunneling.table.type=Type
|
||||
|
||||
@@ -132,6 +132,14 @@ termora.new-host.terminal.startup-commands=启动命令
|
||||
termora.new-host.terminal.env=环境
|
||||
|
||||
|
||||
termora.new-host.serial=串口
|
||||
termora.new-host.serial.port=端口
|
||||
termora.new-host.serial.baud-rate=波特率
|
||||
termora.new-host.serial.data-bits=数据位
|
||||
termora.new-host.serial.parity=校验位
|
||||
termora.new-host.serial.stop-bits=停止位
|
||||
termora.new-host.serial.flow-control=流控
|
||||
|
||||
|
||||
termora.new-host.test-connection=测试连接
|
||||
termora.new-host.test-connection-successful=连接成功
|
||||
|
||||
@@ -130,6 +130,14 @@ termora.new-host.terminal.startup-commands=啟動命令
|
||||
termora.new-host.terminal.heartbeat-interval=心跳間隔
|
||||
termora.new-host.terminal.env=環境
|
||||
|
||||
termora.new-host.serial=串口
|
||||
termora.new-host.serial.port=端口
|
||||
termora.new-host.serial.baud-rate=波特率
|
||||
termora.new-host.serial.data-bits=資料位
|
||||
termora.new-host.serial.parity=校驗位
|
||||
termora.new-host.serial.stop-bits=停止位
|
||||
termora.new-host.serial.flow-control=流控
|
||||
|
||||
termora.new-host.test-connection=測試連接
|
||||
termora.new-host.test-connection-successful=連線成功
|
||||
|
||||
|
||||
4
src/main/resources/icons/plugin.svg
Normal file
4
src/main/resources/icons/plugin.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 4H7C5.34315 4 4 5.34315 4 7V9C4 10.6569 5.34315 12 7 12H11V11V10V6V5V4ZM12 5V4C12 3.44772 11.5523 3 11 3H7C5.13616 3 3.57006 4.27477 3.12602 6H1C0.447715 6 0 6.44772 0 7V9C0 9.55228 0.447715 10 1 10H3.12602C3.57006 11.7252 5.13616 13 7 13H11C11.5523 13 12 12.5523 12 12V11H15.5C15.7761 11 16 10.7761 16 10.5C16 10.2239 15.7761 10 15.5 10H12V6H15.5C15.7761 6 16 5.77614 16 5.5C16 5.22386 15.7761 5 15.5 5H12ZM3 7V9H1V7L3 7Z" fill="#6C707E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 724 B |
4
src/main/resources/icons/plugin_dark.svg
Normal file
4
src/main/resources/icons/plugin_dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 4H7C5.34315 4 4 5.34315 4 7V9C4 10.6569 5.34315 12 7 12H11V11V10V6V5V4ZM12 5V4C12 3.44772 11.5523 3 11 3H7C5.13616 3 3.57006 4.27477 3.12602 6H1C0.447715 6 0 6.44772 0 7V9C0 9.55228 0.447715 10 1 10H3.12602C3.57006 11.7252 5.13616 13 7 13H11C11.5523 13 12 12.5523 12 12V11H15.5C15.7761 11 16 10.7761 16 10.5C16 10.2239 15.7761 10 15.5 10H12V6H15.5C15.7761 6 16 5.77614 16 5.5C16 5.22386 15.7761 5 15.5 5H12ZM3 7V9H1V7L3 7Z" fill="#CED0D6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 724 B |
Reference in New Issue
Block a user