From 9a5b231c88ff3d82240b9165154268db86a42ff1 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 6 Sep 2025 21:29:48 +0800 Subject: [PATCH] feat: llgo monitor -target target -port port --- cmd/internal/flags/flags.go | 10 +- cmd/internal/monitor/monitor.go | 69 ++++++++ cmd/llgo/monitor_cmd.gox | 29 +++ cmd/llgo/xgo_autogen.go | 33 +++- doc/Embedded_Cmd.md | 2 +- go.mod | 9 +- go.sum | 19 ++ internal/monitor/monitor.go | 303 ++++++++++++++++++++++++++++++++ 8 files changed, 464 insertions(+), 10 deletions(-) create mode 100644 cmd/internal/monitor/monitor.go create mode 100644 cmd/llgo/monitor_cmd.gox create mode 100644 internal/monitor/monitor.go diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index adb7c219..d2a7aadf 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -21,6 +21,7 @@ var Tags string var Target string var Emulator bool var Port string +var BaudRate int var AbiMode int var CheckLinkArgs bool var CheckLLFiles bool @@ -30,7 +31,6 @@ func AddBuildFlags(fs *flag.FlagSet) { fs.BoolVar(&Verbose, "v", false, "Verbose mode") fs.StringVar(&Tags, "tags", "", "Build tags") fs.StringVar(&BuildEnv, "buildenv", "", "Build environment") - fs.StringVar(&Target, "target", "", "Target platform (e.g., rp2040, wasi)") if buildenv.Dev { fs.IntVar(&AbiMode, "abi", 2, "ABI mode (default 2). 0 = none, 1 = cfunc, 2 = allfunc.") fs.BoolVar(&CheckLinkArgs, "check-linkargs", false, "check link args valid") @@ -46,7 +46,9 @@ func AddEmulatorFlags(fs *flag.FlagSet) { } func AddEmbeddedFlags(fs *flag.FlagSet) { + fs.StringVar(&Target, "target", "", "Target platform (e.g., rp2040, wasi)") fs.StringVar(&Port, "port", "", "Target port for flashing") + fs.IntVar(&BaudRate, "baudrate", 115200, "Baudrate for serial communication") } func AddCmpTestFlags(fs *flag.FlagSet) { @@ -57,18 +59,18 @@ func UpdateConfig(conf *build.Config) { conf.Tags = Tags conf.Verbose = Verbose conf.Target = Target + conf.Port = Port + conf.BaudRate = BaudRate switch conf.Mode { case build.ModeBuild: conf.OutFile = OutputFile conf.FileFormat = FileFormat case build.ModeRun, build.ModeTest: conf.Emulator = Emulator - conf.Port = Port case build.ModeInstall: - conf.Port = Port + case build.ModeCmpTest: conf.Emulator = Emulator - conf.Port = Port conf.GenExpect = Gen } if buildenv.Dev { diff --git a/cmd/internal/monitor/monitor.go b/cmd/internal/monitor/monitor.go new file mode 100644 index 00000000..9392d024 --- /dev/null +++ b/cmd/internal/monitor/monitor.go @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package monitor + +import ( + "fmt" + "os" + + "github.com/goplus/llgo/cmd/internal/base" + "github.com/goplus/llgo/cmd/internal/flags" + "github.com/goplus/llgo/internal/monitor" +) + +// Cmd represents the monitor command. +var Cmd = &base.Command{ + UsageLine: "llgo monitor [flags] [executable]", + Short: "Monitor serial output from device", +} + +func init() { + flags.AddEmbeddedFlags(&Cmd.Flag) + Cmd.Run = runMonitor +} + +func runMonitor(cmd *base.Command, args []string) { + cmd.Flag.Parse(args) + args = cmd.Flag.Args() + + if len(args) > 1 { + fmt.Fprintf(os.Stderr, "llgo monitor: too many arguments\n") + os.Exit(1) + } + + if flags.Port == "" && flags.Target == "" { + fmt.Fprintf(os.Stderr, "llgo monitor: must specify either -port or -target\n") + return + } + + var executable string + if len(args) == 1 { + executable = args[0] + } + + config := monitor.MonitorConfig{ + Port: flags.Port, + Target: flags.Target, + BaudRate: flags.BaudRate, + Executable: executable, + } + + if err := monitor.Monitor(config, true); err != nil { + fmt.Fprintf(os.Stderr, "llgo monitor: %v\n", err) + os.Exit(1) + } +} diff --git a/cmd/llgo/monitor_cmd.gox b/cmd/llgo/monitor_cmd.gox new file mode 100644 index 00000000..f87a4e85 --- /dev/null +++ b/cmd/llgo/monitor_cmd.gox @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import ( + self "github.com/goplus/llgo/cmd/internal/monitor" +) + +use "monitor [flags] [executable]" + +short "Monitor serial output from device" + +flagOff + +run args => { + self.Cmd.Run self.Cmd, args +} \ No newline at end of file diff --git a/cmd/llgo/xgo_autogen.go b/cmd/llgo/xgo_autogen.go index 1bf5cb7d..c42e19b6 100644 --- a/cmd/llgo/xgo_autogen.go +++ b/cmd/llgo/xgo_autogen.go @@ -8,6 +8,7 @@ import ( "github.com/goplus/llgo/cmd/internal/build" "github.com/goplus/llgo/cmd/internal/clean" "github.com/goplus/llgo/cmd/internal/install" + "github.com/goplus/llgo/cmd/internal/monitor" "github.com/goplus/llgo/cmd/internal/run" "github.com/goplus/llgo/cmd/internal/test" "github.com/goplus/llgo/internal/env" @@ -40,6 +41,10 @@ type Cmd_install struct { type App struct { xcmd.App } +type Cmd_monitor struct { + xcmd.Command + *App +} type Cmd_run struct { xcmd.Command *App @@ -64,10 +69,11 @@ func (this *App) Main() { _xgo_obj2 := &Cmd_cmptest{App: this} _xgo_obj3 := &Cmd_get{App: this} _xgo_obj4 := &Cmd_install{App: this} - _xgo_obj5 := &Cmd_run{App: this} - _xgo_obj6 := &Cmd_test{App: this} - _xgo_obj7 := &Cmd_version{App: this} - xcmd.Gopt_App_Main(this, _xgo_obj0, _xgo_obj1, _xgo_obj2, _xgo_obj3, _xgo_obj4, _xgo_obj5, _xgo_obj6, _xgo_obj7) + _xgo_obj5 := &Cmd_monitor{App: this} + _xgo_obj6 := &Cmd_run{App: this} + _xgo_obj7 := &Cmd_test{App: this} + _xgo_obj8 := &Cmd_version{App: this} + xcmd.Gopt_App_Main(this, _xgo_obj0, _xgo_obj1, _xgo_obj2, _xgo_obj3, _xgo_obj4, _xgo_obj5, _xgo_obj6, _xgo_obj7, _xgo_obj8) } //line cmd/llgo/build_cmd.gox:20 @@ -163,6 +169,25 @@ func (this *Cmd_install) Classfname() string { return "install" } +//line cmd/llgo/monitor_cmd.gox:21 +func (this *Cmd_monitor) Main(_xgo_arg0 string) { + this.Command.Main(_xgo_arg0) +//line cmd/llgo/monitor_cmd.gox:21:1 + this.Use("monitor [flags] [executable]") +//line cmd/llgo/monitor_cmd.gox:23:1 + this.Short("Monitor serial output from device") +//line cmd/llgo/monitor_cmd.gox:25:1 + this.FlagOff() +//line cmd/llgo/monitor_cmd.gox:27:1 + this.Run__1(func(args []string) { +//line cmd/llgo/monitor_cmd.gox:28:1 + monitor.Cmd.Run(monitor.Cmd, args) + }) +} +func (this *Cmd_monitor) Classfname() string { + return "monitor" +} + //line cmd/llgo/run_cmd.gox:20 func (this *Cmd_run) Main(_xgo_arg0 string) { this.Command.Main(_xgo_arg0) diff --git a/doc/Embedded_Cmd.md b/doc/Embedded_Cmd.md index 58d9386d..d689eae7 100644 --- a/doc/Embedded_Cmd.md +++ b/doc/Embedded_Cmd.md @@ -19,7 +19,7 @@ Compile program to output file. ### llgo run Compile and run program. - No `-target`: Run locally -- With `-target`: Run on device or emulator +- With `-target`: Run on device or emulator (equivalent to `install` + `monitor`) ### llgo test Compile and run tests. diff --git a/go.mod b/go.mod index 0324e027..0bd566d3 100644 --- a/go.mod +++ b/go.mod @@ -15,11 +15,18 @@ require ( golang.org/x/tools v0.36.0 ) -require github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 +require ( + github.com/mattn/go-tty v0.0.7 + github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 + go.bug.st/serial v1.6.4 +) require ( + github.com/creack/goselect v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect ) replace github.com/goplus/llgo/runtime => ./runtime diff --git a/go.sum b/go.sum index b5da526b..ab56b0c4 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/goplus/cobra v1.9.12 h1:0F9EdEbeGyITGz+mqoHoJ5KpUw97p1CkxV74IexHw5s= @@ -10,13 +14,28 @@ github.com/goplus/llvm v0.8.5 h1:DUnFeYC3Rco622tBEKGg8xkigRAV2fh5ZIfBCt7gOSs= github.com/goplus/llvm v0.8.5/go.mod h1:PeVK8GgzxwAYCiMiUAJb5wJR6xbhj989tu9oulKLLT4= github.com/goplus/mod v0.17.1 h1:ITovxDcc5zbURV/Wrp3/SBsYLgC1KrxY6pq1zMM2V94= github.com/goplus/mod v0.17.1/go.mod h1:iXEszBKqi38BAyQApBPyQeurLHmQN34YMgC2ZNdap50= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= +github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/qiniu/x v1.15.1 h1:avE+YQaowp8ZExjylOeSM73rUo3MQKBAYVxh4NJ8dY8= github.com/qiniu/x v1.15.1/go.mod h1:AiovSOCaRijaf3fj+0CBOpR1457pn24b0Vdb1JpwhII= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I= github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/monitor/monitor.go b/internal/monitor/monitor.go new file mode 100644 index 00000000..8622b5ae --- /dev/null +++ b/internal/monitor/monitor.go @@ -0,0 +1,303 @@ +package monitor + +import ( + "debug/dwarf" + "debug/elf" + "debug/macho" + "debug/pe" + "errors" + "fmt" + "go/token" + "io" + "os" + "os/signal" + "regexp" + "strconv" + "time" + + "github.com/goplus/llgo/internal/crosscompile" + "github.com/mattn/go-tty" + "go.bug.st/serial" +) + +// MonitorConfig contains configuration for the monitor +type MonitorConfig struct { + Port string // Serial port device + Target string // Target name for crosscompile config + BaudRate int // Baudrate of serial monitor + Executable string // Optional path to executable for debug info + WaitTime int // Wait time for port connection (ms) +} + +// Monitor starts serial monitoring with the given configuration +func Monitor(config MonitorConfig, verbose bool) error { + // Set defaults + if config.BaudRate == 0 { + config.BaudRate = 115200 + } + if config.WaitTime == 0 { + config.WaitTime = 300 + } + + // If target is specified, try to get port from crosscompile config + if config.Target != "" && config.Port == "" { + port, err := getPortFromTarget(config.Target) + if err != nil && verbose { + fmt.Fprintf(os.Stderr, "Warning: could not get port from target: %v\n", err) + } + if port != "" { + config.Port = port + } + } + + if config.Port == "" { + return fmt.Errorf("port not specified and could not determine from target") + } + + if verbose { + fmt.Fprintf(os.Stderr, "Connecting to %s at %d baud\n", config.Port, config.BaudRate) + } + + // Open serial port with retry + var serialConn serial.Port + var err error + for i := 0; i <= config.WaitTime; i++ { + serialConn, err = serial.Open(config.Port, &serial.Mode{BaudRate: config.BaudRate}) + if err != nil { + if i < config.WaitTime { + time.Sleep(10 * time.Millisecond) + continue + } + return fmt.Errorf("failed to open port %s: %w", config.Port, err) + } + break + } + defer serialConn.Close() + + // Open TTY for input + tty, err := tty.Open() + if err != nil { + return fmt.Errorf("failed to open TTY: %w", err) + } + defer tty.Close() + + // Setup signal handling + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt) + defer signal.Stop(sig) + + // Create output writer with optional debug info + var writer *outputWriter + if config.Executable != "" { + writer = newOutputWriter(os.Stdout, config.Executable) + } else { + writer = newOutputWriter(os.Stdout, "") + } + + fmt.Printf("Connected to %s. Press Ctrl-C to exit.\n", config.Port) + + errCh := make(chan error, 2) + + // Goroutine for reading from serial port + go func() { + buf := make([]byte, 100*1024) + for { + n, err := serialConn.Read(buf) + if err != nil { + errCh <- fmt.Errorf("serial read error: %w", err) + return + } + writer.Write(buf[:n]) + } + }() + + // Goroutine for reading from TTY and writing to serial port + go func() { + for { + r, err := tty.ReadRune() + if err != nil { + errCh <- fmt.Errorf("TTY read error: %w", err) + return + } + if r == 0 { + continue + } + serialConn.Write([]byte(string(r))) + } + }() + + // Wait for signal or error + select { + case <-sig: + if verbose { + fmt.Fprintf(os.Stderr, "\nDisconnected from %s\n", config.Port) + } + return nil + case err := <-errCh: + return err + } +} + +// getPortFromTarget tries to get serial port from target configuration +func getPortFromTarget(target string) (string, error) { + export, err := crosscompile.Use("", "", false, target) + if err != nil { + return "", err + } + + // Try to get port from serial port list + if len(export.Flash.SerialPort) > 0 { + return export.Flash.SerialPort[0], nil + } + + return "", fmt.Errorf("no serial port found in target configuration") +} + +var addressMatch = regexp.MustCompile(`^panic: runtime error at 0x([0-9a-f]+): `) + +// Extract the address from the "panic: runtime error at" message. +func extractPanicAddress(line []byte) uint64 { + matches := addressMatch.FindSubmatch(line) + if matches != nil { + address, err := strconv.ParseUint(string(matches[1]), 16, 64) + if err == nil { + return address + } + } + return 0 +} + +// Convert an address in the binary to a source address location. +func addressToLine(executable string, address uint64) (token.Position, error) { + data, err := readDWARF(executable) + if err != nil { + return token.Position{}, err + } + r := data.Reader() + + for { + e, err := r.Next() + if err != nil { + return token.Position{}, err + } + if e == nil { + break + } + switch e.Tag { + case dwarf.TagCompileUnit: + r.SkipChildren() + lr, err := data.LineReader(e) + if err != nil { + return token.Position{}, err + } + var lineEntry = dwarf.LineEntry{ + EndSequence: true, + } + for { + // Read the next .debug_line entry. + prevLineEntry := lineEntry + err := lr.Next(&lineEntry) + if err != nil { + if err == io.EOF { + break + } + return token.Position{}, err + } + + if prevLineEntry.EndSequence && lineEntry.Address == 0 { + // Tombstone value. This symbol has been removed, for + // example by the --gc-sections linker flag. It is still + // here in the debug information because the linker can't + // just remove this reference. + // Read until the next EndSequence so that this sequence is + // skipped. + // For more details, see (among others): + // https://reviews.llvm.org/D84825 + for { + err := lr.Next(&lineEntry) + if err != nil { + return token.Position{}, err + } + if lineEntry.EndSequence { + break + } + } + } + + if !prevLineEntry.EndSequence { + // The chunk describes the code from prevLineEntry to + // lineEntry. + if prevLineEntry.Address <= address && lineEntry.Address > address { + return token.Position{ + Filename: prevLineEntry.File.Name, + Line: prevLineEntry.Line, + Column: prevLineEntry.Column, + }, nil + } + } + } + } + } + + return token.Position{}, nil // location not found +} + +// Read the DWARF debug information from a given file (in various formats). +func readDWARF(executable string) (*dwarf.Data, error) { + f, err := os.Open(executable) + if err != nil { + return nil, err + } + defer f.Close() + + if file, err := elf.NewFile(f); err == nil { + return file.DWARF() + } else if file, err := macho.NewFile(f); err == nil { + return file.DWARF() + } else if file, err := pe.NewFile(f); err == nil { + return file.DWARF() + } else { + return nil, errors.New("unknown binary format") + } +} + +type outputWriter struct { + out io.Writer + executable string + line []byte +} + +// newOutputWriter returns an io.Writer that will intercept panic addresses and +// will try to insert a source location in the output if the source location can +// be found in the executable. +func newOutputWriter(out io.Writer, executable string) *outputWriter { + return &outputWriter{ + out: out, + executable: executable, + } +} + +func (w *outputWriter) Write(p []byte) (n int, err error) { + start := 0 + for i, c := range p { + if c == '\n' { + w.out.Write(p[start : i+1]) + start = i + 1 + if w.executable != "" { + address := extractPanicAddress(w.line) + if address != 0 { + loc, err := addressToLine(w.executable, address) + if err == nil && loc.Filename != "" { + fmt.Printf("[llgo: panic at %s]\n", loc.String()) + } + } + } + w.line = w.line[:0] + } else { + w.line = append(w.line, c) + } + } + w.out.Write(p[start:]) + n = len(p) + return +}