diff --git a/.gitignore b/.gitignore index 51da262..de1d6ee 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .idea dist/ /go.sum +/view/* diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 35410ca..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/README.md b/README.md index 4e2c3c1..a00bc55 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ * [x] 活动端口探测 * [x] 数据日志保存 * [x] Hex断帧设置 -* [x] UDP数据转发 -* [x] TCP数据转发 +* [x] UDP数据转发(支持多服) +* [x] TCP数据转发(支持多服) * [x] 参数交互配置 * [x] Ctrl组合键 * [x] 文件接收发送(trzsz lrzsz都支持) @@ -49,4 +49,6 @@ 9. 时间戳 `./COM -p COM8 -t` ![img9.png](image/img9.png) 10. 格式修改 `./COM -p COM11 -t='<2006-01-02 15:04:05>'` - ![img10.png](image/img10.png) \ No newline at end of file + ![img10.png](image/img10.png) +11. 多服同步转发 `./COM -p COM11 -f 1 -a 127.0.0.1:23456 -f 1 -a 127.0.0.1:23457` + ![img12.png](image/img12.png) \ No newline at end of file diff --git a/command.go b/command.go index 149bbf1..dfe9c35 100644 --- a/command.go +++ b/command.go @@ -14,7 +14,10 @@ type Command struct { function func() } -var commands []Command +var ( + commands []Command + args []string +) func cmdhelp() { var page = 0 @@ -24,6 +27,8 @@ func cmdhelp() { } } func cmdexit() { + CloseTrzsz() + CloseSerial() os.Exit(0) } func cmdargs() { @@ -31,12 +36,11 @@ func cmdargs() { strout(out, config.outputCode, fmt.Sprintf("%q\n", args[1:])) } func cmdctrl() { + var err error b := []byte(args[1]) x := []byte{b[0] & 0x1f} _, err = serialPort.Write(x) - if err != nil { - log.Fatal(err) - } + ErrorF(err) strout(out, config.outputCode, fmt.Sprintf("Ctrl+%s\n", b)) } func cmdhex() { diff --git a/config.go b/config.go index ddd07b7..3cb43b8 100644 --- a/config.go +++ b/config.go @@ -19,11 +19,11 @@ type Config struct { endStr string enableLog bool logFilePath string - forWard int + forWard []int frameSize int timesTamp bool timesFmt string - address string + address []string } type FoeWardMode int @@ -35,17 +35,18 @@ const ( var config Config -func setForWardClient() (conn net.Conn) { - switch FoeWardMode(config.forWard) { +func setForWardClient(mode FoeWardMode, add string) (conn net.Conn) { + var err error + switch mode { case NOT: case TCPC: - conn, err = net.Dial("tcp", config.address) + conn, err = net.Dial("tcp", add) if err != nil { log.Fatal(err) } case UDPC: - conn, err = net.Dial("udp", config.address) + conn, err = net.Dial("udp", add) if err != nil { log.Fatal(err) } diff --git a/flag.go b/flag.go index 19d2f40..1c05a78 100644 --- a/flag.go +++ b/flag.go @@ -20,7 +20,9 @@ import ( type ptrVal struct { *string + sl *[]string *int + il *[]int *bool *float64 *float32 @@ -52,8 +54,8 @@ var ( endStr = Flag{ptrVal{string: &config.endStr}, "e", "end", Val{string: "\n"}, "终端换行符"} logExt = Flag{v: ptrVal{ext: &config.logFilePath}, sStr: "l", lStr: "log", dv: Val{extdef: "./%s-$s.txt", string: ""}, help: "日志保存路径"} timeExt = Flag{v: ptrVal{ext: &config.timesFmt}, sStr: "t", lStr: "time", dv: Val{extdef: "[06-01-02 15:04:05.000]", string: ""}, help: "时间戳格式化字段"} - 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"}, "转发服务地址"} + forWard = Flag{ptrVal{il: &config.forWard}, "f", "forward", Val{int: 0}, "转发模式(0: 无 1:TCP-C 2:UDP-C 支持多次传入)"} + address = Flag{ptrVal{sl: &config.address}, "a", "address", Val{string: "127.0.0.1:12345"}, "转发服务地址(支持多次传入)"} frameSize = Flag{ptrVal{int: &config.frameSize}, "F", "Frame", Val{int: 16}, "帧大小"} 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, forWard, address, frameSize, parityBit, logExt, timeExt} @@ -129,6 +131,12 @@ func flagInit(f *Flag) { pflag.StringVarP(f.v.ext, f.lStr, f.sStr, f.dv.string, f.help) pflag.Lookup(f.lStr).NoOptDefVal = f.dv.extdef } + if f.v.sl != nil { + pflag.StringArrayVarP(f.v.sl, f.lStr, f.sStr, []string{f.dv.string}, f.help) + } + if f.v.il != nil { + pflag.IntSliceVarP(f.v.il, f.lStr, f.sStr, []int{f.dv.int}, f.help) + } } func flagExt() { if config.logFilePath != "" { @@ -209,7 +217,7 @@ func getCliFlag() { b, _ := inf.NewText( text.WithPrompt("格式化字段:"), text.WithPromptStyle(theme.DefaultTheme.PromptStyle), - text.WithDefaultValue(logExt.dv.extdef), + text.WithDefaultValue(timeExt.dv.extdef), ).Display() config.timesFmt = b } @@ -268,7 +276,7 @@ func getCliFlag() { ).Display() config.outputCode = t } - + G_F_mode: s, _ = inf.NewSingleSelect( forwards, singleselect.WithKeyBinding(selectKeymap), @@ -276,13 +284,14 @@ func getCliFlag() { singleselect.WithFilterInput(inputs), ).Display("选择转发模式") if s != 0 { - config.forWard = s + config.forWard = append(config.forWard, s) t, _ = inf.NewText( text.WithPrompt("地址:"), text.WithPromptStyle(theme.DefaultTheme.PromptStyle), text.WithDefaultValue(address.dv.string), ).Display() - config.address = t + config.address = append(config.address, t) + goto G_F_mode } e, _ := inf.NewConfirmWithSelection( diff --git a/go.mod b/go.mod index 4f82d1a..88582a1 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22 require ( github.com/charmbracelet/bubbles v0.18.0 github.com/fzdwx/infinite v0.12.1 + github.com/gobwas/ws v1.4.0 github.com/spf13/pflag v1.0.5 github.com/trzsz/trzsz-go v1.1.7 github.com/zimolab/charsetconv v0.1.2 @@ -27,6 +28,8 @@ require ( github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect github.com/duke-git/lancet/v2 v2.2.1 // indirect github.com/fzdwx/iter v0.0.0-20230511075109-0afee9319312 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // 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 diff --git a/image/12.png b/image/12.png new file mode 100644 index 0000000..b8f9615 Binary files /dev/null and b/image/12.png differ diff --git a/main.go b/main.go index b1d4c0b..d2468c1 100644 --- a/main.go +++ b/main.go @@ -1,60 +1,13 @@ package main import ( - "bufio" "fmt" "github.com/spf13/pflag" - "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 ( - serialPort serial.Port - err error - 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 { - log.Fatal(err) - } - if len(ports) == 0 { - return nil, fmt.Errorf("无串口") - } - if name == "" { - return ports, fmt.Errorf("串口未指定") - } - for _, port := range ports { - if strings.Compare(port, name) == 0 { - return ports, nil - } - } - return ports, fmt.Errorf("串口 " + name + " 未在线") -} - func init() { log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile | log.Lmsgprefix) for _, f := range flags { @@ -63,68 +16,6 @@ func init() { cmdinit() } -func input(in io.Reader) { - input := bufio.NewScanner(in) - var ok = false - for { - input.Scan() - ok = false - args = strings.Split(input.Text(), " ") - for _, cmd := range commands { - if strings.Compare(strings.TrimSpace(args[0]), cmd.name) == 0 { - cmd.function() - ok = true - } - } - if !ok { - _, err := io.WriteString(stdinPipe, input.Text()) - if err != nil { - log.Fatal(err) - } - _, err = io.WriteString(stdinPipe, config.endStr) - if err != nil { - log.Fatal(err) - } - } - err = serialPort.Drain() - if err != nil { - log.Fatal(err) - } - } -} - -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() { - if strings.Compare(config.inputCode, "hex") == 0 { - 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 { - 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) - } -} func main() { pflag.Parse() flagExt() @@ -137,68 +28,25 @@ func main() { printUsage(ports) os.Exit(0) } - mode := &serial.Mode{ - BaudRate: config.baudRate, - StopBits: serial.StopBits(config.stopBits), - DataBits: config.dataBits, - Parity: serial.Parity(config.parityBit), - } - serialPort, err = serial.Open(config.portName, mode) - if err != nil { - log.Fatal(err) - } - 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 - } - 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 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) - } - }(conn) - } + // 日志文件输出检测 checkLogOpen() - if len(ins) != 0 { - for _, reader := range ins { - go input(reader) - } - } + //串口设备开启 + OpenSerial() + + defer CloseSerial() + // 打开文件服务 + OpenTrzsz() + + defer CloseTrzsz() + + //开启转发 + OpenForwarding() + + // 获取终端输入 + go input(in) + if len(outs) != 1 { out = io.MultiWriter(outs...) } diff --git a/mutual.go b/mutual.go new file mode 100644 index 0000000..4255ac3 --- /dev/null +++ b/mutual.go @@ -0,0 +1,85 @@ +package main + +import ( + "bufio" + "fmt" + "github.com/trzsz/trzsz-go/trzsz" + "github.com/zimolab/charsetconv" + "go.bug.st/serial" + "io" + "log" + "os" + "strings" + "time" +) + +var ( + serialPort serial.Port + in io.Reader = os.Stdin + out io.Writer = os.Stdout + outs = []io.Writer{os.Stdout} + trzszFilter *trzsz.TrzszFilter + clientIn *io.PipeReader + stdoutPipe *io.PipeReader + stdinPipe *io.PipeWriter + clientOut *io.PipeWriter +) + +func input(in io.Reader) { + var err error + input := bufio.NewScanner(in) + var ok = false + for { + input.Scan() + ok = false + args = strings.Split(input.Text(), " ") + for _, cmd := range commands { + if strings.Compare(strings.TrimSpace(args[0]), cmd.name) == 0 { + cmd.function() + ok = true + } + } + if !ok { + _, err := io.WriteString(stdinPipe, input.Text()) + if err != nil { + log.Fatal(err) + } + _, err = io.WriteString(stdinPipe, config.endStr) + if err != nil { + log.Fatal(err) + } + } + err = serialPort.Drain() + ErrorF(err) + } +} + +func strout(out io.Writer, cs, str string) { + err := charsetconv.EncodeWith(strings.NewReader(str), out, charsetconv.Charset(cs), false) + ErrorF(err) +} + +func output() { + var err error + if strings.Compare(config.inputCode, "hex") == 0 { + 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 { + 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) + } + } + ErrorP(err) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..c362599 --- /dev/null +++ b/utils.go @@ -0,0 +1,125 @@ +package main + +import ( + "fmt" + "github.com/trzsz/trzsz-go/trzsz" + "go.bug.st/serial" + "golang.org/x/term" + "io" + "log" + "net" + "os" + "os/signal" + "runtime" + "strings" +) + +func checkPortAvailability(name string) ([]string, error) { + ports, err := serial.GetPortsList() + if err != nil { + log.Fatal(err) + } + if len(ports) == 0 { + return nil, fmt.Errorf("无串口") + } + if name == "" { + return ports, fmt.Errorf("串口未指定") + } + for _, port := range ports { + if strings.Compare(port, name) == 0 { + return ports, nil + } + } + return ports, fmt.Errorf("串口 " + name + " 未在线") +} + +func OpenSerial() { + var err error + mode := &serial.Mode{ + BaudRate: config.baudRate, + StopBits: serial.StopBits(config.stopBits), + DataBits: config.dataBits, + Parity: serial.Parity(config.parityBit), + } + serialPort, err = serial.Open(config.portName, mode) + ErrorF(err) + return +} + +func CloseSerial() { + err := serialPort.Close() + ErrorF(err) + return +} + +var termch chan os.Signal + +// OpenTrzsz create a TrzszFilter to support trzsz ( trz / tsz ). +// +// ┌────────┐ stdinPipe ┌────────┐ ClientIn ┌─────────────┐ SerialIn ┌────────┐ +// │ ├─────────────►│ ├─────────────►│ ├─────────────►│ │ +// │ mutual │ │ Client │ │ TrzszFilter │ │ Serial │ +// │ │◄─────────────│ │◄─────────────┤ │◄─────────────┤ │ +// └────────┘ stdoutPipe └────────┘ ClientOut └─────────────┘ SerialOut └────────┘ +func OpenTrzsz() { + 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 + } + + 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) + termch = make(chan os.Signal, 1) + go func() { + for range termch { + width, _, err := term.GetSize(fd) + if err != nil { + fmt.Printf("term get size failed: %s\n", err) + continue + } + trzszFilter.SetTerminalColumns(int32(width)) + } + }() +} + +func CloseTrzsz() { + signal.Stop(termch) + close(termch) +} + +func OpenForwarding() { + for i, mode := range config.forWard { + if FoeWardMode(mode) != NOT { + conn := setForWardClient(FoeWardMode(mode), config.address[i]) + outs = append(outs, conn) + go func() { + defer func(conn net.Conn) { + err := conn.Close() + if err != nil { + log.Fatal(err) + } + }(conn) + input(conn) + }() + } + } +} + +func ErrorP(err error) { + if err != nil { + fmt.Fprint(os.Stderr, err) + } +} +func ErrorF(err error) { + if err != nil { + log.Fatal(err) + } +}