diff --git a/protocol/meshcore/node.go b/protocol/meshcore/node.go index 1c9f313..bb1ebbb 100644 --- a/protocol/meshcore/node.go +++ b/protocol/meshcore/node.go @@ -1,16 +1,8 @@ package meshcore import ( - "encoding/binary" - "encoding/hex" "errors" - "fmt" "io" - "math/rand/v2" - "slices" - "strings" - "sync" - "time" "git.maze.io/go/ham/protocol" "git.maze.io/go/ham/radio" @@ -40,6 +32,18 @@ func NewCompanion(conn io.ReadWriteCloser) (*Node, error) { }, nil } +func NewRepeater(conn io.ReadWriteCloser, hasSNR bool) (*Node, error) { + driver := newRepeaterDriver(conn, hasSNR) + + if err := driver.Setup(); err != nil { + return nil, err + } + + return &Node{ + driver: driver, + }, nil +} + func (dev *Node) Close() error { return dev.driver.Close() } @@ -76,457 +80,6 @@ type nodeTracer interface { Trace(path []byte) (snr []float64, err error) } -type CompanionError struct { - Code byte -} - -func (err CompanionError) Error() string { - switch err.Code { - case companionErrCodeUnsupported: - return "meshcore: companion: unsupported" - case companionErrCodeNotFound: - return "meshcore: companion: not found" - case companionErrCodeTableFull: - return "meshcore: companion: table full" - case companionErrCodeBadState: - return "meshcore: companion: bad state" - case companionErrCodeFileIOError: - return "meshcore: companion: file input/output error" - case companionErrCodeIllegalArgument: - return "meshcore: companion: illegal argument" - default: - return fmt.Sprintf("meshcore: companion: unknown error code %#02x", err.Code) - } -} - -type companionDriver struct { - conn io.ReadWriteCloser - mu sync.Mutex - packets chan *Packet - rawPackets chan *protocol.Packet - waiting chan *companionDriverWaiting - info companionInfo - traceTag uint32 - traceAuthCode uint32 -} - -type companionDriverWaiting struct { - expect []byte - response chan []byte - err chan error -} - -func newCompanionDriverWaiting(expect []byte) *companionDriverWaiting { - return &companionDriverWaiting{ - expect: expect, - response: make(chan []byte), - err: make(chan error), - } -} - -func (wait *companionDriverWaiting) Respond(response []byte) { - select { - case wait.response <- response: - default: - Logger.Warnf("meshcore: waiting for %02x discard response: %02x", wait.expect, response) - } -} - -func (wait *companionDriverWaiting) Error(err error) { - select { - case wait.err <- err: - default: - Logger.Warnf("meshcore: waiting for %02x discard error: %v", wait.expect, err) - } -} - -func (wait *companionDriverWaiting) Close() { - Logger.Tracef("meshcore: waiting for %02x closing", wait.expect) - close(wait.response) - close(wait.err) -} - -func (wait *companionDriverWaiting) Wait() ([]byte, error) { - Logger.Tracef("meshcore: waiting for %02x", wait.expect) - select { - case err := <-wait.err: - Logger.Tracef("meshcore: waiting for %02x received error: %v", wait.expect, err) - return nil, err - case response := <-wait.response: - Logger.Tracef("meshcore: waiting for %02x received response: %d", wait.expect, len(response)) - return response, nil - } -} - -type companionInfo 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 - MaxContacts int - MaxGroupChannels int - BLEPIN [4]byte -} - -func newCompanionDriver(conn io.ReadWriteCloser) *companionDriver { - return &companionDriver{ - conn: conn, - waiting: make(chan *companionDriverWaiting, 16), - traceTag: rand.Uint32(), - //traceAuthCode: rand.Uint32(), - } -} - -func (drv *companionDriver) Close() error { - return drv.conn.Close() -} - -func (drv *companionDriver) Setup() (err error) { - go drv.poll() - if err = drv.sendAppStart(); err != nil { - return - } - if err = drv.sendDeviceInfo(); err != nil { - return - } - return -} - -func (drv *companionDriver) Packets() <-chan *Packet { - if drv.packets == nil { - drv.packets = make(chan *Packet, 16) - } - return drv.packets -} - -func (drv *companionDriver) RawPackets() <-chan *protocol.Packet { - if drv.rawPackets == nil { - drv.rawPackets = make(chan *protocol.Packet, 16) - } - return drv.rawPackets -} - -func (drv *companionDriver) Info() *radio.Info { - var pos *radio.Position - if drv.info.Latitude != 0 && drv.info.Longitude != 0 { - pos = &radio.Position{ - Latitude: drv.info.Latitude, - Longitude: drv.info.Longitude, - } - } - - var firmwareDate time.Time - for _, layout := range []string{ - "02 Jan 2006", - "02-01-2006", - } { - var terr error - if firmwareDate, terr = time.Parse(layout, drv.info.FirmwareBuildDate); terr == nil { - break - } - } - - var ( - manufacturerPart = strings.SplitN(drv.info.Manufacturer, " ", 2) - manufacturer = manufacturerPart[0] - device string - ) - if len(manufacturerPart) > 1 { - device = manufacturerPart[1] - } - - return &radio.Info{ - Name: drv.info.Name, - Manufacturer: manufacturer, - Device: device, - FirmwareDate: firmwareDate, - FirmwareVersion: drv.info.FirmwareVersion, - Modulation: protocol.LoRa, - Position: pos, - Frequency: drv.info.Frequency, - Bandwidth: drv.info.Bandwidth, - Power: float64(drv.info.Power), - LoRaSF: drv.info.SpreadingFactor, - LoRaCR: drv.info.CodingRate, - } -} - -func (drv *companionDriver) Trace(path []byte) (snr []float64, err error) { - var ( - args = make([]byte, 4+4+1+len(path)) - data []byte - ) - binary.LittleEndian.PutUint32(args[0:], drv.traceTag) // tag - binary.LittleEndian.PutUint32(args[4:], drv.traceAuthCode) // authcode - args[8] = 0 // flags - copy(args[9:], path) // path - - Logger.Debugf("meshcore: trace %02x tag %08x authcode %08x", path, drv.traceTag, drv.traceAuthCode) - if data, err = drv.writeCommand(companionSendTracePath, args, companionResponseSent); err != nil { - return - } - - Logger.Debugf("trace response:\n%s", hex.Dump(data)) - return -} - -func (drv *companionDriver) readFrame() ([]byte, error) { - var frame [3 + maxCompanionFrameSize]byte - for { - n, err := drv.conn.Read(frame[:]) - if err != nil { - return nil, err - } else if n < 3 { - continue - } - - if frame[0] != '>' { - // not a companion frame - continue - } - - size := int(binary.LittleEndian.Uint16(frame[1:])) - if size > maxCompanionFrameSize { - return nil, fmt.Errorf("meshcore: companion sent frame size of %d, which exceeds maximum of %d", size, maxCompanionFrameSize) - } - - // Make sure we have read all bytes - o := n - for (o - 3) < size { - if n, err = drv.conn.Read(frame[o:]); err != nil { - return nil, err - } - o += n - } - - Logger.Tracef("read %d:\n%s", size, hex.Dump(frame[:3+size])) - return frame[3 : 3+size], nil - } -} - -func (drv *companionDriver) writeFrame(b []byte) (err error) { - if len(b) > maxCompanionFrameSize { - return fmt.Errorf("meshcore: companion: frame size %d exceed maximum of %d", len(b), maxCompanionFrameSize) - } - - var frame [3 + maxCompanionFrameSize]byte - frame[0] = '<' - binary.LittleEndian.PutUint16(frame[1:], uint16(len(b))) - n := copy(frame[3:], b) - - //Logger.Tracef("send %d:\n%s", n, hex.Dump(frame[:3+n])) - _, err = drv.conn.Write(frame[:3+n]) - return -} - -func (drv *companionDriver) writeCommand(cmd byte, args []byte, wait ...byte) ([]byte, error) { - drv.mu.Lock() - defer drv.mu.Unlock() - - if err := drv.writeFrame(append([]byte{cmd}, args...)); err != nil { - return nil, err - } - - return drv.wait(wait...) -} - -func (drv *companionDriver) wait(expect ...byte) ([]byte, error) { - wait := newCompanionDriverWaiting(expect) - defer wait.Close() - - drv.waiting <- wait - return wait.Wait() -} - -func (drv *companionDriver) handlePushFrame(b []byte) { - if len(b) < 1 { - return // illegal - } - switch b[0] { - case companionPushAdvert: - case companionPushMessageWaiting: - case companionPushLogRXData: - drv.handleRXData(b[1:]) - case companionPushNewAdvert: - default: - Logger.Warnf("meshcore: unhandled push %02x:\n%s", b[0], hex.Dump(b[1:])) - } -} - -func (drv *companionDriver) handleRXData(b []byte) { - if drv.packets == nil && drv.rawPackets == nil { - return // not listening for packets, discard - } - - if len(b) < 2+minPacketSize { - return // too short - } - - var ( - now = time.Now().UTC() - snr = float64(b[0]) / 4 - rssi = int8(b[1]) - ) - - // Decode raw - if drv.rawPackets != nil { - select { - case drv.rawPackets <- &protocol.Packet{ - Time: now, - Protocol: protocol.MeshCore, - SNR: snr, - RSSI: rssi, - Raw: b[2:], - }: - default: - Logger.Warn("meshcore: raw packet channel full, dropping packet") - } - } - - // Decode payload - if drv.packets != nil { - packet := new(Packet) - if err := packet.UnmarshalBytes(b[2:]); err == nil { - packet.SNR = snr - packet.RSSI = rssi - select { - case drv.packets <- packet: - default: - Logger.Warn("meshcore: packet channel full, dropping packet") - } - } - } -} - -func (drv *companionDriver) sendAppStart() (err error) { - var ( - b []byte - args = append(make([]byte, 8), []byte("git.maze.io/go/ham")...) - ) - if b, err = drv.writeCommand(companionAppStart, args, companionResponseSelfInfo); err != nil { - return fmt.Errorf("meshcore: can't send application start: %v", err) - } - //log.Printf("companion app start response:\n%s", hex.Dump(b)) - - const expect = 1 + 1 + 1 + 1 + 32 + 4 + 4 + 1 + 1 + 1 + 1 + 4 + 4 + 1 + 1 - if len(b) < expect { - return fmt.Errorf("companion: expected %d bytes of self info, got %d", expect, len(b)) - } - if b[0] != companionResponseSelfInfo { - return fmt.Errorf("companion: expected self info response, got %#02x", b[0]) - } - b = b[1:] - - drv.info.Type = NodeType(b[0]) - drv.info.Power = b[1] - drv.info.MaxPower = b[2] - copy(drv.info.PublicKey[:], b[3:]) - drv.info.Latitude, drv.info.Longitude = decodeLatLon(b[35:]) - drv.info.HasMultiACKs = b[43] != 0 - drv.info.AdvertLocationPolicy = b[44] - drv.info.TelemetryFlags = b[45] - drv.info.ManualAddContacts = b[46] - drv.info.Frequency = decodeFrequency(b[47:]) - drv.info.Bandwidth = decodeFrequency(b[51:]) - drv.info.SpreadingFactor = b[55] - drv.info.CodingRate = b[56] - drv.info.Name = strings.TrimRight(string(b[57:]), "\x00") - - return -} - -func (drv *companionDriver) sendDeviceInfo() (err error) { - var ( - args = []byte{0x03} - data []byte - ) - if data, err = drv.writeCommand(companionDeviceQuery, args, companionResponseDeviceInfo); err != nil { - return - } - - const expect = 4 + 4 + 12 + 40 + 20 - if len(data) < expect { - return fmt.Errorf("companion: expected %d bytes of self info, got %d", expect, len(data)) - } - if data[0] != companionResponseDeviceInfo { - return fmt.Errorf("companion: expected device info response, got %#02x", data[0]) - } - data = data[1:] - - drv.info.FirmwareVersionCode = data[0] - drv.info.MaxContacts = int(data[1]) * 2 - drv.info.MaxGroupChannels = int(data[2]) - drv.info.FirmwareBuildDate = decodeCString(data[7:19]) - drv.info.Manufacturer = decodeCString(data[19:59]) - drv.info.FirmwareVersion = decodeCString(data[59:79]) - - return -} - -func (drv *companionDriver) poll() { - for { - frame, err := drv.readFrame() - if err != nil { - Logger.Errorf("meshcore: unrecoverable error: %v", err) - return - } else if len(frame) < 1 { - continue - } - - response := frame[0] - Logger.Tracef("meshcore: handle %s (%02x, %d bytes)", companionResponseName(response), response, len(frame[1:])) - switch { - case response == companionResponseError: - err := CompanionError{Code: frame[1]} - select { - case waiting := <-drv.waiting: - Logger.Debugf("meshcore: sending error to waiting: %v", err) - waiting.Error(err) - default: - Logger.Debugf("meshcore: unexpected error: %v", err) - } - - case response >= 0x80: - drv.handlePushFrame(frame) - - default: - select { - case waiting := <-drv.waiting: - if len(waiting.expect) == 0 { - //Logger.Debugf("meshcore: respond %02x verbatim", response) - waiting.Respond(frame) - } else if slices.Contains(waiting.expect, response) { - //Logger.Debugf("meshcore: respond %02x to expected %02x", response, waiting.expect) - waiting.Respond(frame) - } else { - //Logger.Debugf("meshcore: unexpected %02x response (want %02x)", response, waiting.expect) - waiting.Error(fmt.Errorf("unexpected %02x response", response)) - } - default: - Logger.Warnf("meshcore: unhandled %02x response", response) - } - } - } -} - var ( _ protocol.PacketReceiver = (*Node)(nil) _ nodeDriver = (*companionDriver)(nil) diff --git a/protocol/meshcore/node_companion.go b/protocol/meshcore/node_companion.go new file mode 100644 index 0000000..3d16e50 --- /dev/null +++ b/protocol/meshcore/node_companion.go @@ -0,0 +1,467 @@ +package meshcore + +import ( + "encoding/binary" + "encoding/hex" + "fmt" + "io" + "math/rand/v2" + "slices" + "strings" + "sync" + "time" + + "git.maze.io/go/ham/protocol" + "git.maze.io/go/ham/radio" +) + +type CompanionError struct { + Code byte +} + +func (err CompanionError) Error() string { + switch err.Code { + case companionErrCodeUnsupported: + return "meshcore: companion: unsupported" + case companionErrCodeNotFound: + return "meshcore: companion: not found" + case companionErrCodeTableFull: + return "meshcore: companion: table full" + case companionErrCodeBadState: + return "meshcore: companion: bad state" + case companionErrCodeFileIOError: + return "meshcore: companion: file input/output error" + case companionErrCodeIllegalArgument: + return "meshcore: companion: illegal argument" + default: + return fmt.Sprintf("meshcore: companion: unknown error code %#02x", err.Code) + } +} + +type companionDriver struct { + conn io.ReadWriteCloser + mu sync.Mutex + packets chan *Packet + rawPackets chan *protocol.Packet + waiting chan *companionDriverWaiting + info companionInfo + traceTag uint32 + traceAuthCode uint32 +} + +type companionDriverWaiting struct { + expect []byte + response chan []byte + err chan error +} + +func newCompanionDriverWaiting(expect []byte) *companionDriverWaiting { + return &companionDriverWaiting{ + expect: expect, + response: make(chan []byte), + err: make(chan error), + } +} + +func (wait *companionDriverWaiting) Respond(response []byte) { + select { + case wait.response <- response: + default: + Logger.Warnf("meshcore: waiting for %02x discard response: %02x", wait.expect, response) + } +} + +func (wait *companionDriverWaiting) Error(err error) { + select { + case wait.err <- err: + default: + Logger.Warnf("meshcore: waiting for %02x discard error: %v", wait.expect, err) + } +} + +func (wait *companionDriverWaiting) Close() { + Logger.Tracef("meshcore: waiting for %02x closing", wait.expect) + close(wait.response) + close(wait.err) +} + +func (wait *companionDriverWaiting) Wait() ([]byte, error) { + Logger.Tracef("meshcore: waiting for %02x", wait.expect) + select { + case err := <-wait.err: + Logger.Tracef("meshcore: waiting for %02x received error: %v", wait.expect, err) + return nil, err + case response := <-wait.response: + Logger.Tracef("meshcore: waiting for %02x received response: %d", wait.expect, len(response)) + return response, nil + } +} + +type companionInfo 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 + MaxContacts int + MaxGroupChannels int + BLEPIN [4]byte +} + +func newCompanionDriver(conn io.ReadWriteCloser) *companionDriver { + return &companionDriver{ + conn: conn, + waiting: make(chan *companionDriverWaiting, 16), + traceTag: rand.Uint32(), + //traceAuthCode: rand.Uint32(), + } +} + +func (drv *companionDriver) Close() error { + return drv.conn.Close() +} + +func (drv *companionDriver) Setup() (err error) { + go drv.poll() + if err = drv.sendAppStart(); err != nil { + return + } + if err = drv.sendDeviceInfo(); err != nil { + return + } + return +} + +func (drv *companionDriver) Packets() <-chan *Packet { + if drv.packets == nil { + drv.packets = make(chan *Packet, 16) + } + return drv.packets +} + +func (drv *companionDriver) RawPackets() <-chan *protocol.Packet { + if drv.rawPackets == nil { + drv.rawPackets = make(chan *protocol.Packet, 16) + } + return drv.rawPackets +} + +func (drv *companionDriver) Info() *radio.Info { + var pos *radio.Position + if drv.info.Latitude != 0 && drv.info.Longitude != 0 { + pos = &radio.Position{ + Latitude: drv.info.Latitude, + Longitude: drv.info.Longitude, + } + } + + var firmwareDate time.Time + for _, layout := range []string{ + "02 Jan 2006", + "02-01-2006", + } { + var terr error + if firmwareDate, terr = time.Parse(layout, drv.info.FirmwareBuildDate); terr == nil { + break + } + } + + var ( + manufacturerPart = strings.SplitN(drv.info.Manufacturer, " ", 2) + manufacturer = manufacturerPart[0] + device string + ) + if len(manufacturerPart) > 1 { + device = manufacturerPart[1] + } + + return &radio.Info{ + Name: drv.info.Name, + Manufacturer: manufacturer, + Device: device, + FirmwareDate: firmwareDate, + FirmwareVersion: drv.info.FirmwareVersion, + Modulation: protocol.LoRa, + Position: pos, + Frequency: drv.info.Frequency, + Bandwidth: drv.info.Bandwidth, + Power: float64(drv.info.Power), + LoRaSF: drv.info.SpreadingFactor, + LoRaCR: drv.info.CodingRate, + } +} + +func (drv *companionDriver) Trace(path []byte) (snr []float64, err error) { + var ( + args = make([]byte, 4+4+1+len(path)) + data []byte + ) + binary.LittleEndian.PutUint32(args[0:], drv.traceTag) // tag + binary.LittleEndian.PutUint32(args[4:], drv.traceAuthCode) // authcode + args[8] = 0 // flags + copy(args[9:], path) // path + + Logger.Debugf("meshcore: trace %02x tag %08x authcode %08x", path, drv.traceTag, drv.traceAuthCode) + if data, err = drv.writeCommand(companionSendTracePath, args, companionResponseSent); err != nil { + return + } + + Logger.Debugf("trace response:\n%s", hex.Dump(data)) + return +} + +func (drv *companionDriver) readFrame() ([]byte, error) { + var frame [3 + maxCompanionFrameSize]byte + for { + n, err := drv.conn.Read(frame[:]) + if err != nil { + return nil, err + } else if n < 3 { + continue + } + + if frame[0] != '>' { + // not a companion frame + continue + } + + size := int(binary.LittleEndian.Uint16(frame[1:])) + if size > maxCompanionFrameSize { + return nil, fmt.Errorf("meshcore: companion sent frame size of %d, which exceeds maximum of %d", size, maxCompanionFrameSize) + } + + // Make sure we have read all bytes + o := n + for (o - 3) < size { + if n, err = drv.conn.Read(frame[o:]); err != nil { + return nil, err + } + o += n + } + + Logger.Tracef("read %d:\n%s", size, hex.Dump(frame[:3+size])) + return frame[3 : 3+size], nil + } +} + +func (drv *companionDriver) writeFrame(b []byte) (err error) { + if len(b) > maxCompanionFrameSize { + return fmt.Errorf("meshcore: companion: frame size %d exceed maximum of %d", len(b), maxCompanionFrameSize) + } + + var frame [3 + maxCompanionFrameSize]byte + frame[0] = '<' + binary.LittleEndian.PutUint16(frame[1:], uint16(len(b))) + n := copy(frame[3:], b) + + //Logger.Tracef("send %d:\n%s", n, hex.Dump(frame[:3+n])) + _, err = drv.conn.Write(frame[:3+n]) + return +} + +func (drv *companionDriver) writeCommand(cmd byte, args []byte, wait ...byte) ([]byte, error) { + drv.mu.Lock() + defer drv.mu.Unlock() + + if err := drv.writeFrame(append([]byte{cmd}, args...)); err != nil { + return nil, err + } + + return drv.wait(wait...) +} + +func (drv *companionDriver) wait(expect ...byte) ([]byte, error) { + wait := newCompanionDriverWaiting(expect) + defer wait.Close() + + drv.waiting <- wait + return wait.Wait() +} + +func (drv *companionDriver) handlePushFrame(b []byte) { + if len(b) < 1 { + return // illegal + } + switch b[0] { + case companionPushAdvert: + case companionPushMessageWaiting: + case companionPushLogRXData: + drv.handleRXData(b[1:]) + case companionPushNewAdvert: + default: + Logger.Warnf("meshcore: unhandled push %02x:\n%s", b[0], hex.Dump(b[1:])) + } +} + +func (drv *companionDriver) handleRXData(b []byte) { + if drv.packets == nil && drv.rawPackets == nil { + return // not listening for packets, discard + } + + if len(b) < 2+minPacketSize { + return // too short + } + + var ( + now = time.Now().UTC() + snr = float64(b[0]) / 4 + rssi = int8(b[1]) + ) + + // Decode raw + if drv.rawPackets != nil { + select { + case drv.rawPackets <- &protocol.Packet{ + Time: now, + Protocol: protocol.MeshCore, + SNR: snr, + RSSI: rssi, + Raw: b[2:], + }: + default: + Logger.Warn("meshcore: raw packet channel full, dropping packet") + } + } + + // Decode payload + if drv.packets != nil { + packet := new(Packet) + if err := packet.UnmarshalBytes(b[2:]); err == nil { + packet.SNR = snr + packet.RSSI = rssi + select { + case drv.packets <- packet: + default: + Logger.Warn("meshcore: packet channel full, dropping packet") + } + } + } +} + +func (drv *companionDriver) sendAppStart() (err error) { + var ( + b []byte + args = append(make([]byte, 8), []byte("git.maze.io/go/ham")...) + ) + if b, err = drv.writeCommand(companionAppStart, args, companionResponseSelfInfo); err != nil { + return fmt.Errorf("meshcore: can't send application start: %v", err) + } + //log.Printf("companion app start response:\n%s", hex.Dump(b)) + + const expect = 1 + 1 + 1 + 1 + 32 + 4 + 4 + 1 + 1 + 1 + 1 + 4 + 4 + 1 + 1 + if len(b) < expect { + return fmt.Errorf("companion: expected %d bytes of self info, got %d", expect, len(b)) + } + if b[0] != companionResponseSelfInfo { + return fmt.Errorf("companion: expected self info response, got %#02x", b[0]) + } + b = b[1:] + + drv.info.Type = NodeType(b[0]) + drv.info.Power = b[1] + drv.info.MaxPower = b[2] + copy(drv.info.PublicKey[:], b[3:]) + drv.info.Latitude, drv.info.Longitude = decodeLatLon(b[35:]) + drv.info.HasMultiACKs = b[43] != 0 + drv.info.AdvertLocationPolicy = b[44] + drv.info.TelemetryFlags = b[45] + drv.info.ManualAddContacts = b[46] + drv.info.Frequency = decodeFrequency(b[47:]) + drv.info.Bandwidth = decodeFrequency(b[51:]) + drv.info.SpreadingFactor = b[55] + drv.info.CodingRate = b[56] + drv.info.Name = strings.TrimRight(string(b[57:]), "\x00") + + return +} + +func (drv *companionDriver) sendDeviceInfo() (err error) { + var ( + args = []byte{0x03} + data []byte + ) + if data, err = drv.writeCommand(companionDeviceQuery, args, companionResponseDeviceInfo); err != nil { + return + } + + const expect = 4 + 4 + 12 + 40 + 20 + if len(data) < expect { + return fmt.Errorf("companion: expected %d bytes of self info, got %d", expect, len(data)) + } + if data[0] != companionResponseDeviceInfo { + return fmt.Errorf("companion: expected device info response, got %#02x", data[0]) + } + data = data[1:] + + drv.info.FirmwareVersionCode = data[0] + drv.info.MaxContacts = int(data[1]) * 2 + drv.info.MaxGroupChannels = int(data[2]) + drv.info.FirmwareBuildDate = decodeCString(data[7:19]) + drv.info.Manufacturer = decodeCString(data[19:59]) + drv.info.FirmwareVersion = decodeCString(data[59:79]) + + return +} + +func (drv *companionDriver) poll() { + for { + frame, err := drv.readFrame() + if err != nil { + Logger.Errorf("meshcore: unrecoverable error: %v", err) + return + } else if len(frame) < 1 { + continue + } + + response := frame[0] + Logger.Tracef("meshcore: handle %s (%02x, %d bytes)", companionResponseName(response), response, len(frame[1:])) + switch { + case response == companionResponseError: + err := CompanionError{Code: frame[1]} + select { + case waiting := <-drv.waiting: + Logger.Debugf("meshcore: sending error to waiting: %v", err) + waiting.Error(err) + default: + Logger.Debugf("meshcore: unexpected error: %v", err) + } + + case response >= 0x80: + drv.handlePushFrame(frame) + + default: + select { + case waiting := <-drv.waiting: + if len(waiting.expect) == 0 { + //Logger.Debugf("meshcore: respond %02x verbatim", response) + waiting.Respond(frame) + } else if slices.Contains(waiting.expect, response) { + //Logger.Debugf("meshcore: respond %02x to expected %02x", response, waiting.expect) + waiting.Respond(frame) + } else { + //Logger.Debugf("meshcore: unexpected %02x response (want %02x)", response, waiting.expect) + waiting.Error(fmt.Errorf("unexpected %02x response", response)) + } + default: + Logger.Warnf("meshcore: unhandled %02x response", response) + } + } + } +} diff --git a/protocol/meshcore/node_repeater.go b/protocol/meshcore/node_repeater.go new file mode 100644 index 0000000..164f1b1 --- /dev/null +++ b/protocol/meshcore/node_repeater.go @@ -0,0 +1,420 @@ +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! + } + + 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 ( + snr float64 + rssi int8 + data []byte + ) + if drv.hasSNR { + snr = float64(b[0]) / 4 + rssi = int8(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[3:]) + case strings.HasPrefix(part, "RSSI="): + rssi, _ = strconv.Atoi(part[4:]) + } + } + + if drv.rawPackets != nil { + select { + case drv.rawPackets <- &protocol.Packet{ + Time: drv.lastFrameAt, + Protocol: protocol.MeshCore, + SNR: float64(snr), + RSSI: int8(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 = int8(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 +}