Modbus sniffer for Function code 03, 06, 16

Hello I’m a student and I’m currently working for a small association and they asked me to create a modbus sniffer for the function code 0x03, 0x06 and 0x10. I’ve never used c# before in my life and they gave me a little code already written. I tried to read and understand it all and it seems correct for me. But when I start the debugging it doesn’t go very well. I will put here part of the code and another part in the first comment.

using Modbus_Connect_4;
using System;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.IO.Ports;
using System.Web;
using System.Windows.Forms;
using System.Collections.Generic;

namespace Modbus_Connect_4
{
    public partial class Form1 : Form
    {
        private DataSet data;
        private static SerialPort serialPort;


        // private static TimeSpan differenza;
        private static DateTime oravecchia = DateTime.Now;   
        private static DateTime ora;
        private static string file;
        private static BackgroundWorker backgroundWorker1 = new BackgroundWorker();

        // richieste, risposte e il tema inizialmente predefinite a true
        private static Boolean richieste = true;
        private static Boolean risposte = true;
        private Boolean chiaro = true;

        // impostazioni necessarie per iniziare il funzionamento dell'app inizializzate a null
        private String porta = null;
        private String baudrate = null;
        private String databits = null;
        private String parita = null;
        private String stopbits = null;

        // seconde impostazioni che i predecessori ci hanno lasciato (non ho capito a cosa servano, credo per la comunicazione tra 2 porte e quindi leggevano il doppio delle informazioni)
    //    private String porta2;
    //    private String baudrate2;
    //    private String databits2;
    //    private String parita2;
    //    private String stopbits2;

        // enum con i fc richiesti
        enum Function_Code
        {
            SingleWrite = 3,
            MultiWrite = 6,
            Read = 16
        }
        public Form1()
        {
            InitializeComponent();
            // file dove verranno registrati i dati raccolti e letti dal programma
            file = "ModbusConnect4_LOG_" + DateTime.Now.ToString("dd_MM_yyyy_HH_mm_ss") + ".log";
            private CancellationTokenSource cancellationTokenSource;

            int marginButton = 35; // Margine dei button
            int marginTable = 20; // Margine della tabella
            int buttonWidth = 100; // lunghezza dei button
            int buttonHeight = 30; // altezza dei button

            // Imposta posizione e dimensioni per button1
            button1.Anchor = AnchorStyles.Top | AnchorStyles.Left;
            button1.Location = new Point(marginButton, marginButton);
            button1.Size = new Size(buttonWidth, buttonHeight);

            // Imposta posizione e dimensioni per button2 nell'angolo in alto a destra
            button2.Anchor = AnchorStyles.Top | AnchorStyles.Right;
            button2.Location = new Point(this.ClientSize.Width - buttonWidth - marginButton, marginButton);
            button2.Width = buttonWidth;
            button2.Height = buttonHeight;

            // Imposta posizione e dimensioni per button5 vicino al button 1
            button5.Anchor = AnchorStyles.Top | AnchorStyles.Left;
            button5.Size = new Size(buttonWidth, buttonHeight);
            button5.Location = new Point(button1.Right + marginButton, marginButton);

            // Imposta posizione e dimensioni per button3 = request
            button3.Anchor = AnchorStyles.Top | AnchorStyles.Left;
            button3.Size = new Size(buttonWidth, buttonHeight);
            button3.Location = new Point(button5.Right + marginButton, marginButton);

            // Imposta posizione e dimensioni per button4 = response
            button4.Anchor = AnchorStyles.Top | AnchorStyles.Left;
            button4.Size = new Size(buttonWidth, buttonHeight);
            button4.Location = new Point(button3.Right + marginButton, marginButton);

            // Imposta posizione verticale per dataGridView1 in base alla posizione dei pulsanti
            int verticalPosition = button1.Bottom + marginTable;

            // Imposta posizione e dimensioni per dataGridView1
            dataGridView1.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom;
            dataGridView1.Location = new Point(marginTable, verticalPosition);
            dataGridView1.Size = new Size(this.ClientSize.Width - 2 * marginTable, this.ClientSize.Height - verticalPosition - marginTable);

            string[] portNames = SerialPort.GetPortNames();
            string[] baud = { "300", "1200", "9600", "19200", "28800", "57600", "115200" };
            string[] databit = { "7 Bit", "8 Bit" };
            string[] parity = { "Nessuna", "Odd", "Even" };
            string[] stop = { "1 Bit", "2 Bit" };

            data = new DataSet("prova");

            //settaggio tabella
            DataTable table = new DataTable("prova2");
            table.Columns.Add("Ora");
            table.Columns.Add("Differenza (ms)");
            table.Columns.Add("ID");
            table.Columns.Add("FC");
            table.Columns.Add("N.Byte/Indirizzo iniziale");
            table.Columns.Add("N registri");
            table.Columns.Add("Contenuto Registri");
            table.Columns.Add("CRC");
            data.Tables.Add(table);
            dataGridView1.DataSource = data.Tables["prova2"];

            // divisione in colonne della tabella dove vengono riportati i dati
            dataGridView1.Columns["Ora"].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            dataGridView1.Columns["ID"].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            dataGridView1.Columns["FC"].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            dataGridView1.Columns["N.Byte/Indirizzo iniziale"].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            dataGridView1.Columns["N registri"].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            dataGridView1.Columns["Contenuto Registri"].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
            dataGridView1.Columns["CRC"].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

            // impostazione del tema chiaro come predefinito
            celleChiare();


            //settaggio Worker
            backgroundWorker1.WorkerReportsProgress = true;
            backgroundWorker1.WorkerSupportsCancellation = true;
            backgroundWorker1.DoWork += backgroundWorker1_DoWork;
            backgroundWorker1.ProgressChanged += backgroundWorker1_ProgressChanged;
            backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;


            //input informazioni porta seriale
            // scelta della porta seriale da dover leggere
            foreach (string port in portNames)
            {
                ToolStripMenuItem portItem = new ToolStripMenuItem(port);
                portItem.Click += PortItem_Click;
                portItem.CheckOnClick = true;
                portaToolStripMenuItem.DropDownItems.Add(portItem);
            }

            // scelta del baud della porta seriale
            foreach (string port in baud)
            {
                ToolStripMenuItem baudratemenu = new ToolStripMenuItem(port);
                baudratemenu.Click += baudItem_Click;
                baudratemenu.CheckOnClick = true;
                baudrateToolStripMenuItem.DropDownItems.Add(baudratemenu);
            }
            baudrateToolStripMenuItem.DropDownItems.Add(new ToolStripSeparator());
            baudrateToolStripMenuItem.DropDownItems.Add("Personalizzato [WIP]");

            // scelta del databit della porta seriale
            foreach (string port in databit)
            {
                ToolStripMenuItem databitsmenu = new ToolStripMenuItem(port);
                databitsmenu.Click += databitsItem_Click;
                databitsmenu.CheckOnClick = true;
                dataBitsToolStripMenuItem.DropDownItems.Add(databitsmenu);
            }

            // scelta della parity della porta seriale
            foreach (string port in parity)
            {
                ToolStripMenuItem paritamenu = new ToolStripMenuItem(port);
                paritamenu.Click += paritaItem_Click;
                paritamenu.CheckOnClick = true;
                paritaToolStripMenuItem.DropDownItems.Add(paritamenu);
            }

            foreach (string port in stop)
            {
                ToolStripMenuItem stopmenu = new ToolStripMenuItem(port);
                stopmenu.Click += StopItem_Click;
                stopmenu.CheckOnClick = true;
                stopBitToolStripMenuItem.DropDownItems.Add(stopmenu);
            }


            /*
             * per la seconda porta non in uso 
             * 
            foreach (string port in portNames)
            {
                ToolStripMenuItem portItem = new ToolStripMenuItem(port);
                portItem.Click += PortItem_Click2;
                portItem.CheckOnClick = true;
                porta2ToolStripMenuItem1.DropDownItems.Add(portItem);
            }

            foreach (string port in baud)
            {
                ToolStripMenuItem baudratemenu = new ToolStripMenuItem(port);
                baudratemenu.Click += baudItem_Click2;
                baudratemenu.CheckOnClick = true;
                baudrate2ToolStripMenuItem1.DropDownItems.Add(baudratemenu);
            }
            baudrate2ToolStripMenuItem1.DropDownItems.Add(new ToolStripSeparator());
            baudrate2ToolStripMenuItem1.DropDownItems.Add("Personalizzato [WIP]");


            foreach (string port in databit)
            {
                ToolStripMenuItem databitsmenu = new ToolStripMenuItem(port);
                databitsmenu.Click += databitsItem_Click2;
                databitsmenu.CheckOnClick = true;
                dataBits2ToolStripMenuItem1.DropDownItems.Add(databitsmenu);
            }


            foreach (string port in parity)
            {
                ToolStripMenuItem paritamenu = new ToolStripMenuItem(port);
                paritamenu.Click += paritaItem_Click2;
                paritamenu.CheckOnClick = true;
                parita2ToolStripMenuItem1.DropDownItems.Add(paritamenu);
            }

            foreach (string port in stop)
            {
                ToolStripMenuItem stopmenu = new ToolStripMenuItem(port);
                stopmenu.Click += stopItem_Click2;
                stopmenu.CheckOnClick = true;
                stopBits2ToolStripMenuItem.DropDownItems.Add(stopmenu);
            }*/
        }

        // lavoro che il backgroundworker deve svolgere quando chiamato
        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {

            //imposta i valori della porta seriale al worker
            int baudRate = Int32.Parse(baudrate);
            Parity parity = CalculateParity();
            int dataBits = ParseDataBits();
            StopBits stopBits = ParseStopBits();

            /*int baudRate2 = Int32.Parse(baudrate2);
            var parity2 = CalculateParity2();
            var dataBits2 = ParseDataBits2();
            var stopBits2 = ParseStopBits2();*/

            serialPort = new SerialPort(porta, baudRate, parity, dataBits, stopBits);

            serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
            
            // aprire la porta se possibile, altrimenti messaggio di errore
            try
            {
                serialPort.Open();
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }


            // porta seriale rimane aperta finchè il backgroundworker lavora
            while (!backgroundWorker1.CancellationPending)
            {
                System.Threading.Thread.Sleep(1);

            }

            serialPort.Close();

        }

        // Funzione che aggiorna la tabella
        private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            if (e.UserState is string[] values)
            {
                if (data != null)
                {
                    DataTable table = data.Tables["prova2"];
                    // se la tabella esiste
                    if (table != null)
                    {
                        // aggiunge la riga con i valori dell'array values
                        table.Rows.Add(values);
                        // aggiorna la tabella
                        dataGridView1.Refresh();
                        dataGridView1.Update();
                    }
                }
            }
        }

        // Funzione che finisce il funzionamento del backgroundworker
        private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                MessageBox.Show("An error occurred: " + e.Error.Message);
            }
            else if (e.Result is Exception ex)
            {
                MessageBox.Show("An error occurred: " + ex.Message);
            }
        }

        // Codice del button5 o button per fermare la lettura
        private void ButtonStop_Click(object sender, EventArgs e)
        {
            // richiama una funzione che ha la sua stessa funzione
            fermaLetturaToolStripMenuItem_Click(sender, e);
        }
        
        // Codice del button1 o button per iniziare la lettura
        private void Start_Click(object sender, EventArgs e)
        {
            // richiama una funzione che ha la sua stessa funzione
            avviaLetturaToolStripMenuItem_Click(sender, e);
        }

        // Funzione che gestisce i dati ricevuti
        private static void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
        {
            SerialPort sp = (SerialPort)sender;
            string hexLine = BitConverter.ToString(ReadBytesFromSerialPort(sp)).Replace("-", " ");

            ParseModbusLine(hexLine);
        }

        // Funzione che legge i bytes dalla porta seriale
        private static byte[] ReadBytesFromSerialPort(SerialPort sp)
        {
            int bytesToRead = sp.BytesToRead;
            // creazione array di bytes dove viene contenuto ciò che viene letto
            byte[] buffer = new byte[bytesToRead];
            sp.Read(buffer, 0, bytesToRead);
            return buffer;
        }

        // Funzione che analizza la stringa di bytes che gli viene inviata
        public static void ParseModbusLine(string hexLine)
        {
            Console.WriteLine(hexLine);
            // stringa viene divisa
            string[] bytes = hexLine.Split(' ');

            // se la lunghezza è minore di 5 non può essere ne una request, ne una response di modbus fc03, fc06, fc16
            if (bytes.Length < 5)
            {
                return;
            }

            // indirizzo dello slave salvato
            string slaveAddress = bytes[0];
            // tipo di function code salvato
            string functionCode = bytes[1];

            // controllo se la stringa passata è una Request o una Response
            if (functionCode == Function_Code.Read.ToString("x"))
            {
                if ((bytes.Length - 5) % 2 == 0)
                {
                    ParseModbusResponse(bytes);
                }
                else
                {
                    ParseModbusRequest(bytes);
                }
            }
        }

        // Funzione in caso la stringa passata precedentemente sia una Request, stringa viene salvata su file e su griglia
        public static void ParseModbusRequest(string[] bytes)
        {
            
            if (bytes.Length < 8)
            {
                return;
            }

            // Aggiornamento orario attuale
            ora = DateTime.Now;

            // Calcola la differenza tra i due tempi
            TimeSpan differenza = ora - oravecchia;

            // Aggiorna il tempo precedente per il prossimo calcolo
            oravecchia = ora;

            // Visualizza la differenza in millisecondi (o in qualsiasi formato desiderato)
            // MessageBox.Show("Differenza di tempo: " + differenza.TotalMilliseconds + " millisecondi");

            // salvataggio della stringa passata
            string slaveAddress = bytes[0];
            string functionCode = bytes[1];
            string addressFirstRegister = bytes[2] + bytes[3];
            string amountRegisters = bytes[4] + bytes[5];
            string crc = bytes[6] + bytes[7];

            // Preparazione valori da dover inviare alla DataGridView
            string[] values = new string[]
            {
                ora.ToString("o"),
                differenza.TotalMilliseconds + " ms",
                slaveAddress,
                functionCode + " - Read Request",
                addressFirstRegister,
                amountRegisters,
                string.Empty, // Non vengono specificati i registri in caso di Request
                crc
            };

            // Controllo che il backgroundworker sia libero per poter scrivere i dati in tabella e nel file
            if (backgroundWorker1.IsBusy)
            {
                // aspettare che il backgroundworker possa procedere
                System.Threading.Thread.Sleep(200);

                if (richieste == true)
                {
                    // creazione della cartella se non è già presente nel PC
                    try
                    {
                        Directory.CreateDirectory(@"c:modbus");
                    }
                    catch (Exception ex)
                    {
                        // non si fa nulla
                    }
                    // rootPath per scrivere nel file
                    string rootPath = @"C:modbus";
                    string filePath = Path.Combine(rootPath, file);

                    // scrittura nel file
                    using (StreamWriter outputFile = new StreamWriter(filePath, true))
                    {
                        // ogni riga in values viene scritta nel file
                        foreach (string line in values)
                        {
                            outputFile.WriteLine(line);
                        }
                        // Riga vuota per staccare ogni riga
                        outputFile.WriteLine("n");
                    }

                    // riportare i progressi al backgroundworker
                    backgroundWorker1.ReportProgress(0, values);
                }


            } else // stessi passaggi se il backgroundworker non è da aspettare
            {
                if (richieste == true)
                {
                    // creazione della cartella se non è già presente nel PC
                    try
                    {
                        Directory.CreateDirectory(@"c:modbus");
                    }
                    catch (Exception ex)
                    {
                        // non si fa nulla
                    }
                    // rootPath per scrivere nel file
                    string rootPath = @"C:modbus";
                    string filePath = Path.Combine(rootPath, file);

                    // scrittura nel file
                    using (StreamWriter outputFile = new StreamWriter(filePath, true))
                    {
                        // ogni riga in values viene scritta nel file
                        foreach (string line in values)
                        {
                            outputFile.WriteLine(line);
                        }
                        // Riga vuota per staccare ogni riga
                        outputFile.WriteLine("n");
                    }

                    // riportare i progressi al backgroundworker
                    backgroundWorker1.ReportProgress(0, values);
                }
            }
        }
        // Funzione in caso la stringa passata precedentemente sia una Response, stringa viene salvata su file e sulla griglia
        public static void ParseModbusResponse(string[] bytes)
        {
            // le Response delle nostre fc possono essere di 5 o 7 bytes
            if (bytes.Length <= 5)
            {
                return;
            }

            // informazioni della stringa vengono salvate
            string slaveAddress = bytes[0];
            string functionCode = bytes[1];
            // numero di bytes letti
            string numberOfBytes = bytes[2];

            // prova di conversione in bytes della stringa numberOfBytes, se non possibile la funzione termina
            if (!byte.TryParse(numberOfBytes, System.Globalization.NumberStyles.HexNumber, null, out byte byteCount))
            {
                // in caso la converisone avvenisse il valore è contenuto in byteCount
                return;
            }

            // lunghezza aspettata dalla conversione
            int expectedLength = 3 + byteCount + 2;

            // se la lunghezza è minore rispetto alla lunghezza aspettata la funzione termina
            if (bytes.Length < expectedLength)
            {
                return;
            }

            // creazione del CRC
            string crc = bytes[bytes.Length - 2] + bytes[bytes.Length - 1];

            
            for (int i = 3; i < 3 + byteCount; i += 2)
            {
                if (i + 1 < bytes.Length)
                {
                    string registerValue = bytes[i] + bytes[i + 1];


                    // aggiornamento dell'orario
                    ora = DateTime.Now;

                    // Calcola la differenza tra i due tempi
                    TimeSpan differenza = ora - oravecchia;

                    // Aggiorna il tempo precedente per il prossimo calcolo
                    oravecchia = ora;



                    // Prepara i valori da inviare alla DataGridView
                    string[] values = new string[]
                    {
                        ora.ToString("o"),
                        differenza.TotalMilliseconds + " ms",
                        slaveAddress,
                        functionCode+ " - Read Response",
                        numberOfBytes,
                        "1", // Since we are adding one register per row
                        registerValue,
                        crc
                    };

                    // Riporta i valori al backgroundworker per aggiornare la DataGridView
                    if (backgroundWorker1.IsBusy)
                    {
                        // se bisogna aspettare backgroundworker una sleep 
                        System.Threading.Thread.Sleep(200);
                        if (risposte==true)
                        {
                            try
                            {
                                // creazione della directory se non ancora presente sul PC
                                Directory.CreateDirectory(@"c:modbus");
                            }
                            catch (Exception ex)
                            {
                                // in caso di eccezione non si fa nulla
                            }
                            // rootpath dove è presente il file dove devono essere scritti i dati
                            string rootPath = @"C:modbus";
                            string filePath = Path.Combine(rootPath, file);

                            // scrittura sul file tramite StreamWriter
                            using (StreamWriter outputFile = new StreamWriter(filePath, true))
                            {
                                foreach (string line in values)
                                {
                                    outputFile.WriteLine(line);
                                }
                                // Riga per separare i dati
                                outputFile.WriteLine("n");
                            }
                        // aggiornamenti dei dati inviati al backgroundworker per aggiornare la tabella
                        backgroundWorker1.ReportProgress(0, values);
                        }

                    } else
                    {
                        // ripetizione del codice se il backgroundworker non è busy
                        if (risposte==true)
                        {

                            try
                            {
                                // creazione cartella se non presente nel PC
                                Directory.CreateDirectory(@"c:modbus");
                            }
                            catch (Exception ex)
                            {
                                // nulla in caso di eccezione
                            }
                            // rootpath dove è stato creato il file dove verranno scritti i dati letti
                            string rootPath = @"C:modbus";
                            string filePath = Path.Combine(rootPath, file);

                            // StreamWriter per scrivere sui file
                            using (StreamWriter outputFile = new StreamWriter(filePath, true))
                            {
                                // scrittura di ogni riga presente in values sul file
                                foreach (string line in values)
                                {
                                    outputFile.WriteLine(line);
                                }
                                outputFile.WriteLine("n");
                            }
                            backgroundWorker1.ReportProgress(0, values);
                        }
                    }
                }
            }
        }

        

It sometimes read and print data in the output but never in the datagridview

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật