btm can-net 透传协议
This commit is contained in:
parent
eb13545d55
commit
2be3990983
53
third_party/message/can_atp_req.go
vendored
53
third_party/message/can_atp_req.go
vendored
@ -30,36 +30,6 @@ type CanFrameId struct {
|
||||
ID4 byte
|
||||
}
|
||||
|
||||
func (f *CanFrameId) Encode(bits CanBitsWriter) {
|
||||
bits.AddByte(f.ID1)
|
||||
bits.AddByte(f.ID2)
|
||||
bits.AddByte(f.ID3)
|
||||
bits.AddBits(f.ID4, 5)
|
||||
}
|
||||
func (f *CanFrameId) Decode(bits CanBitsReader) bool {
|
||||
if id, ok := bits.ReadByte(); ok {
|
||||
f.ID1 = id
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
if id, ok := bits.ReadByte(); ok {
|
||||
f.ID2 = id
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
if id, ok := bits.ReadByte(); ok {
|
||||
f.ID3 = id
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
if id, ok := bits.ReadBits(5); ok {
|
||||
f.ID4 = id
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// NewCanFrameId 创建CAN串行总线帧ID
|
||||
// dstAddr 目的设备地址
|
||||
// srcAddr 源设备地址
|
||||
@ -102,10 +72,11 @@ func NewAtpRequestFrame(sn byte) *AtpRequestFrame {
|
||||
FId: *NewCanFrameId(CAN_ADDR_REQ_BTM, CAN_ADDR_REQ_ATP, CAN_FRAME_ATP_REQ, sn),
|
||||
}
|
||||
}
|
||||
func (f *AtpRequestFrame) Decode(bits CanBitsReader) bool {
|
||||
if !f.FId.Decode(bits) {
|
||||
return false
|
||||
}
|
||||
func (f *AtpRequestFrame) Decode(cf *CanetFrame) bool {
|
||||
f.FId = cf.CanId
|
||||
//
|
||||
bits := NewCanBitsReader(cf.CanData, 8)
|
||||
//
|
||||
if d, ok := bits.ReadBits(1); ok {
|
||||
f.PowerAmplifierTurnOn = d == 1
|
||||
} else {
|
||||
@ -188,10 +159,14 @@ func (f *AtpRequestFrame) Decode(bits CanBitsReader) bool {
|
||||
//
|
||||
return true
|
||||
}
|
||||
func (f *AtpRequestFrame) Encode() CanBusData {
|
||||
bits := NewCanBitsWriter(12)
|
||||
func (f *AtpRequestFrame) Encode() *CanetFrame {
|
||||
cf := &CanetFrame{}
|
||||
cf.CanId = f.FId
|
||||
cf.CanLen = 8
|
||||
cf.FF = true
|
||||
cf.RTR = false
|
||||
//
|
||||
f.FId.Encode(bits)
|
||||
bits := NewCanBitsWriter(8)
|
||||
//
|
||||
if f.PowerAmplifierTurnOn {
|
||||
bits.AddBits(1, 1)
|
||||
@ -223,5 +198,7 @@ func (f *AtpRequestFrame) Encode() CanBusData {
|
||||
bits.AddByte(byte(crc16 >> 8))
|
||||
bits.AddByte(byte(crc16))
|
||||
//
|
||||
return bits.(CanBusData)
|
||||
cf.CanData = bits.(CanBusData).GetData()
|
||||
//
|
||||
return cf
|
||||
}
|
||||
|
88
third_party/message/can_btm_data.go
vendored
88
third_party/message/can_btm_data.go
vendored
@ -16,20 +16,26 @@ func NewBtmDataMessageFrame(sn byte, offset byte) *BtmDataMessageFrame {
|
||||
FId: *NewCanFrameId(CAN_ADDR_RSP_ATP, CAN_ADDR_RSP_BTM, 0x80+offset, sn),
|
||||
}
|
||||
}
|
||||
func (f *BtmDataMessageFrame) Encode() CanBusData {
|
||||
writer := NewCanBitsWriter(12)
|
||||
func (f *BtmDataMessageFrame) Encode() *CanetFrame {
|
||||
cf := &CanetFrame{}
|
||||
cf.CanId = f.FId
|
||||
cf.CanLen = 8
|
||||
cf.FF = true
|
||||
cf.RTR = false
|
||||
//
|
||||
f.FId.Encode(writer)
|
||||
writer := NewCanBitsWriter(8)
|
||||
//
|
||||
for _, data := range f.Message {
|
||||
writer.AddByte(data)
|
||||
}
|
||||
return writer.(CanBusData)
|
||||
cf.CanData = writer.(CanBusData).GetData()
|
||||
//
|
||||
return cf
|
||||
}
|
||||
func (f *BtmDataMessageFrame) Decode(reader CanBitsReader) bool {
|
||||
if !f.FId.Decode(reader) {
|
||||
return false
|
||||
}
|
||||
func (f *BtmDataMessageFrame) Decode(cf *CanetFrame) bool {
|
||||
f.FId = cf.CanId
|
||||
//
|
||||
reader := NewCanBitsReader(cf.CanData, 8)
|
||||
//
|
||||
f.Message = make([]byte, 0, 8)
|
||||
for c := 0; c < 8; c++ {
|
||||
@ -61,10 +67,14 @@ func NewBtmDataMessageTimeAFrame(sn byte) *BtmDataMessageTimeAFrame {
|
||||
FId: *NewCanFrameId(CAN_ADDR_RSP_ATP, CAN_ADDR_RSP_BTM, 0x80+0x0d, sn),
|
||||
}
|
||||
}
|
||||
func (f *BtmDataMessageTimeAFrame) Encode() CanBusData {
|
||||
writer := NewCanBitsWriter(12)
|
||||
func (f *BtmDataMessageTimeAFrame) Encode() *CanetFrame {
|
||||
cf := &CanetFrame{}
|
||||
cf.CanId = f.FId
|
||||
cf.CanLen = 8
|
||||
cf.FF = true
|
||||
cf.RTR = false
|
||||
//
|
||||
f.FId.Encode(writer)
|
||||
writer := NewCanBitsWriter(8)
|
||||
//
|
||||
byteMsk := uint32(0x00_00_00_ff)
|
||||
//
|
||||
@ -78,13 +88,15 @@ func (f *BtmDataMessageTimeAFrame) Encode() CanBusData {
|
||||
writer.AddByte(byte((f.Crc32A >> 8) & byteMsk))
|
||||
writer.AddByte(byte(f.Crc32A & byteMsk))
|
||||
//
|
||||
return writer.(CanBusData)
|
||||
cf.CanData = writer.(CanBusData).GetData()
|
||||
//
|
||||
return cf
|
||||
}
|
||||
|
||||
func (f *BtmDataMessageTimeAFrame) Decode(reader CanBitsReader) bool {
|
||||
if !f.FId.Decode(reader) {
|
||||
return false
|
||||
}
|
||||
func (f *BtmDataMessageTimeAFrame) Decode(cf *CanetFrame) bool {
|
||||
f.FId = cf.CanId
|
||||
//
|
||||
reader := NewCanBitsReader(cf.CanData, 8)
|
||||
//
|
||||
f.TimeA = 0
|
||||
a1, a1Ok := reader.ReadByte()
|
||||
@ -157,10 +169,14 @@ func NewBtmDataMessageTimeBFrame(sn byte) *BtmDataMessageTimeBFrame {
|
||||
FId: *NewCanFrameId(CAN_ADDR_RSP_ATP, CAN_ADDR_RSP_BTM, 0x80+0x0e, sn),
|
||||
}
|
||||
}
|
||||
func (f *BtmDataMessageTimeBFrame) Encode() CanBusData {
|
||||
writer := NewCanBitsWriter(12)
|
||||
func (f *BtmDataMessageTimeBFrame) Encode() *CanetFrame {
|
||||
cf := &CanetFrame{}
|
||||
cf.CanId = f.FId
|
||||
cf.CanLen = 8
|
||||
cf.FF = true
|
||||
cf.RTR = false
|
||||
//
|
||||
f.FId.Encode(writer)
|
||||
writer := NewCanBitsWriter(8)
|
||||
//
|
||||
byteMsk := uint32(0x00_00_00_ff)
|
||||
//
|
||||
@ -174,12 +190,14 @@ func (f *BtmDataMessageTimeBFrame) Encode() CanBusData {
|
||||
writer.AddByte(byte((f.Crc32B >> 8) & byteMsk))
|
||||
writer.AddByte(byte(f.Crc32B & byteMsk))
|
||||
//
|
||||
return writer.(CanBusData)
|
||||
cf.CanData = writer.(CanBusData).GetData()
|
||||
//
|
||||
return cf
|
||||
}
|
||||
func (f *BtmDataMessageTimeBFrame) Decode(reader CanBitsReader) bool {
|
||||
if !f.FId.Decode(reader) {
|
||||
return false
|
||||
}
|
||||
func (f *BtmDataMessageTimeBFrame) Decode(cf *CanetFrame) bool {
|
||||
f.FId = cf.CanId
|
||||
//
|
||||
reader := NewCanBitsReader(cf.CanData, 8)
|
||||
//
|
||||
f.TimeB = 0
|
||||
a1, a1Ok := reader.ReadByte()
|
||||
@ -253,10 +271,14 @@ func NewBtmDataMessageEndFrame(sn byte) *BtmDataMessageEndFrame {
|
||||
FId: *NewCanFrameId(CAN_ADDR_RSP_ATP, CAN_ADDR_RSP_BTM, 0x80+0x7f, sn),
|
||||
}
|
||||
}
|
||||
func (f *BtmDataMessageEndFrame) Encode() CanBusData {
|
||||
writer := NewCanBitsWriter(12)
|
||||
func (f *BtmDataMessageEndFrame) Encode() *CanetFrame {
|
||||
cf := &CanetFrame{}
|
||||
cf.CanId = f.FId
|
||||
cf.CanLen = 8
|
||||
cf.FF = true
|
||||
cf.RTR = false
|
||||
//
|
||||
f.FId.Encode(writer)
|
||||
writer := NewCanBitsWriter(8)
|
||||
//
|
||||
byteMsk := uint32(0x00_00_00_ff)
|
||||
//
|
||||
@ -270,12 +292,14 @@ func (f *BtmDataMessageEndFrame) Encode() CanBusData {
|
||||
writer.AddByte(byte((f.Crc32C >> 8) & byteMsk))
|
||||
writer.AddByte(byte(f.Crc32C & byteMsk))
|
||||
//
|
||||
return writer.(CanBusData)
|
||||
cf.CanData = writer.(CanBusData).GetData()
|
||||
//
|
||||
return cf
|
||||
}
|
||||
func (f *BtmDataMessageEndFrame) Decode(reader CanBitsReader) bool {
|
||||
if !f.FId.Decode(reader) {
|
||||
return false
|
||||
}
|
||||
func (f *BtmDataMessageEndFrame) Decode(cf *CanetFrame) bool {
|
||||
f.FId = cf.CanId
|
||||
//
|
||||
reader := NewCanBitsReader(cf.CanData, 8)
|
||||
//
|
||||
f.TkB = 0
|
||||
a1, a1Ok := reader.ReadByte()
|
||||
|
82
third_party/message/can_btm_rsp.go
vendored
82
third_party/message/can_btm_rsp.go
vendored
@ -61,60 +61,38 @@ func CreateBtmRspFramesData(statusRsp *BtmStatusRspFrame, msg []byte, msgPackErr
|
||||
end := NewBtmDataMessageEndFrame(sn)
|
||||
end.TkB = tkTimeB
|
||||
//
|
||||
crc32CData := make([]byte, 0, 132) //17*8-4
|
||||
rt := make([]byte, 0, 204) //17*12
|
||||
// 状态应答帧
|
||||
statusData := statusRsp.Encode()
|
||||
crc32CData = append(crc32CData, canFrameContent(statusData)...)
|
||||
rt = append(rt, statusData.GetData()...)
|
||||
// 数据帧
|
||||
statusCf := statusRsp.Encode()
|
||||
dmsCfs := make([]*CanetFrame, 0, 13)
|
||||
for _, dm := range dms {
|
||||
dmData := dm.Encode()
|
||||
crc32CData = append(crc32CData, canFrameContent(dmData)...)
|
||||
rt = append(rt, dmData.GetData()...)
|
||||
}
|
||||
// 消息时刻A
|
||||
dtAData := dtA.Encode()
|
||||
crc32CData = append(crc32CData, canFrameContent(dtAData)...)
|
||||
rt = append(rt, dtAData.GetData()...)
|
||||
// 消息时刻B
|
||||
dtBData := dtB.Encode()
|
||||
crc32CData = append(crc32CData, canFrameContent(dtBData)...)
|
||||
rt = append(rt, dtBData.GetData()...)
|
||||
// 消息结束帧
|
||||
crc32CData = append(crc32CData, canTimeToBytes(end.TkB)...)
|
||||
if len(crc32CData) != 132 {
|
||||
slog.Warn("BtmDataMessageEndFrame crc32 校验数据须132字节")
|
||||
return nil, false
|
||||
}
|
||||
end.Crc32C = calculateDataRspCrc32C(crc32CData)
|
||||
rt = append(rt, end.Encode().GetData()...)
|
||||
if len(rt) != 204 {
|
||||
slog.Warn("BTM响应ATP 17帧须136字节")
|
||||
return nil, false
|
||||
dmsCfs = append(dmsCfs, dm.Encode())
|
||||
}
|
||||
dtACf := dtA.Encode()
|
||||
dtBCf := dtB.Encode()
|
||||
//
|
||||
crc32cData := make([]byte, 0, 132)
|
||||
crc32cData = append(crc32cData, statusCf.CanData...)
|
||||
for _, dmCf := range dmsCfs {
|
||||
crc32cData = append(crc32cData, dmCf.CanData...)
|
||||
}
|
||||
crc32cData = append(crc32cData, dtACf.CanData...)
|
||||
crc32cData = append(crc32cData, dtBCf.CanData...)
|
||||
crc32cData = append(crc32cData, canTimeToBytes(end.TkB)...)
|
||||
//
|
||||
end.Crc32C = calculateDataRspCrc32C(crc32cData)
|
||||
//
|
||||
endCf := end.Encode()
|
||||
//
|
||||
rt := make([]byte, 0, 221) //17*13
|
||||
rt = append(rt, statusCf.Encode()...)
|
||||
for _, dmCf := range dmsCfs {
|
||||
rt = append(rt, dmCf.Encode()...)
|
||||
}
|
||||
rt = append(rt, dtACf.Encode()...)
|
||||
rt = append(rt, dtBCf.Encode()...)
|
||||
rt = append(rt, endCf.Encode()...)
|
||||
if len(rt) != 221 {
|
||||
slog.Warn("len(rt)!=221")
|
||||
return nil, false
|
||||
}
|
||||
return rt, true
|
||||
}
|
||||
|
||||
// CAN帧中除去开始29bits的剩余8字节数据
|
||||
func canFrameContent(canFrame CanBusData) []byte {
|
||||
rd := NewCanBitsReader(canFrame.GetData(), canFrame.GetLastRowBitsLen())
|
||||
rd.ReadByte() //ID1
|
||||
rd.ReadByte() //ID2
|
||||
rd.ReadByte() //ID3
|
||||
rd.ReadBits(5) //ID4
|
||||
//
|
||||
rt := make([]byte, 0, 8)
|
||||
for {
|
||||
d, ok := rd.ReadByte()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
rt = append(rt, d)
|
||||
}
|
||||
if len(rt) != 8 {
|
||||
panic("CAN帧中数据体须为8字节")
|
||||
}
|
||||
return rt
|
||||
}
|
||||
|
21
third_party/message/can_btm_status_rsp.go
vendored
21
third_party/message/can_btm_status_rsp.go
vendored
@ -29,10 +29,10 @@ func NewBtmStatusRspFrame(sn byte) *BtmStatusRspFrame {
|
||||
FId: *NewCanFrameId(CAN_ADDR_RSP_ATP, CAN_ADDR_RSP_BTM, CAN_FRAME_STATUS_RSP, sn),
|
||||
}
|
||||
}
|
||||
func (f *BtmStatusRspFrame) Decode(reader CanBitsReader) bool {
|
||||
if !f.FId.Decode(reader) {
|
||||
return false
|
||||
}
|
||||
func (f *BtmStatusRspFrame) Decode(cf *CanetFrame) bool {
|
||||
f.FId = cf.CanId
|
||||
//
|
||||
reader := NewCanBitsReader(cf.CanData, 8)
|
||||
//数据流水号
|
||||
if d, ok := reader.ReadByte(); ok {
|
||||
f.Dsn = d
|
||||
@ -106,10 +106,14 @@ func (f *BtmStatusRspFrame) Decode(reader CanBitsReader) bool {
|
||||
//
|
||||
return true
|
||||
}
|
||||
func (f *BtmStatusRspFrame) Encode() CanBusData {
|
||||
writer := NewCanBitsWriter(12)
|
||||
func (f *BtmStatusRspFrame) Encode() *CanetFrame {
|
||||
cf := &CanetFrame{}
|
||||
cf.CanId = f.FId
|
||||
cf.CanLen = 8
|
||||
cf.FF = true
|
||||
cf.RTR = false
|
||||
//
|
||||
f.FId.Encode(writer)
|
||||
writer := NewCanBitsWriter(8)
|
||||
writer.AddByte(f.Dsn)
|
||||
writer.AddByte(f.BaliseCounter)
|
||||
writer.AddByte(f.MessageCounter)
|
||||
@ -150,5 +154,6 @@ func (f *BtmStatusRspFrame) Encode() CanBusData {
|
||||
writer.AddByte(tk3)
|
||||
writer.AddByte(tk4)
|
||||
//
|
||||
return writer.(CanBusData)
|
||||
cf.CanData = writer.(CanBusData).GetData()
|
||||
return cf
|
||||
}
|
||||
|
21
third_party/message/can_btm_time_sync_rsp.go
vendored
21
third_party/message/can_btm_time_sync_rsp.go
vendored
@ -17,11 +17,10 @@ func NewBtmTimeSyncCheckFrame(sn byte) *BtmTimeSyncCheckFrame {
|
||||
FId: *NewCanFrameId(CAN_ADDR_RSP_ATP, CAN_ADDR_RSP_BTM, CAN_FRAME_TIME_SYNC_RSP, sn),
|
||||
}
|
||||
}
|
||||
func (f *BtmTimeSyncCheckFrame) Decode(reader CanBitsReader) bool {
|
||||
func (f *BtmTimeSyncCheckFrame) Decode(cf *CanetFrame) bool {
|
||||
f.FId = cf.CanId
|
||||
//
|
||||
if !f.FId.Decode(reader) {
|
||||
return false
|
||||
}
|
||||
reader := NewCanBitsReader(cf.CanData, 8)
|
||||
//T2
|
||||
t21, t21Ok := reader.ReadByte()
|
||||
if !t21Ok {
|
||||
@ -69,9 +68,14 @@ func (f *BtmTimeSyncCheckFrame) Decode(reader CanBitsReader) bool {
|
||||
//
|
||||
return true
|
||||
}
|
||||
func (f *BtmTimeSyncCheckFrame) Encode() CanBusData {
|
||||
writer := NewCanBitsWriter(12)
|
||||
f.FId.Encode(writer)
|
||||
func (f *BtmTimeSyncCheckFrame) Encode() *CanetFrame {
|
||||
cf := &CanetFrame{}
|
||||
cf.CanId = f.FId
|
||||
cf.CanLen = 8
|
||||
cf.FF = true
|
||||
cf.RTR = false
|
||||
//
|
||||
writer := NewCanBitsWriter(8)
|
||||
//
|
||||
timeByteMsk := uint32(0x00_00_00_ff)
|
||||
//t2
|
||||
@ -93,5 +97,6 @@ func (f *BtmTimeSyncCheckFrame) Encode() CanBusData {
|
||||
writer.AddByte(t33)
|
||||
writer.AddByte(t34)
|
||||
//
|
||||
return writer.(CanBusData)
|
||||
cf.CanData = writer.(CanBusData).GetData()
|
||||
return cf
|
||||
}
|
||||
|
4
third_party/message/can_bus.go
vendored
4
third_party/message/can_bus.go
vendored
@ -4,8 +4,6 @@ package message
|
||||
|
||||
// CanBits 可以在CAN总线上传输的bit流
|
||||
// 按bit位来存储数据
|
||||
// 一个字节中bit位编号,从高位到低位依次为bit7-bit0
|
||||
// 对于一个CAN帧:29bits帧ID+64bits帧数据,以bit为单位,最后一字节即第12个字节的低位3bits为无用数据
|
||||
type CanBits struct {
|
||||
Data []byte //bits
|
||||
LastBitsLen int //Data中最后一个字节中从高位到低位存储数据的有效位数,值范围[1,8],8-LastBitsLen则为最后一个字节中低位剩余的空位数量
|
||||
@ -211,7 +209,7 @@ var (
|
||||
crc32CanTable []uint32 = nil
|
||||
)
|
||||
|
||||
func CanCreateCrcTable() {
|
||||
func CreateCanCrcTable() {
|
||||
if crc16AtpReqTable == nil {
|
||||
crc16AtpReqTable = CreateCrcTable(CAN_CRC16_ATPREQ, 16, false)
|
||||
}
|
||||
|
71
third_party/message/can_net.go
vendored
Normal file
71
third_party/message/can_net.go
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CanetFrame USR-CANET200_V1.0.7 设备 以太网-CAN 透传协议帧(13字节)
|
||||
type CanetFrame struct {
|
||||
FF bool //true-1扩展帧
|
||||
RTR bool //true-1远程帧
|
||||
CanLen byte //CAN帧数据长度[0,8],CanData中有效数据字节数
|
||||
CanId CanFrameId //CAN帧ID
|
||||
CanData []byte //CAN帧数据,8字节
|
||||
}
|
||||
|
||||
func NewCanetFrame(buf []byte) *CanetFrame {
|
||||
cf := &CanetFrame{}
|
||||
cf.Decode(buf)
|
||||
return cf
|
||||
}
|
||||
func (p *CanetFrame) Encode() []byte {
|
||||
buf := make([]byte, 0, 13)
|
||||
//canet200帧信息
|
||||
b1 := byte(0x00)
|
||||
if p.FF {
|
||||
b1 |= 0x80 //bit7
|
||||
}
|
||||
if p.RTR {
|
||||
b1 |= 0x40 //bit6
|
||||
}
|
||||
b1 |= p.CanLen & 0x0f
|
||||
buf = append(buf, b1)
|
||||
//CAN 帧ID
|
||||
buf = append(buf, p.CanId.ID1)
|
||||
buf = append(buf, p.CanId.ID2)
|
||||
buf = append(buf, p.CanId.ID3)
|
||||
buf = append(buf, p.CanId.ID4<<3)
|
||||
//CAN 帧数据
|
||||
if len(p.CanData) != 8 {
|
||||
panic("len(p.CanData)!=8")
|
||||
}
|
||||
buf = append(buf, p.CanData...)
|
||||
return buf
|
||||
}
|
||||
func (p *CanetFrame) Decode(buf []byte) {
|
||||
if len(buf) != 13 {
|
||||
panic("len(buf)!=13")
|
||||
}
|
||||
//
|
||||
p.FF = buf[0]&0x80 == 0x80
|
||||
p.RTR = buf[0]&0x40 == 0x40
|
||||
p.CanLen = buf[0] & 0x0f
|
||||
//1 2 3 4
|
||||
p.CanId.ID1 = buf[1]
|
||||
p.CanId.ID2 = buf[2]
|
||||
p.CanId.ID3 = buf[3]
|
||||
p.CanId.ID4 = buf[4] >> 3
|
||||
//
|
||||
p.CanData = buf[5:]
|
||||
}
|
||||
func (p *CanetFrame) String() string {
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString(fmt.Sprintf("FF = %t, RTR = %t, CanLen = %d, ID1 = 0x%0x, ID2 = 0x%0x, ID3 = 0x%0x, ID4 = 0x%0x,", p.FF, p.RTR, p.CanLen,
|
||||
p.CanId.ID1, p.CanId.ID2, p.CanId.ID3, p.CanId.ID4))
|
||||
sb.WriteString("CanData = ")
|
||||
for _, d := range p.CanData {
|
||||
sb.WriteString(fmt.Sprintf(" 0x%0x ", d))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
Loading…
Reference in New Issue
Block a user