add support for concatenated text messages with UDH

master v1.1.0
Florian Thienel 2 years ago
parent 2a2635ccf1
commit 5c7df72d1c

@ -2,7 +2,7 @@
The tetra-pei library allows to communicate with a TETRA radio terminal through its peripheral equipment interface (PEI), with the main focus on simple radio control and the handling of SDS messages. This implementation is solely based on the relevant ETSI specifications: The tetra-pei library allows to communicate with a TETRA radio terminal through its peripheral equipment interface (PEI), with the main focus on simple radio control and the handling of SDS messages. This implementation is solely based on the relevant ETSI specifications:
* ETSI TS 100 392-2 V2.9.2 (2020-06): Terrestrial Trunked Radio (TETRA); Voice plus Data (V+D); Part 2: Air Interface (AI) * ETSI TS 100 392-2 V3.9.2 (2020-06): Terrestrial Trunked Radio (TETRA); Voice plus Data (V+D); Part 2: Air Interface (AI)
* ETSI EN 300 392-5 V2.7.1 (2020-04): Terrestrial Trunked Radio (TETRA); Voice plus Data (V+D) and Direct Mode Operation (DMO); Part 5: Peripheral Equipment Interface (PEI) * ETSI EN 300 392-5 V2.7.1 (2020-04): Terrestrial Trunked Radio (TETRA); Voice plus Data (V+D) and Direct Mode Operation (DMO); Part 5: Peripheral Equipment Interface (PEI)
## Restrictions ## Restrictions

@ -122,6 +122,11 @@ func (p ProtocolIdentifier) Encode(bytes []byte, bits int) ([]byte, int) {
return append(bytes, byte(p)), bits + 8 return append(bytes, byte(p)), bits + 8
} }
// Length of this protocol identifier in bytes.
func (p ProtocolIdentifier) Length() int {
return 1
}
// All protocol identifiers relevant for SDS handling, according to [AI] table 29.21 // All protocol identifiers relevant for SDS handling, according to [AI] table 29.21
const ( const (
SimpleTextMessaging ProtocolIdentifier = 0x02 SimpleTextMessaging ProtocolIdentifier = 0x02
@ -381,6 +386,72 @@ func NewTextMessageTransfer(messageReference MessageReference, immediate bool, d
} }
} }
// NewConcatenatedMessageTransfer returns a set of SDS_TRANSFER PDUs for that make up the given text using concatenated text messages with a UDH.
func NewConcatenatedMessageTransfer(messageReference MessageReference, deliveryReport DeliveryReportRequest, encoding TextEncoding, maxPDUBits int, text string) []SDSTransfer {
blueprint := SDSTransfer{
protocol: UserDataHeaderMessaging,
MessageReference: messageReference,
DeliveryReportRequest: deliveryReport,
UserData: ConcatenatedTextSDU{
TextSDU: TextSDU{
TextHeader: TextHeader{
Encoding: encoding,
},
Text: "",
},
UserDataHeader: ConcatenatedTextUDH{
ElementID: ConcatenatedTextMessageWithShortReference,
MessageReference: uint16(messageReference),
TotalNumber: 0,
SequenceNumber: 0,
},
},
}
blueprintBits := blueprint.Length() * 8
textParts := SplitToMaxBits(encoding, maxPDUBits-blueprintBits, text)
if len(textParts) == 1 {
return []SDSTransfer{{
protocol: TextMessaging,
MessageReference: messageReference,
DeliveryReportRequest: deliveryReport,
UserData: TextSDU{
TextHeader: TextHeader{
Encoding: encoding,
},
Text: text,
},
}}
}
result := make([]SDSTransfer, len(textParts))
for i, textPart := range textParts {
result[i] = SDSTransfer{
protocol: UserDataHeaderMessaging,
ServiceSelectionShortFormReport: true,
MessageReference: messageReference + MessageReference(i),
DeliveryReportRequest: deliveryReport,
UserData: ConcatenatedTextSDU{
TextSDU: TextSDU{
TextHeader: TextHeader{
Encoding: encoding,
},
Text: textPart,
},
UserDataHeader: ConcatenatedTextUDH{
ElementID: ConcatenatedTextMessageWithShortReference,
MessageReference: uint16(messageReference),
TotalNumber: byte(len(textParts)),
SequenceNumber: byte(i + 1),
},
},
}
}
return result
}
// SDSTransfer represents the SDS-TRANSFER PDU contents as defined in [AI] 29.4.2.4 // SDSTransfer represents the SDS-TRANSFER PDU contents as defined in [AI] 29.4.2.4
type SDSTransfer struct { type SDSTransfer struct {
protocol ProtocolIdentifier protocol ProtocolIdentifier
@ -410,12 +481,27 @@ func (m SDSTransfer) Encode(bytes []byte, bits int) ([]byte, int) {
case TextSDU: case TextSDU:
bytes, bits = sdu.Encode(bytes, bits) bytes, bits = sdu.Encode(bytes, bits)
case ConcatenatedTextSDU: case ConcatenatedTextSDU:
// bytes, bits = sdu.Encode(bytes, bits) bytes, bits = sdu.Encode(bytes, bits)
} }
return bytes, bits return bytes, bits
} }
// Length of this SDS-TRANSFER in bytes.
func (m SDSTransfer) Length() int {
var result int
result += m.protocol.Length()
result++ // byte1
result++ // message reference
switch sdu := m.UserData.(type) {
case TextSDU:
result += sdu.Length()
case ConcatenatedTextSDU:
result += sdu.Length()
}
return result
}
// ReceivedReportRequested indicates if for this SDS-TRANSFER PDU a delivery report is requested for receipt // ReceivedReportRequested indicates if for this SDS-TRANSFER PDU a delivery report is requested for receipt
func (m SDSTransfer) ReceivedReportRequested() bool { func (m SDSTransfer) ReceivedReportRequested() bool {
return m.DeliveryReportRequest == MessageReceivedReportRequested || return m.DeliveryReportRequest == MessageReceivedReportRequested ||
@ -857,7 +943,7 @@ func ParseConcatenatedTextSDU(bytes []byte) (ConcatenatedTextSDU, error) {
00: Message Type[4], Delivery Report Request[2], Service Selection/Short form report[1], Store/forward control[1] 00: Message Type[4], Delivery Report Request[2], Service Selection/Short form report[1], Store/forward control[1]
C9: Message Reference[8] (0xC9) <-- This one is incremented for each part of the concatenated message C9: Message Reference[8] (0xC9) <-- This one is incremented for each part of the concatenated message
<no store forward control information> <no store forward control information>
8D: Timestamp Used[1] (yes), Text Encoding Scheme[7] (ISO8859-15/Latin 0 ???) 8D: Timestamp Used[1] (yes), Text Encoding Scheme[7] (ISO8859-15/Latin 0)
04 5A 8F: Timestamp[24] 04 5A 8F: Timestamp[24]
05: User Data Header length[8] (5) 05: User Data Header length[8] (5)
00: UDH Information Element ID[8] (0) 00: UDH Information Element ID[8] (0)
@ -901,6 +987,15 @@ type ConcatenatedTextSDU struct {
UserDataHeader ConcatenatedTextUDH UserDataHeader ConcatenatedTextUDH
} }
// Encode this concatenated text SDU
func (t ConcatenatedTextSDU) Encode(bytes []byte, bits int) ([]byte, int) {
bytes, bits = t.TextHeader.Encode(bytes, bits)
bytes, bits = t.UserDataHeader.Encode(bytes, bits)
bytes, bits = AppendEncodedPayloadText(bytes, bits, t.Text, t.TextHeader.Encoding)
return bytes, bits
}
// Length returns the length of this encoded concatenated text SDU in bytes. // Length returns the length of this encoded concatenated text SDU in bytes.
func (t ConcatenatedTextSDU) Length() int { func (t ConcatenatedTextSDU) Length() int {
return t.TextSDU.Length() + t.UserDataHeader.Length() return t.TextSDU.Length() + t.UserDataHeader.Length()
@ -949,9 +1044,45 @@ type ConcatenatedTextUDH struct {
SequenceNumber byte SequenceNumber byte
} }
// Encode this concatenated text UDH
func (h ConcatenatedTextUDH) Encode(bytes []byte, bits int) ([]byte, int) {
headerLengthIndex := len(bytes)
bytes = append(bytes, 0)
bits += 8
bytes = append(bytes, byte(h.ElementID))
bits += 8
elementLengthIndex := len(bytes)
bytes = append(bytes, 0)
bits += 8
bytes = append(bytes, byte(h.MessageReference))
bits += 8
if h.ElementID == ConcatenatedTextMessageWithLongReference {
bytes = append(bytes, byte(h.MessageReference>>8))
bits += 8
}
bytes = append(bytes, h.TotalNumber)
bits += 8
bytes = append(bytes, h.SequenceNumber)
bits += 8
bytes[headerLengthIndex] = byte(len(bytes) - headerLengthIndex - 1)
bytes[elementLengthIndex] = byte(len(bytes) - elementLengthIndex - 1)
return bytes, bits
}
// Length returns the length of this header in bytes. // Length returns the length of this header in bytes.
func (h ConcatenatedTextUDH) Length() int { func (h ConcatenatedTextUDH) Length() int {
return int(h.HeaderLength) + 1 // the HeaderLength byte itself result := 6
if h.ElementID == ConcatenatedTextMessageWithLongReference {
result++
}
return result
} }
// UDHInformationElementID enum according to [AI] 29.5.9.4.1 // UDHInformationElementID enum according to [AI] 29.5.9.4.1

@ -9,6 +9,7 @@ import (
) )
func TestParseMessage(t *testing.T) { func TestParseMessage(t *testing.T) {
expectedTimestamp := time.Date(time.Now().Year(), time.April, 11, 10, 15, 0, 0, time.Local)
tt := []struct { tt := []struct {
desc string desc string
header string header string
@ -143,7 +144,7 @@ func TestParseMessage(t *testing.T) {
UserData: TextSDU{ UserData: TextSDU{
TextHeader: TextHeader{ TextHeader: TextHeader{
Encoding: ISO8859_1, Encoding: ISO8859_1,
Timestamp: time.Date(2021, time.April, 11, 10, 15, 0, 0, time.Local), Timestamp: expectedTimestamp,
}, },
Text: "testmessage", Text: "testmessage",
}, },
@ -163,7 +164,7 @@ func TestParseMessage(t *testing.T) {
TextSDU: TextSDU{ TextSDU: TextSDU{
TextHeader: TextHeader{ TextHeader: TextHeader{
Encoding: ISO8859_1, Encoding: ISO8859_1,
Timestamp: time.Date(2021, time.April, 11, 10, 15, 0, 0, time.Local), Timestamp: expectedTimestamp,
}, },
Text: "testmessage", Text: "testmessage",
}, },
@ -192,7 +193,7 @@ func TestParseMessage(t *testing.T) {
TextSDU: TextSDU{ TextSDU: TextSDU{
TextHeader: TextHeader{ TextHeader: TextHeader{
Encoding: ISO8859_1, Encoding: ISO8859_1,
Timestamp: time.Date(2021, time.April, 11, 10, 15, 0, 0, time.Local), Timestamp: expectedTimestamp,
}, },
Text: "testmessage", Text: "testmessage",
}, },
@ -446,6 +447,7 @@ func TestParseHeader(t *testing.T) {
} }
func TestEncode(t *testing.T) { func TestEncode(t *testing.T) {
expectedTimestamp := time.Date(time.Now().Year(), time.April, 11, 8, 15, 0, 0, time.UTC)
tt := []struct { tt := []struct {
desc string desc string
values []Encoder values []Encoder
@ -484,13 +486,13 @@ func TestEncode(t *testing.T) {
UserData: TextSDU{ UserData: TextSDU{
TextHeader: TextHeader{ TextHeader: TextHeader{
Encoding: ISO8859_1, Encoding: ISO8859_1,
Timestamp: time.Date(2021, time.April, 11, 10, 15, 0, 0, time.UTC), Timestamp: expectedTimestamp,
}, },
Text: "testmessage", Text: "testmessage",
}, },
}, },
}, },
expectedBytes: []byte{0x82, 0x06, 0xC9, 0x81, 0x44, 0x5A, 0x8F, 0x74, 0x65, 0x73, 0x74, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65}, expectedBytes: []byte{0x82, 0x06, 0xC9, 0x81, 0x44, 0x5A, 0x0F, 0x74, 0x65, 0x73, 0x74, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65},
expectedBits: 144, expectedBits: 144,
}, },
{ {
@ -505,6 +507,34 @@ func TestEncode(t *testing.T) {
expectedBytes: []byte{0x02, 0x01, 0x74, 0x65, 0x73, 0x74, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65}, expectedBytes: []byte{0x02, 0x01, 0x74, 0x65, 0x73, 0x74, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65},
expectedBits: 104, expectedBits: 104,
}, },
{
desc: "SDS-TRANSFER concatenated text message with UDH",
values: []Encoder{
SDSTransfer{
protocol: UserDataHeaderMessaging,
MessageReference: 0xC9,
UserData: ConcatenatedTextSDU{
TextSDU: TextSDU{
TextHeader: TextHeader{
Encoding: ISO8859_1,
Timestamp: expectedTimestamp,
},
Text: "testmessage",
},
UserDataHeader: ConcatenatedTextUDH{
HeaderLength: 5,
ElementID: 0,
ElementLength: 3,
MessageReference: 0xC9,
TotalNumber: 2,
SequenceNumber: 1,
},
},
},
},
expectedBytes: []byte{0x8A, 0x02, 0xC9, 0x81, 0x44, 0x5A, 0x0F, 0x05, 0x00, 0x03, 0xC9, 0x02, 0x01, 0x74, 0x65, 0x73, 0x74, 0x6D, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65},
expectedBits: 192,
},
} }
for _, tc := range tt { for _, tc := range tt {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {

@ -124,6 +124,40 @@ func TextBytesToBits(encoding TextEncoding, length int) int {
} }
} }
// BitsToTextBytes returns the number of bytes of a text that fit into the given number of bits using the given encoding
func BitsToTextBytes(encoding TextEncoding, bits int) int {
switch encoding {
case Packed7Bit:
return bits / 7
default:
return bits / 8
}
}
// SplitToMaxBits splits the given text into parts that do not exceed the given maximum number of bits using the given encoding
func SplitToMaxBits(encoding TextEncoding, maxPDUBits int, text string) []string {
if text == "" {
return []string{}
}
maxPartLength := BitsToTextBytes(encoding, maxPDUBits)
maxPartsCount := len(text)/maxPartLength + 1
result := make([]string, 0, maxPartsCount)
remainingText := text
for len(remainingText) > maxPartLength {
part := remainingText[0:maxPartLength]
remainingText = remainingText[maxPartLength:]
if part != "" {
result = append(result, part)
}
}
if len(remainingText) > 0 {
result = append(result, remainingText)
}
return result
}
// ParseTextHeader in text messages and concatenated text messages. // ParseTextHeader in text messages and concatenated text messages.
func ParseTextHeader(bytes []byte) (TextHeader, error) { func ParseTextHeader(bytes []byte) (TextHeader, error) {
if len(bytes) < 1 { if len(bytes) < 1 {

@ -6,6 +6,116 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestBitsToTextBytes(t *testing.T) {
tt := []struct {
desc string
encoding TextEncoding
bits int
expectedBytes int
}{
{
desc: "7bit, 0",
encoding: Packed7Bit,
bits: 0,
expectedBytes: 0,
},
{
desc: "8bit, 0",
encoding: ISO8859_1,
bits: 0,
expectedBytes: 0,
},
{
desc: "7bit, 1",
encoding: Packed7Bit,
bits: 1,
expectedBytes: 0,
},
{
desc: "8bit, 1",
encoding: ISO8859_1,
bits: 1,
expectedBytes: 0,
},
{
desc: "7bit, 8",
encoding: Packed7Bit,
bits: 8,
expectedBytes: 1,
},
{
desc: "7bit, 14",
encoding: Packed7Bit,
bits: 14,
expectedBytes: 2,
},
{
desc: "8bit, 14",
encoding: ISO8859_1,
bits: 14,
expectedBytes: 1,
},
{
desc: "7bit, 56",
encoding: Packed7Bit,
bits: 56,
expectedBytes: 8,
},
{
desc: "8bit, 56",
encoding: ISO8859_1,
bits: 56,
expectedBytes: 7,
},
}
for _, tc := range tt {
t.Run(tc.desc, func(t *testing.T) {
actualBytes := BitsToTextBytes(tc.encoding, tc.bits)
assert.Equal(t, tc.expectedBytes, actualBytes)
})
}
}
func TestSplitToMaxBits(t *testing.T) {
tt := []struct {
encoding TextEncoding
maxPDUBits int
text string
expectedParts []string
}{
{
encoding: Packed7Bit,
maxPDUBits: 56,
text: "7-bit, 056",
expectedParts: []string{"7-bit, 0", "56"},
},
{
encoding: ISO8859_1,
maxPDUBits: 56,
text: "8-bit, 056",
expectedParts: []string{"8-bit, ", "056"},
},
{
encoding: Packed7Bit,
maxPDUBits: 128,
text: "7-bit, 128",
expectedParts: []string{"7-bit, 128"},
},
{
encoding: ISO8859_1,
maxPDUBits: 128,
text: "8-bit, 128",
expectedParts: []string{"8-bit, 128"},
},
}
for _, tc := range tt {
t.Run(tc.text, func(t *testing.T) {
actualParts := SplitToMaxBits(tc.encoding, tc.maxPDUBits, tc.text)
assert.Equal(t, tc.expectedParts, actualParts)
})
}
}
func TestSplitLeadingOPTA(t *testing.T) { func TestSplitLeadingOPTA(t *testing.T) {
tt := []struct { tt := []struct {
desc string desc string

Loading…
Cancel
Save