package meshcore import ( "bufio" "encoding/hex" "fmt" "io" "math" "strconv" "strings" "time" "git.maze.io/go/ham/protocol" "git.maze.io/go/ham/radio" ) type repeaterDriver struct { conn io.ReadWriteCloser packets chan *Packet rawPackets chan *protocol.Packet waiting chan *repeaterDriverWaiting hasSNR bool lastFrame []byte lastFrameAt time.Time info repeaterInfo err error } type repeaterDriverWaiting struct { response chan string err chan error } func newRepeaterDriverWaiting() *repeaterDriverWaiting { return &repeaterDriverWaiting{ response: make(chan string), err: make(chan error), } } func (wait *repeaterDriverWaiting) Respond(response string) { select { case wait.response <- response: default: Logger.Warnf("meshcore: waiting discard response: %q", response) } } func (wait *repeaterDriverWaiting) Error(err error) { select { case wait.err <- err: default: Logger.Warnf("meshcore: waiting discard error: %q", err) } } func (wait *repeaterDriverWaiting) Close() { Logger.Trace("meshcore: waiting closing") close(wait.response) close(wait.err) } func (wait *repeaterDriverWaiting) Wait() (string, error) { Logger.Trace("meshcore: waiting for response") select { case err := <-wait.err: Logger.Tracef("meshcore: waiting received error: %v", err) return "", err case response := <-wait.response: Logger.Tracef("meshcore: waiting received response: %d", len(response)) return response, nil } } type repeaterInfo struct { // Fields returns by CMD_APP_START. Type NodeType Power byte // in dBm MaxPower byte // in dBm PublicKey [32]byte Latitude float64 Longitude float64 HasMultiACKs bool AdvertLocationPolicy byte TelemetryFlags byte ManualAddContacts byte Frequency float64 // in MHz Bandwidth float64 // in kHz SpreadingFactor byte CodingRate byte Name string // Fields returns by CMD_DEVICE_QUERY. FirmwareVersion string FirmwareVersionCode byte FirmwareBuildDate string Manufacturer string } func newRepeaterDriver(conn io.ReadWriteCloser, hasSNR bool) *repeaterDriver { return &repeaterDriver{ conn: conn, waiting: make(chan *repeaterDriverWaiting, 16), hasSNR: hasSNR, } } func (drv *repeaterDriver) Close() error { return drv.conn.Close() } func (drv *repeaterDriver) Setup() (err error) { go drv.poll() if err = drv.queryDeviceInfo(); err != nil { return err } return drv.err } func (drv *repeaterDriver) Info() *radio.Info { var ( firmwareVersion = drv.info.FirmwareVersion firmwareDate time.Time ) if strings.Contains(firmwareVersion, "(Build: ") { // "v1.13.0 (Build: 15 Feb 2026)" part := strings.SplitN(firmwareVersion, "(Build: ", 2) firmwareVersion = strings.TrimSpace(part[0]) firmwareDate, _ = time.Parse("2 Jan 2006", strings.TrimRight(part[1], ")")) } var ( manufacturerPart = strings.SplitN(drv.info.Manufacturer, " ", 2) manufacturer = manufacturerPart[0] device string ) if len(manufacturerPart) > 1 { device = manufacturerPart[1] } var pos *radio.Position if drv.info.Latitude != 0 && drv.info.Longitude != 0 { pos = &radio.Position{ Latitude: drv.info.Latitude, Longitude: drv.info.Longitude, } } return &radio.Info{ Name: drv.info.Name, Manufacturer: manufacturer, Device: device, FirmwareVersion: firmwareVersion, FirmwareDate: firmwareDate, Modulation: protocol.LoRa, Position: pos, Frequency: math.Ceil(drv.info.Frequency*1000) / 1000, Bandwidth: drv.info.Bandwidth, Power: float64(drv.info.Power), LoRaSF: drv.info.SpreadingFactor, LoRaCR: drv.info.CodingRate, } } func (drv *repeaterDriver) Packets() <-chan *Packet { if drv.packets == nil { drv.packets = make(chan *Packet, 16) } return drv.packets } func (drv *repeaterDriver) RawPackets() <-chan *protocol.Packet { if drv.rawPackets == nil { drv.rawPackets = make(chan *protocol.Packet, 16) } return drv.rawPackets } func (drv *repeaterDriver) queryDeviceInfo() (err error) { if drv.info.FirmwareVersion, err = drv.writeCommand("ver"); err != nil { return } // Fetch name if drv.info.Name, err = drv.writeCommand("get", "name"); err != nil { return } var line string // Fetch frequency, bandwidth and LoRa settings if line, err = drv.writeCommand("get", "radio"); err != nil { return } if _, err = fmt.Sscanf(line, "%f,%f,%d,%d", &drv.info.Frequency, &drv.info.Bandwidth, &drv.info.SpreadingFactor, &drv.info.CodingRate); err != nil { return err } // Fetch tx power if line, err = drv.writeCommand("get", "tx"); err != nil { return } if _, err = fmt.Sscanf(line, "%d", &drv.info.Power); err != nil { return } // Fetch location if line, err = drv.writeCommand("get", "lat"); err != nil { return } if line != "0.0" { drv.info.Latitude, _ = strconv.ParseFloat(line, 64) if line, err = drv.writeCommand("get", "lon"); err != nil { return } drv.info.Longitude, _ = strconv.ParseFloat(line, 64) } // Fetch node type if line, err = drv.writeCommand("get", "role"); err != nil { return err } switch line { case "repeater": drv.info.Type = Repeater case "room_server": drv.info.Type = Room case "sensor": drv.info.Type = Sensor } // Fetch board type if drv.info.Manufacturer, err = drv.writeCommand("board"); err != nil { return } return err } func (drv *repeaterDriver) writeCommand(command string, args ...string) (response string, err error) { full := append([]string{command}, args...) if _, err = fmt.Fprintf(drv.conn, "%s\r\n", strings.Join(full, " ")); err != nil { return } return drv.wait() } func (drv *repeaterDriver) wait() (string, error) { wait := newRepeaterDriverWaiting() defer wait.Close() drv.waiting <- wait return wait.Wait() } func (drv *repeaterDriver) handleRXData(line string) { part := strings.SplitN(line, " RAW: ", 2) if len(part) == 1 || len(part[0]) < 21 { return } b, err := hex.DecodeString(part[1]) if err != nil { Logger.Warnf("meshcore: corrupt raw packet: %v", err) return } else if len(b) < 2 { return // nothing to do! } /* companion/repeater time isn't reliable, as they often don't have an RTC: when, err := time.Parse("15:04:05 - 02/3/2006", strings.TrimSpace(line[:21])) if err != nil { Logger.Warnf("meshcore: corrupt raw packet: %v", err) } */ var ( when = time.Now() snr float64 rssi int data []byte ) if drv.hasSNR { snr = float64(b[0]) / 4 rssi = int(b[1]) data = b[2:] if drv.rawPackets != nil { select { case drv.rawPackets <- &protocol.Packet{ Time: when, Protocol: protocol.MeshCore, SNR: snr, RSSI: rssi, Raw: data, }: default: Logger.Warn("meshcore: raw packet channel full, dropping packet") } } // Decode payload if drv.packets != nil { packet := new(Packet) if err := packet.UnmarshalBytes(data); err == nil { packet.SNR = snr packet.RSSI = rssi select { case drv.packets <- packet: default: Logger.Warn("meshcore: packet channel full, dropping packet") } } } } else { // Record last frame drv.lastFrame = b drv.lastFrameAt = when } } func (drv *repeaterDriver) handleRX(line string) { if drv.hasSNR || len(drv.lastFrame) == 0 { return // nothing to do here } var snr, rssi int // "11:08:55 - 15/5/2024 U: RX, len=74 (type=5, route=F, payload_len=67) SNR=12 RSSI=-94 score=1000 time=738 hash=C46CD3BB7D0D99E1" for _, part := range strings.Split(line, " ") { switch { case strings.HasPrefix(part, "SNR="): snr, _ = strconv.Atoi(part[4:]) case strings.HasPrefix(part, "RSSI="): rssi, _ = strconv.Atoi(part[5:]) } } if drv.rawPackets != nil { select { case drv.rawPackets <- &protocol.Packet{ Time: drv.lastFrameAt, Protocol: protocol.MeshCore, SNR: float64(snr), RSSI: rssi, Raw: drv.lastFrame, }: default: Logger.Warn("meshcore: raw packet channel full, dropping packet") } } // Decode payload if drv.packets != nil { packet := new(Packet) if err := packet.UnmarshalBytes(drv.lastFrame); err == nil { packet.SNR = float64(snr) packet.RSSI = rssi select { case drv.packets <- packet: default: Logger.Warn("meshcore: packet channel full, dropping packet") } } } // Clear state drv.lastFrame = nil } func (drv *repeaterDriver) poll() { r := bufio.NewReader(drv.conn) for { line, err := r.ReadString('\n') if err != nil { Logger.Errorf("meshcore: unrecoverable error: %v", err) drv.err = err return } line = strings.TrimSpace(line) Logger.Tracef("meshcore: handle %q", line) switch { case strings.HasPrefix(line, "-> "): select { case waiting := <-drv.waiting: waiting.Respond(strings.TrimPrefix(line[3:], "> ")) default: Logger.Warnf("meshcore: unhandled response %q", line[3:]) } case strings.Contains(line, " RAW: "): // "10:53:04 - 15/5/2024 U RAW: 0917081DE1B03B130A270E01233EA0FB261218CCABBAA02278F51E97585B9B3285B95EFEEC83BE91E3D1E4F79D88B2C9484AA6882EB217C992B5C3C99C" drv.handleRXData(line) case strings.Contains(line, ": RX,"): drv.handleRX(line) case strings.Contains(line, ": TX,"): // ignore (for now) default: Logger.Tracef("meshcore: repeater sent gibberish %q", line) } } } func (drv *repeaterDriver) parsePacket(line string) (time.Time, string, error) { // "11:08:38 - 15/5/2024 U: TX, len=124 (type=4, route=D, payload_len=122)" if len(line) < 22 { return time.Time{}, "", io.EOF } i := strings.IndexByte(line, ':') - 2 if i < 0 { return time.Time{}, "", io.EOF } t, err := time.Parse("15:04:05 - 02/3/2006", line[:i]) if err != nil { return time.Time{}, "", err } return t, line[i:], nil }