12 Commits

Author SHA1 Message Date
jixishi
17950c05dc UP releaser 2024-05-30 16:23:53 +08:00
jixishi
9b374fc42d Merge branch 'dev' 2024-05-30 16:04:55 +08:00
jixishi
82ec65958e 时间戳 文件传输 支持 2024-05-30 15:56:52 +08:00
jixishi
68f285b5ed up 2024-04-26 22:33:08 +08:00
jixishi
d2f8c8a268 增加Ctrl组合键发送指令.ctrl 如.ctrl c 2024-04-26 22:29:16 +08:00
jixishi
8d4273df77 增加交互式配置支持 2024-04-26 22:29:00 +08:00
jixishi
92c92e67e1 增加交互式配置支持 2024-04-26 22:23:32 +08:00
jixishi
604e5bb4ad 修复帧指定的错误参数指向 2024-04-26 12:49:32 +08:00
jixishi
e3415ae05a 去除TCP服务端连接 2024-04-26 11:55:26 +08:00
jixishi
d19c09e4cd TCP|UDP 客户端数据转发支持更新 帧长设置支持 2024-04-26 11:03:44 +08:00
jixishi
5bf90d1b63 TCP|UDP 客户端数据转发支持更新 2024-04-19 22:42:28 +08:00
jixishi
dffb269247 RT 2024-04-19 17:03:28 +08:00
11 changed files with 429 additions and 42 deletions

View File

@@ -23,6 +23,15 @@ builds:
- linux
- windows
- darwin
ldflags:
- -s -w
upx:
- enabled: true
goos:
- windows
goarch:
- amd64
archives:
- format: tar.gz

View File

@@ -1,10 +1,21 @@
# SerialTerminalForWindowsTerminal
编写这个项目之前在windows Terminal对串口设备的支持不是很棒,
开始这个项目之前我发现Windows Terminal对串口设备的支持并不理想。
用过一段时间的
[SerialPortForWindowsTerminal](https://github.com/Zhou-zhi-peng/SerialPortForWindowsTerminal/)
项目。
此项目没有编码转换能力在使用过程中有乱码的问题,并且作者截至目前并没有进行后续支持,于是我便编写了此项目
我试用了一段时间[Zhou-zhi-peng的SerialPortForWindowsTerminal](https://github.com/Zhou-zhi-peng/SerialPortForWindowsTerminal/)项目。
然而,这个项目存在着编码转换的问题,导致数据显示乱码,并且作者目前并没有进行后续支持。因此,我决定创建了这个项目。
## 功能进展
* [x] Hex接收发送(大写hex与原文同显)
* [x] 双向编码转换
* [x] 活动端口探测
* [x] 数据日志保存
* [x] Hex断帧设置
* [x] UDP数据转发
* [x] TCP数据转发
* [x] 参数交互配置
* [x] Ctrl组合键
* [x] 文件接收发送(trzsz lrzsz都支持)
## 运行示例
@@ -25,3 +36,11 @@
5. Hex发送 `./COM -p COM8 -b 115200`
![img5.png](image/img5.png)
6. 交互配置 `./COM`
![img6.png](image/img6.png)
7. Ctrl组合键发送指令.ctrl `.ctrl c`
![img7.png](image/img7.png)
8. 文件上传演示 `index.html`
![img8.png](image/img8.png)

View File

@@ -18,21 +18,30 @@ var commands []Command
func cmdhelp() {
var page = 0
fmt.Printf(">-------Help(%v)-------<\n", page)
strout(out, config.outputCode, fmt.Sprintf(">-------Help(%v)-------<\n", page))
for i := 0; i < len(commands); i++ {
strout(config.outputCode, fmt.Sprintf(" %-10v --%v\n", commands[i].name, commands[i].description))
strout(out, config.outputCode, fmt.Sprintf(" %-10v --%v\n", commands[i].name, commands[i].description))
}
}
func cmdexit() {
os.Exit(0)
}
func cmdargs() {
fmt.Printf(">-------Args(%v)-------<\n", len(args)-1)
fmt.Printf("%q\n", args[1:])
strout(out, config.outputCode, fmt.Sprintf(">-------Args(%v)-------<\n", len(args)-1))
strout(out, config.outputCode, fmt.Sprintf("%q\n", args[1:]))
}
func cmdctrl() {
b := []byte(args[1])
x := []byte{b[0] & 0x1f}
_, err = serialPort.Write(x)
if err != nil {
log.Fatal(err)
}
strout(out, config.outputCode, fmt.Sprintf("Ctrl+%s\n", b))
}
func cmdhex() {
fmt.Printf(">-----Hex Send-----<\n")
fmt.Printf("%q\n", args[1:])
strout(out, config.outputCode, fmt.Sprintf(">-----Hex Send-----<\n"))
strout(out, config.outputCode, fmt.Sprintf("%q\n", args[1:]))
s := strings.Join(args[1:], "")
b, err := hex.DecodeString(s)
if err != nil {
@@ -45,7 +54,7 @@ func cmdhex() {
}
func cmdinit() {
commands = append(commands, Command{name: ".help", description: "帮助信息", function: cmdhelp})
commands = append(commands, Command{name: ".args", description: "参数信息", function: cmdargs})
commands = append(commands, Command{name: ".ctrl", description: "发送Ctrl组合键", function: cmdctrl})
commands = append(commands, Command{name: ".hex", description: "发送Hex", function: cmdhex})
commands = append(commands, Command{name: ".exit", description: "退出终端", function: cmdexit})
}

View File

@@ -1,16 +1,64 @@
package main
import (
"log"
"net"
"os"
)
type Config struct {
portName string
baudRate int
dataBits int
stopBits int
parityBit int
outputCode string
inputCode string
endStr string
enableLog bool
logFilePath string
parityBit int
forWard int
frameSize int
timesTamp bool
timesFmt string
address string
}
type FoeWardMode int
const (
NOT FoeWardMode = iota
TCPC
UDPC
)
var config Config
func setForWardClient() (conn net.Conn) {
switch FoeWardMode(config.forWard) {
case NOT:
case TCPC:
conn, err = net.Dial("tcp", config.address)
if err != nil {
log.Fatal(err)
}
case UDPC:
conn, err = net.Dial("udp", config.address)
if err != nil {
log.Fatal(err)
}
default:
panic("未知模式设置")
}
return conn
}
func checkLogOpen() {
if config.enableLog {
f, err := os.OpenFile(config.logFilePath, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
log.Fatal(err)
}
outs = append(outs, f)
}
}

193
flag.go
View File

@@ -3,6 +3,18 @@ package main
import (
"flag"
"fmt"
"github.com/charmbracelet/bubbles/key"
inf "github.com/fzdwx/infinite"
"github.com/fzdwx/infinite/color"
"github.com/fzdwx/infinite/components"
"github.com/fzdwx/infinite/components/input/text"
"github.com/fzdwx/infinite/components/selection/confirm"
"github.com/fzdwx/infinite/components/selection/singleselect"
"github.com/fzdwx/infinite/style"
"github.com/fzdwx/infinite/theme"
"go.bug.st/serial"
"log"
"strconv"
"strings"
)
@@ -38,8 +50,23 @@ var (
endStr = Flag{ptrVal{string: &config.endStr}, "e", "end", Val{string: "\n"}, "终端换行符"}
enableLog = Flag{ptrVal{bool: &config.enableLog}, "l", "log", Val{bool: false}, "是否启用日志保存"}
logFilePath = Flag{ptrVal{string: &config.logFilePath}, "P", "Path", Val{string: "./Log.txt"}, "日志保存路径"}
forWard = Flag{ptrVal{int: &config.forWard}, "f", "forward", Val{int: 0}, "转发模式(0: 无 1:TCP-C 2:UDP-C)"}
address = Flag{ptrVal{string: &config.address}, "a", "address", Val{string: "127.0.0.1:12345"}, "转发服务地址"}
frameSize = Flag{ptrVal{int: &config.frameSize}, "F", "Frame", Val{int: 16}, "帧大小"}
timesTamp = Flag{ptrVal{bool: &config.timesTamp}, "t", "tamp", Val{bool: false}, "是否启用接收时间戳"}
timesFmt = Flag{ptrVal{string: &config.timesFmt}, "T", "TimeFmt", Val{string: "[06-01-02 15:04:05]"}, "时间戳格式化字段"}
parityBit = Flag{ptrVal{int: &config.parityBit}, "v", "verify", Val{int: 0}, "奇偶校验(0:无校验、1:奇校验、2:偶校验、3:1校验、4:0校验)"}
flags = []Flag{portName, baudRate, dataBits, stopBits, outputCode, inputCode, endStr, enableLog, logFilePath, parityBit}
flags = []Flag{portName, baudRate, dataBits, stopBits, outputCode, inputCode, endStr, enableLog, logFilePath, forWard, address, frameSize, timesTamp, timesFmt, parityBit}
)
var (
bauds = []string{"自定义", "300", "600", "1200", "2400", "4800", "9600",
"14400", "19200", "38400", "56000", "57600", "115200", "128000",
"256000", "460800", "512000", "750000", "921600", "1500000"}
datas = []string{"5", "6", "7", "8"}
stops = []string{"1", "1.5", "2"}
paritys = []string{"无校验", "奇校验", "偶校验", "1校验", "0校验"}
forwards = []string{"No", "TCP-C", "UDP-C"}
)
type ValType int
@@ -96,3 +123,167 @@ func flagInit(f *Flag) {
flag.IntVar(f.v.int, f.lStr, f.dv.int, f.help)
}
}
func getCliFlag() {
ports, err := serial.GetPortsList()
if err != nil {
log.Fatal(err)
}
inputs := components.NewInput()
inputs.Prompt = "Filtering: "
inputs.PromptStyle = style.New().Bold().Italic().Fg(color.LightBlue)
selectKeymap := singleselect.DefaultSingleKeyMap()
selectKeymap.Confirm = key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "finish select"),
)
selectKeymap.Choice = key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "finish select"),
)
selectKeymap.NextPage = key.NewBinding(
key.WithKeys("right"),
key.WithHelp("->", "next page"),
)
selectKeymap.PrevPage = key.NewBinding(
key.WithKeys("left"),
key.WithHelp("<-", "prev page"),
)
s, _ := inf.NewSingleSelect(
ports,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(4),
singleselect.WithFilterInput(inputs),
).Display("选择串口")
config.portName = ports[s]
s, _ = inf.NewSingleSelect(
bauds,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(4),
).Display("选择波特率")
if s != 0 {
config.baudRate, _ = strconv.Atoi(bauds[s])
} else {
b, _ := inf.NewText(
text.WithPrompt("BaudRate:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue("115200"),
).Display()
config.baudRate, _ = strconv.Atoi(b)
}
v, _ := inf.NewConfirmWithSelection(
confirm.WithPrompt("启用Hex"),
).Display()
if v {
config.inputCode = "hex"
b, _ := inf.NewText(
text.WithPrompt("Frames:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue("16"),
).Display()
config.frameSize, _ = strconv.Atoi(b)
}
v, _ = inf.NewConfirmWithSelection(
confirm.WithPrompt("启用时间戳"),
).Display()
if v {
config.inputCode = "hex"
b, _ := inf.NewText(
text.WithPrompt("Frames:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue("16"),
).Display()
config.frameSize, _ = strconv.Atoi(b)
}
v, _ = inf.NewConfirmWithSelection(
confirm.WithPrompt("启用高级配置"),
).Display()
if v {
s, _ = inf.NewSingleSelect(
datas,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(4),
singleselect.WithFilterInput(inputs),
).Display("选择数据位")
config.dataBits, _ = strconv.Atoi(datas[s])
s, _ = inf.NewSingleSelect(
stops,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(4),
singleselect.WithFilterInput(inputs),
).Display("选择停止位")
config.stopBits = s
s, _ = inf.NewSingleSelect(
paritys,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(4),
singleselect.WithFilterInput(inputs),
).Display("选择校验位")
config.parityBit = s
t, _ := inf.NewText(
text.WithPrompt("换行符:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue(endStr.dv.string),
).Display()
config.endStr = t
v, _ = inf.NewConfirmWithSelection(
confirm.WithDefaultYes(),
confirm.WithPrompt("启用编码转换"),
).Display()
if v {
t, _ = inf.NewText(
text.WithPrompt("输入编码:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue(inputCode.dv.string),
).Display()
config.inputCode = t
t, _ = inf.NewText(
text.WithPrompt("输出编码:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue(outputCode.dv.string),
).Display()
config.outputCode = t
}
s, _ = inf.NewSingleSelect(
forwards,
singleselect.WithKeyBinding(selectKeymap),
singleselect.WithPageSize(3),
singleselect.WithFilterInput(inputs),
).Display("选择转发模式")
if s != 0 {
config.forWard = s
t, _ = inf.NewText(
text.WithPrompt("地址:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue(address.dv.string),
).Display()
config.address = t
}
e, _ := inf.NewConfirmWithSelection(
confirm.WithDefaultYes(),
confirm.WithPrompt("启用日志"),
).Display()
config.enableLog = e
if e {
t, _ = inf.NewText(
text.WithPrompt("Path:"),
text.WithPromptStyle(theme.DefaultTheme.PromptStyle),
text.WithDefaultValue(logFilePath.dv.string),
).Display()
config.logFilePath = t
}
}
}

43
go.mod
View File

@@ -3,12 +3,51 @@ module COM
go 1.22
require (
github.com/charmbracelet/bubbles v0.18.0
github.com/charmbracelet/bubbletea v0.25.0
github.com/fzdwx/infinite v0.12.1
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/trzsz/trzsz-go v1.1.7
github.com/zimolab/charsetconv v0.1.2
go.bug.st/serial v1.6.2
)
require (
github.com/UserExistsError/conpty v0.1.2 // indirect
github.com/akavel/rsrc v0.10.2 // indirect
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v0.9.1 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/creack/goselect v0.1.2 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.6.0 // indirect
github.com/creack/pty v1.1.21 // indirect
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect
github.com/duke-git/lancet/v2 v2.2.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fzdwx/iter v0.0.0-20230511075109-0afee9319312 // indirect
github.com/josephspurrier/goversioninfo v1.4.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/ncruces/zenity v0.10.10 // indirect
github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
github.com/rotisserie/eris v0.5.4 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
github.com/trzsz/go-arg v1.5.3 // indirect
github.com/trzsz/promptui v0.10.5 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/image v0.14.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 41 KiB

BIN
image/img6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

BIN
image/img7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
image/img8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

114
main.go
View File

@@ -4,12 +4,18 @@ import (
"bufio"
"flag"
"fmt"
"github.com/trzsz/trzsz-go/trzsz"
"github.com/zimolab/charsetconv"
"go.bug.st/serial"
"golang.org/x/term"
"io"
"log"
"net"
"os"
"os/signal"
"runtime"
"strings"
"time"
)
var (
@@ -18,6 +24,18 @@ var (
args []string
)
var (
in io.Reader = os.Stdin
out io.Writer = os.Stdout
ins = []io.Reader{os.Stdin}
outs = []io.Writer{os.Stdout}
trzszFilter *trzsz.TrzszFilter
clientIn *io.PipeReader
stdoutPipe *io.PipeReader
stdinPipe *io.PipeWriter
clientOut *io.PipeWriter
)
func checkPortAvailability(name string) ([]string, error) {
ports, err := serial.GetPortsList()
if err != nil {
@@ -45,10 +63,11 @@ func init() {
cmdinit()
}
func input() {
input := bufio.NewScanner(os.Stdin)
func input(in io.Reader) {
input := bufio.NewScanner(in)
var ok = false
for input.Scan() {
for {
input.Scan()
ok = false
args = strings.Split(input.Text(), " ")
for _, cmd := range commands {
@@ -58,35 +77,49 @@ func input() {
}
}
if !ok {
_, err := io.WriteString(serialPort, input.Text())
_, err := io.WriteString(stdinPipe, input.Text())
if err != nil {
log.Fatal(err)
}
_, err = io.WriteString(serialPort, config.endStr)
_, err = io.WriteString(stdinPipe, config.endStr)
if err != nil {
log.Fatal(err)
}
}
err := serialPort.Drain()
err = serialPort.Drain()
if err != nil {
log.Fatal(err)
}
}
}
func strout(cs, str string) {
err = charsetconv.EncodeWith(strings.NewReader(str), os.Stdout, charsetconv.Charset(cs), false)
func strout(out io.Writer, cs, str string) {
err = charsetconv.EncodeWith(strings.NewReader(str), out, charsetconv.Charset(cs), false)
if err != nil {
log.Fatal(err)
}
}
func output(out io.Writer) {
func output() {
if strings.Compare(config.inputCode, "hex") == 0 {
b, _ := bufio.NewReader(io.LimitReader(serialPort, 16)).Peek(16)
_, err = fmt.Fprintf(out, "% X %q \n", b, b)
b := make([]byte, config.frameSize)
r, _ := io.LimitReader(stdoutPipe, int64(config.frameSize)).Read(b)
if r != 0 {
if config.timesTamp {
strout(out, config.outputCode, fmt.Sprintf("%v % X %q \n", time.Now().Format(config.timesFmt), b, b))
} else {
err = charsetconv.ConvertWith(io.LimitReader(serialPort, 1024), charsetconv.Charset(config.inputCode), out, charsetconv.Charset(config.outputCode), false)
strout(out, config.outputCode, fmt.Sprintf("% X %q \n", b, b))
}
}
} else {
if config.timesTamp {
line, _, _ := bufio.NewReader(stdoutPipe).ReadLine()
if line != nil {
strout(out, config.outputCode, fmt.Sprintf("%v %s\n", time.Now().Format(config.timesFmt), line))
}
} else {
err = charsetconv.ConvertWith(stdoutPipe, charsetconv.Charset(config.inputCode), out, charsetconv.Charset(config.outputCode), false)
}
}
if err != nil {
log.Fatal(err)
@@ -94,6 +127,9 @@ func output(out io.Writer) {
}
func main() {
flag.Parse()
if config.portName == "" {
getCliFlag()
}
ports, err := checkPortAvailability(config.portName)
if err != nil {
fmt.Println(err)
@@ -113,23 +149,59 @@ func main() {
defer func(port serial.Port) {
err := port.Close()
if err != nil {
log.Fatal(err)
}
}(serialPort)
fd := int(os.Stdin.Fd())
width, _, err := term.GetSize(fd)
if err != nil {
if runtime.GOOS != "windows" {
fmt.Printf("term get size failed: %s\n", err)
return
}
width = 80
}
go input()
clientIn, stdinPipe = io.Pipe()
stdoutPipe, clientOut = io.Pipe()
trzszFilter = trzsz.NewTrzszFilter(clientIn, clientOut, serialPort, serialPort,
trzsz.TrzszOptions{TerminalColumns: int32(width), EnableZmodem: true})
trzsz.SetAffectedByWindows(false)
ch := make(chan os.Signal, 1)
go func() {
for range ch {
width, _, err := term.GetSize(fd)
if err != nil {
fmt.Printf("term get size failed: %s\n", err)
continue
}
trzszFilter.SetTerminalColumns(int32(width))
}
}()
defer func() { signal.Stop(ch); close(ch) }()
if config.enableLog {
f, err := os.OpenFile(config.logFilePath, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
if FoeWardMode(config.forWard) != NOT {
conn := setForWardClient()
ins = append(ins, conn)
outs = append(outs, conn)
defer func(conn net.Conn) {
err := conn.Close()
if err != nil {
log.Fatal(err)
}
out := io.MultiWriter(os.Stdout, f)
for {
output(out)
}(conn)
}
} else {
for {
output(os.Stdout)
checkLogOpen()
if len(ins) != 0 {
for _, reader := range ins {
go input(reader)
}
}
if len(outs) != 1 {
out = io.MultiWriter(outs...)
}
for {
output()
}
}