×

收藏的CDMA_PDU编解码js文件

woxigouadmin woxigouadmin 发表于2020-04-28 17:14:16 浏览542 评论0

抢沙发发表评论

CDMA制式模块发送3GPP2协议短信js文件,网络收藏的,用得上的可以拿去自己修改,暂时还没完全看懂,想改成java的但java水平太弱鸡不懂改,先保存下来以后使用,有改好的可以给一份不?

CDMA_SMS.rar

CS0015_SMS_CDMA_3GPP2.pdf协议原版文档。

<!--
 Copyright 2012 Chuck Lee @ Mozilla Taiwan

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
-->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <script type="text/javascript" src="CDMASMS.js"></script>
  <style>
  div {
    width: 600px;
    word-wrap: break-word;
    vertical-align:text-top;
    text-align:left;
  }
  table, tr, td {
    border-width: 2px;
    border-spacing: 0px;
    border-style: outset;
    border-color: gray;
    border-collapse: separate;
    background-color: white;
    vertical-align:text-top;
  }
  label {
    color : red;
    background: yellow;
  }
  input, text, textarea, select {
    color: white;
    background: black;
    width: 500px;
  }
  </style>
</head>

<body >
  <table>
  <tr><td>
    <h1>Decoder Test</h1>
    <div>
      <h2>Reference PDU</h2>
      <input type="radio" name="testPDU" id="testPDU01" onclick="document.getElementById('smsPDU').value = document.getElementById('testPDU01_content').innerHTML;"/><label for="testPDU01" id="testPDU01_content">0000021002020702C54EA488649C0601FC08120003101BB00103100C100306110804182257</label></label><br />
      <a href="http://blog.csdn.net/lele52141/article/details/6754776" target="_blank">Test data source</a><br /><br />

      <input type="radio" name="testPDU" id="testPDU02" onclick="document.getElementById('smsPDU').value = document.getElementById('testPDU02_content').innerHTML;"/><label for="testPDU02" id="testPDU02_content">0000021002040602448d159e240601fc08100003200010010910461c58d8b266a918</label><br />
      <a href="http://blog.csdn.net/arthur_zeng/article/details/7059783" target="_blank">Test data source</a>, error corrected<br /><br />

      <input type="radio" name="testPDU" id="testPDU03" onclick="document.getElementById('smsPDU').value = document.getElementById('testPDU03_content').innerHTML;"/><label for="testPDU03" id="testPDU03_content">0000021002040702C4CC484898580601FC08220003200010010A104F88307C61F106C41004060807311647090801000901000A01C0</label><br />
      <a href="http://blog.csdn.net/arthur_zeng/article/details/7059783" target="_blank">Test data source</a><br /><br />

      <input type="radio" name="testPDU" id="testPDU04" onclick="document.getElementById('smsPDU').value = document.getElementById('testPDU04_content').innerHTML;"/><label for="testPDU04" id="testPDU04_content">0000021002020702C4CC4848985806012408220003100010010A104F88307C61F106C41003061104161701270801000901000A01C0</label><br />
      <a href="http://blog.csdn.net/arthur_zeng/article/details/7059783" target="_blank">Test data source</a><br /><br />

      <input type="radio" name="testPDU" id="testPDU05" onclick="document.getElementById('smsPDU').value = document.getElementById('testPDU05_content').innerHTML;"/><label for="testPDU05" id="testPDU05_content">00000210020207028CE95DCC65800601FC08150003168D3001061024183060800306101004044847</label><br />
      <a href="http://stackoverflow.com/questions/11182650/cdma-pdu-parsing-on-android" target="_blank">Test data source</a><br /><br />

      <input type="radio" name="testPDU" id="testPDU06" onclick="document.getElementById('smsPDU').value = document.getElementById('testPDU06_content').innerHTML;"/><label for="testPDU06" id="testPDU06_content">0000021002020702c54ce225a8a80601c0089d00031001e8018e2230018801780193108b09fb087b317b012b6a080162e38c8e63b422e07b65980162b942e872e4b3b4246f7a70500162e54bbf9a7053f67c7e3801729f544c0b108bb423918a75d00163317a70033b0ae07ce3e00162b943108bb4236b54158a71680162ff5a7283b42371c33b2b71c29dd80173108b09fb087b317c1a933cb80162b943659b6a0bb4227122e5c00306081229192611</label><br />
      <a href="http://blog.lytsing.org/archives/180.html" target="_blank">Test data source</a>, Unicode<br /><br />

      <input type="radio" name="testPDU" id="testPDU07" onclick="document.getElementById('smsPDU').value = document.getElementById('testPDU07_content').innerHTML;"/><label for="testPDU07" id="testPDU07_content">0000021002020702c54ce225a8a806014c084d00031001f8013e20f00190017801900162dfca7004b1acb1abb4239614c6700162963b2b12b9827ae310c001729f544c0b108bb423918a75d00163317a70029f52e07cf0f80306081229192616</label><br />
      <a href="http://blog.lytsing.org/archives/180.html" target="_blank">Test data source</a>, Unicode<br /><br />
    </div>
    <hr />
    PDU String <textarea id="smsPDU" /></textarea><br />
    <button onclick="Decode();">Decode</button>
    <div id="decoderResult"></div><br />
  </td>
  <td>
    <h1>Encoder Test</h1>
    Receiver <input type="text" id="smsReceiver" value="123456789"/>
    <br />(encoded by DTFM)<br /><br />
    Message <textarea id="smsMessage" />abc123*#</textarea><br />
    <!--
    Encoding <select id="smsEncoding">
      <option value="0">Octet</option>
      <option value="2" selected>7-bit ASCII</option>
      <option value="4">Unicode</option>
    </select><br />
    -->
    Prority <select id="smsPriority">
      <option value="0">normal</option>
      <option value="1">interactive</option>
      <option value="2">urgent</option>
      <option value="3">emergency</option>
    </select><br />
    <button onclick="Encode();">Encode</button>
    <div id="encoderResult"></div><br />
  </td></tr>
  <tr><td colspan="2">
    <h1>Debug Log</h1>
    <div id="debugLog"></div>
  </td></tr>
  </table>
</body>

</html>


/* Copyright 2012 Chuck Lee @ Mozilla Taiwan
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

function decoderOutput(msg) {
  document.getElementById("decoderResult").innerHTML =
    "delivery: " + msg.delivery + "<br />" +
    "deliveryStatus: " + msg.deliveryStatus + "<br />" +
    "sender: " + msg.sender + "<br />" +
    "receiver: " + msg.receiver + "<br />" +
    "timestamp: " + msg.timestamp + "<br />" +
    "messageClass: " + msg.messageClass + "<br />" +
    "body: " + msg.body;
}

function encoderOutput(msg) {
  document.getElementById("encoderResult").innerHTML = msg;
}

function Decode() {
  document.getElementById("debugLog").innerHTML = "";
  document.getElementById("decoderResult").innerHTML = "";
  Buf.processIncoming(document.getElementById("smsPDU").value);
  var message = CdmaPDUHelper.readMessage();
  decoderOutput(message);
  return;
}

function Encode() {
  document.getElementById("debugLog").innerHTML = "";
  document.getElementById("encoderResult").innerHTML = "";
  var options = {};
  options.address = document.getElementById("smsReceiver").value;
  options.body = document.getElementById("smsMessage").value;
  //Auto Detect
  //options.encoding = parseInt(document.getElementById("smsEncoding").options[document.getElementById("smsEncoding").selectedIndex].value, 10);
  options.priority = parseInt(document.getElementById("smsPriority").options[document.getElementById("smsPriority").selectedIndex].value, 10);
  options.timestamp = new Date();
  CdmaPDUHelper.sendSMS(options);
  return;
}

var DEBUG = true;
function debug(msg) {
  if (DEBUG) {
    document.getElementById("debugLog").innerHTML += (msg + "<br />");
  }
}

// Simulate Buf in ril_worker.js
var Buf = {
  incomingBytes: [],
  incomingBytesSize: 0,
  outgoingBytes: [],
  outgoingBytesSize: 0,
  outgoingPosition: [],

  readUint8: function readUint8() {
    return this.incomingBytes.shift();
  },

  readUint8Array: function readUint8Array(length) {
    var subArray = [];
    for(var i = 0; i < length; i++) {
      subArray.push(this.incomingBytes.shift());
    }
    return subArray;
  },

  // It's Little Endian
  readUint16: function readUint16() {
    return this.readUint8() | this.readUint8() << 8;
  },

  readUint32: function readUint32() {
    return this.readUint8()       | this.readUint8() <<  8 |
           this.readUint8() << 16 | this.readUint8() << 24;
  },

  readParcelSize: function readParcelSize() {
    return this.incomingBytesSize;
  },

  processIncoming: function processIncoming(incoming) {
    /*
    for(var i = 0; i < incoming.length; i++) {
      // Real Buf uses 2 bytes to represent 1 char, UNICODE?
      this.incomingBytes.push(incoming.charCodeAt(i));
      this.incomingBytes.push(0);
    }
    //*/
    this.incomingBytes = [];
    this.incomingBytesSize = 0;
    for(var i = 0; i < incoming.length; i+=2) {
      // Real Buf uses 2 bytes to represent 1 char, UNICODE?
      // this.incomingBytes.push(incoming.charCodeAt(i));
      this.incomingBytes.push(parseInt(incoming.substr(i, 2), 16));
    }
    this.incomingBytesSize = this.incomingBytes.length;
  },

  writeUint8: function writeUint8(value) {
    this.outgoingBytes.push(value & 0xFF);
  },

  // Little Endian
  writeUint16: function writeUint16(value) {
    this.writeUint8(value & 0xff);
    this.writeUint8((value >> 8) & 0xff);
  },

  writeUint32: function writeUint32(value) {
    this.writeUint8(value & 0xff);
    this.writeUint8((value >> 8) & 0xff);
    this.writeUint8((value >> 16) & 0xff);
    this.writeUint8((value >> 24) & 0xff);
  },

  newParcel: function newParcel(type, options) {
    this.outgoingBytes = [];
    this.outgoingBytesSize = 0;
    return;
  },

  sendParcel: function sendParcel() {
    var rawData = "";

    this.outgoingBytesSize = this.outgoingBytes.length;
    for (var i = 0; i < this.outgoingBytesSize; i++) {
      var hexString = this.outgoingBytes[i].toString(16);
      rawData += (hexString.length === 1 ? "0" : "") + hexString;
    }

    encoderOutput(rawData);

    this.outgoingBytes = [];
    this.outgoingBytesSize = 0;

    return;
  },

  getCurrentOutgoinPosition: function getCurrentOutgoinPosition() {
    return (this.outgoingBytes.length - 1);
  },

  writeUint8ToPosition: function writeUint8ToPosition(value, position) {
    this.outgoingBytes[position] = value & 0xFF;
  }
};

/*
 * Basic PDU I/O function for both GSM and CDMA, all read/write operation
 * are applied to Buf directly.
 */
var bitBuffer = {
  readCache: 0,
  readCacheSize: 0,
  readBuffer: [],
  readIndex: 0,
  writeCache: 0,
  writeCacheSize: 0,
  writeBuffer: [],

  // Max length is 32 because we use integer as read/write cache.
  // All read/write functions are implemented based on bitwise operation.
  readBits: function readBits(length) {
    if (length <= 0 || length > 32) {
      return null;
    }

    if (length > this.readCacheSize) {
      var bytesToRead = Math.ceil((length - this.readCacheSize) / 8);
      for(var i = 0; i < bytesToRead; i++) {
        this.readCache = (this.readCache << 8) | (this.readBuffer[this.readIndex++] & 0xFF);
        this.readCacheSize += 8;
      }
    }

    var bitOffset = (this.readCacheSize - length),
        resultMask = (1 << length) - 1,
        result = 0;

    result = (this.readCache >> bitOffset) & resultMask;
    this.readCacheSize -= length;

    return result;
  },

  writeBits: function writeBits(value, length) {
    if (length <= 0 || length > 32) {
      return;
    }

    var totalLength = length + this.writeCacheSize;

    // 8-byte cache not full
    if (totalLength < 8) {
      var valueMask = (1 << length) - 1;
      this.writeCache = (this.writeCache << length) | (value & valueMask);
      this.writeCacheSize += length;
      return;
    }

    // Deal with unaligned part
    if (this.writeCacheSize) {
      var mergeLength = 8 - this.writeCacheSize,
          valueMask = (1 << mergeLength) - 1;

      this.writeCache = (this.writeCache << mergeLength) | ((value >> (length - mergeLength)) & valueMask);
      this.writeBuffer.push(this.writeCache & 0xFF);
      length -= mergeLength;
    }

    // Aligned part, just copy
    this.writeCache = 0;
    this.writeCacheSize = 0;
    while (length >= 8) {
      length -= 8;
      this.writeBuffer.push((value >> length) & 0xFF);
    }

    // Rest part is saved into cache
    this.writeCacheSize = length;
    this.writeCache = value & ((1 << length) - 1);

    return;
  },

  // Drop what still in read cache and goto next 8-byte alignment.
  // There might be a better naming.
  nextOctetAlign: function nextOctetAlign() {
    this.readCache = 0;
    this.readCacheSize = 0;
  },

  // Flush current write cache to Buf with padding 0s.
  // There might be a better naming.
  flushWithPadding: function flushWithPadding() {
    if (this.writeCacheSize) {
      this.writeBuffer.push(this.writeCache << (8 - this.writeCacheSize));
    }
    this.writeCache = 0;
    this.writeCacheSize = 0;
  },

  startWrite: function startWrite(dataBuffer) {
    this.writeBuffer = dataBuffer;
    this.writeCache = 0;
    this.writeCacheSize = 0;
  },

  startRead: function startRead(dataBuffer) {
    this.readBuffer = dataBuffer;
    this.readCache = 0;
    this.readCacheSize = 0;
    this.readIndex = 0;
  },

  getWriteBufferSize: function getWriteBufferSize() {
    return this.writeBuffer.length;
  },

  overwriteWriteBuffer: function overwriteWriteBuffer(position, data) {
    var writeLength = data.length;
    if (writeLength + position >= this.writeBuffer.length) {
      writeLength = this.writeBuffer.length - position;
    }
    for (var i = 0; i < writeLength; i++) {
      this.writeBuffer[i] = data[i];
    }
  }
};
var pduHelper = {
  /*
   * Common helper function
   */
  BcdDecoder: function BcdDecoder() {
    return bitBuffer.readBits(4) * 10 +
            bitBuffer.readBits(4);
  },

  BcdEncoder: function BcdEncoder(value) {
    bitBuffer.writeBits((value / 10), 4);
    bitBuffer.writeBits((value % 10), 4);
  }
};

/*
 * CDMA SMS Parameter Chart
 * Mandatory is implemented first, then common optional, then others
 *
 * P2P:Point-to-Point
 * BCAST:Broadcast
 * ACK:Acknowledge
 *
 * MO:Mobile-Originated(Sender)
 * MT:Mobile-Termiated(Receiver)
 *
 * M:Mandatory, O:Optional, X:Unavailable
 *
 *                       P2P-MO   P2P-MT   BCAST   ACK-MO   ACK-MT
 * Teleservice ID           M        M       X        X        X
 * Service Category         O        O       M        X        X
 * Originating Address      X        M       X        X        X
 * Originating Subaddress   X        O       X        X        X
 * Destination Address      M        X       X        M        X
 * Destination Subaddress   O        X       X        O        O
 * Bearer Reply Option      O        O       X        X        X
 * Cause codes              X        X       X        M        M
 * Beaer Data               O        O       O        X        X
 */

/*
 * CDMA SMS Teleservice-Subparameter Chart
 * Mandatory is implemented first, then common optional, then others
 *
 * BCAST:Broadcast
 * CMT-91:IS-91 Extended Protocol Enhanced Services
 * WPT:Wireless Paging Teleservice
 * WMT:Wireless Messaging Teleservice
 * VMN:Voice Mail Notification
 * WAP:Wireless Application Protocol
 * WEMT:Wireless Enhanced Messaging Teleservice
 * SCPT:Service Category Programming Teleservice
 * CATPT:Card Application Toolkit Protocol Teleservice
 *
 * MO:Mobile-Originated(Sender)
 * MT:Mobile-Termiated(Receiver)
 * ACK:Acknowledge
 *
 * M:Mandatory, O:Optional, X:Unavailable, C:Conditional
 *
 *                                    BCAST  CMT-91  WPT  WPT  WMT  WMT
 *                                      MT     MT     MT   MO   MT   MO
 * Message Identifier                   M       M     M    M    M    M
 * User Data                            O       M     O    O    O    O
 * Message Center Time Stamp            O       X     O    X    O    O
 * Validity Period - Absolute           O       X     X    X    O    O
 * Validity Period - Relative           O       X     X    X    O    O
 * Deferred Delivery Time - Absolute    X       X     X    X    X    X
 * Deferred Delivery Time - Relative    X       X     X    X    X    X
 * Priority Indicator                   O       X     O    O    O    O
 * Privacy Indicator                    X       X     O    O    O    O
 * Reply Option                         X       X     O    O    O    O
 * Number of Messages                   X       X     O    X    O    O
 * Alert on Message Delivery            O       X     X    X    O    O
 * Language Indicator                   O       X     X    X    O    O
 * Call-Back Number                     O       X     O    O    O    O
 * Message Display Mode                 O       X     O    X    O    O
 * Multiple Encoding User Data          O       X     O    O    O    O
 * Message Deposit Index                X       X     O    O    O    O
 * Service Category Program Data        X       X     X    X    X    X
 * Service Category Program Results     X       X     X    X    X    X
 * Message Status                       X       X     X    X    X    X
 * T-P Failure Cause                    X       X     X    X    X    X
 * Enhanced VMX                         X       X     X    X    X    X
 * Enhanced VMX ACK                     X       X     X    X    X    X
 *
 *
 *                                     VMN  WAP  WAP  WEMT  WEMT  SCPT  SCPT
 *                                      MT   MT   MO   MT    MO    MT    MO
 * Message Identifier                   M    M    M    M     M     M     M
 * User Data                            O    M    M    M     M     X     X
 * Message Center Time Stamp            O    X    X    O     X     O     X
 * Validity Period - Absolute           X    X    X    O     O     X     X
 * Validity Period - Relative           X    X    X    O     O     X     X
 * Deferred Delivery Time - Absolute    X    X    X    X     O     X     X
 * Deferred Delivery Time - Relative    X    X    X    X     O     X     X
 * Priority Indicator                   O    X    X    O     O     X     X
 * Privacy Indicator                    O    X    X    O     O     X     X
 * Reply Option                         X    X    X    O     O     X     X
 * Number of Messages                   M    X    X    O     X     X     X
 * Alert on Message Delivery            X    X    X    O     O     X     X
 * Language Indicator                   X    X    X    O     O     X     X
 * Call-Back Number                     X    X    X    O     O     X     X
 * Message Display Mode                 X    X    X    O     X     X     X
 * Multiple Encoding User Data          O    X    X    O     O     X     X
 * Message Deposit Index                X    X    X    O     O     X     X
 * Service Category Program Data        X    X    X    X     X     M     X
 * Service Category Program Results     X    X    X    X     X     X     M
 * Message Status                       X    X    X    X     X     X     X
 * T-P Failure Cause                    X    X    X    X     X     X     X
 * Enhanced VMX                         O    X    X    X     X     X     X
 * Enhanced VMX ACK                     O    X    X    X     X     X     X
 *
 *
 *                                    CATPT  CATPT   CATPT
 *                                      MT     MO   USER ACK
 * Message Identifier                   M      M       M
 * User Data                            M      M       O
 * User Response Code                   X      X       O
 * Message Center Time Stamp            X      X       X
 * Validity Period - Absolute           X      X       X
 * Validity Period - Relative           X      X       X
 * Deferred Delivery Time - Absolute    X      X       X
 * Deferred Delivery Time - Relative    X      X       X
 * Priority Indicator                   X      X       X
 * Privacy Indicator                    X      X       X
 * Reply Option                         X      X       X
 * Number of Messages                   X      X       X
 * Alert on Message Delivery            X      X       X
 * Language Indicator                   X      X       X
 * Call-Back Number                     X      X       X
 * Message Display Mode                 X      X       X
 * Multiple Encoding User Data          X      X       X
 * Message Deposit Index                X      X       X
 * Service Category Program Data        X      X       X
 * Service Category Program Results     X      X       X
 * Message Status                       X      X       X
 * T-P Failure Cause                    X      X       X
 * Enhanced VMX                         X      X       X
 * Enhanced VMX ACK                     X      X       X
 *
 *
 *                                    SMS    SMS     SMS    SMS     SMS     SMS
 *                                   CANCEL  USER  DELIVER  READ  DELIVER  SUBMIT
 *                                           ACK     ACK    ACK    REPORT  REPORT
 * Message Identifier                  M      M       M      M        M       M
 * User Data                           X      O       O      O        O       O
 * User Response Code                  X      O       X      O        X       X
 * Message Center Time Stamp           X      O       O      X        X       X
 * Validity Period - Absolute          X      X       X      X        X       X
 * Validity Period - Relative          X      X       X      X        X       X
 * Deferred Delivery Time - Absolute   X      X       X      X        X       X
 * Deferred Delivery Time - Relative   X      X       X      X        X       X
 * Priority Indicator                  X      X       X      X        X       X
 * Privacy Indicator                   X      X       X      X        X       X
 * Reply Option                        X      X       X      X        X       X
 * Number of Messages                  X      X       X      X        X       X
 * Alert on Message Delivery           X      X       X      X        X       X
 * Language Indicator                  X      X       X      X        O       O
 * Call-Back Number                    X      X       X      X        X       X
 * Message Display Mode                X      X       X      X        X       X
 * Multiple Encoding User Data         X      O       O      O        O       O
 * Message Deposit Index               X      O       X      O        X       X
 * Service Category Program Data       X      X       X      X        X       X
 * Service Category Program Results    X      X       X      X        X       X
 * Message Status                      X      X       O      X        X       X
 * T-P Failure Cause                   X      X       X      X        C       C
 * Enhanced VMX                        X      X       X      X        X       X
 * Enhanced VMX ACK                    X      X       X      X        X       X
 */

var CdmaPDUHelper = {
  dtmfChars: "D1234567890*#ABC",

  /**
   * Entry point for SMS encoding
   */
  sendSMS: function sendSMS(options) {
    // Deal with some overhead
    Buf.newParcel();

    bitBuffer.startWrite(Buf.outgoingBytes);
    // Encoder
    this.writeMessage(options);

    // Send message
    Buf.sendParcel();
  },

  encodingDetection: function encodingDetection(options) {
    // Try to detect 7-bit ASCII or Unicode
    // FIXME: How to detect others?
    options.encoding = 2; // Default 7-bit ASCII, FIXME: set to system default?
    for (var i = 0; i < options.body.length; i++) {
      var charCode = options.body.charCodeAt(i);
      if (charCode > 0xFF) {
        options.encoding = 4; // Unicode Detected
        break;
      } else if (charCode > 0x7F) {
        options.encoding = 0; // Octet
      }
    }
  },

  writeMessage: function writeMessage(options) {
    this.encodingDetection(options);
    debug("Detected encoding: " + msgEncodingMap[options.encoding] + "(" + options.encoding + ")");

    // Point-to-Point
    bitBuffer.writeBits(0, 8);

    // Teleservice Index : 4098(CDMA Cellular Messaging Teleservice)
    this.smsParameterEncoder(0, 4098);

    // Destination Address
    this.smsParameterEncoder(4, {address: options.address, digitMode: 0, numberMode: 0});

    // Bearer Reply Option
    this.smsParameterEncoder(6, 63 /* FIXME : Message ID*/);

    // Bearer Data
    this.smsParameterEncoder(8, {
      msgId: {type: 2, id: 1},
      msgData: {encoding: options.encoding, body: options.body},
      timestamp: options.timestamp,
      priority: options.priority
    });
  },

  smsParameterEncoder: function smsParameterEncoder(id, data) {
    bitBuffer.writeBits(id, 8);
    switch(id) {
      case 0: // Teleservice Identify, C.S0015-B v2.0, 3.4.3.1
        bitBuffer.writeBits(2, 8);
        bitBuffer.writeBits(data, 16);
        break;
      case 1: // Service Category, C.S0015-B v2.0, 3.4.3.2
        bitBuffer.writeBits(2, 8);
        bitBuffer.writeBits(data, 16);
        break;
      case 2: // Originate Address, C.S0015-B v2.0, 3.4.3.3
        this.addressEncoder(data);
        break;
      case 3: // Originate Subaddress, C.S0015-B v2.0, 3.4.3.4
        // Unsupported
        break;
      case 4: // Destination Address,  C.S0015-B v2.0, 3.4.3.3
        this.addressEncoder(data);
        break;
      case 5: // Destination Subaddress, C.S0015-B v2.0, 3.4.3.4
        // Unsupported
        break;
      case 6: // Bearer Reply Option, C.S0015-B v2.0, 3.4.3.5
        bitBuffer.writeBits(1, 8);
        bitBuffer.writeBits(data, 6);
        bitBuffer.flushWithPadding();
        break;
      case 7: // Cause Code, C.S0015-B v2.0, 3.4.3.6
        if(data.errorClass === 0) {
          bitBuffer.writeBits(1, 8);
        } else {
          bitBuffer.writeBits(2, 8);
        }
        bitBuffer.writeBits(data.replySeq, 6);
        bitBuffer.writeBits(data.errorClass, 2);
        if (data.errorClass !== 0) {
          bitBuffer.writeBits(data.causeCode, 8);
        }
        break;
      case 8: // Bearer Data, C.S0015-B v2.0, 3.4.3.7, too complex so implement
              // in another decoder
        this.smsSubparameterEncoder(data);
        break;
      default:
        break;
    }
  },

  smsSubparameterEncoder: function smsSubparameterEncoder(data) {
    // Reserve one byte for size
    bitBuffer.writeBits(0, 8);
    var lengthPosition = Buf.getCurrentOutgoinPosition();

    for (key in data) {
      var parameter = data[key];
      switch (key) {
        case 'msgId':
          bitBuffer.writeBits(0, 8);
          bitBuffer.writeBits(3, 8);
          bitBuffer.writeBits(parameter.type || 0, 4);
          bitBuffer.writeBits(parameter.id || 0, 16);
          bitBuffer.writeBits(parameter.userHeader || 0, 1);
          // Add padding
          bitBuffer.flushWithPadding();
          break;
        case 'msgData':
          bitBuffer.writeBits(1, 8);
          this.messageEncoder(parameter);
          break;
        case 'timestamp':
          bitBuffer.writeBits(3, 8);
          this.timeStampEncoder(parameter);
          break;
        case 'priority':
          bitBuffer.writeBits(8, 8);
          bitBuffer.writeBits(1, 8);
          bitBuffer.writeBits(parameter, 2);
          // Add padding
          bitBuffer.flushWithPadding();
          break;
      }
    }

    // Calculate data size and refill
    var endPosition = Buf.getCurrentOutgoinPosition(),
        dataSize = endPosition - lengthPosition;
    Buf.writeUint8ToPosition(dataSize, lengthPosition);
  },

  messageEncoder: function messageEncoder(msgData) {
    // Reserve one byte for size
    bitBuffer.writeBits(0, 8);
    var lengthPosition = Buf.getCurrentOutgoinPosition();

    if (!msgData.encoding)
      msgData.encoding = 0;

    bitBuffer.writeBits(msgData.encoding, 5);

    if (msgData.encoding === 1 ||
        msgData.encoding === 10) {
        bitBuffer.writeBits(msgData.type || 0, 8);
    }

    var msgSize = msgData.body.length;
    bitBuffer.writeBits(msgSize, 8);

    for (var i = 0; i < msgSize; i++) {
      switch (msgData.encoding) {
        case 0: // Octec
          var msgDigit = msgData.body.charCodeAt(i);
          bitBuffer.writeBits(msgDigit, 8);
          break;
        case 1: // IS-91 Extended Protocol Message
          break;
        case 2: // 7-bit ASCII
          var msgDigit = msgData.body.charCodeAt(i);
          bitBuffer.writeBits(msgDigit, 7);
          break;
        case 3: // IA5
          break;
        case 4: // Unicode
         var msgDigit = msgData.body.charCodeAt(i);
          bitBuffer.writeBits(msgDigit, 16);
          break;
        case 5: // Shift-6 JIS
          break;
        case 6: // Korean
          break;
        case 7: // Latin/Hebrew
          break;
        case 8: // Latin
          break;
        case 10: // GSM 7-bit default alphabet
          break;
        default:
          break;
      };
    }

    // Add padding
    bitBuffer.flushWithPadding();

    // Calculate data size and refill
    var endPosition = Buf.getCurrentOutgoinPosition(),
        dataSize = endPosition - lengthPosition;
    Buf.writeUint8ToPosition(dataSize, lengthPosition);
  },

  addressEncoder: function addressEncoder(addressInfo) {
    // Reserve one byte for size
    bitBuffer.writeBits(0, 8);
    var lengthPosition = Buf.getCurrentOutgoinPosition();

    // Fill address options
    bitBuffer.writeBits(addressInfo.digitMode, 1);
    if ( addressInfo.numberMode !== null ) {
      bitBuffer.writeBits(addressInfo.numberMode, 1);
    }
    if (addressInfo.digitMode === 1) {
      bitBuffer.writeBits(addressInfo.numberType || 0, 3);
      if (addressInfo.numberMode === 0) {
        bitBuffer.writeBits(addressInfo.numberPlan || 0, 4);
      }
    }

    // Fill address size
    bitBuffer.writeBits(addressInfo.address.length, 8);

    // Fill address
    for (var i = 0; i < addressInfo.address.length; i++) {
      if (addressInfo.digitMode === 1) {
        var addressDigit = addressInfo.address.charCodeAt(i) & 0x7F;
        bitBuffer.writeBits(addressDigit, 8);
      } else {
        var addressDigit = this.dtmfChars.indexOf(addressInfo.address.charAt(i)) || 0;
        bitBuffer.writeBits(addressDigit, 4);
      }
    }

    // Add padding
    bitBuffer.flushWithPadding();

    // Calculate data size and refill
    var endPosition = Buf.getCurrentOutgoinPosition(),
        dataSize = endPosition - lengthPosition;
    Buf.writeUint8ToPosition(dataSize, lengthPosition);
  },

  timeStampEncoder: function timeStampEncoder(timestamp) {
    var year = timestamp.getFullYear(),
        month = timestamp.getMonth(),
        day = timestamp.getDate(),
        hour = timestamp.getHours(),
        min = timestamp.getMinutes(),
        sec = timestamp.getSeconds();

    if (year >= 1996 && year <= 1999) {
      year -= 1900;
    } else {
      year -= 2000;
    }

    bitBuffer.writeBits(6, 8);
    pduHelper.BcdEncoder(year);
    pduHelper.BcdEncoder(month);
    pduHelper.BcdEncoder(day);
    pduHelper.BcdEncoder(hour);
    pduHelper.BcdEncoder(min);
    pduHelper.BcdEncoder(sec);
  },

  /**
   * Entry point for SMS decoding
   */
  readMessage: function cdmaReadMessage() {
    // SMS message structure, C.S0015-B v2.0
    // Table 3.4-1, 3.4.2.1-1, 3.4.2.2-1, 3.4.2.3-1
    var msg = {
      // P2P:Point-to-Point, BCAST:Broadcast, ACK:Acknowledge
      // MO:Mobile-Originated(Sender), MT:Mobile-Termiated(Receiver)
      // M:Mandatory, O:Optional, X:Unavailable
      smsType:        null, // 0:Point-to-Point
                            // 1:Braodcast
                            // 2:Acknowledge

                            // P2P-MO   P2P-MT   BCAST   ACK-MO   ACK-MT
      tID:            null, //    M        M       X        X        X    Teleservice ID
      category:       null, //    O        O       M        X        X    Service Category
      originAddr:     null, //    X        M       X        X        X    Originating Address
      originSubAddr:  null, //    X        O       X        X        X    Originating Subaddress
      destAddr:       null, //    M        X       X        M        X    Destination Address
      destSubAddr:    null, //    O        X       X        O        O    Destination Subaddress
      bearerReplyOpt: {},   //    O        O       X        X        X    Bearer Reply Option
      causeCode:      {},   //    X        X       X        M        M    Cause codes
      bearerData:     {},   //    O        O       O        X        X    Beaer Data
    },
    pduSize = Buf.readParcelSize();

    bitBuffer.startRead(Buf.incomingBytes);

    // SMS Type, C.S0015-B v2.0, Table 3.4-1
    msg.smsType = bitBuffer.readBits(8);
    pduSize--;
    debug("Message Type :" + smsTypeMap[msg.smsType] + "(" + msg.smsType + ")");
    /*
    switch (msg.smsType) {
      case 0:
        // SMS Point-to-Point
        return null;
      case 1:
        // SMS Broadcast
        return null;
      case 2:
        // SMS Acknowledge
        return null
      default:
        return null;
    }
    //*/

    while (pduSize > 0) {
      var parameterId = bitBuffer.readBits(8);
      if (typeof parameterId === 'undefined')
        break;

      pduSize -= (this.smsParameterDecoder(parameterId, msg) + 2);
    }

    // Return same object structure as GSM
    return {delivery: msgTypeMap[msg.bearerData.msgType],
            deliveryStatus: msg.bearerData.responseCode || 0,
            sender: msg.originAddr, // + msg.originSubAddr
            receiver: msg.destAddr, // + msg.destSubAddr
            messageClass: priorityMap[(msg.bearerData.priority || 0)],
            timestamp: msg.bearerData.timestamp,
            body: msg.bearerData.message
            };
  },

  /*
   * forceNumberMode is used to assign numberMode, which only used
   * while decode callback address.
   */
  addressDecoder: function addressDecoder(forceNumberMode) {
    // C.S0015-B v2.0, 3.4.3.3
    var digitMode = bitBuffer.readBits(1),
        numberMode = forceNumberMode || bitBuffer.readBits(1),
        numberType = null,
        numberPlan = null,
        address = "";

    if (digitMode === 1) {
      numberType = bitBuffer.readBits(3);
      if (numberMode === 0) {
        numberPlan = bitBuffer.readBits(4);
      }
    }

    debug("[addressDecoder]")
    debug(" digitMode: " + digitMode + ", numberMode: " + numberMode +
          ", numberType: " + numberType + ", numberPlan: " + numberPlan);

    var numFields = bitBuffer.readBits(8);

    debug("numFields :" + numFields);

    for(var i = 0; i < numFields; i++) {
      var addrDigit = null;
      if (digitMode === 0) {
        // DTMF 4 bit encoding, C.S0005-D, 2.7.1.3.2.4-4
        addrDigit = bitBuffer.readBits(4);
        address += this.dtmfChars.charAt(addrDigit);
      } else {
        addrDigit = bitBuffer.readBits(8);
        if (numberMode === 0) {
          // ASCII represntation with MSB set to 0
          // Just treat as normal ASCII?
          address += String.fromCharCode(addrDigit);
        } else {
          if (numberType === 2) {
            // 8 bit ASCII
            address += String.fromCharCode(addrDigit);
          } else if (numberType === 1) {
            // Binary value of an octet of the address
            // FIXME: I don't known what it means
          }
        }
      }
    }

    debug("Address: " + address);

    return address;
  },

  timeStampDecoder: function timeStampDecoder() {
    var year = pduHelper.BcdDecoder(),
        month = pduHelper.BcdDecoder(),
        day = pduHelper.BcdDecoder(),
        hour = pduHelper.BcdDecoder(),
        min = pduHelper.BcdDecoder(),
        sec = pduHelper.BcdDecoder();

    if (year >= 96 && year <= 99) {
      year += 1900;
    } else {
      year += 2000;
    }

    return new Date(year, month, day, hour, min, sec, 0);
  },

  relativeTimeDecoder: function relativeTimeDecoder() {
    var relativeTime = bitBuffer.readBits(8);
    if (relativeTime === 248) {
      // Valid until registration area changes, discard if not registered
      return -2;
    } else if (relativeTime === 247) {
      // Valid until mobile becomes inactive, Deliver when mobile next becomes active
      return -1;
    } else if (relativeTime == 246) {
      // Immediate
      return 0;
    } else if (relativeTime == 245) {
      // How to represent forever?
      return 99999999999;
    } else if (relativeTime >= 197) {
      // (value - 192) weeks
      return (relativeTime - 192) * 604800;
    } else if (relativeTime >= 168) {
      // (value - 166) days
      return (relativeTime - 166) * 86400;
    } else if (relativeTime >= 144) {
      // 12 hr + (value - 143) * 30 min
      return (relativeTime - 143) * 1800 + 21600;
    }

    // (value + 1) * 5 min
    return (relativeTime + 1) * 300;
  },

  smsParameterDecoder: function(id, msg) {
    var length = bitBuffer.readBits(8);

    debug("===== SMS Parameter Decoder =====");
    debug("Parameter: " + parameterIdMap[id] + "(" + id + ")");
    debug("Length: " + length);

    switch(id) {
      case 0: // Teleservice Identify, C.S0015-B v2.0, 3.4.3.1
        if (length !== 2) {
          // Length must be 2
          return;
        }

        msg.tID = bitBuffer.readBits(16);
        debug("Value: " + msg.tID);
        break;
      case 1: // Service Category, C.S0015-B v2.0, 3.4.3.2
        if (length !== 2) {
          // Length must be 2
          return;
        }

        msg.category = bitBuffer.readBits(16);
        debug("Value: " + msg.category);
        break;
      case 2: // Originate Address, C.S0015-B v2.0, 3.4.3.3
        msg.originAddr = this.addressDecoder(false);
        break;
      case 3: // Originate Subaddress, C.S0015-B v2.0, 3.4.3.4
        // Unsupported
        break;
      case 4: // Destination Address,  C.S0015-B v2.0, 3.4.3.3
        msg.destAddr = this.addressDecoder(false);
        break;
      case 5: // Destination Subaddress, C.S0015-B v2.0, 3.4.3.4
        // Unsupported
        break;
      case 6: // Bearer Reply Option, C.S0015-B v2.0, 3.4.3.5
        msg.bearerReplyOpt.replySeq = bitBuffer.readBits(6);
        debug("Value: " + msg.bearerReplyOpt.replySeq);
        break;
      case 7: // Cause Code, C.S0015-B v2.0, 3.4.3.6
        msg.causeCode.replySeq = bitBuffer.readBits(6);
        msg.causeCode.errorClass = bitBuffer.readBits(2);
        if (msg.causeCode.errorClass !== 0) {
          msg.causeCode.causeCode = bitBuffer.readBits(8);
        }
        break;
      case 8: // Bearer Data, C.S0015-B v2.0, 3.4.3.7, too complex so implement
              // in another decoder
        msg.bearerData = this.smsSubparameterDecoder(length);
        break;
      default:
        break;
    };
    bitBuffer.nextOctetAlign();
    return length;
  },

  messageDecoder: function messageDecoder(encoding, msgSize) {
    var message = "",
        msgDigit = 0;
    while (msgSize > 0) {
      switch (encoding) {
        case 0: // Octec
          msgDigit = bitBuffer.readBits(8);
          message += String.fromCharCode(msgDigit);
          msgSize--;
          break;
        case 1: // IS-91 Extended Protocol Message
          break;
        case 2: // 7-bit ASCII
          msgDigit = bitBuffer.readBits(7);
          message += String.fromCharCode(msgDigit);
          msgSize--;
          break;
        case 3: // IA5
          break;
        case 4: // Unicode
          msgDigit = bitBuffer.readBits(16);
          message += String.fromCharCode(msgDigit);
          msgSize--;
          break;
        case 5: // Shift-6 JIS
          break;
        case 6: // Korean
          break;
        case 7: // Latin/Hebrew
          break;
        case 8: // Latin
          break;
        case 10: // GSM 7-bit default alphabet
          break;
        default:
          break;
      };
    }
    return message;
  },

  smsSubparameterDecoder: function smsSubparameterDecoder(dataBufSize) {
    var bearerData = {},
        remainBufSize = dataBufSize;  // In bytes
    while (remainBufSize > 0) {
      // Fixed header
      var id = bitBuffer.readBits(8),
          length = bitBuffer.readBits(8);

      remainBufSize -= (2 + length);

      debug("~~~~~ SMS Subparameter Decoder ~~~~~");
      debug("Parameter: " + subparameterIdMap[id] + "(" + id + ")");
      debug("Length: " + length);

      switch(id) {
        case 0: // Message Identifier, C.S0015-B v2.0, 4.5.1
          bearerData.msgType = bitBuffer.readBits(4);
          bearerData.msgId = bitBuffer.readBits(16);
          bearerData.userHeader = bitBuffer.readBits(1);
          debug("MSG Type: " + msgTypeMap[bearerData.msgType] + "(" + bearerData.msgType +
               "), MSG ID: " + bearerData.msgId + ", user header: " + bearerData.userHeader);
          break;
        case 1: // User Data, C.S0015-B v2.0, 4.5.2
          bearerData.msgEncoding = bitBuffer.readBits(5);
          if (bearerData.msgEncoding === 1 ||
              bearerData.msgEncoding === 10) {
              bearerData.userMsgType = bitBuffer.readBits(8);
          }

          debug("MSG Encoding: " + msgEncodingMap[bearerData.msgEncoding] +
               "(" + bearerData.msgEncoding + "), msgType: " + bearerData.userMsgType );

          // Decode message based on encoding
          var numFields = bitBuffer.readBits(8);
          debug("Text Length: " + numFields);
          bearerData.message = (bearerData.message || "") + this.messageDecoder(bearerData.msgEncoding, numFields);
          debug( "Message: \"" + bearerData.message + "\"");
          break;
        case 2: // User Response Code, C.S0015-B v2.0, 4.5.3
          bearerData.responseCode = bitBuffer.readBits(8);
          debug("Value: " + bearerData.responseCode);
          break;
        case 3: // Message Center Time Stamp, C.S0015-B v2.0, 4.5.4
          bearerData.timestamp = this.timeStampDecoder();
          debug("Value: " + bearerData.timestamp);
          break;
        case 4: // Validity Period – Absolute, C.S0015-B v2.0, 4.5.5
          bearerData.validityPeriodAbsolute = this.timeStampDecoder();
          debug("Value: " + bearerData.validityPeriodAbsolute);
          break;
        case 5: // Validity Period - Relative, C.S0015-B v2.0, 4.5.6
          // Transform to local time??
          bearerData.validityPeriodRelative = this.relativeTimeDecoder();
          v("Value: " + bearerData.validityPeriodRelative + " seconds");
          break;
        case 6: // Deferred Delivery Time - Absolute, C.S0015-B v2.0, 4.5.7
          bearerData.deliveryTimeAbsolute = this.timeStampDecoder();
          debug("Value: " + bearerData.deliveryTimeAbsolute);
          break;
        case 7: // Deferred Delivery Time - Relative, C.S0015-B v2.0, 4.5.8
          // Transform to local time??
          bearerData.deliveryTimeRelative = this.relativeTimeDecoder();
          debug("Value: " + bearerData.deliveryTimeRelative + " seconds");
          break;
        case 8: // Priority Indicator, C.S0015-B v2.0, 4.5.9
          bearerData.priority = bitBuffer.readBits(2);
          debug("Value: " + priorityMap[bearerData.priority] + "(" + bearerData.priority + ")" );
          break;
        case 9: // Privacy Indicator, C.S0015-B v2.0, 4.5.10
          bearerData.privacy = bitBuffer.readBits(2);
          v("Value: " + privacyMap[bearerData.privacy] + "(" + bearerData.privacy + ")" );
          break;
        case 10: // Reply Option, C.S0015-B v2.0, 4.5.11
          bearerData.userAck = bitBuffer.readBits(1);
          bearerData.deliverAck = bitBuffer.readBits(1);
          bearerData.readAck = bitBuffer.readBits(1);
          bearerData.deliverReport = bitBuffer.readBits(1);
          break;
        case 11: // Number of Messages, C.S0015-B v2.0, 4.5.12
          bearerData.msgNum = pduHelper.BcdDecoder(data);
          break;
        case 12: // Alert on Message Delivery, C.S0015-B v2.0, 4.5.13
          bearerData.alertPriority = bitBuffer.readBits(2);
          break;
        case 13: // Language Indicator, C.S0015-B v2.0, 4.5.14
          bearerData.languageIndex = bitBuffer.readBits(8);
          break;
        case 14: // Callback Number, C.S0015-B v2.0, 4.5.15
          bearerData.callbackNumber = this.addressDecoder(data, 0);
          break;
        case 15: // Message Display Mode, C.S0015-B v2.0, 4.5.16
          bearerData.msgDiplayMode = bitBuffer.readBits(2);
          break;
        case 16: // Multiple Encoding User Data, C.S0015-B v2.0, 4.5.17
          // FIXME: Not Tested
          while (true) {
            var msgEncoding = bitBuffer.readBits(5),
                numFields = bitBuffer.readBits(8);
            if (!msgEncoding) {
              break;
            }

            debug("Multi-part, MSG Encoding: " + msgEncoding + ", numFields: " + numFields );

            bearerData.message = (bearerData.message || "") + this.messageDecoder(msgEncoding, numFields);
            debug( "Message: \"" + bearerData.message + "\"");
          }
          break;
        case 17: // Message Deposit Index, C.S0015-B v2.0, 4.5.18
          bearerData.msgDepositIndex = bitBuffer.readBits(16);
          break;
        case 20: // Message Status, C.S0015-B v2.0, 4.5.21
          bearerData.msgErrorClass = bitBuffer.readBits(2);
          bearerData.msgStatuCode = bitBuffer.readBits(6);
          break;
        case 21: // TP-Failure Cause, C.S0015-B v2.0, 4.5.22
          bearerData.tpFailureCause = bitBuffer.readBits(8);
          break;
        default:
          // For other unimplemented subparameter, just ignore the data
          break;
      };
      bitBuffer.nextOctetAlign();
    }
  return bearerData;
  }
};

// String Mapping
var smsTypeMap = [
  "Point-to-Point",
  "Broadcast",
  "Acknowledge"
];

var priorityMap = [
  "normal",
  "interactive",
  "urgent",
  "emergency"
];

var privacyMap = [
  "Not restricted",
  "Restricted",
  "Confidential",
  "Secret"
];

var msgTypeMap = [
  "Reserved",
  "received", //"Deliver",
  "sent",     //"Submit",
  "Cancellation",
  "Deliever Acknowledge",
  "User Acknowledge",
  "Read Acknowledge",
  "Deliver Report",
  "Submit Report"
];

var parameterIdMap = [
  "Teleservice Identity",
  "Service Category",
  "Originating Address",
  "Originating Subaddress",
  "Destination Address",
  "Destination Subaddress",
  "Bearer Reply Option",
  "Cause Codes",
  "Bearer Data"
];

var subparameterIdMap = [
  "Message Identifier",
  "User Data",
  "User Response Code",
  "Message Center Time Stamp",
  "Validity Period – Absolute",
  "Validity Period – Relative",
  "Deferred Delivery Time – Absolute",
  "Deferred Delivery Time – Relative",
  "Priority Indicator",
  "Privacy Indicator",
  "Reply Option",
  "Number of Messages",
  "Alert on Message Delivery",
  "Language Indicator",
  "Call-Back Number",
  "Message Display Mode",
  "Multiple Encoding User Data",
  "Message Deposit Index",
  "Service Category Program Data",
  "Service Category Program Results",
  "Message Status",
  "TP-Failure Cause",
  "Enhanced VMN",
  "Enhanced VMN Ack"
];

var msgEncodingMap = [
  "Octet",
  "IS-91 Extended Protocol Message",
  "7-bit ASCII",
  "IA5",
  "UNICODE",
  "Shift-6 JIS",
  "Korean",
  "Latin/ Hebrew",
  "Latin",
  "GSM 7-bit default alphabet"
];

var GSM_7_BIT_DEFAULT_ALPHABET_TABLE =
  // 01.....23.....4.....5.....6.....7.....8.....9.....A.B.....C.....D.E.....F.....
    "@\u00a3$\u00a5\u00e8\u00e9\u00f9\u00ec\u00f2\u00c7\n\u00d8\u00f8\r\u00c5\u00e5"
  // 0.....12.....3.....4.....5.....6.....7.....8.....9.....A.....B.....C.....D.....E.....F.....
  + "\u0394_\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e\uffff\u00c6\u00e6\u00df\u00c9"
  // 012.34.....56789ABCDEF
  + " !\"#\u00a4%&'()*+,-./"
  // 0123456789ABCDEF
  + "0123456789:;<=>?"
  // 0.....123456789ABCDEF
  + "\u00a1ABCDEFGHIJKLMNO"
  // 0123456789AB.....C.....D.....E.....F.....
  + "PQRSTUVWXYZ\u00c4\u00d6\u00d1\u00dc\u00a7"
  // 0.....123456789ABCDEF
  + "\u00bfabcdefghijklmno"
  // 0123456789AB.....C.....D.....E.....F.....
  + "pqrstuvwxyz\u00e4\u00f6\u00f1\u00fc\u00e0";