diff --git a/README.md b/README.md index 4903812..436ad4e 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,13 @@ Feedback is very much welcome and please request features 🙂 [HeboTech.GsmApi](https://github.com/hbjorgo/GsmApi) is a REST API wrapping this library. ## Supported commands: -- Send SMS (text and PDU (GSM7bit, UCS2, ANSI)) +- Send SMS in text or PDU (GSM 7 bit or UCS2) format. +- Send concatenated SMS (message that spans over multiple SMSs) in PDU (GSM 7 bit or UCS2) format +- SMS supports emojies - List SMSs -- Read SMS (text and PDU) +- Read SMS (text or PDU (GSM 7 bit or UCS2) - Delete SMS -- Set SMS message format (text and PDU (GSM7bit, UCS2, ANSI)) +- Set SMS message format (text or PDU (GSM 7 bit or UCS2)) - Dial number - Answer incoming call - Hang up call @@ -93,7 +95,7 @@ if (simStatus == SimStatus.SIM_PIN) var smsTextFormatResult = await modem.SetSmsMessageFormatAsync(SmsTextFormat.Text); // Send SMS to the specified number -var smsReference = await modem.SendSMSAsync(new PhoneNumber("0123456789"), "Hello ATLib!"); +var smsReference = await modem.SendSmsInTextFormatAsync(new PhoneNumber("123456789"), "Hello ATLib!"); Console.WriteLine($"SMS Reference: {smsReference}"); ``` Because it relies on a stream, you can even control a modem over a network! Either use a network attached modem, or forward a modem serial port to a network port. diff --git a/src/HeboTech.ATLib.TestConsole/FunctionalityTest.cs b/src/HeboTech.ATLib.TestConsole/FunctionalityTest.cs index 1628c38..9ed155b 100644 --- a/src/HeboTech.ATLib.TestConsole/FunctionalityTest.cs +++ b/src/HeboTech.ATLib.TestConsole/FunctionalityTest.cs @@ -5,6 +5,7 @@ using HeboTech.ATLib.Modems.D_LINK; using HeboTech.ATLib.Parsers; using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace HeboTech.ATLib.TestConsole @@ -149,21 +150,24 @@ public static async Task RunAsync(System.IO.Stream stream, string pin) string smsMessage = Console.ReadLine(); Console.WriteLine("Sending SMS..."); - ModemResponse smsReference = null; switch (smsTextFormat) { case SmsTextFormat.PDU: - smsReference = await modem.SendSmsInPduFormatAsync(phoneNumber, smsMessage, smsCodingScheme); + IEnumerable> smsReferences = await modem.SendSmsInPduFormatAsync(phoneNumber, smsMessage, smsCodingScheme); + foreach (var smsReference in smsReferences) + Console.WriteLine($"SMS Reference: {smsReference}"); break; case SmsTextFormat.Text: - smsReference = await modem.SendSmsInTextFormatAsync(phoneNumber, smsMessage); + { + ModemResponse smsReference = await modem.SendSmsInTextFormatAsync(new PhoneNumber(phoneNumberString), smsMessage); + if (smsReference is not null) + Console.WriteLine($"SMS Reference: {smsReference}"); + } break; default: Console.WriteLine("Unsupported SMS text format"); break; } - if (smsReference is not null) - Console.WriteLine($"SMS Reference: {smsReference}"); } break; case ConsoleKey.R: diff --git a/src/HeboTech.ATLib.TestConsole/HeboTech.ATLib.TestConsole.csproj b/src/HeboTech.ATLib.TestConsole/HeboTech.ATLib.TestConsole.csproj index 0e37a85..85c2b17 100644 --- a/src/HeboTech.ATLib.TestConsole/HeboTech.ATLib.TestConsole.csproj +++ b/src/HeboTech.ATLib.TestConsole/HeboTech.ATLib.TestConsole.csproj @@ -2,7 +2,7 @@ Exe - net6.0;net48 + net7.0 @@ -10,7 +10,7 @@ - + diff --git a/src/HeboTech.ATLib.TestConsole/Program.cs b/src/HeboTech.ATLib.TestConsole/Program.cs index e6eedf0..2d4cbad 100644 --- a/src/HeboTech.ATLib.TestConsole/Program.cs +++ b/src/HeboTech.ATLib.TestConsole/Program.cs @@ -46,7 +46,7 @@ static async Task Main(string[] args) /* ######## UNCOMMENT THIS SECTION TO USE NETWORK SOCKET ######## */ //using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - //socket.Connect("192.168.1.81", 7000); + //socket.Connect("192.168.1.144", 7000); //Console.WriteLine("Network socket opened"); //Stream stream; //stream = new NetworkStream(socket); diff --git a/src/HeboTech.ATLib.Tests/CodingSchemes/AnsiTests.cs b/src/HeboTech.ATLib.Tests/CodingSchemes/AnsiTests.cs deleted file mode 100644 index 575ff49..0000000 --- a/src/HeboTech.ATLib.Tests/CodingSchemes/AnsiTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using HeboTech.ATLib.CodingSchemes; -using Xunit; - -namespace HeboTech.ATLib.Tests.PDU -{ - public class AnsiTests - { - [Theory] - [InlineData("A", "41")] - [InlineData("AB", "4142")] - [InlineData("ABC", "414243")] - [InlineData("Google", "476F6F676C65")] - [InlineData("SMS Rulz", "534D532052756C7A")] - [InlineData("Hello.", "48656C6C6F2E")] - [InlineData("This is testdata!", "5468697320697320746573746461746121")] - [InlineData("The quick brown fox jumps over the lazy dog", "54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F67")] - public void Encoder_returns_encoded_text(string encoded, string expected) - { - string result = Ansi.Encode(encoded); - - Assert.Equal(expected, result); - } - - [Theory] - [InlineData("41", "A")] - [InlineData("4142", "AB")] - [InlineData("414243", "ABC")] - [InlineData("476F6F676C65", "Google")] - [InlineData("534D532052756C7A", "SMS Rulz")] - [InlineData("48656C6C6F2E", "Hello.")] - [InlineData("5468697320697320746573746461746121", "This is testdata!")] - [InlineData("54686520717569636B2062726F776E20666F78206A756D7073206F76657220746865206C617A7920646F67", "The quick brown fox jumps over the lazy dog")] - public void Decoder_returns_decoded_text(string encoded, string expected) - { - string result = Ansi.Decode(encoded); - - Assert.Equal(expected, result); - } - } -} diff --git a/src/HeboTech.ATLib.Tests/CodingSchemes/Gsm7Tests.cs b/src/HeboTech.ATLib.Tests/CodingSchemes/Gsm7Tests.cs index 416ec62..859e965 100644 --- a/src/HeboTech.ATLib.Tests/CodingSchemes/Gsm7Tests.cs +++ b/src/HeboTech.ATLib.Tests/CodingSchemes/Gsm7Tests.cs @@ -1,10 +1,59 @@ using HeboTech.ATLib.CodingSchemes; +using System; using Xunit; namespace HeboTech.ATLib.Tests.PDU { public class Gsm7Tests { + [Theory] + [InlineData("41", "41")] + [InlineData("4142", "4121")] + [InlineData("54616461203A29", "D430390CD2A500")] + [InlineData("54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67", "54741914AFA7C76B9058FEBEBB41E6371EA4AEB7E173D0DB5E9683E8E832881DD6E741E4F719")] + public void Pack_returns_packed_bytes(string gsm7Bit, string expected) + { + byte[] result = Gsm7.Pack(Convert.FromHexString(gsm7Bit)); + + Assert.Equal(Convert.FromHexString(expected), result); + } + + [Theory] + [InlineData("41" , "41")] + [InlineData("4121", "4142")] + [InlineData("D430390CD2A500", "54616461203A29")] + [InlineData("54741914AFA7C76B9058FEBEBB41E6371EA4AEB7E173D0DB5E9683E8E832881DD6E741E4F719", "54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67")] + public void Unpack_returns_unpacked_bytes(string gsm7Bit, string expected) + { + byte[] result = Gsm7.Unpack(Convert.FromHexString(gsm7Bit)); + + Assert.Equal(Convert.FromHexString(expected), result); + } + + [Theory] + [InlineData("A", "41")] + [InlineData("AB", "4142")] + [InlineData("The quick brown fox jumps over the lazy dog", "54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67")] + [InlineData("Tada :)", "54616461203A29")] + public void EncodeToBytes_returns_encoded_bytes(string gsm7Bit, string expected) + { + byte[] result = Gsm7.EncodeToBytes(gsm7Bit.ToCharArray()); + + Assert.Equal(Convert.FromHexString(expected), result); + } + + [Theory] + [InlineData("41", "A")] + [InlineData("4142", "AB")] + [InlineData("54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f67", "The quick brown fox jumps over the lazy dog")] + [InlineData("54616461203A29", "Tada :)")] + public void DecodeFromBytes_returns_decoded_text(string gsm7Bit, string expected) + { + string result = Gsm7.DecodeFromBytes(Convert.FromHexString(gsm7Bit)); + + Assert.Equal(expected, result); + } + [Theory] [InlineData("A", "41")] [InlineData("AB", "4121")] @@ -12,30 +61,124 @@ public class Gsm7Tests [InlineData("Google", "C7F7FBCC2E03")] [InlineData("SMS Rulz", "D3E61424ADB3F5")] [InlineData("Hello.", "C8329BFD7601")] + [InlineData("Hello world", "C8329BFD06DDDF723619")] [InlineData("This is testdata!", "54747A0E4ACF41F4F29C4E0ED3C321")] [InlineData("The quick brown fox jumps over the lazy dog", "54741914AFA7C76B9058FEBEBB41E6371EA4AEB7E173D0DB5E9683E8E832881DD6E741E4F719")] [InlineData("Tada :)", "D430390CD2A500")] - public void Encoder_returns_encoded_text(string gsm7Bit, string expected) + [InlineData("hellohello", "E8329BFD4697D9EC37")] + [InlineData("Hi", "C834")] + public void Encode_and_pack_returns_encoded_text(string gsm7Bit, string expected) + { + byte[] result = Gsm7.Pack(Gsm7.EncodeToBytes(gsm7Bit)); + + Assert.Equal(Convert.FromHexString(expected), result); + } + + [Theory] + [InlineData("41", 0, "A")] + [InlineData("4121", 0, "AB")] + [InlineData("C834", 0, "Hi")] + [InlineData("41E110", 0, "ABC")] + [InlineData("C7F7FBCC2E03", 0, "Google")] + [InlineData("D430390CD2A500", 0, "Tada :)")] + [InlineData("D3E61424ADB3F5", 0, "SMS Rulz")] + [InlineData("C8329BFD7601", 0, "Hello.")] + [InlineData("C8329BFD06DDDF723619", 0, "Hello world")] + [InlineData("54747A0E4ACF41F4F29C4E0ED3C321", 0, "This is testdata!")] + [InlineData("54741914AFA7C76B9058FEBEBB41E6371EA4AEB7E173D0DB5E9683E8E832881DD6E741E4F719", 0, "The quick brown fox jumps over the lazy dog")] + [InlineData( + "986F79B90D4AC3E7F53688FC66BFE5A0799A0E0AB7CB741668FC76CFCB637A995E9783C2E4343C3D1FA7DD6750999DA6B340F33219447E83CAE9FABCFD2683E8E536FC2D07A5DDE334394DAEBBE9A03A1DC40E8BDFF232A84C0791DFECB7BC0C6A87CFEE3028CC4EC7EB6117A84A0795DDE936284C06B5D3EE741B642FBBD3E1360B14AFA7E7", 1, + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis")] + public void Unpack_and_decode_returns_decoded_text(string gsm7Bit, int paddingBits, string expected) { - string result = Gsm7.Encode(gsm7Bit); + byte[] unpacked = Gsm7.Unpack(Convert.FromHexString(gsm7Bit), paddingBits); + string result = Gsm7.DecodeFromBytes(unpacked); Assert.Equal(expected, result); } [Theory] - [InlineData("41", "A")] - [InlineData("4121", "AB")] - [InlineData("41E110", "ABC")] - [InlineData("C7F7FBCC2E03", "Google")] - [InlineData("D3E61424ADB3F5", "SMS Rulz")] - [InlineData("C8329BFD7601", "Hello.")] - [InlineData("54747A0E4ACF41F4F29C4E0ED3C321", "This is testdata!")] - [InlineData("54741914AFA7C76B9058FEBEBB41E6371EA4AEB7E173D0DB5E9683E8E832881DD6E741E4F719", "The quick brown fox jumps over the lazy dog")] - public void Decoder_returns_decoded_text(string gsm7Bit, string expected) + [InlineData("{", "1B28")] + [InlineData("{}", "1B281B29")] + [InlineData("()", "2829")] + public void EncodeToBytes_returns_encoded_bytes_with_default_extension_table(string gsm7Bit, string expected) + { + byte[] result = Gsm7.EncodeToBytes(gsm7Bit); + + Assert.Equal(Convert.FromHexString(expected), result); + } + + [Theory] + [InlineData("À", Gsm7.Extension.Portugese, Gsm7.Extension.Portugese, "14")] + [InlineData("Φ", Gsm7.Extension.Portugese, Gsm7.Extension.Portugese, "1B12")] + [InlineData("ΦΣ", Gsm7.Extension.Portugese, Gsm7.Extension.Portugese, "1B121B18")] + public void EncodeToBytes_returns_encoded_bytes_with_extension_table(string gsm7Bit, Gsm7.Extension singleShift, Gsm7.Extension lockingShift, string expected) + { + byte[] result = Gsm7.EncodeToBytes(gsm7Bit, singleShift, lockingShift); + + Assert.Equal(Convert.FromHexString(expected), result); + } + + [Theory] + [InlineData(new byte[] { 0x1B }, " ")] + [InlineData(new byte[] { 0x1B, 0x28 }, "{")] + [InlineData(new byte[] { 0x1B, 0x28, 0x1B, 0x29 }, "{}")] + [InlineData(new byte[] { 0x28, 0x29 }, "()")] + public void DecodeFromBytes_returns_decoded_text_with_default_extension_table(byte[] gsm7Bit, string expected) { - string result = Gsm7.Decode(gsm7Bit); + string result = Gsm7.DecodeFromBytes(gsm7Bit); Assert.Equal(expected, result); } + + [Theory] + [InlineData("14", Gsm7.Extension.Portugese, Gsm7.Extension.Portugese, "À")] + [InlineData("1B", Gsm7.Extension.Portugese, Gsm7.Extension.Portugese, " ")] + [InlineData("1B12", Gsm7.Extension.Portugese, Gsm7.Extension.Portugese, "Φ")] + public void DecodeFromBytes_returns_decoded_text_with_extension_table(string gsm7Bit, Gsm7.Extension singleShift, Gsm7.Extension lockingShift, string expected) + { + string result = Gsm7.DecodeFromBytes(Convert.FromHexString(gsm7Bit), singleShift, lockingShift); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("A", 0, "41")] + [InlineData("Hello world", 0, "C8329BFD06DDDF723619")] + [InlineData("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostru", 0, "CCB7BCDC06A5E1F37A1B447EB3DF72D03C4D0785DB653A0B347EBBE7E531BD4CAFCB4161721A9E9E8FD3EE33A8CC4ED359A079990C22BF41E5747DDE7E9341F4721BFE9683D2EE719A9C26D7DD74509D0E6287C56F791954A683C86FF65B5E06B5C36777181466A7E3F5B00B54A583CAEE741B142683DA6977BA0DB297DDE9709B058AD7D37390FB3DA7CBEB")] + [InlineData("Hello world", 1, "906536FB0DBABFE56C3200")] + [InlineData("Hi", 2, "20D300")] + [InlineData("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad minim veniam, quis", 1, "986F79B90D4AC3E7F53688FC66BFE5A0799A0E0AB7CB741668FC76CFCB637A995E9783C2E4343C3D4F8FD3EE33A8CC4ED359A079990C22BF41E5747DDE7E9341F4721BFE9683D2EE719A9C26D7DD74509D0E6287C56F791954A683C86FF65B5E06B5C36777181466A7E3F5B0AB4A0795DDE936284C06B5D3EE741B642FBBD3E1360B14AFA7E700")] + [InlineData(" nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolor", 1, "40EEF79C2EAF9341657C593E4ED3C3F4F4DB0DAAB3D9E1F6F80D6287C56F797A0E72A7E769509D0E0AB3D3F17A1A0E2AE341E53068FC6EB7DFE43768FC76CFCBF17A98EE22D6D37350B84E2F83D2F2BABC0C22BFD96F3928ED06C9CB7079195D7693CBF2341D947683EC6F761D4E0FD3CB207B999DA683CAF37919344EB3D9F53688FC66BFE500")] + [InlineData("e eu fugiat nulla pariatur.Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 1, "CAA0721D64AE9FD3613AC85D67B3C32078589E0ED3EB7257113F2EC3E9E5BA1C344FBBE9A0F7781C2E8FC374D0B80E4F93C3F4301DE47EBB4170F93B4D2EBBE92CD0BCEEA683D26ED0B8CE868741F17A1AF4369BD3E37418442ECFCBF2BA9B0E6ABFD9EC341D1476A7DBA03419549ED341ECB0F82DAFB75D00")] + public void Encoder_returns_encoded_text_with_padding(string gsm7Bit, int paddingBits, string expected) + { + byte[] result = Gsm7.Pack(Gsm7.EncodeToBytes(gsm7Bit), paddingBits); + + Assert.Equal(expected, BitConverter.ToString(result).Replace("-", "")); + } + + [Theory] + [InlineData("A")] + [InlineData("AB")] + [InlineData("ABC")] + [InlineData("Google")] + [InlineData("SMS Rulz")] + [InlineData("Hello.")] + [InlineData("Hello world")] + [InlineData("This is testdata!")] + [InlineData("The quick brown fox jumps over the lazy dog")] + [InlineData("Tada :)")] + [InlineData("hellohello")] + [InlineData("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud")] + public void Encode_decode_returns_original_text(string text) + { + byte[] encoded = Gsm7.EncodeToBytes(text); + byte[] packed = Gsm7.Pack(encoded); + byte[] decoded = Gsm7.Unpack(packed); + string result = Gsm7.DecodeFromBytes(decoded); + + Assert.Equal(text, result); + } } } diff --git a/src/HeboTech.ATLib.Tests/DTOs/PhoneNumberExtensionsTests.cs b/src/HeboTech.ATLib.Tests/DTOs/PhoneNumberExtensionsTests.cs deleted file mode 100644 index 6152bd9..0000000 --- a/src/HeboTech.ATLib.Tests/DTOs/PhoneNumberExtensionsTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using HeboTech.ATLib.DTOs; -using Xunit; - -namespace HeboTech.ATLib.Tests.DTOs -{ - public class PhoneNumberExtensionsTests - { - [Fact] - public void GetTypeOfNumber_returns_international() - { - PhoneNumber dut = new("+123456789"); - - Assert.Equal(TypeOfNumber.International, dut.GetTypeOfNumber()); - } - - [Fact] - public void GetTypeOfNumber_returns_national() - { - PhoneNumber dut = new("123456789"); - - Assert.Equal(TypeOfNumber.National, dut.GetTypeOfNumber()); - } - - [Fact] - public void GetNumberPlanIdentification_returns_ISDN() - { - PhoneNumber dut = new("+123456789"); - - Assert.Equal(NumberPlanIdentification.ISDN, dut.GetNumberPlanIdentification()); - } - - [Fact] - public void GetNumberPlanIdentification_returns_Unknown() - { - PhoneNumber dut = new("123456789"); - - Assert.Equal(NumberPlanIdentification.Unknown, dut.GetNumberPlanIdentification()); - } - } -} diff --git a/src/HeboTech.ATLib.Tests/DTOs/PhoneNumberTests.cs b/src/HeboTech.ATLib.Tests/DTOs/PhoneNumberTests.cs index b470c2f..f1c7c49 100644 --- a/src/HeboTech.ATLib.Tests/DTOs/PhoneNumberTests.cs +++ b/src/HeboTech.ATLib.Tests/DTOs/PhoneNumberTests.cs @@ -6,19 +6,54 @@ namespace HeboTech.ATLib.Tests.DTOs public class PhoneNumberTests { [Fact] - public void Number_property_is_set() + public void Properties_are_set() { - PhoneNumber dut = new("+123456789"); + PhoneNumber dut = new("1", "23456789"); - Assert.Equal("+123456789", dut.Number); + Assert.Equal("1", dut.CountryCode); + Assert.Equal("23456789", dut.NationalNumber); + } + + [Theory] + [InlineData("", "23456789", "23456789")] + [InlineData("1", "23456789", "+123456789")] + public void ToString_returns_number(string countryCode, string nationalNumber, string expected) + { + PhoneNumber dut = new(countryCode, nationalNumber); + + Assert.Equal(expected, dut.ToString()); + } + + [Fact] + public void GetTypeOfNumber_returns_international() + { + PhoneNumber dut = new("1", "23456789"); + + Assert.Equal(TypeOfNumber.International, dut.GetTypeOfNumber()); + } + + [Fact] + public void GetTypeOfNumber_returns_national() + { + PhoneNumber dut = new("123456789"); + + Assert.Equal(TypeOfNumber.National, dut.GetTypeOfNumber()); + } + + [Fact] + public void GetNumberPlanIdentification_returns_ISDN() + { + PhoneNumber dut = new("1", "23456789"); + + Assert.Equal(NumberPlanIdentification.ISDN, dut.GetNumberPlanIdentification()); } [Fact] - public void ToString_returns_number() + public void GetNumberPlanIdentification_returns_Unknown() { - PhoneNumber dut = new("+123456789"); + PhoneNumber dut = new("123456789"); - Assert.Equal("+123456789", dut.ToString()); + Assert.Equal(NumberPlanIdentification.Unknown, dut.GetNumberPlanIdentification()); } } } diff --git a/src/HeboTech.ATLib.Tests/DTOs/ValidityPeriodTests.cs b/src/HeboTech.ATLib.Tests/DTOs/ValidityPeriodTests.cs new file mode 100644 index 0000000..e6fb938 --- /dev/null +++ b/src/HeboTech.ATLib.Tests/DTOs/ValidityPeriodTests.cs @@ -0,0 +1,76 @@ +using HeboTech.ATLib.DTOs; +using HeboTech.ATLib.Extensions; +using System; +using System.Linq; +using Xunit; + +namespace HeboTech.ATLib.Tests.DTOs +{ + public class ValidityPeriodTests + { + [Fact] + public void NotPresent_is_correct() + { + ValidityPeriod dut = ValidityPeriod.NotPresent(); + + Assert.Equal(ValidityPeriodFormat.NotPresent, dut.Format); + Assert.Equal(Array.Empty(), dut.Value); + } + + [Theory] + [InlineData(0)] + [InlineData(127)] + [InlineData(255)] + public void Relative_is_correct(byte value) + { + ValidityPeriod dut = ValidityPeriod.Relative(value); + + Assert.Equal(ValidityPeriodFormat.Relative, dut.Format); + Assert.Equal(new byte[] { value }, dut.Value); + } + + [Theory] + [InlineData("2013-03-25 23:01:56 +07:00", new byte[] { 0x31, 0x30, 0x52, 0x32, 0x10, 0x65, 0x82 })] + public void Absolute_with_positive_timezone_is_correct(string dateTimeString, byte[] value) + { + DateTimeOffset dateTime = DateTimeOffset.Parse(dateTimeString); + ValidityPeriod dut = ValidityPeriod.Absolute(dateTime); + + Assert.Equal(ValidityPeriodFormat.Absolute, dut.Format); + Assert.Equal(value, dut.Value); + } + + [Theory] + [InlineData("2013-03-25 23:01:56 -07:00", new byte[] { 0x31, 0x30, 0x52, 0x32, 0x10, 0x65, 0x8A })] + public void Absolute_with_negative_timezone_is_correct(string dateTimeString, byte[] value) + { + DateTimeOffset dateTime = DateTimeOffset.Parse(dateTimeString); + ValidityPeriod dut = ValidityPeriod.Absolute(dateTime); + + Assert.Equal(ValidityPeriodFormat.Absolute, dut.Format); + Assert.Equal(value, dut.Value); + } + + [Theory] + [InlineData("2013-03-25 23:01:56 +07:00", "31305232106582")] + public void Absolute_with_positive_timezone_is_correct_as_string(string dateTimeString, string value) + { + DateTimeOffset dateTime = DateTimeOffset.Parse(dateTimeString); + ValidityPeriod dut = ValidityPeriod.Absolute(dateTime); + + Assert.Equal(ValidityPeriodFormat.Absolute, dut.Format); + Assert.Equal(value, String.Join("", dut.Value.Select(x => x.ToString("X2")))); + } + + [Theory] + [InlineData("2013-03-25 23:01:56 -07:00", "3130523210658A")] + public void Absolute_with_negative_timezone_is_correct_as_string(string dateTimeString, string value) + { + DateTimeOffset dateTime = DateTimeOffset.Parse(dateTimeString); + ValidityPeriod dut = ValidityPeriod.Absolute(dateTime); + + Assert.Equal(ValidityPeriodFormat.Absolute, dut.Format); + Assert.Equal(value, String.Join("", dut.Value.Select(x => x.ToString("X2")))); + } + } +} diff --git a/src/HeboTech.ATLib.Tests/Extensions/BcdHelperTests.cs b/src/HeboTech.ATLib.Tests/Extensions/BcdHelperTests.cs new file mode 100644 index 0000000..0b1bdc0 --- /dev/null +++ b/src/HeboTech.ATLib.Tests/Extensions/BcdHelperTests.cs @@ -0,0 +1,38 @@ +using HeboTech.ATLib.Extensions; +using Xunit; + +namespace HeboTech.ATLib.Tests.Extensions +{ + public class BcdHelperTests + { + [Theory] + [InlineData(0x00, 0x00)] + [InlineData(0x01, 0x10)] + [InlineData(0x13, 0x31)] + [InlineData(0x98, 0x89)] + public void SwapNibbles_test(byte input, byte expected) + { + Assert.Equal(expected, input.SwapNibbles()); + } + + [Theory] + [InlineData(00, 0x00)] + [InlineData(01, 0x01)] + [InlineData(12, 0x12)] + [InlineData(98, 0x98)] + public void DecimalToBcd_test(byte input, byte expected) + { + Assert.Equal(expected, input.DecimalToBcd()); + } + + [Theory] + [InlineData(0x00, 00)] + [InlineData(0x01, 01)] + [InlineData(0x12, 12)] + [InlineData(0x98, 98)] + public void BcdToDecimal_test(byte input, byte expected) + { + Assert.Equal(expected, input.BcdToDecimal()); + } + } +} diff --git a/src/HeboTech.ATLib.Tests/Extensions/StringExtensionsTests.cs b/src/HeboTech.ATLib.Tests/Extensions/StringExtensionsTests.cs new file mode 100644 index 0000000..4f33c5a --- /dev/null +++ b/src/HeboTech.ATLib.Tests/Extensions/StringExtensionsTests.cs @@ -0,0 +1,18 @@ +using HeboTech.ATLib.Extensions; +using Xunit; + +namespace HeboTech.ATLib.Tests.Extensions +{ + public class StringExtensionsTests + { + [Theory] + [InlineData("00", new byte[] { 0x00 })] + [InlineData("01", new byte[] { 0x01 })] + [InlineData("0001", new byte[] { 0x00, 0x01 })] + [InlineData("000102030405060708090A0B0C0D0E0F1011", new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11 })] + public static void ToByteArray_tests(string hexString, byte[] expected) + { + Assert.Equal(expected, hexString.ToByteArray()); + } + } +} diff --git a/src/HeboTech.ATLib.Tests/HeboTech.ATLib.Tests.csproj b/src/HeboTech.ATLib.Tests/HeboTech.ATLib.Tests.csproj index f41927f..b608095 100644 --- a/src/HeboTech.ATLib.Tests/HeboTech.ATLib.Tests.csproj +++ b/src/HeboTech.ATLib.Tests/HeboTech.ATLib.Tests.csproj @@ -1,7 +1,7 @@  - net6.0;net48 + net7.0 false @@ -10,10 +10,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/HeboTech.ATLib.Tests/PDU/PduTests.cs b/src/HeboTech.ATLib.Tests/PDU/PduTests.cs deleted file mode 100644 index 1acacea..0000000 --- a/src/HeboTech.ATLib.Tests/PDU/PduTests.cs +++ /dev/null @@ -1,59 +0,0 @@ -using HeboTech.ATLib.CodingSchemes; -using HeboTech.ATLib.DTOs; -using HeboTech.ATLib.PDU; -using System; -using Xunit; - -namespace HeboTech.ATLib.Tests.PDU -{ - public class PduTests - { - - [Theory] - [InlineData("56840182", "D430390C", Gsm7.DataCodingSchemeCode, true, "0011000882654810280000AA04D430390C")] - [InlineData("56840182", "D430390CD2A500", Gsm7.DataCodingSchemeCode, true, "0011000882654810280000AA07D430390CD2A500")] - public void Encode_SmsSubmit_test(string phoneNumber, string encodedMessage, byte dataCodingScheme, bool includeEmptySmscLength, string answer) - { - string encoded = Pdu.EncodeSmsSubmit(new PhoneNumber(phoneNumber), encodedMessage, dataCodingScheme, includeEmptySmscLength); - - Assert.Equal(answer, encoded); - } - - [Theory] - [InlineData("07917238010010F5040BC87238880900F10000993092516195800AE8329BFD4697D9EC37", "+27831000015", "27838890001", "99-03-29-15-16-59-+02", "hellohello")] - [InlineData("07911326040000F0040B911346610089F60000208062917314800CC8F71D14969741F977FD07", "+31624000000", "+31641600986", "02-08-26-19-37-41-+02", "How are you?")] - public void Decode_SmsDeliver_tests(string data, string serviceCenterNumber, string senderNumber, string timestamp, string message) - { -#if NETFRAMEWORK - SmsDeliver pduMessage = Pdu.DecodeSmsDeliver(data.AsSpan()); -#else - SmsDeliver pduMessage = Pdu.DecodeSmsDeliver(data); -#endif - - Assert.NotNull(pduMessage); - Assert.Equal(TypeOfNumber.International, pduMessage.ServiceCenterNumber.GetTypeOfNumber()); - Assert.Equal(serviceCenterNumber, pduMessage.ServiceCenterNumber.Number); - Assert.Equal(senderNumber, pduMessage.SenderNumber.Number); - Assert.Equal(timestamp, pduMessage.Timestamp.ToString("yy-MM-dd-HH-mm-ss-zz")); - Assert.Equal(message, pduMessage.Message); - } - - [Theory] - [InlineData("0011000B916407281553F80000AA0AE8329BFD4697D9EC37", "", "+46708251358", "hellohello")] - [InlineData("058178563412110008812143658700000B2B54741914AFA7C76B9058FEBEBB41E6371EA4AEB7E173D0DB5E9683E8E832881DD6E741E4F719", "87654321", "12345678", "The quick brown fox jumps over the lazy dog")] - [InlineData("0011000802231537180000AA0D5062154403D1CB68D03DED06", "", "32517381", "PDU 4 teh win")] - public void Decode_SmsSubmit_tests(string data, string serviceCenterNumber, string senderNumber, string message) - { -#if NETFRAMEWORK - SmsSubmit pduMessage = Pdu.DecodeSmsSubmit(data.AsSpan()); -#else - SmsSubmit pduMessage = Pdu.DecodeSmsSubmit(data); -#endif - - Assert.NotNull(pduMessage); - Assert.Equal(serviceCenterNumber, pduMessage.ServiceCenterNumber?.Number ?? ""); - Assert.Equal(senderNumber, pduMessage.SenderNumber.Number); - Assert.Equal(message, pduMessage.Message); - } - } -} diff --git a/src/HeboTech.ATLib.Tests/PDU/SmsDeliverDecoderTests.cs b/src/HeboTech.ATLib.Tests/PDU/SmsDeliverDecoderTests.cs new file mode 100644 index 0000000..74037af --- /dev/null +++ b/src/HeboTech.ATLib.Tests/PDU/SmsDeliverDecoderTests.cs @@ -0,0 +1,25 @@ +using HeboTech.ATLib.DTOs; +using HeboTech.ATLib.Extensions; +using HeboTech.ATLib.PDU; +using Xunit; + +namespace HeboTech.ATLib.Tests.PDU +{ + public class SmsDeliverDecoderTests + { + [Theory] + [InlineData("07917238010010F5040BC87238880900F10000993092516195800AE8329BFD4697D9EC37", "+27831000015", "27838890001", "99-03-29-15-16-59-+02", "hellohello")] + [InlineData("07911326040000F0040B911346610089F60000208062917314800CC8F71D14969741F977FD07", "+31624000000", "+31641600986", "02-08-26-19-37-41-+02", "How are you?")] + public void Decode_SmsDeliver_tests(string data, string serviceCenterNumber, string senderNumber, string timestamp, string message) + { + var bytes = data.ToByteArray(); + SmsDeliver pduMessage = SmsDeliverDecoder.Decode(bytes); + + Assert.NotNull(pduMessage); + Assert.Equal(serviceCenterNumber, pduMessage.ServiceCenterNumber.ToString()); + Assert.Equal(senderNumber, pduMessage.SenderNumber.ToString()); + Assert.Equal(timestamp, pduMessage.Timestamp.ToString("yy-MM-dd-HH-mm-ss-zz")); + Assert.Equal(message, pduMessage.Message); + } + } +} diff --git a/src/HeboTech.ATLib.Tests/PDU/SmsSubmitEncoderTests.cs b/src/HeboTech.ATLib.Tests/PDU/SmsSubmitEncoderTests.cs new file mode 100644 index 0000000..97be838 --- /dev/null +++ b/src/HeboTech.ATLib.Tests/PDU/SmsSubmitEncoderTests.cs @@ -0,0 +1,66 @@ +using HeboTech.ATLib.CodingSchemes; +using HeboTech.ATLib.DTOs; +using HeboTech.ATLib.PDU; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace HeboTech.ATLib.Tests.PDU +{ + public class SmsSubmitEncoderTests + { + + [Theory] + [InlineData("", "56840182", "Tada", CodingScheme.Gsm7, true, new string[] { "00010008A065481028000004D430390C" })] + [InlineData("", "56840182", "Tada :)", CodingScheme.Gsm7, true, new string[] { "00010008A065481028000007D430390CD2A500" })] + [InlineData("", "12345678", "A", CodingScheme.Gsm7, true, new string[] { "00010008A02143658700000141" })] + [InlineData("", "12345678", "A", CodingScheme.UCS2, true, new string[] { "00010008A0214365870008020041" })] + [InlineData("", "12345678", "A", CodingScheme.UCS2, false, new string[] { "010008A0214365870008020041" })] + [InlineData("", "12345678", "😀", CodingScheme.UCS2, true, new string[] { "00010008A021436587000804D83DDE00" })] + [InlineData("", "12345678", "😀😹📱📶📞", CodingScheme.UCS2, true, new string[] { "00010008A021436587000814D83DDE00D83DDE39D83DDCF1D83DDCF6D83DDCDE" })] + [InlineData("", "12345678", "A😀B😹C📱D📶E📞F", CodingScheme.UCS2, true, new string[] { "00010008A0214365870008200041D83DDE000042D83DDE390043D83DDCF10044D83DDCF60045D83DDCDE0046" })] + [InlineData( + "1", + "5125551234", + "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + CodingScheme.Gsm7, + true, + new string[] { + "0041000B915121551532F40000A00500030C0301986F79B90D4AC3E7F53688FC66BFE5A0799A0E0AB7CB741668FC76CFCB637A995E9783C2E4343C3D4F8FD3EE33A8CC4ED359A079990C22BF41E5747DDE7E9341F4721BFE9683D2EE719A9C26D7DD74509D0E6287C56F791954A683C86FF65B5E06B5C36777181466A7E3F5B0AB4A0795DDE936284C06B5D3EE741B642FBBD3E1360B14AFA7E700", + "0041000B915121551532F40000A00500030C030240EEF79C2EAF9341657C593E4ED3C3F4F4DB0DAAB3D9E1F6F80D6287C56F797A0E72A7E769509D0E0AB3D3F17A1A0E2AE341E53068FC6EB7DFE43768FC76CFCBF17A98EE22D6D37350B84E2F83D2F2BABC0C22BFD96F3928ED06C9CB7079195D7693CBF2341D947683EC6F761D4E0FD3CB207B999DA683CAF37919344EB3D9F53688FC66BFE500", + "0041000B915121551532F40000900500030C0303CAA0721D64AE9FD3613AC85D67B3C32078589E0ED3EB7257113F2EC3E9E5BA1C344FBBE9A0F7781C2E8FC374D0B80E4F93C3F4301DE47EBB4170F93B4D2EBBE92CD0BCEEA683D26ED0B8CE868741F17A1AF4369BD3E37418442ECFCBF2BA9B0E6ABFD9EC341D1476A7DBA03419549ED341ECB0F82DAFB75D00" + })] + + public void Encode_SmsSubmit_test(string countryCode, string subscriberNumber, string encodedMessage, CodingScheme dataCodingScheme, bool includeEmptySmscLength, string[] answer) + { + IEnumerable encoded = SmsSubmitEncoder.Encode( + new SmsSubmitRequest( + new PhoneNumber(countryCode, subscriberNumber), + encodedMessage, + dataCodingScheme) + { + IncludeEmptySmscLength = includeEmptySmscLength, + MessageReferenceNumber = 12 + }); + + Assert.Equal(answer, encoded.ToArray()); + } + + [Theory] + [InlineData("", "56840182", 39_016, CodingScheme.Gsm7, true)] // Max message length is 39015 characters + [InlineData("", "56840182", 17_086, CodingScheme.UCS2, true)] // Max message length is 17085 characters + public void Encode_SmsSubmit_message_too_long_test(string countryCode, string subscriberNumber, int characterCount, CodingScheme dataCodingScheme, bool includeEmptySmscLength) + { + var request = new SmsSubmitRequest( + new PhoneNumber(countryCode, subscriberNumber), + new string('a', characterCount), + dataCodingScheme) + { + IncludeEmptySmscLength = includeEmptySmscLength, + MessageReferenceNumber = 12 + }; + Assert.Throws(() => SmsSubmitEncoder.Encode(request).ToList()); + } + } +} diff --git a/src/HeboTech.ATLib.Tests/PDU/TpduTimeTests.cs b/src/HeboTech.ATLib.Tests/PDU/TpduTimeTests.cs new file mode 100644 index 0000000..0635713 --- /dev/null +++ b/src/HeboTech.ATLib.Tests/PDU/TpduTimeTests.cs @@ -0,0 +1,32 @@ +using HeboTech.ATLib.PDU; +using System; +using Xunit; + +namespace HeboTech.ATLib.Tests.PDU +{ + public class TpduTimeTests + { + [Theory] + [InlineData("2023.01.12 13:14:15 +02:00", new byte[] { 0x32, 0x10, 0x21, 0x31, 0x41, 0x51, 0x80 })] + [InlineData("2023.01.12 13:14:15 -02:00", new byte[] { 0x32, 0x10, 0x21, 0x31, 0x41, 0x51, 0x88 })] + public void EncodeTimestamp_returns_bytes_tests(string dateTime, byte[] expected) + { + DateTimeOffset timestamp = DateTimeOffset.Parse(dateTime); + byte[] encoded = TpduTime.EncodeTimestamp(timestamp); + + Assert.Equal(expected, encoded); + } + + [Theory] + [InlineData(new byte[] { 0x32, 0x10, 0x21, 0x31, 0x41, 0x51, 0x80 }, "2023.01.12 13:14:15 +02:00")] + [InlineData(new byte[] { 0x32, 0x10, 0x21, 0x31, 0x41, 0x51, 0x88 }, "2023.01.12 13:14:15 -02:00")] + public void DecodeTimestamp_returns_DateTimeOffset_tests(byte[] bytes, string expected) + { + DateTimeOffset timestamp = TpduTime.DecodeTimestamp(bytes); + + DateTimeOffset expectedTimestamp = DateTimeOffset.Parse(expected); + + Assert.Equal(expectedTimestamp, timestamp); + } + } +} diff --git a/src/HeboTech.ATLib.Tests/PDU/UdhTests.cs b/src/HeboTech.ATLib.Tests/PDU/UdhTests.cs new file mode 100644 index 0000000..3249b2a --- /dev/null +++ b/src/HeboTech.ATLib.Tests/PDU/UdhTests.cs @@ -0,0 +1,33 @@ +using HeboTech.ATLib.PDU; +using System; +using System.Linq; +using Xunit; + +namespace HeboTech.ATLib.Tests.PDU +{ + public class UdhTests + { + [Fact] + public void Empty_returns_empty_udh() + { + Udh result = Udh.Empty(); + + Assert.Equal(0, result.Length); + Assert.Equal(Array.Empty(), result.InformationElements); + } + + [Fact] + public void Parse_returns_udh() + { + Udh result = Udh.Parse(5, new byte[] { 0x00, 0x03, 0x02, 0x01 }); + + Assert.Equal(5, result.Length); + Assert.Single(result.InformationElements); + Assert.Equal(0x00, result.InformationElements.First().IEI); + Assert.Equal(0x03, result.InformationElements.First().Length); + Assert.Equal(0x03, result.InformationElements.First().Data[0]); + Assert.Equal(0x02, result.InformationElements.First().Data[1]); + Assert.Equal(0x01, result.InformationElements.First().Data[2]); + } + } +} diff --git a/src/HeboTech.ATLib/CodingSchemes/Ansi.cs b/src/HeboTech.ATLib/CodingSchemes/Ansi.cs deleted file mode 100644 index 1828de0..0000000 --- a/src/HeboTech.ATLib/CodingSchemes/Ansi.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Text; - -namespace HeboTech.ATLib.CodingSchemes -{ - /// - /// Encode / decode ANSI 8-bit strings - /// - public class Ansi - { - public const byte DataCodingSchemeCode = 0xF4; - - /// - /// Encode to ANSI 8-bit - /// - /// String to encode - /// ANSI encoded string - public static string Encode(string input) - { - byte[] bytes = Encoding.ASCII.GetBytes(input); - return BitConverter.ToString(bytes).Replace("-", ""); - } - - /// - /// Decode from ANSI 8-bit - /// - /// ANSI encoded string - /// Decoded string - public static string Decode(string input) - { - byte[] bytes = CodingHelpers.StringToByteArray(input); - return Encoding.ASCII.GetString(bytes); - } - } -} diff --git a/src/HeboTech.ATLib/CodingSchemes/CodingHelpers.cs b/src/HeboTech.ATLib/CodingSchemes/CodingHelpers.cs deleted file mode 100644 index 383d0d1..0000000 --- a/src/HeboTech.ATLib/CodingSchemes/CodingHelpers.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Linq; - -namespace HeboTech.ATLib.CodingSchemes -{ - internal class CodingHelpers - { - public static byte[] StringToByteArray(string hex) - { - return Enumerable.Range(0, hex.Length) - .Where(x => x % 2 == 0) - .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) - .ToArray(); - } - } -} diff --git a/src/HeboTech.ATLib/CodingSchemes/CodingScheme.cs b/src/HeboTech.ATLib/CodingSchemes/CodingScheme.cs index 7020b32..4a5e472 100644 --- a/src/HeboTech.ATLib/CodingSchemes/CodingScheme.cs +++ b/src/HeboTech.ATLib/CodingSchemes/CodingScheme.cs @@ -1,9 +1,8 @@ namespace HeboTech.ATLib.CodingSchemes { - public enum CodingScheme + public enum CodingScheme : byte { - Ansi, - Gsm7, - UCS2 + Gsm7 = 0x00, + UCS2 = 0x08, } } diff --git a/src/HeboTech.ATLib/CodingSchemes/Gsm7.cs b/src/HeboTech.ATLib/CodingSchemes/Gsm7.cs index 3a5f0f6..58b2827 100644 --- a/src/HeboTech.ATLib/CodingSchemes/Gsm7.cs +++ b/src/HeboTech.ATLib/CodingSchemes/Gsm7.cs @@ -1,89 +1,212 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Text; namespace HeboTech.ATLib.CodingSchemes { /// - /// Encode / decode GSM 7-bit strings + /// Encode / decode GSM 7-bit strings (GSM 03.38 or 3GPP 23.038) /// - public class Gsm7 + public static class Gsm7 { - public const byte DataCodingSchemeCode = 0x00; - - /// - /// Encode to GSM7 - /// - /// String to encode - /// GSM7 encoded string - public static string Encode(string text) + public enum Extension : byte { - return Encode(Encoding.ASCII.GetBytes(text)); + Default = 0x00, + Turkish = 0x01, + Spanish = 0x02, + Portugese = 0x03, + BengaliAndAssamese = 0x04, + Gujarati = 0x05, + Hindi = 0x06, + Kannada = 0x07, + Malayalam = 0x08, + Oriya = 0x09, + Punjabi = 0x0A, + Tamil = 0x0B, + Telugu = 0x0C, + Urdu = 0x0D, } - public static string Encode(byte[] data) + // ` is not a conversion, just a untranslatable letter + private static readonly Dictionary regularTable = new Dictionary() { - byte[] textBytes = data.Reverse().ToArray(); - bool[] bits = new bool[textBytes.Length * 7]; - for (int i = 0; i < textBytes.Length; i++) + { Extension.Default, "@£$¥èéùìòÇ`Øø`ÅåΔ_ΦΓΛΩΠΨΣΘΞ`ÆæßÉ !\"#¤%&'()*=,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑÜ`¿abcdefghijklmnopqrstuvwxyzäöñüà" }, + { Extension.Turkish, "@£$¥€éùıòÇLĞğCÅåΔ_ΦΓΛΩΠΨΣΘFEŞRßÉ !\"#¤%&'()ΞS,ş./0123456789*C<->?İABCDEFGHI:+L=NOPQRSTUVWXYJ;ÖMܧçabcdefghiZKlÑnopqrstuvwxyjÄömüà" }, + { Extension.Spanish, "@£$¥èéùìòÇ`Øø`ÅåΔ_ΦΓΛΩΠΨΣΘΞ`ÆæßÉ !\"#¤%&'()*=,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑÜ`¿abcdefghijklmnopqrstuvwxyzäöñüà" }, + { Extension.Portugese, "@£$¥êéúíóçLÔôCÁáΔ_ªÇÀ∞^\\€ÓFEÂRÊÉ !\"#º%&'()|S,â./0123456789*C<->?ÍABCDEFGHI:+L=NOPQRSTUVWXYJ;ÕMܧ~abcdefghiZKlÚnopqrstuvwxyjÃõmüà" }, + { Extension.BengaliAndAssamese, "◌◌◌অআইঈউঊঋLঌ`C`এঁংঃওঔকখগঘঙFEছRঝঞঐ``ঠডঢণত)(চS,জ.ন !ট3456789থC`ধফ?012যর`ল```:দসপ◌ঽ◌ভম◌◌◌◌``◌শ;`হ়◌ব◌◌ুূৃৄghে◌ষl◌◌্ািীcdefwxiৈ`ডোৌo" }, + { Extension.Gujarati, "◌◌◌અઆઇઈઉઊઋLઌઍC`એઁંઃઓઔકખગઘઙFEછRઝઞઐઑ`ઠડઢણત)(ચS,જ.ન !ટ3456789થC`ધફ?012યર`લળ`વ:દસપ◌ઽબભમ◌◌◌◌◌`◌શ;`હ઼◌◌◌◌ુૂૃૄૅhે◌ષl◌◌્ાિીcdefgxiૈ◌ૡોૌo" }, + { Extension.Hindi, "◌◌◌अआइईउऊऋLऌऍCऎएँंःओऔकखगघङFEछRझञऐऑऒठडढणत)(चS,ज.न !ट3456789थCऩधफ?012यरऱलळऴव:दसप◌ऽबभम◌◌◌◌◌◌◌श;◌ह़◌◌◌◌ुूृॄॅॆे◌षॊ◌◌्ािीcdefghiै◌lोौo" }, + { Extension.Kannada, "`ಂಃಅಆಇಈಉಊಋLಌ`Cಎಏಐ`ಒಓಔಕಖಗಘಙFEಛRಝಞ !ಟಠಪಢಣತ)(ಚS,ಜ.ನ0123456789ಥC`ಧಫ?ಬಭಮಯರಱಲಳ`ವ:ದಸಪ಼ಽಾಿೀುೂೃೄ`ೆೇಶ;ೊಹೌ್ೕabcdefghiೈಷlೋnopqrstuvwxyj`ೠmೢೣ" }, + { Extension.Malayalam, "`ംഃഅആഇഈഉഊഋLഌ`Cഎഏഐ`ഒഓഔകഖഗഘങFEഛRഝഞ !ടഠഡഢണത)(ചS,ജ.ന0123456789ഥC`ധഫ?ബഭമയരറലളഴവ:ദസപ`ഽാിീുൂൃൄ`െേശ;ൊഹൌ്ൗabcdefghiൈഷlോnopqrstuvwxyj`ൡmൣ൹" }, + { Extension.Oriya, "◌◌◌ଅଆଇଈଉଊଋLଌ`C`ଏଁଂଃଓଔକଖଗଘଙFEଛRଝଞଐ``ଠଡଢଣତ)(ଚS,ଜ.ନ !ଟ3456789ଥC`ଧଫ?012ଯର`ଲଳ`ଵ:ଦସପ◌ଽବଭମ◌◌◌ୄ``◌ଶ;`ହ଼◌◌◌◌ୁୂୃfghେ◌ଷl◌◌୍ାିୀcdevwxiୈ`ୠୋୌo" }, + { Extension.Punjabi, "◌◌◌ਅਆਇਈਉਊ`L``C`ਏਁਂਃਓਔਕਖਗਘਙFEਛRਝਞਐ``ਠਡਢਣਤ)(ਚS,ਜ.ਨ !ਟ3456789ਥC`ਧਫ?012ਯਰ`ਲਲ`ਵ:ਦਸਪ◌`ਬਭਮ◌◌``਼`◌ਸ;`ਹ਼◌◌◌◌ੁੂef`hੇ਼`l◌◌੍ਾਿੀcduvgxi◌`◌ੋੌo" }, + { Extension.Tamil, "`◌◌அஆஇஈஉஊ`L``Cஎஏஐஂஃஓஔக```ஙFE`R`ஞ `ஒ```ணத)(சS,ஜ.ந0!ட3456789`Cன``?`12யரறலளழவ:`ஸப``◌`ம◌◌```◌◌ஶ;◌ஹ◌◌ா◌◌ுூefgெே◌ஷொ◌ௌ்ௐிீcduvwhiை`lோno" }, + { Extension.Telugu, "◌◌◌అఆఇఈఉఊఋLఌ`CఎఏఁంఃఓఔకఖగఘఙFEఛRఝఞఐ`ఒఠడఢణత)(చS,జ.న !ట3456789థC`ధఫ?012యరఱలళ`వ:దసప`ఽబభమ◌◌◌◌`◌◌శ;◌హ◌◌◌◌◌ుూృౄgెే◌షొ◌ౌ్ాిీcdefwhiై`lోno" }, + { Extension.Urdu, "اآبٻڀپڦتۂٿLٹٽCٺټثجځڄڃڅچڇحخFEڌRډڊ !ڏڍذرڑړ)(دS,ڈ.ژ0123456789ڙCښږش?صضطظعفقکڪګ:زڱسمنںڻڼوۄەہھءیگ;◌ل◌◌◌abcdefghiېڳٍ◌ُٗٔqrstuvwxyjےlِno" }, + }; + private static readonly Dictionary extendedTable = new Dictionary() + { + { Extension.Default, "````````````````````^```````````````````{}`````\\````````````[~]`|````````````````````````````````````€``````````````````````````" }, + { Extension.Turkish, "````````````````````^```````````````````{}`````\\````````````[~]`|``````Ğ`İ`````````Ş```````````````ç`€`ğ`ı`````````ş````````````" }, + { Extension.Spanish, "`````````ç``````````^```````````````````{}`````\\````````````[~]`|Á```````Í`````Ó`````Ú```````````á```€```í`````ó`````ú``````````" }, + { Extension.Portugese, "`````ê```ç`Ôô`Áá``ΦΓ^ΩΠΨΣΘ`````Ê````````{}`````\\````````````[~]`|À```````Í`````Ó`````Ú`````ÃÕ````Â```€```í`````ó`````ú`````ãõ``â" }, + { Extension.BengaliAndAssamese, "@£$¥¿\"¤%&'`*+`-/<=>¡^¡_#*০১`২৩৪৫৬৭৮৯যৠৡ◌{}◌৲৳৴৵\\৶৷৸৹়``ৢ``ৣ`[~]`|ABC৺EF`HI`KLMNOPQRSDUVGXYJ`````````T€`W``Z`````````````````````" }, + { Extension.Gujarati, "@£$¥¿\"¤%&'`*+`-/<=>¡^¡_#*।॥`૦૧૨૩૪૫૬૭૮૯``{}`````\\````````````[~]`|ABCDEFGHIJKLMNOPQRSTUVWXYZ``````````€``````````````````````````" }, + { Extension.Hindi, "@£$¥¿\"¤%&'`*+`-/<=>¡^¡_#*।॥`०१२३४५६७८९◌◌{}◌◌कखग\\जडढफयॠ॒॑◌॰़़़॓॔`़़़़़Eॡ◌ॣIॱ`[~]O|ABCDUFॢHYJKLMN`PQRST€VGX`Z````````````W````````" }, + { Extension.Kannada, "@£$¥¿\"¤%&'`*+`-/<=>¡^¡_#*।॥`೦೧೨೩೪೫೬೭೮೯ೞೱ{}ೲ````\\````````````]~]`|ABCDEFGHIJKLMNOPQRSTUVWXYZ``````````€``````````````````````````" }, + { Extension.Malayalam, "@£$¥¿\"¤%&'`*+`-/<=>¡^¡_#*।॥`൦൧൨൩൪൫൬൭൮൯൰൱{}൲൳൴൵ൺ\\ൻർൽൾൿ```````[~]`-ABCDEFGHIJKLMNOPQRSTUVWXYZ``````````€``````````````````````````" }, + { Extension.Oriya, "@£$¥¿\"¤%&'`*+`-/<=>¡^¡_#*।॥`୦୧୨୩୪୫୬୭୮୯ଡଢ{}ୟ୰ୱ``\\``````଼଼````[~]`|ABCDE``HIJKLMNOPQRSTUFGXYZ``````````€VW````````````````````````" }, + { Extension.Punjabi, "@£$¥¿\"¤%&'`*+`-/<=>¡^¡_#*।॥`੦੧੨੩੪੫੬੭੮੯ਖਗ{}ਜੜਫ◌`\\``````਼਼``਼`਼ੵ]`|ABCDE``HI`K[~NOPQRSTUFGXYJ`LM```````€VW``Z`````````````````````" }, + { Extension.Tamil, "@£$¥¿\"¤%&'`*+`-/<=>¡^¡_#*।॥`௦௧௨௩௪௫௬௭௮௯௳௴{}௵௶௷௸௺\\````````````[~]`|ABCDEFGHIJKLMNOPQRSTUVWXYZ``````````€``````````````````````````" }, + { Extension.Telugu, "@£$¥¿\"¤%&'`*+`-/<=>¡^¡_#*```౦౧౨౩౪౫౬౭౮౯ౘౙ{}౸౹౺౻౼\\౽౾౿`````````[~]`|ABCDEFGHIJKLMNOPQRSTUVWXYZ`````````````````````````````````````" }, + { Extension.Urdu, "@£$¥¿\"¤%&'`*+`-/<=>¡^¡_#*؀؁`۰۱۲۳۴۵۶۷۸۹،؍{}؎؏◌◌◌\\◌◌؛؟ـ◌◌٫٬ٲٳۍؐؑؒ۔ؓؔBCDْ٘GHIJK[~]O|ARSTEFWXYZ`LMN`PQ```UV``````````````€``````````" }, + }; + + public static bool IsGsm7Compatible(IEnumerable text, Extension lockingShift = Extension.Default, Extension singleShift = Extension.Default) + { + string defaultString = regularTable[lockingShift]; + string extendedString = extendedTable[singleShift]; + + for (int i = 0; i < text.Count(); i++) { - for (int j = 0; j < 7; j++) - { - bits[i * 7 + j] = (textBytes[i] & (0x40 >> j)) != 0; - } + char c = text.ElementAt(i); + + int intGSMTable = defaultString.IndexOf(c); + if (intGSMTable != -1) + continue; + + int intExtendedTable = extendedString.IndexOf(c); + if (intExtendedTable == -1) + return false; } - byte[] octets = new byte[(int)Math.Ceiling(bits.Length / 8.0)]; - int offset = octets.Length * 8 - bits.Length; - int bitShift = 0; - for (int i = bits.Length - 1; i >= 0; i--) + return true; + } + + public static byte[] EncodeToBytes(IEnumerable text, Extension lockingShift = Extension.Default, Extension singleShift = Extension.Default) + { + string defaultString = regularTable[lockingShift]; + string extendedString = extendedTable[singleShift]; + + List byteGSMOutput = new List(); + + for (int i = 0; i < text.Count(); i++) { - octets[(i + offset) / 8] |= (byte)(bits[i] ? 0x01 << bitShift : 0x00); - bitShift++; - bitShift %= 8; + char c = text.ElementAt(i); + + int intGSMTable = defaultString.IndexOf(c); + if (intGSMTable != -1) + { + byteGSMOutput.Add((byte)intGSMTable); + continue; + } + + int intExtendedTable = extendedString.IndexOf(c); + if (intExtendedTable != -1) + { + byteGSMOutput.Add(27); + byteGSMOutput.Add((byte)intExtendedTable); + } } - octets = octets.Reverse().ToArray(); - string str = BitConverter.ToString(octets).Replace("-", ""); - return str; + return byteGSMOutput.ToArray(); } - /// - /// Decode from GSM7 - /// - /// GSM7 encoded string - /// Decoded string - public static string Decode(string strGsm7bit) + public static string DecodeFromBytes(IEnumerable bytes, Extension lockingShift = Extension.Default, Extension singleShift = Extension.Default) { - return Decode(CodingHelpers.StringToByteArray(strGsm7bit)); + string defaultString = regularTable[lockingShift]; + string extendedString = extendedTable[singleShift]; + + StringBuilder sb = new StringBuilder(bytes.Count()); + + bool isExtended = false; + for (int i = 0; i < bytes.Count(); i++) + { + byte b = bytes.ElementAt(i); + + if (b == 27) + { + if (i == bytes.Count() - 1) // If the ESC character is the last character for some reason - treat it as a space + { + sb.Append(' '); + continue; + } + isExtended = true; + continue; + } + + if (isExtended) + { + sb.Append(extendedString[b]); + isExtended = false; + continue; + } + + if (b < defaultString.Length) + { + sb.Append(defaultString[b]); + continue; + } + } + + return sb.ToString(); } - public static string Decode(byte[] data) + public static byte[] Pack(byte[] data, int paddingBits = 0) { - byte[] octets = data.Reverse().ToArray(); + // Array for all packed bits (n x 7) + BitArray packedBits = new BitArray((int)Math.Ceiling(data.Length * 7 / 8.0) * 8 + paddingBits); - bool[] bits = new bool[octets.Length * 8]; - for (int i = 0; i < octets.Length; i++) + // Loop through all characters + for (int i = 0; i < data.Length; i++) { - for (int j = 0; j < 8; j++) + // Only 7 bits in each byte is data + for (int j = 0; j < 7; j++) { - bits[i * 8 + j] = (octets[i] & (0x80 >> j)) != 0; + // For each 7 bits in each byte, add it to the bit array + int index = (i * 7) + j + paddingBits; + bool isSet = (data[i] & (1 << j)) != 0; + packedBits.Set(index, isSet); } } - byte[] septets = new byte[(int)Math.Floor(bits.Length / 7.0)]; - int offset = bits.Length - septets.Length * 7; - int bitShift = 0; - for (int i = bits.Length - 1; i >= 0; i--) + // Convert the bit array to a byte array + byte[] packed = new byte[(int)Math.Ceiling(packedBits.Length / 8.0)]; + packedBits.CopyTo(packed, 0); + + // Return the septets packed as octets + // If the last character is empty - skip it + //if (packed[^1] == 0) + // return packed[..^1]; + return packed; + } + + public static byte[] Unpack(byte[] data, int paddingBits = 0) + { + BitArray packedBits = new BitArray(data); + packedBits.Length += paddingBits; + packedBits.RightShift(paddingBits); + byte[] unpacked = new byte[packedBits.Length / 7]; + + byte value = 0; + for (int i = 0; i < unpacked.Length * 7; i += 7) { - septets[(i - offset) / 7] |= (byte)(bits[i] ? 0x01 << bitShift : 0x00); - bitShift++; - bitShift %= 7; + for (int j = 0; j < 7; j++) + { + value |= packedBits[i + j] ? (byte)(1 << j) : (byte)(0 << j); + } + unpacked[i / 7] = value; + value = 0; } - septets = septets.Reverse().ToArray(); - string str = Encoding.ASCII.GetString(septets); - return str; + // If the last character is empty - skip it. + // It means that one bit of the last octet was used by the last character and the last 7 bits weren't used + if (unpacked[^1] == 0) + return unpacked[..^1]; + return unpacked; } } } diff --git a/src/HeboTech.ATLib/CodingSchemes/UCS2.cs b/src/HeboTech.ATLib/CodingSchemes/UCS2.cs index 922fd82..b45078f 100644 --- a/src/HeboTech.ATLib/CodingSchemes/UCS2.cs +++ b/src/HeboTech.ATLib/CodingSchemes/UCS2.cs @@ -1,6 +1,6 @@ -using System; +using HeboTech.ATLib.Extensions; +using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Text; @@ -12,7 +12,7 @@ namespace HeboTech.ATLib.CodingSchemes /// public class UCS2 { - public const byte DataCodingSchemeCode = 0x08; + public const CodingScheme DataCodingSchemeCode = CodingScheme.UCS2; /// /// Encode to UCS2 @@ -25,6 +25,11 @@ public static string Encode(string input) return BitConverter.ToString(bytes).Replace("-", ""); } + public static byte[] EncodeToBytes(char[] input) + { + return Encoding.BigEndianUnicode.GetBytes(input); + } + /// /// Decode from UCS2 /// @@ -32,8 +37,13 @@ public static string Encode(string input) /// Decoded string public static string Decode(string input) { - IEnumerable bytes = CodingHelpers.StringToByteArray(input); + IEnumerable bytes = input.ToByteArray(); return Encoding.BigEndianUnicode.GetString(bytes.ToArray()); } + + public static string Decode(IEnumerable input) + { + return Encoding.BigEndianUnicode.GetString(input.ToArray()); + } } } diff --git a/src/HeboTech.ATLib/DTOs/PhoneNumber.cs b/src/HeboTech.ATLib/DTOs/PhoneNumber.cs index 1c00b48..a57a215 100644 --- a/src/HeboTech.ATLib/DTOs/PhoneNumber.cs +++ b/src/HeboTech.ATLib/DTOs/PhoneNumber.cs @@ -1,17 +1,80 @@ -namespace HeboTech.ATLib.DTOs +using System; +using System.Linq; + +namespace HeboTech.ATLib.DTOs { + /// + /// Phone Number + /// public class PhoneNumber { - public PhoneNumber(string number) + /// + /// Initializes a new instance + /// + /// National Number (National Destination Code and Subscriber Number) + public PhoneNumber(string nationalNumber) + : this(string.Empty, nationalNumber) + { + } + + /// + /// Initializes a new instance + /// + /// Country Code + /// National Number (National Destination Code and Subscriber Number) + /// + public PhoneNumber(string countryCode, string nationalNumber) + { + if (!countryCode.All(char.IsDigit)) + throw new ArgumentException("Must be numeric only", nameof(countryCode)); + if (!nationalNumber.All(char.IsDigit)) + throw new ArgumentException("Must be numeric only", nameof(nationalNumber)); + if (countryCode.Length + nationalNumber.Length > 15) + throw new ArgumentException("Total phone number length cannot exceed 15 characters"); + + CountryCode = countryCode; + NationalNumber = nationalNumber; + } + + /// + /// Country code + /// + public string CountryCode { get; } + + /// + /// National number + /// + public string NationalNumber { get; } + + /// + /// Get Type Of Number (TON) + /// + /// Type Of Number (TON) + public TypeOfNumber GetTypeOfNumber() { - Number = number; + if (CountryCode != string.Empty) + return TypeOfNumber.International; + else + return TypeOfNumber.National; } - public string Number { get; } + /// + /// Get Number Plan Identification (NPI) + /// + /// Number Plan Identification (NPI) + public NumberPlanIdentification GetNumberPlanIdentification() + { + if (CountryCode != string.Empty) + return NumberPlanIdentification.ISDN; + else + return NumberPlanIdentification.Unknown; + } public override string ToString() { - return Number; + if (CountryCode != string.Empty) + return $"+{CountryCode}{NationalNumber}"; + return NationalNumber.ToString(); } } } diff --git a/src/HeboTech.ATLib/DTOs/PhoneNumberDTO.cs b/src/HeboTech.ATLib/DTOs/PhoneNumberDTO.cs new file mode 100644 index 0000000..ff2f55b --- /dev/null +++ b/src/HeboTech.ATLib/DTOs/PhoneNumberDTO.cs @@ -0,0 +1,23 @@ +namespace HeboTech.ATLib.DTOs +{ + /// + /// Phone number received from the modem + /// + public class PhoneNumberDTO + { + public PhoneNumberDTO(string number) + { + Number = number; + } + + /// + /// The phone number received from the modem + /// + public string Number { get; } + + public override string ToString() + { + return Number; + } + } +} diff --git a/src/HeboTech.ATLib/DTOs/PhoneNumberExtensions.cs b/src/HeboTech.ATLib/DTOs/PhoneNumberExtensions.cs deleted file mode 100644 index 6983a43..0000000 --- a/src/HeboTech.ATLib/DTOs/PhoneNumberExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace HeboTech.ATLib.DTOs -{ - public static class PhoneNumberExtensions - { - public static TypeOfNumber GetTypeOfNumber(this PhoneNumber phoneNumber) - { -#if NETSTANDARD2_0 - if (phoneNumber.Number.StartsWith("+")) -#else - if (phoneNumber.Number.StartsWith('+')) -#endif - return TypeOfNumber.International; - else - return TypeOfNumber.National; - } - - public static NumberPlanIdentification GetNumberPlanIdentification(this PhoneNumber phoneNumber) - { -#if NETSTANDARD2_0 - if (phoneNumber.Number.StartsWith("+")) -#else - if (phoneNumber.Number.StartsWith('+')) -#endif - return NumberPlanIdentification.ISDN; - else - return NumberPlanIdentification.Unknown; - } - } -} diff --git a/src/HeboTech.ATLib/DTOs/Sms.cs b/src/HeboTech.ATLib/DTOs/Sms.cs index 96f94d0..f2ec98b 100644 --- a/src/HeboTech.ATLib/DTOs/Sms.cs +++ b/src/HeboTech.ATLib/DTOs/Sms.cs @@ -4,7 +4,7 @@ namespace HeboTech.ATLib.DTOs { public class Sms { - public Sms(SmsStatus status, PhoneNumber sender, DateTimeOffset receiveTime, string message) + public Sms(SmsStatus status, PhoneNumberDTO sender, DateTimeOffset receiveTime, string message) { Status = status; Sender = sender; @@ -13,7 +13,7 @@ public Sms(SmsStatus status, PhoneNumber sender, DateTimeOffset receiveTime, str } public SmsStatus Status { get; } - public PhoneNumber Sender { get; } + public PhoneNumberDTO Sender { get; } public DateTimeOffset ReceiveTime { get;} public string Message { get; } diff --git a/src/HeboTech.ATLib/DTOs/SmsDeliver.cs b/src/HeboTech.ATLib/DTOs/SmsDeliver.cs new file mode 100644 index 0000000..05e14dd --- /dev/null +++ b/src/HeboTech.ATLib/DTOs/SmsDeliver.cs @@ -0,0 +1,37 @@ +using System; + +namespace HeboTech.ATLib.DTOs +{ + /// + /// Data object for a received SMS + /// + public class SmsDeliver + { + public SmsDeliver(PhoneNumberDTO serviceCenterNumber, PhoneNumberDTO senderNumber, string message, DateTimeOffset timestamp) + { + ServiceCenterNumber = serviceCenterNumber; + SenderNumber = senderNumber; + Message = message; + Timestamp = timestamp; + } + + public SmsDeliver(PhoneNumberDTO serviceCenterNumber, PhoneNumberDTO senderNumber, string message, DateTimeOffset timestamp, int messageReferenceNumber, int totalNumberOfParts, int partNumber) + { + ServiceCenterNumber = serviceCenterNumber; + SenderNumber = senderNumber; + Message = message; + Timestamp = timestamp; + MessageReferenceNumber = messageReferenceNumber; + TotalNumberOfParts = totalNumberOfParts; + PartNumber = partNumber; + } + + public PhoneNumberDTO ServiceCenterNumber { get; } + public PhoneNumberDTO SenderNumber { get; } + public string Message { get; } + public DateTimeOffset Timestamp { get; } + public int MessageReferenceNumber { get; } + public int TotalNumberOfParts { get; } + public int PartNumber { get; } + } +} diff --git a/src/HeboTech.ATLib/DTOs/SmsSubmitRequest.cs b/src/HeboTech.ATLib/DTOs/SmsSubmitRequest.cs new file mode 100644 index 0000000..8adeb19 --- /dev/null +++ b/src/HeboTech.ATLib/DTOs/SmsSubmitRequest.cs @@ -0,0 +1,71 @@ +using HeboTech.ATLib.CodingSchemes; + +namespace HeboTech.ATLib.DTOs +{ + /// + /// Data object for submitting an SMS in PDU format. + /// + public class SmsSubmitRequest + { + /// + /// Creates a data object for submitting an SMS in PDU format. + /// Chooses GSM 7 bit encoding if the message content is compatible, otherwise, UCS2 is used. + /// No ValidityPeriod is set. + /// + /// The receiver phone number + /// The message to send + public SmsSubmitRequest( + PhoneNumber phoneNumber, + string message) + : this( + phoneNumber, + message, + Gsm7.IsGsm7Compatible(message.ToCharArray()) ? CodingScheme.Gsm7 : CodingScheme.UCS2) + { + } + + /// + /// Creates a data object for submitting an SMS in PDU format. + /// + /// The receiver phone number + /// The message to send + /// The coding scheme to use + public SmsSubmitRequest( + PhoneNumber phoneNumber, + string message, + CodingScheme codingScheme) + : this( + phoneNumber, + message, + codingScheme, + ValidityPeriod.NotPresent()) + { + } + + /// + /// Creates a data object for submitting an SMS in PDU format. + /// + /// The receiver phone number + /// The message to send + /// The coding scheme to use + /// The validity period to use + public SmsSubmitRequest( + PhoneNumber phoneNumber, + string message, + CodingScheme codingScheme, + ValidityPeriod validityPeriod) + { + PhoneNumber = phoneNumber; + Message = message; + CodingScheme = codingScheme; + ValidityPeriod = validityPeriod; + } + + public PhoneNumber PhoneNumber { get; } + public string Message { get; } + public CodingScheme CodingScheme { get; } + public bool IncludeEmptySmscLength { get; set; } + public byte MessageReferenceNumber { get; set; } + public ValidityPeriod ValidityPeriod { get; set; } + } +} diff --git a/src/HeboTech.ATLib/DTOs/SmsWithIndex.cs b/src/HeboTech.ATLib/DTOs/SmsWithIndex.cs index 26a3a65..ab2f161 100644 --- a/src/HeboTech.ATLib/DTOs/SmsWithIndex.cs +++ b/src/HeboTech.ATLib/DTOs/SmsWithIndex.cs @@ -4,7 +4,7 @@ namespace HeboTech.ATLib.DTOs { public class SmsWithIndex : Sms { - public SmsWithIndex(int index, SmsStatus status, PhoneNumber sender, DateTimeOffset receiveTime, string message) + public SmsWithIndex(int index, SmsStatus status, PhoneNumberDTO sender, DateTimeOffset receiveTime, string message) : base(status, sender, receiveTime, message) { Index = index; diff --git a/src/HeboTech.ATLib/DTOs/SupportedPreferredMessageStorages.cs b/src/HeboTech.ATLib/DTOs/SupportedPreferredMessageStorages.cs index 39a921a..f7bf9a4 100644 --- a/src/HeboTech.ATLib/DTOs/SupportedPreferredMessageStorages.cs +++ b/src/HeboTech.ATLib/DTOs/SupportedPreferredMessageStorages.cs @@ -18,17 +18,11 @@ public SupportedPreferredMessageStorages(IEnumerable storage1, IEnumerab public override string ToString() { -#if NETSTANDARD2_0 - return - $"Storage1: {string.Join(",", Storage1)}{Environment.NewLine}" + - $"Storage2: {string.Join(",", Storage2)}{Environment.NewLine}" + - $"Storage3: {string.Join(",", Storage3)}"; -#elif NETSTANDARD2_1_OR_GREATER + return $"Storage1: {string.Join(',', Storage1)}{Environment.NewLine}" + $"Storage2: {string.Join(',', Storage2)}{Environment.NewLine}" + $"Storage3: {string.Join(',', Storage3)}"; -#endif } } } diff --git a/src/HeboTech.ATLib/DTOs/ValidityPeriod.cs b/src/HeboTech.ATLib/DTOs/ValidityPeriod.cs new file mode 100644 index 0000000..3c64718 --- /dev/null +++ b/src/HeboTech.ATLib/DTOs/ValidityPeriod.cs @@ -0,0 +1,54 @@ +using HeboTech.ATLib.PDU; +using System; + +namespace HeboTech.ATLib.DTOs +{ + public enum ValidityPeriodFormat : byte + { + NotPresent = 0x00, + Enhanced = 0x01, + Relative = 0x02, + Absolute = 0x03 + } + + public class ValidityPeriod + { + private ValidityPeriod(ValidityPeriodFormat format, byte[] value) + { + Format = format; + Value = value; + } + + public ValidityPeriodFormat Format { get; } + public byte[] Value { get; } + + /// + /// No validity period + /// + /// + public static ValidityPeriod NotPresent() => new ValidityPeriod(ValidityPeriodFormat.NotPresent, Array.Empty()); + + /// + /// A realative validity period. + /// + /// + /// Value 0-143: (Value + 1) x 5 minutes. Possible values: 5, 10, 15 minutes ... 11:55, 12:00 hours + /// Value 144-167: (12 + (Value - 143) / 2) hours. Possible values: 12:30, 13:00, ... 23:30, 24:00 hours + /// Value 168-196: (Value - 166) days. Possible values: 2, 3, 4, ... 30 days + /// Value 197-255: (Value - 192) weeks. Possible values: 5, 6, 7, ... 63 weeks + /// + /// + public static ValidityPeriod Relative(byte value) => new ValidityPeriod(ValidityPeriodFormat.Relative, new byte[] { value }); + + /// + /// An absolute validity period + /// + /// The date and time the validity expires + /// + public static ValidityPeriod Absolute(DateTimeOffset value) + { + byte[] encoded = TpduTime.EncodeTimestamp(value); + return new ValidityPeriod(ValidityPeriodFormat.Absolute, encoded); + } + } +} diff --git a/src/HeboTech.ATLib/Events/MissedCallEventArgs.cs b/src/HeboTech.ATLib/Events/MissedCallEventArgs.cs index 64b76a3..380a478 100644 --- a/src/HeboTech.ATLib/Events/MissedCallEventArgs.cs +++ b/src/HeboTech.ATLib/Events/MissedCallEventArgs.cs @@ -13,11 +13,7 @@ public MissedCallEventArgs(string time, string phoneNumber) public static MissedCallEventArgs CreateFromResponse(string response) { -#if NETSTANDARD2_0 - string[] split = response.Split(new char[] { ' ' }, 3); -#elif NETSTANDARD2_1_OR_GREATER string[] split = response.Split(' ', 3); -#endif return new MissedCallEventArgs(split[1], split[2]); } } diff --git a/src/HeboTech.ATLib/Extensions/BcdHelper.cs b/src/HeboTech.ATLib/Extensions/BcdHelper.cs new file mode 100644 index 0000000..cd937ac --- /dev/null +++ b/src/HeboTech.ATLib/Extensions/BcdHelper.cs @@ -0,0 +1,23 @@ +namespace HeboTech.ATLib.Extensions +{ + internal static class BcdHelper + { + // 0x28 => 0x82 + // 0x35 => 0x53 + // etc. + public static byte SwapNibbles(this byte value) + { + return (byte)((value & 0x0F) << 4 | (value & 0xF0) >> 4); + } + + public static byte DecimalToBcd(this byte value) + { + return (byte)((value / 10 << 4) | (value % 10)); + } + + public static byte BcdToDecimal(this byte value) + { + return (byte)(((value & 0xF0) >> 4) * 10 + (value & 0x0F)); + } + } +} diff --git a/src/HeboTech.ATLib/Extensions/EnumExtensions.cs b/src/HeboTech.ATLib/Extensions/EnumExtensions.cs index b7e323d..2aed8db 100644 --- a/src/HeboTech.ATLib/Extensions/EnumExtensions.cs +++ b/src/HeboTech.ATLib/Extensions/EnumExtensions.cs @@ -3,7 +3,7 @@ namespace HeboTech.ATLib.Extensions { - public static class EnumExtensions + internal static class EnumExtensions { public static string GetDescription(this Enum enumerationValue) { diff --git a/src/HeboTech.ATLib/Extensions/StringExtensions.cs b/src/HeboTech.ATLib/Extensions/StringExtensions.cs new file mode 100644 index 0000000..e78e83b --- /dev/null +++ b/src/HeboTech.ATLib/Extensions/StringExtensions.cs @@ -0,0 +1,16 @@ +using System; +using System.Linq; + +namespace HeboTech.ATLib.Extensions +{ + internal static class StringExtensions + { + public static byte[] ToByteArray(this string hexString) + { + return Enumerable.Range(0, hexString.Length) + .Where(x => x % 2 == 0) + .Select(x => Convert.ToByte(hexString.Substring(x, 2), 16)) + .ToArray(); + } + } +} diff --git a/src/HeboTech.ATLib/HeboTech.ATLib.csproj b/src/HeboTech.ATLib/HeboTech.ATLib.csproj index 0a76bc1..6adb44b 100644 --- a/src/HeboTech.ATLib/HeboTech.ATLib.csproj +++ b/src/HeboTech.ATLib/HeboTech.ATLib.csproj @@ -1,31 +1,35 @@  - - netstandard2.1;netstandard2.0 - HeboTech - HeboTech ATLib - 6.0.2 - HeboTech.ATLib - atcommand at command gsm sms hayes simcom d-link tp-link dwm-222 ma260 sim5320 - AT command library that makes it easy to communicate with modems. - https://github.com/hbjorgo/ATLib - true - MIT - https://github.com/hbjorgo/ATLib - https://github.com/hbjorgo/ATLib/releases - 6.0.2.0 - 6.0.2.0 - b8328b1a-795d-4e26-9238-43eee2160ffc - + + netstandard2.1 + HeboTech + HeboTech ATLib + 7.0.0-RC1 + 7.0.0-RC1 + 7.0.0.0 + 7.0.0.0 + HeboTech.ATLib + AT command library that makes it easy to communicate with modems. + AT command library that makes it easy to communicate with modems. + atcommand at command gsm sms hayes simcom d-link tp-link dwm-222 ma260 sim5320 + https://github.com/hbjorgo/ATLib + true + MIT + https://github.com/hbjorgo/ATLib + https://github.com/hbjorgo/ATLib/releases + b8328b1a-795d-4e26-9238-43eee2160ffc + - - - - - + + + + + - - + + + <_Parameter1>HeboTech.ATLib.Tests + diff --git a/src/HeboTech.ATLib/Modems/Generic/ModemBase.cs b/src/HeboTech.ATLib/Modems/Generic/ModemBase.cs index 5f1bb77..a98371e 100644 --- a/src/HeboTech.ATLib/Modems/Generic/ModemBase.cs +++ b/src/HeboTech.ATLib/Modems/Generic/ModemBase.cs @@ -1,6 +1,7 @@ using HeboTech.ATLib.CodingSchemes; using HeboTech.ATLib.DTOs; using HeboTech.ATLib.Events; +using HeboTech.ATLib.Extensions; using HeboTech.ATLib.Parsers; using HeboTech.ATLib.PDU; using System; @@ -123,11 +124,7 @@ public virtual async Task>> GetAvailableCharac var match = Regex.Match(line, @"\+CSCS:\s\((?:""(?\w+)"",*)+\)"); if (match.Success) { -#if NETSTANDARD2_0 - return ModemResponse.ResultSuccess(match.Groups["characterSet"].Captures.Cast().Select(x => x.Value)); -#elif NETSTANDARD2_1_OR_GREATER return ModemResponse.ResultSuccess(match.Groups["characterSet"].Captures.Select(x => x.Value)); -#endif } } return ModemResponse.ResultError>(); @@ -207,56 +204,76 @@ public virtual async Task> SendSmsInTextFormatAsync( return ModemResponse.ResultError(); } - public abstract Task> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme); + public abstract Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message); - protected virtual async Task> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme, bool includeEmptySmscLength) + protected virtual async Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, bool includeEmptySmscLength) { if (phoneNumber is null) throw new ArgumentNullException(nameof(phoneNumber)); if (message is null) throw new ArgumentNullException(nameof(message)); - byte dataCodingScheme; - string encodedMessage; - switch (codingScheme) + IEnumerable pdus = SmsSubmitEncoder.Encode(new SmsSubmitRequest(phoneNumber, message) { IncludeEmptySmscLength = includeEmptySmscLength }); + List> references = new List>(); + foreach (string pdu in pdus) { - case CodingScheme.Ansi: - encodedMessage = Ansi.Encode(message); - dataCodingScheme = Ansi.DataCodingSchemeCode; - break; - case CodingScheme.Gsm7: - encodedMessage = Gsm7.Encode(message); - dataCodingScheme = Gsm7.DataCodingSchemeCode; - break; - case CodingScheme.UCS2: - encodedMessage = UCS2.Encode(message); - dataCodingScheme = UCS2.DataCodingSchemeCode; - break; - default: - throw new ArgumentException("The encoding scheme is not supported"); + string cmd1 = $"AT+CMGS={(pdu.Length) / 2}"; + string cmd2 = pdu; + AtResponse response = await channel.SendSmsAsync(cmd1, cmd2, "+CMGS:", TimeSpan.FromSeconds(30)); + + if (response.Success) + { + string line = response.Intermediates.First(); + var match = Regex.Match(line, @"\+CMGS:\s(?\d+)"); + if (match.Success) + { + int mr = int.Parse(match.Groups["mr"].Value); + references.Add(ModemResponse.ResultSuccess(new SmsReference(mr))); + } + } + else + { + if (AtErrorParsers.TryGetError(response.FinalResponse, out Error error)) + references.Add(ModemResponse.ResultError(error.ToString())); + } } + return references; + } - string pdu = Pdu.EncodeSmsSubmit(phoneNumber, encodedMessage, dataCodingScheme, includeEmptySmscLength); - string cmd1 = $"AT+CMGS={(pdu.Length) / 2}"; - string cmd2 = pdu; - AtResponse response = await channel.SendSmsAsync(cmd1, cmd2, "+CMGS:", TimeSpan.FromSeconds(30)); + public abstract Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme); - if (response.Success) + protected virtual async Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme, bool includeEmptySmscLength) + { + if (phoneNumber is null) + throw new ArgumentNullException(nameof(phoneNumber)); + if (message is null) + throw new ArgumentNullException(nameof(message)); + + IEnumerable pdus = SmsSubmitEncoder.Encode(new SmsSubmitRequest(phoneNumber, message, codingScheme) { IncludeEmptySmscLength = includeEmptySmscLength }); + List> references = new List>(); + foreach (string pdu in pdus) { - string line = response.Intermediates.First(); - var match = Regex.Match(line, @"\+CMGS:\s(?\d+)"); - if (match.Success) + string cmd1 = $"AT+CMGS={(pdu.Length) / 2}"; + string cmd2 = pdu; + AtResponse response = await channel.SendSmsAsync(cmd1, cmd2, "+CMGS:", TimeSpan.FromSeconds(30)); + + if (response.Success) { - int mr = int.Parse(match.Groups["mr"].Value); - return ModemResponse.ResultSuccess(new SmsReference(mr)); + string line = response.Intermediates.First(); + var match = Regex.Match(line, @"\+CMGS:\s(?\d+)"); + if (match.Success) + { + int mr = int.Parse(match.Groups["mr"].Value); + references.Add(ModemResponse.ResultSuccess(new SmsReference(mr))); + } + } + else + { + if (AtErrorParsers.TryGetError(response.FinalResponse, out Error error)) + references.Add(ModemResponse.ResultError(error.ToString())); } } - else - { - if (AtErrorParsers.TryGetError(response.FinalResponse, out Error error)) - return ModemResponse.ResultError(error.ToString()); - } - return ModemResponse.ResultError(); + return references; } public virtual async Task> GetSupportedPreferredMessageStoragesAsync() @@ -347,11 +364,7 @@ public virtual async Task> ReadSmsAsync(int index, SmsTextFor SmsStatus status = SmsStatusHelpers.ToSmsStatus(statusCode); string pdu = line2Match.Groups["status"].Value; -#if NETSTANDARD2_0 - SmsDeliver pduMessage = Pdu.DecodeSmsDeliver(pdu.AsSpan()); -#elif NETSTANDARD2_1_OR_GREATER - SmsDeliver pduMessage = Pdu.DecodeSmsDeliver(pdu); -#endif + SmsDeliver pduMessage = SmsDeliverDecoder.Decode(pdu.ToByteArray()); return ModemResponse.ResultSuccess(new Sms(status, pduMessage.SenderNumber, pduMessage.Timestamp, pduMessage.Message)); } @@ -367,7 +380,7 @@ public virtual async Task> ReadSmsAsync(int index, SmsTextFor if (match.Success) { SmsStatus status = SmsStatusHelpers.ToSmsStatus(match.Groups["status"].Value); - PhoneNumber sender = new PhoneNumber(match.Groups["sender"].Value); + PhoneNumberDTO sender = new PhoneNumberDTO(match.Groups["sender"].Value); int year = int.Parse(match.Groups["year"].Value); int month = int.Parse(match.Groups["month"].Value); int day = int.Parse(match.Groups["day"].Value); @@ -407,7 +420,7 @@ public virtual async Task>> ListSmssAsync(SmsSt { int index = int.Parse(match.Groups["index"].Value); SmsStatus status = SmsStatusHelpers.ToSmsStatus(match.Groups["status"].Value); - PhoneNumber sender = new PhoneNumber(match.Groups["sender"].Value); + PhoneNumberDTO sender = new PhoneNumberDTO(match.Groups["sender"].Value); int year = int.Parse(match.Groups["year"].Value); int month = int.Parse(match.Groups["month"].Value); int day = int.Parse(match.Groups["day"].Value); @@ -463,16 +476,6 @@ public virtual async Task> GetSimStatusAsync() if (match.Success) { string cpinResult = match.Groups["pinresult"].Value; -#if NETSTANDARD2_0 - switch (cpinResult) - { - case "SIM PIN": return ModemResponse.ResultSuccess(SimStatus.SIM_PIN); - case "SIM PUK": return ModemResponse.ResultSuccess(SimStatus.SIM_PUK); - case "PH-NET PIN": return ModemResponse.ResultSuccess(SimStatus.SIM_NETWORK_PERSONALIZATION); - case "READY": return ModemResponse.ResultSuccess(SimStatus.SIM_READY); - default: return ModemResponse.ResultSuccess(SimStatus.SIM_ABSENT);// Treat unsupported lock types as "sim absent" - }; -#elif NETSTANDARD2_1_OR_GREATER return cpinResult switch { "SIM PIN" => ModemResponse.ResultSuccess(SimStatus.SIM_PIN), @@ -481,7 +484,6 @@ public virtual async Task> GetSimStatusAsync() "READY" => ModemResponse.ResultSuccess(SimStatus.SIM_READY), _ => ModemResponse.ResultSuccess(SimStatus.SIM_ABSENT),// Treat unsupported lock types as "sim absent" }; -#endif } return ModemResponse.ResultError(); diff --git a/src/HeboTech.ATLib/Modems/IModem.cs b/src/HeboTech.ATLib/Modems/IModem.cs index 790a8dd..a0c5c0b 100644 --- a/src/HeboTech.ATLib/Modems/IModem.cs +++ b/src/HeboTech.ATLib/Modems/IModem.cs @@ -195,6 +195,14 @@ public interface IModem : IDisposable /// Command status with SMS reference Task> SendSmsInTextFormatAsync(PhoneNumber phoneNumber, string message); + /// + /// Sends an SMS in PDU format. This will automatically select the Data Coding Scheme that will result in the fewest messages being sent in case of a concatenated SMS based on the content of the message. + /// + /// The number to send to + /// The message body + /// Command status with SMS reference + Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message); + /// /// Sends an SMS in PDU format /// @@ -202,7 +210,7 @@ public interface IModem : IDisposable /// The message body /// Encoding to use /// Command status with SMS reference - Task> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme); + Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme); /// /// Sends an USSD code. Results in an UssdResponseReceived event diff --git a/src/HeboTech.ATLib/Modems/Qualcomm/MDM9225.cs b/src/HeboTech.ATLib/Modems/Qualcomm/MDM9225.cs index beb64f3..eeb2c17 100644 --- a/src/HeboTech.ATLib/Modems/Qualcomm/MDM9225.cs +++ b/src/HeboTech.ATLib/Modems/Qualcomm/MDM9225.cs @@ -2,6 +2,7 @@ using HeboTech.ATLib.DTOs; using HeboTech.ATLib.Modems.Generic; using HeboTech.ATLib.Parsers; +using System.Collections.Generic; using System.Threading.Tasks; namespace HeboTech.ATLib.Modems.Qualcomm @@ -13,7 +14,12 @@ public MDM9225(AtChannel channel) { } - public override Task> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme) + public override Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message) + { + return base.SendSmsInPduFormatAsync(phoneNumber, message, false); + } + + public override Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme) { return base.SendSmsInPduFormatAsync(phoneNumber, message, codingScheme, false); } diff --git a/src/HeboTech.ATLib/Modems/SIMCOM/SIM5320.cs b/src/HeboTech.ATLib/Modems/SIMCOM/SIM5320.cs index 3a0ee8c..86ff7be 100644 --- a/src/HeboTech.ATLib/Modems/SIMCOM/SIM5320.cs +++ b/src/HeboTech.ATLib/Modems/SIMCOM/SIM5320.cs @@ -1,5 +1,6 @@ using HeboTech.ATLib.CodingSchemes; using HeboTech.ATLib.DTOs; +using HeboTech.ATLib.Extensions; using HeboTech.ATLib.Modems.Generic; using HeboTech.ATLib.Parsers; using HeboTech.ATLib.PDU; @@ -42,7 +43,12 @@ public virtual async Task GetRemainingPinPukAttemptsAsy #region _3GPP_TS_27_005 - public override Task> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme) + public override Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message) + { + return base.SendSmsInPduFormatAsync(phoneNumber, message, false); + } + + public override Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme) { return base.SendSmsInPduFormatAsync(phoneNumber, message, codingScheme, false); } @@ -66,11 +72,7 @@ public override async Task> ReadSmsAsync(int index, SmsTextFo string alphabet = line1Match.Groups["alphabet"].Value; int length = int.Parse(line1Match.Groups["length"].Value); string pdu = line2Match.Groups["pdu"].Value; -#if NETSTANDARD2_0 - SmsDeliver pduMessage = Pdu.DecodeSmsDeliver(pdu.AsSpan()); -#elif NETSTANDARD2_1_OR_GREATER - SmsDeliver pduMessage = Pdu.DecodeSmsDeliver(pdu); -#endif + SmsDeliver pduMessage = SmsDeliverDecoder.Decode(pdu.ToByteArray()); return ModemResponse.ResultSuccess(new Sms((SmsStatus)status, pduMessage.SenderNumber, pduMessage.Timestamp, pduMessage.Message)); } } @@ -85,7 +87,7 @@ public override async Task> ReadSmsAsync(int index, SmsTextFo if (match.Success) { SmsStatus status = SmsStatusHelpers.ToSmsStatus(match.Groups["status"].Value); - PhoneNumber sender = new PhoneNumber(match.Groups["sender"].Value); + PhoneNumberDTO sender = new PhoneNumberDTO(match.Groups["sender"].Value); int year = int.Parse(match.Groups["year"].Value); int month = int.Parse(match.Groups["month"].Value); int day = int.Parse(match.Groups["day"].Value); @@ -119,7 +121,7 @@ public override async Task>> ListSmssAsync(SmsS { int index = int.Parse(match.Groups["index"].Value); SmsStatus status = SmsStatusHelpers.ToSmsStatus(match.Groups["status"].Value); - PhoneNumber sender = new PhoneNumber(match.Groups["sender"].Value); + PhoneNumberDTO sender = new PhoneNumberDTO(match.Groups["sender"].Value); int year = int.Parse(match.Groups["year"].Value); int month = int.Parse(match.Groups["month"].Value); int day = int.Parse(match.Groups["day"].Value); diff --git a/src/HeboTech.ATLib/Modems/TP-LINK/MA260.cs b/src/HeboTech.ATLib/Modems/TP-LINK/MA260.cs index 5f591ca..70f8c2b 100644 --- a/src/HeboTech.ATLib/Modems/TP-LINK/MA260.cs +++ b/src/HeboTech.ATLib/Modems/TP-LINK/MA260.cs @@ -3,6 +3,7 @@ using HeboTech.ATLib.Modems.Generic; using HeboTech.ATLib.Parsers; using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace HeboTech.ATLib.Modems.TP_LINK @@ -20,7 +21,12 @@ public MA260(AtChannel channel) { } - public override Task> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme) + public override Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message) + { + return base.SendSmsInPduFormatAsync(phoneNumber, message, false); + } + + public override Task>> SendSmsInPduFormatAsync(PhoneNumber phoneNumber, string message, CodingScheme codingScheme) { return base.SendSmsInPduFormatAsync(phoneNumber, message, codingScheme, false); } diff --git a/src/HeboTech.ATLib/PDU/PduType.cs b/src/HeboTech.ATLib/PDU/MessageTypeIndicator.cs similarity index 85% rename from src/HeboTech.ATLib/PDU/PduType.cs rename to src/HeboTech.ATLib/PDU/MessageTypeIndicator.cs index 2d514ae..c90a3e1 100644 --- a/src/HeboTech.ATLib/PDU/PduType.cs +++ b/src/HeboTech.ATLib/PDU/MessageTypeIndicator.cs @@ -1,6 +1,6 @@ namespace HeboTech.ATLib.PDU { - public enum PduType : byte + public enum MessageTypeIndicator : byte { SMS_DELIVER_REPORT = 0x00, SMS_DELIVER = 0x00, diff --git a/src/HeboTech.ATLib/PDU/Pdu.cs b/src/HeboTech.ATLib/PDU/Pdu.cs deleted file mode 100644 index 3370a14..0000000 --- a/src/HeboTech.ATLib/PDU/Pdu.cs +++ /dev/null @@ -1,445 +0,0 @@ -using HeboTech.ATLib.CodingSchemes; -using HeboTech.ATLib.DTOs; -using System; -using System.Globalization; -using System.Text; - -namespace HeboTech.ATLib.PDU -{ -#if NETSTANDARD2_0 - public class Pdu - { - public static string EncodeSmsSubmit(PhoneNumber phoneNumber, string encodedMessage, byte dataCodingScheme, bool includeEmptySmscLength = true) - { - StringBuilder sb = new StringBuilder(); - // Length of SMSC information - if (includeEmptySmscLength) - sb.Append("00"); - // First octed of the SMS-SUBMIT message - sb.Append("11"); - // TP-Message-Reference. '00' lets the phone set the message reference number itself - sb.Append("00"); - // Address length. Length of phone number (number of digits) - sb.Append((phoneNumber.ToString().TrimStart('+').Length).ToString("X2")); - // Type-of-Address - sb.Append(GetAddressType(phoneNumber).ToString("X2")); - // Phone number in semi octets. 12345678 is represented as 21436587 - sb.Append(SwapPhoneNumberDigits(phoneNumber.ToString().TrimStart('+'))); - // TP-PID Protocol identifier - sb.Append("00"); - // TP-DCS Data Coding Scheme. '00'-7bit default alphabet. '04'-8bit - sb.Append((dataCodingScheme).ToString("X2")); - // TP-Validity-Period. 'AA'-4 days - sb.Append("AA"); - // TP-User-Data-Length. If TP-DCS field indicates 7-bit data, the length is the number of septets. - // If TP-DCS indicates 8-bit data or Unicode, the length is the number of octets. - if (dataCodingScheme == 0) - { - int messageBitLength = encodedMessage.Length / 2 * 7; - int messageLength = messageBitLength % 8 == 0 ? messageBitLength / 8 : (messageBitLength / 8) + 1; - sb.Append((messageLength).ToString("X2")); - } - else - sb.Append((encodedMessage.Length / 2 * 8 / 7).ToString("X2")); - sb.Append(encodedMessage); - - return sb.ToString(); - } - - public static SmsDeliver DecodeSmsDeliver(ReadOnlySpan text, int timestampYearOffset = 2000) - { - int offset = 0; - - // SMSC information - byte smsc_length = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - PhoneNumber serviceCenterNumber = null; - if (smsc_length > 0) - { - serviceCenterNumber = DecodePhoneNumber(text.SliceOnIndex(offset,(offset += smsc_length * 2))); - } - - // SMS-DELIVER start - byte header = HexToByte(text.SliceOnIndex(offset,(offset += 2))); - - int tp_mti = header & 0b0000_0011; - if (tp_mti != (byte)PduType.SMS_DELIVER) - throw new ArgumentException("Invalid SMS-DELIVER data"); - - int tp_mms = header & 0b0000_0100; - int tp_rp = header & 0b1000_0000; - - byte tp_oa_length = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - tp_oa_length = (byte)(tp_oa_length % 2 == 0 ? tp_oa_length : tp_oa_length + 1); - PhoneNumber oa = null; - if (tp_oa_length > 0) - { - int oa_digits = tp_oa_length + 2; // Add 2 for TON - oa = DecodePhoneNumber(text.SliceOnIndex(offset, (offset += oa_digits))); - } - byte tp_pid = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - byte tp_dcs = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - ReadOnlySpan tp_scts = text.SliceOnIndex(offset, (offset += 14)); - byte tp_udl = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - int udlBytes = (int)Math.Ceiling(tp_udl * 7 / 8.0); - - ReadOnlySpan tp_ud = text.SliceOnIndex(offset, (offset += ((udlBytes) * 2))); - string message = null; - switch (tp_dcs) - { - case 0x00: - message = Gsm7.Decode(tp_ud.ToString()); - break; - default: - break; - } - DateTimeOffset scts = DecodeTimestamp(tp_scts, timestampYearOffset); - return new SmsDeliver(serviceCenterNumber, oa, message, scts); - } - - public static SmsSubmit DecodeSmsSubmit(ReadOnlySpan text, int timestampYearOffset = 2000) - { - int offset = 0; - - // SMSC information - byte smsc_length = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - PhoneNumber serviceCenterNumber = null; - if (smsc_length > 0) - { - serviceCenterNumber = DecodePhoneNumber(text.SliceOnIndex(offset, (offset += smsc_length * 2))); - } - - // SMS-DELIVER start - byte header = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - - int tp_mti = header & 0b0000_0011; - if (tp_mti != (byte)PduType.SMS_SUBMIT) - throw new ArgumentException("Invalid SMS-SUBMIT data"); - - int tp_rd = header & 0b0000_0100; - int tp_vpf = header & 0b0001_1000; - int tp_rp = header & 0b1000_0000; - - byte tp_mr = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - byte tp_oa_length = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - tp_oa_length = (byte)(tp_oa_length % 2 == 0 ? tp_oa_length : tp_oa_length + 1); - PhoneNumber oa = null; - if (tp_oa_length > 0) - { - int oa_digits = tp_oa_length + 2; // Add 2 for TON - oa = DecodePhoneNumber(text.SliceOnIndex(offset, (offset += oa_digits))); - } - byte tp_pid = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - byte tp_dcs = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - byte tp_vp = 0; - if (tp_vpf == 0x00) - tp_vp = HexToByte(text.SliceOnIndex(offset, (offset += 0))); - else if (tp_vpf == 0x01) - tp_vp = HexToByte(text.SliceOnIndex(offset, (offset += 14))); - else if (tp_vpf == 0x10) - tp_vp = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - else if (tp_vpf == 0x11) - tp_vp = HexToByte(text.SliceOnIndex(offset, (offset += 14))); - byte tp_udl = HexToByte(text.SliceOnIndex(offset, (offset += 2))); - - string message = null; - switch (tp_dcs) - { - case 0x00: - int length = (tp_udl * 7 / 8) + 1; - ReadOnlySpan tp_ud = text.SliceOnIndex(offset, (offset += ((length) * 2))); - message = Gsm7.Decode(tp_ud.ToString()); - break; - default: - break; - } - return new SmsSubmit(serviceCenterNumber, oa, message); - } - - private static byte HexToByte(ReadOnlySpan text) - { - byte retVal = (byte)int.Parse(text.ToString(), NumberStyles.HexNumber); - return retVal; - } - - private static char[] SwapPhoneNumberDigits(string data) - { - char[] swappedData = new char[data.Length]; - for (int i = 0; i < data.Length; i += 2) - { - swappedData[i] = data[i + 1]; - swappedData[i + 1] = data[i]; - } - if (swappedData[swappedData.Length - 1] == 'F') - { - char[] subArray = new char[swappedData.Length - 1]; - Array.Copy(swappedData, subArray, subArray.Length); - return subArray; - } - return swappedData; - } - - private static byte GetAddressType(PhoneNumber phoneNumber) - { - return (byte)(0b1000_0000 + (byte)phoneNumber.GetTypeOfNumber() + (byte)phoneNumber.GetNumberPlanIdentification()); - } - - private static PhoneNumber DecodePhoneNumber(ReadOnlySpan data) - { - if (data.Length < 4) - return default; - TypeOfNumber ton = (TypeOfNumber)((HexToByte(data.Slice(0, 2)) & 0b0111_0000) >> 4); - string number = string.Empty; - if (ton == TypeOfNumber.International) - number = "+"; - number += new String(SwapPhoneNumberDigits(data.Slice(2).ToString())); - return new PhoneNumber(number); - } - - private static DateTimeOffset DecodeTimestamp(ReadOnlySpan data, int timestampYearOffset = 2000) - { - char[] swappedData = new char[data.Length]; - for (int i = 0; i < swappedData.Length; i += 2) - { - swappedData[i] = data[i + 1]; - swappedData[i + 1] = data[i]; - } - ReadOnlySpan swappedSpan = swappedData; - - byte offset = DecimalToByte(swappedSpan.SliceOnIndex(12, 14)); - bool positive = (offset & (1 << 7)) == 0; - byte offsetQuarters = (byte)(offset & 0b0111_1111); - - DateTimeOffset timestamp = new DateTimeOffset( - DecimalToByte(swappedSpan.SliceOnIndex(0, 2)) + timestampYearOffset, - DecimalToByte(swappedSpan.SliceOnIndex(2, 4)), - DecimalToByte(swappedSpan.SliceOnIndex(4, 6)), - DecimalToByte(swappedSpan.SliceOnIndex(6, 8)), - DecimalToByte(swappedSpan.SliceOnIndex(8, 10)), - DecimalToByte(swappedSpan.SliceOnIndex(10, 12)), - TimeSpan.FromMinutes(offsetQuarters * 15)); // Offset in quarter of hours - return timestamp; - } - - private static byte DecimalToByte(ReadOnlySpan text) - { - return (byte)int.Parse(text.ToString(), NumberStyles.Integer); - } - } - -#elif NETSTANDARD2_1_OR_GREATER - public class Pdu - { - public static string EncodeSmsSubmit(PhoneNumber phoneNumber, string encodedMessage, byte dataCodingScheme, bool includeEmptySmscLength = true) - { - StringBuilder sb = new StringBuilder(); - // Length of SMSC information - if (includeEmptySmscLength) - sb.Append("00"); - // First octed of the SMS-SUBMIT message - sb.Append("11"); - // TP-Message-Reference. '00' lets the phone set the message reference number itself - sb.Append("00"); - // Address length. Length of phone number (number of digits) - sb.Append((phoneNumber.ToString().TrimStart('+').Length).ToString("X2")); - // Type-of-Address - sb.Append(GetAddressType(phoneNumber).ToString("X2")); - // Phone number in semi octets. 12345678 is represented as 21436587 - sb.Append(SwapPhoneNumberDigits(phoneNumber.ToString().TrimStart('+'))); - // TP-PID Protocol identifier - sb.Append("00"); - // TP-DCS Data Coding Scheme. '00'-7bit default alphabet. '04'-8bit - sb.Append((dataCodingScheme).ToString("X2")); - // TP-Validity-Period. 'AA'-4 days - sb.Append("AA"); - // TP-User-Data-Length. If TP-DCS field indicates 7-bit data, the length is the number of septets. - // If TP-DCS indicates 8-bit data or Unicode, the length is the number of octets. - if (dataCodingScheme == 0) - { - int messageBitLength = encodedMessage.Length / 2 * 7; - int messageLength = messageBitLength % 8 == 0 ? messageBitLength / 8 : (messageBitLength / 8) + 1; - sb.Append((messageLength).ToString("X2")); - } - else - sb.Append((encodedMessage.Length / 2 * 8 / 7).ToString("X2")); - sb.Append(encodedMessage); - - return sb.ToString(); - } - - public static SmsDeliver DecodeSmsDeliver(ReadOnlySpan text, int timestampYearOffset = 2000) - { - int offset = 0; - - // SMSC information - byte smsc_length = HexToByte(text[offset..(offset += 2)]); - PhoneNumber serviceCenterNumber = null; - if (smsc_length > 0) - { - serviceCenterNumber = DecodePhoneNumber(text[offset..(offset += smsc_length * 2)]); - } - - // SMS-DELIVER start - byte header = HexToByte(text[offset..(offset += 2)]); - - int tp_mti = header & 0b0000_0011; - if (tp_mti != (byte)PduType.SMS_DELIVER) - throw new ArgumentException("Invalid SMS-DELIVER data"); - - int tp_mms = header & 0b0000_0100; - int tp_rp = header & 0b1000_0000; - - byte tp_oa_length = HexToByte(text[offset..(offset += 2)]); - tp_oa_length = (byte)(tp_oa_length % 2 == 0 ? tp_oa_length : tp_oa_length + 1); - PhoneNumber oa = null; - if (tp_oa_length > 0) - { - int oa_digits = tp_oa_length + 2; // Add 2 for TON - oa = DecodePhoneNumber(text[offset..(offset += oa_digits)]); - } - byte tp_pid = HexToByte(text[offset..(offset += 2)]); - byte tp_dcs = HexToByte(text[offset..(offset += 2)]); - ReadOnlySpan tp_scts = text[offset..(offset += 14)]; - byte tp_udl = HexToByte(text[offset..(offset += 2)]); - int udlBytes = (int)Math.Ceiling(tp_udl * 7 / 8.0); - - ReadOnlySpan tp_ud = text[offset..(offset += ((udlBytes) * 2))]; - string message = null; - switch (tp_dcs) - { - case 0x00: - message = Gsm7.Decode(new string(tp_ud)); - break; - default: - break; - } - DateTimeOffset scts = DecodeTimestamp(tp_scts, timestampYearOffset); - return new SmsDeliver(serviceCenterNumber, oa, message, scts); - } - - public static SmsSubmit DecodeSmsSubmit(ReadOnlySpan text, int timestampYearOffset = 2000) - { - int offset = 0; - - // SMSC information - byte smsc_length = HexToByte(text[offset..(offset += 2)]); - PhoneNumber serviceCenterNumber = null; - if (smsc_length > 0) - { - serviceCenterNumber = DecodePhoneNumber(text[offset..(offset += smsc_length * 2)]); - } - - // SMS-DELIVER start - byte header = HexToByte(text[offset..(offset += 2)]); - - int tp_mti = header & 0b0000_0011; - if (tp_mti != (byte)PduType.SMS_SUBMIT) - throw new ArgumentException("Invalid SMS-SUBMIT data"); - - int tp_rd = header & 0b0000_0100; - int tp_vpf = header & 0b0001_1000; - int tp_rp = header & 0b1000_0000; - - byte tp_mr = HexToByte(text[offset..(offset += 2)]); - byte tp_oa_length = HexToByte(text[offset..(offset += 2)]); - tp_oa_length = (byte)(tp_oa_length % 2 == 0 ? tp_oa_length : tp_oa_length + 1); - PhoneNumber oa = null; - if (tp_oa_length > 0) - { - int oa_digits = tp_oa_length + 2; // Add 2 for TON - oa = DecodePhoneNumber(text[offset..(offset += oa_digits)]); - } - byte tp_pid = HexToByte(text[offset..(offset += 2)]); - byte tp_dcs = HexToByte(text[offset..(offset += 2)]); - byte tp_vp = 0; - if (tp_vpf == 0x00) - tp_vp = HexToByte(text[offset..(offset += 0)]); - else if (tp_vpf == 0x01) - tp_vp = HexToByte(text[offset..(offset += 14)]); - else if (tp_vpf == 0x10) - tp_vp = HexToByte(text[offset..(offset += 2)]); - else if (tp_vpf == 0x11) - tp_vp = HexToByte(text[offset..(offset += 14)]); - byte tp_udl = HexToByte(text[offset..(offset += 2)]); - - string message = null; - switch (tp_dcs) - { - case 0x00: - int length = (tp_udl * 7 / 8) + 1; - ReadOnlySpan tp_ud = text[offset..(offset += ((length) * 2))]; - message = Gsm7.Decode(new string(tp_ud)); - break; - default: - break; - } - return new SmsSubmit(serviceCenterNumber, oa, message); - } - - private static byte HexToByte(ReadOnlySpan text) - { - byte retVal = (byte)int.Parse(text, NumberStyles.HexNumber); - return retVal; - } - - private static char[] SwapPhoneNumberDigits(ReadOnlySpan data) - { - char[] swappedData = new char[data.Length]; - for (int i = 0; i < data.Length; i += 2) - { - swappedData[i] = data[i + 1]; - swappedData[i + 1] = data[i]; - } - if (swappedData[^1] == 'F') - return swappedData[..^1]; - return swappedData; - } - - private static byte GetAddressType(PhoneNumber phoneNumber) - { - return (byte)(0b1000_0000 + (byte)phoneNumber.GetTypeOfNumber() + (byte)phoneNumber.GetNumberPlanIdentification()); - } - - private static PhoneNumber DecodePhoneNumber(ReadOnlySpan data) - { - if (data.Length < 4) - return default; - TypeOfNumber ton = (TypeOfNumber)((HexToByte(data[0..2]) & 0b0111_0000) >> 4); - string number = string.Empty; - if (ton == TypeOfNumber.International) - number = "+"; - number += new String(SwapPhoneNumberDigits(data[2..])); - return new PhoneNumber(number); - } - - private static DateTimeOffset DecodeTimestamp(ReadOnlySpan data, int timestampYearOffset = 2000) - { - char[] swappedData = new char[data.Length]; - for (int i = 0; i < swappedData.Length; i += 2) - { - swappedData[i] = data[i + 1]; - swappedData[i + 1] = data[i]; - } - ReadOnlySpan swappedSpan = swappedData; - - byte offset = DecimalToByte(swappedSpan[12..14]); - bool positive = (offset & (1 << 7)) == 0; - byte offsetQuarters = (byte)(offset & 0b0111_1111); - - DateTimeOffset timestamp = new DateTimeOffset( - DecimalToByte(swappedSpan[..2]) + timestampYearOffset, - DecimalToByte(swappedSpan[2..4]), - DecimalToByte(swappedSpan[4..6]), - DecimalToByte(swappedSpan[6..8]), - DecimalToByte(swappedSpan[8..10]), - DecimalToByte(swappedSpan[10..12]), - TimeSpan.FromMinutes(offsetQuarters * 15)); // Offset in quarter of hours - return timestamp; - - static byte DecimalToByte(ReadOnlySpan text) - { - return (byte)int.Parse(text, NumberStyles.Integer); - } - } - } -#endif -} diff --git a/src/HeboTech.ATLib/PDU/PduMessage.cs b/src/HeboTech.ATLib/PDU/PduMessage.cs deleted file mode 100644 index 4a07586..0000000 --- a/src/HeboTech.ATLib/PDU/PduMessage.cs +++ /dev/null @@ -1,38 +0,0 @@ -using HeboTech.ATLib.DTOs; -using System; - -namespace HeboTech.ATLib.PDU -{ - public abstract class PduMessage - { - public PduMessage(PhoneNumber serviceCenterNumber, PhoneNumber senderNumber, string message) - { - ServiceCenterNumber = serviceCenterNumber; - SenderNumber = senderNumber; - Message = message; - } - - public PhoneNumber ServiceCenterNumber { get; } - public PhoneNumber SenderNumber { get; } - public string Message { get; } - } - - public class SmsDeliver : PduMessage - { - public SmsDeliver(PhoneNumber serviceCenterNumber, PhoneNumber senderNumber, string message, DateTimeOffset timestamp) - : base(serviceCenterNumber, senderNumber, message) - { - Timestamp = timestamp; - } - - public DateTimeOffset Timestamp { get; } - } - - public class SmsSubmit : PduMessage - { - public SmsSubmit(PhoneNumber serviceCenterNumber, PhoneNumber senderNumber, string message) - : base(serviceCenterNumber, senderNumber, message) - { - } - } -} diff --git a/src/HeboTech.ATLib/PDU/ReadOnlySpanExtensions.cs b/src/HeboTech.ATLib/PDU/ReadOnlySpanExtensions.cs deleted file mode 100644 index 7354ef4..0000000 --- a/src/HeboTech.ATLib/PDU/ReadOnlySpanExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -#if NETSTANDARD2_0 -using System; - -namespace HeboTech.ATLib.PDU -{ - internal static class ReadOnlySpanExtensions - { - public static ReadOnlySpan SliceOnIndex(this ReadOnlySpan span, int start, int end) - { - return span.Slice(start, end - start); - } - } -} -#endif diff --git a/src/HeboTech.ATLib/PDU/SmsDeliverDecoder.cs b/src/HeboTech.ATLib/PDU/SmsDeliverDecoder.cs new file mode 100644 index 0000000..5ab2260 --- /dev/null +++ b/src/HeboTech.ATLib/PDU/SmsDeliverDecoder.cs @@ -0,0 +1,158 @@ +using HeboTech.ATLib.CodingSchemes; +using HeboTech.ATLib.DTOs; +using HeboTech.ATLib.Extensions; +using System; +using System.Linq; + +namespace HeboTech.ATLib.PDU +{ + internal class SmsDeliverDecoder + { + private class SmsDeliverHeader + { + private SmsDeliverHeader() + { + } + + public SmsDeliverHeader(MessageTypeIndicator mti, bool mms, bool lp, bool sri, bool udhi, bool rp) + { + MTI = mti; + MMS = mms; + LP = lp; + SRI = sri; + UDHI = udhi; + RP = rp; + } + + public MessageTypeIndicator MTI { get; private set; } + public bool MMS { get; private set; } + public bool LP { get; private set; } + public bool SRI { get; private set; } + public bool UDHI { get; private set; } + public bool RP { get; private set; } + + public static SmsDeliverHeader Parse(byte header) + { + SmsDeliverHeader parsedHeader = new SmsDeliverHeader(); + + parsedHeader.MTI = (MessageTypeIndicator)(header & 0b0000_0011); + if (parsedHeader.MTI != (byte)MessageTypeIndicator.SMS_DELIVER) + throw new ArgumentException("Invalid SMS-DELIVER data"); + + parsedHeader.MMS = (header & 0b0000_0100) != 0; + parsedHeader.SRI = (header & 0b0000_1000) != 0; + parsedHeader.UDHI = (header & 0b0100_0000) != 0; + parsedHeader.RP = (header & 0b1000_0000) != 0; + + return parsedHeader; + } + } + + /// + /// Decodes SMS-Deliver bytes in PDU format + /// + /// Data + /// Year offset + /// A decoded SMS-Deliver object + /// + public static SmsDeliver Decode(ReadOnlySpan bytes, int timestampYearOffset = 2000) + { + int offset = 0; + + // SMSC information + byte smsc_length = bytes[offset++]; + PhoneNumberDTO serviceCenterNumber = null; + if (smsc_length > 0) + { + serviceCenterNumber = DecodePhoneNumber(bytes[offset..(offset += smsc_length)]); + } + + // SMS-DELIVER start + byte headerByte = bytes[offset++]; + SmsDeliverHeader header = SmsDeliverHeader.Parse(headerByte); + + byte tp_oa_nibbles_length = bytes[offset++]; + byte tp_oa_bytes_length = (byte)(tp_oa_nibbles_length % 2 == 0 ? tp_oa_nibbles_length / 2 : (tp_oa_nibbles_length / 2) + 1); + tp_oa_bytes_length++; + PhoneNumberDTO oa = null; + if (tp_oa_bytes_length > 0) + { + oa = DecodePhoneNumber(bytes[offset..(offset += tp_oa_bytes_length)]); + } + + byte tp_pid = bytes[offset++]; + + byte tp_dcs_byte = bytes[offset++]; + if (!Enum.IsDefined(typeof(CodingScheme), tp_dcs_byte)) + throw new ArgumentException($"DCS with value {tp_dcs_byte} is not supported"); + CodingScheme tp_dcs = (CodingScheme)tp_dcs_byte; + + ReadOnlySpan tp_scts = bytes[offset..(offset += 7)]; + DateTimeOffset scts = TpduTime.DecodeTimestamp(tp_scts, timestampYearOffset); + + byte tp_udl = bytes[offset++]; + int udlBytes = 0; + switch (tp_dcs) + { + case CodingScheme.Gsm7: + udlBytes = (int)Math.Ceiling(tp_udl * 7 / 8.0); + break; + case CodingScheme.UCS2: + udlBytes = tp_udl; + break; + default: + throw new ArgumentException($"DCS with value {tp_dcs} is not supported"); + } + + ReadOnlySpan tp_ud = bytes[offset..(offset += udlBytes)]; + Udh udh; + ReadOnlySpan payload; + if (header.UDHI) + { + byte udhl = tp_ud[0]; + ReadOnlySpan udh_bytes = tp_ud[1..(udhl + 1)]; + udh = Udh.Parse(udhl, udh_bytes); + payload = tp_ud[(udhl + 1)..]; + } + else + { + udh = Udh.Empty(); + payload = tp_ud; + } + + string message; + switch (tp_dcs) + { + case CodingScheme.Gsm7: + int fillBits = 0; + if (header.UDHI) + fillBits = 7 - (((1 + udh.Length) * 8) % 7); + + var unpacked = Gsm7.Unpack(payload.ToArray(), fillBits); + message = Gsm7.DecodeFromBytes(unpacked); + break; + case CodingScheme.UCS2: + message = UCS2.Decode(payload.ToArray()); + break; + default: + throw new ArgumentException($"DCS with value {tp_dcs} is not supported"); + } + + return new SmsDeliver(serviceCenterNumber, oa, message, scts); + } + + private static PhoneNumberDTO DecodePhoneNumber(ReadOnlySpan data) + { + byte ext_ton_npi = data[0]; + TypeOfNumber ton = (TypeOfNumber)((ext_ton_npi & 0b0111_0000) >> 4); + + string number = string.Empty; + if (ton == TypeOfNumber.International) + number = "+"; + number += string.Join("", data[1..].ToArray().Select(x => x.SwapNibbles().ToString("X2"))); + if (number[^1] == 'F') + number = number[..^1]; + return new PhoneNumberDTO(number); + } + } +} diff --git a/src/HeboTech.ATLib/PDU/SmsSubmitEncoder.cs b/src/HeboTech.ATLib/PDU/SmsSubmitEncoder.cs new file mode 100644 index 0000000..ce5430d --- /dev/null +++ b/src/HeboTech.ATLib/PDU/SmsSubmitEncoder.cs @@ -0,0 +1,343 @@ +using HeboTech.ATLib.CodingSchemes; +using HeboTech.ATLib.DTOs; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; + +namespace HeboTech.ATLib.PDU +{ + internal class SmsSubmitEncoder + { + protected const int MAX_SINGLE_MESSAGE_SIZE_GSM7 = 160; + protected const int MAX_SINGLE_MESSAGE_SIZE_UCS2 = 70; + + protected const int MAX_MESSAGE_PART_SIZE_GSM7 = 153; + protected const int MAX_MESSAGE_PART_SIZE_UCS2 = 67; + + protected const int MAX_NUMBER_OF_MESSAGE_PARTS = 255; + + // First octet of the message + protected byte header = 0x00; + // TP-Message-Reference. '00' lets the phone set the message reference number itself + protected byte mr; + // Address length.Length of phone number (number of digits) + protected byte daLength; + // Type-of-Address + protected byte daType; + // Phone number in semi octets. 12345678 is represented as 21436587 + protected string daNumber = string.Empty; + // TP-PID Protocol identifier + protected byte pi; + // TP-DCS Data Coding Scheme. '00'-7bit default alphabet. '04'-8bit + protected CodingScheme dcs; + // TP-Validity-Period. 'AA'-4 days + protected List vp = new List(); + // Message + protected Message partitionedMessage; + + protected SmsSubmitEncoder() + { + header = (byte)MessageTypeIndicator.SMS_SUBMIT; + } + + protected static SmsSubmitEncoder Initialize() + { + return new SmsSubmitEncoder(); + } + + /// + /// Encode a message in PDU format + /// + /// Data object + /// PDUs + public static IEnumerable Encode(SmsSubmitRequest smsSubmit) + { + // Build TPDU + var messageParts = SmsSubmitEncoder + .Initialize() + .DestinationAddress(smsSubmit.PhoneNumber) + .ValidityPeriod(smsSubmit.ValidityPeriod) + .Message(smsSubmit.Message, smsSubmit.CodingScheme, smsSubmit.MessageReferenceNumber) + .Build(); + + foreach (var messagePart in messageParts) + { + StringBuilder sb = new StringBuilder(); + + // Length of SMSC information + if (smsSubmit.IncludeEmptySmscLength) + sb.Append("00"); + + sb.Append(messagePart); + + yield return sb.ToString(); + } + } + + protected bool UserDataHeaderIndicatorIsSet => (header & (1 << 6)) != 0x00; + + protected SmsSubmitEncoder EnableUserDataHeaderIndicator() + { + header |= 0b0100_0000; + return this; + } + + /// + /// Mandatory + /// + /// + protected SmsSubmitEncoder EnableReplyPath() + { + header |= 0b1000_0000; + return this; + } + + protected static byte GetAddressType(PhoneNumber phoneNumber) + { + return (byte)(0b1000_0000 + ((byte)phoneNumber.GetTypeOfNumber() << 4) + (byte)phoneNumber.GetNumberPlanIdentification()); + } + + protected static string SwapPhoneNumberDigits(string data) + { + if (data.Length % 2 != 0) + data += 'F'; + char[] swappedData = new char[data.Length]; + for (int i = 0; i < data.Length; i += 2) + { + swappedData[i] = data[i + 1]; + swappedData[i + 1] = data[i]; + } + if (swappedData[^1] == 'F') + return new string(swappedData[..^1]); + return new string(swappedData); + } + + /// + /// Mandatory + /// + /// + protected SmsSubmitEncoder RejectDuplicates() + { + header |= 0b0000_0100; + return this; + } + + /// + /// Mandatory + /// + /// + /// + protected SmsSubmitEncoder ValidityPeriod(ValidityPeriod validityPeriod) + { + // Set format + byte mask = 0b0001_1000; + header = (byte)((header & ~mask) | ((byte)validityPeriod.Format & mask)); + + // Set value + vp.Clear(); + vp.AddRange(validityPeriod.Value); + + return this; + } + + protected SmsSubmitEncoder EnableStatusReportRequest() + { + header |= 0b0010_0000; + return this; + } + + /// + /// Mandatory + /// + /// + /// + protected SmsSubmitEncoder MessageReference(byte value) + { + mr = value; + return this; + } + + /// + /// Mandatory + /// + /// + /// + /// + protected SmsSubmitEncoder DestinationAddress(PhoneNumber phoneNumber) + { + if (phoneNumber == null) + throw new ArgumentNullException(nameof(phoneNumber)); + daLength = (byte)(phoneNumber.CountryCode.Length + phoneNumber.NationalNumber.Length); + daType = GetAddressType(phoneNumber); + daNumber = SwapPhoneNumberDigits(phoneNumber.CountryCode + phoneNumber.NationalNumber); // TODO: Old: .TrimStart('+') + return this; + } + + /// + /// Mandatory + /// + /// + /// + protected SmsSubmitEncoder ProtocolIdentifier(byte value) + { + pi = value; + return this; + } + + /// + /// Mandatory + /// + /// + /// + protected SmsSubmitEncoder Message(string message, CodingScheme dataCodingScheme, byte messageReferenceNumber) + { + dcs = dataCodingScheme; + partitionedMessage = CreateMessageParts(message, dataCodingScheme, messageReferenceNumber); + return this; + } + + protected IEnumerable Build() + { + if (partitionedMessage.Parts.Count() > 1) + EnableUserDataHeaderIndicator(); + + foreach (var part in partitionedMessage.Parts) + { + StringBuilder sb = new StringBuilder(); + + sb.Append(header.ToString("X2")); + sb.Append(mr.ToString("X2")); + sb.Append(daLength.ToString("X2")); + sb.Append(daType.ToString("X2")); + sb.Append(daNumber); + sb.Append(pi.ToString("X2")); + sb.Append(((byte)dcs).ToString("X2")); + if (vp.Count > 0) + sb.Append(String.Join("", vp.Select(x => x.ToString("X2")))); + + switch (dcs) + { + case CodingScheme.Gsm7: + int fillBits = 0; + if (UserDataHeaderIndicatorIsSet) + fillBits = 7 - ((part.Header.Length * 8) % 7); + + var gsm7 = Gsm7.EncodeToBytes(part.Data); + var encoded = Gsm7.Pack(gsm7, fillBits); + + int udlBits = part.Header.Length * 8 + gsm7.Length * 7 + fillBits; + int udlSeptets = udlBits / 7; + + sb.Append((udlSeptets).ToString("X2")); + sb.Append(string.Join("", part.Header.Select(x => x.ToString("X2")))); + sb.Append(string.Join("", encoded.Select(x => x.ToString("X2")))); + break; + case CodingScheme.UCS2: + var ucs2Bytes = UCS2.EncodeToBytes(part.Data.ToArray()); + sb.Append((part.Header.Length + ucs2Bytes.Length).ToString("X2")); + sb.Append(string.Join("", part.Header.Select(x => x.ToString("X2")))); + sb.Append(string.Join("", ucs2Bytes.Select(x => x.ToString("X2")))); + break; + default: + throw new ArgumentException($"Coding scheme {nameof(dcs)} is not supported"); + } + + yield return sb.ToString(); + } + } + + protected static Message CreateMessageParts(string message, CodingScheme dcs, byte messageReferenceNumber) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + int maxMessagePartSize; + int maxSingleMessageSize; + switch (dcs) + { + case CodingScheme.Gsm7: + maxSingleMessageSize = MAX_SINGLE_MESSAGE_SIZE_GSM7; + maxMessagePartSize = MAX_MESSAGE_PART_SIZE_GSM7; + break; + case CodingScheme.UCS2: + maxSingleMessageSize = MAX_SINGLE_MESSAGE_SIZE_UCS2; + maxMessagePartSize = MAX_MESSAGE_PART_SIZE_UCS2; + break; + default: + throw new ArgumentException($"Coding scheme {nameof(dcs)} is not supported"); + }; + + // The message does not need to be concatenated. Return empty array + if (message.Length <= maxSingleMessageSize) + return new Message(0, 1, new MessagePart(Array.Empty(), message.ToCharArray())); + + int numberOfParts = (message.Length / maxMessagePartSize) + (message.Length % maxMessagePartSize == 0 ? 0 : 1); + + if (numberOfParts > MAX_NUMBER_OF_MESSAGE_PARTS) + throw new ArgumentException("Message is too large!"); + + MessagePart[] parts = new MessagePart[numberOfParts]; + for (int i = 0; i < numberOfParts; i++) + { + parts[i] = new MessagePart( + new byte[] + { + // Length of UDH + 0x05, + // IEI (0x00) for concatenated SMS + (byte)IEI.ConcatenatedShortMessages, + // Length of header for concatenated SMS (excluding the two first octets) + 0x03, + // CSMS reference number + messageReferenceNumber, + // Total number of parts + (byte)numberOfParts, + // This part's sequence number, starting at 1 + (byte)(i + 1) + }, + // Each part of the total message + message.Skip(i * maxMessagePartSize).Take(maxMessagePartSize).ToArray()); + } + + return new Message(messageReferenceNumber, (byte)numberOfParts, parts); + } + } + + internal class Message + { + public Message(byte messageReferenceNumber, byte numberOfParts, params MessagePart[] parts) + { + MessageReferenceNumber = messageReferenceNumber; + NumberOfParts = numberOfParts; + Parts = parts; + } + + public byte MessageReferenceNumber { get; } + public byte NumberOfParts { get; } + public IEnumerable Parts { get; } + + public override string ToString() + { + return $"Msg. ref. no.: {MessageReferenceNumber}, #Parts: {NumberOfParts}"; + } + } + + internal class MessagePart + { + public MessagePart(byte[] header, char[] data) + { + Header = header; + Data = data; + } + + public byte[] Header { get; } + public char[] Data { get; } + + public override string ToString() + { + return $"({Data.Length} chars): {new string(Data)}"; + } + } +} diff --git a/src/HeboTech.ATLib/PDU/TpduTime.cs b/src/HeboTech.ATLib/PDU/TpduTime.cs new file mode 100644 index 0000000..9a6e7ee --- /dev/null +++ b/src/HeboTech.ATLib/PDU/TpduTime.cs @@ -0,0 +1,49 @@ +using HeboTech.ATLib.Extensions; +using System; +using System.Linq; + +namespace HeboTech.ATLib.PDU +{ + internal static class TpduTime + { + public static byte[] EncodeTimestamp(DateTimeOffset value) + { + byte year = ((byte)(value.Year % 100)).DecimalToBcd().SwapNibbles(); + byte month = ((byte)value.Month).DecimalToBcd().SwapNibbles(); + byte day = ((byte)value.Day).DecimalToBcd().SwapNibbles(); + byte hour = ((byte)value.Hour).DecimalToBcd().SwapNibbles(); + byte minute = ((byte)value.Minute).DecimalToBcd().SwapNibbles(); + byte second = ((byte)value.Second).DecimalToBcd().SwapNibbles(); + + byte timeZoneQuarters = ((byte)(Math.Abs(value.Offset.TotalMinutes) / 15)).DecimalToBcd().SwapNibbles(); + if (value.Offset.TotalMinutes < 0) + timeZoneQuarters |= 0b0000_1000; + + return new byte[] { year, month, day, hour, minute, second, timeZoneQuarters }; + } + + public static DateTimeOffset DecodeTimestamp(ReadOnlySpan data, int timestampYearOffset = 2000) + { + byte[] swappedData = data.ToArray().Select(x => x.SwapNibbles()).ToArray(); + + byte year = swappedData[0].BcdToDecimal(); + byte month = swappedData[1].BcdToDecimal(); + byte day = swappedData[2].BcdToDecimal(); + byte hour = swappedData[3].BcdToDecimal(); + byte minute = swappedData[4].BcdToDecimal(); + byte second = swappedData[5].BcdToDecimal(); + byte offsetQuarters = ((byte)(swappedData[6] & 0b0111_1111)).BcdToDecimal(); + bool isOffsetPositive = (swappedData[6] & 0b1000_0000) == 0; + + DateTimeOffset timestamp = new DateTimeOffset( + year + timestampYearOffset, + month, + day, + hour, + minute, + second, + TimeSpan.FromMinutes((offsetQuarters * 15) * (isOffsetPositive ? 1 : -1))); // Offset in quarter of hours + return timestamp; + } + } +} diff --git a/src/HeboTech.ATLib/PDU/UDH.cs b/src/HeboTech.ATLib/PDU/UDH.cs new file mode 100644 index 0000000..5acb1d4 --- /dev/null +++ b/src/HeboTech.ATLib/PDU/UDH.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace HeboTech.ATLib.PDU +{ + public enum IEI : byte + { + ConcatenatedShortMessages = 0x00, + //NationalLanguageSingleShift = 0x24, + //NationalLanguageLockingShift = 0x25, + } + + public class Udh + { + private Udh(byte length, IEnumerable informationElements) + { + Length = length; + InformationElements = informationElements; + } + + public byte Length { get; } + public IEnumerable InformationElements { get; } + + public static Udh Empty() => new Udh(0, Array.Empty()); + + public static Udh Parse(byte totalLength, ReadOnlySpan data) + { + List informationElements = new List(); + for (int i = 0; i < totalLength;) + { + byte iei = data[i]; + byte length = data[i + 1]; + ReadOnlySpan payload = data[(i + 1)..(i + 1 + length)]; + informationElements.Add(new InformationElement(iei, length, payload.ToArray())); + i += 2 + length; + } + + return new Udh(totalLength, informationElements); + } + } + + public class InformationElement + { + public InformationElement(byte iei, byte length, byte[] data) + { + IEI = iei; + Length = length; + Data = data; + } + + public byte IEI { get; } + public byte Length { get; } + public byte[] Data { get; } + } +}