Search code examples
delphigsmpdu

Looking for GSM 7bit encode/decode algorithm


I need to Send Short message in pdu mode. Anybody can give me a GSM 7bit encode/decode algorithm?


Solution

  • See if this is of any use to you. Code taken from one of my very old projects - may be used as you wish.

    unit SMSCodec;
    
    interface
    
    const
      //:Default 7-bit alphabet.
      CPDU7bit = #10#13' !"#$&''()*+,-./0123456789:;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
    
    type
      {:Encoder result.
        @enum esEncoded   Message encoded successfully.
        @enum esTruncated Message encoded successfully, but truncated because it was too long.
        @enum esError     Error.
      }
      TEncoderStatus = (esEncoded, esTruncated, esError);
    
      {:Decoder result.
        @enum dsDecoded   Message decoded successfully.
        @enum dsError     Error.
      }
      TDecoderStatus = (dsDecoded, dsError);
    
      {:Message format.
        @enum mf0340 ETS 300 901 (GSM 03.40)
        @enum mf0705 GSM 07.05
      }
      TMessageFormat = (mf0340, mf0705);
    
      {:Message Type
      }
      TMessageType = (mtSMSDeliver, mtSMSDeliverReport, mtSMSSubmitReport,
        mtSMSSubmit, mtSMSStatusReport, mtSMSCommand, mtReserved);
    
      {:TP-Status major information.
      }
      TTPStatusMajor = (tpsmDelivered, tpsmTemporaryError, tpsmPermanentError,
        tpsmReserved);
    
      {:TP-Status detailed information.
      }
      TTPStatusDetailed = (
        // tpsmDelivered
        tpsdReceived,                    // Short message received by the SME
        tpsdForwardedNotConfirmed,       // Short message forwarded by the SC to the SME but the SC is unable to confirm delivery
        tpsdReplaced,                    // Short message replaced by the SC
        // tpsmTemporaryError
        tpsdCongestion,                  // Congestion
        tpsdSMEBusy,                     // SME busy
        tpsdNoResponseFromSME,           // No response from SME
        tpsdServiceRejected,             // Service rejected
        tpsdErrorInSME,                  // Error in SME
        // tpsmPermanentError
        tpsdRemoteProcedureError,        // Remote procedure error
        tpsdIncompatibleDestination,     // Incompatible destination
        tpsdConnectionRejectedBySME,     // Connection rejected by SME
        tpsdNotObtainable,               // Not obtainable
        tpsdNoInternetworkingAvailable,  // No interworking available
        tpsdSMValitidyPerionExpired,     // SM Validity Period Expired
        tpsdSMDeletedByOriginatingSME,   // SM Deleted by originating SME
        tpsdSMDeletedBySCAdministration, // SM Deleted by SC Administration
        tpsdSMDoesNotExist,              // SM does not exist (The SM may have previously existed in the SC but the SC no longer has knowledge of it or the SM may never have previously existed in the SC)
        // tpsmTemporaryError and tpsmPermanentError
        tpsdQOSNotAvailable,             // Quality of service not available
        // all major classes
        tpsdReserved
      );
    
      {:Decoded TP-Status, as specified in ETSI GSM 03.40 specification, 9.2.3.15
      }
      TTPStatus = record
        tpsMajor        : TTPStatusMajor;
        tpsWillContinue : boolean;
        tpsDetailed     : TTPStatusDetailed;
        tpsOriginal     : byte;
      end;
    
      {:Decoded message
      }
      TSMSDecodedMessage = record
        sdcOriginalMessage : string;
        sdcMessageType     : TMessageType;
                                            // Set if sdcMessageType =
        sdcSMSCenterNumber : string;        // *
        sdcNumber          : string;        // mtSMSDeliver, mtSMSSubmit
        sdcShortMessage    : string;        // mtSMSDeliver, mtSMSSubmit
        sdcValidityMinutes : integer;       // mtSMSSubmit
        sdcRequestReport   : boolean;       // mtSMSSubmit
        sdcMessageReference: byte;          // mtSMSSubmit, mtSMSStatusReport
        sdcRecipientAddress: string;        // mtSMSStatusReport
        sdcSCTimeStamp     : TDateTime;     // mtSMSStatusReport
        sdcDischargeTime   : TDateTime;     // mtSMSStatusReport
        sdcStatus          : TTPStatus;     // mtSMSStatusReport
        sdcMessageFormat   : TMessageFormat;// mtSMSDeliver, mtSMSStatusReport
        sdcFFPadded        : boolean;       // mtSMSDeliver, mtSMSStatusReport
      end;
    
      {:SMS PDU coder/decoder.
      }
      TSMSCodec = class
      private
        tbl7bit: array [char] of byte;
        tbl8bit: array [byte] of char;
        procedure Create7bitLookupTable;
        function  Decode7bitText(var pdu: string; txtLen: byte; var decoded: boolean): string;
        function  DecodeDischargeTime(dtime: string): TDateTime;
        function  DecodeNumber(var pdu: string; countOctets: boolean; var decoded: boolean): string;
        function  DecodeTimeStamp(tstamp: string): TDateTime;
        function  DecodeTPStatus(status: string): TTPStatus;
        function  Encode7bitText(txt: string; maxLen: byte; var truncated: boolean): string;
        function  EncodeNumber(num: string; countOctets: boolean): string;
        function  EncodeTP_VP(validityMin: integer): string;
      public
        constructor Create;
        destructor  Destroy; override;
        function    DecodeMessage(PDUMessage: string;
          var DecodedMessage: TSMSDecodedMessage): TDecoderStatus;
        function    EncodeMessage(smsMessage: TSMSDecodedMessage;
          var PDUMessage: string; var tpDataLen: integer): TEncoderStatus;
      end;
    
      function GetSMSDetailedErrorMessage(status: TTPStatusDetailed): string;
    
    implementation
    
    uses
      SysUtils,
      GpIFF,
      Gp17String;
    
    resourcestring
      SSmsDetailedErrReserved                          = '(reserved)';
      SSmsDetailedErrCongestion                        = 'Congestion';
      SSmsDetailedErrConnectionRejectedBySME           = 'Connection rejected by SME';
      SSmsDetailedErrErrorInSME                        = 'Error in SME';
      SSmsDetailedErrIncompatibleDestination           = 'Incompatible destination';
      SSmsDetailedErrMValidityPeriodExpired            = 'SM Validity Period Expired';
      SSmsDetailedErrNoInterworkingAvailable           = 'No interworking available';
      SSmsDetailedErrNoResponseFromSME                 = 'No response from SME';
      SSmsDetailedErrNotObtainable                     = 'Not obtainable';
      SSmsDetailedErrQualityOfServiceNotAvailable      = 'Quality of service not available';
      SSmsDetailedErrRemoteProcedureError              = 'Remote procedure error';
      SSmsDetailedErrServiceRejected                   = 'Service rejected';
      SSmsDetailedErrShortMessageForwardedByTheSCtoThe = 'Short message forwarded by the SC to the SME but the SC is unable to confirm delivery';
      SSmsDetailedErrShortMessageReceivedByTheSME      = 'Short message received by the SME';
      SSmsDetailedErrShortMessageReplacedByTheSC       = 'Short message replaced by the SC';
      SSmsDetailedErrSMDeletedByOriginatingSME         = 'SM Deleted by originating SME';
      SSmsDetailedErrSMDeletedBySCAdministration       = 'SM Deleted by SC Administration';
      SSmsDetailedErrSMdoesNotExist                    = 'SM does not exist (The SM may have previously existed in the SC but the SC no longer has knowledge of it or the SM may never have previously existed in the SC)';
      SSmsDetailedErrSMEbusy                           = 'SME busy';
      SSmsDetailedErrSMValidityPeriodExpired           = 'SM validity period expired';
    
      function GetSMSDetailedErrorMessage(status: TTPStatusDetailed): string;
      begin
        case status of
          tpsdReceived:                    Result := SSmsDetailedErrShortMessageReceivedByTheSME;
          tpsdForwardedNotConfirmed:       Result := SSmsDetailedErrShortMessageForwardedByTheSCtoThe;
          tpsdReplaced:                    Result := SSmsDetailedErrShortMessageReplacedByTheSC;
          tpsdCongestion:                  Result := SSmsDetailedErrCongestion;
          tpsdSMEBusy:                     Result := SSmsDetailedErrSMEbusy;
          tpsdNoResponseFromSME:           Result := SSmsDetailedErrNoResponseFromSME;
          tpsdServiceRejected:             Result := SSmsDetailedErrServiceRejected;
          tpsdErrorInSME:                  Result := SSmsDetailedErrErrorInSME;
          tpsdRemoteProcedureError:        Result := SSmsDetailedErrRemoteProcedureError;
          tpsdIncompatibleDestination:     Result := SSmsDetailedErrIncompatibleDestination;
          tpsdConnectionRejectedBySME:     Result := SSmsDetailedErrConnectionRejectedBySME;
          tpsdNotObtainable:               Result := SSmsDetailedErrNotObtainable;
          tpsdNoInternetworkingAvailable:  Result := SSmsDetailedErrNoInterworkingAvailable;
          tpsdSMValitidyPerionExpired:     Result := SSmsDetailedErrSMValidityPeriodExpired;
          tpsdSMDeletedByOriginatingSME:   Result := SSmsDetailedErrSMDeletedByOriginatingSME;
          tpsdSMDeletedBySCAdministration: Result := SSmsDetailedErrSMDeletedBySCAdministration;
          tpsdSMDoesNotExist:              Result := SSmsDetailedErrSMdoesNotExist;
          tpsdQOSNotAvailable:             Result := SSmsDetailedErrQualityOfServiceNotAvailable;
          else                             Result := SSmsDetailedErrReserved;
        end; //case
      end;
    
    { TSMSCodec }
    
    {: TSMSCodec constructor. Prepares lookup table for character conversion.
    }
    constructor TSMSCodec.Create;
    begin
      inherited;
      Create7bitLookupTable;
    end;
    
    {:Creates lookup table to convert from 8-bit to 7-bit character codes.
    }
    procedure TSMSCodec.Create7bitLookupTable;
    var
      b : byte;
      i : integer;
      ch: char;
    const
      eqlASCII : string = CPDU7bit;
    begin
      // TODO 1 -oPrimoz Gabrijelcic: Incomplete: all Greek characters and umlauts are missing
      for ch := Low(tbl7bit) to High(tbl7bit) do
        tbl7bit[ch] := $20; // space
      for i := 1 to Length(eqlASCII) do
        tbl7bit[eqlASCII[i]] := Ord(eqlASCII[i]);
      tbl7bit['@'] := $00;
      tbl7bit['$'] := $02;
    
      for b := Low(tbl8bit) to High(tbl8bit) do
        tbl8bit[b] := ' ';
      for ch := Low(tbl7bit) to High(tbl7bit) do
        if tbl7bit[ch] <> $20 then
          tbl8bit[tbl7bit[ch]] := ch;
    end;
    
    {:Decodes 7-bit "packed" form (coded in hexadecimal - as received in PDU SMS)
      into 8-bit text.
      @param   pdu       Hexadecimal representation of packed form.
      @param   txtLen    Length of unpacked string.
      @param   decoded   True if decoded successfully.
      @returns Unpacked string.
    }
    function TSMSCodec.Decode7bitText(var pdu: string; txtLen: byte;
      var decoded: boolean): string;
    var
      by    : byte;
      currBy: byte;
      i     : integer;
      left  : byte;
      mask  : byte;
      nextBy: byte;
    begin
      decoded := false;
      Result := '';
      left := 7;
      mask := $7F;
      nextBy := 0;
      for i := 1 to txtLen do begin
        if mask = 0 then begin
          Result := Result + tbl8bit[nextBy];
          left := 7;
          mask := $7F;
          nextBy := 0;
        end
        else begin
          if pdu = '' then Exit;
          by := StrToInt('$'+Copy(pdu,1,2)); Delete(pdu,1,2);
          currBy := ((by AND mask) SHL (7-left)) OR nextBy;
          nextBy := (by AND (NOT mask)) shr left;
          Result := Result + tbl8bit[currBy];
          mask := mask SHR 1;
          left := left-1;
        end;
      end; //for
      decoded := true;
    end;
    
    {:Decodes 7-byte discharge time.
      @param   dtime Discharge time in hexadecimal form (0340530S.PDF, 9.2.3.13).
      @returns Decoded discharge time.
      @since   2000-09-05 (1.02)
    }
    function TSMSCodec.DecodeDischargeTime(dtime: string): TDateTime;
    begin
      Result := DecodeTimestamp(dtime);
    end;
    
    {:Decodes PDU message. Most flags are ignored.
      @param   PDUMessage     Encoded message, represented in hexadecimal form (as received from GSM 07.05 device).
      @param   DecodedMessage (out) Decoded message.
      @returns Error status.
    }
    function TSMSCodec.DecodeMessage(PDUMessage: string;
      var DecodedMessage: TSMSDecodedMessage): TDecoderStatus;
    
      // Mobitel (293 41) sometimes pads PDU with FF bytes up to maximum length -
      // this function detects this condition. It is called with unparsed part of
      // PDU as parameter. This parameter should be empty or at least contain only
      // 'F' characters.
      function AllFF(s: string): boolean;
      var
        iCh: integer;
      begin
        Result := false;
        for iCh := 1 to Length(s) do
          if s[iCh] <> 'F' then
            Exit;
        Result := true;
      end;
    
    var
      decoded   : boolean;
      origPDU   : string;
      PDUtype   : byte;
      UDL       : byte;
      workaround: boolean;
    //  DCS     : byte;
    //  PID     : byte;
    //  SCTC    : int64;
    begin
      DecodedMessage.sdcMessageType := mtReserved; // not decoded
      DecodedMessage.sdcOriginalMessage := PDUMessage;
      DecodedMessage.sdcFFPadded := false;
      Result := dsError;
      origPDU := PDUMessage;
      try
        DecodedMessage.sdcMessageFormat := mf0340;
        for workaround := false to true do begin
          PDUMessage := origPDU;
          if workaround then begin
            // Try to detect whether message is in 03.40 format (without SMS Center
            // Number) or in 07.05 format (with SMS Center Number).
            DecodedMessage.sdcSMSCenterNumber := DecodeNumber(PDUMessage,true,decoded);
            if not decoded then
              Exit;
            DecodedMessage.sdcMessageFormat := mf0705;
          end;
          PDUtype := StrToInt('$'+Copy(PDUMessage,1,2)); Delete(PDUMessage,1,2);
          case PDUtype AND $03 of
            0: DecodedMessage.sdcMessageType := mtSMSDeliver;
            1: DecodedMessage.sdcMessageType := mtSMSSubmitReport;
            2: DecodedMessage.sdcMessageType := mtSMSStatusReport;
            3: DecodedMessage.sdcMessageType := mtReserved;
          end; //case
          if (not workaround) and (DecodedMessage.sdcMessageType = mtReserved) then
            continue; // ??? maybe we are decoding PDU from not-completely-compliant telephone ???
          case DecodedMessage.sdcMessageType of
            mtSMSDeliver:
              begin
                DecodedMessage.sdcNumber := DecodeNumber(PDUMessage,false,decoded);
                if not decoded then
                  Exit;
                {PID := StrToInt('$'+Copy(PDUMessage,1,2));} Delete(PDUMessage,1,2);
                {DCS := StrToInt('$'+Copy(PDUMessage,1,2));} Delete(PDUMessage,1,2);
                {SCTC := StrToInt64('$'+Copy(PDUMessage,1,14));} Delete(PDUMessage,1,14);
                UDL := StrToInt('$'+Copy(PDUMessage,1,2)); Delete(PDUMessage,1,2);
                DecodedMessage.sdcShortMessage := Decode7bitText(PDUMessage,UDL,decoded);
                if not decoded then
                  if not workaround then
                    continue // ??? maybe we are decoding PDU from not-completely-compliant telephone ???
                  else
                    Exit;
              end; //mtSMSDeliver
            mtSMSSubmitReport:
              begin
                // don't know how to decode, yet
                if workaround then // if first way round, assume that we only tried the wrong approach
                  PDUMessage := '';
              end; //mtSMSSubmitReport
            mtSMSStatusReport:
              begin // 0340530S.PDF, 9.2.2.3 SMS-STATUS-REPORT type
                DecodedMessage.sdcMessageReference := StrToInt('$'+Copy(PDUMessage,1,2)); Delete(PDUMessage,1,2);
                DecodedMessage.sdcRecipientAddress := DecodeNumber(PDUMessage,false,decoded);
                if not decoded then
                  Exit;
                DecodedMessage.sdcSCTimeStamp := DecodeTimeStamp(Copy(PDUMessage,1,14)); Delete(PDUMessage,1,14);
                DecodedMessage.sdcDischargeTime := DecodeDischargeTime(Copy(PDUMessage,1,14)); Delete(PDUMessage,1,14);
                DecodedMessage.sdcStatus := DecodeTPStatus(Copy(PDUMessage,1,2)); Delete(PDUMessage,1,2);
              end; //mtSMSStatusReport
            mtReserved:
              begin
                // don't know how to decode - obviously
                PDUMessage := '';
              end; //mtReserved
          end; //case
          if PDUMessage = '' then begin
            Result := dsDecoded;
            break;
          end;
          if AllFF(PDUMessage) then begin
            DecodedMessage.sdcFFPadded := true;
            Result := dsDecoded;
            break;
          end;
        end; //for workaround
      except
        on EConvertError do ;
      end;
    end;
    
    {:Decodes number by GSM standards. Understands two formats - prefixed with
      number of bytes (if countOctets is set) or number of digits in original number.
      @param   pdu         (in, out) PDU string. Number will be cut from it.
      @param   countOctets If true, number is written with number of resulting bytes prepended.
      @param   decoded     (out) Set to true if number was decoded successfully.
      @returns Decoded number.
    }
    function TSMSCodec.DecodeNumber(var pdu: string;
      countOctets: boolean; var decoded: boolean): string;
    var
      iOct   : integer;
      n1     : integer;
      n2     : integer;
      numLen : byte;
      numType: byte;
    begin
      Result := '';
      decoded := false;
      if pdu <> '' then begin
        try
          numLen  := StrToInt('$'+Copy(pdu,1,2)); Delete(pdu,1,2);
          numType := StrToInt('$'+Copy(pdu,1,2)); Delete(pdu,1,2);
          if (numType AND $90) = $90 then
            Result := '+';
          if not countOctets then
            numLen := (numLen+1) div 2 + 1;
          for iOct := 1 to numLen-1 do begin
            n1 := StrToInt('$'+Copy(pdu,1,1)); Delete(pdu,1,1);
            n2 := StrToInt('$'+Copy(pdu,1,1)); Delete(pdu,1,1);
            Result := Result + IntToStr(n2);
            if n1 <> $F then
              Result := Result + IntToStr(n1);
          end; //for
          decoded := true;
        except
          on EConvertError do Result := '';
          on ERangeError   do Result := '';
        end;
      end;
    end;
    
    {:Decodes 7-byte timestamp.
      @param   tstamp Timestamp in hexadecimal form (0340530S.PDF, 9.2.3.11).
      @returns Decoded timestamp.
      @since   2000-09-05 (1.02)
    }
    function TSMSCodec.DecodeTimeStamp(tstamp: string): TDateTime;
    var
      day    : integer;
      gmt    : integer;
      gmtSign: integer;
      hour   : integer;
      minute : integer;
      month  : integer;
      second : integer;
      year   : integer;
    begin
      year    := StrToInt(tstamp[ 2]+tstamp[ 1]);
      month   := StrToInt(tstamp[ 4]+tstamp[ 3]);
      day     := StrToInt(tstamp[ 6]+tstamp[ 5]);
      hour    := StrToInt(tstamp[ 8]+tstamp[ 7]);
      minute  := StrToInt(tstamp[10]+tstamp[ 9]);
      second  := StrToInt(tstamp[12]+tstamp[11]);
      gmtSign := IFF(StrToInt(tstamp[14]) AND 8 = 0, 1, -1);
      gmt     := (StrToInt(tstamp[13]) + 10*(StrToInt(tstamp[14]) AND (NOT 8))) * gmtSign;
      if year > 80 then 
        year := 1900 + year
      else
        year := 2000 + year;
      try
        Result := EncodeDate(year,month,day) + EncodeTime(hour, minute, second, 0) - gmt;
      except
        on EConvertError do
          Result := 0;
      end;
    end;
    
    {:Decodes TP-Status.
      @param   status TP-Status (0340530S.PDF, 9.2.3.15).
      @returns Decoded status
      @since   2000-09-05 (1.02)
    }
    function TSMSCodec.DecodeTPStatus(status: string): TTPStatus;
    begin
      Result.tpsOriginal := StrToInt('$'+status);
      if Result.tpsOriginal <= 2 then
        Result.tpsMajor := tpsmDelivered
      else if (Result.tpsOriginal AND $60) = $20 then begin
        Result.tpsMajor := tpsmTemporaryError;
        Result.tpsWillContinue := true;
      end
      else if (Result.tpsOriginal AND $60) = $40 then begin
        Result.tpsMajor := tpsmPermanentError;
        Result.tpsWillContinue := false;
      end
      else if (Result.tpsOriginal AND $60) = $40 then begin
        Result.tpsMajor := tpsmTemporaryError;
        Result.tpsWillContinue := false;
      end
      else
        Result.tpsMajor := tpsmReserved;
      case Result.tpsMajor of
        tpsmDelivered:
          begin
            case Result.tpsOriginal of
              0:   Result.tpsDetailed := tpsdReceived;
              1:   Result.tpsDetailed := tpsdForwardedNotConfirmed;
              2:   Result.tpsDetailed := tpsdReplaced;
              else Result.tpsDetailed := tpsdReserved;
            end; //case
          end; // tmspDelivered
        tpsmTemporaryError:
          begin
            case Result.tpsOriginal AND (NOT $40) of
              32:  Result.tpsDetailed := tpsdCongestion;
              33:  Result.tpsDetailed := tpsdSMEBusy;
              34:  Result.tpsDetailed := tpsdNoResponseFromSME;
              35:  Result.tpsDetailed := tpsdServiceRejected;
              36:  Result.tpsDetailed := tpsdQOSNotAvailable;
              37:  Result.tpsDetailed := tpsdErrorInSME;
              else Result.tpsDetailed := tpsdReserved;
            end; //case
          end; // tpsmTemporaryError
        tpsmPermanentError:
          begin
            case Result.tpsOriginal of
              64:  Result.tpsDetailed := tpsdRemoteProcedureError;
              65:  Result.tpsDetailed := tpsdIncompatibleDestination;
              66:  Result.tpsDetailed := tpsdConnectionRejectedBySME;
              67:  Result.tpsDetailed := tpsdNotObtainable;
              68:  Result.tpsDetailed := tpsdQOSNotAvailable;
              69:  Result.tpsDetailed := tpsdNoInternetworkingAvailable;
              70:  Result.tpsDetailed := tpsdSMValitidyPerionExpired;
              71:  Result.tpsDetailed := tpsdSMDeletedByOriginatingSME;
              72:  Result.tpsDetailed := tpsdSMDeletedBySCAdministration;
              73:  Result.tpsDetailed := tpsdSMDoesNotExist;
              else Result.tpsDetailed := tpsdReserved;
            end; //case
          end; // tpsmPermanentError
        tpsmReserved:
          begin
            Result.tpsDetailed := tpsdReserved;
          end; // tpsmReserved
      end; //case
    end;
    
    {: TSMSCodec destructor. No special cleanup required.
    }
    destructor TSMSCodec.Destroy;
    begin
      inherited;
    end;
    
    {:Encodes 8-bit text into 7-bit "packed" form. 160 8-bit characters can be
      packed into 140 bytes (consisting of 160 7-bit characters). Packed string is
      converted into hexadecimal form as required by GSM standards. If input string
      is longer that maxLen parameter, truncated flag is set and string is truncated.
      @param   txt       Original 8-bit character string.
      @param   maxLen    Maximum length of original string.
      @param   truncated (out) Set if original string is longer than maxLen.
      @returns Packed string in hexadecimal form.
    }
    function TSMSCodec.Encode7bitText(txt: string; maxLen: byte; var truncated: boolean): string;
    var
      buffer  : byte;
      ch      : byte;
      i       : integer;
      leftover: byte;
    begin
      truncated := (Length(txt) > maxLen);
      if truncated then
        txt := First(txt,maxLen);
      Result := '';
      buffer := 0;
      leftover := 0;
      for i := 1 to Length(txt) do begin
        ch := tbl7bit[txt[i]];
        if leftover = 0 then begin
          buffer := ch;
          leftover := 1;
        end
        else begin
          buffer := buffer OR byte(ch SHL (8-leftover));
          Result := Result + HexStr(buffer,1);
          if leftover < 7 then begin
            buffer := ch SHR leftover;
            Inc(leftover);
          end
          else begin
            buffer := 0;
            leftover := 0;
          end;
        end;
      end; //for
      if leftover > 0 then
        Result := Result + HexStr(buffer,1);
    end;
    
    {:Prepares PDU message. If original message is longer than 160 characters, it
      will be truncated. Most of the parameters are currently hardcoded.
      @param   decodedMessage  Message record.
      @param   PDUMessage      (out) Encoded message, represented in hexadecimal form (suitable for sending to GSM 07.05 device).
      @param   tpDataLen       (out) Number of bytes in TP layer data unit.
      @returns Error status.
    }
    function TSMSCodec.EncodeMessage(smsMessage: TSMSDecodedMessage;
      var PDUMessage: string; var tpDataLen: integer): TEncoderStatus;
    var
      DCS    : byte;
      MR     : byte;
      PDUtype: byte;
      PID    : byte;
      TP_VP  : string;
      TP_VPF : integer;
      tpLayer: string;
      trunc  : boolean;
      UD     : string;
      UDL    : byte;
    begin
      // Some parameters are hardcoded
      if smsMessage.sdcValidityMinutes = 0 then begin
        TP_VPF := 0; // TP-VP field not present
        TP_VP  := '';
      end
      else begin
        TP_VPF := 2; // TP-VP field present and integer represented (relative)
        TP_VP  := EncodeTP_VP(smsMessage.sdcValidityMinutes);
      end;
      PDUtype := $01 OR (TP_VPF SHL 3) OR ((Ord(smsMessage.sdcRequestReport) AND 1) SHL 5);
    
      MR      := smsMessage.sdcMessageReference;
      PID     := $00;
      DCS     := $00;
      UD      := Encode7bitText(smsMessage.sdcShortMessage,160,trunc);
      UDL     := Length(smsMessage.sdcShortMessage);
      tpLayer :=
        HexStr(PDUtype,1)                   +
        HexStr(MR,1)                        +
        EncodeNumber(smsMessage.sdcNumber,false) +
        HexStr(PID,1)                       +
        HexStr(DCS,1)                       +
        TP_VP                               +
        HexStr(UDL,1)                       +
        UD;
      PDUMessage :=
        EncodeNumber(smsMessage.sdcSMSCenterNumber,true) +
        tpLayer;
      tpDataLen := Length(tpLayer) div 2;
      if trunc then
        Result := esTruncated
      else
        Result := esEncoded;
    end;
    
    {:Encodes number by GSM standards. Prefixes it with either number of bytes
      (if countOctets is set) or number of digits in original number.
      @param   num         Telephone number if international (starts with + or 00) or local form.
      @param   countOctets If true, number of resulting bytes will be prepended, if false, number of digits in num.
      @returns Encoded number.
    }
    function TSMSCodec.EncodeNumber(num: string; countOctets: boolean): string;
    var
      numLen : byte;
      numType: byte;
    begin
      num := Replace(ReplaceAllSet(num,[#0..#255]-['0'..'9','+'],' '),' ','');
      if num <> '' then begin
        if num[1] = '+' then begin
          Delete(num,1,1);
          numType := $91;
        end
        else if First(num,2) = '00' then begin
          Delete(num,1,2);
          numType := $91;
        end
        else numType := $81;
        if countOctets then
          numLen := ((Length(num)+1) div 2) + 1
        else
          numLen := Length(num);
        if Odd(Length(num)) then num := num + 'F';
        Result := HexStr(numLen,1) + HexStr(numType,1);
        while num <> '' do begin
          Result := Result + num[2] + num[1];
          Delete(num,1,2);
        end; //while
      end
      else Result := '00';
    end;
    
    {:Encodes relative validity into TP_VP parameter. GSM 03.40, 9.2.3.12.
      @param   validityMin Validity period in minutes.
      @returns Encoded TP_VP.
      @since   2000-09-06 (1.02)
    }
    function TSMSCodec.EncodeTP_VP(validityMin: integer): string;
    var
      value: byte;
    begin
    //  5 minute intervals :    5 minutes               to 12 hours (720 minutes)
    //  30 minute intervals: 12.5 hours (750 minutes)   to 24 hours (1440 minutes)
    //  1 hour intervals   :    2 days  (2880 minutes)  to 30 days  (43200 minutes)
    //  1 week intervals   :    5 weeks (50400 minutes) to 63 weeks (635040 minutes)
    
      if validityMin <= 720 then begin
        if validityMin < 5 then
          validityMin := 5;
        value := ((validityMin-1) div 5);
      end
      else if validityMin <= 1440 then
        value := (((validityMin-1) - 720) div 30) + 144
      else if validityMin <= 43200 then
        value := ((validityMin-1) div 1440) + 167
      else begin
        if validityMin > 635040 then
          validityMin := 635040;
        value := ((validityMin-1) div 10080) + 193;
      end;
      Result := HexStr(value,1);
    end;
    
    end.