Показаны сообщения с ярлыком C#. Показать все сообщения
Показаны сообщения с ярлыком C#. Показать все сообщения

среда, 30 мая 2018 г.

C# и CLI

Задался вопросом: как компилировать программы на C# не имея установленных средств разработки типа Visual Studio или там Sharp Develop. Ну просто и компьютер слабоват, и вообще... Выяснилось, что можно - соответствующие компиляторы и утилиты штатно поставляются вместе с .NET Framework - но, конечно, не так комфортно, как во всех этих WYSIWYG редакторах.

При разработке интерфейсов показалось более удобным воспользоваться XAML, поэтому возникла вторая задача - как этот самый XAML использовать тоже из командной строки.

Кроме того, есть существенная особенность - всё происходит под Windows XP с установленным на ней .NET Framework v3.5. Итак, приступим.

1. Простейшее консольное приложение
Файл Program.cs
using System;  

class HelloWorld  
{  
    static void Main()  
    {  
        Console.WriteLine("Привет, мир!");  
    }  
}  
как оказалось, без проблем компилируется командой:
C:\WINDOWS\Microsoft.NET\Framework\v3.5\csc.exe  Program.cs
Причем компилятор каким-то чудесным образом корректно обрабатывает русские буквы, при этом автоматически определяя кодировку исходного файла.

2. Если приложение посложнее, то знающие люди советуют не пользоваться компилятором напрямую ввиду обилия разнообразных опций, которые потребуется выставить, а использовать утилиту MS Build. Утилита берёт настройки из файла *.csproj, так что проект дополняется ещё одним файлом:
Файл project.csproj
<?xml version="1.0" encoding="utf-8"?>
<Project  DefaultTargets="Rebuild"  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">  

 <PropertyGroup>

  <!-- имя сборки -->
  <AssemblyName>main</AssemblyName>  

  <!-- в какую папку будет компилироваться сборка -->
  <OutputPath>bin\</OutputPath>  

 </PropertyGroup>  


 <ItemGroup>  
  <Compile Include="Program.cs" />  
 </ItemGroup>  


 <Target Name="Clean">  
  <Delete Files="$(OutputPath)$(AssemblyName).exe" Condition="Exists('$(OutputPath)')" />  
 </Target>  

 <Target Name="Build">  

  <!-- если папка, в которую будет компилироваться проект, не существует, создаём её -->
  <MakeDir Directories="$(OutputPath)" Condition="!Exists('$(OutputPath)')" />  

  <!-- компилируем сборку -->
  <Csc Sources="@(Compile)" OutputAssembly="$(OutputPath)$(AssemblyName).exe" />

 </Target>

 <Target Name="Rebuild" DependsOnTargets="Clean;Build" /> 

</Project>
и собирается следующей командой:
C:\WINDOWS\Microsoft.NET\Framework\v3.5\msbuild.exe  project.csproj
Тут уже можно задавать некую структуру проекта, например, создавать отдельную папку для результата компиляции. Кстати, если присмотреться, секции <Target> идеологически сильно напоминают правила линуксовой утилиты make.

3. Но пришло время перейти к оконным приложениям. Поскольку за кулисами оконного интерфейса скрывается экземпляр класса System.Windows.Forms.Application, то изменяем файл Program.cs - теперь там этот самый экземпляр будет создаваться и вводиться в работу:
Файл Program.cs
using System;
using System.Windows.Forms;

namespace myProject
{
 static class Program
 {
  /// <summary>
  /// Точка входа в программу
  /// </summary>
  [STAThread]
  static void Main()
  {
   Application.EnableVisualStyles();
   Application.SetCompatibleTextRenderingDefault(false);
   Application.Run(new MainForm());
  }
 }
}
Кроме того, теперь требуется описать окно нашего приложения. В Visual Studio его описание разбито на несколько файлов, и нужно согласиться, что это достаточно удобно - вся рутина, посвященная созданию окна и расположенных на нем компонентов, вынесена в отдельный файл *.Designer.cs. Мы последуем этому примеру и создадим наши два файла для описания главного окна:
Файл MainForm.cs
using System;
using System.Windows.Forms;

namespace myProject
{
 public partial class MainForm : Form
 {
  public MainForm()
  {
   InitializeComponent();
  }
 }
}
Файл MainForm.Designer.cs
namespace myProject
{
 partial class MainForm
 {
  private void InitializeComponent()
  {
   System.Windows.Forms.Button button1 = new System.Windows.Forms.Button();

   button1.Location = new System.Drawing.Point(8, 8);
   button1.Name = "button1";
   button1.Size = new System.Drawing.Size(75, 23);
   button1.TabIndex = 0;
   button1.Text = "Кнопка";

   this.Controls.Add(button1);
  }
 }
}
Кроме того, изменения, очевидно, коснутся файла проекта - теперь он будет выглядеть так:
Файл project.csproj
<?xml version="1.0" encoding="utf-8"?>
<Project  DefaultTargets="Rebuild"  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">  

 <PropertyGroup>

  <!-- имя сборки -->
  <AssemblyName>main</AssemblyName>  

  <!-- в какую папку будет компилироваться сборка -->
  <OutputPath>bin\</OutputPath>  

 </PropertyGroup>  


 <ItemGroup>  

  <Compile Include="Program.cs" />

  <!-- главное окно программы -->
  <Compile Include="MainForm.Designer.cs" />
  <Compile Include="MainForm.cs" />

 </ItemGroup>


 <Target Name="Clean">  
  <Delete Files="$(OutputPath)$(AssemblyName).exe" Condition="Exists('$(OutputPath)')" />  
 </Target>  

 <Target Name="Build">  

  <!-- если папка, в которую будет компилироваться проект, не существует, создаём её -->
  <MakeDir Directories="$(OutputPath)" Condition="!Exists('$(OutputPath)')" />  

  <!-- компилируем сборку -->
  <Csc Sources="@(Compile)" OutputAssembly="$(OutputPath)$(AssemblyName).exe" />

 </Target>

 <Target Name="Rebuild" DependsOnTargets="Clean;Build" /> 

</Project>

4. Как видим, всё хорошо, но описать форму можно удобнее. Воспользуемся таким инструментом, как XAML. Для этого заменяем файл Program.cs на его эквивалент:
Файл Program.xaml
<?xml version="1.0" encoding="utf-8"?>
<Application x:Class="myProject.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="MainWindow.xaml">
</Application>
и заменяем файлы с описанием главного окна программы (MainForm.Designer.cs и MainForm.cs) на вот такие:
Файл MainWindow.xaml
<?xml version="1.0" encoding="utf-8"?>
<Window x:Class="myProject.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Тестовое окно"
 Height="300"
 Width="400"
 >
    <Grid>
        <Button Height="35" Margin="8,8,8,8" Name="button1" VerticalAlignment="Top">Test</Button>
    </Grid>
</Window>
Файл MainWindow.xaml.cs
using System;
using System.Windows;

namespace myProject
{
 /// <summary>
 /// Логика работы окна MainWindow.xaml
 /// </summary>
 public partial class MainWindow : Window
 {
  public MainWindow()
  {
   InitializeComponent();
  }
 }
}
Эти изменения, разумеется, найдут своё отражение в файле проекта:
Файл project.csproj
<?xml version="1.0" encoding="utf-8"?>
<Project  xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="3.5">  

 <PropertyGroup>

  <!-- имя сборки -->
  <AssemblyName>main</AssemblyName>  

  <!-- в какую папку будет компилироваться сборка -->
  <OutputPath>bin\</OutputPath> 

  <OutputType>winexe</OutputType>
  <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>

 </PropertyGroup>  


 <ItemGroup>

  <Reference Include="System"/>
  <Reference Include="WindowsBase"/>
  <Reference Include="PresentationCore"/>
  <Reference Include="PresentationFramework"/>

  <ApplicationDefinition Include="Program.xaml"/>

  <Page Include="MainWindow.xaml"/>
  <Compile Include="MainWindow.xaml.cs"/>

 </ItemGroup>  


 <!-- вся магия на самом деле скрыта тут -->
 <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

</Project>

Таким образом, можно вполне создавать оконные приложения .NET имея под рукой только блокнот. Не скажу, что этот вариант хоть в чем-то выигрывает по сравнению с IDE, но само наличие такой возможности не может не радовать.

Литература
Walkthrough: Creating an MSBuild Project File from Scratch
MSBuild

воскресенье, 16 октября 2016 г.

C#: Двухфакторная аутентификация без смартфона

Возникла тут интересная проблема. Если вкратце, то одна из "программ-от-знакомства-с-которыми-нельзя-отказаться" после смены версии вдруг вообразила себя крутой финансовой системой и затребовала двухфакторную аутентификацию. Не знаю, чем уж там разработчики соблазнили заказчиков, но теперь после ввода логина-пароля эта чудесная софтина показывает некий QR-код и предлагает пользователю установить на смартфон Google Authenticator, которому надо скормить этот её QR-код, чтобы этот GA начал генерировать PIN-коды, которые теперь будут дополнительно требоваться при входе. Короче, если хочешь поработать, покупай смартфон.

Меня, гордого пользователя Samsung SGH-X100 с периодически отваливающимся от старости аккумулятором, такое положение дел устроить никак не могло. Поэтому пришлось познакомиться с этим Google Authenticator-ом поближе. Как я понял, механизм тут такой.

Пусть есть клиент и сервер, и этот сервер хочет предоставить клиенту дополнительную защиту от взлома, даже если у этого самого клиента уведут пароль. Сервер предоставляет клиенту при первом входе некое уникальное число X0, которое клиент запоминает у себя и бережно хранит.

Когда клиент хочет аутентифицироваться на сервере, он берёт это число X0, текущее время T и применяет к ним описанную в стандарте детерминированную функцию F(X0, T), которая на выходе выдаёт PIN-код, передаваемый серверу. Сервер, в свою очередь, проделывает у себя ту же операцию, сравнивает полученный и рассчитанный PIN-коды и, если они совпадают, позволяет клиенту работать.

Как видим, алгоритм сильно зависит от того, насколько хорошо синхронизированы часы на сервере и у клиента. Чтобы упростить задачу, в алгоритме используется не количество, например, миллисекунд, прошедших с начала эпохи UNIX, а количество 30-секундных интервалов. То есть, допускается разбег между часами клиента и сервера в пределах 30 секунд.

Уникальное же число X0 как раз и предоставляется сервером клиенту в виде картинки с QR-кодом - видимо, для пущей безопасности.

Вооружившись этими знаниями, написал простенькую программку, используя которую можно считать этот QR-код с экрана и получать PIN-коды для двухфакторной аутентификации. Ну, то есть, хрен бы я чего написал, если бы не поиск Google, подсказки http://stackoverflow.com и волшебная библиотека MessagingToolkit.QRCode.dll.

Вот тут научился работать с получением всемирного координированного времени по протоколу NTP:
http://stackoverflow.com/questions/1193955/how-to-query-an-ntp-server-using-c

Вот тут рассказали, как получить снимок части экрана:
http://stackoverflow.com/questions/5049122/capture-the-screen-shot-using-net

Вот тут научили работать с base 32:
http://stackoverflow.com/questions/641361/base32-decoding

Вот тут нашелся рабочий код для Google Authenticator:
http://stackoverflow.com/questions/6421950/is-there-a-tutorial-on-how-to-implement-google-authenticator-in-net-apps

Вот тут (кажется) скачал библиотеку MessagingToolkit.QRCode.dll:
http://osdn.net/projects/sfnet_qrreader/downloads/MessagingToolkit.QRCode.dll/

Ну и, собственно, сам результат:

Программа

Исходники

UPD 2017-01-20: Как оказалось, история не закончилась. Нашлись QR-коды, на которых MessagingToolkit сбоит. Например, если QR-код содержит картинку-логотип. Пришлось перетащить проект на другую библиотеку, ZXing.NET

Программа (ZXing)

Исходники (ZXing)

А тут - небольшая памятка, как этой библиотекой пользоваться.

воскресенье, 21 декабря 2014 г.

C#: получить снимки с цифровой камеры

Понадобилось тут поработать с цифровой камерой: скопировать с неё снимки и запихнуть их в базу данных. Если вторая часть задачи решилась хоть и не изящно, но достаточно просто, то с реализацией первой возникли определенные проблемы.

Дело в том, что потребовалось получать и обрабатывать несколько снимков за раз. Я думал обойтись штатным OpenFileDialog. Ведь цифровая камера, будучи подключенной по USB к компьютеру, притворяется обычной папкой. Но, оказалось, "папка" эта не совсем обычная. В частности, например, при попытке доступа к изображению система создает его копию во временных файлах, и позволяет работать уже с этой копией. Если же выбрать несколько изображений, то OpenFileDialog вернёт вместо путей что-то маловразумительное типа "004\Root\IMG_1111" "004\Root\IMG_1112" "004\Root\IMG_1113".

Поэтому возникла мысль использовать обходной путь. В Windows есть нечто под названием Windows Image Acquisition. Статус этой технологии мне остался до конца неясным - то ли она уже устарела, то ли еще нет - но и в XP, и в 7 эта штука присутствует, хоть и двух разных версий.

Сначала попытался использовать WIA первой версии. Думал, обратная совместимость вывезет. Работаю я на WinXP, там WIA 1.0, что называется, "из коробки". Итак, добавляю в проект ссылку на Microsoft Windows Image Acquisition 1.01 Type Library (Add Reference, вкладка COM), получаю пространство имен WIALib и в нем кучку классов, с которыми уже можно работать:
using WIALib;
...

private void button1_Click(object sender, EventArgs e)
{
  object selectUsingUI = System.Reflection.Missing.Value;
  try
  {
    // создаем класс для работы с WIA
    WiaClass wiamanager = new WiaClass();

    // выводим диалог выбора цифровой камеры
    ItemClass wiaRoot = (ItemClass)wiamanager.Create(ref selectUsingUI);
    if (wiaRoot == null)
      return;

    // выводим диалог выбора изображений, хранящихся в камере
    CollectionClass wiaPics = (CollectionClass)wiaRoot.GetItemsFromUI(WiaFlag.UseCommonUI, WiaIntent.ImageTypeColor);

    // каждое выбранное изображение копируем в каталог с нашей программой
    if (wiaPics != null)
      foreach (object wiaObj in wiaPics)
      {
        ItemClass wiaItem = (ItemClass)Marshal.CreateWrapperOfType(wiaObj, typeof(ItemClass));
        string tmpFileName = Path.GetDirectoryName(Application.ExecutablePath) + "\\" + wiaItem.Name;
        wiaItem.Transfer(tmpFileName, false);
      }
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
}
Всё работает чудесно. Но обнаружились два момента. Первое: без дополнительной обработки изображения получаются чудовищного размера (без всякого сжатия), и второе, самое главное: этот способ не сработал в Windows 7. А именно, при запуске своей программы на Windows 7 64bit я получил такое сообщение об ошибке: Сбой при получении производства объектов класса COM для компонента с CLSID {4EC4272E-2E6F-4EEB-91D0-EBC4D58E8DEE} в результате следующей ошибки: 80040154. В общем, похоже, в современных виндах WIA 1.0 нет.

Пришлось обратиться к WIA 2.0. Под Windows XP потребовалось её установить дополнительно. Я брал дистрибутив отсюда: http://vbnet.mvps.org/files/updates/wiaautsdk.zip. Дальше добавляю в проект ссылку на Microsoft Windows Image Acquisition Library v2.0 (всё на той же вкладке COM) и получаю пространство имен WIA. Использовал это примерно так:
using WIA;
...

private void button1_Click(object sender, EventArgs e)
{

  // создаем класс для работы с камерой
  WIA.CommonDialogClass dlg = null;
  try
  {
    dlg = new WIA.CommonDialogClass();
  }
  catch(Exception ex)
  {
    MessageBox.Show("Невозможно подключиться к интерфейсу работы с камерой:\n" + ex.Message, "Ошибка",
        MessageBoxButtons.OK, MessageBoxIcon.Error);
  }
  if (dlg == null)
    return;

  // выбираем камеру
  WIA.DeviceClass d = null;
  try
  {
    d = (WIA.DeviceClass)dlg.ShowSelectDevice(WIA.WiaDeviceType.CameraDeviceType, true, false);
  }
  catch (Exception ex)
  {
    MessageBox.Show("Невозможно подключиться к камере:\n" + ex.Message, "Ошибка",
        MessageBoxButtons.OK, MessageBoxIcon.Error);
  }
  if (d == null)
    return;

  // выбираем изображения
  WIA.Items items = dlg.ShowSelectItems(d, WIA.WiaImageIntent.ColorIntent,
      WIA.WiaImageBias.MinimizeSize, false, true, false);

  if (items != null)
    foreach (WIA.Item item in items)
    {
      WIA.ImageFile img = (WIA.ImageFile)item.Transfer(WIA.FormatID.wiaFormatJPEG);

      // выясняем имя и расширение выбранного изображения
      string
        tmpFileName = "",
        tmpFileExt = "";
      foreach (WIA.Property prop in item.Properties)
      {
        if (prop.Name == "Item Name")
          tmpFileName = prop.get_Value().ToString();
        if (prop.Name == "Filename extension")
          tmpFileExt = prop.get_Value().ToString();
      }
      if (tmpFileExt == "")
        tmpFileExt = img.FileExtension;
      if (Path.HasExtension(tmpFileName))
        tmpFileExt = "";

      // копируем выбранное изображение в каталог с нашей программой
      string
        tmpFullName = Path.GetDirectoryName(Application.ExecutablePath) + "\\"
            + tmpFileName + ((tmpFileExt != "") ? "." : "") + tmpFileExt;

      img.SaveFile(tmpFullName);
    }
}
Код получился немного длинее, но это из-за проверок. Основной плюс - помимо того, что эта конструкция сработала и в Windows 7 - возможность сохранять изображения в формате JPEG.

Кстати, во втором примере в свойствах изображения есть много интересного. Я себе на память сделал скриншот:

среда, 10 декабря 2014 г.

C#: Превышена максимальная длина запроса

Помогла вот эта статья: Загрузка файла большого размера.

Вкратце - в web.config в узел <system.web> надо добавить опцию:
<httpRuntime maxRequestLength="16384" />

вторник, 9 ноября 2010 г.

Ошибка при взаимодействии с Excel

На одном компьютере (WinXP MSOffice 2007) при попытке выгрузить отчет из самописной (.NET 3.5) программы в Excel проявилась следующая ошибка: Невозможно привести COM-объект типа "Microsoft.Office.Interop.Excel.ApplicationClass" к интерфейсному типу "Microsoft.Office.Interop.Excel._Application". Операция завершилась со сбоем, поскольку вызов QueryInterface COM-объекта для интерфейса с IID "{000208D5-0000-0000-C000-000000000046}" возвратил следующую ошибку: Ошибка при загрузке библиотеки. (Исключение из HRESULT: 0x80029C4A (TYPE_E_CANTLOADLIBRARY)).

Ну да, программа компилировалась на компе с MSOffice 2003, и это наложило определенный отпечаток на набор ее библиотек. Но ведь на других станциях всё работает... Единственная особенность - на проблемной машине зачем-то были установлены primary interop assemblies 2007. В итоге помог метод грубой силы: деинсталляция этих самых assemblies и восстановление офиса.

UPD 2018-08-10: По крайней мере, в одном случае удалось установить причину этой ошибки. Как оказалось, по адресу HKEY_CLASSES_ROOT\Interface\{000208D5-0000-0000-C000-000000000046}\TypeLib лежит некий guid (в моём случае {00020813-0000-0000-C000-000000000046}) и номер версии, который нужно использовать (в моём случае 1.6).

Так вот, в ветке реестра HKCR\TypeLib\{00020813-0000-0000-C000-000000000046} перечислены разные версии, и среди них оказалась одна запись (1.9), оставшаяся от деинсталлированного офиса 2010. Если эту запись, отсылающую к несуществующей версии excel-а, удалить, то ошибка пропадает.

Навело на решение это обсуждение.

пятница, 3 сентября 2010 г.

Как инсталлировать сервис .NET

Installutil MyService.exe

ну и, соответственно, деинсталлировать:
Installutil /u MyService.exe

суббота, 31 июля 2010 г.

Отправлять длинные SMS из своей программы

Чудо, чудо! Если использовать конкатенацию SMS, то можно обойти жуткое ограничение в 70 символов на сообщение!

Но, конечно, пришлось разбивать сообщение на кусочки, к каждому из которых в начало прописывать по шесть байтов:

text =
"06" + // Length of User Data Header
"08" + // Concatenated short messages, 16-bit reference number
"04" + // Length of the header, excluding the first two fields
CSMS_reference_number + // уникальный номер длинного сообщения
chunks.Count.ToString("X2") + // число кусков сообщения
(i+1).ToString("X2") + // номер куска
text;

и в SMS-SUBMIT PDU устанавливать 6-й бит, так что он теперь стал не "11", а "51".

воскресенье, 25 июля 2010 г.

Отправлять SMS из своей программы

В общем-то, C# рулит...

1. Самой большой проблемой было добиться от сотового телефона на команду "AT" внятный ответ "OK".
Использовал System.IO.Ports.SerialPort:

System.IO.Ports.SerialPort serialPort = new SerialPort("COM5");
serialPort.DataReceived += new SerialDataReceivedEventHandler(serialPort_DataReceived);
serialPort.BaudRate = 115200;
serialPort.Open();
serialPort.WriteLine("AT\r");

В обработчике serialPort_DataReceived получилось примерно следующее:

void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
this.SetText(serialPort.ReadExisting());
}

Соответственно, основные пляски с бубном - это позволить написать что-то в textBox1 из другой нитки:

delegate void SetTextCallback(string text);
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.textBox1.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.textBox1.Text += "\r\nrcv: " + text;
}
}

2. Теперь о собственно передаче сообщений. Очень заманчиво было бы воспользоваться советами, приведенными тут. В самом деле,

AT+CSCS="UCS2"\r
AT+CSMP=1,167,0,8\r
AT+CMGS="+31638740161"\r
06450631062D06280627 + Convert.ToChar(26)

и всё, sms-ка ушла, что может быть проще? Но почему-то оказалось не судьба. На второй команде Siemens CX75 взбрыкнул, и пришлось пойти длинным путем.

Вот тут и тут написана куча полезного. Получилось такое:

AT+CMGF=0\r
AT+CMGS= + (sms.Length/2-1).ToString() + \r
sms + Convert.ToChar(26)

Здесь строка sms конструируется так:

string text = TextToSMS("АБРАКАДАБРА");
string sms =
"00" + // длина SMSC информации (пусть телефон ее додумает сам)
"11" + // first octet of the SMS-SUBMIT PDU
// (установлен бит 0 -> вид сообщения=SMS-SUBMIT)
// (установлен бит 3 -> есть поле TP-VP )
"00" + // TP-Message-Reference. "00" lets the phone set the message reference number itself.
PhoneNumberToSMS("79030123456") + // номер получателя
"00" + // PID - идентификатор номера протокола
"08" + // DCS схема кодирования данных (08 - UCS2=кириллица)
"AA" + // TP-VP - Validity Period, время жизни сообщения на сервере (AA - установлен в 4 дня)
(text.Length/2).ToString("X2") + // длина сообшения
text;


Две функции, PhoneNumberToSMS и TextToSMS, служат для кодирования номера телефона и текста сообщения соответственно:

private string PhoneNumberToSMS(string number)
{
number += "F";
string result =
"0B" + // длина номера (12 цифр, включая F)
"91"; // международный формат номера
int i = 0;
while (i < number.Length)
{
result += number[i + 1].ToString() + number[i].ToString();
i += 2;
}
return result;
}

private string TextToSMS(string text)
{
byte[] b = (new System.Text.UnicodeEncoding()).GetBytes(text);
string result = "";
int i = 0;
while (i < b.Length)
{
result += b[i + 1].ToString("X2") + b[i].ToString("X2");
i += 2;
}
return result;
}