package printer import ( "regexp" "strconv" "strings" ) var ( tempRe = regexp.MustCompile(`(T\d*|B|C):\s*(-?\d+(?:\.\d+)?)\s*/\s*(-?\d+(?:\.\d+)?)`) sdRe = regexp.MustCompile(`(?i)SD printing byte\s+(\d+)\s*/\s*(\d+)`) notSDRe = regexp.MustCompile(`(?i)Not SD printing`) sdDoneRe = regexp.MustCompile(`(?i)Done printing file`) fileOpen = regexp.MustCompile(`(?i)File opened:\s*(\S+)`) sdListStart = regexp.MustCompile(`(?i)^Begin file list`) sdListEnd = regexp.MustCompile(`(?i)^End file list`) sdListLongRe = regexp.MustCompile(`"([^"]+)"`) ) // Parse one line of printer output and mutate s. Returns true if anything changed. func (s *State) Parse(line string) bool { line = strings.TrimSpace(line) if line == "" { return false } changed := false for _, m := range tempRe.FindAllStringSubmatch(line, -1) { tool := m[1] cur, _ := strconv.ParseFloat(m[2], 64) tgt, _ := strconv.ParseFloat(m[3], 64) if s.Temps == nil { s.Temps = map[string]TempPair{} } s.Temps[tool] = TempPair{Current: cur, Target: tgt} changed = true } if s.Firmware == "" { if idx := strings.Index(line, "FIRMWARE_NAME:"); idx >= 0 { rest := line[idx+len("FIRMWARE_NAME:"):] // Cut at the next CAPS_TOKEN: marker that Marlin emits after FIRMWARE_NAME for _, sep := range []string{" SOURCE_CODE_URL:", " PROTOCOL_VERSION:", " MACHINE_TYPE:", " EXTRUDER_COUNT:"} { if i := strings.Index(rest, sep); i >= 0 { rest = rest[:i] } } s.Firmware = strings.TrimSpace(rest) if len(s.Firmware) > 48 { s.Firmware = s.Firmware[:48] } if s.Firmware != "" { changed = true } } } if m := fileOpen.FindStringSubmatch(line); m != nil { s.SDFilename = m[1] changed = true } // SD card directory listing (M20). The firmware emits "Begin file list", // one entry per line, then "End file list". While we're inside that // block, every non-marker line is a file entry; we accumulate into // SDFiles. When the block ends we flip SDListing off and leave the // completed slice in place for the UI to consume. if sdListStart.MatchString(line) { s.SDListing = true s.SDFiles = s.SDFiles[:0] changed = true } else if sdListEnd.MatchString(line) { if s.SDListing { s.SDListing = false changed = true } } else if s.SDListing { if f, ok := parseSDFileEntry(line); ok { s.SDFiles = append(s.SDFiles, f) changed = true } } if m := sdRe.FindStringSubmatch(line); m != nil { byteN, _ := strconv.ParseInt(m[1], 10, 64) total, _ := strconv.ParseInt(m[2], 10, 64) s.SDByte = byteN s.SDTotal = total if byteN > 0 && total > 0 { if s.PrintState != StateSDPrinting && s.PrintState != StatePaused { s.PrintState = StateSDPrinting if s.SDStart.IsZero() { s.SDStart = nowFn() } } else if s.SDStart.IsZero() { s.SDStart = nowFn() } } changed = true } else if notSDRe.MatchString(line) { if s.PrintState == StateSDPrinting { s.PrintState = StateIdle s.SDByte, s.SDTotal = 0, 0 s.SDStart = zeroTime changed = true } } else if sdDoneRe.MatchString(line) { s.PrintState = StateIdle s.SDByte = s.SDTotal changed = true } return changed } // parseSDFileEntry tries to read a single line from inside the // "Begin file list" / "End file list" block. Marlin emits // "FILENAME.GCO 123456" or "FILENAME.GCO 123456 \"Long Name.gcode\"". // Directories are reported with a trailing "/" — we surface them as // entries too so the user can spot them, even though M23 won't enter them. func parseSDFileEntry(line string) (SDFile, bool) { line = strings.TrimSpace(line) if line == "" { return SDFile{}, false } // Strip a trailing long-filename if present, capture it. long := "" if m := sdListLongRe.FindStringSubmatch(line); m != nil { long = m[1] line = strings.TrimSpace(line[:strings.Index(line, "\"")]) } fields := strings.Fields(line) if len(fields) == 0 { return SDFile{}, false } name := fields[0] var size int64 if len(fields) >= 2 { size, _ = strconv.ParseInt(fields[len(fields)-1], 10, 64) } return SDFile{Name: name, LongName: long, Size: size}, true }