You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tetra-pei/sds/sds.go

1198 lines
36 KiB
Go

package sds
import (
"fmt"
"log"
"strconv"
"strings"
"time"
"github.com/ftl/tetra-pei/tetra"
)
// ParseIncomingMessage parses an incoming message with the given header and PDU bytes. The message may
// be part of a concatenated text message with user data header, a simple text message, a text message,
// or a status.
func ParseIncomingMessage(headerString string, pduHex string) (IncomingMessage, error) {
header, err := ParseHeader(headerString)
if err != nil {
return IncomingMessage{}, err
}
pduBytes, err := tetra.HexToBinary(pduHex)
if err != nil {
return IncomingMessage{}, fmt.Errorf("cannot decode hex PDU data: %w", err)
}
if len(pduBytes) != header.PDUBytes() {
log.Printf("got different count of pdu bytes, expected %d, but got %d", len(pduBytes), header.PDUBytes())
}
if len(pduBytes) > header.PDUBytes() {
pduBytes = pduBytes[0:header.PDUBytes()]
}
var result IncomingMessage
result.Header = header
switch header.AIService {
case SDSTLService:
result.Payload, err = ParseSDSTLPDU(pduBytes)
case StatusService:
result.Payload, err = ParseStatus(pduBytes)
default:
return IncomingMessage{}, fmt.Errorf("AI service %s is not supported", header.AIService)
}
if err != nil {
return IncomingMessage{}, err
}
return result, nil
}
type IncomingMessage struct {
Header Header
Payload interface{}
}
// ParseHeader from the given string. The string must include the +CTSDSR: token.
func ParseHeader(s string) (Header, error) {
if !strings.HasPrefix(s, "+CTSDSR:") {
return Header{}, fmt.Errorf("invalid header, +CTSDSR expected: %s", s)
}
var result Header
headerFields := strings.Split(s[8:], ",")
switch len(headerFields) {
case 3, 4: // minimum set
result.AIService = AIService(strings.TrimSpace(headerFields[0]))
result.Destination = tetra.Identity(strings.TrimSpace(headerFields[1]))
case 6, 7: // with source, with end-to-end encryption
result.AIService = AIService(strings.TrimSpace(headerFields[0]))
result.Source = tetra.Identity(strings.TrimSpace(headerFields[1]))
result.Destination = tetra.Identity(strings.TrimSpace(headerFields[3]))
default:
return Header{}, fmt.Errorf("invalid header, wrong field count: %s", s)
}
pduBitCountField := headerFields[len(headerFields)-1]
var err error
result.PDUBits, err = strconv.Atoi(strings.TrimSpace(pduBitCountField))
if err != nil {
return Header{}, fmt.Errorf("invalid PDU bit count %s: %v", pduBitCountField, err)
}
return result, nil
}
// Header represents the information provided with the AT+CTSDSR unsolicited response indicating an incoming SDS.
// see [PEI] 6.13.3
type Header struct {
AIService AIService
Source tetra.Identity
Destination tetra.Identity
PDUBits int
}
// PDUBytes returns the size of the following PDU in bytes.
func (h Header) PDUBytes() int {
result := h.PDUBits / 8
if (h.PDUBits % 8) != 0 {
result++
}
return result
}
// AIService enum according to [PEI] 6.17.3
type AIService string
// All AI services relevant for SDS handling, according to [PEI] 6.17.3
const (
SDS1Service AIService = "9"
SDS2Service AIService = "10"
SDS3Service AIService = "11"
SDSTLService AIService = "12"
StatusService AIService = "13"
)
/* General types used in the PDU */
// ProtocolIdentifier enum according to [AI] 29.4.3.9
type ProtocolIdentifier byte
// Encode this protocol identifier
func (p ProtocolIdentifier) Encode(bytes []byte, bits int) ([]byte, int) {
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
const (
SimpleTextMessaging ProtocolIdentifier = 0x02
SimpleImmediateTextMessaging ProtocolIdentifier = 0x09
SimpleConcatenatedSDSMessaging ProtocolIdentifier = 0x0C
TextMessaging ProtocolIdentifier = 0x82
ImmediateTextMessaging ProtocolIdentifier = 0x89
UserDataHeaderMessaging ProtocolIdentifier = 0x8A
ConcatenatedSDSMessaging ProtocolIdentifier = 0x8C
)
/* SDS-TL related types and functions */
// ParseSDSTLPDU parses an SDS-TL PDU from the given bytes according to [AI] 29.4.1.
// This function currently supports only a subset of the possible protocol identifiers:
// Simple text messaging (0x02), simple immediate text messaging (0x09), text messaging (0x82),
// immediate text messaging (0x89), message with user data header (0x8A)
func ParseSDSTLPDU(bytes []byte) (interface{}, error) {
if len(bytes) == 0 {
return nil, fmt.Errorf("empty payload")
}
switch ProtocolIdentifier(bytes[0]) {
case SimpleTextMessaging, SimpleImmediateTextMessaging:
return ParseSimpleTextMessage(bytes)
case TextMessaging, ImmediateTextMessaging, UserDataHeaderMessaging:
return parseSDSTLMessage(bytes)
default:
return nil, fmt.Errorf("protocol 0x%x not supported", bytes[0])
}
}
func parseSDSTLMessage(bytes []byte) (interface{}, error) {
if len(bytes) < 2 {
return nil, fmt.Errorf("payload too short: %d", len(bytes))
}
messageType := SDSTLMessageType(bytes[1] >> 4)
switch messageType {
case SDSTransferMessage:
return ParseSDSTransfer(bytes)
case SDSReportMessage:
return ParseSDSReport(bytes)
case SDSAcknowledgeMessage:
return ParseSDSAcknowledge(bytes)
default:
return nil, fmt.Errorf("SDS-TL message type 0x%x is not supported", messageType)
}
}
// SDSTLMessageType enum according to [AI] 29.4.3.8
type SDSTLMessageType byte
// All SDS-TL message types according to [AI] table 29.20
const (
SDSTransferMessage SDSTLMessageType = 0
SDSReportMessage SDSTLMessageType = 1
SDSAcknowledgeMessage SDSTLMessageType = 2
)
// ParseSDSAcknowledge parses a SDS-ACK PDU from the given bytes
func ParseSDSAcknowledge(bytes []byte) (SDSAcknowledge, error) {
if len(bytes) < 4 {
return SDSAcknowledge{}, fmt.Errorf("SDS-ACK PDU too short: %d", len(bytes))
}
var result SDSAcknowledge
result.protocol = ProtocolIdentifier(bytes[0])
result.DeliveryStatus = DeliveryStatus(bytes[2])
result.MessageReference = MessageReference(bytes[3])
return result, nil
}
// SDSAcknowledge represents the SDS-ACK PDU contents as defined in [AI] 29.4.2.1
type SDSAcknowledge struct {
protocol ProtocolIdentifier
DeliveryStatus DeliveryStatus
MessageReference MessageReference
}
// ParseSDSReport parses a SDS-REPORT PDU from the given bytes
func ParseSDSReport(bytes []byte) (SDSReport, error) {
if len(bytes) < 4 {
return SDSReport{}, fmt.Errorf("SDS-REPORT PDU too short: %d", len(bytes))
}
var result SDSReport
result.protocol = ProtocolIdentifier(bytes[0])
result.AckRequired = ((bytes[1] & 0x08) != 0)
storeForwardControl := (bytes[1] & 0x01) != 0
result.DeliveryStatus = DeliveryStatus(bytes[2])
result.MessageReference = MessageReference(bytes[3])
userdataStart := 4
if storeForwardControl {
sfc, err := ParseStoreForwardControl(bytes[4:])
if err != nil {
return SDSReport{}, err
}
result.StoreForwardControl = sfc
userdataStart += sfc.Length()
}
if userdataStart < len(bytes) {
result.UserData = bytes[userdataStart:]
}
return result, nil
}
// NewSDSReport creates a new SDS-REPORT PDU based on the given SDS-TRANSFER PDU without store/forward control information.
func NewSDSReport(sdsTransfer SDSTransfer, ackRequired bool, deliveryStatus DeliveryStatus) SDSReport {
return SDSReport{
protocol: sdsTransfer.protocol,
AckRequired: ackRequired,
DeliveryStatus: deliveryStatus,
MessageReference: sdsTransfer.MessageReference,
}
}
// SDSReport represents the SDS-REPORT PDU contents as defined in [AI] 29.4.2.2
type SDSReport struct {
protocol ProtocolIdentifier
AckRequired bool
DeliveryStatus DeliveryStatus
MessageReference MessageReference
StoreForwardControl StoreForwardControl
// user data
UserData []byte
}
// Encode this SDS-REPORT PDU
func (r SDSReport) Encode(bytes []byte, bits int) ([]byte, int) {
bytes, bits = r.protocol.Encode(bytes, bits)
var byte1 byte
byte1 = byte(SDSReportMessage) << 4
if r.AckRequired {
byte1 |= 0x08
}
bytes = append(bytes, byte1)
bits += 8
bytes, bits = r.DeliveryStatus.Encode(bytes, bits)
bytes, bits = r.MessageReference.Encode(bytes, bits)
return bytes, bits
}
// ParseSDSShortReport parses a SDS-SHORT-REPORT PDU from the given bytes
func ParseSDSShortReport(bytes []byte) (SDSShortReport, error) {
if len(bytes) != 2 {
return SDSShortReport{}, fmt.Errorf("SDS-SHORT-REPORT PDU invalid length %d", len(bytes))
}
if (bytes[0] & SDSShortReportPDUIdentifier) != SDSShortReportPDUIdentifier {
return SDSShortReport{}, fmt.Errorf("SDS-SHORT-REPORT PDU invalid PDU identifier 0x%x", bytes[0]&SDSShortReportPDUIdentifier)
}
var result SDSShortReport
result.ReportType = ShortReportType(bytes[0] & 0x03)
result.MessageReference = MessageReference(bytes[1])
return result, nil
}
// SDSShortReportPDUIdentifier for SDS-SHORT-REPORT PDUs
const SDSShortReportPDUIdentifier byte = 0x7A
// SDSShortReport represents the SDS-SHORT-REPORT PDU contents as defined in [AI] 29.4.2.3
type SDSShortReport struct {
ReportType ShortReportType
MessageReference MessageReference
}
// Encode this SDS-SHORT-REPORT PDU
func (r SDSShortReport) Encode(bytes []byte, bits int) ([]byte, int) {
byte0 := byte(0x7C) | byte(r.ReportType)
bytes = append(bytes, byte0)
bits += 8
bytes, bits = r.MessageReference.Encode(bytes, bits)
return bytes, bits
}
// ParseSDSTransfer parses a SDS-TRANSFER PDU from the given bytes
func ParseSDSTransfer(bytes []byte) (SDSTransfer, error) {
if len(bytes) < 4 {
return SDSTransfer{}, fmt.Errorf("SDS-TRANSFER PDU too short: %d", len(bytes))
}
var result SDSTransfer
result.protocol = ProtocolIdentifier(bytes[0])
result.DeliveryReportRequest = DeliveryReportRequest((bytes[1] & 0x0C) >> 2)
result.ServiceSelectionShortFormReport = (bytes[1] & 0x02) == 0
storeForwardControl := (bytes[1] & 0x01) != 0
result.MessageReference = MessageReference(bytes[2])
userdataStart := 3
if storeForwardControl {
sfc, err := ParseStoreForwardControl(bytes[3:])
if err != nil {
return SDSTransfer{}, err
}
result.StoreForwardControl = sfc
userdataStart += sfc.Length()
}
var sdu interface{}
var err error
switch result.protocol {
case TextMessaging, ImmediateTextMessaging:
sdu, err = ParseTextSDU(bytes[userdataStart:])
case UserDataHeaderMessaging:
sdu, err = ParseConcatenatedTextSDU(bytes[userdataStart:])
default:
return SDSTransfer{}, fmt.Errorf("protocol 0x%x is not supported as SDS-TRANSFER content", bytes[0])
}
if err != nil {
return SDSTransfer{}, err
}
result.UserData = sdu
return result, nil
}
// NewTextMessageTransfer returns a new SDS-TRANSFER PDU for text messaging with the given parameters
func NewTextMessageTransfer(messageReference MessageReference, immediate bool, deliveryReport DeliveryReportRequest, encoding TextEncoding, text string) SDSTransfer {
var protocol ProtocolIdentifier
if immediate {
protocol = ImmediateTextMessaging
} else {
protocol = TextMessaging
}
return SDSTransfer{
protocol: protocol,
MessageReference: messageReference,
DeliveryReportRequest: deliveryReport,
UserData: TextSDU{
TextHeader: TextHeader{
Encoding: encoding,
},
Text: text,
},
}
}
// 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
type SDSTransfer struct {
protocol ProtocolIdentifier
DeliveryReportRequest DeliveryReportRequest
ServiceSelectionShortFormReport bool
MessageReference MessageReference
StoreForwardControl StoreForwardControl
UserData interface{}
}
// Encode this SDS-TRANSFER PDU
func (m SDSTransfer) Encode(bytes []byte, bits int) ([]byte, int) {
bytes, bits = m.protocol.Encode(bytes, bits)
var byte1 byte
byte1 = byte(SDSTransferMessage) << 4
byte1 |= byte(m.DeliveryReportRequest) << 2
if !m.ServiceSelectionShortFormReport {
byte1 |= 0x02
}
bytes = append(bytes, byte1)
bits += 8
bytes, bits = m.MessageReference.Encode(bytes, bits)
switch sdu := m.UserData.(type) {
case TextSDU:
bytes, bits = sdu.Encode(bytes, bits)
case ConcatenatedTextSDU:
bytes, bits = sdu.Encode(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
func (m SDSTransfer) ReceivedReportRequested() bool {
return m.DeliveryReportRequest == MessageReceivedReportRequested ||
m.DeliveryReportRequest == MessageReceivedAndConsumedReportRequested
}
// ConsumedReportRequested indicates if for this SDS-TRANSFER PDU a delivery report is requested for consumation
func (m SDSTransfer) ConsumedReportRequested() bool {
return m.DeliveryReportRequest == MessageConsumedReportRequested ||
m.DeliveryReportRequest == MessageReceivedAndConsumedReportRequested
}
// Immediate indiciates if this message should be displayed/handled immediately by the TE.
func (m SDSTransfer) Immediate() bool {
return m.protocol == ImmediateTextMessaging
}
// MessageReference according to [AI] 29.4.3.7
type MessageReference byte
// Encode this message reference
func (m MessageReference) Encode(bytes []byte, bits int) ([]byte, int) {
return append(bytes, byte(m)), bits + 8
}
// DeliveryStatus according to [AI] 29.4.3.2
type DeliveryStatus byte
// Encode this delivery status
func (s DeliveryStatus) Encode(bytes []byte, bits int) ([]byte, int) {
return append(bytes, byte(s)), bits + 8
}
// Success indicates if this status represents a success (see [AI] table 29.16).
func (s DeliveryStatus) Success() bool {
return (s & 0xE0) == 0x00
}
// TemporaryError indicates if this status represents a temporary error (see [AI] table 29.16).
func (s DeliveryStatus) TemporaryError() bool {
return (s & 0xE0) == 0x20
}
// DataDeliveryFailed indicates if this status represents a data transfer failure (see [AI] table 29.16).
func (s DeliveryStatus) DataDeliveryFailed() bool {
return (s & 0xE0) == 0x40
}
// FlowControl indicates if this status represents flow control information (see [AI] table 29.16).
func (s DeliveryStatus) FlowControl() bool {
return (s & 0xE0) == 0x06
}
// EndToEndControl indicates if this status represents end to end control information (see [AI] table 29.16).
func (s DeliveryStatus) EndToEndControl() bool {
return (s & 0xE0) == 0x80
}
// All DeliveryStatus values according to [AI] table 29.16
const (
// Success
ReceiptAckByDestination DeliveryStatus = 0x00
ReceiptReportAck DeliveryStatus = 0x01
ConsumedByDestination DeliveryStatus = 0x02
ConsumedReportAck DeliveryStatus = 0x03
MessageForwardedToExternalNetwork DeliveryStatus = 0x04
SentToGroupAckPresented DeliveryStatus = 0x05
ConcatenationPartReceiptAckByDestination DeliveryStatus = 0x06
// Temporary Error
Congestion DeliveryStatus = 0x20
MessageStored DeliveryStatus = 0x21
DestinationNotReachableMessageStored DeliveryStatus = 0x22
// Data Transfer Failed
NetworkOverload DeliveryStatus = 0x40
ServicePermanentlyNotAvailable DeliveryStatus = 0x41
ServiceTemporaryNotAvailable DeliveryStatus = 0x42
SourceNotAuthorized DeliveryStatus = 0x43
DestinationNotAuthorzied DeliveryStatus = 0x44
UnknownDestGatewayServiceAddress DeliveryStatus = 0x45
UnknownForwardAddress DeliveryStatus = 0x46
GroupAddressWithIndividualService DeliveryStatus = 0x47
ValidityPeriodExpiredNotReceived DeliveryStatus = 0x48
ValidityPeriodExpiredNotConsumed DeliveryStatus = 0x49
DeliveryFailed DeliveryStatus = 0x4A
DestinationNotRegistered DeliveryStatus = 0x4B
DestinationQueueFull DeliveryStatus = 0x4C
MessageTooLong DeliveryStatus = 0x4D
DestinationDoesNotSupportSDSTL DeliveryStatus = 0x4E
DestinationHostNotConnected DeliveryStatus = 0x4F
ProtocolNotSupported DeliveryStatus = 0x50
DataCodingSchemeNotSupported DeliveryStatus = 0x51
DestinationMemoryFullMessageDiscarded DeliveryStatus = 0x52
DestinationNotAcceptingSDS DeliveryStatus = 0x53
ConcatednatedMessageTooLong DeliveryStatus = 0x54
DestinationAddressProhibited DeliveryStatus = 0x56
CannotRouteToExternalNetwork DeliveryStatus = 0x57
UnknownExternalSubscriberNumber DeliveryStatus = 0x58
NegativeReportAcknowledgement DeliveryStatus = 0x59
DestinationNotReachable DeliveryStatus = 0x5A
TextDistributionError DeliveryStatus = 0x5B
CorruptInformationElement DeliveryStatus = 0x5C
NotAllConcatenationPartsReceived DeliveryStatus = 0x5D
DestinationEngagedInAnotherServiceBySwMI DeliveryStatus = 0x5E
DestinationEngagedInAnotherServiceByDest DeliveryStatus = 0x5F
// Flow Control
DestinationMemoryFull DeliveryStatus = 0x60
DestinationMemoryAvailable DeliveryStatus = 0x61
StartPendingMessages DeliveryStatus = 0x62
NoPendingMessages DeliveryStatus = 0x63
// End to End Control
StopSending DeliveryStatus = 0x80
StartSending DeliveryStatus = 0x81
)
// ShortReportType enum according to [AI] 29.4.3.10
type ShortReportType byte
// All short report type values accoring to [AI] table 29.22
const (
ProtocolOrEncodingNotSupportedShort ShortReportType = 0x00
DestinationMemoryFullShort ShortReportType = 0x01
MessageReceivedShort ShortReportType = 0x02
MessageConsumedShort ShortReportType = 0x03
)
// DeliveryReportRequest enum according to [AI] 29.4.3.3
type DeliveryReportRequest byte
// All delivery report requests according to [AI] table 29.17
const (
NoReportRequested DeliveryReportRequest = 0x00
MessageReceivedReportRequested DeliveryReportRequest = 0x01
MessageConsumedReportRequested DeliveryReportRequest = 0x02
MessageReceivedAndConsumedReportRequested DeliveryReportRequest = 0x03
)
// ParseStoreForwardControl from the given bytes.
func ParseStoreForwardControl(bytes []byte) (StoreForwardControl, error) {
if len(bytes) < 1 {
return StoreForwardControl{}, fmt.Errorf("store forward control too short: %d", len(bytes))
}
var result StoreForwardControl
result.Valid = true
result.ValidityPeriod = ParseValidityPeriod(bytes[0] >> 3)
result.ForwardAddressType = ForwardAddressType(bytes[0] & 3)
switch result.ForwardAddressType {
case ForwardToSNA:
if len(bytes) < 2 {
return StoreForwardControl{}, fmt.Errorf("store forward control with SNA too short: %d", len(bytes))
}
result.ForwardAddressSNA = ForwardAddressSNA(bytes[1])
case ForwardToSSI:
if len(bytes) < 4 {
return StoreForwardControl{}, fmt.Errorf("store forward control with SSI too short: %d", len(bytes))
}
copy(result.ForwardAddressSSI[:], bytes[1:4])
case ForwardToTSI:
if len(bytes) < 4 {
return StoreForwardControl{}, fmt.Errorf("store forward control with TSI too short: %d", len(bytes))
}
copy(result.ForwardAddressSSI[:], bytes[1:4])
case ForwardToExternalSubscriberNumber:
if len(bytes) < 2 {
return StoreForwardControl{}, fmt.Errorf("store forward control with external subscriber number too short: %d", len(bytes))
}
l := int(bytes[1])
bl := l / 2
if l%2 > 0 {
bl++
}
if len(bytes) < 2+bl {
return StoreForwardControl{}, fmt.Errorf("store forward control with external subscriber number too short: %d", len(bytes))
}
result.ExternalSubscriberNumber = make(ExternalSubscriberNumber, 0, l)
d := 0
for i := 0; i < bl; i++ {
result.ExternalSubscriberNumber[d] = ExternalSubscriberNumberDigit(bytes[i] >> 4)
d++
if d < l {
result.ExternalSubscriberNumber[d+1] = ExternalSubscriberNumberDigit(bytes[i] & 0x0F)
d++
}
}
}
return result, nil
}
// StoreForwardControl represents the optional store and forward control information contained in the SDS-REPORT and SDS-TRANSFER PDUs.
type StoreForwardControl struct {
// Valid indicates if this StoreForwardControl instance contains valid data. Valid is false if store and forward control is not used with this message.
Valid bool
ValidityPeriod ValidityPeriod
ForwardAddressType ForwardAddressType
ForwardAddressSNA ForwardAddressSNA
ForwardAddressSSI ForwardAddressSSI
ForwardAddressExtension ForwardAddressExtension
ExternalSubscriberNumber ExternalSubscriberNumber
}
// Length returns the length of this encoded store forward control in bytes.
func (s StoreForwardControl) Length() int {
switch s.ForwardAddressType {
case ForwardToSNA:
return 2
case ForwardToSSI:
return 4
case ForwardToTSI:
return 4
case ForwardToExternalSubscriberNumber:
l := len(s.ExternalSubscriberNumber) / 2
if len(s.ExternalSubscriberNumber)%2 > 0 {
l++
}
return 2 + l
case NoForwardAddressPresent:
return 1
default:
return 1
}
}
// ValidityPeriod according to [AI] 29.4.3.14
type ValidityPeriod time.Duration
// InfinitelyValid represents the infinite validity period (31).
const InfinitelyValid ValidityPeriod = -1
// DecodeValidityPeriod from a 5 bits value according to [AI] table 29.25
func ParseValidityPeriod(b byte) ValidityPeriod {
switch {
case b == 0:
return 0
case b <= 6:
return ValidityPeriod(time.Duration(b) * 10 * time.Second)
case b <= 10:
return ValidityPeriod(time.Duration(b-5) * time.Minute)
case b <= 16:
return ValidityPeriod(time.Duration(b-10) * 10 * time.Minute)
case b <= 21:
return ValidityPeriod(time.Duration(b-15) * time.Hour)
case b <= 24:
return ValidityPeriod(time.Duration(b-20) * 6 * time.Hour)
case b <= 30:
return ValidityPeriod(time.Duration(b-24) * 48 * time.Hour)
default:
return InfinitelyValid
}
}
// Encode the validity period into 5 bits, according to [AI] table 29.25
func (p ValidityPeriod) Encode() ([]byte, int) {
d := time.Duration(p)
var result byte
incIfRemainder := func(resultDuration time.Duration) {
remainder := d - resultDuration
if remainder > 0 {
result++
}
}
switch {
case d == 0:
return []byte{0}, 8
case d <= time.Minute:
result = byte(int(d.Truncate(time.Second).Seconds() / 10))
incIfRemainder(time.Duration(result) * 10 * time.Second)
return []byte{result}, 8
case d <= 5*time.Minute:
result = byte(int(d.Truncate(time.Minute).Minutes()))
incIfRemainder(time.Duration(result) * time.Minute)
return []byte{result + 5}, 8
case d <= time.Hour:
result = byte(int(d.Truncate(time.Minute).Minutes() / 10))
incIfRemainder(time.Duration(result) * 10 * time.Minute)
return []byte{result + 10}, 8
case d <= 6*time.Hour:
result = byte(int(d.Truncate(time.Hour).Hours()))
incIfRemainder(time.Duration(result) * time.Hour)
return []byte{result + 15}, 8
case d <= 24*time.Hour:
result = byte(int(d.Truncate(time.Hour).Hours() / 6))
incIfRemainder(time.Duration(result) * 6 * time.Hour)
return []byte{result + 20}, 8
case d <= 12*24*time.Hour:
result = byte(int(d.Truncate(time.Hour).Hours() / 48))
incIfRemainder(time.Duration(result) * 48 * time.Hour)
return []byte{result + 24}, 8
default:
return []byte{31}, 8 // infinite
}
}
// ForwardAddressType enum according to [AI] 29.4.3.5
type ForwardAddressType byte
// All forward address type values according to [AI] table 29.18
const (
ForwardToSNA ForwardAddressType = 0x00
ForwardToSSI ForwardAddressType = 0x01
ForwardToTSI ForwardAddressType = 0x02
ForwardToExternalSubscriberNumber ForwardAddressType = 0x03
NoForwardAddressPresent ForwardAddressType = 0x07
)
// ForwardAddressSNA according to [AI] 29.4.3.6
type ForwardAddressSNA byte
// ForwardAddressSSI according to [AI] 29.4.3.6
type ForwardAddressSSI [3]byte
// ForwardAddressExtendsion according to [AI] 29.4.3.6
type ForwardAddressExtension [3]byte
// ExternalSubscriberNumber according to [AI] 29.4.3.6, contains an arbitrary number of digits.
type ExternalSubscriberNumber []ExternalSubscriberNumberDigit
// ExternalSubscriberNumberDigit represents one digit in the ExternalSubscriberNumber
type ExternalSubscriberNumberDigit byte // its only 4 bits per digit
/* Simple Text Messaging related types and functions */
// ParseSimpleTextMessage parses a simple text message PDU
func ParseSimpleTextMessage(bytes []byte) (SimpleTextMessage, error) {
if len(bytes) < 2 {
return SimpleTextMessage{}, fmt.Errorf("simple text message PDU too short: %d", len(bytes))
}
var result SimpleTextMessage
result.protocol = ProtocolIdentifier(bytes[0])
result.Encoding = TextEncoding(bytes[1] & 0x7F)
text, err := DecodePayloadText(result.Encoding, bytes[2:])
if err != nil {
return SimpleTextMessage{}, err
}
result.Text = text
return result, nil
}
// NewSimpleTextMessage returns a new simple text message PDU according to the given parameters
func NewSimpleTextMessage(immediate bool, encoding TextEncoding, text string) SimpleTextMessage {
var protocol ProtocolIdentifier
if immediate {
protocol = ImmediateTextMessaging
} else {
protocol = TextMessaging
}
return SimpleTextMessage{
protocol: protocol,
Encoding: encoding,
Text: text,
}
}
// SimpleTextMessage represents the data of a simple text messaging PDU, according to [AI] 29.5.2.3
type SimpleTextMessage struct {
protocol ProtocolIdentifier
Encoding TextEncoding
Text string
}
// Immediate indiciates if this message should be displayed/handled immediately by the TE.
func (m SimpleTextMessage) Immediate() bool {
return m.protocol == SimpleImmediateTextMessaging
}
// Encode this simple text message
func (m SimpleTextMessage) Encode(bytes []byte, bits int) ([]byte, int) {
bytes, bits = m.protocol.Encode(bytes, bits)
bytes = append(bytes, byte(m.Encoding))
bits += 8
bytes, bits = AppendEncodedPayloadText(bytes, bits, m.Text, m.Encoding)
return bytes, bits
}
/* Text messaging related types and functions */
// ParseTextSDU parses the user data of a text message.
func ParseTextSDU(bytes []byte) (TextSDU, error) {
textHeader, err := ParseTextHeader(bytes)
if err != nil {
return TextSDU{}, err
}
textPayloadStart := textHeader.Length()
text, err := DecodePayloadText(textHeader.Encoding, bytes[textPayloadStart:])
if err != nil {
return TextSDU{}, err
}
return TextSDU{
TextHeader: textHeader,
Text: text,
}, nil
}
// TextSDU according to [AI] 29.5.3.3
type TextSDU struct {
TextHeader
Text string
}
// Encode this text SDU
func (t TextSDU) Encode(bytes []byte, bits int) ([]byte, int) {
bytes, bits = t.TextHeader.Encode(bytes, bits)
bytes, bits = AppendEncodedPayloadText(bytes, bits, t.Text, t.TextHeader.Encoding)
return bytes, bits
}
// Length returns the length of this encoded text SDU in bytes.
func (t TextSDU) Length() int {
return t.TextHeader.Length() + TextBytes(t.Encoding, len(t.Text))
}
/* Concatenated text messageing related types and functions */
// ParseConcatenatedTextSDU parses the user data of a message with user data header.
func ParseConcatenatedTextSDU(bytes []byte) (ConcatenatedTextSDU, error) {
/*
Example PDU with User Data Header: 8A00C98D045A8F050003C90201
8A: Protocol Identifier[8]
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
<no store forward control information>
8D: Timestamp Used[1] (yes), Text Encoding Scheme[7] (ISO8859-15/Latin 0)
04 5A 8F: Timestamp[24]
05: User Data Header length[8] (5)
00: UDH Information Element ID[8] (0)
03: UDH Information Element Length[8] (3)
C9: Message Reference[8] (0xC9) <-- This is always the message reference of the first part
02: Total number of parts[8] (2)
01: Sequence number of current part[8] (1) <-- 1-based, first part == 1
and then comes the text data
*/
textHeader, err := ParseTextHeader(bytes)
if err != nil {
return ConcatenatedTextSDU{}, err
}
udhStart := textHeader.Length()
udh, err := ParseConcatenatedTextUDH(bytes[udhStart:])
if err != nil {
return ConcatenatedTextSDU{}, err
}
textPayloadStart := udhStart + udh.Length()
text, err := DecodePayloadText(textHeader.Encoding, bytes[textPayloadStart:])
if err != nil {
return ConcatenatedTextSDU{}, err
}
return ConcatenatedTextSDU{
TextSDU: TextSDU{
TextHeader: textHeader,
Text: text,
},
UserDataHeader: udh,
}, nil
}
// ConcatenatedTextSDU according to [AI] 29.5.10.3
type ConcatenatedTextSDU struct {
TextSDU
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.
func (t ConcatenatedTextSDU) Length() int {
return t.TextSDU.Length() + t.UserDataHeader.Length()
}
// ParseConcatenatedTextUDH according to [AI] table 29.48
func ParseConcatenatedTextUDH(bytes []byte) (ConcatenatedTextUDH, error) {
if len(bytes) < 6 {
return ConcatenatedTextUDH{}, fmt.Errorf("concatenated text UDH too short: %d", len(bytes))
}
var result ConcatenatedTextUDH
result.HeaderLength = bytes[0]
result.ElementID = UDHInformationElementID(bytes[1])
result.ElementLength = bytes[2]
numbersStart := 4
if result.ElementID == ConcatenatedTextMessageWithShortReference {
if result.ElementLength != 3 {
return ConcatenatedTextUDH{}, fmt.Errorf("UDH information element length invalid, got %d but expected 3", result.ElementLength)
}
result.MessageReference = uint16(bytes[3])
} else {
if result.ElementLength != 4 {
return ConcatenatedTextUDH{}, fmt.Errorf("UDH information element length invalid, got %d but expected 4", result.ElementLength)
}
if len(bytes) < 7 {
return ConcatenatedTextUDH{}, fmt.Errorf("concatenated text UDH with long reference too short: %d", len(bytes))
}
numbersStart = 5
result.MessageReference = (uint16(bytes[4]) << 8) | uint16(bytes[3])
}
result.TotalNumber = bytes[numbersStart]
result.SequenceNumber = bytes[numbersStart+1]
return result, nil
}
// ConcatenatedTextUDH contents according to [AI] 29.5.10.3
type ConcatenatedTextUDH struct {
HeaderLength byte
ElementID UDHInformationElementID
ElementLength byte
MessageReference uint16
TotalNumber 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.
func (h ConcatenatedTextUDH) Length() int {
result := 6
if h.ElementID == ConcatenatedTextMessageWithLongReference {
result++
}
return result
}
// UDHInformationElementID enum according to [AI] 29.5.9.4.1
type UDHInformationElementID byte
// The relevant UDHInformationElementID values for concatenated text according to [AI] table 29.47.
const (
ConcatenatedTextMessageWithShortReference UDHInformationElementID = 0x00
ConcatenatedTextMessageWithLongReference UDHInformationElementID = 0x08
)
/* Status related types and functions */
// ParseStatus from the given bytes.
func ParseStatus(bytes []byte) (interface{}, error) {
if len(bytes) < 2 {
return 0, fmt.Errorf("status value too short: %v", bytes)
}
if (bytes[0] & SDSShortReportPDUIdentifier) == SDSShortReportPDUIdentifier {
return ParseSDSShortReport(bytes)
}
var result Status
result = (Status(bytes[0]) << 8) | Status(bytes[1])
return result, nil
}
// Status represents a pre-coded status according to [AI] 14.8.34
type Status uint16
// Bytes returns this status as byte slice.
func (s Status) Bytes() []byte {
return []byte{
byte(s >> 8),
byte(s),
}
}
// Encode this status
func (s Status) Encode(bytes []byte, bits int) ([]byte, int) {
return append(bytes, s.Bytes()...), bits + 16
}
// Length returns the length of this encoded status in bytes.
func (s Status) Length() int {
return 2
}
// Some relevant status values
const (
// requests
Status0 Status = 0x8002
Status1 Status = 0x8003
Status2 Status = 0x8004
Status3 Status = 0x8005
Status4 Status = 0x8006
Status5 Status = 0x8007
Status6 Status = 0x8008
Status7 Status = 0x8009
Status8 Status = 0x800A
Status9 Status = 0x800B
// responses
StatusA Status = 0x80F2
StatusE Status = 0x80F3
StatusC Status = 0x80F4
StatusF Status = 0x80F5
StatusH Status = 0x80F6
StatusJ Status = 0x80F7
StatusL Status = 0x80F8
StatusP Status = 0x80F9
Statusd Status = 0x80FC
Statush Status = 0x80FD
Statuso Status = 0x80FE
Statusu Status = 0x80FF
)
// DecodeTimestamp according to [AI] 29.5.4.4
func DecodeTimestamp(bytes []byte) (time.Time, error) {
if len(bytes) != 3 {
return time.Now(), fmt.Errorf("a timestamp must be 3 bytes long")
}
locations := []*time.Location{time.Local, time.UTC, time.Local, time.Local}
location := locations[(bytes[0]&0xC0)>>6]
year := time.Now().Year()
month := bytes[0] & 0x0F
day := int((bytes[1] & 0xF8) >> 3)
hour := int(((bytes[1] & 0x07) << 2) | ((bytes[2] & 0xC0) >> 6))
minute := int(bytes[2] & 0x3F)
return time.Date(year, time.Month(month), day, hour, minute, 0, 0, location), nil
}
// EncodeTimestampUTC according to [AI] 29.5.4.4, always using timeframe type UTC
func EncodeTimestampUTC(timestamp time.Time) []byte {
result := make([]byte, 3)
utc := timestamp.UTC()
result[0] = 0x40 // always use timeframe type UTC
result[0] |= byte(utc.Month()) & 0x0F
result[1] = (byte(utc.Day()) << 3) & 0xF8
result[1] |= (byte(utc.Hour()) >> 2) & 0x07
result[2] = (byte(utc.Hour()) << 6) & 0xC0
result[2] |= byte(utc.Minute()) & 0x3F
return result
}