0721 머신이어 마이크

2025. 7. 21. 22:52·프로젝트

MainWindow.xaml.cs
0.04MB
MainWindow.xaml
0.01MB

<Window x:Class="MachineEar_MIC.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MachineEar_MIC"
        mc:Ignorable="d"
        Title="MainWindow" Height="595" Width="400">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="0*"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>


        <Grid HorizontalAlignment="Center" Height="99" Margin="0,62,0,0" VerticalAlignment="Top" Width="264" Grid.Column="1">
            <Label Content="IP" HorizontalAlignment="Left" Height="30" VerticalAlignment="Top" Width="48" HorizontalContentAlignment="Center"/>
            <Label Content="MAC" HorizontalAlignment="Left" Height="30" VerticalAlignment="Bottom" Width="48" HorizontalContentAlignment="Center"/>
            <Label Content="PORT" HorizontalAlignment="Left" Height="30" VerticalAlignment="Center" Width="48" HorizontalContentAlignment="Center"/>
            <Button x:Name="btn_connect" Content="연결" Height="40" Margin="196,3,8,0" VerticalAlignment="Top" Click="connect_btn_click" Width="60">
                <UIElement.RenderTransform>
                    <TransformGroup>
                        <ScaleTransform/>
                        <SkewTransform/>
                        <RotateTransform/>
                        <TranslateTransform/>
                    </TransformGroup>
                </UIElement.RenderTransform>
            </Button>
            <Button x:Name="btn_disconnec" Content="해제" HorizontalAlignment="Left" Height="40" Margin="196,49,0,0" VerticalAlignment="Top" Width="60" Click="btn_disconnec_Click"/>
            <TextBox x:Name="textbox_ip" HorizontalAlignment="Left" Margin="64,3,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Height="25" TextChanged="TextBox_TextChanged"/>
            <TextBox x:Name="textbox_port" HorizontalAlignment="Left" Margin="64,0,0,0" TextWrapping="Wrap" VerticalAlignment="Center" Width="120" Height="25" TextChanged="TextBox_TextChanged_1"/>
            <TextBox x:Name="textbox_mac" HorizontalAlignment="Left" Margin="64,68,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Height="25" IsEnabled="False"/>
        </Grid>
        <Label Content="Network Connection" Grid.ColumnSpan="2" HorizontalAlignment="Left" Margin="40,35,0,0" VerticalAlignment="Top"/>
        <Label Content="Input : Sound Wave" Grid.ColumnSpan="2" HorizontalAlignment="Left" Margin="72,171,0,0" VerticalAlignment="Top"/>
        <WrapPanel Grid.ColumnSpan="2" HorizontalAlignment="Left" Height="15" Margin="232,178,0,0" VerticalAlignment="Top" Width="111">
            <RadioButton x:Name="radio_mic" Content="mic" Checked="radio_mic_Checked" Width="45" Margin="0,0,6,0"/>
            <RadioButton x:Name="radio_csv" Content="wav" Checked="radio_csv_Checked" Width="45"/>
        </WrapPanel>
        <WrapPanel HorizontalAlignment="Center" Height="30" VerticalAlignment="Top" Width="260" Grid.Column="1" Margin="0,200,0,0">
            <Label Content="MIC ID" HorizontalAlignment="Left" Height="30" VerticalAlignment="Bottom" Width="50" HorizontalContentAlignment="Center" RenderTransformOrigin="0.104,1.133"/>
            <ComboBox x:Name="ComboBox_mic" HorizontalAlignment="Left" Height="26" VerticalAlignment="Top" Width="130" Margin="0,0,5,0"/>
            <Button x:Name="btn_browse" Content="불러오기" HorizontalAlignment="Center" Height="26" VerticalAlignment="Top" Width="75" Click="btn_mac_connect_Click" IsEnabled="False"/>
        </WrapPanel>
        <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Center" Height="28" VerticalAlignment="Top" Width="380" Grid.Column="1" Margin="0,340,0,0">
            <Label Content="mic level" HorizontalAlignment="Left" Height="24" Margin="4,0,0,0" VerticalAlignment="Center" Width="58"/>
        </Border>
        <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Center" Height="192" VerticalAlignment="Top" Width="380" Grid.Column="1" Margin="0,368,0,0"/>
        <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Center" Height="57" VerticalAlignment="Top" Width="380" Grid.Column="1" Margin="0,274,0,0"/>
        <Canvas x:Name="canvas_waveform" Width="362" Height="170" Margin="0,379,0,0" HorizontalAlignment="Center" VerticalAlignment="Top" Grid.Column="1"/>
        <Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Center" Height="28" VerticalAlignment="Top" Width="380" Grid.Column="1" Margin="0,246,0,0">
            <Label Content="peak" HorizontalAlignment="Left" Height="25" Margin="4,-1,0,0" VerticalAlignment="Top" Width="51"/>
        </Border>
        <StackPanel Grid.ColumnSpan="2" HorizontalAlignment="Left" Height="31" Margin="257,3,0,0" VerticalAlignment="Top" Width="116" Orientation="Horizontal">


            <Label Content="서버 접속 상태" Height="26" Width="93"/>
            <Ellipse


        Width="16" StrokeThickness="1" Stroke="Black" x:Name="ellipse_status" Height="15" Fill="Gray" RenderTransformOrigin="1.533,-1.864" />
        </StackPanel>

    </Grid>
</Window>
using Microsoft.VisualBasic;
using Microsoft.Win32;
using NAudio.Wave;
using Newtonsoft.Json;
using ScottPlot;
using SkiaSharp;
using SkiaSharp.Views.WPF;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.NetworkInformation;  // MAC 주소 관련
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Xml;
using Formatting = Newtonsoft.Json.Formatting;


namespace MachineEar_MIC
{
    public enum ProtocolName
    {
        Connect,        // 0-0-0
        StatusCheck,    // 0-0-1
        AudioSend,      // 0-1-0
        DeviceStatus    // 0-2-0
    }

    /// Interaction logic for MainWindow.xaml
    public partial class MainWindow : Window
    {
        private TcpClientService tcpService;
        private const int AUDIO_SEND_PERIOD_SEC = 5;
        private System.Timers.Timer wavTimer;
        private System.Timers.Timer micTimer;
        private WaveInEvent waveIn;
        private MemoryStream audioBuffer;

        public MainWindow()
        {
            InitializeComponent();

            // config 파일 경로 설정
            path_server = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");

            // config 불러오기 (없는 경우 생성 포함)
            load_serveraddr(path_server);

            // UI에 설정값 반영
            textbox_ip.Text = iPAddress.IP;
            textbox_port.Text = iPAddress.PORT.ToString();
            textbox_mac.Text = iPAddress.MAC;

            // 서버 접속 상태
            tcpService = new TcpClientService();
            tcpService.ConnectionStatusChanged += (sender, isConnected) =>
            {
                SetConnectionStatus(isConnected);
            };

            var waveIn = GetWaveIn();
            InitConfig(); // 마이크 설정

        }

        public class IPAddress_Local
        {
            public string MAC { get; set; }
            public string IP { get; set; }
            public int PORT { get; set; }
        }

        private void InitConfig()
        {
            path_server = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");

            if (!File.Exists(path_server))
            {
                // MAC 주소 랜덤 생성
                string mac = GenerateRandomMac();
                var config = new IPAddress_Local { MAC = mac, IP = "", PORT = 0 };
                string json = JsonConvert.SerializeObject(config, Formatting.Indented);
                File.WriteAllText(path_server, json);
            }

            if (string.IsNullOrWhiteSpace(iPAddress.MAC))
            {
                iPAddress.MAC = GenerateRandomMac();
                File.WriteAllText(path_server, JsonConvert.SerializeObject(iPAddress, Formatting.Indented));
                Debug.WriteLine("💡 MAC 값이 없어서 새로 생성 후 저장했습니다.");
            }

            string read = File.ReadAllText(path_server);
            this.iPAddress = JsonConvert.DeserializeObject<IPAddress_Local>(read);
        }

        private string GenerateRandomMac()
        {
            Random rand = new Random();
            byte[] mac = new byte[6];
            rand.NextBytes(mac);
            mac[0] = (byte)((mac[0] & 0xFE) | 0x02); // Locally Administered
            return string.Join(":", mac.Select(b => b.ToString("X2")));
        }


        public class TcpClientService : IDisposable
        {
            // 서버에서 수신한 NUM_PIN 저장용
            private string _numPin;
            public string NumPin => _numPin;
            public Action<string> OnNumPinReceived; // NUM_PIN이 저장된 후 자동 실행될 콜백


            // 프로토콜 타입에 따라 문자열 반환
            private string GetProtocolCode(ProtocolName type)
            {
                return type switch
                {
                    ProtocolName.Connect => "0-0-0",
                    ProtocolName.StatusCheck => "0-0-1",
                    ProtocolName.AudioSend => "0-1-0",
                    ProtocolName.DeviceStatus => "0-2-0",
                    _ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
                };
            }

            // 프로토콜 종류에 따라 JSON 구성
            private string CreateProtocolJson
            (  // 매개변수:
                ProtocolName type,
                string mac = null,
                string pin = null,
                int? size = null,
                int? time = null,
                string source = null,
                string connection = null
            )

            {
                var json = new Dictionary<string, object>
                {
                    { "PROTOCOL", GetProtocolCode(type) }
                };

                switch (type)
                {
                    case ProtocolName.Connect:
                        json["MAC"] = mac;
                        break;

                    case ProtocolName.StatusCheck:
                        json["NUM_PIN"] = pin;
                        break;

                    case ProtocolName.AudioSend:
                        json["NUM_PIN"] = pin;
                        json["__META__"] = new Dictionary<string, object>
                        {
                            { "SIZE", size ?? 0 },
                            { "SAMPLING_RATE", 16000 }, // 16KHz,
                            { "TIME", time ?? 0 },
                            { "SOURCE", source ?? "WAV" }
                        };
                        break;

                    case ProtocolName.DeviceStatus:
                        json["NUM_PIN"] = pin;
                        json["TYPE"] = "MIC";
                        json["CONNECTION"] = connection ?? "Normal";
                        json["LASTTIME"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                        break;
                }

                return JsonConvert.SerializeObject(json, Formatting.Indented);
            }

            // 최종 전송 진입 함수 (유연한 JSON 생성 후 전송)
            public async Task<bool> SendProtocolAsync
            (  // 매개변수:
                ProtocolName type,
                string mac = null,
                string pin = null,
                int? size = null,
                int? time = null,
                string source = null,
                string connection = null,
                byte[] fileData = null
            )
            {
                try
                {
                    string json = CreateProtocolJson(type, mac, pin, size, time, source, connection);
                    return await SendMessageAsync(json);
                }
                catch (Exception ex)
                {
                    OnErrorOccurred($"SendProtocolAsync 오류: {ex.Message}");
                    return false;
                }
            }

            // TCP 클라이언트 소켓
            private Socket _clientSocket;
            private CancellationTokenSource _receiveCts;
            private const int BufferSize = 8192;

            public event EventHandler<string> MessageReceived;
            public event EventHandler<string> ErrorOccurred;
            public event EventHandler<bool> ConnectionStatusChanged;


            public bool IsConnected => _clientSocket != null && _clientSocket.Connected;

            private byte[] _receiveBuffer = new byte[8192]; // 소켓에서 직접 읽어올 임시 버퍼
            private MemoryStream _currentMessageBuffer = new MemoryStream(); // 현재 메시지를 구성하는 데이터를 축적할 버퍼

            // 메시지 파싱 상태를 나타내는 열거형
            private enum ReceiveState
            {
                WaitingForHeader,
                WaitingForJsonData,
                WaitingForFileData
            }

            private ReceiveState _currentState = ReceiveState.WaitingForHeader; // 현재 수신 상태
            private int _totalMessageSize = 0; // 헤더에서 읽은 전체 메시지 크기
            private int _jsonSize = 0;       // 헤더에서 읽은 JSON 데이터 크기

            public TcpClientService()
            {
                // 생성자에서는 특별한 초기화 없이 이벤트만 정의
            }

            public async Task<bool> ConnectAsync(string ipAddress, int port, string macAddress)
            {
                if (IsConnected)
                {
                    Debug.WriteLine("이미 서버에 연결되어 있습니다.");
                    return true;
                }

                try
                {
                    Debug.WriteLine("[ConnectAsync] 시작");

                    _clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                    IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse(ipAddress), port);

                    Debug.WriteLine($"서버에 연결 중... ({ipAddress}:{port})");
                    await _clientSocket.ConnectAsync(serverEndPoint);

                    Debug.WriteLine("서버에 성공적으로 연결되었습니다!");
                    OnConnectionStatusChanged(true);

                    // ✅ 리팩토링된 전송 방식
                    await SendProtocolAsync(ProtocolName.Connect, mac: macAddress);

                    // 수신 루프 시작
                    _receiveCts = new CancellationTokenSource();
                    _ = Task.Run(() => ReceiveDataAsync(_receiveCts.Token));

                    return true;
                }
                catch (Exception ex)
                {
                    OnErrorOccurred($"연결 오류: {ex.Message}");
                }

                return false;
            }

            /// <summary>
            /// 서버로부터 데이터를 비동기적으로 수신하는 루프입니다.
            /// </summary>
            private async Task ReceiveDataAsync(CancellationToken cancellationToken)
            {
                //Debug.WriteLine("ReceiveDataAsync 진입");

                // byte[] buffer = new byte[BufferSize]; // 이 임시 버퍼 대신 클래스 멤버인 _receiveBuffer 사용
                try
                {
                    while (!cancellationToken.IsCancellationRequested && IsConnected)
                    {
                        // ✅ 수신 대기 로그
                        //Debug.WriteLine("👉 [ReceiveAsync 대기 시작]");

                        int bytesRead = await _clientSocket.ReceiveAsync(
                            new ArraySegment<byte>(_receiveBuffer),
                            SocketFlags.None
                        );

                        //Debug.WriteLine($"✅ [수신 완료] bytesRead={bytesRead}");

                        if (bytesRead == 0)
                        {
                            Debug.WriteLine("서버에서 연결을 종료했습니다.");
                            break;
                        }

                        // 받은 데이터를 현재 메시지 버퍼에 추가
                        _currentMessageBuffer.Write(_receiveBuffer, 0, bytesRead);

                        // 받은 데이터가 있을 때마다 메시지를 파싱 시도
                        ProcessReceivedData();
                    }
                }
                // ... (기존 예외 처리 로직은 동일)
                catch (ObjectDisposedException)
                {
                    Debug.WriteLine("수신 소켓이 닫혔습니다.");
                }
                catch (SocketException ex)
                {
                    if (ex.SocketErrorCode == SocketError.ConnectionReset || ex.SocketErrorCode == SocketError.Interrupted)
                    {
                        OnErrorOccurred("서버와의 연결이 강제로 끊어졌습니다.");
                    }
                    else
                    {
                        OnErrorOccurred($"수신 오류: {ex.Message}");
                    }
                }
                catch (Exception ex)
                {
                    OnErrorOccurred($"예기치 않은 수신 오류: {ex.Message}");
                }
                finally
                {
                    if (IsConnected)
                    {
                        Debug.WriteLine("수신 루프 종료. 연결을 끊습니다.");
                        Disconnect();
                    }
                    _currentMessageBuffer.Dispose(); // MemoryStream 자원 해제
                }
            }

            //////////////////수신부//////////////////
            private void ProcessReceivedData()
            {
                //Debug.WriteLine("📩 [ProcessReceivedData 진입]");

                // _currentMessageBuffer의 현재 위치를 처음으로 설정하여 읽을 수 있도록 함
                _currentMessageBuffer.Position = 0;

                // 데이터를 모두 읽고 처리할 때까지 반복
                while (true)
                {
                    switch (_currentState)
                    {
                        case ReceiveState.WaitingForHeader:
                            // 헤더(8바이트)를 받을 수 있는 충분한 데이터가 있는지 확인
                            //Debug.WriteLine($"[상태: {_currentState}] Position={_currentMessageBuffer.Position}, Length={_currentMessageBuffer.Length}");

                            if (_currentMessageBuffer.Length >= 8)
                            {
                                byte[] headerBytes = new byte[8];
                                _currentMessageBuffer.Read(headerBytes, 0, 8); // 버퍼에서 8바이트 읽기

                                // 바이트 배열을 정수로 변환 (리틀 엔디안 가정)
                                _totalMessageSize = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(headerBytes, 0));
                                _jsonSize = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(headerBytes, 4));

                                // 헤더 유효성 검사 (예: JSON 크기가 전체 크기보다 클 수 없음)
                                if (_jsonSize > _totalMessageSize || _totalMessageSize <= 8) // 최소 크기는 헤더 8바이트 + 최소 JSON/파일 데이터
                                {
                                    OnErrorOccurred($"잘못된 헤더 수신: TotalSize={_totalMessageSize}, JsonSize={_jsonSize}. 연결을 끊습니다.");
                                    Disconnect(); // 잘못된 헤더는 치명적인 오류로 간주하여 연결 끊기
                                    return; // 루프 종료
                                }

                                //Debug.WriteLine($"헤더 수신: 전체 크기={_totalMessageSize}, JSON 크기={_jsonSize}");
                                _currentState = ReceiveState.WaitingForJsonData; // 다음 상태로 전환
                            }
                            else
                            {
                                return;
                            }
                            break;

                        case ReceiveState.WaitingForJsonData:
                            // JSON 데이터를 받을 수 있는 충분한 데이터가 있는지 확인
                            //Debug.WriteLine($"[상태: {_currentState}] Position={_currentMessageBuffer.Position}, Length={_currentMessageBuffer.Length}");

                            if (_currentMessageBuffer.Length - _currentMessageBuffer.Position >= _jsonSize)
                            {
                                byte[] jsonBytes = new byte[_jsonSize];
                                _currentMessageBuffer.Read(jsonBytes, 0, _jsonSize); // JSON 크기만큼 데이터 읽기

                                // JSON 데이터 수신: jsonData
                                string jsonData = Encoding.UTF8.GetString(jsonBytes);
                                Debug.WriteLine($"JSON 데이터 수신: {jsonData}");

                                try
                                {
                                    var parsed = JsonConvert.DeserializeObject<Dictionary<string, object>>(jsonData);

                                    if (parsed.ContainsKey("PROTOCOL") && parsed["PROTOCOL"].ToString() == "0-0-0")
                                    {
                                        if (parsed.ContainsKey("RESPONSE") && parsed["RESPONSE"].ToString() == "OK")
                                        {
                                            // ✅ 서버가 반환한 NUM_PIN 저장
                                            if (parsed.ContainsKey("NUM_PIN"))
                                            {
                                                _numPin = parsed["NUM_PIN"].ToString();
                                                Debug.WriteLine($"[NUM_PIN 저장됨] {_numPin}");
                                            }
                                        }
                                    }

                                    if (parsed.ContainsKey("NUM_PIN"))
                                    {
                                        _numPin = parsed["NUM_PIN"].ToString();
                                        Debug.WriteLine($"[NUM_PIN 저장됨] {_numPin}");

                                        // ✅ NUM_PIN 저장 후 콜백 호출
                                        OnNumPinReceived?.Invoke(_numPin);
                                    }


                                    // 이후 다른 응답 처리도 필요 시 여기에 확장
                                }
                                catch (Exception ex)
                                {
                                    Debug.WriteLine($"❌ JSON 파싱 오류: {ex.Message}");
                                }

                                _currentState = ReceiveState.WaitingForFileData; // 다음 상태로 전환
                            }
                            else
                            {
                                // JSON 데이터를 읽을 데이터가 부족하면 루프 종료
                                return;
                            }
                            break;

                        case ReceiveState.WaitingForFileData:
                            // 파일 데이터 크기 계산
                            //Debug.WriteLine($"[상태: {_currentState}] Position={_currentMessageBuffer.Position}, Length={_currentMessageBuffer.Length}");

                            int fileDataSize = _totalMessageSize - _jsonSize;
                            if (fileDataSize < 0) // 예외 처리
                            {
                                OnErrorOccurred($"파일 데이터 크기 계산 오류: FileDataSize={fileDataSize}. 연결을 끊습니다.");
                                Disconnect();
                                return;
                            }

                            // 파일 데이터를 받을 수 있는 충분한 데이터가 있는지 확인
                            if (_currentMessageBuffer.Length - _currentMessageBuffer.Position >= fileDataSize)
                            {
                                byte[] fileDataBytes = new byte[fileDataSize];
                                _currentMessageBuffer.Read(fileDataBytes, 0, fileDataSize); // 파일 크기만큼 데이터 읽기

                                Debug.WriteLine($"파일 데이터 수신: {fileDataSize} 바이트");
                                // TODO: 여기서 파일 데이터를 처리하는 로직 추가
                                // 예: File.WriteAllBytes("received_file.bin", fileDataBytes);

                                // 한 메시지 처리가 완료되었으므로 버퍼 정리 및 상태 초기화
                                // 남은 데이터를 다음 메시지의 시작으로 옮기기
                                CleanupMessageBuffer();

                                _currentState = ReceiveState.WaitingForHeader; // 다음 메시지를 위해 상태 초기화
                                _totalMessageSize = 0;
                                _jsonSize = 0;
                            }
                            else
                            {
                                // 파일 데이터를 읽을 데이터가 부족하면 루프 종료
                                return;
                            }
                            break;
                    }
                }
            }

            private void CleanupMessageBuffer()
            {
                // 현재 읽기 위치(Position)부터 끝까지 남은 데이터의 길이
                long remainingLength = _currentMessageBuffer.Length - _currentMessageBuffer.Position;

                if (remainingLength > 0)
                {
                    // 남은 데이터를 새 버퍼로 옮김
                    byte[] remainingBytes = new byte[remainingLength];
                    _currentMessageBuffer.Read(remainingBytes, 0, (int)remainingLength);

                    // 기존 스트림을 재설정하고 남은 데이터를 다시 씀
                    _currentMessageBuffer.SetLength(0); // 스트림 길이 0으로 리셋
                    _currentMessageBuffer.Position = 0; // ⬅️ 중요!

                    _currentMessageBuffer.Write(remainingBytes, 0, remainingBytes.Length);
                    _currentMessageBuffer.Position = 0; // ⬅️ 여기 반드시 추가!

                }
                else
                {
                    // 남은 데이터가 없으면 스트림을 완전히 비움
                    _currentMessageBuffer.SetLength(0);
                    _currentMessageBuffer.Position = 0; // 위치도 0으로 재설정
                }
            }

            /// 서버로 메시지를 비동기적으로 전송합니다.
            /// <param name="message">전송할 메시지</param>
            /// <returns>전송 성공 여부</returns>

            /// 송신부
            public async Task<bool> SendMessageAsync(string jsonMessage, byte[] fileData = null)
            {
                //Debug.WriteLine("[SendMessageAsync] 진입");

                if (!IsConnected)
                {
                    OnErrorOccurred("오류: 서버에 연결되어 있지 않습니다.");
                    return false;
                }
                if (string.IsNullOrEmpty(jsonMessage))
                {
                    OnErrorOccurred("오류: 보낼 메시지를 입력하세요.");
                    return false;
                }

                try
                {
                    // JSON 바이트 변환
                    byte[] jsonBytes = Encoding.UTF8.GetBytes(jsonMessage);
                    int jsonSize = jsonBytes.Length;
                    int fileSize = fileData?.Length ?? 0;
                    int totalSize = jsonSize; // 오디오 없으면 totalSize = jsonSize

                    // [헤더] 4바이트(totalSize) + 4바이트(jsonSize), 네트워크 바이트 오더
                    byte[] header = new byte[8];
                    Array.Copy(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(totalSize)), 0, header, 0, 4);
                    Array.Copy(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(jsonSize)), 0, header, 4, 4);

                    // 최종 전송 버퍼: header + json
                    byte[] sendBuffer = new byte[8 + jsonSize];
                    Array.Copy(header, 0, sendBuffer, 0, 8);
                    Array.Copy(jsonBytes, 0, sendBuffer, 8, jsonSize);

                    if (fileSize > 0)
                        Array.Copy(fileData, 0, sendBuffer, 8 + jsonSize, fileSize);

                    // 전송
                    await _clientSocket.SendAsync(new ArraySegment<byte>(sendBuffer), SocketFlags.None);

                    Debug.WriteLine($"[전송] 총 {sendBuffer.Length} 바이트 (헤더+JSON)");
                    Debug.WriteLine($"[전송 JSON] {jsonMessage}");
                    return true;
                }
                catch (SocketException ex)
                {
                    OnErrorOccurred($"전송 오류: {ex.Message}");
                    Disconnect();
                }
                catch (ObjectDisposedException)
                {
                    OnErrorOccurred("오류: 소켓이 이미 닫혔습니다. 다시 연결해주세요.");
                    Disconnect();
                }
                catch (Exception ex)
                {
                    OnErrorOccurred($"예기치 않은 전송 오류: {ex.Message}");
                    Disconnect();
                }

                return false;
            }

            /// 서버와의 연결을 끊고 자원을 해제합니다.
            public void Disconnect()
            {
                // 수신 작업을 취소
                _receiveCts?.Cancel();

                if (_clientSocket != null)
                {
                    try
                    {
                        if (_clientSocket.Connected)
                        {
                            _clientSocket.Shutdown(SocketShutdown.Both);
                        }
                        _clientSocket.Close();
                        _clientSocket.Dispose();
                        _clientSocket = null;
                        Debug.WriteLine("서버와의 연결이 끊어졌습니다.");
                        OnConnectionStatusChanged(false);
                    }
                    catch (SocketException ex)
                    {
                        OnErrorOccurred($"연결 해제 오류: {ex.Message}");
                    }
                    catch (Exception ex)
                    {
                        OnErrorOccurred($"예기치 않은 연결 해제 오류: {ex.Message}");
                    }
                }
                _receiveCts?.Dispose();
                _receiveCts = null;
            }

            // 이벤트 발생 도우미 메서드 (null 체크)
            protected virtual void OnConnectionStatusChanged(bool isConnected)
            {
                ConnectionStatusChanged?.Invoke(this, isConnected);
            }

            protected virtual void OnMessageReceived(string message)
            {
                MessageReceived?.Invoke(this, message);
            }

            protected virtual void OnErrorOccurred(string errorMessage)
            {
                ErrorOccurred?.Invoke(this, errorMessage);
            }

            // IDisposable 구현: 객체 소멸 시 자원 해제 보장
            public void Dispose()
            {
                Disconnect(); // Dispose 호출 시 연결 해제
            }
        }




        /// ////////////////////////////////////////////////////////////

        private IPAddress_Local iPAddress = new IPAddress_Local();

        public string path_server { get; private set; }

        private void load_serveraddr(string path_meta)
        {
            try
            {
                if (!File.Exists(path_meta))
                {
                    // MAC 주소가 없는 경우 랜덤 생성
                    string mac = GenerateRandomMac();

                    var initialConfig = new
                    {
                        MAC = mac,
                        IP = "",
                        PORT = 0
                    };
                    File.WriteAllText(path_meta, JsonConvert.SerializeObject(initialConfig, Formatting.Indented));
                    //File.WriteAllText(path_meta, json);
                }

                string read_string = File.ReadAllText(path_meta);
                var read_json = JsonConvert.DeserializeObject<IPAddress_Local>(read_string);

                this.iPAddress = new IPAddress_Local
                {
                    MAC = read_json.MAC,
                    IP = read_json.IP,
                    PORT = read_json.PORT
                };
            }
            catch (Exception ex)
            {
                MessageBox.Show("Config 파일 로딩 실패: " + ex.Message);
            }
        }


        // MAC 및 서버 주소 파일 저장하기
        private void save_serveraddr(string path_meta, string IP, int port)
        {
            Dictionary<string, object> dict_json = new Dictionary<string, object>();

            dict_json.Add("MAC", iPAddress.MAC); // MAC 주소는 이미 iPAddress에 저장되어 있음
            dict_json.Add("IP", IP);
            dict_json.Add("PORT", port);

            string json = JsonConvert.SerializeObject(dict_json);
            File.WriteAllText(path_meta, json);
        }

        private static WaveInEvent GetWaveIn()
        {
            return new NAudio.Wave.WaveInEvent
            {
                DeviceNumber = 0, // 사용할 마이크 장치 인덱스 (0: 기본 장치)
                WaveFormat = new NAudio.Wave.WaveFormat(rate: 1000, bits: 16, channels: 1), // 1kHz, 16비트, Mono
                BufferMilliseconds = 10 // 10ms 단위로 버퍼 처리 (짧을수록 실시간 반응)
            };
        }

        private async void connect_btn_click(object sender, RoutedEventArgs e)
        {
            string ip = textbox_ip.Text.Trim();
            string portText = textbox_port.Text.Trim();

            if (!int.TryParse(portText, out int port))
            {
                MessageBox.Show("유효하지 않은 포트 번호입니다.", "오류", MessageBoxButton.OK, MessageBoxImage.Error);
                return;
            }

            tcpService = new TcpClientService();

            // ✅ NUM_PIN 수신 시 자동 상태조회 전송
            tcpService.OnNumPinReceived = async (pin) =>
            {
                Debug.WriteLine($"[MainWindow] NUM_PIN 수신됨 → 상태 조회 전송 시작");
                await tcpService.SendProtocolAsync(ProtocolName.StatusCheck, pin: pin);
            };

            // 서버 연결 시도
            bool success = await tcpService.ConnectAsync(ip, port, iPAddress.MAC);

            if (success)
            {
                // 서버 연결 성공 시 설정 저장
                iPAddress.IP = ip;
                iPAddress.PORT = port;
                save_serveraddr(path_server, ip, port);
                //Debug.WriteLine("Config 저장됨: " + JsonConvert.SerializeObject(iPAddress));

                //MessageBox.Show("서버 연결 성공 및 JSON 전송 완료", "알림", MessageBoxButton.OK, MessageBoxImage.Information);
                ellipse_status.Fill = Brushes.LimeGreen; // 연결 상태 표시
            }
            else
            {
                MessageBox.Show("연결 실패", "오류", MessageBoxButton.OK, MessageBoxImage.Error);
                ellipse_status.Fill = Brushes.Red; // 연결 실패 시 상태 표시 (빨간색)
            }
        }


        private void StartWavTimer()
        {
            // 중복 실행 방지
            if (wavTimer != null)
            {
                wavTimer.Stop();
                wavTimer.Dispose();
                wavTimer = null;
            }

            wavTimer = new System.Timers.Timer(AUDIO_SEND_PERIOD_SEC * 1000);
            wavTimer.Elapsed += async (s, e) =>
            {
                if (Directory.Exists(wav_file_path)) // 폴더가 유효한지 확인
                {
                    var files = Directory.GetFiles(wav_file_path, "*.wav");
                    if (files.Length > 0)
                    {
                        var randomFile = files[new Random().Next(files.Length)];
                        byte[] wavData = File.ReadAllBytes(randomFile);

                        // JSON 메타데이터 전송
                        await tcpService.SendProtocolAsync(
                            ProtocolName.AudioSend,
                            pin: tcpService.NumPin,
                            size: wavData.Length,
                            time: AUDIO_SEND_PERIOD_SEC,
                            source: "WAV",
                            fileData: wavData
                        );
                        Debug.WriteLine($"[WAV 전송 완료] 파일: {System.IO.Path.GetFileName(randomFile)}, 크기: {wavData.Length} 바이트");
                    }
                    else
                    {
                        Debug.WriteLine("[WAV 전송] 해당 폴더에 .wav 파일이 존재하지 않습니다.");
                    }
                }
                else
                {
                    Debug.WriteLine("[WAV 전송] 설정된 wav_file_path 경로가 존재하지 않습니다.");
                }
            };
            wavTimer.Start();
        }


        private bool IsValidIp(string ip)
        {
            return System.Net.IPAddress.TryParse(ip, out _);
        }

        private bool IsValidPort(string portText, out int port)
        {
            return int.TryParse(portText, out port) && port > 0 && port <= 65535;
        }

        private bool TryConnectToIpPort(string ip, int port)
        {
            try
            {
                using (var client = new TcpClient())
                {
                    client.Connect(ip, port);
                    return true;
                }
            }
            catch
            {
                return false;
            }
        }

        private string wav_file_path; // 폴더 경로 저장 변수

        private void btn_mac_connect_Click(object sender, RoutedEventArgs e)
        {
            if (tcpService == null || !tcpService.IsConnected)
            {
                MessageBox.Show("서버에 연결되어 있지 않습니다. 먼저 서버에 연결해주세요.", "오류", MessageBoxButton.OK, MessageBoxImage.Error);
                return;
            }

            var dialog = new System.Windows.Forms.FolderBrowserDialog();

            if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                wav_file_path = dialog.SelectedPath;
                Debug.WriteLine($"[선택된 폴더] {wav_file_path}");

                // ✅ 폴더 선택 후 WAV 전송 타이머 시작
                StartWavTimer();
            }
        }

        // WAV 파일에서 샘플 추출 (16bit PCM, mono/stereo 지원)
        private float[] ReadWavFileSamples(string filePath)
        {
            using (var reader = new BinaryReader(File.OpenRead(filePath)))
            {
                // WAV 헤더 스킵
                reader.BaseStream.Seek(44, SeekOrigin.Begin);

                var samples = new List<float>();
                while (reader.BaseStream.Position < reader.BaseStream.Length)
                {
                    short sample = reader.ReadInt16();
                    samples.Add(sample / 32768f); // 16bit PCM 정규화
                }
                return samples.ToArray();
            }
        }
        // 파형 그리기 (Canvas 필요)
        private void DrawWaveform(float[] samples)
        {
            canvas_waveform.Children.Clear();

            int width = (int)canvas_waveform.ActualWidth;
            int height = (int)canvas_waveform.ActualHeight;
            if (width == 0 || height == 0) { width = 400; height = 100; }

            Polyline polyline = new Polyline
            {
                Stroke = Brushes.Blue,
                StrokeThickness = 1
            };

            int sampleCount = samples.Length;
            int displayCount = width; // 픽셀 수만큼 샘플 표시
            for (int i = 0; i < displayCount; i++)
            {
                int sampleIndex = i * sampleCount / displayCount;
                float sample = samples[sampleIndex];
                double x = i;
                double y = height / 2 - sample * (height / 2 - 2);
                polyline.Points.Add(new System.Windows.Point(x, y));
            }

            canvas_waveform.Children.Add(polyline);
        }


        private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
        {

        }

        private void TextBox_TextChanged_1(object sender, TextChangedEventArgs e)
        {

        }

        private void radio_mic_Checked(object sender, RoutedEventArgs e)
        {
            btn_browse.IsEnabled = false;
            var waveIn = GetWaveIn();
            ComboBox_mic.Items.Clear();
            ComboBox_mic.Items.Add($"Device {waveIn.DeviceNumber}: {waveIn.WaveFormat.SampleRate}Hz, {waveIn.WaveFormat.BitsPerSample}bit, {waveIn.WaveFormat.Channels}ch");
        }

        private void radio_csv_Checked(object sender, RoutedEventArgs e)
        {
            btn_browse.IsEnabled = true;
        }

        private void SetConnectionStatus(bool isConnected)
        {
            Application.Current.Dispatcher.Invoke(() =>
            {
                ellipse_status.Fill = isConnected ? Brushes.LimeGreen : Brushes.Gray;

            });
        }

        private void StartMicCapture()
        {
            audioBuffer = new MemoryStream();

            waveIn = new WaveInEvent();
            waveIn.DeviceNumber = 0; // 첫 번째 마이크
            waveIn.WaveFormat = new WaveFormat(16000, 1); // 16kHz, 모노

            waveIn.DataAvailable += (s, a) =>
            {
                // 캡처된 소리 데이터를 버퍼에 저장
                audioBuffer.Write(a.Buffer, 0, a.BytesRecorded);
            };

            waveIn.StartRecording();

            micTimer = new System.Timers.Timer(2000); // 2초 타이머
            micTimer.Elapsed += async (s, e) =>
            {
                micTimer.Stop();

                byte[] audioBytes = audioBuffer.ToArray();
                audioBuffer.SetLength(0); // 버퍼 초기화

                // 패킷 생성 및 전송
                //await SendPacketAsync(audioBytes, "MIC", 2);

                micTimer.Start();
            };
            micTimer.Start();
        }

        private void StopMicCapture()
        {
            micTimer?.Stop();
            waveIn?.StopRecording();
            waveIn?.Dispose();
            audioBuffer?.Dispose();
        }

        private void btn_disconnec_Click(object sender, RoutedEventArgs e)
        {
            wavTimer?.Stop(); // WAV 전송 타이머 중지
            ellipse_status.Fill = Brushes.Gray; // 연결 상태 표시 (회색)
            tcpService?.Disconnect(); // TCP 연결 해제
        }
    }
}

'프로젝트' 카테고리의 다른 글

[MyCoach] 문제해결과정 - 체크리스트 생성 로직 수정 (요일 회전 기반)  (3) 2025.08.07
[MyCoach] 1-0-3 요청에 대한 응답도 1-0-3으로 할까?  (1) 2025.08.05
n주차 잘 전송되는 코드  (1) 2025.08.04
복겟몬 조건  (1) 2025.03.17
개인 / C언어 / 로또 프로그램  (0) 2025.03.15
'프로젝트' 카테고리의 다른 글
  • [MyCoach] 1-0-3 요청에 대한 응답도 1-0-3으로 할까?
  • n주차 잘 전송되는 코드
  • 복겟몬 조건
  • 개인 / C언어 / 로또 프로그램
joo_coding
joo_coding
2025.02.18~
  • joo_coding
    주코딩일지
    joo_coding
  • 전체
    오늘
    어제
    • 분류 전체보기 (163) N
      • 일지 (19)
      • 계획표 (7)
      • 프로젝트 (6) N
      • C언어 (35)
        • 연습장 (12)
      • C++ (3)
      • Python (28)
        • 연습장 (11)
      • TCP IP (4)
      • DB (2)
      • ubuntu (1)
      • Git (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    c언어 #vscode #gcc #윈도우 #c언어윈도우 #gcc윈도우 #vscode윈도우 #c #c++
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
joo_coding
0721 머신이어 마이크
상단으로

티스토리툴바