├── .gitignore ├── .goreleaser.yaml ├── README.md ├── command.go ├── config.go ├── flag.go ├── go.mod ├── image ├── img1.png ├── img10.png ├── img11.png ├── img12.png ├── img2.png ├── img3.png ├── img4.png ├── img5.png ├── img6.png ├── img7.png ├── img8.png └── img9.png ├── main.go ├── mutual.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | .idea 3 | dist/ 4 | /go.sum 5 | /view/* 6 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | #file: noinspection YAMLSchemaValidation 2 | # This is an example .goreleaser.yml file with some sensible defaults. 3 | # Make sure to check the documentation at https://goreleaser.com 4 | 5 | # The lines below are called `modelines`. See `:help modeline` 6 | # Feel free to remove those if you don't want/need to use them. 7 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 8 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 9 | 10 | version: 1 11 | 12 | before: 13 | hooks: 14 | # You may remove this if you don't use go modules. 15 | # - go mod tidy 16 | # you may remove this if you don't need go generate 17 | # - go generate ./... 18 | 19 | builds: 20 | - env: 21 | - CGO_ENABLED=0 22 | goos: 23 | - linux 24 | - windows 25 | - darwin 26 | ldflags: 27 | - -s -w 28 | 29 | upx: 30 | - enabled: true 31 | goos: 32 | - windows 33 | goarch: 34 | - amd64 35 | 36 | archives: 37 | - format: tar.gz 38 | # this name template makes the OS and Arch compatible with the results of `uname`. 39 | name_template: >- 40 | {{ .ProjectName }}_ 41 | {{- title .Os }}_ 42 | {{- if eq .Arch "amd64" }}x86_64 43 | {{- else if eq .Arch "386" }}i386 44 | {{- else }}{{ .Arch }}{{ end }} 45 | {{- if .Arm }}v{{ .Arm }}{{ end }} 46 | # use zip for windows archives 47 | format_overrides: 48 | - goos: windows 49 | format: zip 50 | checksum: 51 | name_template: 'checksums.txt' 52 | 53 | snapshot: 54 | name_template: 'v1.0.0-snapshot' 55 | 56 | changelog: 57 | sort: asc 58 | filters: 59 | exclude: 60 | - "^docs:" 61 | - "^test:" 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SerialTerminalForWindowsTerminal 2 | 在开始这个项目之前,我发现Windows Terminal对串口设备的支持并不理想。 3 | 4 | 我试用了一段时间[Zhou-zhi-peng的SerialPortForWindowsTerminal](https://github.com/Zhou-zhi-peng/SerialPortForWindowsTerminal/)项目。 5 | 6 | 然而,这个项目存在着编码转换的问题,导致数据显示乱码,并且作者目前并没有进行后续支持。因此,我决定创建了这个项目。 7 | 8 | ## 功能进展 9 | * [x] Hex接收发送(大写hex与原文同显) 10 | * [x] 双向编码转换 11 | * [x] 活动端口探测 12 | * [x] 数据日志保存 13 | * [x] Hex断帧设置 14 | * [x] UDP数据转发(支持多服) 15 | * [x] TCP数据转发(支持多服) 16 | * [x] 参数交互配置 17 | * [x] Ctrl组合键 18 | * [x] 文件接收发送(trzsz lrzsz都支持) 19 | 20 | ## 运行示例 21 | 22 | 1. 参数帮助 `./COM` 23 | 24 | ![img1.png](image/img1.png) 25 | 26 | 2. 输入设备输出UTF8 终端输出GBK `./COM -p COM8 -b 115200 -o GBK` 27 | 28 | ![img2.png](image/img2.png) 29 | 3. 彩色终端输出 30 | 31 | ![img3.png](image/img3.png) 32 | 33 | 4. Hex接收 `./COM -p COM8 -b 115200 -i hex` 34 | 35 | ![img4.png](image/img4.png) 36 | 5. Hex发送 `./COM -p COM8 -b 115200` 37 | 38 | ![img5.png](image/img5.png) 39 | 6. 交互配置 `./COM` 40 | 41 | ![img6.png](image/img6.png) 42 | 7. Ctrl组合键发送指令.ctrl `.ctrl c` 43 | 44 | ![img7.png](image/img7.png) 45 | 8. 文件上传演示 `index.html` 46 | ![img8.png](image/img8.png) 47 | 内容对比 48 | ![img11.png](image/img11.png) 49 | 9. 时间戳 `./COM -p COM8 -t` 50 | ![img9.png](image/img9.png) 51 | 10. 格式修改 `./COM -p COM11 -t='<2006-01-02 15:04:05>'` 52 | ![img10.png](image/img10.png) 53 | 11. 多服同步转发 `./COM -p COM11 -f 1 -a 127.0.0.1:23456 -f 1 -a 127.0.0.1:23457` 54 | ![img12.png](image/img12.png) -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | type Command struct { 12 | name string 13 | description string 14 | function func() 15 | } 16 | 17 | var ( 18 | commands []Command 19 | args []string 20 | ) 21 | 22 | func cmdhelp() { 23 | var page = 0 24 | strout(out, config.outputCode, fmt.Sprintf(">-------Help(%v)-------<\n", page)) 25 | for i := 0; i < len(commands); i++ { 26 | strout(out, config.outputCode, fmt.Sprintf(" %-10v --%v\n", commands[i].name, commands[i].description)) 27 | } 28 | } 29 | func cmdexit() { 30 | CloseTrzsz() 31 | CloseSerial() 32 | os.Exit(0) 33 | } 34 | func cmdargs() { 35 | strout(out, config.outputCode, fmt.Sprintf(">-------Args(%v)-------<\n", len(args)-1)) 36 | strout(out, config.outputCode, fmt.Sprintf("%q\n", args[1:])) 37 | } 38 | func cmdctrl() { 39 | var err error 40 | b := []byte(args[1]) 41 | x := []byte{b[0] & 0x1f} 42 | _, err = serialPort.Write(x) 43 | ErrorF(err) 44 | strout(out, config.outputCode, fmt.Sprintf("Ctrl+%s\n", b)) 45 | } 46 | func cmdhex() { 47 | strout(out, config.outputCode, fmt.Sprintf(">-----Hex Send-----<\n")) 48 | strout(out, config.outputCode, fmt.Sprintf("%q\n", args[1:])) 49 | s := strings.Join(args[1:], "") 50 | b, err := hex.DecodeString(s) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | _, err = serialPort.Write(b) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | } 59 | func cmdinit() { 60 | commands = append(commands, Command{name: ".help", description: "帮助信息", function: cmdhelp}) 61 | commands = append(commands, Command{name: ".ctrl", description: "发送Ctrl组合键", function: cmdctrl}) 62 | commands = append(commands, Command{name: ".hex", description: "发送Hex", function: cmdhex}) 63 | commands = append(commands, Command{name: ".exit", description: "退出终端", function: cmdexit}) 64 | } 65 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "os" 8 | "time" 9 | ) 10 | 11 | type Config struct { 12 | portName string 13 | baudRate int 14 | dataBits int 15 | stopBits int 16 | parityBit int 17 | outputCode string 18 | inputCode string 19 | endStr string 20 | enableLog bool 21 | logFilePath string 22 | forWard []int 23 | frameSize int 24 | timesTamp bool 25 | timesFmt string 26 | address []string 27 | } 28 | type FoeWardMode int 29 | 30 | const ( 31 | NOT FoeWardMode = iota 32 | TCPC 33 | UDPC 34 | ) 35 | 36 | var config Config 37 | 38 | func setForWardClient(mode FoeWardMode, add string) (conn net.Conn) { 39 | var err error 40 | switch mode { 41 | case NOT: 42 | 43 | case TCPC: 44 | conn, err = net.Dial("tcp", add) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | case UDPC: 49 | conn, err = net.Dial("udp", add) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | default: 54 | panic("未知模式设置") 55 | } 56 | return conn 57 | } 58 | 59 | func checkLogOpen() { 60 | if config.enableLog { 61 | path := fmt.Sprintf(config.logFilePath, config.portName, time.Now().Format("2006_01_02T150405")) 62 | f, err := os.OpenFile(path, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | outs = append(outs, f) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/charmbracelet/bubbles/key" 6 | inf "github.com/fzdwx/infinite" 7 | "github.com/fzdwx/infinite/color" 8 | "github.com/fzdwx/infinite/components" 9 | "github.com/fzdwx/infinite/components/input/text" 10 | "github.com/fzdwx/infinite/components/selection/confirm" 11 | "github.com/fzdwx/infinite/components/selection/singleselect" 12 | "github.com/fzdwx/infinite/style" 13 | "github.com/fzdwx/infinite/theme" 14 | "github.com/spf13/pflag" 15 | "go.bug.st/serial" 16 | "log" 17 | "strconv" 18 | "strings" 19 | ) 20 | 21 | type ptrVal struct { 22 | *string 23 | sl *[]string 24 | *int 25 | il *[]int 26 | *bool 27 | *float64 28 | *float32 29 | ext *string 30 | } 31 | type Val struct { 32 | string 33 | int 34 | bool 35 | float64 36 | float32 37 | extdef string 38 | } 39 | type Flag struct { 40 | v ptrVal 41 | sStr string 42 | lStr string 43 | dv Val 44 | help string 45 | } 46 | 47 | var ( 48 | portName = Flag{ptrVal{string: &config.portName}, "p", "port", Val{string: ""}, "要连接的串口\t(/dev/ttyUSB0、COMx)"} 49 | baudRate = Flag{ptrVal{int: &config.baudRate}, "b", "baud", Val{int: 115200}, "波特率"} 50 | dataBits = Flag{ptrVal{int: &config.dataBits}, "d", "data", Val{int: 8}, "数据位"} 51 | stopBits = Flag{ptrVal{int: &config.stopBits}, "s", "stop", Val{int: 0}, "停止位停止位(0: 1停止 1:1.5停止 2:2停止)"} 52 | outputCode = Flag{ptrVal{string: &config.outputCode}, "o", "out", Val{string: "UTF-8"}, "输出编码"} 53 | inputCode = Flag{ptrVal{string: &config.inputCode}, "i", "in", Val{string: "UTF-8"}, "输入编码"} 54 | endStr = Flag{ptrVal{string: &config.endStr}, "e", "end", Val{string: "\n"}, "终端换行符"} 55 | logExt = Flag{v: ptrVal{ext: &config.logFilePath}, sStr: "l", lStr: "log", dv: Val{extdef: "./%s-$s.txt", string: ""}, help: "日志保存路径"} 56 | timeExt = Flag{v: ptrVal{ext: &config.timesFmt}, sStr: "t", lStr: "time", dv: Val{extdef: "[06-01-02 15:04:05.000]", string: ""}, help: "时间戳格式化字段"} 57 | forWard = Flag{ptrVal{il: &config.forWard}, "f", "forward", Val{int: 0}, "转发模式(0: 无 1:TCP-C 2:UDP-C 支持多次传入)"} 58 | address = Flag{ptrVal{sl: &config.address}, "a", "address", Val{string: "127.0.0.1:12345"}, "转发服务地址(支持多次传入)"} 59 | frameSize = Flag{ptrVal{int: &config.frameSize}, "F", "Frame", Val{int: 16}, "帧大小"} 60 | parityBit = Flag{ptrVal{int: &config.parityBit}, "v", "verify", Val{int: 0}, "奇偶校验(0:无校验、1:奇校验、2:偶校验、3:1校验、4:0校验)"} 61 | flags = []Flag{portName, baudRate, dataBits, stopBits, outputCode, inputCode, endStr, forWard, address, frameSize, parityBit, logExt, timeExt} 62 | ) 63 | 64 | var ( 65 | bauds = []string{"自定义", "300", "600", "1200", "2400", "4800", "9600", 66 | "14400", "19200", "38400", "56000", "57600", "115200", "128000", 67 | "256000", "460800", "512000", "750000", "921600", "1500000"} 68 | datas = []string{"5", "6", "7", "8"} 69 | stops = []string{"1", "1.5", "2"} 70 | paritys = []string{"无校验", "奇校验", "偶校验", "1校验", "0校验"} 71 | forwards = []string{"No", "TCP-C", "UDP-C"} 72 | ) 73 | 74 | type ValType int 75 | 76 | const ( 77 | notVal ValType = iota 78 | stringVal 79 | intVal 80 | boolVal 81 | extVal 82 | ) 83 | 84 | func printUsage(ports []string) { 85 | fmt.Printf("\n参数帮助:\n") 86 | for _, f := range flags { 87 | flagprint(f) 88 | } 89 | fmt.Printf("\n在线串口: %v\n", strings.Join(ports, ",")) 90 | } 91 | func flagFindValue(v ptrVal) ValType { 92 | if v.string != nil { 93 | return stringVal 94 | } 95 | if v.bool != nil { 96 | return boolVal 97 | } 98 | if v.int != nil { 99 | return intVal 100 | } 101 | if v.ext != nil { 102 | return extVal 103 | } 104 | return notVal 105 | } 106 | func flagprint(f Flag) { 107 | switch flagFindValue(f.v) { 108 | case stringVal: 109 | fmt.Printf("\t-%v -%v %T \n\t %v\t默认值:%q\n", f.sStr, f.lStr, f.dv.string, f.help, f.dv.string) 110 | case intVal: 111 | fmt.Printf("\t-%v -%v %T \n\t %v\t默认值:%v\n", f.sStr, f.lStr, f.dv.int, f.help, f.dv.int) 112 | case boolVal: 113 | fmt.Printf("\t-%v -%v %T \n\t %v\t默认值:%v\n", f.sStr, f.lStr, f.dv.bool, f.help, f.dv.bool) 114 | case extVal: 115 | fmt.Printf("\t-%v -%v %T \n\t %v\t默认值:%v\n", f.sStr, f.lStr, f.dv.extdef, f.help, f.dv.extdef) 116 | default: 117 | panic("unhandled default case") 118 | } 119 | } 120 | func flagInit(f *Flag) { 121 | if f.v.string != nil { 122 | pflag.StringVarP(f.v.string, f.lStr, f.sStr, f.dv.string, f.help) 123 | } 124 | if f.v.bool != nil { 125 | pflag.BoolVarP(f.v.bool, f.lStr, f.sStr, f.dv.bool, f.help) 126 | } 127 | if f.v.int != nil { 128 | pflag.IntVarP(f.v.int, f.lStr, f.sStr, f.dv.int, f.help) 129 | } 130 | if f.v.ext != nil { 131 | pflag.StringVarP(f.v.ext, f.lStr, f.sStr, f.dv.string, f.help) 132 | pflag.Lookup(f.lStr).NoOptDefVal = f.dv.extdef 133 | } 134 | if f.v.sl != nil { 135 | pflag.StringArrayVarP(f.v.sl, f.lStr, f.sStr, []string{f.dv.string}, f.help) 136 | } 137 | if f.v.il != nil { 138 | pflag.IntSliceVarP(f.v.il, f.lStr, f.sStr, []int{f.dv.int}, f.help) 139 | } 140 | } 141 | func flagExt() { 142 | if config.logFilePath != "" { 143 | config.enableLog = true 144 | } 145 | if config.timesFmt != "" { 146 | config.timesTamp = true 147 | } 148 | } 149 | func getCliFlag() { 150 | ports, err := serial.GetPortsList() 151 | if err != nil { 152 | log.Fatal(err) 153 | } 154 | 155 | inputs := components.NewInput() 156 | inputs.Prompt = "Filtering: " 157 | inputs.PromptStyle = style.New().Bold().Italic().Fg(color.LightBlue) 158 | 159 | selectKeymap := singleselect.DefaultSingleKeyMap() 160 | selectKeymap.Confirm = key.NewBinding( 161 | key.WithKeys("enter"), 162 | key.WithHelp("enter", "finish select"), 163 | ) 164 | selectKeymap.Choice = key.NewBinding( 165 | key.WithKeys("enter"), 166 | key.WithHelp("enter", "finish select"), 167 | ) 168 | selectKeymap.NextPage = key.NewBinding( 169 | key.WithKeys("right"), 170 | key.WithHelp("->", "next page"), 171 | ) 172 | selectKeymap.PrevPage = key.NewBinding( 173 | key.WithKeys("left"), 174 | key.WithHelp("<-", "prev page"), 175 | ) 176 | 177 | s, _ := inf.NewSingleSelect( 178 | ports, 179 | singleselect.WithKeyBinding(selectKeymap), 180 | singleselect.WithPageSize(4), 181 | singleselect.WithFilterInput(inputs), 182 | ).Display("选择串口") 183 | config.portName = ports[s] 184 | 185 | s, _ = inf.NewSingleSelect( 186 | bauds, 187 | singleselect.WithKeyBinding(selectKeymap), 188 | singleselect.WithPageSize(4), 189 | ).Display("选择波特率") 190 | if s != 0 { 191 | config.baudRate, _ = strconv.Atoi(bauds[s]) 192 | } else { 193 | b, _ := inf.NewText( 194 | text.WithPrompt("BaudRate:"), 195 | text.WithPromptStyle(theme.DefaultTheme.PromptStyle), 196 | text.WithDefaultValue("115200"), 197 | ).Display() 198 | config.baudRate, _ = strconv.Atoi(b) 199 | } 200 | v, _ := inf.NewConfirmWithSelection( 201 | confirm.WithPrompt("启用Hex"), 202 | ).Display() 203 | if v { 204 | config.inputCode = "hex" 205 | b, _ := inf.NewText( 206 | text.WithPrompt("Frames:"), 207 | text.WithPromptStyle(theme.DefaultTheme.PromptStyle), 208 | text.WithDefaultValue("16"), 209 | ).Display() 210 | config.frameSize, _ = strconv.Atoi(b) 211 | } 212 | v, _ = inf.NewConfirmWithSelection( 213 | confirm.WithPrompt("启用时间戳"), 214 | ).Display() 215 | config.timesTamp = v 216 | if v { 217 | b, _ := inf.NewText( 218 | text.WithPrompt("格式化字段:"), 219 | text.WithPromptStyle(theme.DefaultTheme.PromptStyle), 220 | text.WithDefaultValue(timeExt.dv.extdef), 221 | ).Display() 222 | config.timesFmt = b 223 | } 224 | v, _ = inf.NewConfirmWithSelection( 225 | confirm.WithPrompt("启用高级配置"), 226 | ).Display() 227 | if v { 228 | s, _ = inf.NewSingleSelect( 229 | datas, 230 | singleselect.WithKeyBinding(selectKeymap), 231 | singleselect.WithPageSize(4), 232 | singleselect.WithFilterInput(inputs), 233 | ).Display("选择数据位") 234 | config.dataBits, _ = strconv.Atoi(datas[s]) 235 | 236 | s, _ = inf.NewSingleSelect( 237 | stops, 238 | singleselect.WithKeyBinding(selectKeymap), 239 | singleselect.WithPageSize(4), 240 | singleselect.WithFilterInput(inputs), 241 | ).Display("选择停止位") 242 | config.stopBits = s 243 | 244 | s, _ = inf.NewSingleSelect( 245 | paritys, 246 | singleselect.WithKeyBinding(selectKeymap), 247 | singleselect.WithPageSize(4), 248 | singleselect.WithFilterInput(inputs), 249 | ).Display("选择校验位") 250 | config.parityBit = s 251 | 252 | t, _ := inf.NewText( 253 | text.WithPrompt("换行符:"), 254 | text.WithPromptStyle(theme.DefaultTheme.PromptStyle), 255 | text.WithDefaultValue(endStr.dv.string), 256 | ).Display() 257 | config.endStr = t 258 | 259 | v, _ = inf.NewConfirmWithSelection( 260 | confirm.WithDefaultYes(), 261 | confirm.WithPrompt("启用编码转换"), 262 | ).Display() 263 | 264 | if v { 265 | t, _ = inf.NewText( 266 | text.WithPrompt("输入编码:"), 267 | text.WithPromptStyle(theme.DefaultTheme.PromptStyle), 268 | text.WithDefaultValue(inputCode.dv.string), 269 | ).Display() 270 | config.inputCode = t 271 | 272 | t, _ = inf.NewText( 273 | text.WithPrompt("输出编码:"), 274 | text.WithPromptStyle(theme.DefaultTheme.PromptStyle), 275 | text.WithDefaultValue(outputCode.dv.string), 276 | ).Display() 277 | config.outputCode = t 278 | } 279 | G_F_mode: 280 | s, _ = inf.NewSingleSelect( 281 | forwards, 282 | singleselect.WithKeyBinding(selectKeymap), 283 | singleselect.WithPageSize(3), 284 | singleselect.WithFilterInput(inputs), 285 | ).Display("选择转发模式") 286 | if s != 0 { 287 | config.forWard = append(config.forWard, s) 288 | t, _ = inf.NewText( 289 | text.WithPrompt("地址:"), 290 | text.WithPromptStyle(theme.DefaultTheme.PromptStyle), 291 | text.WithDefaultValue(address.dv.string), 292 | ).Display() 293 | config.address = append(config.address, t) 294 | goto G_F_mode 295 | } 296 | 297 | e, _ := inf.NewConfirmWithSelection( 298 | confirm.WithDefaultYes(), 299 | confirm.WithPrompt("启用日志"), 300 | ).Display() 301 | config.enableLog = e 302 | if e { 303 | t, _ = inf.NewText( 304 | text.WithPrompt("Path:"), 305 | text.WithPromptStyle(theme.DefaultTheme.PromptStyle), 306 | text.WithDefaultValue("./%s-$s.txt"), 307 | ).Display() 308 | config.logFilePath = t 309 | } 310 | } 311 | 312 | } 313 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module COM 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/charmbracelet/bubbles v0.18.0 7 | github.com/fzdwx/infinite v0.12.1 8 | github.com/gobwas/ws v1.4.0 9 | github.com/spf13/pflag v1.0.5 10 | github.com/trzsz/trzsz-go v1.1.7 11 | github.com/zimolab/charsetconv v0.1.2 12 | go.bug.st/serial v1.6.2 13 | golang.org/x/term v0.19.0 14 | ) 15 | 16 | require ( 17 | github.com/UserExistsError/conpty v0.1.2 // indirect 18 | github.com/akavel/rsrc v0.10.2 // indirect 19 | github.com/alexflint/go-scalar v1.2.0 // indirect 20 | github.com/atotto/clipboard v0.1.4 // indirect 21 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 22 | github.com/charmbracelet/bubbletea v0.25.0 // indirect 23 | github.com/charmbracelet/lipgloss v0.9.1 // indirect 24 | github.com/chzyer/readline v1.5.1 // indirect 25 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect 26 | github.com/creack/goselect v0.1.2 // indirect 27 | github.com/creack/pty v1.1.21 // indirect 28 | github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect 29 | github.com/duke-git/lancet/v2 v2.2.1 // indirect 30 | github.com/fzdwx/iter v0.0.0-20230511075109-0afee9319312 // indirect 31 | github.com/gobwas/httphead v0.1.0 // indirect 32 | github.com/gobwas/pool v0.2.1 // indirect 33 | github.com/josephspurrier/goversioninfo v1.4.0 // indirect 34 | github.com/klauspost/compress v1.17.4 // indirect 35 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 36 | github.com/mattn/go-isatty v0.0.19 // indirect 37 | github.com/mattn/go-localereader v0.0.1 // indirect 38 | github.com/mattn/go-runewidth v0.0.15 // indirect 39 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 40 | github.com/muesli/cancelreader v0.2.2 // indirect 41 | github.com/muesli/reflow v0.3.0 // indirect 42 | github.com/muesli/termenv v0.15.2 // indirect 43 | github.com/ncruces/zenity v0.10.10 // indirect 44 | github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect 45 | github.com/rivo/uniseg v0.4.6 // indirect 46 | github.com/rotisserie/eris v0.5.4 // indirect 47 | github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect 48 | github.com/trzsz/go-arg v1.5.3 // indirect 49 | github.com/trzsz/promptui v0.10.5 // indirect 50 | golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect 51 | golang.org/x/image v0.14.0 // indirect 52 | golang.org/x/sync v0.2.0 // indirect 53 | golang.org/x/sys v0.19.0 // indirect 54 | golang.org/x/text v0.14.0 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /image/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img1.png -------------------------------------------------------------------------------- /image/img10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img10.png -------------------------------------------------------------------------------- /image/img11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img11.png -------------------------------------------------------------------------------- /image/img12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img12.png -------------------------------------------------------------------------------- /image/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img2.png -------------------------------------------------------------------------------- /image/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img3.png -------------------------------------------------------------------------------- /image/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img4.png -------------------------------------------------------------------------------- /image/img5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img5.png -------------------------------------------------------------------------------- /image/img6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img6.png -------------------------------------------------------------------------------- /image/img7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img7.png -------------------------------------------------------------------------------- /image/img8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img8.png -------------------------------------------------------------------------------- /image/img9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jixishi/SerialTerminalForWindowsTerminal/f6eff2da22add36791e6d565eea55e4553d57b3c/image/img9.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/pflag" 6 | "io" 7 | "log" 8 | "os" 9 | ) 10 | 11 | func init() { 12 | log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile | log.Lmsgprefix) 13 | for _, f := range flags { 14 | flagInit(&f) 15 | } 16 | cmdinit() 17 | } 18 | 19 | func main() { 20 | pflag.Parse() 21 | flagExt() 22 | if config.portName == "" { 23 | getCliFlag() 24 | } 25 | ports, err := checkPortAvailability(config.portName) 26 | if err != nil { 27 | fmt.Println(err) 28 | printUsage(ports) 29 | os.Exit(0) 30 | } 31 | 32 | // 日志文件输出检测 33 | checkLogOpen() 34 | 35 | //串口设备开启 36 | OpenSerial() 37 | 38 | defer CloseSerial() 39 | // 打开文件服务 40 | OpenTrzsz() 41 | 42 | defer CloseTrzsz() 43 | 44 | //开启转发 45 | OpenForwarding() 46 | 47 | // 获取终端输入 48 | go input(in) 49 | 50 | if len(outs) != 1 { 51 | out = io.MultiWriter(outs...) 52 | } 53 | for { 54 | output() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /mutual.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/trzsz/trzsz-go/trzsz" 7 | "github.com/zimolab/charsetconv" 8 | "go.bug.st/serial" 9 | "io" 10 | "log" 11 | "os" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var ( 17 | serialPort serial.Port 18 | in io.Reader = os.Stdin 19 | out io.Writer = os.Stdout 20 | outs = []io.Writer{os.Stdout} 21 | trzszFilter *trzsz.TrzszFilter 22 | clientIn *io.PipeReader 23 | stdoutPipe *io.PipeReader 24 | stdinPipe *io.PipeWriter 25 | clientOut *io.PipeWriter 26 | ) 27 | 28 | func input(in io.Reader) { 29 | var err error 30 | input := bufio.NewScanner(in) 31 | var ok = false 32 | for { 33 | input.Scan() 34 | ok = false 35 | args = strings.Split(input.Text(), " ") 36 | for _, cmd := range commands { 37 | if strings.Compare(strings.TrimSpace(args[0]), cmd.name) == 0 { 38 | cmd.function() 39 | ok = true 40 | } 41 | } 42 | if !ok { 43 | _, err := io.WriteString(stdinPipe, input.Text()) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | _, err = io.WriteString(stdinPipe, config.endStr) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | } 52 | err = serialPort.Drain() 53 | ErrorF(err) 54 | } 55 | } 56 | 57 | func strout(out io.Writer, cs, str string) { 58 | err := charsetconv.EncodeWith(strings.NewReader(str), out, charsetconv.Charset(cs), false) 59 | ErrorF(err) 60 | } 61 | 62 | func output() { 63 | var err error 64 | if strings.Compare(config.inputCode, "hex") == 0 { 65 | b := make([]byte, config.frameSize) 66 | r, _ := io.LimitReader(stdoutPipe, int64(config.frameSize)).Read(b) 67 | if r != 0 { 68 | if config.timesTamp { 69 | strout(out, config.outputCode, fmt.Sprintf("%v % X %q \n", time.Now().Format(config.timesFmt), b, b)) 70 | } else { 71 | strout(out, config.outputCode, fmt.Sprintf("% X %q \n", b, b)) 72 | } 73 | } 74 | } else { 75 | if config.timesTamp { 76 | line, _, _ := bufio.NewReader(stdoutPipe).ReadLine() 77 | if line != nil { 78 | strout(out, config.outputCode, fmt.Sprintf("%v %s\n", time.Now().Format(config.timesFmt), line)) 79 | } 80 | } else { 81 | err = charsetconv.ConvertWith(stdoutPipe, charsetconv.Charset(config.inputCode), out, charsetconv.Charset(config.outputCode), false) 82 | } 83 | } 84 | ErrorP(err) 85 | } 86 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/trzsz/trzsz-go/trzsz" 6 | "go.bug.st/serial" 7 | "golang.org/x/term" 8 | "io" 9 | "log" 10 | "net" 11 | "os" 12 | "os/signal" 13 | "runtime" 14 | "strings" 15 | ) 16 | 17 | func checkPortAvailability(name string) ([]string, error) { 18 | ports, err := serial.GetPortsList() 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | if len(ports) == 0 { 23 | return nil, fmt.Errorf("无串口") 24 | } 25 | if name == "" { 26 | return ports, fmt.Errorf("串口未指定") 27 | } 28 | for _, port := range ports { 29 | if strings.Compare(port, name) == 0 { 30 | return ports, nil 31 | } 32 | } 33 | return ports, fmt.Errorf("串口 " + name + " 未在线") 34 | } 35 | 36 | func OpenSerial() { 37 | var err error 38 | mode := &serial.Mode{ 39 | BaudRate: config.baudRate, 40 | StopBits: serial.StopBits(config.stopBits), 41 | DataBits: config.dataBits, 42 | Parity: serial.Parity(config.parityBit), 43 | } 44 | serialPort, err = serial.Open(config.portName, mode) 45 | ErrorF(err) 46 | return 47 | } 48 | 49 | func CloseSerial() { 50 | err := serialPort.Close() 51 | ErrorF(err) 52 | return 53 | } 54 | 55 | var termch chan os.Signal 56 | 57 | // OpenTrzsz create a TrzszFilter to support trzsz ( trz / tsz ). 58 | // 59 | // ┌────────┐ stdinPipe ┌────────┐ ClientIn ┌─────────────┐ SerialIn ┌────────┐ 60 | // │ ├─────────────►│ ├─────────────►│ ├─────────────►│ │ 61 | // │ mutual │ │ Client │ │ TrzszFilter │ │ Serial │ 62 | // │ │◄─────────────│ │◄─────────────┤ │◄─────────────┤ │ 63 | // └────────┘ stdoutPipe └────────┘ ClientOut └─────────────┘ SerialOut └────────┘ 64 | func OpenTrzsz() { 65 | fd := int(os.Stdin.Fd()) 66 | width, _, err := term.GetSize(fd) 67 | if err != nil { 68 | if runtime.GOOS != "windows" { 69 | fmt.Printf("term get size failed: %s\n", err) 70 | return 71 | } 72 | width = 80 73 | } 74 | 75 | clientIn, stdinPipe = io.Pipe() 76 | stdoutPipe, clientOut = io.Pipe() 77 | trzszFilter = trzsz.NewTrzszFilter(clientIn, clientOut, serialPort, serialPort, 78 | trzsz.TrzszOptions{TerminalColumns: int32(width), EnableZmodem: true}) 79 | trzsz.SetAffectedByWindows(false) 80 | termch = make(chan os.Signal, 1) 81 | go func() { 82 | for range termch { 83 | width, _, err := term.GetSize(fd) 84 | if err != nil { 85 | fmt.Printf("term get size failed: %s\n", err) 86 | continue 87 | } 88 | trzszFilter.SetTerminalColumns(int32(width)) 89 | } 90 | }() 91 | } 92 | 93 | func CloseTrzsz() { 94 | signal.Stop(termch) 95 | close(termch) 96 | } 97 | 98 | func OpenForwarding() { 99 | for i, mode := range config.forWard { 100 | if FoeWardMode(mode) != NOT { 101 | conn := setForWardClient(FoeWardMode(mode), config.address[i]) 102 | outs = append(outs, conn) 103 | go func() { 104 | defer func(conn net.Conn) { 105 | err := conn.Close() 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | }(conn) 110 | input(conn) 111 | }() 112 | } 113 | } 114 | } 115 | 116 | func ErrorP(err error) { 117 | if err != nil { 118 | fmt.Fprint(os.Stderr, err) 119 | } 120 | } 121 | func ErrorF(err error) { 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | } 126 | --------------------------------------------------------------------------------