心跳封包

This commit is contained in:
jasonchenwork 2025-06-03 16:52:44 +08:00
parent 4d02d56152
commit c623bbe26d
5 changed files with 269 additions and 115 deletions

View File

@ -1,39 +1,132 @@
using System.Net.Http;
using System.Text;
using System.Text.Json; // 適用於 .NET Core 3.0+ / .NET 5/6/7/8
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text;
using System.Timers; namespace HeartbeatSender
namespace HeartbeatSender{ {
public class HeartbeatData
{
public string branch_name { get; set; }
public string room_name { get; set; }
public string room_ip { get; set; }
public string email { get; set; }
public string password { get; set; }
}
public class heartbeatSender public class heartbeatSender
{ {
private readonly UdpClient udpClient; private readonly HttpClient httpClient = new HttpClient();
private readonly IPEndPoint remoteEndPoint; private string token;
private readonly System.Timers.Timer heartbeatTimer; private string heartbeatUrl;
public IPEndPoint RemoteEndPoint => remoteEndPoint;
public heartbeatSender(string targetIp, int targetPort, int intervalMilliseconds = 3000) public heartbeatSender(string heartbeatUrl)
{ {
udpClient = new UdpClient(); this.heartbeatUrl = heartbeatUrl;
// 設置 IP 和 PORT
remoteEndPoint = new IPEndPoint(IPAddress.Parse(targetIp), targetPort);
// 每3秒發送一次
heartbeatTimer = new System.Timers.Timer(intervalMilliseconds);
heartbeatTimer.Elapsed += SendHeartbeat;
heartbeatTimer.AutoReset = true;
} }
private void SendHeartbeat(object sender, ElapsedEventArgs e) public static string GetLocalIPv4()
{ {
foreach (var ip in Dns.GetHostEntry(Dns.GetHostName()).AddressList)
{
if (ip.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(ip))
{
return ip.ToString();
}
}
return "127.0.0.1"; // fallback
}
public async Task<bool> LoginAndGetTokenAsync()
{
var loginUrl = "http://zqd.superstar.dnsnet.cc/api/room/receiveRegister";
string hostName = System.Net.Dns.GetHostName();
var loginPayload = new
{
branch_name = "測試",
room_name = "PC" + hostName.Substring(Math.Max(0, hostName.Length - 3)),
room_ip = GetLocalIPv4(),
email = "MachineKTV@gmail.com",
password = "aa147258-"
};
var json = JsonSerializer.Serialize(loginPayload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
try try
{ {
string heartbeatMessage = "HEARTBEAT"; var response = await httpClient.PostAsync(loginUrl, content);
byte[] data = Encoding.UTF8.GetBytes(heartbeatMessage); response.EnsureSuccessStatusCode();
udpClient.Send(data, data.Length, remoteEndPoint);
Console.WriteLine($"Heartbeat sent to {remoteEndPoint.Address}:{remoteEndPoint.Port}"); var responseJson = await response.Content.ReadAsStringAsync();
// Console.WriteLine("API 回傳內容:" + responseJson);
using var doc = JsonDocument.Parse(responseJson);
if (doc.RootElement.TryGetProperty("data", out JsonElement dataElement) &&
dataElement.ValueKind == JsonValueKind.Object)
{
if (dataElement.TryGetProperty("token", out JsonElement tokenElement))
{
token = tokenElement.GetString();
}
}
// Console.WriteLine("登入成功,取得 token" + token);
return true;
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Error sending heartbeat: {ex.Message}"); Console.WriteLine($"登入失敗:{ex.Message}");
return false;
} }
} }
public void Start() => heartbeatTimer.Start();
public void Stop() => heartbeatTimer.Stop(); public async Task SendHeartbeatAsync()
{
if (string.IsNullOrEmpty(token))
{
Console.WriteLine("請先登入取得 token");
return;
}
//Console.WriteLine(GetLocalIPv4());
string hostName = System.Net.Dns.GetHostName();
var heartbeatData = new
{
branch_name = "測試",
hostname = "PC" + hostName.Substring(Math.Max(0, hostName.Length - 3)),
ip = GetLocalIPv4(),
};
var json = JsonSerializer.Serialize(heartbeatData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
heartbeatUrl = "http://zqd.superstar.dnsnet.cc/api/room/heartbeat";
var request = new HttpRequestMessage(HttpMethod.Post, heartbeatUrl);
request.Content = content;
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
// Console.WriteLine("送出的 JSON");
// Console.WriteLine(json);
var response = await httpClient.SendAsync(request);
try
{
// Console.WriteLine($"心跳送出狀態:{response.StatusCode}");
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(responseJson);
// Console.WriteLine("API 回傳內容:" + responseJson);
}
catch (Exception ex)
{
var errorContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"送出心跳錯誤:{ex.Message}");
Console.WriteLine($"後端回應內容:{errorContent}");
}
}
} }
}
}

View File

@ -6,14 +6,50 @@ using System.Diagnostics;
namespace DualScreenDemo{ namespace DualScreenDemo{
public partial class PrimaryForm public partial class PrimaryForm
{ {
private static string connectionStringfortest = "";
public static void LoadConnectionStringFromFile(string filePath)
{
filePath = Path.Combine(Application.StartupPath, "txt", filePath);
try
{
if (File.Exists(filePath))
{
string fileContent = File.ReadAllText(filePath).Trim();
if (!string.IsNullOrWhiteSpace(fileContent))
{
connectionStringfortest = fileContent;
Console.WriteLine("連線字串載入成功。");
}
else
{
Console.WriteLine("檔案內容為空。");
}
}
else
{
Console.WriteLine("找不到指定的檔案:" + filePath);
}
}
catch (Exception ex)
{
Console.WriteLine("讀取檔案時發生錯誤:" + ex.Message);
}
}
private static string GetConnectionString()
{
return connectionStringfortest;
}
public bool isLoggedIn = false; public bool isLoggedIn = false;
public string userPhone = string.Empty; public string userPhone = string.Empty;
public List<SongData> SearchSongs_Mysql(string query) public List<SongData> SearchSongs_Mysql(string query)
{ {
List<SongData> searchResults = new List<SongData>(); List<SongData> searchResults = new List<SongData>();
Console.WriteLine(query); Console.WriteLine(query);
string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;"; //string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;";
string connectionString = GetConnectionString();
using (var connection = new MySqlConnection(connectionString)) using (var connection = new MySqlConnection(connectionString))
{ {
Stopwatch stopwatch = new Stopwatch(); Stopwatch stopwatch = new Stopwatch();
@ -65,8 +101,8 @@ namespace DualScreenDemo{
public static List<Artist> SearchSingers_Mysql(string query){ public static List<Artist> SearchSingers_Mysql(string query){
List<Artist> searchResults = new List<Artist>(); List<Artist> searchResults = new List<Artist>();
Console.WriteLine(query); Console.WriteLine(query);
string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;"; //string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;";
string connectionString = GetConnectionString();
using (var connection = new MySqlConnection(connectionString)) using (var connection = new MySqlConnection(connectionString))
{ {
Stopwatch stopwatch = new Stopwatch(); Stopwatch stopwatch = new Stopwatch();
@ -100,8 +136,8 @@ namespace DualScreenDemo{
string query = $"INSERT INTO FavoriteSongs (userPhone,songNumber) VALUES ('{userPhone}','{songNumber}');"; string query = $"INSERT INTO FavoriteSongs (userPhone,songNumber) VALUES ('{userPhone}','{songNumber}');";
Console.WriteLine(query); Console.WriteLine(query);
string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;"; //string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;";
string connectionString = GetConnectionString();
using (var connection = new MySqlConnection(connectionString)) using (var connection = new MySqlConnection(connectionString))
{ {
@ -128,8 +164,8 @@ namespace DualScreenDemo{
string songlist = phonenumber + "的歌單"; string songlist = phonenumber + "的歌單";
string query = $"INSERT INTO FavoriteSongs (userPhone,songNumber) VALUES ('{phonenumber}','{songlist}');"; string query = $"INSERT INTO FavoriteSongs (userPhone,songNumber) VALUES ('{phonenumber}','{songlist}');";
Console.WriteLine(query); Console.WriteLine(query);
string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;"; //string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;";
string connectionString = GetConnectionString();
using (var connection = new MySqlConnection(connectionString)) using (var connection = new MySqlConnection(connectionString))
{ {
@ -163,7 +199,8 @@ namespace DualScreenDemo{
} }
public bool checkPhoneNumberExist(string phonenumber){ public bool checkPhoneNumberExist(string phonenumber){
string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;"; //string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;";
string connectionString = GetConnectionString();
bool exists = false; bool exists = false;
using (var connection = new MySqlConnection(connectionString)) using (var connection = new MySqlConnection(connectionString))
{ {
@ -212,8 +249,8 @@ namespace DualScreenDemo{
public void AddSongCount(string id){ public void AddSongCount(string id){
string query = $"UPDATE songs SET song_counts = song_counts+1 WHERE id = '{id}';"; string query = $"UPDATE songs SET song_counts = song_counts+1 WHERE id = '{id}';";
Console.WriteLine(query); Console.WriteLine(query);
string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;"; //string connectionString = "Server=192.168.11.4;Port=3306;Database=Karaoke-Kingpin;User=Karaoke-Kingpin;Password=ESM7yTPMnavFmbBH;";
string connectionString = GetConnectionString();
using (var connection = new MySqlConnection(connectionString)) using (var connection = new MySqlConnection(connectionString))
{ {

View File

@ -184,31 +184,31 @@ namespace DualScreenDemo
public PrimaryForm() public PrimaryForm()
{ {
Instance = this; Instance = this;
// 添加 DPI 感知支持 // 添加 DPI 感知支持
if (Environment.OSVersion.Version.Major >= 6) if (Environment.OSVersion.Version.Major >= 6)
{ {
SetProcessDPIAware(); SetProcessDPIAware();
} }
this.DoubleBuffered = true; this.DoubleBuffered = true;
InitializeProgressBar(); InitializeProgressBar();
// 初始化自动刷新Timer // 初始化自动刷新Timer
autoRefreshTimer = new Timer(); autoRefreshTimer = new Timer();
autoRefreshTimer.Interval = 1000; // 1秒 autoRefreshTimer.Interval = 1000; // 1秒
autoRefreshTimer.Tick += AutoRefreshTimer_Tick; autoRefreshTimer.Tick += AutoRefreshTimer_Tick;
// 設置窗體屬性 // 設置窗體屬性
this.WindowState = FormWindowState.Maximized; this.WindowState = FormWindowState.Maximized;
this.FormBorderStyle = FormBorderStyle.None; this.FormBorderStyle = FormBorderStyle.None;
lightControlTimer = new Timer(); lightControlTimer = new Timer();
lightControlTimer.Interval = 5; lightControlTimer.Interval = 5;
lightControlTimer.Tick += LightControlTimer_Tick; lightControlTimer.Tick += LightControlTimer_Tick;
volumeUpTimer = new Timer(); volumeUpTimer = new Timer();
volumeUpTimer.Interval = 100; volumeUpTimer.Interval = 100;
volumeUpTimer.Tick += VolumeUpTimer_Tick; volumeUpTimer.Tick += VolumeUpTimer_Tick;
volumeDownTimer = new Timer(); volumeDownTimer = new Timer();
volumeDownTimer.Interval = 100; volumeDownTimer.Interval = 100;
@ -216,12 +216,12 @@ namespace DualScreenDemo
micControlTimer = new Timer(); micControlTimer = new Timer();
micControlTimer.Interval = 100; micControlTimer.Interval = 100;
micControlTimer.Tick += MicControlTimer_Tick; micControlTimer.Tick += MicControlTimer_Tick;
InitializeRecording(); InitializeRecording();
InitializeMediaPlayer(); InitializeMediaPlayer();
LoadSongData(); LoadSongData();
LoadImages(); LoadImages();
InitializeFormAndControls(); InitializeFormAndControls();
InitializeMultiPagePanel(); InitializeMultiPagePanel();
OverlayQRCodeOnImage(HttpServer.randomFolderPath); OverlayQRCodeOnImage(HttpServer.randomFolderPath);
@ -229,16 +229,17 @@ namespace DualScreenDemo
InitializeHandWritingForSongs(); InitializeHandWritingForSongs();
InitializeSendOffPanel(); InitializeSendOffPanel();
InitializePromotionsAndMenuPanel(); InitializePromotionsAndMenuPanel();
SaveInitialControlStates(this); SaveInitialControlStates(this);
// 包廂 + port 名稱 // 包廂 + port 名稱
this.Paint += PrimaryForm_Paint; this.Paint += PrimaryForm_Paint;
this.Paint += User_Paint; this.Paint += User_Paint;
// 註冊多頁面面板的頁碼改變事件處理 // 註冊多頁面面板的頁碼改變事件處理
multiPagePanel.PageIndexChanged += HandlePageChanged; multiPagePanel.PageIndexChanged += HandlePageChanged;
// 添加 Load 事件處理 // 添加 Load 事件處理
this.Load += PrimaryForm_Load; this.Load += PrimaryForm_Load;
LoadConnectionStringFromFile("test.env");
} }

View File

@ -2,6 +2,7 @@ using System.IO;
using Microsoft.Win32; using Microsoft.Win32;
using System.Diagnostics; using System.Diagnostics;
using DBObj; using DBObj;
using HeartbeatSender;
namespace DualScreenDemo namespace DualScreenDemo
{ {
@ -14,76 +15,101 @@ namespace DualScreenDemo
private static PrimaryForm primaryForm; // 儲存實例的參考 private static PrimaryForm primaryForm; // 儲存實例的參考
[STAThread] [STAThread]
static void Main() static void Main()
{
try
{
// COM 初始化
int hr = ComInterop.CoInitializeEx(IntPtr.Zero, ComInterop.COINIT_APARTMENTTHREADED);
if (hr < 0)
{ {
Console.WriteLine("Failed to initialize COM library."); var sender = new HeartbeatSender.heartbeatSender("http://zqd.superstar.dnsnet.cc/api/room/receiveRegister");
return;
}
// 初始化管理器
songListManager = new SongListManager(); // 使用单例 // 同步呼叫非同步登入取得 token
//artistManager = new ArtistManager(); bool loginSuccess = sender.LoginAndGetTokenAsync().GetAwaiter().GetResult();
var commandHandler = new CommandHandler(songListManager); if (loginSuccess)
serialPortManager = new SerialPortManager(commandHandler);
serialPortManager.InitializeSerialPort();
// 輸出屏幕信息
Console.WriteLine($"Virtual Screen: {SystemInformation.VirtualScreen}");
foreach (var screen in Screen.AllScreens)
{
Console.WriteLine($"Screen: {screen.DeviceName} Resolution: {screen.Bounds.Width}x{screen.Bounds.Height}");
}
// 啟動服務器
Task.Run(() => HttpServerManager.StartServer());
Task.Run(() => TCPServerManager.StartServer());
// 註冊事件
Application.ApplicationExit += (sender, e) => SerialPortManager.CloseSerialPortSafely();
SystemEvents.DisplaySettingsChanged += OnDisplaySettingsChanged;
// 創建主窗體
primaryForm = new PrimaryForm();
//primaryForm.allSongs = songListManager.AllSongs;
//primaryForm.allArtists = artistManager.AllArtists;
primaryForm.StartPosition = FormStartPosition.Manual;
primaryForm.Location = new Point(0, 0);
primaryForm.Size = new Size(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
// 在完整初始化後檢查狀態
string stateFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"txt","states.txt");
bool isClosedState = File.Exists(stateFilePath) &&
File.ReadAllText(stateFilePath).Trim().Equals("CLOSE", StringComparison.OrdinalIgnoreCase);
InitializeSecondaryScreen();
// 使用 Shown 事件來確保窗體完全加載後再顯示送客畫面
if (isClosedState)
{
primaryForm.Shown += (s, e) =>
{ {
primaryForm.ShowSendOffScreen(); // 先送一次心跳 (同步呼叫)
}; sender.SendHeartbeatAsync().GetAwaiter().GetResult();
}
primaryForm.Show(); // 背景持續每3秒送心跳
Application.Run(primaryForm); _ = Task.Run(async () =>
} {
catch (Exception ex) while (true)
{ {
WriteLog(ex.ToString()); await sender.SendHeartbeatAsync();
} await Task.Delay(3000); // 每3秒送一次
finally }
{ });
SystemEvents.DisplaySettingsChanged -= OnDisplaySettingsChanged; }
} else
{
Console.WriteLine("登入失敗,無法送出心跳");
}
try
{
// COM 初始化
int hr = ComInterop.CoInitializeEx(IntPtr.Zero, ComInterop.COINIT_APARTMENTTHREADED);
if (hr < 0)
{
Console.WriteLine("Failed to initialize COM library.");
return;
}
// 初始化管理器
songListManager = new SongListManager(); // 使用单例
//artistManager = new ArtistManager();
var commandHandler = new CommandHandler(songListManager);
serialPortManager = new SerialPortManager(commandHandler);
serialPortManager.InitializeSerialPort();
// 輸出屏幕信息
Console.WriteLine($"Virtual Screen: {SystemInformation.VirtualScreen}");
foreach (var screen in Screen.AllScreens)
{
Console.WriteLine($"Screen: {screen.DeviceName} Resolution: {screen.Bounds.Width}x{screen.Bounds.Height}");
}
// 啟動服務器
Task.Run(() => HttpServerManager.StartServer());
Task.Run(() => TCPServerManager.StartServer());
// 註冊事件
Application.ApplicationExit += (sender, e) => SerialPortManager.CloseSerialPortSafely();
SystemEvents.DisplaySettingsChanged += OnDisplaySettingsChanged;
// 創建主窗體
primaryForm = new PrimaryForm();
//primaryForm.allSongs = songListManager.AllSongs;
//primaryForm.allArtists = artistManager.AllArtists;
primaryForm.StartPosition = FormStartPosition.Manual;
primaryForm.Location = new Point(0, 0);
primaryForm.Size = new Size(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
// 在完整初始化後檢查狀態
string stateFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "txt", "states.txt");
bool isClosedState = File.Exists(stateFilePath) &&
File.ReadAllText(stateFilePath).Trim().Equals("CLOSE", StringComparison.OrdinalIgnoreCase);
InitializeSecondaryScreen();
// 使用 Shown 事件來確保窗體完全加載後再顯示送客畫面
if (isClosedState)
{
primaryForm.Shown += (s, e) =>
{
primaryForm.ShowSendOffScreen();
};
}
primaryForm.Show();
Application.Run(primaryForm);
}
catch (Exception ex)
{
WriteLog(ex.ToString());
}
finally
{
SystemEvents.DisplaySettingsChanged -= OnDisplaySettingsChanged;
}
} }

View File

@ -176,9 +176,6 @@
<Compile Update="PrimaryFormParts\SingerSearch\PrimaryForm.SingerSearch.PinyinSearch.cs"> <Compile Update="PrimaryFormParts\SingerSearch\PrimaryForm.SingerSearch.PinyinSearch.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
<Compile Update="PrimaryFormParts\SingerSearch\PrimaryForm.SingerSearch.StrokeCountSearch.cs">
<SubType>Form</SubType>
</Compile>
<Compile Update="PrimaryFormParts\SingerSearch\PrimaryForm.SingerSearch.WordCountSearch.cs"> <Compile Update="PrimaryFormParts\SingerSearch\PrimaryForm.SingerSearch.WordCountSearch.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>