diff --git a/HMIcode/SmallProject/.vs/ProjectEvaluation/smallproject.metadata.v7.bin b/HMIcode/SmallProject/.vs/ProjectEvaluation/smallproject.metadata.v7.bin new file mode 100644 index 0000000..40162aa Binary files /dev/null and b/HMIcode/SmallProject/.vs/ProjectEvaluation/smallproject.metadata.v7.bin differ diff --git a/HMIcode/SmallProject/.vs/ProjectEvaluation/smallproject.projects.v7.bin b/HMIcode/SmallProject/.vs/ProjectEvaluation/smallproject.projects.v7.bin new file mode 100644 index 0000000..070cfdc Binary files /dev/null and b/HMIcode/SmallProject/.vs/ProjectEvaluation/smallproject.projects.v7.bin differ diff --git a/HMIcode/SmallProject/.vs/SmallProject/DesignTimeBuild/.dtbcache.v2 b/HMIcode/SmallProject/.vs/SmallProject/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000..02594a6 Binary files /dev/null and b/HMIcode/SmallProject/.vs/SmallProject/DesignTimeBuild/.dtbcache.v2 differ diff --git a/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/47464208-905c-4968-b7b4-daab71868c10.vsidx b/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/47464208-905c-4968-b7b4-daab71868c10.vsidx new file mode 100644 index 0000000..7c93b3f Binary files /dev/null and b/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/47464208-905c-4968-b7b4-daab71868c10.vsidx differ diff --git a/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/99016061-35df-4ad0-b450-4d2ed0662252.vsidx b/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/99016061-35df-4ad0-b450-4d2ed0662252.vsidx new file mode 100644 index 0000000..44cd1cd Binary files /dev/null and b/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/99016061-35df-4ad0-b450-4d2ed0662252.vsidx differ diff --git a/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/d8c4e8e4-1873-4e77-8586-5052f00c4dfb.vsidx b/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/d8c4e8e4-1873-4e77-8586-5052f00c4dfb.vsidx new file mode 100644 index 0000000..be6bbf5 Binary files /dev/null and b/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/d8c4e8e4-1873-4e77-8586-5052f00c4dfb.vsidx differ diff --git a/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/ec6c225a-3a78-469e-b487-b371c742c8a7.vsidx b/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/ec6c225a-3a78-469e-b487-b371c742c8a7.vsidx new file mode 100644 index 0000000..e3d6f27 Binary files /dev/null and b/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/ec6c225a-3a78-469e-b487-b371c742c8a7.vsidx differ diff --git a/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/f5ecb179-9d7b-4c07-b409-f6e5f7fc6b55.vsidx b/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/f5ecb179-9d7b-4c07-b409-f6e5f7fc6b55.vsidx new file mode 100644 index 0000000..d349136 Binary files /dev/null and b/HMIcode/SmallProject/.vs/SmallProject/FileContentIndex/f5ecb179-9d7b-4c07-b409-f6e5f7fc6b55.vsidx differ diff --git a/HMIcode/SmallProject/.vs/SmallProject/v17/.futdcache.v2 b/HMIcode/SmallProject/.vs/SmallProject/v17/.futdcache.v2 new file mode 100644 index 0000000..8b06c54 Binary files /dev/null and b/HMIcode/SmallProject/.vs/SmallProject/v17/.futdcache.v2 differ diff --git a/HMIcode/SmallProject/OpenBLive/Client/BApiClient.cs b/HMIcode/SmallProject/OpenBLive/Client/BApiClient.cs new file mode 100644 index 0000000..25857bf --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Client/BApiClient.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json; +using OpenBLive.Client.Data; +using OpenBLive.Runtime; + +namespace OpenBLive.Client +{ + public class BApiClient : IBApiClient + { + /// + /// 开启互动玩法 + /// + /// + /// + /// + public async Task StartInteractivePlay(string code, string appId) + { + var respStr = await BApi.StartInteractivePlay(code, appId); + return JsonConvert.DeserializeObject(respStr); + } + + /// + /// 关闭应用 + /// + /// 应用Id + /// 开启应用 返回的gameId + /// + public async Task EndInteractivePlay(string appId, string gameId) + { + var respStr = await BApi.EndInteractivePlay(appId, gameId); + return JsonConvert.DeserializeObject(respStr); + } + + /// + /// 批量应用心跳 + /// + /// 开启应用 返回的gameId + /// + public async Task HeartBeatInteractivePlay(string gameId) + { + var respStr = await BApi.HeartBeatInteractivePlay(gameId); + return JsonConvert.DeserializeObject(respStr); + } + + /// + /// 批量应用心跳 + /// + /// 开启应用 返回的gameId + /// + public async Task BatchHeartBeatInteractivePlay(string[] gameIds) + { + var respStr = await BApi.BatchHeartBeatInteractivePlay(gameIds); + return JsonConvert.DeserializeObject(respStr); + } + } +} diff --git a/HMIcode/SmallProject/OpenBLive/Client/Data/AppStartInfo.cs b/HMIcode/SmallProject/OpenBLive/Client/Data/AppStartInfo.cs new file mode 100644 index 0000000..16948b3 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Client/Data/AppStartInfo.cs @@ -0,0 +1,110 @@ +using Newtonsoft.Json; + +namespace OpenBLive.Client.Data +{ + public class AppStartInfo + { + /// + /// 请求相应 非0为异常case 业务处理 + /// + [JsonProperty("code")] + public int Code; + /// + /// 异常case提示文案 + /// + [JsonProperty("message")] + public string Message; + /// + ///响应体 + /// + [JsonProperty("data")] + public AppStartData Data { get; set; } + + + /// + /// 获取GameId + /// + /// + public string GetGameId() => Data?.GameInfo?.GameId; + /// + /// 获取长链地址 + /// + /// + public IList GetWssLink() => Data?.WebsocketInfo?.WssLink; + + + /// + /// 获取长链地址 + /// + /// + public string GetAuthBody() => Data?.WebsocketInfo?.AuthBody; + } + + + + public class AppStartData + { + /// + /// 场次信息 + /// + [JsonProperty("game_info")] + public AppStartGameInfo GameInfo; + /// + /// 长连信息 + /// + [JsonProperty("websocket_info")] + public AppStartWebsocketInfo WebsocketInfo; + /// + /// 主播信息 + /// + [JsonProperty("anchor_info")] + public AppStartAnchorInfo AnchorInfo; + } + + public class AppStartGameInfo + { + /// + /// 场次id,心跳key(心跳保持20s-60s)调用一次,超过60s无心跳自动关闭,长连停止推送消息 + /// + [JsonProperty("game_id")] + public string GameId; + } + public class AppStartWebsocketInfo + { + /// + /// 长连使用的请求json体 第三方无需关注内容,建立长连时使用即可 + /// + [JsonProperty("auth_body")] + public string AuthBody; + /// + /// wss 长连地址 + /// + [JsonProperty("wss_link")] + public List WssLink; + } + + + public class AppStartAnchorInfo + { + /// + /// 主播房间号 + /// + [JsonProperty("room_id")] + public long RoomId; + /// + /// 主播昵称 + /// + [JsonProperty("uname")] + public string UName; + /// + /// 主播头像 + /// + [JsonProperty("uface")] + public string UFace; + /// + /// 主播Uid + /// + [JsonProperty("uid")] + public string Uid; + } +} diff --git a/HMIcode/SmallProject/OpenBLive/Client/Data/EmptyInfo.cs b/HMIcode/SmallProject/OpenBLive/Client/Data/EmptyInfo.cs new file mode 100644 index 0000000..7059cff --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Client/Data/EmptyInfo.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace OpenBLive.Client.Data +{ + public class EmptyInfo + { + /// + /// 请求相应 非0为异常case 业务处理 + /// + [JsonProperty("code")] + public int Code; + /// + /// 异常case提示文案 + /// + [JsonProperty("message")] + public string Message; + + } +} diff --git a/HMIcode/SmallProject/OpenBLive/Client/IBApiClient.cs b/HMIcode/SmallProject/OpenBLive/Client/IBApiClient.cs new file mode 100644 index 0000000..87598ac --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Client/IBApiClient.cs @@ -0,0 +1,43 @@ +using OpenBLive.Client.Data; + +namespace OpenBLive.Client +{ + /// + /// BapiClient 相关文档 https://open-live.bilibili.com/document/doc&tool/auth.html + /// + public interface IBApiClient + { + /// + /// 开启应用 + /// + /// + /// 身份码 + /// 获取地址 :https://link.bilibili.com/p/center/index#/my-room/start-live + /// + /// + /// AppId + /// 申请的应用Id + /// + /// + Task StartInteractivePlay(string code, string appId); + /// + /// 关闭应用 + /// + /// 应用Id + /// 开启玩法 返回的gameId + /// + Task EndInteractivePlay(string appId, string gameId); + /// + /// 应用心跳 + /// + /// 开启玩法 返回的gameId + /// + Task HeartBeatInteractivePlay(string gameId); + /// + /// 批量应用心跳 + /// + /// 开启玩法 返回的gameId + /// + Task BatchHeartBeatInteractivePlay(string[] gameIds); + } +} diff --git a/HMIcode/SmallProject/OpenBLive/OpenBLive.csproj b/HMIcode/SmallProject/OpenBLive/OpenBLive.csproj new file mode 100644 index 0000000..3b32491 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/OpenBLive.csproj @@ -0,0 +1,23 @@ + + + + Library + net6.0 + enable + enable + + + + 1701;1702;CS8618;CS8625;8603;8600;8601 + + + + 1701;1702;CS8618;CS8625;8603;8600;8601 + + + + + + + + diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/BApi.cs b/HMIcode/SmallProject/OpenBLive/Runtime/BApi.cs new file mode 100644 index 0000000..1436ae9 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/BApi.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OpenBLive.Runtime.Data; +using OpenBLive.Runtime.Utilities; +using Logger = OpenBLive.Runtime.Utilities.Logger; +#if NET5_0_OR_GREATER +using System.Net; +#elif UNITY_2020_3_OR_NEWER +using UnityEngine.Networking; +#endif + +namespace OpenBLive.Runtime +{ + /// + /// 各类b站api + /// + public static class BApi + { + /// + /// 是否为测试环境的api + /// + public static bool isTestEnv; + + /// + /// 开放平台域名 + /// + private static string OpenLiveDomain => + isTestEnv ? "http://test-live-open.biliapi.net" : "https://live-open.biliapi.com"; + + /// + /// 应用开启 + /// + private const string k_InteractivePlayStart = "/v2/app/start"; + + /// + /// 应用关闭 + /// + private const string k_InteractivePlayEnd = "/v2/app/end"; + + /// + /// 应用心跳 + /// + private const string k_InteractivePlayHeartBeat = "/v2/app/heartbeat"; + + /// + /// 应用批量心跳 + /// + private const string k_InteractivePlayBatchHeartBeat = "/v2/app/batchHeartbeat"; + + + private const string k_Post = "POST"; + + + + public static async Task StartInteractivePlay(string code, string appId) + { + var postUrl = OpenLiveDomain + k_InteractivePlayStart; + var param = $"{{\"code\":\"{code}\",\"app_id\":{appId}}}"; + + var result = await RequestWebUTF8(postUrl, k_Post, param); + + return result; + } + + public static async Task EndInteractivePlay(string appId, string gameId) + { + var postUrl = OpenLiveDomain + k_InteractivePlayEnd; + var param = $"{{\"app_id\":{appId},\"game_id\":\"{gameId}\"}}"; + + var result = await RequestWebUTF8(postUrl, k_Post, param); + return result; + } + + public static async Task HeartBeatInteractivePlay(string gameId) + { + var postUrl = OpenLiveDomain + k_InteractivePlayHeartBeat; + string param = ""; + if (gameId != null) + { + param = $"{{\"game_id\":\"{gameId}\"}}"; + + } + + var result = await RequestWebUTF8(postUrl, k_Post, param); + return result; + } + + public static async Task BatchHeartBeatInteractivePlay(string[] gameIds) + { + var postUrl = OpenLiveDomain + k_InteractivePlayBatchHeartBeat; + GameIds games = new GameIds() + { + gameIds = gameIds + }; + var param = JsonConvert.SerializeObject(games); + var result = await RequestWebUTF8(postUrl, k_Post, param); + return result; + } + + private static async Task RequestWebUTF8(string url, string method, string param, + string cookie = null) + { +#if NET5_0_OR_GREATER + string result = ""; + HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); + req.Method = method; + + if (param != null) + { + SignUtility.SetReqHeader(req, param, cookie); + } + + HttpWebResponse httpResponse = (HttpWebResponse)(await req.GetResponseAsync()); + Stream stream = httpResponse.GetResponseStream(); + + if (stream != null) + { + using StreamReader reader = new StreamReader(stream, Encoding.UTF8); + result = await reader.ReadToEndAsync(); + } + + return result; + +#elif UNITY_2020_3_OR_NEWER + UnityWebRequest webRequest = new UnityWebRequest(url); + webRequest.method = method; + if (param != null) + { + SignUtility.SetReqHeader(webRequest, param, cookie); + } + + webRequest.downloadHandler = new DownloadHandlerBuffer(); + webRequest.disposeUploadHandlerOnDispose = true; + webRequest.disposeDownloadHandlerOnDispose = true; + await webRequest.SendWebRequest(); + var text = webRequest.downloadHandler.text; + + webRequest.Dispose(); + return text; +#endif + } +#if UNITY_2020_3_OR_NEWER + private static TaskAwaiter GetAwaiter(this UnityEngine.AsyncOperation asyncOp) + { + var tcs = new TaskCompletionSource(); + asyncOp.completed += _ => { tcs.SetResult(null); }; + return ((Task) tcs.Task).GetAwaiter(); + } +#endif + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/BLiveClient.cs b/HMIcode/SmallProject/OpenBLive/Runtime/BLiveClient.cs new file mode 100644 index 0000000..d1ea89c --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/BLiveClient.cs @@ -0,0 +1,233 @@ +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OpenBLive.Runtime.Data; +using OpenBLive.Runtime.Utilities; + +namespace OpenBLive.Runtime +{ + public delegate void ReceiveDanmakuEvent(Dm dm); + + public delegate void ReceiveGiftEvent(SendGift sendGift); + + public delegate void ReceiveGuardBuyEvent(Guard guard); + + public delegate void ReceiveSuperChatEvent(SuperChat e); + + public delegate void ReceiveSuperChatDelEvent(SuperChatDel e); + + public delegate void ReceiveLikeEvent(Like like); + + public delegate void ReceiveEnterEvent(Enter enter); + + public delegate void ReceiveLiveStartEvent(LiveStart liveStart); + + public delegate void ReceiveLiveEndEvent(LiveEnd liveEnd); + + public delegate void ReceiveRawNotice(string raw, JObject jObject); + + public abstract class BLiveClient : IDisposable + { + private Timer m_Timer; + protected string token; + + /// + /// 弹幕数据 + /// + public event ReceiveDanmakuEvent OnDanmaku; + + /// + /// 赠送礼物 + /// + public event ReceiveGiftEvent OnGift; + + public event ReceiveGuardBuyEvent OnGuardBuy; + + /// + /// SC赠送 + /// + public event ReceiveSuperChatEvent OnSuperChat; + + /// + /// SC删除 + /// + public event ReceiveSuperChatDelEvent OnSuperChatDel; + + /// + /// 点赞信息 + /// + public event ReceiveLikeEvent OnLike; + + /// + /// 进入房间 + /// + public event ReceiveEnterEvent OnEnter; + + /// + /// 开始直播 + /// + public event ReceiveLiveStartEvent OnLiveStart; + + /// + /// 停止直播 + /// + public event ReceiveLiveEndEvent OnLiveEnd; + + /// + /// 原始数据包 + /// + public event ReceiveRawNotice ReceiveNotice; + + /// + /// 更新人气 + /// + public event EventHandler UpdatePopularity; + + public event EventHandler Open; + public abstract void Connect(); + /// + /// 带有重连 + /// + public abstract void Connect(TimeSpan timeout); + public abstract void Disconnect(); + public abstract void Dispose(); + public abstract void Send(byte[] packet); + public abstract Task SendAsync(byte[] packet); + public abstract void Send(Packet packet); + protected abstract Task SendAsync(Packet packet); + + protected virtual void OnOpen() + { + SendAsync(Packet.Authority(token)); + + m_Timer?.Dispose(); + m_Timer = new Timer((e) => ( + (BLiveClient)e)?.SendAsync(Packet.HeartBeat()), this, 0, 30 * 1000); + } +#if UNITY_2021_2_OR_NEWER || NET5_0_OR_GREATER + protected void ProcessPacket(ReadOnlySpan bytes) => + ProcessPacketAsync(new Packet(bytes)); +#else + protected void ProcessPacket(byte[] bytes) => + ProcessPacketAsync(new Packet(bytes)); +#endif + + + private void ProcessPacketAsync(Packet packet) + { + var header = packet.Header; + switch (header.ProtocolVersion) + { + case ProtocolVersion.UnCompressed: + case ProtocolVersion.HeartBeat: + break; + case ProtocolVersion.Zlib: + //no Zlib compress in OpenBLive wss + //await foreach (var packet1 in ZlibDeCompressAsync(packet.PacketBody)) + //ProcessPacketAsync(packet1); + return; + case ProtocolVersion.Brotli: + //no Brotli compress in OpenBLive wss + //await foreach (var packet1 in BrotliDecompressAsync(packet.PacketBody)) + //ProcessPacketAsync(packet1); + return; + default: + throw new NotSupportedException( + "New bilibili danmaku protocol appears, please contact the author if you see this Exception."); + } + + switch (header.Operation) + { + case Operation.AuthorityResponse: + Open?.Invoke(this, null); + break; + case Operation.HeartBeatResponse: + Array.Reverse(packet.PacketBody); + +#if UNITY_2021_2_OR_NEWER || NET5_0_OR_GREATER + var popularity = BitConverter.ToInt32(packet.PacketBody); +#else + var popularity = BitConverter.ToInt32(packet.PacketBody,0); +#endif + + UpdatePopularity?.Invoke(this, popularity); + break; + case Operation.ServerNotify: + try + { + ProcessNotice(Encoding.UTF8.GetString(packet.PacketBody)); + } + catch (Exception) + { + } + break; + // HeartBeat packet request, only send by client + case Operation.HeartBeat: + // This operation key only used for sending authority packet by client + case Operation.Authority: + default: + break; + } + } + + + private void ProcessNotice(string rawMessage) + { + var json = JObject.Parse(rawMessage); + ReceiveNotice?.Invoke(rawMessage, json); + var data = json["data"]?.ToString(); + if (String.IsNullOrWhiteSpace(data)) + return; + Logger.Log($"收到长连发来数据:\n{json?.ToString()}"); + try + { + switch (json["cmd"]?.ToString()) + { + case "LIVE_OPEN_PLATFORM_DM": + var dm = JsonConvert.DeserializeObject(data); + OnDanmaku?.Invoke(dm); + break; + case "LIVE_OPEN_PLATFORM_SUPER_CHAT": + var superChat = JsonConvert.DeserializeObject(data); + OnSuperChat?.Invoke(superChat); + break; + case "LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL": + var superChatDel = JsonConvert.DeserializeObject(data); + OnSuperChatDel?.Invoke(superChatDel); + break; + case "LIVE_OPEN_PLATFORM_SEND_GIFT": + var gift = JsonConvert.DeserializeObject(data); + OnGift?.Invoke(gift); + break; + case "LIVE_OPEN_PLATFORM_GUARD": + var guard = JsonConvert.DeserializeObject(data); + OnGuardBuy?.Invoke(guard); + break; + case "LIVE_OPEN_PLATFORM_LIKE": + var like = JsonConvert.DeserializeObject(data); + OnLike?.Invoke(like); + break; + case "LIVE_OPEN_PLATFORM_LIVE_ROOM_ENTER": + var enter = JsonConvert.DeserializeObject(data); + OnEnter?.Invoke(enter); + break; + case "LIVE_OPEN_PLATFORM_LIVE_START": + var live_start = JsonConvert.DeserializeObject(data); + OnLiveStart?.Invoke(live_start); + break; + case "LIVE_OPEN_PLATFORM_LIVE_END": + var live_end = JsonConvert.DeserializeObject(data); + OnLiveEnd?.Invoke(live_end); + break; + } + } + catch (Exception e) + { + Utilities.Logger.LogError("json数据解析异常 rawMessage: " + rawMessage + e.Message); + } + } + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/AnchorInfo.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/AnchorInfo.cs new file mode 100644 index 0000000..35aac5d --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/AnchorInfo.cs @@ -0,0 +1,32 @@ +using System; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 礼物数据中的主播数据 https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8 + /// + [Serializable] + public struct AnchorInfo + { + /// + /// 主播UID(即将废弃) + /// + [JsonProperty("uid")] public long uid; + + /// + /// 主播open_id + /// + [JsonProperty("open_id")] public string open_id; + + /// + /// 收礼主播昵称 + /// + [JsonProperty("uname")] public string userName; + + /// + /// 收礼主播头像 + /// + [JsonProperty("uface")] public string userFace; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/BToken.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/BToken.cs new file mode 100644 index 0000000..5c9174e --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/BToken.cs @@ -0,0 +1,30 @@ +namespace OpenBLive.Runtime.Data +{ + public struct BToken + { + /// + /// -1=non oauthKey + /// -2=oauthKey not marching + /// -4=未扫码 + /// -5=已扫码 + /// 0=登录成功 + /// + public int code; + /// + /// 登录用户的uid + /// + public long dedeUserID; + /// + /// 用户md5key + /// + public string dedeUserIDCkMd5; + /// + /// accesskey 或 浏览器中的cookie + /// + public string sessData; + /// + /// 登录cookie + /// + public string biliJct; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/Dm.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/Dm.cs new file mode 100644 index 0000000..3af23b1 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/Dm.cs @@ -0,0 +1,69 @@ +using System; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 弹幕数据 https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8 + /// + [Serializable] + public struct Dm + { + /// + /// 用户UID(即将废弃) + /// + [JsonProperty("uid")] public long uid; + + /// + /// 用户open_id + /// + [JsonProperty("open_id")] public string open_id; + + /// + /// 用户昵称 + /// + [JsonProperty("uname")] public string userName; + + /// + /// 用户头像 + /// + [JsonProperty("uface")] public string userFace; + + /// + /// 弹幕发送时间秒级时间戳 + /// + [JsonProperty("timestamp")] public long timestamp; + + + /// + /// 弹幕内容 + /// + [JsonProperty("msg")] public string msg; + + /// + /// 粉丝勋章等级 + /// + [JsonProperty("fans_medal_level")] public long fansMedalLevel; + + /// + /// 粉丝勋章名 + /// + [JsonProperty("fans_medal_name")] public string fansMedalName; + + /// + /// 佩戴的粉丝勋章佩戴状态 + /// + [JsonProperty("fans_medal_wearing_status")] + public bool fansMedalWearingStatus; + + /// + /// 大航海等级 + /// + [JsonProperty("guard_level")] public long guardLevel; + + /// + /// 弹幕接收的直播间 + /// + [JsonProperty("room_id")] public long roomId; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/Enter.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/Enter.cs new file mode 100644 index 0000000..68d6890 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/Enter.cs @@ -0,0 +1,37 @@ +using System; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 进入房间数据 https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8 + /// + [Serializable] + public struct Enter + { + /// + /// 用户昵称 + /// + [JsonProperty("uname")] public string uname; + + /// + /// 用户唯一标识 + /// + [JsonProperty("open_id")] public string open_id; + + /// + /// 用户头像 + /// + [JsonProperty("uface")] public string uface; + + /// + /// 时间秒级时间戳 + /// + [JsonProperty("timestamp")] public long timestamp; + + /// + /// 发生的直播间 + /// + [JsonProperty("room_id")] public long room_id; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/GameIds.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/GameIds.cs new file mode 100644 index 0000000..044dbb0 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/GameIds.cs @@ -0,0 +1,16 @@ +using System; +using Newtonsoft.Json; +namespace OpenBLive.Runtime.Data +{ + /// + /// 互动玩法心跳 https://open-live.bilibili.com/doc/2/1/3 + /// + public struct GameIds + { + /// + /// 玩法场次 + /// + [JsonProperty("game_ids")] + public string[] gameIds; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/Guard.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/Guard.cs new file mode 100644 index 0000000..f1f0970 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/Guard.cs @@ -0,0 +1,53 @@ +using System; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 大航海数据 https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8 + /// + [Serializable] + public struct Guard + { + /// + /// 大航海等级 + /// r + [JsonProperty("guard_level")] public long guardLevel; + + /// + /// 大航海数量 + /// + [JsonProperty("guard_num")] public long guardNum; + + /// + /// 大航海单位(正常单位为“月”,如为其他内容,无视guard_num以本字段内容为准,例如*3天) + /// + [JsonProperty("guard_unit")] public string guardUnit; + + /// + /// 粉丝勋章等级 + /// + [JsonProperty("fans_medal_level")] public long fansMedalLevel; + + /// + /// 粉丝勋章名 + /// + [JsonProperty("fans_medal_name")] public string fansMedalName; + + /// + /// 佩戴的粉丝勋章佩戴状态 + /// + [JsonProperty("fans_medal_wearing_status")] + public bool fansMedalWearingStatus; + + /// + /// 赠送大航海的用户数据 + /// + [JsonProperty("user_info")] public UserInfo userInfo; + + /// + /// 房间号 + /// + [JsonProperty("room_id")] public long roomID; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/Like.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/Like.cs new file mode 100644 index 0000000..6e9e743 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/Like.cs @@ -0,0 +1,67 @@ +using System; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 点赞数据 https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8 + /// + [Serializable] + public struct Like + { + /// + /// 用户昵称 + /// + [JsonProperty("uname")] public string uname; + + /// + /// 用户UID(即将废弃) + /// + [JsonProperty("uid")] public long uid; + + /// + /// 用户唯一标识(2024-03-11后上线) + /// + [JsonProperty("open_id")] public string open_id; + + /// + /// 用户头像 + /// + [JsonProperty("uface")] public string uface; + + /// + /// 时间秒级时间戳 + /// + [JsonProperty("timestamp")] public long timestamp; + + /// + /// 发生的直播间 + /// + [JsonProperty("room_id")] public long room_id; + + /// + /// 点赞文案 + /// + [JsonProperty("like_text")] public string like_text; + + /// + /// 对单个用户最近2秒的点赞次数聚合 + /// + [JsonProperty("like_count")] public long unamelike_count; + + /// + /// 该房间粉丝勋章佩戴情况 + /// + [JsonProperty("fans_medal_wearing_status")] public bool fans_medal_wearing_status; + + /// + /// 粉丝勋章名 + /// + [JsonProperty("fans_medal_name")] public string fans_medal_name; + + /// + /// 对应房间勋章信息 + /// + [JsonProperty("fans_medal_level")] public long fans_medal_level; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/LiveEnd.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LiveEnd.cs new file mode 100644 index 0000000..5905be8 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LiveEnd.cs @@ -0,0 +1,38 @@ +using System; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 结束直播数据 https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8 + /// + [Serializable] + public struct LiveEnd + { + /// + /// 用户唯一标识 + /// + [JsonProperty("open_id")] public string open_id; + + + /// + /// 时间秒级时间戳 + /// + [JsonProperty("timestamp")] public long timestamp; + + /// + /// 发生的直播间 + /// + [JsonProperty("room_id")] public long room_id; + + /// + /// 开播时的标题 + /// + [JsonProperty("title")] public string title; + + /// + /// 开播的分区ID + /// + [JsonProperty("area_id")] public long area_id; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/LiveStart.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LiveStart.cs new file mode 100644 index 0000000..b942420 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LiveStart.cs @@ -0,0 +1,38 @@ +using System; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 开始直播数据 https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8 + /// + [Serializable] + public struct LiveStart + { + /// + /// 用户唯一标识 + /// + [JsonProperty("open_id")] public string open_id; + + + /// + /// 时间秒级时间戳 + /// + [JsonProperty("timestamp")] public long timestamp; + + /// + /// 发生的直播间 + /// + [JsonProperty("room_id")] public long room_id; + + /// + /// 开播时的标题 + /// + [JsonProperty("title")] public string title; + + /// + /// 开播的分区ID + /// + [JsonProperty("area_id")] public long area_id; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginStatusData.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginStatusData.cs new file mode 100644 index 0000000..2933742 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginStatusData.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + public struct LoginStatusData + { + [JsonProperty("url")] + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginStatusReady.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginStatusReady.cs new file mode 100644 index 0000000..725db24 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginStatusReady.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 登录成功 + /// + public struct LoginStatusReady + { + [JsonProperty("code")] public int code; + [JsonProperty("status")] public bool status; + [JsonProperty("data")] public LoginStatusData data; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginStatusScanning.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginStatusScanning.cs new file mode 100644 index 0000000..5de67b9 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginStatusScanning.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 扫码中未登录 + /// + public struct LoginStatusScanning + { + [JsonProperty("status")] public bool status; + [JsonProperty("data")] public int data; + [JsonProperty("message")] public string message; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginUrl.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginUrl.cs new file mode 100644 index 0000000..e4ac305 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginUrl.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + public struct LoginUrl + { + [JsonProperty("data")] + public LoginUrlData data; + [JsonProperty("status")] + public bool status; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginUrlData.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginUrlData.cs new file mode 100644 index 0000000..2b972e2 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/LoginUrlData.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +namespace OpenBLive.Runtime.Data +{ + public struct LoginUrlData + { + [JsonProperty("oauthKey")] + public string oauthKey; + [JsonProperty("url")] + public string url; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/Packet.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/Packet.cs new file mode 100644 index 0000000..83a3560 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/Packet.cs @@ -0,0 +1,138 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +#if UNITY_2021_2_OR_NEWER || NET5_0_OR_GREATER +using System.Buffers; +#else +using System.Net; +#endif + + +namespace OpenBLive.Runtime.Data +{ + public struct Packet + { + private static readonly Packet s_NoBodyHeartBeatPacket = new Packet() + { + Header = new PacketHeader() + { + HeaderLength = PacketHeader.KPacketHeaderLength, + SequenceId = 1, + ProtocolVersion = ProtocolVersion.HeartBeat, + Operation = Operation.HeartBeat + } + }; + + public PacketHeader Header; + + public int Length => Header.PacketLength; + + public byte[] PacketBody; +#if UNITY_2021_2_OR_NEWER || NET5_0_OR_GREATER + public Packet(ReadOnlySpan bytes) + { + var headerBuffer = bytes[0..PacketHeader.KPacketHeaderLength]; + Header = new PacketHeader(headerBuffer); + PacketBody = bytes[Header.HeaderLength..Header.PacketLength].ToArray(); + } +#else + public Packet(byte[] bytes) + { + var headerBuffer = new ArraySegment(bytes, 0, PacketHeader.KPacketHeaderLength); + Header = new PacketHeader(headerBuffer); + var body = new ArraySegment(bytes, Header.HeaderLength, Header.BodyLength).ToArray(); + PacketBody = body; + } +#endif + public Packet(Operation operation, byte[] body = null) + { + Header = new PacketHeader + { + Operation = operation, + ProtocolVersion = ProtocolVersion.UnCompressed, + PacketLength = PacketHeader.KPacketHeaderLength + (body?.Length ?? 0) + }; + PacketBody = body; + } + + public byte[] ToBytes + { + get + { + if (PacketBody != null) + Header.PacketLength = Header.HeaderLength + PacketBody.Length; + else + Header.PacketLength = Header.HeaderLength; + var arr = new byte[Header.PacketLength]; +#if UNITY_2021_2_OR_NEWER || NET5_0_OR_GREATER + Array.Copy(((ReadOnlySpan) Header).ToArray(), arr, Header.HeaderLength); +#else + Array.Copy((byte[]) Header, arr, Header.HeaderLength); +#endif + if (PacketBody != null) + Array.Copy(PacketBody, 0, arr, Header.HeaderLength, PacketBody.Length); + return arr; + } + } + + /// + /// 生成附带msg信息的心跳包 + /// + /// 需要带的信息 + /// 心跳包 + public static Packet HeartBeat(string msg) + { + return HeartBeat(Encoding.UTF8.GetBytes(msg)); + } + + /// + /// 生成附带msg信息的心跳包 + /// + /// 需要带的信息 + /// 心跳包 + public static Packet HeartBeat(byte[] msg = null) + { + if (msg == null) return s_NoBodyHeartBeatPacket; + return new Packet() + { + Header = new PacketHeader() + { + PacketLength = PacketHeader.KPacketHeaderLength + msg.Length, + ProtocolVersion = ProtocolVersion.HeartBeat, + Operation = Operation.HeartBeat, + SequenceId = 1, + HeaderLength = PacketHeader.KPacketHeaderLength + }, + PacketBody = msg + }; + } + + /// + /// 生成验证用数据包 + /// + /// http请求获取的token + /// 协议版本 + /// 验证请求数据包 + public static Packet Authority(string token, + ProtocolVersion protocolVersion = ProtocolVersion.Brotli) + { + var obj = Encoding.UTF8.GetBytes(token); + + return new Packet + { + Header = new PacketHeader + { + Operation = Operation.Authority, + ProtocolVersion = ProtocolVersion.HeartBeat, + SequenceId = 1, + HeaderLength = PacketHeader.KPacketHeaderLength, + PacketLength = PacketHeader.KPacketHeaderLength + obj.Length + }, + PacketBody = obj + }; + } + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/PacketHeader.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/PacketHeader.cs new file mode 100644 index 0000000..37b71d1 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/PacketHeader.cs @@ -0,0 +1,180 @@ +using System; +#if UNITY_2021_2_OR_NEWER || NET5_0_OR_GREATER +using System.Buffers.Binary; +#else +using System.Net; +using OpenBLive.Runtime.Utilities; +#endif + +namespace OpenBLive.Runtime.Data +{ + /// + /// 弹幕数据包头部 + /// + public struct PacketHeader + { + public const int KPacketHeaderLength = 16; + + public int PacketLength; + public short HeaderLength; + public ProtocolVersion ProtocolVersion; + public Operation Operation; + public int SequenceId; + + public int BodyLength => PacketLength - HeaderLength; + + +#if UNITY_2021_2_OR_NEWER || NET5_0_OR_GREATER + /// + /// 构造方法 + /// + /// 弹幕头16字节 + public PacketHeader(ReadOnlySpan bytes) + { + if (bytes.Length < KPacketHeaderLength) throw new ArgumentException("No Supported Protocol Header"); + + var b = bytes; + PacketLength = BinaryPrimitives.ReadInt32BigEndian(b[0..4]); + HeaderLength = BinaryPrimitives.ReadInt16BigEndian(b[4..6]); + ProtocolVersion = (ProtocolVersion)BinaryPrimitives.ReadInt16BigEndian(b[6..8]); + Operation = (Operation)BinaryPrimitives.ReadInt32BigEndian(b[8..12]); + SequenceId = BinaryPrimitives.ReadInt32BigEndian(b[12..16]); + } + + /// + /// 生成弹幕协议的头部 + /// + /// 所对应的弹幕头部byte数组 + public static explicit operator ReadOnlySpan(PacketHeader header) => GetBytes(header.PacketLength, + header.HeaderLength, header.ProtocolVersion, header.Operation, header.SequenceId); + + /// + /// 生成弹幕协议的头部 + /// + /// 消息数据包长度 + /// 头部长度 + /// 弹幕协议版本 + /// 数据包操作 + /// 序列号 + /// + public static byte[] GetBytes(int packetLength, short headerLength, ProtocolVersion protocolVersion, + Operation operation, int sequenceId = 1) + { + var bytes = new byte[KPacketHeaderLength].AsSpan(); + BinaryPrimitives.WriteInt32BigEndian(bytes[0..4], packetLength); + BinaryPrimitives.WriteInt16BigEndian(bytes[4..6], headerLength); + BinaryPrimitives.WriteInt16BigEndian(bytes[6..8], (short)protocolVersion); + BinaryPrimitives.WriteInt32BigEndian(bytes[8..12], (int)operation); + BinaryPrimitives.WriteInt32BigEndian(bytes[12..16], sequenceId); + + return bytes.ToArray(); + } +#else + /// + /// 构造方法 + /// + /// 弹幕头16字节 + public PacketHeader(ArraySegment bytes) + { + if (bytes.Count < KPacketHeaderLength) throw new ArgumentException("No Supported Protocol Header"); + + var b = bytes.Array; + PacketLength = EndianBitConverter.BigEndian.ToInt32(b, 0); + HeaderLength = EndianBitConverter.BigEndian.ToInt16(b, 4); + ProtocolVersion = (ProtocolVersion) EndianBitConverter.BigEndian.ToInt16(b, 6); + Operation = (Operation) EndianBitConverter.BigEndian.ToInt32(b, 8); + SequenceId = EndianBitConverter.BigEndian.ToInt32(b, 12); + } + + /// + /// 生成弹幕协议的头部 + /// + /// 所对应的弹幕头部byte数组 + public static explicit operator byte[](PacketHeader header) => GetBytes(header.PacketLength, + header.HeaderLength, header.ProtocolVersion, header.Operation, header.SequenceId); + + /// + /// 生成弹幕协议的头部 + /// + /// 消息数据包长度 + /// 头部长度 + /// 弹幕协议版本 + /// 数据包操作 + /// 序列号 + /// + public static byte[] GetBytes(int packetLength, short headerLength, ProtocolVersion protocolVersion, + Operation operation, int sequenceId = 1) + { + var bytes = new byte[KPacketHeaderLength]; + var pl = EndianBitConverter.BigEndian.GetBytes(packetLength); + var hl = EndianBitConverter.BigEndian.GetBytes(headerLength); + var pv = EndianBitConverter.BigEndian.GetBytes((short) protocolVersion); + var ot = EndianBitConverter.BigEndian.GetBytes((int) operation); + var si = EndianBitConverter.BigEndian.GetBytes(sequenceId); + Buffer.BlockCopy(pl, 0, bytes, 0, pl.Length); + Buffer.BlockCopy(hl, 0, bytes, pl.Length, hl.Length); + Buffer.BlockCopy(pv, 0, bytes, pl.Length + hl.Length, pv.Length); + Buffer.BlockCopy(ot, 0, bytes, pl.Length + hl.Length + pv.Length, ot.Length); + Buffer.BlockCopy(si, 0, bytes, pl.Length + hl.Length + pv.Length + ot.Length, si.Length); + return bytes; + } +#endif + } + + /// + /// 操作数据 + /// + public enum Operation + { + /// + /// 心跳包 + /// + HeartBeat = 2, + + /// + /// 服务器心跳回应(包含人气信息) + /// + HeartBeatResponse = 3, + + /// + /// 服务器消息(正常消息) + /// + ServerNotify = 5, + + /// + /// 客户端认证请求 + /// + Authority = 7, + + /// + /// 认证回应 + /// + AuthorityResponse = 8 + } + + /// + /// 弹幕协议版本 + /// + public enum ProtocolVersion + { + /// + /// 未压缩数据 + /// + UnCompressed = 0, + + /// + /// 心跳数据 + /// + HeartBeat = 1, + + /// + /// zlib数据 + /// + Zlib = 2, + + /// + /// Br数据 + /// + Brotli = 3 + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/SendGift.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/SendGift.cs new file mode 100644 index 0000000..3a2cbbc --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/SendGift.cs @@ -0,0 +1,93 @@ +using System; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 礼物数据 https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8 + /// + [Serializable] + public struct SendGift + { + /// + /// 房间号 + /// + [JsonProperty("room_id")] public long roomId; + + /// + /// 购买的用户UID(即将废弃) + /// + [JsonProperty("uid")] public long uid; + + /// + /// 购买的用户open_id + /// + [JsonProperty("open_id")] public string open_id; + + /// + /// 送礼用户昵称 + /// + [JsonProperty("uname")] public string userName; + + /// + /// 送礼用户头像 + /// + [JsonProperty("uface")] public string userFace; + + /// + /// 道具id(盲盒:爆出道具id) + /// + [JsonProperty("gift_id")] public long giftId; + + /// + /// 道具名(盲盒:爆出道具名) + /// + [JsonProperty("gift_name")] public string giftName; + + /// + /// 赠送道具数量 + /// + [JsonProperty("gift_num")] public long giftNum; + + /// + /// 支付金额(1000 = 1元 = 10电池),盲盒:爆出道具的价值 + /// + [JsonProperty("price")] public long price; + + /// + /// 是否真的花钱(电池道具) + /// + [JsonProperty("paid")] public bool paid; + + /// + /// 粉丝勋章等级 + /// + [JsonProperty("fans_medal_level")] public long fansMedalLevel; + + /// + /// 粉丝勋章名 + /// + [JsonProperty("fans_medal_name")] public string fansMedalName; + + /// + /// 佩戴的粉丝勋章佩戴状态 + /// + [JsonProperty("fans_medal_wearing_status")] + public bool fansMedalWearingStatus; + + /// + /// 大航海等级 + /// + [JsonProperty("guard_level")] public long guardLevel; + + /// + /// 收礼时间秒级时间戳 + /// + [JsonProperty("timestamp")] public long timestamp; + + /// + /// 主播信息 + /// + [JsonProperty("anchor_info")] public AnchorInfo anchorInfo; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/SuperChat.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/SuperChat.cs new file mode 100644 index 0000000..02ee2fd --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/SuperChat.cs @@ -0,0 +1,88 @@ +using System; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 付费留言数据 https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8 + /// + [Serializable] + public struct SuperChat + { + /// + /// 直播间ID + /// + [JsonProperty("room_id")] public long roomId; + + /// + /// 购买的用户UID(即将废弃) + /// + [JsonProperty("uid")] public long uid; + + /// + /// 购买的用户open_id + /// + [JsonProperty("open_id")] public string open_id; + + /// + /// 购买的用户昵称 + /// + [JsonProperty("uname")] public string userName; + + /// + /// 购买用户头像 + /// + [JsonProperty("uface")] public string userFace; + + /// + /// 留言id(风控场景下撤回留言需要) + /// + [JsonProperty("message_id")] public long messageId; + + /// + /// 留言内容 + /// + [JsonProperty("message")] public string message; + + /// + /// 支付金额(元) + /// + [JsonProperty("rmb")] public long rmb; + + /// + /// 赠送时间秒级 + /// + [JsonProperty("timestamp")] public long timeStamp; + + /// + /// 生效开始时间 + /// + [JsonProperty("start_time")] public long startTime; + + /// + /// 生效结束时间 + /// + [JsonProperty("end_time")] public long endTime; + + /// + /// 对应房间大航海等级 + /// + [JsonProperty("guard_level")] public long guardLevel; + + /// + /// 对应房间勋章信息 + /// + [JsonProperty("fans_medal_level")] public long fansMedalLevel; + + /// + /// 对应房间勋章名字 + /// + [JsonProperty("fans_medal_name")] public string fansMedalName; + + /// + /// 当前佩戴的粉丝勋章佩戴状态 + /// + [JsonProperty("fans_medal_wearing_status")] + public bool fansMedalWearingStatus; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/SuperChatDel.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/SuperChatDel.cs new file mode 100644 index 0000000..f6920da --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/SuperChatDel.cs @@ -0,0 +1,22 @@ +using System; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 付费留言数据下线数据 https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8 + /// + [Serializable] + public struct SuperChatDel + { + /// + /// 直播间ID + /// + [JsonProperty("room_id")] public long roomId; + + /// + /// 留言id + /// + [JsonProperty("message_ids")] public long[] messageIds; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/UserInfo.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/UserInfo.cs new file mode 100644 index 0000000..ecdbb0f --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/UserInfo.cs @@ -0,0 +1,32 @@ +using System; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 赠送大航海的用户数据 https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8 + /// + [Serializable] + public struct UserInfo + { + /// + /// 购买大航海的用户UID(即将废弃) + /// + [JsonProperty("uid")] public long uid; + + /// + /// 购买大航海的用户open_id + /// + [JsonProperty("open_id")] public string open_id; + + /// + /// 购买大航海的用户昵称 + /// + [JsonProperty("uname")] public string userName; + + /// + /// 购买大航海的用户头像 + /// + [JsonProperty("uface")] public string userFace; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/WebsocketInfo.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/WebsocketInfo.cs new file mode 100644 index 0000000..808ecb1 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/WebsocketInfo.cs @@ -0,0 +1,12 @@ +namespace OpenBLive.Runtime.Data +{ + /// + /// 服务器返回的Websocket长链接信息 https://open-live.bilibili.com/document/657d8e34-f926-a133-16c0-300c1afc6e6b + /// + public struct WebsocketInfo + { + public int code; + public string message; + public WebsocketInfoData data; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Data/WebsocketInfoData.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Data/WebsocketInfoData.cs new file mode 100644 index 0000000..3f39856 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Data/WebsocketInfoData.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace OpenBLive.Runtime.Data +{ + /// + /// 服务器返回的Websocket长链接信息 https://open-live.bilibili.com/document/657d8e34-f926-a133-16c0-300c1afc6e6b + /// + public struct WebsocketInfoData + { + /// + /// ip地址 + /// + [JsonProperty("ip")] + public List ip; + /// + /// host地址 可能是ip 也可能是域名 + /// + [JsonProperty("host")] + public List host; + /// + /// 长连使用的请求json体 第三方无需关注内容,建立长连时使用即可 + /// + [JsonProperty("auth_body")] + public string authBody; + /// + /// tcp 端口号 + /// + [JsonProperty("tcp_port")] + public List tcpPort; + /// + /// ws 端口号 + /// + [JsonProperty("ws_port")] + public List wsPort; + /// + /// wss 端口号 + /// + [JsonProperty("wss_port")] + public List wssPort; + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/InteractivePlayHeartBeat.cs b/HMIcode/SmallProject/OpenBLive/Runtime/InteractivePlayHeartBeat.cs new file mode 100644 index 0000000..c00737f --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/InteractivePlayHeartBeat.cs @@ -0,0 +1,98 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenBLive.Runtime +{ + public delegate void HeartBeatSucceed(); + + public delegate void HeartBeatError(string json); + + public class InteractivePlayHeartBeat : IDisposable + { + public event HeartBeatSucceed HeartBeatSucceed; + public event HeartBeatError HeartBeatError; + private readonly CancellationTokenSource m_Cancellation; + private readonly string[] m_GameIds; + private readonly int m_Time; + + public InteractivePlayHeartBeat(string gameId, int time = 20000, CancellationTokenSource cancellation = null) + { + m_GameIds = new[] { gameId }; + m_Time = time; + m_Cancellation = cancellation ?? new CancellationTokenSource(); + } + + public InteractivePlayHeartBeat(string[] gameIds, int time = 20000, CancellationTokenSource cancellation = null) + { + m_GameIds = gameIds; + m_Time = time; + m_Cancellation = cancellation ?? new CancellationTokenSource(); + } + + private async Task HeartBeatTask() + { + while (true) + { + try + { + string res = ""; + if (m_GameIds.Length == 1) + { + res = await BApi.HeartBeatInteractivePlay(m_GameIds[0]); + } + else + { + res = await BApi.BatchHeartBeatInteractivePlay(m_GameIds); + } + + var json = JObject.Parse(res); + var code = json["code"]!.ToObject(); + if (code == 0) + { + HeartBeatSucceed?.Invoke(); + } + else + { + HeartBeatError?.Invoke(res); + return; + } + } + catch (Exception e) + { + HeartBeatError?.Invoke(e.Message); + return; + } + + switch (m_Cancellation) + { + case { IsCancellationRequested: true }: + case null: + return; + default: + //默认20秒一次心跳 + await Task.Delay(m_Time); + break; + } + } + } + + public void Start() + { + var task = HeartBeatTask(); + if (task.Status == TaskStatus.Created) + task.Start(); + } + + public void Stop() + { + m_Cancellation.Cancel(); + } + + public void Dispose() + { + m_Cancellation.Dispose(); + } + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Textures/22.png b/HMIcode/SmallProject/OpenBLive/Runtime/Textures/22.png new file mode 100644 index 0000000..817ce49 Binary files /dev/null and b/HMIcode/SmallProject/OpenBLive/Runtime/Textures/22.png differ diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/BigEndianBitConverter.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/BigEndianBitConverter.cs new file mode 100644 index 0000000..61edcda --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/BigEndianBitConverter.cs @@ -0,0 +1,57 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +namespace OpenBLive.Runtime.Utilities +{ + /// + /// A big-endian BitConverter that converts base data types to an array of bytes, and an array of bytes to base data types. All conversions are in + /// big-endian format regardless of machine architecture. + /// + internal class BigEndianBitConverter : EndianBitConverter + { + // Instance available from EndianBitConverter.BigEndian + internal BigEndianBitConverter() { } + + public override bool IsLittleEndian { get; } = false; + + public override byte[] GetBytes(short value) + { + return new byte[] { (byte)(value >> 8), (byte)value }; + } + + public override byte[] GetBytes(int value) + { + return new byte[] { (byte)(value >> 24), (byte)(value >> 16), (byte)(value >> 8), (byte)value }; + } + + public override byte[] GetBytes(long value) + { + return new byte[] { + (byte)(value >> 56), (byte)(value >> 48), (byte)(value >> 40), (byte)(value >> 32), + (byte)(value >> 24), (byte)(value >> 16), (byte)(value >> 8), (byte)value + }; + } + + public override short ToInt16(byte[] value, int startIndex) + { + this.CheckArguments(value, startIndex, sizeof(short)); + + return (short)((value[startIndex] << 8) | (value[startIndex + 1])); + } + + public override int ToInt32(byte[] value, int startIndex) + { + this.CheckArguments(value, startIndex, sizeof(int)); + + return (value[startIndex] << 24) | (value[startIndex + 1] << 16) | (value[startIndex + 2] << 8) | (value[startIndex + 3]); + } + + public override long ToInt64(byte[] value, int startIndex) + { + this.CheckArguments(value, startIndex, sizeof(long)); + + int highBytes = (value[startIndex] << 24) | (value[startIndex + 1] << 16) | (value[startIndex + 2] << 8) | (value[startIndex + 3]); + int lowBytes = (value[startIndex + 4] << 24) | (value[startIndex + 5] << 16) | (value[startIndex + 6] << 8) | (value[startIndex + 7]); + return ((uint)lowBytes | ((long)highBytes << 32)); + } + } +} diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/EndianBitConverter.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/EndianBitConverter.cs new file mode 100644 index 0000000..acd4f7c --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/EndianBitConverter.cs @@ -0,0 +1,345 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +namespace OpenBLive.Runtime.Utilities +{ + using System; + using System.Runtime.CompilerServices; + + /// + /// A BitConverter with a specific endianness that converts base data types to an array of bytes, and an array of bytes to base data types, regardless of + /// machine architecture. Access the little-endian and big-endian converters with their respective properties. + /// + /// + /// The EndianBitConverter implementations provide the same interface as , but exclude those methods which perform the + /// same on both big-endian and little-endian machines (such as ). However, is + /// included for consistency. + /// + public abstract class EndianBitConverter + { + /// + /// Get an instance of a , a BitConverter which performs all conversions in little-endian format regardless of + /// machine architecture. + /// + public static EndianBitConverter LittleEndian { get; } = new LittleEndianBitConverter(); + + /// + /// Get an instance of a , a BitConverter which performs all conversions in big-endian format regardless of + /// machine architecture. + /// + public static EndianBitConverter BigEndian { get; } = new BigEndianBitConverter(); + + /// + /// Indicates the byte order ("endianness") in which data should be converted. + /// + public abstract bool IsLittleEndian { get; } + + /// + /// Returns the specified Boolean value as a byte array. + /// + /// A Boolean value. + /// A byte array with length 1. + /// You can convert a byte array back to a value by calling the method. + public byte[] GetBytes(bool value) + { + return new byte[] { value ? (byte)1 : (byte)0 }; + } + + /// + /// Returns the specified Unicode character value as an array of bytes. + /// + /// A character to convert. + /// An array of bytes with length 2. + /// You can convert a byte array back to a value by calling the method. + public byte[] GetBytes(char value) + { + return this.GetBytes((short)value); + } + + /// + /// Returns the specified double-precision floating point value as an array of bytes. + /// + /// The number to convert. + /// An array of bytes with length 8. + /// You can convert a byte array back to a value by calling the method. + public byte[] GetBytes(double value) + { + long val = BitConverter.DoubleToInt64Bits(value); + return this.GetBytes(val); + } + + /// + /// Returns the specified 16-bit signed integer value as an array of bytes. + /// + /// The number to convert. + /// An array of bytes with length 2. + /// You can convert a byte array back to a value by calling the method. + public abstract byte[] GetBytes(short value); + + /// + /// Returns the specified 32-bit signed integer value as an array of bytes. + /// + /// The number to convert. + /// An array of bytes with length 4. + /// You can convert a byte array back to a value by calling the method. + public abstract byte[] GetBytes(int value); + + /// + /// Returns the specified 64-bit signed integer value as an array of bytes. + /// + /// The number to convert. + /// An array of bytes with length 8. + /// You can convert a byte array back to a value by calling the method. + public abstract byte[] GetBytes(long value); + + /// + /// Returns the specified single-precision floating point value as an array of bytes. + /// + /// The number to convert. + /// An array of bytes with length 4. + /// You can convert a byte array back to a value by calling the method. + public byte[] GetBytes(float value) + { + int val = new SingleConverter(value).GetIntValue(); + return this.GetBytes(val); + } + + /// + /// Returns the specified 16-bit unsigned integer value as an array of bytes. + /// + /// The number to convert. + /// An array of bytes with length 2. + /// You can convert a byte array back to a value by calling the method. + + public byte[] GetBytes(ushort value) + { + return this.GetBytes((short)value); + } + + /// + /// Returns the specified 32-bit unsigned integer value as an array of bytes. + /// + /// The number to convert. + /// An array of bytes with length 4. + /// You can convert a byte array back to a value by calling the method. + + public byte[] GetBytes(uint value) + { + return this.GetBytes((int)value); + } + + /// + /// Returns the specified 64-bit unsigned integer value as an array of bytes. + /// + /// The number to convert. + /// An array of bytes with length 8. + /// You can convert a byte array back to a value by calling the method. + + public byte[] GetBytes(ulong value) + { + return this.GetBytes((long)value); + } + + /// + /// Returns a Boolean value converted from the byte at a specified position in a byte array. + /// + /// A byte array. + /// The index of the byte within . + /// + /// true if the byte at in is nonzero; otherwise, false. + /// + /// is null. + /// + /// is less than zero or greater than the length of minus 1. + /// + public bool ToBoolean(byte[] value, int startIndex) + { + this.CheckArguments(value, startIndex, 1); + + return value[startIndex] != 0; + } + + /// + /// Returns a Unicode character converted from two bytes at a specified position in a byte array. + /// + /// An array. + /// The starting position within . + /// A character formed by two bytes beginning at . + /// + /// The ToChar method converts the bytes from index to + 1 to a value. + /// + /// is null. + /// + /// is less than zero or greater than the length of minus 2. + /// + public char ToChar(byte[] value, int startIndex) + { + return (char)this.ToInt16(value, startIndex); + } + + /// + /// Returns a double-precision floating point number converted from eight bytes at a specified position in a byte array. + /// + /// An array of bytes. + /// The starting position within . + /// A double precision floating point number formed by eight bytes beginning at . + /// + /// The ToDouble method converts the bytes from index to + 7 to a + /// value. + /// + /// is null. + /// + /// is less than zero or greater than the length of minus 8. + /// + public double ToDouble(byte[] value, int startIndex) + { + long val = this.ToInt64(value, startIndex); + return BitConverter.Int64BitsToDouble(val); + } + + /// + /// Returns a 16-bit signed integer converted from two bytes at a specified position in a byte array. + /// + /// An array of bytes. + /// The starting position within . + /// A 16-bit signed integer formed by two bytes beginning at . + /// + /// The ToInt16 method converts the bytes from index to + 1 to an + /// value. + /// + /// is null. + /// + /// is less than zero or greater than the length of minus 2. + /// + public abstract short ToInt16(byte[] value, int startIndex); + + /// + /// Returns a 32-bit signed integer converted from four bytes at a specified position in a byte array. + /// + /// An array of bytes. + /// The starting position within . + /// A 32-bit signed integer formed by four bytes beginning at . + /// + /// The ToInt32 method converts the bytes from index to + 3 to an + /// value. + /// + /// is null. + /// + /// is less than zero or greater than the length of minus 4. + /// + public abstract int ToInt32(byte[] value, int startIndex); + + /// + /// Returns a 64-bit signed integer converted from eight bytes at a specified position in a byte array. + /// + /// An array of bytes. + /// The starting position within . + /// A 64-bit signed integer formed by eight bytes beginning at . + /// + /// The ToInt64 method converts the bytes from index to + 7 to an + /// value. + /// + /// is null. + /// + /// is less than zero or greater than the length of minus 8. + /// + public abstract long ToInt64(byte[] value, int startIndex); + + /// + /// Returns a single-precision floating point number converted from four bytes at a specified position in a byte array. + /// + /// An array of bytes. + /// The starting position within . + /// A single-precision floating point number formed by four bytes beginning at . + /// + /// The ToSingle method converts the bytes from index to + 3 to a + /// value. + /// + /// is null. + /// + /// is less than zero or greater than the length of minus 4. + /// + public float ToSingle(byte[] value, int startIndex) + { + int val = this.ToInt32(value, startIndex); + return new SingleConverter(val).GetFloatValue(); + } + + /// + /// Returns a 16-bit unsigned integer converted from two bytes at a specified position in a byte array. + /// + /// + /// The starting position within . + /// A 16-bit unsigned integer formed by two bytes beginning at . + /// + /// The ToUInt16 method converts the bytes from index to + 1 to a + /// value. + /// + /// is null. + /// + /// is less than zero or greater than the length of minus 2. + /// + + public ushort ToUInt16(byte[] value, int startIndex) + { + return (ushort)this.ToInt16(value, startIndex); + } + + /// + /// Returns a 32-bit unsigned integer converted from four bytes at a specified position in a byte array. + /// + /// An array of bytes. + /// The starting position within . + /// A 32-bit unsigned integer formed by four bytes beginning at . + /// + /// The ToUInt32 method converts the bytes from index to + 3 to a + /// value. + /// + /// is null. + /// + /// is less than zero or greater than the length of minus 4. + /// + + public uint ToUInt32(byte[] value, int startIndex) + { + return (uint)this.ToInt32(value, startIndex); + } + + /// + /// Returns a 64-bit unsigned integer converted from eight bytes at a specified position in a byte array. + /// + /// An array of bytes. + /// The starting position within . + /// A 64-bit unsigned integer formed by the eight bytes beginning at . + /// + /// The ToUInt64 method converts the bytes from index to + 7 to a + /// value. + /// + /// is null. + /// + /// is less than zero or greater than the length of minus 8. + /// + + public ulong ToUInt64(byte[] value, int startIndex) + { + return (ulong)this.ToInt64(value, startIndex); + } + + // Testing showed that this method wasn't automatically being inlined, and doing so offers a significant performance improvement. +#if !NET40 + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal void CheckArguments(byte[] value, int startIndex, int byteLength) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + // confirms startIndex is not negative or too far along the byte array + if ((uint)startIndex > value.Length - byteLength) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + } + } +} diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/HttpUtility.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/HttpUtility.cs new file mode 100644 index 0000000..5922364 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/HttpUtility.cs @@ -0,0 +1,109 @@ +#if UNITY_2020_3_OR_NEWER +using System; +using System.Collections.Specialized; +using System.Text; +using UnityEngine.Networking; + +namespace OpenBLive.Runtime.Utilities +{ + public static class HttpUtility + { + private sealed class HttpQsCollection : NameValueCollection + { + public override string ToString () + { + int count = Count; + if (count == 0) + return ""; + StringBuilder sb = new StringBuilder (); + string [] keys = AllKeys; + for (int i = 0; i < count; i++) { + sb.AppendFormat ("{0}={1}&", keys [i], this [keys [i]]); + } + if (sb.Length > 0) + sb.Length--; + return sb.ToString (); + } + } + public static NameValueCollection ParseQueryString(string query) + { + return ParseQueryString (query, Encoding.UTF8); + } + + private static NameValueCollection ParseQueryString (string query, Encoding encoding) + { + if (query == null) + throw new ArgumentNullException ("query"); + if (encoding == null) + throw new ArgumentNullException ("encoding"); + if (query.Length == 0 || (query.Length == 1 && query[0] == '?')) + return new HttpQsCollection (); + if (query[0] == '?') + query = query.Substring (1); + + NameValueCollection result = new HttpQsCollection (); + ParseQueryString (query, encoding, result); + return result; + } + + private static void ParseQueryString(string query, Encoding encoding, NameValueCollection result) + { + if (query.Length == 0) + return; + + var decodedLength = query.Length; + var namePos = 0; + var first = true; + + while (namePos <= decodedLength) + { + int valuePos = -1, valueEnd = -1; + for (var q = namePos; q < decodedLength; q++) + { + if ((valuePos == -1) && (query[q] == '=')) + { + valuePos = q + 1; + } + else if (query[q] == '&') + { + valueEnd = q; + break; + } + } + + if (first) + { + first = false; + if (query[namePos] == '?') + namePos++; + } + + string name; + if (valuePos == -1) + { + name = null; + valuePos = namePos; + } + else + { + name = UnityWebRequest.UnEscapeURL(query.Substring(namePos, valuePos - namePos - 1), encoding); + } + if (valueEnd < 0) + { + namePos = -1; + valueEnd = query.Length; + } + else + { + namePos = valueEnd + 1; + } + var value = UnityWebRequest.UnEscapeURL(query.Substring(valuePos, valueEnd - valuePos), encoding); + + result.Add(name, value); + if (namePos == -1) + break; + } + } + } +} +#endif \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/LittleEndianBitConverter.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/LittleEndianBitConverter.cs new file mode 100644 index 0000000..bfab436 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/LittleEndianBitConverter.cs @@ -0,0 +1,57 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +namespace OpenBLive.Runtime.Utilities +{ + /// + /// A little-endian BitConverter that converts base data types to an array of bytes, and an array of bytes to base data types. All conversions are in + /// little-endian format regardless of machine architecture. + /// + internal class LittleEndianBitConverter : EndianBitConverter + { + // Instance available from EndianBitConverter.LittleEndian + internal LittleEndianBitConverter() { } + + public override bool IsLittleEndian { get; } = true; + + public override byte[] GetBytes(short value) + { + return new byte[] { (byte)value, (byte)(value >> 8) }; + } + + public override byte[] GetBytes(int value) + { + return new byte[] { (byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24) }; + } + + public override byte[] GetBytes(long value) + { + return new byte[] { + (byte)value, (byte)(value >> 8), (byte)(value >> 16), (byte)(value >> 24), + (byte)(value >> 32), (byte)(value >> 40), (byte)(value >> 48), (byte)(value >> 56) + }; + } + + public override short ToInt16(byte[] value, int startIndex) + { + this.CheckArguments(value, startIndex, sizeof(short)); + + return (short)((value[startIndex]) | (value[startIndex + 1] << 8)); + } + + public override int ToInt32(byte[] value, int startIndex) + { + this.CheckArguments(value, startIndex, sizeof(int)); + + return (value[startIndex]) | (value[startIndex + 1] << 8) | (value[startIndex + 2] << 16) | (value[startIndex + 3] << 24); + } + + public override long ToInt64(byte[] value, int startIndex) + { + this.CheckArguments(value, startIndex, sizeof(long)); + + int lowBytes = (value[startIndex]) | (value[startIndex + 1] << 8) | (value[startIndex + 2] << 16) | (value[startIndex + 3] << 24); + int highBytes = (value[startIndex + 4]) | (value[startIndex + 5] << 8) | (value[startIndex + 6] << 16) | (value[startIndex + 7] << 24); + return ((uint)lowBytes | ((long)highBytes << 32)); + } + } +} diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/Logger.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/Logger.cs new file mode 100644 index 0000000..24db50f --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/Logger.cs @@ -0,0 +1,31 @@ +#if NET5_0_OR_GREATER +using System; +#else +using UnityEngine; +#endif + + + +namespace OpenBLive.Runtime.Utilities +{ + public static class Logger + { + public static void LogError(string logInfo) + { +#if NET5_0_OR_GREATER + Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}:{logInfo}"); +#else + Debug.LogError(logInfo); +#endif + } + + public static void Log(string logInfo) + { +#if NET5_0_OR_GREATER + Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}:{logInfo}"); +#else + Debug.Log(logInfo); +#endif + } + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/SignUtility.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/SignUtility.cs new file mode 100644 index 0000000..df8954e --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/SignUtility.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +#if NET5_0_OR_GREATER +using System.Net; +#elif UNITY_2020_3_OR_NEWER +using UnityEngine.Networking; +#endif + +namespace OpenBLive.Runtime.Utilities +{ + public static class SignUtility + { + #region AccessKey + + /// + /// 开放平台的access_key_secret,请妥善保管以防泄露 + /// + public static string accessKeySecret = ""; + + /// + /// 开放平台的access_key_id,请妥善保管以防泄露 + /// + public static string accessKeyId = ""; + #endregion + + private static Dictionary OrderAndMd5(string jsonParam) + { + var keyValuePairs = new Dictionary + { + {"x-bili-content-md5", Md5(jsonParam)}, + {"x-bili-timestamp", DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds.ToString("f0")}, + {"x-bili-signature-method", "HMAC-SHA256"}, + {"x-bili-signature-nonce", Guid.NewGuid().ToString()}, + {"x-bili-accesskeyid", accessKeyId}, + {"x-bili-signature-version", "1.0"} + }; + Dictionary sortDic = + keyValuePairs.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value); + return sortDic; + } + + /// + /// MD5加密 + /// + private static string Md5(this string source) + { + //MD5类是抽象类 + MD5 md5 = MD5.Create(); + //需要将字符串转成字节数组 + byte[] buffer = Encoding.UTF8.GetBytes(source); + //加密后是一个字节类型的数组,这里要注意编码UTF8/Unicode等的选择 + byte[] md5Buffer = md5.ComputeHash(buffer); + // 通过使用循环,将字节类型的数组转换为字符串,此字符串是常规字符格式化所得 + + return md5Buffer.Aggregate(null, (current, b) => current + b.ToString("x2")); + } + + /// + /// 计算签名 + /// + private static string CalculateSignature(Dictionary keyValuePairs) + { + string sig = string.Empty; + foreach (var item in keyValuePairs) + { + if (string.IsNullOrEmpty(sig)) + { + sig += item.Key + ":" + item.Value; + } + else + { + sig += "\n" + item.Key + ":" + item.Value; + } + } + + return HmacSHA256(sig, accessKeySecret); + } + + private static string HmacSHA256(string message, string secret) + { + secret ??= ""; + var encoding = new UTF8Encoding(); + byte[] keyByte = encoding.GetBytes(secret); + byte[] messageBytes = encoding.GetBytes(message); + using var hash256 = new HMACSHA256(keyByte); + byte[] hash = hash256.ComputeHash(messageBytes); + StringBuilder builder = new StringBuilder(); + foreach (var t in hash) + { + builder.Append(t.ToString("x2")); + } + + return builder.ToString(); + } +#if NET5_0_OR_GREATER + public static void SetReqHeader(HttpWebRequest req, string jsonParam, string cookie = null) + { + var sortDic = OrderAndMd5(jsonParam); + var auth = CalculateSignature(sortDic); + foreach (var item in sortDic) + { + req.Headers.Add(item.Key, item.Value); + } + + req.Headers.Add("Authorization", auth); + req.Accept = "application/json"; + req.ContentType = "application/json"; + + + if (cookie != null) + { + req.Headers.Add("Cookie", cookie); + } + + var bytes = Encoding.UTF8.GetBytes(jsonParam); + req.ContentLength = bytes.Length; + using Stream writer = req.GetRequestStream(); + writer.Write(bytes, 0, bytes.Length); + writer.Close(); + } + + + +#elif UNITY_2020_3_OR_NEWER + public static void SetReqHeader(UnityWebRequest webRequest, string jsonParam, string cookie = null) + { + var sortDic = OrderAndMd5(jsonParam); + var auth = CalculateSignature(sortDic); + foreach (var item in sortDic) + { + webRequest.SetRequestHeader(item.Key, item.Value); + } + + webRequest.SetRequestHeader("Authorization", auth); + webRequest.SetRequestHeader("Accept", "application/json"); + webRequest.SetRequestHeader("Content-Type", "application/json"); + if (cookie != null) + { + webRequest.SetRequestHeader("Cookie", cookie); + } + + var bytes = Encoding.UTF8.GetBytes(jsonParam); + webRequest.uploadHandler = new UploadHandlerRaw(bytes); + } +#endif + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/SingleConverter.cs b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/SingleConverter.cs new file mode 100644 index 0000000..957a908 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/Utilities/SingleConverter.cs @@ -0,0 +1,36 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. + +namespace OpenBLive.Runtime.Utilities +{ + using System.Runtime.InteropServices; + + // Converts between Single (float) and Int32 (int), as System.BitConverter does not have a method to do this in all .NET versions. + // A union is used instead of an unsafe pointer cast so we don't have to worry about the trusted environment implications. + [StructLayout(LayoutKind.Explicit)] + internal struct SingleConverter + { + // map int value to offset zero + [FieldOffset(0)] + private int intValue; + + // map float value to offset zero - intValue and floatValue now take the same position in memory + [FieldOffset(0)] + private float floatValue; + + internal SingleConverter(int intValue) + { + this.floatValue = 0; + this.intValue = intValue; + } + + internal SingleConverter(float floatValue) + { + this.intValue = 0; + this.floatValue = floatValue; + } + + internal int GetIntValue() => this.intValue; + + internal float GetFloatValue() => this.floatValue; + } +} diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/WebSocket/LICENSE.txt b/HMIcode/SmallProject/OpenBLive/Runtime/WebSocket/LICENSE.txt new file mode 100644 index 0000000..9201a86 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/WebSocket/LICENSE.txt @@ -0,0 +1,15 @@ +Copyright 2019 Endel Dreyer +Copyright 2018 Jiri Hybek + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/WebSocket/WebSocket.cs b/HMIcode/SmallProject/OpenBLive/Runtime/WebSocket/WebSocket.cs new file mode 100644 index 0000000..298bb1f --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/WebSocket/WebSocket.cs @@ -0,0 +1,851 @@ +#if !NET5_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.WebSockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using AOT; +using System.Runtime.InteropServices; +using UnityEngine; +using System.Collections; + +public class MainThreadUtil : MonoBehaviour +{ + public static MainThreadUtil Instance { get; private set; } + public static SynchronizationContext synchronizationContext { get; private set; } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + public static void Setup() + { + Instance = new GameObject("MainThreadUtil") + .AddComponent(); + synchronizationContext = SynchronizationContext.Current; + } + + public static void Run(IEnumerator waitForUpdate) + { + synchronizationContext.Post(_ => Instance.StartCoroutine( + waitForUpdate), null); + } + + void Awake() + { + gameObject.hideFlags = HideFlags.HideAndDontSave; + DontDestroyOnLoad(gameObject); + } +} + +public class WaitForUpdate : CustomYieldInstruction +{ + public override bool keepWaiting + { + get { return false; } + } + + public MainThreadAwaiter GetAwaiter() + { + var awaiter = new MainThreadAwaiter(); + MainThreadUtil.Run(CoroutineWrapper(this, awaiter)); + return awaiter; + } + + public class MainThreadAwaiter : INotifyCompletion + { + Action continuation; + + public bool IsCompleted { get; set; } + + public void GetResult() { } + + public void Complete() + { + IsCompleted = true; + continuation?.Invoke(); + } + + void INotifyCompletion.OnCompleted(Action continuation) + { + this.continuation = continuation; + } + } + + public static IEnumerator CoroutineWrapper(IEnumerator theWorker, MainThreadAwaiter awaiter) + { + yield return theWorker; + awaiter.Complete(); + } +} + +namespace NativeWebSocket +{ + public delegate void WebSocketOpenEventHandler(); + public delegate void WebSocketMessageEventHandler(byte[] data); + public delegate void WebSocketErrorEventHandler(string errorMsg); + public delegate void WebSocketCloseEventHandler(WebSocketCloseCode closeCode); + + public enum WebSocketCloseCode + { + /* Do NOT use NotSet - it's only purpose is to indicate that the close code cannot be parsed. */ + NotSet = 0, + Normal = 1000, + Away = 1001, + ProtocolError = 1002, + UnsupportedData = 1003, + Undefined = 1004, + NoStatus = 1005, + Abnormal = 1006, + InvalidData = 1007, + PolicyViolation = 1008, + TooBig = 1009, + MandatoryExtension = 1010, + ServerError = 1011, + TlsHandshakeFailure = 1015 + } + + public enum WebSocketState + { + Connecting, + Open, + Closing, + Closed + } + + public interface IWebSocket + { + event WebSocketOpenEventHandler OnOpen; + event WebSocketMessageEventHandler OnMessage; + event WebSocketErrorEventHandler OnError; + event WebSocketCloseEventHandler OnClose; + + WebSocketState State { get; } + } + + public static class WebSocketHelpers + { + public static WebSocketCloseCode ParseCloseCodeEnum(int closeCode) + { + + if (WebSocketCloseCode.IsDefined(typeof(WebSocketCloseCode), closeCode)) + { + return (WebSocketCloseCode)closeCode; + } + else + { + return WebSocketCloseCode.Undefined; + } + + } + + public static WebSocketException GetErrorMessageFromCode(int errorCode, Exception inner) + { + switch (errorCode) + { + case -1: + return new WebSocketUnexpectedException("WebSocket instance not found.", inner); + case -2: + return new WebSocketInvalidStateException("WebSocket is already connected or in connecting state.", inner); + case -3: + return new WebSocketInvalidStateException("WebSocket is not connected.", inner); + case -4: + return new WebSocketInvalidStateException("WebSocket is already closing.", inner); + case -5: + return new WebSocketInvalidStateException("WebSocket is already closed.", inner); + case -6: + return new WebSocketInvalidStateException("WebSocket is not in open state.", inner); + case -7: + return new WebSocketInvalidArgumentException("Cannot close WebSocket. An invalid code was specified or reason is too long.", inner); + default: + return new WebSocketUnexpectedException("Unknown error.", inner); + } + } + } + + public class WebSocketException : Exception + { + public WebSocketException() { } + public WebSocketException(string message) : base(message) { } + public WebSocketException(string message, Exception inner) : base(message, inner) { } + } + + public class WebSocketUnexpectedException : WebSocketException + { + public WebSocketUnexpectedException() { } + public WebSocketUnexpectedException(string message) : base(message) { } + public WebSocketUnexpectedException(string message, Exception inner) : base(message, inner) { } + } + + public class WebSocketInvalidArgumentException : WebSocketException + { + public WebSocketInvalidArgumentException() { } + public WebSocketInvalidArgumentException(string message) : base(message) { } + public WebSocketInvalidArgumentException(string message, Exception inner) : base(message, inner) { } + } + + public class WebSocketInvalidStateException : WebSocketException + { + public WebSocketInvalidStateException() { } + public WebSocketInvalidStateException(string message) : base(message) { } + public WebSocketInvalidStateException(string message, Exception inner) : base(message, inner) { } + } + + public class WaitForBackgroundThread + { + public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter() + { + return Task.Run(() => { }).ConfigureAwait(false).GetAwaiter(); + } + } + +#if UNITY_WEBGL && !UNITY_EDITOR + + /// + /// WebSocket class bound to JSLIB. + /// + public class WebSocket : IWebSocket { + + /* WebSocket JSLIB functions */ + [DllImport ("__Internal")] + public static extern int WebSocketConnect (int instanceId); + + [DllImport ("__Internal")] + public static extern int WebSocketClose (int instanceId, int code, string reason); + + [DllImport ("__Internal")] + public static extern int WebSocketSend (int instanceId, byte[] dataPtr, int dataLength); + + [DllImport ("__Internal")] + public static extern int WebSocketSendText (int instanceId, string message); + + [DllImport ("__Internal")] + public static extern int WebSocketGetState (int instanceId); + + protected int instanceId; + + public event WebSocketOpenEventHandler OnOpen; + public event WebSocketMessageEventHandler OnMessage; + public event WebSocketErrorEventHandler OnError; + public event WebSocketCloseEventHandler OnClose; + + public WebSocket (string url, Dictionary headers = null) { + if (!WebSocketFactory.isInitialized) { + WebSocketFactory.Initialize (); + } + + int instanceId = WebSocketFactory.WebSocketAllocate (url); + WebSocketFactory.instances.Add (instanceId, this); + + this.instanceId = instanceId; + } + + public WebSocket (string url, string subprotocol, Dictionary headers = null) { + if (!WebSocketFactory.isInitialized) { + WebSocketFactory.Initialize (); + } + + int instanceId = WebSocketFactory.WebSocketAllocate (url); + WebSocketFactory.instances.Add (instanceId, this); + + WebSocketFactory.WebSocketAddSubProtocol(instanceId, subprotocol); + + this.instanceId = instanceId; + } + + public WebSocket (string url, List subprotocols, Dictionary headers = null) { + if (!WebSocketFactory.isInitialized) { + WebSocketFactory.Initialize (); + } + + int instanceId = WebSocketFactory.WebSocketAllocate (url); + WebSocketFactory.instances.Add (instanceId, this); + + foreach (string subprotocol in subprotocols) { + WebSocketFactory.WebSocketAddSubProtocol(instanceId, subprotocol); + } + + this.instanceId = instanceId; + } + + ~WebSocket () { + WebSocketFactory.HandleInstanceDestroy (this.instanceId); + } + + public int GetInstanceId () { + return this.instanceId; + } + + public Task Connect () { + int ret = WebSocketConnect (this.instanceId); + + if (ret < 0) + throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); + + return Task.CompletedTask; + } + + public void CancelConnection () { + if (State == WebSocketState.Open) + Close (WebSocketCloseCode.Abnormal); + } + + public Task Close (WebSocketCloseCode code = WebSocketCloseCode.Normal, string reason = null) { + int ret = WebSocketClose (this.instanceId, (int) code, reason); + + if (ret < 0) + throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); + + return Task.CompletedTask; + } + + public Task Send (byte[] data) { + int ret = WebSocketSend (this.instanceId, data, data.Length); + + if (ret < 0) + throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); + + return Task.CompletedTask; + } + + public Task SendText (string message) { + int ret = WebSocketSendText (this.instanceId, message); + + if (ret < 0) + throw WebSocketHelpers.GetErrorMessageFromCode (ret, null); + + return Task.CompletedTask; + } + + public WebSocketState State { + get { + int state = WebSocketGetState (this.instanceId); + + if (state < 0) + throw WebSocketHelpers.GetErrorMessageFromCode (state, null); + + switch (state) { + case 0: + return WebSocketState.Connecting; + + case 1: + return WebSocketState.Open; + + case 2: + return WebSocketState.Closing; + + case 3: + return WebSocketState.Closed; + + default: + return WebSocketState.Closed; + } + } + } + + public void DelegateOnOpenEvent () { + this.OnOpen?.Invoke (); + } + + public void DelegateOnMessageEvent (byte[] data) { + this.OnMessage?.Invoke (data); + } + + public void DelegateOnErrorEvent (string errorMsg) { + this.OnError?.Invoke (errorMsg); + } + + public void DelegateOnCloseEvent (int closeCode) { + this.OnClose?.Invoke (WebSocketHelpers.ParseCloseCodeEnum (closeCode)); + } + + } + +#else + + public class WebSocket : IWebSocket + { + public event WebSocketOpenEventHandler OnOpen; + public event WebSocketMessageEventHandler OnMessage; + public event WebSocketErrorEventHandler OnError; + public event WebSocketCloseEventHandler OnClose; + + private Uri uri; + private Dictionary headers; + private List subprotocols; + private ClientWebSocket m_Socket = new ClientWebSocket(); + + private CancellationTokenSource m_TokenSource; + private CancellationToken m_CancellationToken; + + private readonly object OutgoingMessageLock = new object(); + private readonly object IncomingMessageLock = new object(); + + private bool isSending = false; + private List> sendBytesQueue = new List>(); + private List> sendTextQueue = new List>(); + + public WebSocket(string url, Dictionary headers = null) + { + uri = new Uri(url); + + if (headers == null) + { + this.headers = new Dictionary(); + } + else + { + this.headers = headers; + } + + subprotocols = new List(); + + string protocol = uri.Scheme; + if (!protocol.Equals("ws") && !protocol.Equals("wss")) + throw new ArgumentException("Unsupported protocol: " + protocol); + } + + public WebSocket(string url, string subprotocol, Dictionary headers = null) + { + uri = new Uri(url); + + if (headers == null) + { + this.headers = new Dictionary(); + } + else + { + this.headers = headers; + } + + subprotocols = new List {subprotocol}; + + string protocol = uri.Scheme; + if (!protocol.Equals("ws") && !protocol.Equals("wss")) + throw new ArgumentException("Unsupported protocol: " + protocol); + } + + public WebSocket(string url, List subprotocols, Dictionary headers = null) + { + uri = new Uri(url); + + if (headers == null) + { + this.headers = new Dictionary(); + } + else + { + this.headers = headers; + } + + this.subprotocols = subprotocols; + + string protocol = uri.Scheme; + if (!protocol.Equals("ws") && !protocol.Equals("wss")) + throw new ArgumentException("Unsupported protocol: " + protocol); + } + + public void CancelConnection() + { + m_TokenSource?.Cancel(); + } + + public async Task Connect() + { + try + { + m_TokenSource = new CancellationTokenSource(); + m_CancellationToken = m_TokenSource.Token; + + m_Socket = new ClientWebSocket(); + + foreach (var header in headers) + { + m_Socket.Options.SetRequestHeader(header.Key, header.Value); + } + + foreach (string subprotocol in subprotocols) { + m_Socket.Options.AddSubProtocol(subprotocol); + } + + await m_Socket.ConnectAsync(uri, m_CancellationToken); + OnOpen?.Invoke(); + + await Receive(); + } + catch (Exception ex) + { + OnError?.Invoke(ex.Message); + OnClose?.Invoke(WebSocketCloseCode.Abnormal); + } + finally + { + if (m_Socket != null) + { + m_TokenSource.Cancel(); + m_Socket.Dispose(); + } + } + } + + public WebSocketState State + { + get + { + switch (m_Socket.State) + { + case System.Net.WebSockets.WebSocketState.Connecting: + return WebSocketState.Connecting; + + case System.Net.WebSockets.WebSocketState.Open: + return WebSocketState.Open; + + case System.Net.WebSockets.WebSocketState.CloseSent: + case System.Net.WebSockets.WebSocketState.CloseReceived: + return WebSocketState.Closing; + + case System.Net.WebSockets.WebSocketState.Closed: + return WebSocketState.Closed; + + default: + return WebSocketState.Closed; + } + } + } + + public Task Send(byte[] bytes) + { + // return m_Socket.SendAsync(buffer, WebSocketMessageType.Binary, true, CancellationToken.None); + return SendMessage(sendBytesQueue, WebSocketMessageType.Binary, new ArraySegment(bytes)); + } + + public Task SendText(string message) + { + var encoded = Encoding.UTF8.GetBytes(message); + + // m_Socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None); + return SendMessage(sendTextQueue, WebSocketMessageType.Text, new ArraySegment(encoded, 0, encoded.Length)); + } + + private async Task SendMessage(List> queue, WebSocketMessageType messageType, ArraySegment buffer) + { + // Return control to the calling method immediately. + // await Task.Yield (); + + // Make sure we have data. + if (buffer.Count == 0) + { + return; + } + + // The state of the connection is contained in the context Items dictionary. + bool sending; + + lock (OutgoingMessageLock) + { + sending = isSending; + + // If not, we are now. + if (!isSending) + { + isSending = true; + } + } + + if (!sending) + { + // Lock with a timeout, just in case. + if (!Monitor.TryEnter(m_Socket, 1000)) + { + // If we couldn't obtain exclusive access to the socket in one second, something is wrong. + await m_Socket.CloseAsync(WebSocketCloseStatus.InternalServerError, string.Empty, m_CancellationToken); + return; + } + + try + { + // Send the message synchronously. + var t = m_Socket.SendAsync(buffer, messageType, true, m_CancellationToken); + t.Wait(m_CancellationToken); + } + finally + { + Monitor.Exit(m_Socket); + } + + // Note that we've finished sending. + lock (OutgoingMessageLock) + { + isSending = false; + } + + // Handle any queued messages. + await HandleQueue(queue, messageType); + } + else + { + // Add the message to the queue. + lock (OutgoingMessageLock) + { + queue.Add(buffer); + } + } + } + + private async Task HandleQueue(List> queue, WebSocketMessageType messageType) + { + var buffer = new ArraySegment(); + lock (OutgoingMessageLock) + { + // Check for an item in the queue. + if (queue.Count > 0) + { + // Pull it off the top. + buffer = queue[0]; + queue.RemoveAt(0); + } + } + + // Send that message. + if (buffer.Count > 0) + { + await SendMessage(queue, messageType, buffer); + } + } + + private List m_MessageList = new List(); + + // simple dispatcher for queued messages. + public void DispatchMessageQueue() + { + if (m_MessageList.Count == 0) + { + return; + } + + List messageListCopy; + + lock (IncomingMessageLock) + { + messageListCopy = new List(m_MessageList); + m_MessageList.Clear(); + } + + var len = messageListCopy.Count; + for (int i = 0; i < len; i++) + { + OnMessage?.Invoke(messageListCopy[i]); + } + } + + public async Task Receive() + { + WebSocketCloseCode closeCode = WebSocketCloseCode.Abnormal; + await new WaitForBackgroundThread(); + + ArraySegment buffer = new ArraySegment(new byte[8192]); + try + { + while (m_Socket.State == System.Net.WebSockets.WebSocketState.Open) + { + WebSocketReceiveResult result = null; + + using (var ms = new MemoryStream()) + { + do + { + result = await m_Socket.ReceiveAsync(buffer, m_CancellationToken); + ms.Write(buffer.Array, buffer.Offset, result.Count); + } + while (!result.EndOfMessage); + + ms.Seek(0, SeekOrigin.Begin); + + if (result.MessageType == WebSocketMessageType.Text) + { + lock (IncomingMessageLock) + { + m_MessageList.Add(ms.ToArray()); + } + + //using (var reader = new StreamReader(ms, Encoding.UTF8)) + //{ + // string message = reader.ReadToEnd(); + // OnMessage?.Invoke(this, new MessageEventArgs(message)); + //} + } + else if (result.MessageType == WebSocketMessageType.Binary) + { + + lock (IncomingMessageLock) + { + m_MessageList.Add(ms.ToArray()); + } + } + else if (result.MessageType == WebSocketMessageType.Close) + { + await Close(); + closeCode = WebSocketHelpers.ParseCloseCodeEnum((int)result.CloseStatus); + break; + } + } + } + } + catch (Exception) + { + m_TokenSource.Cancel(); + } + finally + { + await new WaitForUpdate(); + OnClose?.Invoke(closeCode); + } + } + + public async Task Close() + { + if (State == WebSocketState.Open) + { + await m_Socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, m_CancellationToken); + } + } + } +#endif + + /// + /// Factory + /// + + /// + /// Class providing static access methods to work with JSLIB WebSocket or WebSocketSharp interface + /// + public static class WebSocketFactory + { + +#if UNITY_WEBGL && !UNITY_EDITOR + /* Map of websocket instances */ + public static Dictionary instances = new Dictionary (); + + /* Delegates */ + public delegate void OnOpenCallback (int instanceId); + public delegate void OnMessageCallback (int instanceId, System.IntPtr msgPtr, int msgSize); + public delegate void OnErrorCallback (int instanceId, System.IntPtr errorPtr); + public delegate void OnCloseCallback (int instanceId, int closeCode); + + /* WebSocket JSLIB callback setters and other functions */ + [DllImport ("__Internal")] + public static extern int WebSocketAllocate (string url); + + [DllImport ("__Internal")] + public static extern int WebSocketAddSubProtocol (int instanceId, string subprotocol); + + [DllImport ("__Internal")] + public static extern void WebSocketFree (int instanceId); + + [DllImport ("__Internal")] + public static extern void WebSocketSetOnOpen (OnOpenCallback callback); + + [DllImport ("__Internal")] + public static extern void WebSocketSetOnMessage (OnMessageCallback callback); + + [DllImport ("__Internal")] + public static extern void WebSocketSetOnError (OnErrorCallback callback); + + [DllImport ("__Internal")] + public static extern void WebSocketSetOnClose (OnCloseCallback callback); + + /* If callbacks was initialized and set */ + public static bool isInitialized = false; + + /* + * Initialize WebSocket callbacks to JSLIB + */ + public static void Initialize () { + + WebSocketSetOnOpen (DelegateOnOpenEvent); + WebSocketSetOnMessage (DelegateOnMessageEvent); + WebSocketSetOnError (DelegateOnErrorEvent); + WebSocketSetOnClose (DelegateOnCloseEvent); + + isInitialized = true; + + } + + /// + /// Called when instance is destroyed (by destructor) + /// Method removes instance from map and free it in JSLIB implementation + /// + /// Instance identifier. + public static void HandleInstanceDestroy (int instanceId) { + + instances.Remove (instanceId); + WebSocketFree (instanceId); + + } + + [MonoPInvokeCallback (typeof (OnOpenCallback))] + public static void DelegateOnOpenEvent (int instanceId) { + + WebSocket instanceRef; + + if (instances.TryGetValue (instanceId, out instanceRef)) { + instanceRef.DelegateOnOpenEvent (); + } + + } + + [MonoPInvokeCallback (typeof (OnMessageCallback))] + public static void DelegateOnMessageEvent (int instanceId, System.IntPtr msgPtr, int msgSize) { + + WebSocket instanceRef; + + if (instances.TryGetValue (instanceId, out instanceRef)) { + byte[] msg = new byte[msgSize]; + Marshal.Copy (msgPtr, msg, 0, msgSize); + + instanceRef.DelegateOnMessageEvent (msg); + } + + } + + [MonoPInvokeCallback (typeof (OnErrorCallback))] + public static void DelegateOnErrorEvent (int instanceId, System.IntPtr errorPtr) { + + WebSocket instanceRef; + + if (instances.TryGetValue (instanceId, out instanceRef)) { + + string errorMsg = Marshal.PtrToStringAuto (errorPtr); + instanceRef.DelegateOnErrorEvent (errorMsg); + + } + + } + + [MonoPInvokeCallback (typeof (OnCloseCallback))] + public static void DelegateOnCloseEvent (int instanceId, int closeCode) { + + WebSocket instanceRef; + + if (instances.TryGetValue (instanceId, out instanceRef)) { + instanceRef.DelegateOnCloseEvent (closeCode); + } + + } +#endif + + /// + /// Create WebSocket client instance + /// + /// The WebSocket instance. + /// WebSocket valid URL. + public static WebSocket CreateInstance(string url) + { + return new WebSocket(url); + } + + } + +} +#endif \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/WebSocket/WebSocket.jslib b/HMIcode/SmallProject/OpenBLive/Runtime/WebSocket/WebSocket.jslib new file mode 100644 index 0000000..d7fe410 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/WebSocket/WebSocket.jslib @@ -0,0 +1,333 @@ + +var LibraryWebSocket = { + $webSocketState: { + /* + * Map of instances + * + * Instance structure: + * { + * url: string, + * ws: WebSocket + * } + */ + instances: {}, + + /* Last instance ID */ + lastId: 0, + + /* Event listeners */ + onOpen: null, + onMesssage: null, + onError: null, + onClose: null, + + /* Debug mode */ + debug: false + }, + + /** + * Set onOpen callback + * + * @param callback Reference to C# static function + */ + WebSocketSetOnOpen: function(callback) { + + webSocketState.onOpen = callback; + + }, + + /** + * Set onMessage callback + * + * @param callback Reference to C# static function + */ + WebSocketSetOnMessage: function(callback) { + + webSocketState.onMessage = callback; + + }, + + /** + * Set onError callback + * + * @param callback Reference to C# static function + */ + WebSocketSetOnError: function(callback) { + + webSocketState.onError = callback; + + }, + + /** + * Set onClose callback + * + * @param callback Reference to C# static function + */ + WebSocketSetOnClose: function(callback) { + + webSocketState.onClose = callback; + + }, + + /** + * Allocate new WebSocket instance struct + * + * @param url Server URL + */ + WebSocketAllocate: function(url) { + + var urlStr = UTF8ToString(url); + var id = webSocketState.lastId++; + + webSocketState.instances[id] = { + subprotocols: [], + url: urlStr, + ws: null + }; + + return id; + + }, + + /** + * Add subprotocol to instance + * + * @param instanceId Instance ID + * @param subprotocol Subprotocol name to add to instance + */ + WebSocketAddSubProtocol: function(instanceId, subprotocol) { + + var subprotocolStr = UTF8ToString(subprotocol); + webSocketState.instances[instanceId].subprotocols.push(subprotocolStr); + + }, + + /** + * Remove reference to WebSocket instance + * + * If socket is not closed function will close it but onClose event will not be emitted because + * this function should be invoked by C# WebSocket destructor. + * + * @param instanceId Instance ID + */ + WebSocketFree: function(instanceId) { + + var instance = webSocketState.instances[instanceId]; + + if (!instance) return 0; + + // Close if not closed + if (instance.ws && instance.ws.readyState < 2) + instance.ws.close(); + + // Remove reference + delete webSocketState.instances[instanceId]; + + return 0; + + }, + + /** + * Connect WebSocket to the server + * + * @param instanceId Instance ID + */ + WebSocketConnect: function(instanceId) { + + var instance = webSocketState.instances[instanceId]; + if (!instance) return -1; + + if (instance.ws !== null) + return -2; + + instance.ws = new WebSocket(instance.url, instance.subprotocols); + + instance.ws.binaryType = 'arraybuffer'; + + instance.ws.onopen = function() { + + if (webSocketState.debug) + console.log("[JSLIB WebSocket] Connected."); + + if (webSocketState.onOpen) + Module.dynCall_vi(webSocketState.onOpen, instanceId); + + }; + + instance.ws.onmessage = function(ev) { + + if (webSocketState.debug) + console.log("[JSLIB WebSocket] Received message:", ev.data); + + if (webSocketState.onMessage === null) + return; + + if (ev.data instanceof ArrayBuffer) { + + var dataBuffer = new Uint8Array(ev.data); + + var buffer = _malloc(dataBuffer.length); + HEAPU8.set(dataBuffer, buffer); + + try { + Module.dynCall_viii(webSocketState.onMessage, instanceId, buffer, dataBuffer.length); + } finally { + _free(buffer); + } + + } else { + var dataBuffer = (new TextEncoder()).encode(ev.data); + + var buffer = _malloc(dataBuffer.length); + HEAPU8.set(dataBuffer, buffer); + + try { + Module.dynCall_viii(webSocketState.onMessage, instanceId, buffer, dataBuffer.length); + } finally { + _free(buffer); + } + + } + + }; + + instance.ws.onerror = function(ev) { + + if (webSocketState.debug) + console.log("[JSLIB WebSocket] Error occured."); + + if (webSocketState.onError) { + + var msg = "WebSocket error."; + var length = lengthBytesUTF8(msg) + 1; + var buffer = _malloc(length); + stringToUTF8(msg, buffer, length); + + try { + Module.dynCall_vii(webSocketState.onError, instanceId, msgBuffer); + } finally { + _free(msgBuffer); + } + + } + + }; + + instance.ws.onclose = function(ev) { + + if (webSocketState.debug) + console.log("[JSLIB WebSocket] Closed."); + + if (webSocketState.onClose) + Module.dynCall_vii(webSocketState.onClose, instanceId, ev.code); + + delete instance.ws; + + }; + + return 0; + + }, + + /** + * Close WebSocket connection + * + * @param instanceId Instance ID + * @param code Close status code + * @param reasonPtr Pointer to reason string + */ + WebSocketClose: function(instanceId, code, reasonPtr) { + + var instance = webSocketState.instances[instanceId]; + if (!instance) return -1; + + if (!instance.ws) + return -3; + + if (instance.ws.readyState === 2) + return -4; + + if (instance.ws.readyState === 3) + return -5; + + var reason = ( reasonPtr ? UTF8ToString(reasonPtr) : undefined ); + + try { + instance.ws.close(code, reason); + } catch(err) { + return -7; + } + + return 0; + + }, + + /** + * Send message over WebSocket + * + * @param instanceId Instance ID + * @param bufferPtr Pointer to the message buffer + * @param length Length of the message in the buffer + */ + WebSocketSend: function(instanceId, bufferPtr, length) { + + var instance = webSocketState.instances[instanceId]; + if (!instance) return -1; + + if (!instance.ws) + return -3; + + if (instance.ws.readyState !== 1) + return -6; + + instance.ws.send(HEAPU8.buffer.slice(bufferPtr, bufferPtr + length)); + + return 0; + + }, + + /** + * Send text message over WebSocket + * + * @param instanceId Instance ID + * @param bufferPtr Pointer to the message buffer + * @param length Length of the message in the buffer + */ + WebSocketSendText: function(instanceId, message) { + + var instance = webSocketState.instances[instanceId]; + if (!instance) return -1; + + if (!instance.ws) + return -3; + + if (instance.ws.readyState !== 1) + return -6; + + instance.ws.send(UTF8ToString(message)); + + return 0; + + }, + + /** + * Return WebSocket readyState + * + * @param instanceId Instance ID + */ + WebSocketGetState: function(instanceId) { + + var instance = webSocketState.instances[instanceId]; + if (!instance) return -1; + + if (instance.ws) + return instance.ws.readyState; + else + return 3; + + } + +}; + +autoAddDeps(LibraryWebSocket, '$webSocketState'); +mergeInto(LibraryManager.library, LibraryWebSocket); diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/WebSocketBLiveClient.cs b/HMIcode/SmallProject/OpenBLive/Runtime/WebSocketBLiveClient.cs new file mode 100644 index 0000000..e59f486 --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/WebSocketBLiveClient.cs @@ -0,0 +1,188 @@ +using System; +using System.Threading.Tasks; +using OpenBLive.Runtime.Data; +using System.Text; +using OpenBLive.Runtime.Utilities; +using System.Collections.Generic; +using System.Linq; +using OpenBLive.Client.Data; + +#if NET5_0_OR_GREATER +using Websocket.Client; +using System.Net.WebSockets; +#elif UNITY_2020_3_OR_NEWER +using NativeWebSocket; +#endif + +namespace OpenBLive.Runtime +{ + public class WebSocketBLiveClient : BLiveClient + { + + /// + /// wss 长连地址 + /// + public IList WssLink; + +#if NET5_0_OR_GREATER + public WebsocketClient clientWebSocket; +#elif UNITY_2020_3_OR_NEWER + public WebSocket ws; +#endif + + public WebSocketBLiveClient(AppStartInfo info) + { + var websocketInfo = info.Data.WebsocketInfo; + + WssLink = websocketInfo.WssLink; + token = websocketInfo.AuthBody; + } + + public WebSocketBLiveClient(IList wssLink, string authBody) + { + WssLink = wssLink; + token = authBody; + } + + + public override async void Connect() + { + var url = WssLink.FirstOrDefault(); + if (string.IsNullOrEmpty(url)) + { + throw new Exception("wsslink is invalid"); + } +#if NET5_0_OR_GREATER + Disconnect(); + + clientWebSocket = new WebsocketClient(new Uri(url)); + clientWebSocket.MessageReceived.Subscribe(e => + ProcessPacket(e.Binary)); + clientWebSocket.DisconnectionHappened.Subscribe(e => + { + if (e.CloseStatus == WebSocketCloseStatus.Empty) + Console.WriteLine("WS CLOSED"); + else + Console.WriteLine("WS ERROR: " + e.Exception.Message); + }); + + await clientWebSocket.Start(); + if (clientWebSocket.IsStarted) + OnOpen(); + + +#elif UNITY_2020_3_OR_NEWER + //尝试释放已连接的ws + if (ws != null && ws.State != WebSocketState.Closed) + { + await ws.Close(); + } + + ws = new WebSocket(url); + ws.OnOpen += OnOpen; + ws.OnMessage += data => + { + ProcessPacket(data); + }; + ws.OnError += msg => { Logger.LogError("WebSocket Error Message: " + msg); }; + ws.OnClose += code => { Logger.Log("WebSocket Close: " + code); }; + await ws.Connect(); +#endif + } + + /// + /// 带有重连 + /// + /// ReconnectTimeout ErrorReconnectTimeout + public override async void Connect(TimeSpan timeout) + { + var url = WssLink.FirstOrDefault(); + if (string.IsNullOrEmpty(url)) + { + throw new Exception("wsslink is invalid"); + } +#if NET5_0_OR_GREATER + clientWebSocket?.Stop(WebSocketCloseStatus.Empty, string.Empty); + clientWebSocket?.Dispose(); + + clientWebSocket = new WebsocketClient(new Uri(url)); + clientWebSocket.MessageReceived.Subscribe(e => + { + //Console.WriteLine(e.Binary.Length); + ProcessPacket(e.Binary); + }); + clientWebSocket.DisconnectionHappened.Subscribe(e => + { + if (e.CloseStatus == WebSocketCloseStatus.Empty) + Console.WriteLine("WS CLOSED"); + else if (e?.Exception != null) + Console.WriteLine("WS ERROR: " + e?.Exception?.Message); + }); + await clientWebSocket.Start(); + clientWebSocket.IsReconnectionEnabled = true; + clientWebSocket.ReconnectTimeout = timeout; + clientWebSocket.ErrorReconnectTimeout = timeout; + clientWebSocket.ReconnectionHappened.Subscribe(e => + { + SendAsync(Packet.Authority(token)); + }); + if (clientWebSocket.IsStarted) + OnOpen(); + + + +#elif UNITY_2020_3_OR_NEWER + //尝试释放已连接的ws + if (ws != null && ws.State != WebSocketState.Closed) + { + await ws.Close(); + } + + ws = new WebSocket(url); + ws.OnOpen += OnOpen; + ws.OnMessage += data => + { + ProcessPacket(data); + }; + ws.OnError += msg => { Logger.LogError("WebSocket Error Message: " + msg); }; + ws.OnClose += code => { Logger.Log("WebSocket Close: " + code); }; + await ws.Connect(); +#endif + + } + + public override void Disconnect() + { +#if NET5_0_OR_GREATER + clientWebSocket?.Stop(WebSocketCloseStatus.Empty, string.Empty); + clientWebSocket?.Dispose(); +#elif UNITY_2020_3_OR_NEWER + ws?.Close(); + ws = null; +#endif + } + + public override void Dispose() + { + Disconnect(); + GC.SuppressFinalize(this); + } + + public override void Send(byte[] packet) + { +#if NET5_0_OR_GREATER + clientWebSocket?.Send(packet); +#elif UNITY_2020_3_OR_NEWER + if (ws.State == WebSocketState.Open) + { + ws.Send(packet); + } +#endif + } + + + public override void Send(Packet packet) => Send(packet.ToBytes); + public override Task SendAsync(byte[] packet) => Task.Run(() => Send(packet)); + protected override Task SendAsync(Packet packet) => SendAsync(packet.ToBytes); + } +} \ No newline at end of file diff --git a/HMIcode/SmallProject/OpenBLive/Runtime/bilibili.OpenBLive.asmdef b/HMIcode/SmallProject/OpenBLive/Runtime/bilibili.OpenBLive.asmdef new file mode 100644 index 0000000..a1e0f4d --- /dev/null +++ b/HMIcode/SmallProject/OpenBLive/Runtime/bilibili.OpenBLive.asmdef @@ -0,0 +1,14 @@ +{ + "name": "bilibili.OpenBLive", + "rootNamespace": "", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/HMIcode/SmallProject/SmallProject.sln b/HMIcode/SmallProject/SmallProject.sln new file mode 100644 index 0000000..f9055dd --- /dev/null +++ b/HMIcode/SmallProject/SmallProject.sln @@ -0,0 +1,51 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34024.191 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmallProject", "SmallProject\SmallProject.csproj", "{7F07C712-757E-496D-8274-ABF4CBA88233}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenBLive", "OpenBLive\OpenBLive.csproj", "{49E5E897-C08C-47B4-A9A4-7776A44815B0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Yolov5Net.Scorer", "Yolov5Net.Scorer\Yolov5Net.Scorer.csproj", "{0BA03139-55E4-483E-B7B7-67849D56E74A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7F07C712-757E-496D-8274-ABF4CBA88233}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F07C712-757E-496D-8274-ABF4CBA88233}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F07C712-757E-496D-8274-ABF4CBA88233}.Debug|x64.ActiveCfg = Debug|Any CPU + {7F07C712-757E-496D-8274-ABF4CBA88233}.Debug|x64.Build.0 = Debug|Any CPU + {7F07C712-757E-496D-8274-ABF4CBA88233}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F07C712-757E-496D-8274-ABF4CBA88233}.Release|Any CPU.Build.0 = Release|Any CPU + {7F07C712-757E-496D-8274-ABF4CBA88233}.Release|x64.ActiveCfg = Release|Any CPU + {7F07C712-757E-496D-8274-ABF4CBA88233}.Release|x64.Build.0 = Release|Any CPU + {49E5E897-C08C-47B4-A9A4-7776A44815B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49E5E897-C08C-47B4-A9A4-7776A44815B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49E5E897-C08C-47B4-A9A4-7776A44815B0}.Debug|x64.ActiveCfg = Debug|Any CPU + {49E5E897-C08C-47B4-A9A4-7776A44815B0}.Debug|x64.Build.0 = Debug|Any CPU + {49E5E897-C08C-47B4-A9A4-7776A44815B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49E5E897-C08C-47B4-A9A4-7776A44815B0}.Release|Any CPU.Build.0 = Release|Any CPU + {49E5E897-C08C-47B4-A9A4-7776A44815B0}.Release|x64.ActiveCfg = Release|Any CPU + {49E5E897-C08C-47B4-A9A4-7776A44815B0}.Release|x64.Build.0 = Release|Any CPU + {0BA03139-55E4-483E-B7B7-67849D56E74A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0BA03139-55E4-483E-B7B7-67849D56E74A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0BA03139-55E4-483E-B7B7-67849D56E74A}.Debug|x64.ActiveCfg = Debug|x64 + {0BA03139-55E4-483E-B7B7-67849D56E74A}.Debug|x64.Build.0 = Debug|x64 + {0BA03139-55E4-483E-B7B7-67849D56E74A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0BA03139-55E4-483E-B7B7-67849D56E74A}.Release|Any CPU.Build.0 = Release|Any CPU + {0BA03139-55E4-483E-B7B7-67849D56E74A}.Release|x64.ActiveCfg = Release|x64 + {0BA03139-55E4-483E-B7B7-67849D56E74A}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {50CBC801-D0DF-4382-9BBD-5065AEB7DF34} + EndGlobalSection +EndGlobal diff --git a/HMIcode/SmallProject/SmallProject/Aliyun/AiAssistant.cs b/HMIcode/SmallProject/SmallProject/Aliyun/AiAssistant.cs new file mode 100644 index 0000000..f4f0538 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Aliyun/AiAssistant.cs @@ -0,0 +1,165 @@ +using NAudio.Wave; +using Newtonsoft.Json; +using SmallProject.Aliyun.Models; +using SmallProject.Logger; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace SmallProject.Aliyun +{ + internal class AiAssistant + { + private static readonly string workspace_id = "llm-zfcwus2axt2ik8ya"; + private static readonly string apiKey = "sk-87479f81a3494310b1baf77aa72c21d9"; // 替换为你的API Key + private static readonly string app_id = "mm_2f63ea3eca2b48e3a7f1e65fffc4"; + private static readonly string webSocketUrl = "wss://dashscope.aliyuncs.com/api-ws/v1/inference"; + private static readonly int sampleRate = 16000; // 设置采样率为16kHz + private static ClientWebSocket socket; + private static WaveInEvent waveInEvent; + + private AssistantState state = AssistantState.None; + public async Task Init() + { + string outputFilePath = Path.Combine(Directory.GetCurrentDirectory(), "output.wav"); + + //var recorder = new AudioRecorder(outputFilePath); + //recorder.StartRecording(); + //JLog.Info("开始录音"); + //Thread.Sleep(2000); + //recorder.StopRecording(); + //JLog.Info($"录音完成,文件已保存至: {outputFilePath}"); + + // 初始化WebSocket连接 + socket = new ClientWebSocket(); + socket.Options.SetRequestHeader("Authorization", $"Bearer {apiKey}"); + + await socket.ConnectAsync(new Uri(webSocketUrl), CancellationToken.None); + JLog.Info("已连接到WebSocket服务器..."); + + // 发送Start指令 + var msg = new SocketMessage(); + msg.payload.input.workspace_id = workspace_id; + msg.payload.input.app_id = app_id; + string startMessage = JsonConvert.SerializeObject(msg); + await SendMessage(socket, startMessage); + + // 开始录音并实时发送音频数据 + StartRecordingAndSendData(); + + // 接收服务端返回的消息 + await ReceiveMessages(socket); + } + + void StartRecordingAndSendData() + { + waveInEvent = new WaveInEvent + { + WaveFormat = new WaveFormat(sampleRate, 1), // 单声道,16kHz + BufferMilliseconds = 100 // 每次捕获100ms的数据 + }; + + waveInEvent.DataAvailable += WaveInEvent_DataAvailable; + + waveInEvent.StartRecording(); + JLog.Info("开始录音..."); + } + + private void WaveInEvent_DataAvailable(object? sender, WaveInEventArgs e) + { + //JLog.Info("here"); + if (socket.State == WebSocketState.Open&& state == AssistantState.Listening) + { + if (e.Buffer.Length == 0) return; + // 直接发送PCM格式的音频数据 + socket.SendAsync(new ArraySegment(e.Buffer, 0, e.BytesRecorded), + WebSocketMessageType.Binary, e.BytesRecorded < waveInEvent.WaveFormat.AverageBytesPerSecond / 10, CancellationToken.None); + } + } + + async Task SendMessage(ClientWebSocket socket, string message) + { + var buffer = new ArraySegment(Encoding.UTF8.GetBytes(message)); + await socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None); + JLog.Info($"发送消息: {message}"); + } + + async Task ReceiveMessages(ClientWebSocket socket) + { + var buffer = new byte[1024 * 4]; + while (socket.State == WebSocketState.Open) + { + var result = await socket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + if (result.MessageType == WebSocketMessageType.Close) + { + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); + break; + } + else + { + string receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count); + var res = JsonConvert.DeserializeObject(receivedText); + if(res.payload.output.state.Equals(AssistantState.Listening.ToString())) + { + state = AssistantState.Listening; + + } + JLog.Info($"收到服务端消息: {receivedText}"); + } + } + } + + + + } + + + public class AudioRecorder + { + private readonly string _outputFilePath; + private WaveInEvent _waveIn; + private WaveFileWriter _writer; + + public AudioRecorder(string outputFilePath) + { + _outputFilePath = outputFilePath; + } + + public void StartRecording() + { + if (_waveIn != null) return; + + _waveIn = new WaveInEvent + { + WaveFormat = new WaveFormat(16000, 1) // 16kHz, 单声道(适合语音识别) + }; + + _writer = new WaveFileWriter(_outputFilePath, _waveIn.WaveFormat); + + _waveIn.DataAvailable += (s, e) => + { + _writer.Write(e.Buffer, 0, e.BytesRecorded); + }; + + _waveIn.RecordingStopped += (s, e) => + { + _writer.Dispose(); + _writer = null; + _waveIn.Dispose(); + _waveIn = null; + }; + + _waveIn.StartRecording(); + } + + public void StopRecording() + { + _waveIn?.StopRecording(); + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/Aliyun/Models/AssistantState.cs b/HMIcode/SmallProject/SmallProject/Aliyun/Models/AssistantState.cs new file mode 100644 index 0000000..9c4c69a --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Aliyun/Models/AssistantState.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Aliyun.Models +{ + public enum AssistantState + { + None, + Listening, + Speed + } +} diff --git a/HMIcode/SmallProject/SmallProject/Aliyun/Models/SocketMessage.cs b/HMIcode/SmallProject/SmallProject/Aliyun/Models/SocketMessage.cs new file mode 100644 index 0000000..b3e8e9a --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Aliyun/Models/SocketMessage.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Printing; +using System.Security.Policy; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Aliyun.Models +{ + internal class SocketMessage + { + public Header header { get; set; } = new Header(); + + public Payload payload { get; set; } = new Payload(); + } + + public class Header + { + public string action { get; set; } = "run-task"; + + public string task_id { get; set; } = Guid.NewGuid().ToString("N"); + + public string streaming { get; set; } = "duplex"; + } + + public class Payload + { + public string task_group { get; set; } = "aigc"; + + public string task { get; set; } = "multimodal-generation"; + + public string function { get; set; } = "generation"; + + public string model { get; set; } = "multimodal-dialog"; + + public Input input { get; set; } = new Input(); + + public Parameters parameters { get; set; } = new Parameters(); + } + + public class Input + { + public string directive { get; set; } = "Start"; + + public string workspace_id { get; set; } + + public string app_id { get; set; } + + } + + public class Parameters + { + public Upstream upstream { get; set; } = new Upstream(); + + public Client_info client_info = new Client_info(); + } + + public class Upstream + { + public string type { get; set; } = "AudioOnly"; + + public string mode { get; set; } = "duplex"; + } + + public class Client_info + { + public string user_id { get; set; } = Guid.NewGuid().ToString("N"); + public Device device { get; set; } = new Device(); + } + + public class Device + { + public string uuid { get; set; } + } +} diff --git a/HMIcode/SmallProject/SmallProject/Aliyun/Models/SocketReceive.cs b/HMIcode/SmallProject/SmallProject/Aliyun/Models/SocketReceive.cs new file mode 100644 index 0000000..d16393c --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Aliyun/Models/SocketReceive.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Aliyun.Models +{ + internal class SocketReceive + { + public ReceiveHeader header { get; set; } = new ReceiveHeader(); + + public ReceivePayload payload { get; set; } = new ReceivePayload(); + } + + public class ReceiveHeader + { + public string @event { get; set; } = string.Empty; + + public string task_id { get; set; } = string.Empty; + } + + public class ReceivePayload + { + public Output output { get; set; } = new Output(); + public Usage usage { get; set; } = new Usage(); + } + + public class Output + { + public string @event{ get; set; } = string.Empty; + public string dialog_id { get; set; } = string.Empty; + public string state { get; set; } = string.Empty; + } + + public class Usage + { + public int invoke { get; set; } = 0; + + public int model_x { get; set; } = 0; + } +} diff --git a/HMIcode/SmallProject/SmallProject/App.xaml b/HMIcode/SmallProject/SmallProject/App.xaml new file mode 100644 index 0000000..9e4df1e --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/HMIcode/SmallProject/SmallProject/App.xaml.cs b/HMIcode/SmallProject/SmallProject/App.xaml.cs new file mode 100644 index 0000000..29210d4 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/App.xaml.cs @@ -0,0 +1,28 @@ +using SmallProject.Configs; +using SmallProject.Devices.Arm; +using SmallProject.Serials; +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace SmallProject +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + public static JConfiguration JConfig { get; set; } + public static Core Core { get; set; } + + public App() + { + JConfig = ConfigResposity.ReadConfigs(); + Core = new Core(); + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/AssemblyInfo.cs b/HMIcode/SmallProject/SmallProject/AssemblyInfo.cs new file mode 100644 index 0000000..8b5504e --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/HMIcode/SmallProject/SmallProject/BiliBili/BStationLive.cs b/HMIcode/SmallProject/SmallProject/BiliBili/BStationLive.cs new file mode 100644 index 0000000..554dc76 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/BiliBili/BStationLive.cs @@ -0,0 +1,239 @@ +using OpenBLive.Client; +using OpenBLive.Client.Data; +using OpenBLive.Runtime.Utilities; +using OpenBLive.Runtime; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using OpenBLive.Runtime.Data; +using SmallProject.Logger; +using System.Threading; + +namespace SmallProject.BiliBili +{ + internal class BStationLive + { + //初始化于测试的参数 + public const string AccessKeyId = "";//填入你的accessKeyId,可以在直播创作者服务中心【个人资料】页面获取(https://open-live.bilibili.com/open-manage) + public const string AccessKeySecret = "";//填入你的accessKeySecret,可以在直播创作者服务中心【个人资料】页面获取(https://open-live.bilibili.com/open-manage) + public const string AppId = "";//填入你的appId,可以在直播创作者服务中心【我的项目】页面创建应用后获取(https://open-live.bilibili.com/open-manage) + public const string Code = "";//填入你的主播身份码Code,可以在互动玩法首页,右下角【身份码】处获取(互玩首页:https://play-live.bilibili.com/) + + public static IBApiClient bApiClient = new BApiClient(); + public static string game_id = string.Empty; + public static bool IsLive = false; + + public async Task Init() + { + //是否为测试环境(一般用户可无视,给专业对接测试使用) + BApi.isTestEnv = false; + + SignUtility.accessKeyId = AccessKeyId; + SignUtility.accessKeySecret = AccessKeySecret; + var appId = AppId; + var code = Code; + + + var startInfoLive = new AppStartInfo(); + if (!string.IsNullOrEmpty(appId)) + { + startInfoLive = await bApiClient.StartInteractivePlay(code, appId); + if (startInfoLive?.Code != 0) + { + return; + } + + var gameId = startInfoLive?.Data?.GameInfo?.GameId; + if (gameId != null) + { + game_id = gameId; + IsLive = true; + //JLog.InfoLive("成功开启,开始心跳,场次ID: " + gameId); + + //心跳API(用于保持在线) + InteractivePlayHeartBeat m_PlayHeartBeat = new InteractivePlayHeartBeat(gameId); + m_PlayHeartBeat.HeartBeatError += M_PlayHeartBeat_HeartBeatError; + m_PlayHeartBeat.HeartBeatSucceed += M_PlayHeartBeat_HeartBeatSucceed; + m_PlayHeartBeat.Start(); + + //长链接(用户持续接收服务器推送消息) + WebSocketBLiveClient m_WebSocketBLiveClient; + m_WebSocketBLiveClient = new WebSocketBLiveClient(startInfoLive.GetWssLink(), startInfoLive.GetAuthBody()); + m_WebSocketBLiveClient.OnDanmaku += WebSocketBLiveClientOnDanmaku;//弹幕事件 + m_WebSocketBLiveClient.OnGift += WebSocketBLiveClientOnGift;//礼物事件 + m_WebSocketBLiveClient.OnGuardBuy += WebSocketBLiveClientOnGuardBuy;//大航海事件 + m_WebSocketBLiveClient.OnSuperChat += WebSocketBLiveClientOnSuperChat;//SC事件 + m_WebSocketBLiveClient.OnLike += M_WebSocketBLiveClient_OnLike;//点赞事件(点赞需要直播间开播才会触发推送) + m_WebSocketBLiveClient.OnEnter += M_WebSocketBLiveClient_OnEnter;//观众进入房间事件 + m_WebSocketBLiveClient.OnLiveStart += M_WebSocketBLiveClient_OnLiveStart;//直播间开始直播事件 + m_WebSocketBLiveClient.OnLiveEnd += M_WebSocketBLiveClient_OnLiveEnd;//直播间停止直播事件 + //m_WebSocketBLiveClient.Connect();//正常连接 + m_WebSocketBLiveClient.Connect(TimeSpan.FromSeconds(10));//失败后30秒重连 + } + else + { + JLog.InfoLive("开启玩法错误: " + startInfoLive.ToString()); + } + //await Task.Run(async () => + //{ + // var closeTime = int.Parse(closeTimeStr); + // await Task.Delay(closeTime * 1000); + // var ret = await bApiClient.EndInteractivePlay(appId, gameId); + // IsLive = false; + // Console.WriteLine("关闭玩法: " + ret.ToString()); + // return; + //}); + } + + + } + + private static void M_WebSocketBLiveClient_OnLiveEnd(LiveEnd liveEnd) + { + StringBuilder sb = new StringBuilder($"直播间[{liveEnd.room_id}]直播结束,分区ID:【{liveEnd.area_id}】,标题为【{liveEnd.title}】"); + JLog.InfoLive(sb.ToString()); + } + + private static void M_WebSocketBLiveClient_OnLiveStart(LiveStart liveStart) + { + StringBuilder sb = new StringBuilder($"直播间[{liveStart.room_id}]开始直播,分区ID:【{liveStart.area_id}】,标题为【{liveStart.title}】"); + JLog.InfoLive(sb.ToString()); + } + + private static void M_WebSocketBLiveClient_OnEnter(Enter enter) + { + StringBuilder sb = new StringBuilder($"用户[{enter.uname}]进入房间"); + JLog.InfoLive(sb.ToString()); + } + + private static void M_WebSocketBLiveClient_OnLike(Like like) + { + StringBuilder sb = new StringBuilder($"用户[{like.uname}]点赞了{like.unamelike_count}次"); + JLog.InfoLive(sb.ToString()); + } + + private static void M_PlayHeartBeat_HeartBeatSucceed() + { + //JLog.InfoLive("心跳成功"); + } + + private static void M_PlayHeartBeat_HeartBeatError(string json) + { + JsonConvert.DeserializeObject(json); + //JLog.InfoLive("心跳失败" + json); + } + + private static void WebSocketBLiveClientOnSuperChat(SuperChat superChat) + { + StringBuilder sb = new StringBuilder($"用户[{superChat.userName}]发送了{superChat.rmb}元的醒目留言内容:{superChat.message}"); + JLog.InfoLive(sb.ToString()); + } + + private static void WebSocketBLiveClientOnGuardBuy(Guard guard) + { + StringBuilder sb = new StringBuilder($"用户[{guard.userInfo.userName}]充值了{(guard.guardUnit == "月" ? (guard.guardNum + "个月") : guard.guardUnit.TrimStart('*'))}[{(guard.guardLevel == 1 ? "总督" : guard.guardLevel == 2 ? "提督" : "舰长")}]大航海"); + JLog.InfoLive(sb.ToString()); + } + + private static void WebSocketBLiveClientOnGift(SendGift sendGift) + { + StringBuilder sb = new StringBuilder($"用户[{sendGift.userName}]赠送了{sendGift.giftNum}个[{sendGift.giftName}]"); + JLog.InfoLive(sb.ToString()); + + } + + private static void WebSocketBLiveClientOnDanmaku(Dm dm) + { + StringBuilder sb = new StringBuilder($"用户[{dm.userName}]发送弹幕:{dm.msg}"); + JLog.InfoLive(sb.ToString()); + var curr = App.Core.Jyker.currentJoints; + var joint1 = curr.a[0]; + var joint2 = curr.a[1]; + var joint3 = curr.a[2]; + switch (dm.msg) + { + case "111": + if (joint1 > 30) + { + JLog.InfoLive("左不了了"); + return; + } + App.Core.Jyker.Move(new double[]{joint1+20, curr.a[1], curr.a[2] + , curr.a[3], curr.a[4], curr.a[5] }); + break; + case "222": + if (joint1 < -90) + { + JLog.InfoLive("右不了了"); + return; + } + App.Core.Jyker.Move(new double[]{ joint1-20, curr.a[1], curr.a[2] + , curr.a[3], curr.a[4], curr.a[5] }); + break; + case "333": + if (joint2 > 150) + { + JLog.InfoLive("前不了了"); + return; + } + App.Core.Jyker.Move(new double[]{ curr.a[0], joint2+10, joint3-10 + , curr.a[3], curr.a[4], curr.a[5] }); + break; + case "444": + if (joint2 - 1 < -90) + { + JLog.InfoLive("退不了了"); + return; + } + App.Core.Jyker.Move(new double[]{ curr.a[0], joint2-10, joint3+10 + , curr.a[3], curr.a[4], curr.a[5] }); + break; + case "555": + if (curr.a[4] - 10 < -90) + { + JLog.InfoLive("脖子没那么长啦"); + return; + } + App.Core.Jyker.Move(new double[]{ curr.a[0], curr.a[1], curr.a[2] + , curr.a[3], curr.a[4]-20, curr.a[5] }); + break; + case "666": + if (curr.a[4] + 10 > 90) + { + JLog.InfoLive("脖子没那么长啦"); + return; + } + App.Core.Jyker.Move(new double[]{ curr.a[0], curr.a[1], curr.a[2] + , curr.a[3], curr.a[4]+20, curr.a[5] }); + break; + case "777": + App.Core.Jyker.Move(new double[]{curr.a[0], curr.a[1], curr.a[2] + , curr.a[3], curr.a[4], 120 }); + Thread.Sleep(1000); + App.Core.Jyker.GetStatus(6); + Thread.Sleep(200); + JLog.Info($"Current {App.Core.Jyker.motorJ[5].Current}"); + if (App.Core.Jyker.motorJ[5].Current < 0.5) + { + JLog.Info("没抓到东西"); + return; + } + else + { + JLog.Info("抓住了"); + return; + } + case "888": + App.Core.Jyker.Move(new double[]{curr.a[0], curr.a[1], curr.a[2] + , curr.a[3], curr.a[4], 0 }); + break; + default: + break; + } + + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/Configs/ConfigResposity.cs b/HMIcode/SmallProject/SmallProject/Configs/ConfigResposity.cs new file mode 100644 index 0000000..e75b15f --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Configs/ConfigResposity.cs @@ -0,0 +1,71 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using Formatting = Newtonsoft.Json.Formatting; + +namespace SmallProject.Configs +{ + public class ConfigResposity + { + private static string DataDic = "./DATA"; + private static string ReadConfigFile = $"{DataDic}/Config.data"; + + /// + /// 读取产型数据 + /// + /// + public static JConfiguration ReadConfigs() + { + lock (DataDic) + { + if (!Directory.Exists(DataDic)) + { + Directory.CreateDirectory(DataDic); + } + if (!File.Exists(ReadConfigFile)) + { + using (File.Create(ReadConfigFile)) { } + } + var json = File.ReadAllText(ReadConfigFile, Encoding.UTF8); + var conf = JsonConvert.DeserializeObject(json); + if (conf == null) + { + return new JConfiguration(); + } + return conf; + } + + } + + /// + /// 写入产型数据 + /// + /// + public static void WriteConfigs(JConfiguration conf) + { + lock (DataDic) + { + if (!Directory.Exists(DataDic)) + { + Directory.CreateDirectory(DataDic); + } + if (!File.Exists(ReadConfigFile)) + { + File.Create(ReadConfigFile); + } + using (FileStream fileStream = File.Create(ReadConfigFile)) //打开文件流 + { + var str = JsonConvert.SerializeObject(conf, Formatting.Indented); //序列化工程文件 + byte[] by = ASCIIEncoding.UTF8.GetBytes(str); //把序列化的工程文件转成字节流 + fileStream.Write(by, 0, by.Length); //字节流写入到文件 + } + } + + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/Configs/JConfiguration.cs b/HMIcode/SmallProject/SmallProject/Configs/JConfiguration.cs new file mode 100644 index 0000000..592dc56 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Configs/JConfiguration.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Configs +{ + public class JConfiguration + { + /// + /// J1 减速比 + /// + public int ReductionJ1 { get; set; } = 30; + /// + /// J2 减速比 + /// + public int ReductionJ2 { get; set; } = 50; + /// + /// J3 减速比 + /// + public int ReductionJ3 { get; set; } = 30; + /// + /// J4 减速比 + /// + public int ReductionJ4 { get; set; } = 30; + /// + /// J5 减速比 + /// + public int ReductionJ5 { get; set; } = 30; + /// + /// J6 减速比 + /// + public int ReductionJ6 { get; set; } = 1; + + // D_BASE = 0, L_BASE = 161.5, L_ARM = 170, D_ELBOW = 70, L_FOREARM = 117, L_WRIST = 97 + public float L_BASE { get; set; } = 0; + public float D_BASE { get; set; } = 161.5f; + public float L_ARM { get; set; } = 170; + public float D_ELBOW { get; set; } = 70; + public float L_FOREARM { get; set; } = 117; + public float L_WRIST { get; set; } = 97; + + //机械臂速度限制 单位 raw/s + public float Speed { get; set; } = 0.2f; + //最大电流值 单位(A) + public float Current { get; set; } = 1f; + + //小智连接的端点 + public string EndpointUrl { get; set; } = "wss://api.xiaozhi.me/mcp/?token=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjE5NDgyOCwiYWdlbnRJZCI6MTE1MTY5LCJlbmRwb2ludElkIjoiYWdlbnRfMTE1MTY5IiwicHVycG9zZSI6Im1jcC1lbmRwb2ludCIsImlhdCI6MTc1MjgwMTE1NX0.iNZDJOTaAz1pzpvTxnsX42mixld2zbmyAiOvof4q0lv_3tMGXoVa8YK7lCO4qxWc3n1H4yd-dAOg95cW2kjkSQ"; + + public string FindModelPath { get; set; } = "DETECT_IMAGE"; + } +} diff --git a/HMIcode/SmallProject/SmallProject/Core.cs b/HMIcode/SmallProject/SmallProject/Core.cs new file mode 100644 index 0000000..b86898c --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Core.cs @@ -0,0 +1,43 @@ +using SmallProject.Aliyun; +using SmallProject.Configs; +using SmallProject.Devices.Arm; +using SmallProject.Serials; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace SmallProject +{ + public class Core + { + + public string ViewName = ""; + + public Core() { + Serial = new Serial(); + Jyker = new JykerArm(); + + + //定时任务 + Task.Run(() => + { + while (true) + { + Loop?.Invoke(); + Thread.Sleep(20); + } + }); + + } + + public Serial Serial { get; set; } + + public JykerArm Jyker { get; set; } + + public event Action Loop; + + } +} diff --git a/HMIcode/SmallProject/SmallProject/Devices/Arm/CtrlStep/CtrlStepMotor.cs b/HMIcode/SmallProject/SmallProject/Devices/Arm/CtrlStep/CtrlStepMotor.cs new file mode 100644 index 0000000..fe1cb4c --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Devices/Arm/CtrlStep/CtrlStepMotor.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Devices.Arm.CtrlStep +{ + /// + /// 已包含减速器的步进电机 + /// + public class CtrlStepMotor + { + /// + /// 回零之后偏差角度 + /// + public double OffsetAngle { get; set; } = 0; + /// + /// 最大扭转角 + /// + public double AngleLimitMax { get; set; } = 180; + /// + /// 最小扭转角 + /// + public double AngleLimitMin { get; set; } = -0.01; + + /// + /// 加速度 + /// + public double Acceleration { get; set; } = 0; + /// + /// 当前电流(比例得出力矩) + /// + public float Current { get; set; } + /// + /// 速度 + /// + public double Velocity { get; set; } = 50; + /// + /// 当前角度 + /// + public double Angle { get; set; } = 0; + /// + /// 减速比 + /// + public double Reduction { get; set; } = 30; + /// + /// 电机方向 + /// + public int Direction { get; set; } = 1; + /// + /// 是否执行完命令 + /// + public bool IsFinish { get; set; } = true; + + } +} diff --git a/HMIcode/SmallProject/SmallProject/Devices/Arm/JykerArm.cs b/HMIcode/SmallProject/SmallProject/Devices/Arm/JykerArm.cs new file mode 100644 index 0000000..a904c45 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Devices/Arm/JykerArm.cs @@ -0,0 +1,175 @@ +using SmallProject.Configs; +using SmallProject.Devices.Arm.CtrlStep; +using SmallProject.Devices.Arm.Kinematic.Models; +using SmallProject.Devices.Arm.Kinematic; +using SmallProject.Logger; +using SmallProject.Serials.Slcan; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Processing.Processors.Transforms; +using Microsoft.VisualBasic.Logging; + +namespace SmallProject.Devices.Arm +{ + public class JykerArm + { + //角度转换成脉冲 3200 为 360° + public const double DEG_TO_PULSE = 8.888888889; + + public Dof6kinematic dof6Solver; + public CtrlStepMotor[] motorJ; + public Joint6D_t currentJoints; + public Joint6D_t prepareJoints; + public Pose6D_t preparePose6D; + + public JykerArm() { + dof6Solver = new Dof6kinematic(App.JConfig); + motorJ = new CtrlStepMotor[6] { + new CtrlStepMotor(){ AngleLimitMin =-90.1,AngleLimitMax = 90.1,Reduction = App.JConfig.ReductionJ1} + ,new CtrlStepMotor(){ AngleLimitMin = -90.1,AngleLimitMax = 90.1 , Direction =-1, OffsetAngle = -90,Reduction = App.JConfig.ReductionJ2 } + ,new CtrlStepMotor(){ AngleLimitMin = -0.1,AngleLimitMax= 180.1,OffsetAngle = 180 , Direction = 1,Reduction = App.JConfig.ReductionJ3} + ,new CtrlStepMotor(){ AngleLimitMin = -0.1,AngleLimitMax = 180.1 , Direction =-1,Reduction = App.JConfig.ReductionJ4} + ,new CtrlStepMotor(){ AngleLimitMin = -90.1 , AngleLimitMax = 90.1,Reduction = App.JConfig.ReductionJ5} + ,new CtrlStepMotor() { AngleLimitMin = -180.1,AngleLimitMax = 180.1,Reduction = App.JConfig.ReductionJ6} + }; + preparePose6D = new Pose6D_t(); + prepareJoints = Joint6D_t.defult; + currentJoints = Joint6D_t.defult; + } + + public void LoopStatus() + { + Task.Run(() => + { + for (int i = 0; i < 6; i++) + { + GetStatus(i + 1); + } + }); + } + + public void GetStatus(int Id) + { + var motor = motorJ[Id - 1]; + motor.Current = 0; + var frame = SlcanParser.ParseSlcanFrameStr(Id, 0x21); + App.Core?.Serial?.PushDataToQueue(frame); + } + + //更新电流 + public void RecieveCurrent(int Id,float value) + { + var motor = motorJ[Id - 1]; + motor.Current = Math.Abs(value); + } + //更新速度 + public void RecieveVelocity(int Id, float velocity) + { + var motor = motorJ[Id - 1]; + motor.Velocity = Math.Abs(velocity); + } + //更新位置 + public void RecievePos(int Id, float value,bool isFinish) + { + var motor = motorJ[Id - 1]; + motor.Angle = value/ motor.Reduction; + motor.IsFinish = isFinish; + JLog.Info(""+motor.Angle); + } + + //更新机械臂位置 + public void Move(double[] angles) + { + if (angles.Length!=6) + { + JLog.Info("机械臂角度参数缺失"); + return; + } + for (int i = 0; i < angles.Length; i++) + { + if (angles[i] > motorJ[i].AngleLimitMax + || angles[i] < motorJ[i].AngleLimitMin) + { + JLog.Info("机械臂设置角度超限"); + return; + } + } + var speed = App.JConfig.Speed; + if (speed <= 0) + { + JLog.Info("速度设置为0 ,请从新设置"); + return; + } + + //设置新坐标值 + prepareJoints = new Joint6D_t(angles); + dof6Solver.SolveFK(prepareJoints, preparePose6D); + + var angleList = (prepareJoints - currentJoints).a; + var maxAngle = angleList.Max(t => Math.Abs(t)); + var time = maxAngle / speed; + JLog.Info($"time:{time}"); + for (int i = 0; i < angles.Length; i++) + { + if (i == 5) time = 1; + //if (i == 3||i==4||i==5 ) continue; + motorJ[i].Angle = prepareJoints.a[i] - Joint6D_t.defult.a[i]; + var moveAngle = Convert.ToSingle(motorJ[i].Angle * motorJ[i].Direction * motorJ[i].Reduction / 360); + + var frame = SlcanParser.ParseSlcanFrameStr(i + 1, 0x06 + , moveAngle, (float)time); + JLog.Info($"moveAngle[{i}]:{moveAngle} time: {time}"); + App.Core.Serial.PushDataToQueue(frame); + + } + //更新位置 + currentJoints = prepareJoints; + + + + } + //设置当前位置为零位 + public void ApplyHomePosition() + { + var canFrame = SlcanParser.ParseSlcanFrameStr(0, 0x15); + App.Core.Serial?.PushDataToQueue(canFrame); + } + //设置立刻停止 + public void StopNow() + { + var canFrame = SlcanParser.ParseSlcanFrameStr(0, 0x04); + App.Core.Serial?.PushDataToQueue(canFrame); + } + //生成逆解解算结果 + public bool SolveIK(double[] vals) + { + var preparePose6D = new Pose6D_t(vals); + var res =dof6Solver.AfterSolveIK(preparePose6D, prepareJoints, currentJoints, motorJ, out Joint6D_t _outputJoints); + if(res) + { + prepareJoints = _outputJoints; + } + return res; + } + //设置轴的最大限制电流 + public void SetLimitCurrent(int Id,float current) + { + if(current>3000) + { + JLog.Info("电流最大设置为3000mA."); + return; + } + if(current<=0) + { + JLog.Info("电流设置值有误."); + return; + } + var canFrame = SlcanParser.ParseSlcanFrameStr(Id, 0x12, current / 1000); + App.Core.Serial?.PushDataToQueue(canFrame); + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Dof6kinematic.cs b/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Dof6kinematic.cs new file mode 100644 index 0000000..71d8949 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Dof6kinematic.cs @@ -0,0 +1,601 @@ +using SmallProject.Configs; +using SmallProject.Devices.Arm.CtrlStep; +using SmallProject.Devices.Arm.Kinematic.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Devices.Arm.Kinematic +{ + public class Dof6kinematic + { + public const double RAD_TO_DEG = 57.295777754771045f; + double[,] DH_matrix; + double[] L1_base = new double[3]; + double[] L2_arm = new double[3]; + double[] L3_elbow = new double[3]; + double[] L6_wrist = new double[3]; + + double l_se_2; + double l_se; + double l_ew_2; + double l_ew; + double atan_e; + + const double M_PI = Math.PI; + const double M_PI_2 = Math.PI / 2; + + JConfiguration armConfig; + + void MatMultiply(double[] _matrix1, double[] _matrix2, double[] _matrixOut + , int _m, int _l, int _n) + { + double tmp; + int i, j, k; + for (i = 0; i < _m; i++) + { + for (j = 0; j < _n; j++) + { + tmp = 0.0f; + for (k = 0; k < _l; k++) + { + tmp += _matrix1[_l * i + k] * _matrix2[_n * k + j]; + } + _matrixOut[_n * i + j] = tmp; + } + } + } + + void RotMatToEulerAngle(double[] _rotationM, double[] _eulerAngles) + { + double A, B, C, cb; + + if (Math.Abs(_rotationM[6]) >= 1.0 - 0.0001) + { + if (_rotationM[6] < 0) + { + A = 0.0f; + B = Math.PI / 2; + C = Math.Atan2(_rotationM[1], _rotationM[4]); + } + else + { + A = 0.0f; + B = -Math.PI / 2; + C = -Math.Atan2(_rotationM[1], _rotationM[4]); + } + } + else + { + B = Math.Atan2(-_rotationM[6], Math.Sqrt(_rotationM[0] * _rotationM[0] + _rotationM[3] * _rotationM[3])); + cb = Math.Cos(B); + A = Math.Atan2(_rotationM[3] / cb, _rotationM[0] / cb); + C = Math.Atan2(_rotationM[7] / cb, _rotationM[8] / cb); + } + + _eulerAngles[3] = C; + _eulerAngles[4] = B; + _eulerAngles[5] = A; + } + + void EulerAngleToRotMat(double[] _eulerAngles, double[] _rotationM) + { + double ca, cb, cc, sa, sb, sc; + + cc = Math.Cos(_eulerAngles[0]); + cb = Math.Cos(_eulerAngles[1]); + ca = Math.Cos(_eulerAngles[2]); + sc = Math.Sin(_eulerAngles[0]); + sb = Math.Sin(_eulerAngles[1]); + sa = Math.Sin(_eulerAngles[2]); + + _rotationM[0] = ca * cb; + _rotationM[1] = ca * sb * sc - sa * cc; + _rotationM[2] = ca * sb * cc + sa * sc; + _rotationM[3] = sa * cb; + _rotationM[4] = sa * sb * sc + ca * cc; + _rotationM[5] = sa * sb * cc - ca * sc; + _rotationM[6] = -sb; + _rotationM[7] = cb * sc; + _rotationM[8] = cb * cc; + } + + public Dof6kinematic(JConfiguration armConfig) + { + this.armConfig = armConfig; + + double[,] tmp_DH_matrix = new double[6, 4]{ + { 0.0f, armConfig.L_BASE, armConfig.D_BASE, -Math.PI/2}, + { -Math.PI/2, 0.0f, armConfig.L_ARM, 0.0f}, + { Math.PI/2, armConfig.D_ELBOW, 0.0f, Math.PI/2}, + { 0.0f, armConfig.L_FOREARM, 0.0f, -Math.PI/2}, + { 0.0f, 0.0f, 0.0f, Math.PI/2}, + { 0.0f, armConfig.L_WRIST, 0.0f, 0.0f} + }; + DH_matrix = tmp_DH_matrix; + + L1_base = new double[] { armConfig.D_BASE, -armConfig.L_BASE, 0.0f }; + L2_arm = new double[] { armConfig.L_ARM, 0.0f, 0.0f }; + L3_elbow = new double[] { -armConfig.D_ELBOW, 0.0f, armConfig.L_FOREARM }; + L6_wrist = new double[] { 0.0f, 0.0f, armConfig.L_WRIST }; + + l_se_2 = armConfig.L_ARM * armConfig.L_ARM; + l_se = armConfig.L_ARM; + l_ew_2 = armConfig.L_FOREARM * armConfig.L_FOREARM + armConfig.D_ELBOW * armConfig.D_ELBOW; + l_ew = 0; + atan_e = 0; + + + } + //正解 + public bool SolveFK(Joint6D_t joint, Pose6D_t pose) + { + double[] q_in = new double[6]; + double[] q = new double[6]; + double cosq, sinq; + double cosa, sina; + double[] P06 = new double[6]; + double[] R06 = new double[9]; + double[][] R = new double[6][] { new double[9], new double[9], new double[9], new double[9], new double[9], new double[9] }; + double[] R02 = new double[9]; + double[] R03 = new double[9]; + double[] R04 = new double[9]; + double[] R05 = new double[9]; + double[] L0_bs = new double[3]; + double[] L0_se = new double[3]; + double[] L0_ew = new double[3]; + double[] L0_wt = new double[3]; + + for (int i = 0; i < 6; i++) + q_in[i] = joint.a[i] / RAD_TO_DEG; + + for (int i = 0; i < 6; i++) + { + q[i] = q_in[i] + DH_matrix[i, 0]; + cosq = Math.Cos(q[i]); + sinq = Math.Sin(q[i]); + cosa = Math.Cos(DH_matrix[i, 3]); + sina = Math.Sin(DH_matrix[i, 3]); + + R[i][0] = cosq; + R[i][1] = -cosa * sinq; + R[i][2] = sina * sinq; + R[i][3] = sinq; + R[i][4] = cosa * cosq; + R[i][5] = -sina * cosq; + R[i][6] = 0.0f; + R[i][7] = sina; + R[i][8] = cosa; + } + MatMultiply(R[0], R[1], R02, 3, 3, 3); + MatMultiply(R02, R[2], R03, 3, 3, 3); + MatMultiply(R03, R[3], R04, 3, 3, 3); + MatMultiply(R04, R[4], R05, 3, 3, 3); + MatMultiply(R05, R[5], R06, 3, 3, 3); + + MatMultiply(R[0], L1_base, L0_bs, 3, 3, 1); + MatMultiply(R02, L2_arm, L0_se, 3, 3, 1); + MatMultiply(R03, L3_elbow, L0_ew, 3, 3, 1); + MatMultiply(R06, L6_wrist, L0_wt, 3, 3, 1); + + for (int i = 0; i < 3; i++) + P06[i] = L0_bs[i] + L0_se[i] + L0_ew[i] + L0_wt[i]; + + RotMatToEulerAngle(R06, P06); + + pose.X = (float)P06[0]; + pose.Y = (float)P06[1]; + pose.Z = (float)P06[2]; + pose.A = (float)(P06[3] * RAD_TO_DEG); + pose.B = (float)(P06[4] * RAD_TO_DEG); + pose.C = (float)(P06[5] * RAD_TO_DEG); + + //pose.A = (float)(P06[3]); + //pose.B = (float)(P06[4]); + //pose.C = (float)(P06[5]); + pose.R = R06.ToArray(); + + return true; + } + //逆解 + public bool SolveIK(Pose6D_t _inputPose6D, Joint6D_t _lastJoint6D, out IKSolves_t _outputSolves) + { + _outputSolves = new IKSolves_t(); + + double[] qs = new double[2]; + double[][] qa = new double[2][] { new double[2], new double[2] }; + double[][] qw = new double[2][] { new double[3], new double[3] }; + double cosqs, sinqs; + double[] cosqa = new double[2], sinqa = new double[2]; + double cosqw, sinqw; + double[] P06 = new double[6]; + double[] R06 = new double[9]; + double[] P0_w = new double[3]; + double[] P1_w = new double[3]; + double[] L0_wt = new double[3]; + double[] L1_sw = new double[3]; + double[] R10 = new double[9]; + double[] R31 = new double[9]; + double[] R30 = new double[9]; + double[] R36 = new double[9]; + double l_sw_2, l_sw, atan_a, acos_a, acos_e; + + int ind_arm, ind_elbow, ind_wrist; + int i; + + if (0 == l_ew) + { + l_ew = Math.Sqrt(l_ew_2); + atan_e = Math.Atan(armConfig.D_ELBOW / armConfig.L_FOREARM); + } + + P06[0] = _inputPose6D.X; + P06[1] = _inputPose6D.Y; + P06[2] = _inputPose6D.Z; + if (!_inputPose6D.hasR) + { + P06[3] = _inputPose6D.A / RAD_TO_DEG; + P06[4] = _inputPose6D.B / RAD_TO_DEG; + P06[5] = _inputPose6D.C / RAD_TO_DEG; + EulerAngleToRotMat(P06.Skip(3).ToArray(), R06); + } + else + { + Array.Copy(R06, _inputPose6D.R, 9); + } + for (i = 0; i < 2; i++) + { + qs[i] = _lastJoint6D.a[0]; + qa[i][0] = _lastJoint6D.a[1]; + qa[i][1] = _lastJoint6D.a[2]; + qw[i][0] = _lastJoint6D.a[3]; + qw[i][1] = _lastJoint6D.a[4]; + qw[i][2] = _lastJoint6D.a[5]; + } + MatMultiply(R06, L6_wrist, L0_wt, 3, 3, 1); + for (i = 0; i < 3; i++) + { + P0_w[i] = P06[i] - L0_wt[i]; + } + if (Math.Sqrt(P0_w[0] * P0_w[0] + P0_w[1] * P0_w[1]) <= 0.000001) + { + qs[0] = _lastJoint6D.a[0]; + qs[1] = _lastJoint6D.a[0]; + for (i = 0; i < 4; i++) + { + _outputSolves.solFlag[0 + i][0] = -1; + _outputSolves.solFlag[4 + i][0] = -1; + } + } + else + { + qs[0] = Math.Atan2(P0_w[1], P0_w[0]); + qs[1] = Math.Atan2(-P0_w[1], -P0_w[0]); + for (i = 0; i < 4; i++) + { + _outputSolves.solFlag[0 + i][0] = 1; + _outputSolves.solFlag[4 + i][0] = 1; + } + } + for (ind_arm = 0; ind_arm < 2; ind_arm++) + { + cosqs = Math.Cos(qs[ind_arm] + DH_matrix[0, 0]); + sinqs = Math.Sin(qs[ind_arm] + DH_matrix[0, 0]); + + R10[0] = cosqs; + R10[1] = sinqs; + R10[2] = 0.0f; + R10[3] = 0.0f; + R10[4] = 0.0f; + R10[5] = -1.0f; + R10[6] = -sinqs; + R10[7] = cosqs; + R10[8] = 0.0f; + + MatMultiply(R10, P0_w, P1_w, 3, 3, 1); + for (i = 0; i < 3; i++) + { + L1_sw[i] = P1_w[i] - L1_base[i]; + } + l_sw_2 = L1_sw[0] * L1_sw[0] + L1_sw[1] * L1_sw[1]; + l_sw = Math.Sqrt(l_sw_2); + + if (Math.Abs(l_se + l_ew - l_sw) <= 0.000001) + { + qa[0][0] = Math.Atan2(L1_sw[1], L1_sw[0]); + qa[1][0] = qa[0][0]; + qa[0][1] = 0.0f; + qa[1][1] = 0.0f; + if (l_sw > l_se + l_ew) + { + for (i = 0; i < 2; i++) + { + _outputSolves.solFlag[4 * ind_arm + 0 + i][1] = 0; + _outputSolves.solFlag[4 * ind_arm + 2 + i][1] = 0; + } + } + else + { + for (i = 0; i < 2; i++) + { + _outputSolves.solFlag[4 * ind_arm + 0 + i][1] = 1; + _outputSolves.solFlag[4 * ind_arm + 2 + i][1] = 1; + } + } + } + else if (Math.Abs(l_sw - Math.Abs(l_se - l_ew)) <= 0.000001) + { + qa[0][0] = Math.Atan2(L1_sw[1], L1_sw[0]); + qa[1][0] = qa[0][0]; + if (0 == ind_arm) + { + qa[0][1] = M_PI; + qa[1][1] = -M_PI; + } + else + { + qa[0][1] = -M_PI; + qa[1][1] = M_PI; + } + if (l_sw < Math.Abs(l_se - l_ew)) + { + for (i = 0; i < 2; i++) + { + _outputSolves.solFlag[4 * ind_arm + 0 + i][1] = 0; + _outputSolves.solFlag[4 * ind_arm + 2 + i][1] = 0; + } + } + else + { + for (i = 0; i < 2; i++) + { + _outputSolves.solFlag[4 * ind_arm + 0 + i][1] = 1; + _outputSolves.solFlag[4 * ind_arm + 2 + i][1] = 1; + } + } + } + else + { + atan_a = Math.Atan2(L1_sw[1], L1_sw[0]); + acos_a = 0.5f * (l_se_2 + l_sw_2 - l_ew_2) / (l_se * l_sw); + if (acos_a >= 1.0f) acos_a = 0.0f; + else if (acos_a <= -1.0f) acos_a = M_PI; + else acos_a = Math.Acos(acos_a); + acos_e = 0.5f * (l_se_2 + l_ew_2 - l_sw_2) / (l_se * l_ew); + if (acos_e >= 1.0f) acos_e = 0.0f; + else if (acos_e <= -1.0f) acos_e = M_PI; + else acos_e = Math.Acos(acos_e); + if (0 == ind_arm) + { + qa[0][0] = atan_a - acos_a + M_PI_2; + qa[0][1] = atan_e - acos_e + M_PI; + qa[1][0] = atan_a + acos_a + M_PI_2; + qa[1][1] = atan_e + acos_e - M_PI; + + } + else + { + qa[0][0] = atan_a + acos_a + M_PI_2; + qa[0][1] = atan_e + acos_e - M_PI; + qa[1][0] = atan_a - acos_a + M_PI_2; + qa[1][1] = atan_e - acos_e + M_PI; + } + for (i = 0; i < 2; i++) + { + _outputSolves.solFlag[4 * ind_arm + 0 + i][1] = 1; + _outputSolves.solFlag[4 * ind_arm + 2 + i][1] = 1; + } + } + for (ind_elbow = 0; ind_elbow < 2; ind_elbow++) + { + cosqa[0] = Math.Cos(qa[ind_elbow][0] + DH_matrix[1, 0]); + sinqa[0] = Math.Sin(qa[ind_elbow][0] + DH_matrix[1, 0]); + cosqa[1] = Math.Cos(qa[ind_elbow][1] + DH_matrix[2, 0]); + sinqa[1] = Math.Sin(qa[ind_elbow][1] + DH_matrix[2, 0]); + + R31[0] = cosqa[0] * cosqa[1] - sinqa[0] * sinqa[1]; + R31[1] = cosqa[0] * sinqa[1] + sinqa[0] * cosqa[1]; + R31[2] = 0.0f; + R31[3] = 0.0f; + R31[4] = 0.0f; + R31[5] = 1.0f; + R31[6] = cosqa[0] * sinqa[1] + sinqa[0] * cosqa[1]; + R31[7] = -cosqa[0] * cosqa[1] + sinqa[0] * sinqa[1]; + R31[8] = 0.0f; + + MatMultiply(R31, R10, R30, 3, 3, 3); + MatMultiply(R30, R06, R36, 3, 3, 3); + + if (R36[8] >= 1.0 - 0.000001) + { + cosqw = 1.0f; + qw[0][1] = 0.0f; + qw[1][1] = 0.0f; + } + else if (R36[8] <= -1.0 + 0.000001) + { + cosqw = -1.0f; + if (0 == ind_arm) + { + qw[0][1] = M_PI; + qw[1][1] = -M_PI; + } + else + { + qw[0][1] = -M_PI; + qw[1][1] = M_PI; + } + } + else + { + cosqw = R36[8]; + if (0 == ind_arm) + { + qw[0][1] = Math.Acos(cosqw); + qw[1][1] = -Math.Acos(cosqw); + } + else + { + qw[0][1] = -Math.Acos(cosqw); + qw[1][1] = Math.Acos(cosqw); + } + } + if (1.0f == cosqw || -1.0f == cosqw) + { + if (0 == ind_arm) + { + qw[0][0] = _lastJoint6D.a[3]; + cosqw = Math.Cos(_lastJoint6D.a[3] + DH_matrix[3, 0]); + sinqw = Math.Sin(_lastJoint6D.a[3] + DH_matrix[3, 0]); + qw[0][2] = Math.Atan2(cosqw * R36[3] - sinqw * R36[0], cosqw * R36[0] + sinqw * R36[3]); + qw[1][2] = _lastJoint6D.a[5]; + cosqw = Math.Cos(_lastJoint6D.a[5] + DH_matrix[5, 0]); + sinqw = Math.Sin(_lastJoint6D.a[5] + DH_matrix[5, 0]); + qw[1][0] = Math.Atan2(cosqw * R36[3] - sinqw * R36[0], cosqw * R36[0] + sinqw * R36[3]); + } + else + { + qw[0][2] = _lastJoint6D.a[5]; + cosqw = Math.Cos(_lastJoint6D.a[5] + DH_matrix[5, 0]); + sinqw = Math.Sin(_lastJoint6D.a[5] + DH_matrix[5, 0]); + qw[0][0] = Math.Atan2(cosqw * R36[3] - sinqw * R36[0], cosqw * R36[0] + sinqw * R36[3]); + qw[1][0] = _lastJoint6D.a[3]; + cosqw = Math.Cos(_lastJoint6D.a[3] + DH_matrix[3, 0]); + sinqw = Math.Sin(_lastJoint6D.a[3] + DH_matrix[3, 0]); + qw[1][2] = Math.Atan2(cosqw * R36[3] - sinqw * R36[0], cosqw * R36[0] + sinqw * R36[3]); + } + _outputSolves.solFlag[4 * ind_arm + 2 * ind_elbow + 0][2] = -1; + _outputSolves.solFlag[4 * ind_arm + 2 * ind_elbow + 1][2] = -1; + } + else + { + if (0 == ind_arm) + { + qw[0][0] = Math.Atan2(R36[5], R36[2]); + qw[1][0] = Math.Atan2(-R36[5], -R36[2]); + qw[0][2] = Math.Atan2(R36[7], -R36[6]); + qw[1][2] = Math.Atan2(-R36[7], R36[6]); + } + else + { + qw[0][0] = Math.Atan2(-R36[5], -R36[2]); + qw[1][0] = Math.Atan2(R36[5], R36[2]); + qw[0][2] = Math.Atan2(-R36[7], R36[6]); + qw[1][2] = Math.Atan2(R36[7], -R36[6]); + } + _outputSolves.solFlag[4 * ind_arm + 2 * ind_elbow + 0][2] = 1; + _outputSolves.solFlag[4 * ind_arm + 2 * ind_elbow + 1][2] = 1; + } + for (ind_wrist = 0; ind_wrist < 2; ind_wrist++) + { + if (qs[ind_arm] > M_PI) + _outputSolves.config[4 * ind_arm + 2 * ind_elbow + ind_wrist].a[0] = + qs[ind_arm] - M_PI; + else if (qs[ind_arm] < -M_PI) + _outputSolves.config[4 * ind_arm + 2 * ind_elbow + ind_wrist].a[0] = + qs[ind_arm] + M_PI; + else + _outputSolves.config[4 * ind_arm + 2 * ind_elbow + ind_wrist].a[0] = qs[ind_arm]; + + for (i = 0; i < 2; i++) + { + if (qa[ind_elbow][i] > M_PI) + _outputSolves.config[4 * ind_arm + 2 * ind_elbow + ind_wrist].a[1 + i] = + qa[ind_elbow][i] - M_PI; + else if (qa[ind_elbow][i] < -M_PI) + _outputSolves.config[4 * ind_arm + 2 * ind_elbow + ind_wrist].a[1 + i] = + qa[ind_elbow][i] + M_PI; + else + _outputSolves.config[4 * ind_arm + 2 * ind_elbow + ind_wrist].a[1 + i] = + qa[ind_elbow][i]; + } + + for (i = 0; i < 3; i++) + { + if (qw[ind_wrist][i] > M_PI) + _outputSolves.config[4 * ind_arm + 2 * ind_elbow + ind_wrist].a[3 + i] = + qw[ind_wrist][i] - M_PI; + else if (qw[ind_wrist][i] < -M_PI) + _outputSolves.config[4 * ind_arm + 2 * ind_elbow + ind_wrist].a[3 + i] = + qw[ind_wrist][i] + M_PI; + else + _outputSolves.config[4 * ind_arm + 2 * ind_elbow + ind_wrist].a[3 + i] = + qw[ind_wrist][i]; + } + } + } + } + + foreach (var one in _outputSolves.config) + { + for (int j = 0; j < one.a.Length; j++) + { + one.a[j] = one.a[j] * RAD_TO_DEG; + } + } + + return true; + } + + //逆解后选出最优解 + public bool AfterSolveIK(Pose6D_t _inputPose6D, Joint6D_t _currentJoints, Joint6D_t _lastJoint6D + , CtrlStepMotor[] motorJ,out Joint6D_t _outputJoints) + { + _outputJoints = new Joint6D_t(); + SolveIK(_inputPose6D, _lastJoint6D, out IKSolves_t _outputSolves); + bool[] valid = new bool[8]; + int validCnt = 0; + + for (int i = 0; i < 8; i++) + { + valid[i] = true; + + for (int j = 0; j < 6; j++) + { + if (_outputSolves.config[i].a[j] > motorJ[j].AngleLimitMax || + _outputSolves.config[i].a[j] < motorJ[j].AngleLimitMin) + { + valid[i] = false; + continue; + } + } + + if (valid[i]) validCnt++; + } + + if (validCnt > 0) + { + double min = 1000; + int indexConfig = 0; + for (int i = 0; i < 8; i++) + { + if (valid[i]) + { + Joint6D_t tmp = _currentJoints - _lastJoint6D; + double maxAngle = tmp.a.Max(); + if (maxAngle < min) + { + min = maxAngle; + indexConfig = i; + } + + } + + _outputJoints = new Joint6D_t(_outputSolves.config[indexConfig].a[0] + , _outputSolves.config[indexConfig].a[1] + , _outputSolves.config[indexConfig].a[2] + , _outputSolves.config[indexConfig].a[3] + , _outputSolves.config[indexConfig].a[4] + , _outputSolves.config[indexConfig].a[5]); + + return true; + } + } + + return false; + + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Models/IKSolves_t.cs b/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Models/IKSolves_t.cs new file mode 100644 index 0000000..a4052e4 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Models/IKSolves_t.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Devices.Arm.Kinematic.Models +{ + public class IKSolves_t + { + public Joint6D_t[] config = new Joint6D_t[8] { + new Joint6D_t(), new Joint6D_t() + , new Joint6D_t(), new Joint6D_t() + ,new Joint6D_t(), new Joint6D_t() + ,new Joint6D_t(), new Joint6D_t() }; + public int[][] solFlag = new int[8][] { new int[3], new int[3], new int[3], new int[3], new int[3], new int[3], new int[3], new int[3] }; + } +} diff --git a/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Models/Joint6D_t.cs b/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Models/Joint6D_t.cs new file mode 100644 index 0000000..641618e --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Models/Joint6D_t.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Devices.Arm.Kinematic.Models +{ + public class Joint6D_t + { + public Joint6D_t(double a1, double a2, double a3, double a4, double a5, double a6) + { + a = new double[] { a1, a2, a3, a4, a5, a6 }; + } + + public Joint6D_t(double[] a) + { + this.a = a; + } + + public Joint6D_t() { } + + public double[] a = new double[6]; + + public static Joint6D_t operator -(Joint6D_t _joints1, Joint6D_t _joints2) + { + Joint6D_t tmp = new Joint6D_t(); + for (int i = 0; i < 6; i++) + tmp.a[i] = _joints1.a[i] - _joints2.a[i]; + return tmp; + } + + public static Joint6D_t defult = new Joint6D_t(0, 0, 180, 0, -90, 0); + } +} diff --git a/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Models/Pose6D_t.cs b/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Models/Pose6D_t.cs new file mode 100644 index 0000000..837f1cb --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Devices/Arm/Kinematic/Models/Pose6D_t.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Devices.Arm.Kinematic.Models +{ + public class Pose6D_t + { + + public Pose6D_t() + { + + } + + public Pose6D_t(double x, double y, double z, double a, double b, double c) + { + X = x; + Y = y; + Z = z; + A = a; + B = b; + C = c; + } + + public Pose6D_t(double[] vals) + { + X = vals[0]; + Y = vals[1]; + Z = vals[2]; + A = vals[3]; + B = vals[4]; + C = vals[5]; + } + //x 坐标 + public double X { get; set; } + //y 坐标 + public double Y { get; set; } + //z 坐标 + public double Z { get; set; } + //角度 a + public double A { get; set; } + //角度 b + public double B { get; set; } + //角度 c + public double C { get; set; } + //变换矩阵 + public double[] R { get; set; } + //是否存在变换矩阵 + public bool hasR { get; set; } = false; + } +} diff --git a/HMIcode/SmallProject/SmallProject/Dialogs/ConfigDialog.xaml b/HMIcode/SmallProject/SmallProject/Dialogs/ConfigDialog.xaml new file mode 100644 index 0000000..2b31a70 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Dialogs/ConfigDialog.xaml @@ -0,0 +1,54 @@ + + + + + J1减速比 + + + + J2减速比 + + + + J3减速比 + + + + J4减速比 + + + + J5减速比 + + + + J6减速比 + + + + 速度 + + + + 电流(A) + + + + + + + + diff --git a/HMIcode/SmallProject/SmallProject/Dialogs/ConfigDialog.xaml.cs b/HMIcode/SmallProject/SmallProject/Dialogs/ConfigDialog.xaml.cs new file mode 100644 index 0000000..4f9024b --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Dialogs/ConfigDialog.xaml.cs @@ -0,0 +1,43 @@ +using SmallProject.Configs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace SmallProject.Dialogs +{ + /// + /// ConfigDialog.xaml 的交互逻辑 + /// + public partial class ConfigDialog : Rubyer.RubyerWindow + { + public ConfigDialog() + { + InitializeComponent(); + //加载配置项 + this.DataContext = App.JConfig; + this.Loaded += ConfigDialog_Loaded; + } + + //加载 + private void ConfigDialog_Loaded(object sender, RoutedEventArgs e) + { + } + + private void EnterOK_Click(object sender, RoutedEventArgs e) + { + var conf = App.JConfig; + ConfigResposity.WriteConfigs(conf); + Rubyer.MessageBoxR.Success("保存成功。"); + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/Dialogs/MotorConfigDialog.xaml b/HMIcode/SmallProject/SmallProject/Dialogs/MotorConfigDialog.xaml new file mode 100644 index 0000000..ccecaf7 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Dialogs/MotorConfigDialog.xaml @@ -0,0 +1,36 @@ + + + + + 电机ID + + + + 电流限制值(mA) + + + + + + + + + diff --git a/HMIcode/SmallProject/SmallProject/Dialogs/MotorConfigDialog.xaml.cs b/HMIcode/SmallProject/SmallProject/Dialogs/MotorConfigDialog.xaml.cs new file mode 100644 index 0000000..f534c87 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Dialogs/MotorConfigDialog.xaml.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace SmallProject.Dialogs +{ + /// + /// MotorConfigDialog.xaml 的交互逻辑 + /// + public partial class MotorConfigDialog : Rubyer.RubyerWindow + { + public MotorConfigDialog() + { + InitializeComponent(); + } + + //读取信息 + private void bt_Read_Click(object sender, RoutedEventArgs e) + { + + } + + private void bt_EnterOK_Click(object sender, RoutedEventArgs e) + { + var id = (int)tb_ID.Value; + var value = (float)tb_Current.Value; + App.Core.Jyker.SetLimitCurrent(id, value); + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/Logger/JLog.cs b/HMIcode/SmallProject/SmallProject/Logger/JLog.cs new file mode 100644 index 0000000..e59acd4 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Logger/JLog.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Logger +{ + internal class JLog + { + internal static event Action MessageEvent; + internal static event Action MessageEventLive; + + /// + /// 打印错误信息 + /// + /// + internal static void Error(Exception msg) + { + Masuit.Tools.Logging.LogManager.Error(msg); + //MessageEvent?.Invoke(msg.Message); + } + + /// + /// 打印普通信息 + /// + /// + internal static void Info(string msg) + { + Masuit.Tools.Logging.LogManager.Info(msg); + MessageEvent?.Invoke(msg); + } + + /// + /// 打印普通信息 + /// + /// + internal static void InfoLive(string msg) + { + Masuit.Tools.Logging.LogManager.Info(msg); + MessageEventLive?.Invoke(msg); + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/MCP/JykerControlMCP.cs b/HMIcode/SmallProject/SmallProject/MCP/JykerControlMCP.cs new file mode 100644 index 0000000..e3a9f86 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/MCP/JykerControlMCP.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Hosting; +using SmallProject.MCP.Xiaozhi; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SmallProject.MCP.Tools; +using Newtonsoft.Json; +using SmallProject.MCP.Models; + +namespace SmallProject.MCP +{ + internal class JykerControlMCP + { + public async Task Init() + { + McpPip pi = new McpPip(); + pi.Init(); + } + + } +} diff --git a/HMIcode/SmallProject/SmallProject/MCP/Models/RequestBody.cs b/HMIcode/SmallProject/SmallProject/MCP/Models/RequestBody.cs new file mode 100644 index 0000000..f7b5b65 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/MCP/Models/RequestBody.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.MCP.Models +{ + public class RequestBody + { + public int Id { get; set; } + public string Jsonrpc { get; set; } + public string Method { get; set; } + public Params Params { get; set; } + } + + public class Params + { + public string ProtocolVersion { get; set; } + public CapabilitiesD Capabilities { get; set; } + public ClientInfo ClientInfo { get; set; } + + public string Name{get;set;} + public Dictionary Arguments { get; set; } = new Dictionary(); + } + + public class CapabilitiesD + { + public Sampling Sampling { get; set; } + public Roots Roots { get; set; } + } + + public class Sampling + { + // sampling 是一个空对象 {} + } + + public class Roots + { + public bool ListChanged { get; set; } + } + + public class ClientInfo + { + public string Name { get; set; } + public string Version { get; set; } + } +} diff --git a/HMIcode/SmallProject/SmallProject/MCP/Models/ResultBody.cs b/HMIcode/SmallProject/SmallProject/MCP/Models/ResultBody.cs new file mode 100644 index 0000000..cbc7a12 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/MCP/Models/ResultBody.cs @@ -0,0 +1,64 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.MCP.Models +{ + public class ResultBody + { + public string jsonrpc { get; set; } = "2.0"; + public int id { get; set; } + public dynamic result { get; set; } + + public bool isError { get; set; } = false; + } + + public class ToolRoot + { + public List Tools { get; set; } = new List(); + } + + public class Tool + { + public string Name { get; set; } + public string Description { get; set; } + public InputSchema InputSchema { get; set; } = new InputSchema(); + } + + public class InputSchema + { + public Dictionary Properties { get; set; } = new Dictionary(); + public string[] Required { get; set; } + public string Title { get; set; } + public string Type { get; set; } + } + + public class Property + { + public string Title { get; set; } + public string Type { get; set; } + } + + public class ContentRoot + { + public List content = new List(); + } + public class ResultText + { + public string type { get; set; } = "text"; + + public string text { get; set; } + } + + public class ResultContent + { + public bool success { get; set; } = true; + + public object result { get; set; } + } + + +} diff --git a/HMIcode/SmallProject/SmallProject/MCP/Serializers/ToolsSerializer.cs b/HMIcode/SmallProject/SmallProject/MCP/Serializers/ToolsSerializer.cs new file mode 100644 index 0000000..4766bc2 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/MCP/Serializers/ToolsSerializer.cs @@ -0,0 +1,56 @@ +using SmallProject.MCP.Models; +using SmallProject.MCP.Tools; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.MCP.Serializers +{ + internal class ToolsSerializer + { + + + public static List SerializeTool() + { + List tools = new List(); + Type type = typeof(T); + // 获取所有公共实例方法 + var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Where(t=>t.DeclaringType==type); + foreach (MethodInfo method in methods) + { + Tool tool = new Tool(); + // 获取方法的参数 + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length > 0) + { + foreach (ParameterInfo param in parameters) + { + + tool.InputSchema.Properties.Add(param.Name, new Property { Title = param.Name.ToUpper() + , Type = (param.ParameterType == typeof(int)? "integer":"text") + }); + } + tool.InputSchema.Required = parameters.Select(t => t.Name).ToArray(); + } + + // 获取属性 + var desc = method.GetCustomAttribute(); + + + tool.Name = method.Name; + tool.Description = desc.Description; + tool.InputSchema.Title = $"{method.Name}Arguments" ; + tool.InputSchema.Type = "object"; + tools.Add(tool); + } + + return tools; + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/MCP/Tools/JykerControl.cs b/HMIcode/SmallProject/SmallProject/MCP/Tools/JykerControl.cs new file mode 100644 index 0000000..54aa7ef --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/MCP/Tools/JykerControl.cs @@ -0,0 +1,167 @@ +using HalconDotNet; +using NAudio.CoreAudioApi; +using SmallProject.Logger; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace SmallProject.MCP.Tools +{ + public class JykerControl + { + //[Description("For mathamatical calculation, always use this tool to calculate the result of a add b")] + //public long calculator(long a, long b) + //{ + // return a + b; + //} + + [Description("用 前 后 左 右 上 下 六个命令控制方向,如果返回移动了就是移动成功了,没有就是没有成功。")] + public string jyker_move(string direction) + { + var curr = App.Core.Jyker.currentJoints; + JLog.Info(direction); + var joint1 = curr.a[0]; + var joint2 = curr.a[1]; + var joint3 = curr.a[2]; + + switch (direction) + { + case "前": + + App.Core.Jyker.Move(new double[]{ curr.a[0], joint2+10, joint3-10 + , curr.a[3], curr.a[4], curr.a[5] }); + break; + case "后": + if(joint2-1 < -90) + { + return "退不了了"; + } + App.Core.Jyker.Move(new double[]{ curr.a[0], joint2-10, joint3+10 + , curr.a[3], curr.a[4], curr.a[5] }); + break; + case "左": + if(joint1>30) + { + return "左不了了"; + } + App.Core.Jyker.Move(new double[]{joint1+20, curr.a[1], curr.a[2] + , curr.a[3], curr.a[4], curr.a[5] }); + break; + case "右": + if(joint1<-90) + { + return "右不了了"; + } + App.Core.Jyker.Move(new double[]{ joint1-20, curr.a[1], curr.a[2] + , curr.a[3], curr.a[4], curr.a[5] }); + break; + case "上": + if (curr.a[4] - 10 < -90) + { + return "脖子没那么长啦"; + } + App.Core.Jyker.Move(new double[]{ curr.a[0], curr.a[1], curr.a[2] + , curr.a[3], curr.a[4]-20, curr.a[5] }); + break; + case "下": + if (curr.a[4] + 10 > 90) + { + return "脖子没那么长啦"; + } + App.Core.Jyker.Move(new double[]{ curr.a[0], curr.a[1], curr.a[2] + , curr.a[3], curr.a[4]+20, curr.a[5] }); + break; + default: + break; + } + return "移动了"; + } + + [Description("这个方法会返回看到了什么,传入一个direction 的参数,下 代表往下看 上 代表往上看")] + public string jyker_view(string direction) + { + var curr = App.Core.Jyker.currentJoints; + switch (direction) + { + case "上": + if(curr.a[4] - 10<-90) + { + return "脖子没那么长啦"; + } + App.Core.Jyker.Move(new double[]{ curr.a[0], curr.a[1], curr.a[2] + , curr.a[3], curr.a[4]-20, curr.a[5] }); + break; + case "下": + if (curr.a[4]+10>90) + { + return "脖子没那么长啦"; + } + App.Core.Jyker.Move(new double[]{ curr.a[0], curr.a[1], curr.a[2] + , curr.a[3], curr.a[4]+20, curr.a[5] }); + break; + default: + break; + } + JLog.Info("我看到了"); + return "苹果"; + } + + [Description("这个方法传入一个参数state ,如果是抓,就是抓住,如果是放,就是放开")] + public string jyker_clumporopen(string state) + { + var curr = App.Core.Jyker.currentJoints; + var joint6 = curr.a[5]; + JLog.Info(state); + switch (state) + { + case "抓": + App.Core.Jyker.Move(new double[]{curr.a[0], curr.a[1], curr.a[2] + , curr.a[3], curr.a[4], 100 }); + Thread.Sleep(1000); + App.Core.Jyker.GetStatus(6); + Thread.Sleep(200); + JLog.Info($"Current {App.Core.Jyker.motorJ[5].Current}"); + if (App.Core.Jyker.motorJ[5].Current<0.5) + { + JLog.Info("没抓到东西"); + return "没抓到东西"; + } + else + { + JLog.Info("抓住了"); + return "抓住了"; + } + case "放": + App.Core.Jyker.Move(new double[]{curr.a[0], curr.a[1], curr.a[2] + , curr.a[3], curr.a[4], 0 }); + break; + default: + break; + } + + return "ok"; + } + + [Description("当有人跟你说这个东西是什么的时候,麻烦告诉一下系统,让系统知道这个东西是什么 传入的参数就是物体的名字")] + public string jyker_thisis(string thing) + { + JLog.Info($"jyker_thisis {thing}"); + App.Core.ViewName = thing; + //创建shapemodel + + return "知道了"; + } + + [Description("当阿然叫你富贵的时候麻烦告诉一下系统 ")] + public string jyker_wakeup() + { + JLog.Info("唤醒了"); + return "ok"; + } + + } +} diff --git a/HMIcode/SmallProject/SmallProject/MCP/Xiaozhi/McpPip.cs b/HMIcode/SmallProject/SmallProject/MCP/Xiaozhi/McpPip.cs new file mode 100644 index 0000000..cfbf105 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/MCP/Xiaozhi/McpPip.cs @@ -0,0 +1,206 @@ +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using NAudio.CoreAudioApi; +using Newtonsoft.Json; +using SmallProject.Logger; +using SmallProject.MCP.Models; +using SmallProject.MCP.Serializers; +using SmallProject.MCP.Tools; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.WebSockets; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Shapes; +using static System.Net.Mime.MediaTypeNames; + +namespace SmallProject.MCP.Xiaozhi +{ + + internal class McpPip + { + + private const string INITIAL_BACKOFF_ENV = "INITIAL_BACKOFF"; + private const string MAX_BACKOFF_ENV = "MAX_BACKOFF"; + + private static int reconnectAttempt = 0; + private static double backoff = 1.0; + private static readonly Random Random = new(); + + private static readonly string InitialBackoffStr = Environment.GetEnvironmentVariable(INITIAL_BACKOFF_ENV) ?? "1"; + private static readonly string MaxBackoffStr = Environment.GetEnvironmentVariable(MAX_BACKOFF_ENV) ?? "600"; + + private static readonly double INITIAL_BACKOFF = double.TryParse(InitialBackoffStr, out var val) ? val : 1; + private static readonly double MAX_BACKOFF = double.TryParse(MaxBackoffStr, out var val) ? val : 600; + + private static Dictionary ToolsDictionary = new Dictionary(); + + private static ResultBody toolsListBody = new ResultBody(); + + public async Task Init() + { + await AddTools(); + + + var endpointUrl = App.JConfig.EndpointUrl; + + if (string.IsNullOrEmpty(endpointUrl)) + { + JLog.Info("小智接入点为空。。"); + return; + } + + Console.CancelKeyPress += (sender, eventArgs) => + { + //JLog.Info("Received interrupt signal, shutting down..."); + eventArgs.Cancel = true; + return; + }; + + while (true) + { + try + { + await ConnectWithRetry(endpointUrl); + } + catch (Exception ex) + { + JLog.Error(ex); + await Task.Delay(TimeSpan.FromSeconds(backoff)); + backoff = Math.Min(backoff * 2, MAX_BACKOFF); + reconnectAttempt++; + } + Thread.Sleep(50); + } + } + + private async Task ConnectWithRetry(string uri) + { + using var cts = new CancellationTokenSource(); + using var client = new ClientWebSocket(); + + JLog.Info($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Connecting to WebSocket server..."); + + await client.ConnectAsync(new Uri(uri), cts.Token); + + JLog.Info($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Successfully connected to WebSocket server"); + + reconnectAttempt = 0; + backoff = INITIAL_BACKOFF; + + var receiveTask = PipeWebSocketReceive(client, cts.Token); + var sendTask = PipeWebSocketSend(client, cts.Token); + + await Task.WhenAll(receiveTask, sendTask); + + } + private async Task PipeWebSocketReceive(ClientWebSocket ws, CancellationToken token) + { + var buffer = new byte[4096]; + + while (ws.State == WebSocketState.Open) + { + var result = await ws.ReceiveAsync(buffer, token); + + if (result.MessageType == WebSocketMessageType.Close) + { + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, token); + break; + } + + var message = Encoding.UTF8.GetString(buffer, 0, result.Count); + JLog.Info(message); + var res =JsonConvert.DeserializeObject(message); + switch (res.Method) + { + case "initialize": + //JLog.Info("initialize"); + var b = @"{""jsonrpc"":""2.0"",""id"":0,""result"":{""protocolVersion"":""2024-11-05"",""capabilities"":{""experimental"":{},""prompts"":{""listChanged"":false},""resources"":{""subscribe"":false,""listChanged"":false},""tools"":{""listChanged"":false}},""serverInfo"":{""name"":""JykerContrl"",""version"":""1.12.0""}}}"; + await ws.SendAsync(Encoding.UTF8.GetBytes(b), WebSocketMessageType.Text, true, token); + break; + case "notifications/initialized": + //JLog.Info("notifications/initialized"); + break; + case "tools/list": + //JLog.Info("tools/list"); + //var a = @"{""jsonrpc"":""2.0"",""id"":"+res.Id+@",""result"":{""tools"":[{""name"":""calculator"",""description"":""For mathamatical calculation, always use this tool to calculate the result of a add b. You can use 'math' or 'random' directly, without 'import'."",""inputSchema"":{""properties"":{""a"":{""title"":""A"",""type"":""integer""},""b"":{""title"":""B"",""type"":""integer""}},""required"":[""a"",""b""],""title"":""calculatorArguments"",""type"":""object""}}]}}"; + toolsListBody.id = res.Id; + var body = JsonConvert.SerializeObject(toolsListBody).ToLower(); + await ws.SendAsync(Encoding.UTF8.GetBytes(body), WebSocketMessageType.Text, true, token); + break; + case "tools/call": + var oneTool = ToolsDictionary[res.Params.Name]; + if(oneTool!=null) + { + Type type = oneTool.GetType(); + + // 获取并调用公共方法 + MethodInfo publicMethod = type.GetMethod(res.Params.Name, BindingFlags.Public | BindingFlags.Instance); + if (publicMethod != null) + { + var MethodResult = publicMethod + .Invoke(oneTool, res.Params.Arguments.Select(t=>t.Value).ToArray()); + + var resbody = new ResultBody(); + resbody.id = res.Id; + resbody.result = new ContentRoot { content = new List() }; + var ResultText = new ResultText(); + ResultText.text = JsonConvert.SerializeObject(new ResultContent + { + result = MethodResult + }, Formatting.Indented).Replace("\r",""); + resbody.result.content.Add(ResultText); + var json = JsonConvert.SerializeObject(resbody); + await ws.SendAsync(Encoding.UTF8.GetBytes(json), WebSocketMessageType.Text, true, token); + //JLog.Info(json); + } + } + break; + case "ping": + //JLog.Info("ping"); + var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new ResultBody() { id = res.Id}).ToLower()); + await ws.SendAsync(bytes, WebSocketMessageType.Text, true, token); + break; + default: + break; + } + } + } + + private async Task PipeWebSocketSend(ClientWebSocket ws, CancellationToken token) + { + string line = ""; + var bytes = Encoding.UTF8.GetBytes(line); + await ws.SendAsync(bytes, WebSocketMessageType.Text, true, token); + } + /// + /// 添加工具 + /// + /// + /// + public async Task AddTools() where T :new () + { + var tools = ToolsSerializer.SerializeTool(); + foreach (var tool in tools) + { + T t = new T(); + ToolsDictionary.Add(tool.Name.ToLower(), t); + } + if(toolsListBody.result==null) + { + toolsListBody.result = new ToolRoot { Tools = tools }; + } + else + { + toolsListBody.result.tools.AddRange(tools); + } + + } + } + +} diff --git a/HMIcode/SmallProject/SmallProject/MainWindow.xaml b/HMIcode/SmallProject/SmallProject/MainWindow.xaml new file mode 100644 index 0000000..d6e1bd8 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/MainWindow.xaml @@ -0,0 +1,351 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 末端夹角: + + + + + + + + + 末端长度: + + + + + + 0mm + + + + + 末端受力: + + + + + + 0 + + + + + + + + + + + J1: + + 0 + + J2: + + 0 + + J3: + + 0 + + J4: + + 0 + + J5: + + 0 + + J6: + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + X: + + + 0 + + + + + Y: + + + 0 + + + + + Z: + + + 0 + + + + + Angle_A: + + + 0 + + + + + Angle_B: + + + 0 + + + + + Angle_C: + + + 0 + + + + + + Joint1: + + + 0 + + + + + Joint2: + + + 0 + + + + + Joint3: + + + 0 + + + + + Joint4: + + + 0 + + + + + Joint5: + + + 0 + + + + + Joint6: + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/HMIcode/SmallProject/SmallProject/MainWindow.xaml.cs b/HMIcode/SmallProject/SmallProject/MainWindow.xaml.cs new file mode 100644 index 0000000..ccdc430 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/MainWindow.xaml.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Ports; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using System.Xml.Linq; +using System.Text.RegularExpressions; +using System.Drawing; +using Rubyer; +using System.Collections.ObjectModel; +using System.Windows.Forms; +using SmallProject.WindowController; +using SmallProject.Dialogs; + +namespace SmallProject +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Rubyer.RubyerWindow + { + + JykerConnetContrl JykerConnetContrl; + JykerMoveContrl JykerMoveContrl; + LogContrl LogContrl; + JykerStatusContrl JykerStatusContrl; + JykerKinematicContrl JykerKinematicContrl; + JykerViewContrl JykerViewContrl; + LogLiveContrl LogLiveContrl; + public MainWindow() + { + InitializeComponent(); + //窗体加载 + this.Loaded += MainWindow_Loaded; + //mt_Config 事件监听 + mt_Config.Click += Mt_Config_Click; + mt_MotorConfig.Click += Mt_MotorConfig_Click; + + } + //弹出电机配置页面 + private void Mt_MotorConfig_Click(object sender, RoutedEventArgs e) + { + MotorConfigDialog motorConfigDialog = new MotorConfigDialog(); + motorConfigDialog.ShowDialog(); + } + + //弹出配置页面 + private void Mt_Config_Click(object sender, RoutedEventArgs e) + { + ConfigDialog configDialog = new ConfigDialog(); + configDialog.ShowDialog(); + } + + + + private void MainWindow_Loaded(object sender, RoutedEventArgs e) + { + icon(); + + //ContextMenuStrip + contextMenu(); + + //保证窗体显示在上方。 + wsl = WindowState; + + Rubyer.ThemeManager.SwitchThemeMode(Rubyer.Enums.ThemeMode.Dark); + this.StateChanged += MainWindow_StateChanged; + + //初始化控制器 + JykerConnetContrl = new JykerConnetContrl(this); + LogContrl = new LogContrl(this); + JykerMoveContrl = new JykerMoveContrl(this); + JykerStatusContrl = new JykerStatusContrl(this); + JykerKinematicContrl = new JykerKinematicContrl(this); + JykerViewContrl = new JykerViewContrl(this); + LogLiveContrl = new LogLiveContrl(this); + } + + + #region 托盘右键菜单 + WindowState ws; + WindowState wsl; + NotifyIcon notifyIcon; + private void MainWindow_StateChanged(object? sender, EventArgs e) + { + ws = this.WindowState; + if (ws == WindowState.Minimized) + { + this.Hide(); + } + } + + private void icon() + { + string path = System.IO.Path.GetFullPath(@"Static\icon.ico"); + if (File.Exists(path)) + { + this.notifyIcon = new NotifyIcon(); + this.notifyIcon.BalloonTipText = "Jyker"; //设置程序启动时显示的文本 + this.notifyIcon.Text = "Jyker";//最小化到托盘时,鼠标点击时显示的文本 + System.Drawing.Icon icon = new System.Drawing.Icon(path);//程序图标 + this.notifyIcon.Icon = icon; + this.notifyIcon.Visible = true; + notifyIcon.MouseDoubleClick += NotifyIcon_MouseDoubleClick; + + } + + } + + private void NotifyIcon_MouseDoubleClick(object? sender, System.Windows.Forms.MouseEventArgs e) + { + this.Show(); + WindowState = wsl; + } + private void contextMenu() + { + ContextMenuStrip cms = new ContextMenuStrip(); + + //关联 NotifyIcon 和 ContextMenuStrip + notifyIcon.ContextMenuStrip = cms; + + System.Windows.Forms.ToolStripMenuItem exitMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + exitMenuItem.Text = "退出"; + exitMenuItem.Click += new EventHandler(exitMenuItem_Click); + + System.Windows.Forms.ToolStripMenuItem hideMenumItem = new System.Windows.Forms.ToolStripMenuItem(); + hideMenumItem.Text = "隐藏"; + hideMenumItem.Click += new EventHandler(hideMenumItem_Click); + + System.Windows.Forms.ToolStripMenuItem showMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + showMenuItem.Text = "显示"; + showMenuItem.Click += new EventHandler(showMenuItem_Click); + + cms.Items.Add(exitMenuItem); + cms.Items.Add(hideMenumItem); + cms.Items.Add(showMenuItem); + } + + private void exitMenuItem_Click(object? sender, EventArgs e) + { + notifyIcon.Visible = false; + + System.Windows.Application.Current.Shutdown(); + } + + private void hideMenumItem_Click(object? sender, EventArgs e) + { + this.Hide(); + } + + private void showMenuItem_Click(object? sender, EventArgs e) + { + this.Show(); + this.Activate(); + } + #endregion + } +} diff --git a/HMIcode/SmallProject/SmallProject/Serials/Serial.cs b/HMIcode/SmallProject/SmallProject/Serials/Serial.cs new file mode 100644 index 0000000..ca54b46 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Serials/Serial.cs @@ -0,0 +1,139 @@ +using RJCP.IO.Ports; +using SharpCompress.Compressors.Xz; +using SmallProject.Devices.Arm; +using SmallProject.Logger; +using SmallProject.Serials.Slcan; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace SmallProject.Serials +{ + public class Serial + { + + private Queue DataToSend; + + public void PushDataToQueue(string data) + { + if(!OpenResult) + { + JLog.Info("请先打开连接"); + return; + } + DataToSend.Enqueue(data); + } + + private bool OpenResult; + + //串口工具 + private static SerialPortStream stream; + public bool Open(string name) + { + try + { + if (stream != null && stream.IsOpen) + { + stream.Dispose(); + } + stream = new SerialPortStream(name, 115200, 8, RJCP.IO.Ports.Parity.None, RJCP.IO.Ports.StopBits.One); + stream.ReadTimeout = 1000; + + stream.DataReceived += Stream_DataReceived; + stream.Open(); + OpenResult = true; + DataToSend = new Queue(); + LoopSend(); + //设置波特率 + PushDataToQueue("S8\r"); + //开始 + PushDataToQueue("O\r"); + } + catch (Exception) + { + OpenResult = false; + + } + return OpenResult; + } + + //循环发送数据 + private void LoopSend() + { + Task.Run(async () => + { + while(OpenResult) + { + try + { + for (int i = 0; i < DataToSend.Count; i++) + { + if (DataToSend.TryDequeue(out string? send)) + { + stream.Write(send); + JLog.Info(send); + } + } + } + catch (Exception e) + { + JLog.Error(e); + } + await Task.Delay(50); + } + }); + } + + //数据接收 + private void Stream_DataReceived(object? sender, SerialDataReceivedEventArgs e) + { + var sp = sender as SerialPortStream; + var recieveText = sp.ReadExisting(); + var frames =recieveText.Split('\r'); + + foreach(var frame in frames) + { + if (string.IsNullOrEmpty(frame)) continue; + var aFrame = SlcanParser.ParseSlcanFrame(frame); + if(aFrame!=null) + { + switch(aFrame.Cmd) { + case 0x21: + //电流信息 + var current = BitConverter.ToSingle(aFrame.Data.Take(4).ToArray()); + App.Core.Jyker.RecieveCurrent(aFrame.Id, current); + JLog.Info($"电流 {current}"); + break; + case 0x22: + // 速度信息 + var velocity = BitConverter.ToSingle(aFrame.Data.Take(4).ToArray()); + App.Core.Jyker.RecieveVelocity(aFrame.Id, velocity); + break; + case 0x23: + //角度信息 + var angle = BitConverter.ToSingle(aFrame.Data.Take(4).ToArray()); + var isFinish = BitConverter.ToBoolean(aFrame.Data.Skip(4).Take(1).ToArray()); + App.Core.Jyker.RecievePos(aFrame.Id, angle, isFinish); + break; + + } + } + } + } + + public bool SerialDispose() + { + //关闭CAN 传输 + stream.Write("C/r"); + //关闭串口 + stream.Close(); + return true; + } + + + } +} diff --git a/HMIcode/SmallProject/SmallProject/Serials/Slcan/CanFrame.cs b/HMIcode/SmallProject/SmallProject/Serials/Slcan/CanFrame.cs new file mode 100644 index 0000000..8044066 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Serials/Slcan/CanFrame.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Serials.Slcan +{ + public class CanFrame + { + public int Id { get; set; } = 0; + public int Cmd { get; set; } + public byte[] Data { get; set; } = new byte[0]; + public bool IsExtended { get; set; } + public bool IsRtr { get; set; } + public byte Dlc { get; set; } =0; + + public override string ToString() + { + return $"ID: 0x{Id:X8}, DLC: {Dlc}, Data: {BitConverter.ToString(Data)}"; + } + + public string ToStr() + { + var IdCmd = (Id << 7 | Cmd).ToString("X3"); + var DlcStr = Dlc.ToString("X1"); + var DataStr = string.Concat(Data.Select(t => t.ToString("X2"))); + return $"t{IdCmd}{DlcStr}{DataStr}\r"; + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/Serials/Slcan/SlcanParser.cs b/HMIcode/SmallProject/SmallProject/Serials/Slcan/SlcanParser.cs new file mode 100644 index 0000000..bcf8ea1 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Serials/Slcan/SlcanParser.cs @@ -0,0 +1,111 @@ +using SmallProject.Logger; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Serials.Slcan +{ + public static class SlcanParser + { + public static CanFrame ParseSlcanFrame(string frame) + { + try + { + char cmd = frame[0]; + var canFrame = new CanFrame(); + + switch (cmd) + { + case 't': // 标准帧 + canFrame.IsExtended = false; + canFrame.IsRtr = false; + break; + case 'T': // 扩展帧 + canFrame.IsExtended = true; + canFrame.IsRtr = false; + break; + case 'r': // 远程标准帧 + canFrame.IsExtended = false; + canFrame.IsRtr = true; + break; + case 'R': // 远程扩展帧 + canFrame.IsExtended = true; + canFrame.IsRtr = true; + break; + default: + throw new ArgumentException("Unsupported frame type."); + } + + + // 解析 ID 和 DLC + var IdCmd = frame.Substring(1, 3); + canFrame.Id = Convert.ToInt32(IdCmd, 16) >> 7; + canFrame.Cmd = Convert.ToInt32(IdCmd, 16) & 0x7F; + canFrame.Dlc = byte.Parse(frame.Substring(4, 1)); + + if (canFrame.Dlc>0) + { + canFrame.Data = new byte[canFrame.Dlc]; + for (int i = 0; i < canFrame.Dlc; i++) + { + var oneByte = byte.Parse(frame.Substring(5 + i * 2, 2), System.Globalization.NumberStyles.HexNumber); + canFrame.Data[i] = oneByte; + } + } + else + { + canFrame.Data = Array.Empty(); + } + + return canFrame; + } + catch (Exception e) + { + JLog.Error(e); + return null; + } + + + } + + public static CanFrame ParseSlcanFrame(int Id, int Cmd, float value, bool resACK = true) + { + return ParseSlcanFrame(Id, Cmd, value,0, resACK); + } + + public static CanFrame ParseSlcanFrame(int Id, int Cmd, float value1, float value2, bool resACK = true) + { + CanFrame canFrame = new CanFrame(); + canFrame.Id = Id; + canFrame.Cmd = Cmd; + canFrame.Dlc = 8; + canFrame.Data = new byte[8]; + byte[] bytes1 = BitConverter.GetBytes(value1); + for (int i = 0; i < bytes1.Length; i++) + { + canFrame.Data[i] = bytes1[i]; + } + byte[] bytes2 = BitConverter.GetBytes(value2); + for (int i = 0; i < bytes2.Length; i++) + { + canFrame.Data[i + 4] = bytes2[i]; + } + return canFrame; + } + + public static string ParseSlcanFrameStr(int Id, int Cmd) + { + return ParseSlcanFrame(Id, Cmd, 0, 0).ToStr(); + } + public static string ParseSlcanFrameStr(int Id, int Cmd, float value,bool resACK = true) + { + return ParseSlcanFrame(Id, Cmd, value, resACK).ToStr(); + } + public static string ParseSlcanFrameStr(int Id, int Cmd, float value1, float value2) + { + return ParseSlcanFrame(Id, Cmd, value1, value2).ToStr(); + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/SmallProject.csproj b/HMIcode/SmallProject/SmallProject/SmallProject.csproj new file mode 100644 index 0000000..47eb7c9 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/SmallProject.csproj @@ -0,0 +1,45 @@ + + + + WinExe + net7.0-windows + enable + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/HMIcode/SmallProject/SmallProject/Static/icon.ico b/HMIcode/SmallProject/SmallProject/Static/icon.ico new file mode 100644 index 0000000..aaef48f Binary files /dev/null and b/HMIcode/SmallProject/SmallProject/Static/icon.ico differ diff --git a/HMIcode/SmallProject/SmallProject/Static/icon.jpeg b/HMIcode/SmallProject/SmallProject/Static/icon.jpeg new file mode 100644 index 0000000..556d553 Binary files /dev/null and b/HMIcode/SmallProject/SmallProject/Static/icon.jpeg differ diff --git a/HMIcode/SmallProject/SmallProject/Utils/DrawToBitmap.cs b/HMIcode/SmallProject/SmallProject/Utils/DrawToBitmap.cs new file mode 100644 index 0000000..4e29f0b --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Utils/DrawToBitmap.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Utils +{ + public class DrawToBitmap + { + public static void DrawRectangle(Bitmap b, float X, float Y, float width, float height + , int thickness = 10, string text = "", float fontSize = 50.0f) + { + using (Graphics grapPic = Graphics.FromImage(b)) + { + + using (Pen pen = new Pen(Color.FromArgb(255, Color.Red), thickness)) + { + Brush whiteBrush = new SolidBrush(Color.FromArgb(220, Color.Red)); // 画文字用 + + System.Drawing.Font font = new System.Drawing.Font("微软雅黑", fontSize, System.Drawing.FontStyle.Bold); + grapPic.DrawRectangle(pen, X, Y, width, height); + //定义字体 + grapPic.DrawString(text, font, whiteBrush, (X + width / 2) * 1f, (Y + height / 2) * 1f); + } + + } + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/Utils/PicUtil.cs b/HMIcode/SmallProject/SmallProject/Utils/PicUtil.cs new file mode 100644 index 0000000..582ea8a --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Utils/PicUtil.cs @@ -0,0 +1,1376 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using System.Windows.Media; +using System.Windows.Shapes; +using System.Windows; +using System.Xml.Linq; +using System.Drawing; +using System.Drawing.Imaging; + +namespace SmallProject.Utils +{ + public partial class PicUtil + { + + /// + /// 打开本地图片 + /// + /// 路径 + /// BitmapSource + public static BitmapSource OpenLocalPicture(string path) + { + try + { + BitmapImage bi = new BitmapImage(); + bi.BeginInit(); + bi.CacheOption = BitmapCacheOption.OnLoad; + using (Stream ms = new MemoryStream(File.ReadAllBytes(path))) + { + bi.StreamSource = ms; + bi.EndInit(); + bi.Freeze(); + ms.Flush(); + ms?.Close(); + ms?.Dispose(); + } + + return bi; + } + catch (Exception) + { + return null; + } + } + /// + /// 打开网络图片 + /// + /// 链接URL + /// BitmapSource + public BitmapSource OpenWebPicture(string path) + { + try + { + System.Net.WebRequest request = System.Net.WebRequest.Create(path); + System.Net.WebResponse response = request.GetResponse(); + byte[] array; + using (Stream stream = response.GetResponseStream()) + { + using (MemoryStream ms = new MemoryStream()) + { + Byte[] buffer = new Byte[1024]; + int current = 0; + do + { + ms.Write(buffer, 0, current); + } while ((current = stream.Read(buffer, 0, buffer.Length)) != 0); + array = ms.ToArray(); + } + + } + MemoryStream memoryStream = new MemoryStream(array); + BitmapImage bi = new BitmapImage(); + bi.BeginInit(); + bi.StreamSource = memoryStream; + bi.EndInit(); + return bi; + } + catch (Exception) + { + return null; + } + } + /// + /// 创建图片 + /// + /// 宽度 + /// 高度 + /// DpiX + /// DpiY + /// true为白色背景,false为透明背景 + /// BitmapSource + public static BitmapSource CreatePicture(int width, int height, double dpix, double dpiy, bool is_white_bg = false) + { + int rawStride = (width * 32 + 7) / 8; + BitmapSource bs = BitmapSource.Create(width, height, dpix, dpiy, PixelFormats.Bgra32, null, new byte[rawStride * height], rawStride); + WriteableBitmap WB = new WriteableBitmap(bs); + WB.Lock(); + byte[] buffer = new byte[WB.BackBufferStride * WB.PixelHeight]; + for (int i = 0; i < buffer.Length; i += 4) + { + buffer[i + 3] = (byte)(is_white_bg ? 255 : 0); + buffer[i + 2] = buffer[i + 1] = buffer[i] = 255; + } + WB.WritePixels(new Int32Rect(0, 0, WB.PixelWidth, WB.PixelHeight), buffer, WB.BackBufferStride, 0); + WB.Unlock(); + return WB; + } + /// + /// 图片剪切 去除选区内部分 + /// + /// 源图像 + /// 左边距 + /// 上边距 + /// 宽度 + /// 高度 + /// 填充颜色 + /// BitmapSource + public BitmapSource PicCut(BitmapSource origin, int left, int top, int width, int height, string color) + { + byte a = (byte)HexToDec(color.Substring(1, 2)); + byte r = (byte)HexToDec(color.Substring(3, 2)); + byte g = (byte)HexToDec(color.Substring(5, 2)); + byte b = (byte)HexToDec(color.Substring(7, 2)); + WriteableBitmap WB = new WriteableBitmap(origin); + WB.Lock(); + int stride = WB.Format.BitsPerPixel * width / 8; + byte[] buffer = new byte[stride * height]; + for (int i = 0; i < buffer.Length; i += 4) + { + buffer[i + 3] = a; + buffer[i + 2] = r; + buffer[i + 1] = g; + buffer[i] = b; + } + WB.WritePixels(new Int32Rect(left, top, width, height), buffer, stride, 0); + WB.AddDirtyRect(new Int32Rect(left, top, width, height)); + WB.Unlock(); + return WB; + } + /// + /// 图片裁剪 保留选区内部分 + /// + /// 源图像 + /// 左边距 + /// 上边距 + /// 宽度 + /// 高度 + /// BitmapSource + public static BitmapSource PicCrop(BitmapSource origin, int left, int top, int width, int height) + { + if (left < 0) left = 0; + if (top < 0) top = 0; + if (left > origin.PixelWidth || left + width > origin.PixelWidth || top > origin.PixelHeight || top + height > origin.PixelHeight) + { + var pic = CreatePicture(1, 1, 0, 0, false); + return pic; + } + BitmapSource bs = new CroppedBitmap(origin, new Int32Rect(left, top, width, height)); + return bs; + } + + /// + /// 更改图像尺寸 + /// + /// 源图像 + /// 目标宽度 + /// 目标高度 + /// BitmapSource + public static BitmapSource PicResize(BitmapSource origin, int width, int height) + { + TransformedBitmap tb = new TransformedBitmap(); + tb.BeginInit(); + tb.Source = origin; + ScaleTransform st = new ScaleTransform((double)width / origin.PixelWidth, (double)height / origin.PixelHeight); + tb.Transform = st; + tb.EndInit(); + return tb; + } + /// + /// 图像旋转(只能向左或右旋转90度) + /// + /// 源图像 + /// 角度(只能90或-90) + /// BitmapSource + public BitmapSource PicRotate(BitmapSource origin, double angle) + { + TransformedBitmap tb = new TransformedBitmap(); + tb.BeginInit(); + tb.Source = origin; + RotateTransform rt = new RotateTransform(angle); + tb.Transform = rt; + tb.EndInit(); + return tb; + } + /// + /// 图像镜像 + /// + /// 源图像 + /// 模式(水平:Horizontal 垂直:Vertical) + /// BitmapSource + public BitmapSource PicMirror(BitmapSource origin, string mode) + { + WriteableBitmap WB = new WriteableBitmap(origin); + WB.Lock(); + byte[] buffer = new byte[WB.BackBufferStride * WB.PixelHeight]; + origin.CopyPixels(new Int32Rect(0, 0, WB.PixelWidth, WB.PixelHeight), buffer, WB.BackBufferStride, 0); + int p1, p2; + byte temp; + if (mode == "Horizontal") + { + for (int j = 0; j < WB.PixelHeight; j++) + { + for (int i = 0; i < (WB.PixelWidth + 1) / 2; i++) + { + p1 = (WB.PixelWidth * j + i) * 4; + p2 = (WB.PixelWidth * j + WB.PixelWidth - i - 1) * 4; + for (int k = 0; k < 4; k++) + { + temp = buffer[p1 + k]; + buffer[p1 + k] = buffer[p2 + k]; + buffer[p2 + k] = temp; + } + } + } + } + else if (mode == "Vertical") + { + for (int i = 0; i < WB.PixelWidth; i++) + { + for (int j = 0; j < (WB.PixelHeight + 1) / 2; j++) + { + p1 = (WB.PixelWidth * j + i) * 4; + p2 = (WB.PixelWidth * j + WB.PixelWidth - i - 1) * 4; + for (int k = 0; k < 4; k++) + { + temp = buffer[p1 + k]; + buffer[p1 + k] = buffer[p2 + k]; + buffer[p2 + k] = temp; + } + } + } + } + WB.WritePixels(new Int32Rect(0, 0, WB.PixelWidth, WB.PixelHeight), buffer, WB.BackBufferStride, 0); + WB.Unlock(); + return WB; + } + /// + /// 图像反色 + /// + /// 源图像 + /// BitmapSource + public BitmapSource PicColorReverse(BitmapSource origin) + { + WriteableBitmap WB = new WriteableBitmap(origin); + WB.Lock(); + byte[] buffer = new byte[WB.BackBufferStride * WB.PixelHeight]; + origin.CopyPixels(new Int32Rect(0, 0, WB.PixelWidth, WB.PixelHeight), buffer, WB.BackBufferStride, 0); + for (int i = 0; i < buffer.Length; i += 4) + { + buffer[i] = (byte)(255 - buffer[i]); + buffer[i + 1] = (byte)(255 - buffer[i + 1]); + buffer[i + 2] = (byte)(255 - buffer[i + 2]); + } + WB.WritePixels(new Int32Rect(0, 0, WB.PixelWidth, WB.PixelHeight), buffer, WB.BackBufferStride, 0); + WB.Unlock(); + return WB; + } + /// + /// 灰度图 + /// + /// 源图像 + /// 灰度图精度 + /// 每个灰度级像素的个数 + /// BitmapSource + public BitmapSource PicToGray(BitmapSource origin, int coe, double[] p = null) + { + UInt32[,] coes = + { + {1,2,1}, //2 + {2,5,1}, //3 + {4,10,2}, //4 + {9,19,4}, //5 + {19,37,8}, //6 + {38,75,15}, //7 + {76,150,30}, //8 + {153,300,59}, //9 + {306,601,117}, //10 + {612,1202,234}, //11 + {1224,2405,467}, //12 + {2449,4809,934}, //13 + {4898,9618,1868}, //14 + {9797,19235,3736}, //15 + {19595,38469,7472}, //16 + {39190,76939,14943}, //17 + {78381,153878,29885}, //18 + {156762,307757,59769}, //19 + {313524,615514,119538}, //20 + }; + WriteableBitmap WB = new WriteableBitmap(origin); + WB.Lock(); + byte[] buffer = new byte[WB.BackBufferStride * WB.PixelHeight]; + origin.CopyPixels(new Int32Rect(0, 0, WB.PixelWidth, WB.PixelHeight), buffer, WB.BackBufferStride, 0); + if (p == null) + { + for (int i = 0; i < buffer.Length; i += 4) + { + buffer[i] = (byte)((buffer[i + 2] * coes[coe - 2, 0] + buffer[i + 1] * coes[coe - 2, 1] + buffer[i] * coes[coe - 2, 2]) >> coe); + buffer[i + 2] = buffer[i + 1] = buffer[i]; + } + } + else + { + for (int i = 0; i < buffer.Length; i += 4) + { + buffer[i] = (byte)((buffer[i + 2] * coes[coe - 2, 0] + buffer[i + 1] * coes[coe - 2, 1] + buffer[i] * coes[coe - 2, 2]) >> coe); + buffer[i + 2] = buffer[i + 1] = buffer[i]; + p[buffer[i]]++; + } + } + WB.WritePixels(new Int32Rect(0, 0, WB.PixelWidth, WB.PixelHeight), buffer, WB.BackBufferStride, 0); + WB.Unlock(); + return WB; + } + /// + /// 二值图(最大类间方差阈值选择法) + /// + /// 源图像 + /// 灰度图精度 + /// 阈值 + /// BitmapSource + public BitmapSource PicToBinary(BitmapSource origin, int coe, out int threshold) + { + double[] p = new double[256]; + origin = PicToGray(origin, coe, p); + WriteableBitmap WB = new WriteableBitmap(origin); + WB.Lock(); + for (int i = 0; i < 256; i++) + p[i] = p[i] / (WB.PixelWidth * WB.PixelHeight); + double variance = -1; + threshold = 0; + for (int k = 0; k < 256; k++) + { + double w0 = 0, u0 = 0; + for (int i = 0; i < k - 1; i++) + { + w0 += p[i]; + u0 += i * p[i]; + } + double w1 = 1 - w0, u1 = 0; + for (int i = k; i < 256; i++) + u1 += i * p[i]; + u0 /= w0; + u1 /= w1; + double v = w0 * w1 * (u0 - u1) * (u0 - u1); + if (v > variance) + { + variance = v; + threshold = k; + } + } + byte[] buffer = new byte[WB.BackBufferStride * WB.PixelHeight]; + origin.CopyPixels(new Int32Rect(0, 0, WB.PixelWidth, WB.PixelHeight), buffer, WB.BackBufferStride, 0); + for (int i = 0; i < buffer.Length; i += 4) + buffer[i + 2] = buffer[i + 1] = buffer[i] = (byte)(buffer[i] < threshold ? 0 : 255); + WB.WritePixels(new Int32Rect(0, 0, WB.PixelWidth, WB.PixelHeight), buffer, WB.BackBufferStride, 0); + WB.Unlock(); + return WB; + } + /// + /// 图片合成 + /// + /// 源图像 + /// 第二张图片 + /// 左边距 + /// 上边距 + /// 宽度 + /// 高度 + /// 填充颜色 + /// BitmapSource + public static BitmapSource PicSynthesis(BitmapSource origin, BitmapSource sec, int left, int top, int width, int height, string color) + { + //UInt32 Color = UInt32.Parse(color[1..], System.Globalization.NumberStyles.HexNumber); + int W = (width + left > origin.PixelWidth ? (width + left) : origin.PixelWidth) - (left < 0 ? left : 0); + int H = (height + top > origin.PixelHeight ? (height + top) : origin.PixelHeight) - (top < 0 ? top : 0); + byte a = (byte)HexToDec(color.Substring(1, 2)); + byte r = (byte)HexToDec(color.Substring(3, 2)); + byte g = (byte)HexToDec(color.Substring(5, 2)); + byte b = (byte)HexToDec(color.Substring(7, 2)); + int stride = origin.Format.BitsPerPixel * W / 8, p1, p2; + byte[] Bottom = new byte[stride * H]; + WriteableBitmap WB = new WriteableBitmap(BitmapSource.Create(W, H, origin.DpiX, origin.DpiY, PixelFormats.Bgra32, null, Bottom, stride)); + WB.Lock(); + for (int i = 0; i < Bottom.Length; i += 4) + { + Bottom[i + 3] = a; + Bottom[i + 2] = r; + Bottom[i + 1] = g; + Bottom[i] = b; + } + byte[] Top = new byte[origin.Format.BitsPerPixel * origin.PixelWidth / 8 * origin.PixelHeight]; + origin.CopyPixels(new Int32Rect(0, 0, origin.PixelWidth, origin.PixelHeight), Top, origin.Format.BitsPerPixel * origin.PixelWidth / 8, 0); + for (int j = 0, x = left < 0 ? left : 0, y = top < 0 ? top : 0; j < origin.PixelHeight; j++) + { + for (int i = 0; i < origin.PixelWidth; i++) + { + p1 = (WB.PixelWidth * (j - y) + i - x) * 4; + p2 = (origin.PixelWidth * j + i) * 4; + Bottom[p1 + 2] = (byte)((Top[p2 + 2] * Top[p2 + 3] + Bottom[p1 + 2] * Bottom[p1 + 3]) / 255 - Bottom[p1 + 2] * Bottom[p1 + 3] * Top[p2 + 3] / 65025); + Bottom[p1 + 1] = (byte)((Top[p2 + 1] * Top[p2 + 3] + Bottom[p1 + 1] * Bottom[p1 + 3]) / 255 - Bottom[p1 + 1] * Bottom[p1 + 3] * Top[p2 + 3] / 65025); + Bottom[p1] = (byte)((Top[p2] * Top[p2 + 3] + Bottom[p1] * Bottom[p1 + 3]) / 255 - Bottom[p1] * Bottom[p1 + 3] * Top[p2 + 3] / 65025); + Bottom[p1 + 3] = Bottom[p1 + 3] > Top[p2 + 3] ? Bottom[p1 + 3] : Top[p2 + 3]; + } + } + sec = PicResize(sec, width, height); + Top = new byte[sec.Format.BitsPerPixel * sec.PixelWidth / 8 * sec.PixelHeight]; + sec.CopyPixels(new Int32Rect(0, 0, sec.PixelWidth, sec.PixelHeight), Top, sec.Format.BitsPerPixel * sec.PixelWidth / 8, 0); + for (int j = 0, x = left < 0 ? 0 : left, y = top < 0 ? 0 : top; j < sec.PixelHeight; j++) + { + for (int i = 0; i < sec.PixelWidth; i++) + { + p1 = (WB.PixelWidth * (j + y) + i + x) * 4; + p2 = (sec.PixelWidth * j + i) * 4; + Bottom[p1 + 2] = (byte)((Top[p2 + 2] * Top[p2 + 3] + Bottom[p1 + 2] * Bottom[p1 + 3]) / 255 - Bottom[p1 + 2] * Bottom[p1 + 3] * Top[p2 + 3] / 65025); + Bottom[p1 + 1] = (byte)((Top[p2 + 1] * Top[p2 + 3] + Bottom[p1 + 1] * Bottom[p1 + 3]) / 255 - Bottom[p1 + 1] * Bottom[p1 + 3] * Top[p2 + 3] / 65025); + Bottom[p1] = (byte)((Top[p2] * Top[p2 + 3] + Bottom[p1] * Bottom[p1 + 3]) / 255 - Bottom[p1] * Bottom[p1 + 3] * Top[p2 + 3] / 65025); + Bottom[p1 + 3] = Bottom[p1 + 3] > Top[p2 + 3] ? Bottom[p1 + 3] : Top[p2 + 3]; + } + } + WB.WritePixels(new Int32Rect(0, 0, W, H), Bottom, stride, 0); + WB.Unlock(); + return WB; + } + /// + /// 对比度/亮度 + /// + /// 源图像数据 + /// 亮度 + /// 对比度 + /// 处理后的数据 + public byte[] PicBAC(byte[] buffer, double brightness, double contrast) + { + double B = brightness / 127; + double K = Math.Tan((45 + 44 * contrast / 127) / 180 * Math.PI); + double CR, CG, CB; + for (int i = 0; i < buffer.Length; i += 4) + { + CR = (buffer[i + 2] - 127.5 * (1 - B)) * K + 127.5 * (1 + B); + CG = (buffer[i + 1] - 127.5 * (1 - B)) * K + 127.5 * (1 + B); + CB = (buffer[i] - 127.5 * (1 - B)) * K + 127.5 * (1 + B); + buffer[i + 2] = (byte)(CR > 255 ? 255 : (CR < 0 ? 0 : CR)); + buffer[i + 1] = (byte)(CG > 255 ? 255 : (CG < 0 ? 0 : CG)); + buffer[i] = (byte)(CB > 255 ? 255 : (CB < 0 ? 0 : CB)); + } + return buffer; + } + /// + /// 色阶 + /// + /// 源图像数据 + /// 模式:RGB,R,G,B + /// 黑色输入值 + /// 灰色输入值 + /// 白色输入值 + /// 黑色输出值 + /// 白色输出值 + /// 处理后的数据 + public byte[] PicColorScale(byte[] buffer, int mode, double iblack, double igray, double iwhite, double oblack, double owhite) + { + for (int i = 0; i < buffer.Length; i += 4) + { + if (mode == 0 || mode == 1) + { + if (buffer[i + 2] < iblack) + buffer[i + 2] = (byte)oblack; + else if (buffer[i + 2] > iwhite) + buffer[i + 2] = (byte)owhite; + else + buffer[i + 2] = (byte)(oblack + (owhite - oblack) * Math.Pow((buffer[i + 2] - iblack) / (iwhite - iblack), 1 / igray)); + } + if (mode == 0 || mode == 2) + { + if (buffer[i + 1] < iblack) + buffer[i + 1] = (byte)oblack; + else if (buffer[i + 1] > iwhite) + buffer[i + 1] = (byte)owhite; + else + buffer[i + 1] = (byte)(oblack + (owhite - oblack) * Math.Pow((buffer[i + 1] - iblack) / (iwhite - iblack), 1 / igray)); + } + if (mode == 0 || mode == 3) + { + if (buffer[i] < iblack) + buffer[i] = (byte)oblack; + else if (buffer[i] > iwhite) + buffer[i] = (byte)owhite; + else + buffer[i] = (byte)(oblack + (owhite - oblack) * Math.Pow((buffer[i] - iblack) / (iwhite - iblack), 1 / igray)); + } + } + return buffer; + } + /// + /// 色相/饱和度 + /// + /// 源图像数据 + /// 模式(0:HSV,1:HSL) + /// 色相 + /// 饱和度 + /// 明度/亮度 + /// 处理后的数据 + public byte[] PicHS(byte[] buffer, int mode, double hue, double saturation, double value) + { + double[] hs; + int[] rgb; + for (int i = 0; i < buffer.Length; i += 4) + { + hs = mode == 0 ? RGB_HSV(buffer[i + 2], buffer[i + 1], buffer[i]) : RGB_HSL(buffer[i + 2], buffer[i + 1], buffer[i]); + hs[0] += hue; + hs[1] *= (1 + saturation); + hs[2] += value; + hs[0] = (hs[0] < 0) ? (hs[0] + 360) : (hs[0] >= 360 ? (hs[0] % 360) : hs[0]); + hs[1] = hs[1] < 0 ? 0 : (hs[1] > 1 ? 1 : hs[1]); + hs[2] = hs[2] < 0 ? 0 : (hs[2] > 1 ? 1 : hs[2]); + rgb = mode == 0 ? HSV_RGB(hs[0], hs[1], hs[2]) : HSL_RGB(hs[0], hs[1], hs[2]); + buffer[i + 2] = (byte)rgb[0]; + buffer[i + 1] = (byte)rgb[1]; + buffer[i] = (byte)rgb[2]; + } + return buffer; + } + /// + /// 色调分离 + /// + /// 源图像数据 + /// 色调数 + /// 处理后的数据 + public byte[] PicTS(byte[] buffer, int num) + { + int[] value = new int[num]; + double len = 255.0 / (num - 1); + for (int i = 0; i < num; i++) + value[i] = (int)(i * len + 0.5); + for (int i = 0; i < buffer.Length; i += 4) + { + buffer[i + 2] = (byte)value[(int)(buffer[i + 2] / len + 0.5)]; + buffer[i + 1] = (byte)value[(int)(buffer[i + 1] / len + 0.5)]; + buffer[i] = (byte)value[(int)(buffer[i] / len + 0.5)]; + } + return buffer; + } + /// + /// 曲线 + /// + /// 源图像 + /// 各通道曲线数据 + /// BitmapSource + public BitmapSource PicCurve(BitmapSource origin, int[][] data) + { + WriteableBitmap WB = new WriteableBitmap(origin); + WB.Lock(); + byte[] buffer = new byte[WB.BackBufferStride * origin.PixelHeight]; + origin.CopyPixels(new Int32Rect(0, 0, origin.PixelWidth, origin.PixelHeight), buffer, WB.BackBufferStride, 0); + for (int i = 0; i < buffer.Length; i += 4) + { + buffer[i + 2] = (byte)data[0][buffer[i + 2]]; + buffer[i + 1] = (byte)data[1][buffer[i + 1]]; + buffer[i] = (byte)data[2][buffer[i]]; + } + WB.WritePixels(new Int32Rect(0, 0, WB.PixelWidth, WB.PixelHeight), buffer, WB.BackBufferStride, 0); + WB.Unlock(); + return WB; + } + /// + /// 曲线 + /// + /// 源图像数据 + /// 各通道曲线数据 + /// 处理后的数据 + public byte[] PicCurve(byte[] buffer, int[][] data) + { + for (int i = 0; i < buffer.Length; i += 4) + { + buffer[i + 2] = (byte)data[0][buffer[i + 2]]; + buffer[i + 1] = (byte)data[1][buffer[i + 1]]; + buffer[i] = (byte)data[2][buffer[i]]; + } + return buffer; + } + /// + /// 锐化(属于高通滤波) + /// + /// 源图像 + /// 调节系数(范围[0,1]) + /// 宽度 + /// 高度 + /// 处理后的数据 + public byte[] PicSharpen(byte[] buffer, double coe, int width, int height) + { + byte[] buffer2 = buffer.Clone() as byte[]; + int[,] op = new int[,] { { -1, -1, -1 }, { -1, 8, -1 }, { -1, -1, -1 } }; + for (int y = 1; y < height - 1; y++) + { + for (int x = 1; x < width - 1; x++) + { + double[] sum = new double[3] { 0, 0, 0 }; + int p; + for (int i = 0; i < 3; i++) + { + for (int j = 0; j < 3; j++) + { + p = ((y + j - 1) * width + x + i - 1) * 4; + sum[0] += buffer2[p + 2] * op[i, j] * coe; + sum[1] += buffer2[p + 1] * op[i, j] * coe; + sum[2] += buffer2[p] * op[i, j] * coe; + } + } + p = (y * width + x) * 4; + sum[0] += buffer[p + 2]; + sum[1] += buffer[p + 1]; + sum[2] += buffer[p]; + buffer[p + 2] = (byte)(sum[0] < 0 ? 0 : (sum[0] > 255 ? 255 : sum[0])); + buffer[p + 1] = (byte)(sum[1] < 0 ? 0 : (sum[1] > 255 ? 255 : sum[1])); + buffer[p] = (byte)(sum[2] < 0 ? 0 : (sum[2] > 255 ? 255 : sum[2])); + } + } + return buffer; + } + /// + /// 均值滤波 + /// + /// 源图像数据 + /// 半径(目标像素周围的像素圈数) + /// 宽度 + /// 高度 + /// 一行所占字节数 + /// 处理后的数据 + public byte[] PicMeanFilter(byte[] buffer, int radius, int width, int height, int stride) + { + byte[] buffer2 = buffer.Clone() as byte[]; + int p, s0, s1, s2; + for (int y = 0; y < height; y++) + { + int[] sum = new int[3] { 0, 0, 0 }; + for (int x = 0; x < width; x++) + { + if (x == 0) + { + for (int i = -radius; i <= radius; i++) + { + int X = i < 0 ? -i : i; + p = y * stride + X * 4; + sum[0] += buffer2[p + 2]; + sum[1] += buffer2[p + 1]; + sum[2] += buffer2[p]; + } + } + else + { + int X = x - radius - 1; + X = X < 0 ? -X : X; + p = y * stride + X * 4; + sum[0] -= buffer2[p + 2]; + sum[1] -= buffer2[p + 1]; + sum[2] -= buffer2[p]; + X = x + radius; + X = X >= width ? (width - 1 - X % (width - 1)) : X; + p = y * stride + X * 4; + sum[0] += buffer2[p + 2]; + sum[1] += buffer2[p + 1]; + sum[2] += buffer2[p]; + } + s0 = sum[0] / (2 * radius + 1); + s1 = sum[1] / (2 * radius + 1); + s2 = sum[2] / (2 * radius + 1); + p = y * stride + x * 4; + buffer[p + 2] = (byte)(s0 < 0 ? 0 : (s0 > 255 ? 255 : s0)); + buffer[p + 1] = (byte)(s1 < 0 ? 0 : (s1 > 255 ? 255 : s1)); + buffer[p] = (byte)(s2 < 0 ? 0 : (s2 > 255 ? 255 : s2)); + } + } + for (int x = 0; x < width; x++) + { + int[] sum = new int[3] { 0, 0, 0 }; + for (int y = 0; y < height; y++) + { + if (y == 0) + { + for (int j = -radius; j <= radius; j++) + { + int Y = j < 0 ? -j : j; + p = Y * stride + x * 4; + sum[0] += buffer[p + 2]; + sum[1] += buffer[p + 1]; + sum[2] += buffer[p]; + } + } + else + { + int Y = y - radius - 1; + Y = Y < 0 ? -Y : Y; + p = Y * stride + x * 4; + sum[0] -= buffer[p + 2]; + sum[1] -= buffer[p + 1]; + sum[2] -= buffer[p]; + Y = y + radius; + Y = Y >= height ? (height - 1 - Y % (height - 1)) : Y; + p = Y * stride + x * 4; + sum[0] += buffer[p + 2]; + sum[1] += buffer[p + 1]; + sum[2] += buffer[p]; + } + s0 = sum[0] / (2 * radius + 1); + s1 = sum[1] / (2 * radius + 1); + s2 = sum[2] / (2 * radius + 1); + p = y * stride + x * 4; + buffer2[p + 2] = (byte)(s0 < 0 ? 0 : (s0 > 255 ? 255 : s0)); + buffer2[p + 1] = (byte)(s1 < 0 ? 0 : (s1 > 255 ? 255 : s1)); + buffer2[p] = (byte)(s2 < 0 ? 0 : (s2 > 255 ? 255 : s2)); + } + } + return buffer2; + } + /// + /// 白平衡 + /// + /// 源图像数据 + /// 宽度 + /// 高度 + /// 处理后的数据 + public byte[] PicWhiteBalance(byte[] buffer, int width, int height) + { + double r_ave = 0, g_ave = 0, b_ave = 0; + for (int i = 0; i < width * height; i++) + { + int p = i * 4; + r_ave = (r_ave * i + buffer[p + 2]) / (i + 1); + g_ave = (g_ave * i + buffer[p + 1]) / (i + 1); + b_ave = (b_ave * i + buffer[p]) / (i + 1); + } + double gray = (r_ave + g_ave + b_ave) / 3; + double kr = gray / r_ave, kg = gray / g_ave, kb = gray / b_ave; + for (int i = 0; i < buffer.Length; i += 4) + { + double r = buffer[i + 2] * kr; + double g = buffer[i + 1] * kg; + double b = buffer[i] * kb; + buffer[i + 2] = (byte)(r > 255 ? 255 : r); + buffer[i + 1] = (byte)(g > 255 ? 255 : g); + buffer[i] = (byte)(b > 255 ? 255 : b); + } + return buffer; + } + /// + /// 像素化 + /// + /// 源图像数据 + /// 像素块大小 + /// 宽度 + /// 高度 + /// 一行所占字节数 + /// 处理后的数据 + public byte[] PicPixelated(byte[] buffer, int size, int width, int height, int stride) + { + for (int y = 0; y < height;) + { + int ye = (y + size) > height ? height : (y + size); + for (int x = 0; x < width;) + { + int xe = (x + size) > width ? width : (x + size); + int[] sum = new int[3] { 0, 0, 0 }; + for (int j = y; j < ye; j++) + { + for (int i = x; i < xe; i++) + { + int p = j * stride + i * 4; + sum[0] += buffer[p + 2]; + sum[1] += buffer[p + 1]; + sum[2] += buffer[p]; + } + } + sum[0] /= ((ye - y) * (xe - x)); + sum[1] /= ((ye - y) * (xe - x)); + sum[2] /= ((ye - y) * (xe - x)); + for (int j = y; j < ye; j++) + { + for (int i = x; i < xe; i++) + { + int p = j * stride + i * 4; + buffer[p + 2] = (byte)sum[0]; + buffer[p + 1] = (byte)sum[1]; + buffer[p] = (byte)sum[2]; + } + } + x = xe; + } + y = ye; + } + return buffer; + } + /// + /// 通道混合器 + /// + /// 源图像数据 + /// 红色通道数据 + /// 绿色通道数据 + /// 蓝色通道数据 + /// 处理后的数据 + public byte[] PicChannelMixer(byte[] buffer, double[] red, double[] green, double[] blue) + { + for (int i = 0; i < buffer.Length; i += 4) + { + double r = buffer[i + 2] * red[0] + buffer[i + 1] * red[1] + buffer[i] * red[2]; + double g = buffer[i + 2] * green[0] + buffer[i + 1] * green[1] + buffer[i] * green[2]; + double b = buffer[i + 2] * blue[0] + buffer[i + 1] * blue[1] + buffer[i] * blue[2]; + buffer[i + 2] = (byte)(r < 0 ? 0 : (r > 255 ? 255 : r)); + buffer[i + 1] = (byte)(g < 0 ? 0 : (g > 255 ? 255 : g)); + buffer[i] = (byte)(b < 0 ? 0 : (b > 255 ? 255 : b)); + } + return buffer; + } + /// + /// 提取分量 + /// + /// 源图像数据 + /// 提取的通道 + /// 处理后的数据 + public byte[] PicExtractChannel(byte[] buffer, string channel) + { + if (channel == "red") + { + for (int i = 0; i < buffer.Length; i += 4) + { + buffer[i + 1] = buffer[i + 2]; + buffer[i] = buffer[i + 2]; + } + } + else if (channel == "green") + { + for (int i = 0; i < buffer.Length; i += 4) + { + buffer[i + 2] = buffer[i + 1]; + buffer[i] = buffer[i + 1]; + } + } + else if (channel == "blue") + { + for (int i = 0; i < buffer.Length; i += 4) + { + buffer[i + 1] = buffer[i]; + buffer[i + 2] = buffer[i]; + } + } + return buffer; + } + + /// + /// 获取各通道0到255各个像素的个数 + /// + /// 源图像 + /// 红色通道 + /// 绿色通道 + /// 蓝色通道 + /// 三通道个数的最大值 + public static int GetPixelCount(BitmapSource bs, int[] red, int[] green, int[] blue) + { + int max = 0; + int stride = bs.Format.BitsPerPixel * bs.PixelWidth / 8; + byte[] buffer = new byte[stride * bs.PixelHeight]; + bs.CopyPixels(new Int32Rect(0, 0, bs.PixelWidth, bs.PixelHeight), buffer, stride, 0); + for (int i = 0; i < buffer.Length; i += 4) + { + red[buffer[i + 2]]++; + green[buffer[i + 1]]++; + blue[buffer[i]]++; + max = red[buffer[i + 2]] > max ? red[buffer[i + 2]] : max; + max = green[buffer[i + 2]] > max ? green[buffer[i + 2]] : max; + max = blue[buffer[i + 2]] > max ? blue[buffer[i + 2]] : max; + } + return max; + } + /// + /// 专用于[0,255]转两位十六进制的函数 + /// + /// 十进制 + /// string + public string DecToHex(int dec) + { + char[] num = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + string hex = ""; + while (dec > 0) + { + hex = num[dec % 16] + hex; + dec /= 16; + } + return hex == "" ? "00" : (hex.Length == 1 ? "0" + hex : hex); + } + /// + /// 专用于两位十六进制转十进制的函数 + /// + /// 十六进制 + /// int + private static int HexToDec(string hex) + { + int sum = 0; + for (int i = 1; i >= 0; i--) + { + if (hex[i] == 'A') + sum += 10 * (int)Math.Pow(16, 1 - i); + else if (hex[i] == 'B') + sum += 11 * (int)Math.Pow(16, 1 - i); + else if (hex[i] == 'C') + sum += 12 * (int)Math.Pow(16, 1 - i); + else if (hex[i] == 'D') + sum += 13 * (int)Math.Pow(16, 1 - i); + else if (hex[i] == 'E') + sum += 14 * (int)Math.Pow(16, 1 - i); + else if (hex[i] == 'F') + sum += 15 * (int)Math.Pow(16, 1 - i); + else + sum += int.Parse(hex[i].ToString()) * (int)Math.Pow(16, 1 - i); + } + return sum; + } + /// + /// 基于特征向量的容差 + /// + /// 像素1的特征向量,长度为4 + /// 像素2的特征向量,长度为4 + /// double + private double PixelTolerance(double[] p1, double[] p2) + { + double temp = p1[0] * p2[0] + p1[1] * p2[1] + p1[2] * p2[2] + p1[3] * p2[3]; + if (temp == 0) + return Math.Abs(p1[3] - p2[3]); + double angle = Math.Sqrt(p1[0] * p1[0] + p1[1] * p1[1] + p1[2] * p1[2] + p1[3] * p1[3]) * Math.Sqrt(p2[0] * p2[0] + p2[1] * p2[1] + p2[2] * p2[2] + p2[3] * p2[3]); + angle = Math.Acos(temp / angle) * 255 * 2 / Math.PI; + return Math.Pow(2, Math.Abs((p1[3] > p2[3] ? p1[3] : p2[3]) / (p1[3] < p2[3] ? p1[3] : p2[3]) - 1)) * angle / 255; + } + /// + /// RGB转HSV + /// + /// 红色通道 + /// 绿色通道 + /// 蓝色通道 + /// HSV + private double[] RGB_HSV(int r, int g, int b) + { + double max = (r > g ? r : g) > b ? (r > g ? r : g) : b; + double min = (r < g ? r : g) < b ? (r < g ? r : g) : b; + double h = 0; + if (max != min) + { + if (max == r) + h = 60 * (g - b) / (max - min) + (g >= b ? 0 : 360); + else if (max == g) + h = 60 * (b - r) / (max - min) + 120; + else if (max == b) + h = 60 * (r - g) / (max - min) + 240; + } + return new double[] { h, (max == 0 ? 0 : (1 - (double)min / max)), max / 255.0 }; + } + /// + /// HSV转RGB + /// + /// 色相 + /// 饱和度 + /// 明度 + /// RGB + private int[] HSV_RGB(double h, double s, double v) + { + double c = v * s; + double x = c * (1 - Math.Abs((h / 60) % 2 - 1)); + double m = v - c; + if (0 <= h && h < 60) + return new int[] { (int)((c + m) * 255 + 0.5), (int)((x + m) * 255 + 0.5), (int)(m * 255 + 0.5) }; + else if (60 <= h && h < 120) + return new int[] { (int)((x + m) * 255 + 0.5), (int)((c + m) * 255 + 0.5), (int)(m * 255 + 0.5) }; + else if (120 <= h && h < 180) + return new int[] { (int)(m * 255 + 0.5), (int)((c + m) * 255 + 0.5), (int)((x + m) * 255 + 0.5) }; + else if (180 <= h && h < 240) + return new int[] { (int)(m * 255 + 0.5), (int)((x + m) * 255 + 0.5), (int)((c + m) * 255 + 0.5) }; + else if (240 <= h && h < 300) + return new int[] { (int)((x + m) * 255 + 0.5), (int)(m * 255 + 0.5), (int)((c + m) * 255 + 0.5) }; + else if (300 <= h && h < 360) + return new int[] { (int)((c + m) * 255 + 0.5), (int)(m * 255 + 0.5), (int)((x + m) * 255 + 0.5) }; + return new int[] { 0, 0, 0 }; + } + /// + /// RGB转HSL + /// + /// 红色通道 + /// 绿色通道 + /// 蓝色通道 + /// HSL + private double[] RGB_HSL(int r, int g, int b) + { + double max = (r > g ? r : g) > b ? (r > g ? r : g) : b; + double min = (r < g ? r : g) < b ? (r < g ? r : g) : b; + double h = 0, l = (max + min) / 510; + if (max != min) + { + if (max == r) + h = 60 * (g - b) / (max - min) + (g >= b ? 0 : 360); + else if (max == g) + h = 60 * (b - r) / (max - min) + 120; + else if (max == b) + h = 60 * (r - g) / (max - min) + 240; + } + double s = l == 0 ? 0 : 0 < l && l <= 0.5 ? (max - min) / l * 0.5 : (max - min) / (2 - 2 * l); + return new double[] { h, s / 255, l }; + } + /// + /// HSL转RGB + /// + /// 色相 + /// 饱和度 + /// 亮度 + /// RGB + private int[] HSL_RGB(double h, double s, double l) + { + double q = l < 0.5 ? (l * (1 + s)) : (l + s - (l * s)); + double p = 2 * l - q; + double hk = h / 360.0; + double[] t = new double[] { hk + 0.33333333, hk, hk - 0.33333333 }; + for (int i = 0; i < 3; i++) + { + t[i] = t[i] < 0 ? (t[i] + 1) : (t[i] > 1 ? (t[i] - 1) : t[i]); + if (t[i] < 0.16666666) + t[i] = p + ((q - p) * 6 * t[i]); + else if (t[i] >= 0.16666666 && t[i] < 0.5) + t[i] = q; + else if (t[i] >= 0.5 && t[i] < 0.66666666) + t[i] = p + ((q - p) * 6 * (0.66666666 - t[i])); + else + t[i] = p; + } + return new int[] { (int)(t[0] * 255 + 0.5), (int)(t[1] * 255 + 0.5), (int)(t[2] * 255 + 0.5) }; + } + + /// + /// 获取随机id + /// + /// + public static string GenerateRandomName() + { + return DateTime.Now.ToString("yyyyMMddHHmmssfff") + Guid.NewGuid().ToString(); + } + + /// + /// base64 转换成BitmapSource + /// + /// + /// + public static BitmapSource Base64StringToBitmapSource(string inputStr) + { + try + { + byte[] arr = Convert.FromBase64String(inputStr); + MemoryStream ms = new MemoryStream(arr); + System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(ms); + ms.Close(); + IntPtr hBitmap = bmp.GetHbitmap(); + BitmapSource WpfBitmap = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hBitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); + return WpfBitmap; + } + catch (Exception ex) + { + Console.WriteLine(ex.Message.ToString()); + return null; + } + } + + /// + /// BitmapSource 转成base64 + /// + /// + /// + public static string BitmapSourceToBase64(BitmapSource bitmapSource) + { + try + { + byte[] buffer = null; + JpegBitmapEncoder encoder = new JpegBitmapEncoder(); + MemoryStream memoryStream = new MemoryStream(); + encoder.Frames.Add(BitmapFrame.Create(bitmapSource)); + encoder.Save(memoryStream); + memoryStream.Position = 0; + if (memoryStream.Length > 0) + { + using (BinaryReader br = new BinaryReader(memoryStream)) + { + buffer = br.ReadBytes((int)memoryStream.Length); + } + } + memoryStream.Close(); + string strbaser64 = Convert.ToBase64String(buffer); + return strbaser64; + } + catch (Exception) + { + return ""; + } + } + + /// + /// 将 Bitmap 转化为 BitmapSource + /// + /// 要转换的 Bitmap + /// 转换后的 BitmapSource + public static BitmapSource ToBitmapSource(System.Drawing.Bitmap bmp) + { + System.IntPtr hBitmap = bmp.GetHbitmap(); + try + { + return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hBitmap, System.IntPtr.Zero, System.Windows.Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); + } + finally + { + DeleteObject(hBitmap); + } + } + + /// + /// 将 Bitmap 转化为 BitmapSource + /// + /// 要转换的 Bitmap + /// 转换后的 BitmapImage + public static BitmapImage ToBitmapImage(System.Drawing.Bitmap bmp) + { + System.IO.MemoryStream ms = new System.IO.MemoryStream(); + bmp.Save(ms, ImageFormat.Bmp); + + BitmapImage image = new BitmapImage(); + image.BeginInit(); + image.StreamSource = ms; + image.EndInit(); + return image; + } + + /// + /// 将字节数组转换为 BitmapImage + /// + /// 要转换的字节数组 + /// 转换后的 BitmapImage + public static BitmapImage ToBitmapImage(byte[] bytes) + { + BitmapImage image = new BitmapImage(); + image.BeginInit(); + image.StreamSource = new System.IO.MemoryStream(bytes); + image.EndInit(); + return image; + } + + /// + /// 将 Bitmap 转化为字节数组 + /// + /// 要转换的 Bitmap + /// 转换后的字节数组 + public static byte[] ToByteArray(System.Drawing.Bitmap bmp) + { + using (System.IO.MemoryStream ms = new System.IO.MemoryStream()) + { + bmp.Save(ms, ImageFormat.Bmp); + return ms.ToArray(); + } + } + + /// + /// 将 BitmapSource 转化为 Bitmap + /// + /// 要转换的 BitmapSource + /// 转化后的 Bitmap + public static System.Drawing.Bitmap ToBitmap(BitmapSource source) + { + using (System.IO.MemoryStream ms = new System.IO.MemoryStream()) + { + BitmapEncoder encoder = new BmpBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(source)); + encoder.Save(ms); + return new System.Drawing.Bitmap(ms); + } + } + + [System.Runtime.InteropServices.DllImport("Gdi32.dll")] + private static extern bool DeleteObject(System.IntPtr hObject); + + /// 保存图片到文件 + /// + /// 图片数据 + /// 保存路径 + public static void SaveImageToFile(BitmapSource image, string filePath) + { + BitmapEncoder encoder = GetBitmapEncoder(filePath); + encoder.Frames.Add(BitmapFrame.Create(image)); + + using (var stream = new FileStream(filePath, FileMode.Create)) + { + encoder.Save(stream); + } + } + + + /// + /// 根据文件扩展名获取图片编码器 + /// + /// 文件路径 + /// 图片编码器 + private static BitmapEncoder GetBitmapEncoder(string filePath) + { + var extName = System.IO.Path.GetExtension(filePath).ToLower(); + if (extName.Equals(".png")) + { + return new PngBitmapEncoder(); + } + else + { + return new JpegBitmapEncoder(); + } + } + + + //图片转为base64编码的字符串 + public static string ImgToBase64String(Image bmp) + { + try + { + MemoryStream ms = new MemoryStream(); + bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); + byte[] arr = new byte[ms.Length]; + ms.Position = 0; + ms.Read(arr, 0, (int)ms.Length); + ms.Close(); + return Convert.ToBase64String(arr); + } + catch (Exception) + { + return null; + } + } + //threeebase64编码的字符串转为图片 + public static Bitmap Base64StringToImage(string strbase64) + { + try + { + byte[] arr = Convert.FromBase64String(strbase64); + MemoryStream ms = new MemoryStream(arr); + Bitmap bmp = new Bitmap(ms); + ms.Close(); + return bmp; + } + catch (Exception) + { + return null; + } + } + + /// + /// 图像分割 + /// + /// 输入图片 + /// 图片分割了多少行 + /// 图片分割了多少列 + /// 分割图宽 - X轴重叠区域 + /// 分割图高 - Y轴重叠区域 + /// 分割图宽 + /// 分割图高 + /// X轴重叠区域 + /// Y轴重叠区域 + /// + public static List divBitmap(BitmapSource input + , out int MaxRow, out int MaxColumn + , out int newWidth, out int newHeight + , int PixWidth = 1024, int PixHeight = 1024, int PixScollx = 100, int PixScolly = 100) + { + List results = new List(); + MaxRow = 0; + MaxColumn = 0; + newWidth = 0; + newHeight = 0; + if (PixScollx > PixWidth || PixScolly > PixHeight) return results; + newWidth = (int)(PixWidth - PixScollx); + newHeight = (int)(PixHeight - PixScolly); + MaxRow = (int)Math.Ceiling((decimal)input.PixelHeight / (PixHeight - PixScolly)); + MaxColumn = (int)Math.Ceiling((decimal)input.PixelWidth / (PixWidth - PixScollx)); + for (int i = 0; i < MaxRow; i++) + { + for (int j = 0; j < MaxColumn; j++) + { + int PixLeft = j * (PixWidth - PixScollx); + int PixTop = i * (PixHeight - PixScolly); + if (PixLeft + PixWidth <= input.PixelWidth && PixTop + PixHeight <= input.PixelHeight) + { + BitmapSource bitmap = PicCrop(input, PixLeft, PixTop, PixWidth, PixHeight); + results.Add(bitmap); + SaveImageToFile(bitmap, $"D:\\{i}-{j}.jpg"); + } + else + { + + var pic = CreatePicture(PixWidth, PixHeight, 0, 0, false); + int resWidth = input.PixelWidth - PixLeft > PixWidth ? PixWidth : input.PixelWidth - PixLeft; + int resHeight = input.PixelHeight - PixTop > PixHeight ? PixHeight : input.PixelHeight - PixTop; + var sec = PicCrop(input, PixLeft, PixTop, resWidth, resHeight); + var bitmap = PicSynthesis(pic, sec, 0, 0, (int)sec.PixelWidth, (int)sec.PixelHeight, "#FFFFFFFF"); + results.Add(bitmap); + SaveImageToFile(bitmap, $"D:\\{i}-{j}.jpg"); + } + } + } + return results; + + } + + /// + /// 图像分割 + /// + /// 输入图片 + /// 图片分割了多少行 + /// 图片分割了多少列 + /// 分割图宽 - X轴重叠区域 + /// 分割图高 - Y轴重叠区域 + /// 分割图宽 + /// 分割图高 + /// X轴重叠区域 + /// Y轴重叠区域 + /// + public static List divBitmap2(BitmapSource input + , out int MaxRow, out int MaxColumn + , out int newWidth, out int newHeight + , int PixWidth = 1024, int PixHeight = 1024, int PixScollx = 100, int PixScolly = 100) + { + List results = new List(); + MaxRow = 0; + MaxColumn = 0; + newWidth = 0; + newHeight = 0; + if (PixScollx > PixWidth || PixScolly > PixHeight) return results; + newWidth = (int)(PixWidth - PixScollx); + newHeight = (int)(PixHeight - PixScolly); + MaxRow = (int)Math.Ceiling((decimal)input.PixelHeight / (PixHeight - PixScolly)); + MaxColumn = (int)Math.Ceiling((decimal)input.PixelWidth / (PixWidth - PixScollx)); + for (int i = 0; i < MaxRow; i++) + { + for (int j = 0; j < MaxColumn; j++) + { + int PixLeft = j * (PixWidth - PixScollx); + int PixTop = i * (PixHeight - PixScolly); + if (PixLeft + PixWidth <= input.PixelWidth && PixTop + PixHeight <= input.PixelHeight) + { + BitmapSource bitmap = PicCrop(input, PixLeft, PixTop, PixWidth, PixHeight); + + results.Add(ToBitmap(bitmap)); + } + else + { + + var pic = CreatePicture(PixWidth, PixHeight, 0, 0, false); + int resWidth = input.PixelWidth - PixLeft > PixWidth ? PixWidth : input.PixelWidth - PixLeft; + int resHeight = input.PixelHeight - PixTop > PixHeight ? PixHeight : input.PixelHeight - PixTop; + var sec = PicCrop(input, PixLeft, PixTop, resWidth, resHeight); + var bitmap = PicSynthesis(pic, sec, 0, 0, (int)sec.PixelWidth, (int)sec.PixelHeight, "#FFFFFFFF"); + results.Add(ToBitmap(bitmap)); + } + } + } + return results; + + } + + } +} diff --git a/HMIcode/SmallProject/SmallProject/Utils/PicUtilHalcon.cs b/HMIcode/SmallProject/SmallProject/Utils/PicUtilHalcon.cs new file mode 100644 index 0000000..362a17f --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/Utils/PicUtilHalcon.cs @@ -0,0 +1,192 @@ +using HalconDotNet; +using System; +using System.Collections.Generic; +using System.Drawing.Imaging; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.Utils +{ + public partial class PicUtil + { + public static HObject BitmapToHobject(Bitmap bitmap) + { + int height = bitmap.Height;//图像的高度 + int width = bitmap.Width;//图像的宽度 + + + Rectangle imgRect = new Rectangle(0, 0, width, height); + BitmapData bitData = bitmap.LockBits(imgRect, ImageLockMode.ReadOnly, bitmap.PixelFormat); + HObject image; + + //由于Bitmap图像每行的字节数必须保持为4的倍数,因此在行的字节数不满足这个条件时,会对行进行补充,步幅数Stride表示的就是补充过后的每行的字节数,也成为扫描宽度 + int stride = bitData.Stride; + switch (bitmap.PixelFormat) + { + case PixelFormat.Format8bppIndexed: + { + unsafe + { + int count = height * width; + byte[] data = new byte[count]; + byte* bptr = (byte*)bitData.Scan0; + fixed (byte* pData = data) + { + for (int i = 0; i < height; i++) + { + for (int j = 0; j < width; j++) + { + /* + * + 如果直接使用GenImage1,传入BitData的Scan0(图像首元素的指针)作为内存指针的话,如果图像不满足行为4的倍数,那么填充的部分也会参与进来,从而导致图像扭曲 + * + * + */ + + + //舍去填充的部分 + data[i * width + j] = bptr[i * stride + j]; + } + + } + HOperatorSet.GenImage1(out image, "byte", width, height, new IntPtr(pData)); + } + } + + } + break; + case PixelFormat.Format24bppRgb: + { + unsafe + { + int count = height * width * 3;//24位的BitMap每个像素三个字节 + byte[] data = new byte[count]; + byte* bptr = (byte*)bitData.Scan0; + fixed (byte* pData = data) + { + for (int i = 0; i < height; i++) + { + for (int j = 0; j < width * 3; j++) + { + //每个通道的像素需一一对应 + data[i * width * 3 + j] = bptr[i * stride + j]; + } + } + HOperatorSet.GenImageInterleaved(out image, new IntPtr(pData), "bgr", bitmap.Width, bitmap.Height, 0, "byte", bitmap.Width, bitmap.Height, 0, 0, -1, 0); + } + } + } + break; + default: + { + unsafe + { + int count = height * width; + byte[] data = new byte[count]; + byte* bptr = (byte*)bitData.Scan0; + fixed (byte* pData = data) + { + for (int i = 0; i < height; i++) + { + for (int j = 0; j < width; j++) + { + data[i * width + j] = bptr[i * stride + j]; + } + + } + HOperatorSet.GenImage1(out image, "byte", width, height, new IntPtr(pData)); + } + } + + } + break; + } + bitmap.UnlockBits(bitData); + return image; + } + public static Bitmap HObject2Bitmap8(HObject image) + { + Bitmap res; + HTuple hpoint, type, width, height; + const int Alpha = 255; + HOperatorSet.GetImagePointer1(image, out hpoint, out type, out width, out height); + res = new Bitmap(width, height, PixelFormat.Format8bppIndexed); + ColorPalette pal = res.Palette; + for (int i = 0; i <= 255; i++) + { pal.Entries[i] = Color.FromArgb(Alpha, i, i, i); } + + res.Palette = pal; Rectangle rect = new Rectangle(0, 0, width, height); + BitmapData bitmapData = res.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed); + int PixelSize = Bitmap.GetPixelFormatSize(bitmapData.PixelFormat) / 8; + IntPtr ptr1 = bitmapData.Scan0; + IntPtr ptr2 = hpoint; int bytes = width * height; + byte[] rgbvalues = new byte[bytes]; + System.Runtime.InteropServices.Marshal.Copy(ptr2, rgbvalues, 0, bytes); + System.Runtime.InteropServices.Marshal.Copy(rgbvalues, 0, ptr1, bytes); + res.UnlockBits(bitmapData); + + return res; + } + + public static Bitmap HObject2Bitmap24(HObject hObject) + { + + try + { + HTuple pointer, w, h; + //下面这个语句的PNG即为HObject类型 + HOperatorSet.GetImagePointer3(hObject, out HTuple r, out HTuple g, out HTuple b, out pointer, out w, out h); + + byte[] red = new byte[w * h]; + byte[] green = new byte[w * h]; + byte[] blue = new byte[w * h]; + // 将指针指向地址的值取出来放到byte数组中 + Marshal.Copy(r, red, 0, w * h); + Marshal.Copy(g, green, 0, w * h); + Marshal.Copy(b, blue, 0, w * h); + + Bitmap bitmap = new Bitmap(w, h, System.Drawing.Imaging.PixelFormat.Format32bppRgb); + Rectangle rect2 = new Rectangle(0, 0, w, h); + BitmapData bitmapData2 = bitmap.LockBits(rect2, ImageLockMode.ReadWrite, System.Drawing.Imaging.PixelFormat.Format32bppRgb); + unsafe + { + byte* bptr2 = (byte*)bitmapData2.Scan0; + + Parallel.For(0, w * h, i => { + bptr2[i * 4] = blue[i]; + bptr2[i * 4 + 1] = green[i]; + bptr2[i * 4 + 2] = red[i]; + bptr2[i * 4 + 3] = 255; + }); + } + bitmap.UnlockBits(bitmapData2); + return bitmap; + } + catch (Exception) + { + return null; + } + } + + public static HObject Bitmap2HObjectBpp24(Bitmap bmp) + { + try + { + Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); + + BitmapData srcBmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); + HOperatorSet.GenImageInterleaved(out HObject image, srcBmpData.Scan0, "bgrx", bmp.Width, bmp.Height, 0, "byte", 0, 0, 0, 0, -1, 0); + bmp.UnlockBits(srcBmpData); + return image; + } + catch (Exception) + { + return null; + } + } + + } +} diff --git a/HMIcode/SmallProject/SmallProject/WindowController/JykerConnetContrl.cs b/HMIcode/SmallProject/SmallProject/WindowController/JykerConnetContrl.cs new file mode 100644 index 0000000..4f63b06 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/WindowController/JykerConnetContrl.cs @@ -0,0 +1,141 @@ +using Microsoft.Win32; +using SmallProject.Devices.Arm; +using SmallProject.Logger; +using SmallProject.Serials; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.Window; + +namespace SmallProject.WindowController +{ + class JykerConnetContrl + { + private MainWindow M; + + public JykerConnetContrl(MainWindow m) + { + M = m; + m.bt_Link.Click += bt_Link_Click; + m.bt_LinkAuto.Click += bt_LinkAuto_Click; + LoadComList(); + } + + //加载串口列表 + private List LoadComList() + { + var list = new List(); + const string keyPath = @"HARDWARE\DEVICEMAP\SERIALCOMM"; + using (RegistryKey key = Registry.LocalMachine.OpenSubKey(keyPath)) + { + if (key != null) + { + M.Dispatcher.Invoke(() => { + M.cb_ComList.Items.Clear(); + foreach (string valueName in key.GetValueNames()) + { + if (!valueName.Contains("USB")) continue; + string portName = key.GetValue(valueName) as string; + var caption = $"{portName}_{valueName}"; + Match match = Regex.Match(caption, @"COM\d+"); + if (match.Success) + { + string comPortName = match.Value; // 提取匹配到的 COM 名称 + M.cb_ComList.Items.Add(comPortName); + list.Add(comPortName); + } + } + }); + } + } + return list; + } + + //自动连接 + private void bt_LinkAuto_Click(object sender, RoutedEventArgs e) + { + if(M.cb_ComList.Items.Count==0) + { + JLog.Info("没有搜索到连接口,请检查硬件连接"); + } + + if(M.cb_ComList.Items.Count>1) + { + JLog.Info("搜索到多个连接口,请选择一个"); + } + + M.cb_ComList.SelectedIndex = 0; + //自动连接 + var name = M.cb_ComList.SelectedItem + ""; + var res = App.Core.Serial.Open(name); + if (res) + { + M.cb_ComList.IsEnabled = false; + M.bt_Link.Content = "断开连接"; + SetButtomState(true); + M.gb_ClawControl.IsEnabled = true; + M.gb_ArmControl.IsEnabled = true; + M.bt_LinkAuto.IsEnabled = false; + + } + + } + //手动连接 + private void bt_Link_Click(object sender, RoutedEventArgs e) + { + + + + if (M.bt_Link.Content.ToString() == "断开连接") + { + M.bt_LinkAuto.IsEnabled = true; + M.cb_ComList.IsEnabled = true; + M.bt_Link.Content = "手动连接"; + SetButtomState(false); + M.gb_ClawControl.IsEnabled = false; + + } + else + { + var name = M.cb_ComList.SelectedItem + ""; + var res = App.Core.Serial.Open(name); + if (res) + { + M.cb_ComList.IsEnabled = false; + M.bt_Link.Content = "断开连接"; + SetButtomState(true); + M.gb_ClawControl.IsEnabled = true; + M.gb_ArmControl.IsEnabled = true; + M.bt_LinkAuto.IsEnabled = false; + } + } + } + //设置按钮状态 + private void SetButtomState(bool State = false) + { + M.Dispatcher.Invoke(() => + { + M.bt_ApplyHomePosition.IsEnabled = State; + M.bt_StopNow.IsEnabled = State; + //M.bt_FK.IsEnabled = State; + //M.bt_IK.IsEnabled = State; + M.bt_MoveJoint.IsEnabled = State; + M.bt_GetCurrentAngle.IsEnabled = State; + M.bt_AddRecord.IsEnabled = State; + M.bt_MoveArmHand.IsEnabled = State; + M.bt_MoveLoop.IsEnabled = State; + M.bt_MoveLoopStop.IsEnabled = State; + M.bt_DeleteRecord.IsEnabled = State; + + }); + } + } + +} diff --git a/HMIcode/SmallProject/SmallProject/WindowController/JykerKinematicContrl.cs b/HMIcode/SmallProject/SmallProject/WindowController/JykerKinematicContrl.cs new file mode 100644 index 0000000..e1224c8 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/WindowController/JykerKinematicContrl.cs @@ -0,0 +1,137 @@ +using Microsoft.VisualBasic.Logging; +using SmallProject.Devices.Arm.Kinematic.Models; +using SmallProject.Logger; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls; + +namespace SmallProject.WindowController +{ + //正解逆解坐标实时解算 + class JykerKinematicContrl + { + private MainWindow M; + + public JykerKinematicContrl(MainWindow m) + { + M = m; + + //初始化值 + Init(); + + //绑定事件 + m.tb_X.TextChanged += Tb_IK; + m.tb_Y.TextChanged += Tb_IK; + m.tb_Z.TextChanged += Tb_IK; + m.tb_A.TextChanged += Tb_IK; + m.tb_B.TextChanged += Tb_IK; + m.tb_C.TextChanged += Tb_IK; + + m.tb_Joint1.TextChanged += Tb_FK; + m.tb_Joint2.TextChanged += Tb_FK; + m.tb_Joint3.TextChanged += Tb_FK; + m.tb_Joint4.TextChanged += Tb_FK; + m.tb_Joint5.TextChanged += Tb_FK; + m.tb_Joint6.TextChanged += Tb_FK; + } + + private void Init() + { + var joint = App.Core.Jyker.prepareJoints; + M.tb_Joint1.Text = Math.Round(joint.a[0], 2) + ""; + M.tb_Joint2.Text = Math.Round(joint.a[1], 2) + ""; + M.tb_Joint3.Text = Math.Round(joint.a[2], 2) + ""; + M.tb_Joint4.Text = Math.Round(joint.a[3], 2) + ""; + M.tb_Joint5.Text = Math.Round(joint.a[4], 2) + ""; + M.tb_Joint6.Text = Math.Round(joint.a[5], 2) + ""; + + var pose6 = App.Core.Jyker.preparePose6D; + App.Core.Jyker.dof6Solver.SolveFK(App.Core.Jyker.prepareJoints, App.Core.Jyker.preparePose6D); + M.tb_X.Text = Math.Round(pose6.X, 2) + ""; + M.tb_Y.Text = Math.Round(pose6.Y, 2) + ""; + M.tb_Z.Text = Math.Round(pose6.Z, 2) + ""; + M.tb_A.Text = Math.Round(pose6.A, 2) + ""; + M.tb_B.Text = Math.Round(pose6.B, 2) + ""; + M.tb_C.Text = Math.Round(pose6.C, 2) + ""; + } + + //正解 + private void Tb_FK(object sender, TextChangedEventArgs e) + { + double.TryParse(M.tb_Joint1.Text, out double a1); + double.TryParse(M.tb_Joint2.Text, out double a2); + double.TryParse(M.tb_Joint3.Text, out double a3); + double.TryParse(M.tb_Joint4.Text, out double a4); + double.TryParse(M.tb_Joint5.Text, out double a5); + double.TryParse(M.tb_Joint6.Text, out double a6); + + App.Core.Jyker.prepareJoints = new Joint6D_t(a1, a2, a3, a4, a5, a6); + App.Core.Jyker.dof6Solver.SolveFK(App.Core.Jyker.prepareJoints, App.Core.Jyker.preparePose6D); + + + + //更新正解位姿 + var pose6 = App.Core.Jyker.preparePose6D; + M.tb_X.TextChanged -= Tb_IK; + M.tb_Y.TextChanged -= Tb_IK; + M.tb_Z.TextChanged -= Tb_IK; + M.tb_A.TextChanged -= Tb_IK; + M.tb_B.TextChanged -= Tb_IK; + M.tb_C.TextChanged -= Tb_IK; + M.tb_X.Text = Math.Round(pose6.X, 2) + ""; + M.tb_Y.Text = Math.Round(pose6.Y, 2) + ""; + M.tb_Z.Text = Math.Round(pose6.Z, 2) + ""; + M.tb_A.Text = Math.Round(pose6.A, 2) + ""; + M.tb_B.Text = Math.Round(pose6.B, 2) + ""; + M.tb_C.Text = Math.Round(pose6.C, 2) + ""; + M.tb_X.TextChanged += Tb_IK; + M.tb_Y.TextChanged += Tb_IK; + M.tb_Z.TextChanged += Tb_IK; + M.tb_A.TextChanged += Tb_IK; + M.tb_B.TextChanged += Tb_IK; + M.tb_C.TextChanged += Tb_IK; + } + + //逆解 + private void Tb_IK(object sender, TextChangedEventArgs e) + { + double.TryParse(M.tb_X.Text, out double x); + double.TryParse(M.tb_Y.Text, out double y); + double.TryParse(M.tb_Z.Text, out double z); + double.TryParse(M.tb_A.Text, out double a); + double.TryParse(M.tb_B.Text, out double b); + double.TryParse(M.tb_C.Text, out double c); + + var res = App.Core.Jyker.SolveIK(new double[] { x, y, z, a, b, c }); + if (!res) + { + JLog.Info("逆解无解"); + } + var joint = App.Core.Jyker.prepareJoints; + + + M.tb_Joint1.TextChanged -= Tb_FK; + M.tb_Joint2.TextChanged -= Tb_FK; + M.tb_Joint3.TextChanged -= Tb_FK; + M.tb_Joint4.TextChanged -= Tb_FK; + M.tb_Joint5.TextChanged -= Tb_FK; + M.tb_Joint6.TextChanged -= Tb_FK; + M.tb_Joint1.Text = Math.Round(joint.a[0], 2) + ""; + M.tb_Joint2.Text = Math.Round(joint.a[1], 2) + ""; + M.tb_Joint3.Text = Math.Round(joint.a[2], 2) + ""; + M.tb_Joint4.Text = Math.Round(joint.a[3], 2) + ""; + M.tb_Joint5.Text = Math.Round(joint.a[4], 2) + ""; + M.tb_Joint6.Text = Math.Round(joint.a[5], 2) + ""; + M.tb_Joint1.TextChanged += Tb_FK; + M.tb_Joint2.TextChanged += Tb_FK; + M.tb_Joint3.TextChanged += Tb_FK; + M.tb_Joint4.TextChanged += Tb_FK; + M.tb_Joint5.TextChanged += Tb_FK; + M.tb_Joint6.TextChanged += Tb_FK; + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/WindowController/JykerMoveContrl.cs b/HMIcode/SmallProject/SmallProject/WindowController/JykerMoveContrl.cs new file mode 100644 index 0000000..8e51bdb --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/WindowController/JykerMoveContrl.cs @@ -0,0 +1,67 @@ +using SmallProject.Aliyun; +using SmallProject.MCP; +using SmallProject.Serials.Slcan; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.WindowController +{ + internal class JykerMoveContrl + { + private MainWindow M; + private JykerControlMCP jykerControlMCP; + + public JykerMoveContrl(MainWindow m) + { + M = m; + m.bt_ApplyHomePosition.Click += Bt_ApplyHomePosition_Click; ; + m.bt_MoveJoint.Click += Bt_MoveJoint_Click; + m.bt_StopNow.Click += Bt_StopNow_Click; + m.bt_ConnectAi.Click += Bt_ConnectAi_Click; + } + + //连接语音助手 + private void Bt_ConnectAi_Click(object sender, System.Windows.RoutedEventArgs e) + { + //AiAssistant aiAssistant = new AiAssistant(); + //aiAssistant.Init(); + Task.Run(async () => + { + jykerControlMCP = new JykerControlMCP(); + await jykerControlMCP.Init(); + }); + + } + + //设置立刻停止 + private void Bt_StopNow_Click(object sender, System.Windows.RoutedEventArgs e) + { + App.Core.Jyker.StopNow(); + } + + //设置当前位置为0位 + private void Bt_ApplyHomePosition_Click(object sender, System.Windows.RoutedEventArgs e) + { + App.Core.Jyker.ApplyHomePosition(); + } + + + //运动机械臂 + private void Bt_MoveJoint_Click(object sender, System.Windows.RoutedEventArgs e) + { + double.TryParse(M.tb_Joint1.Text, out double a1); + double.TryParse(M.tb_Joint2.Text, out double a2); + double.TryParse(M.tb_Joint3.Text, out double a3); + double.TryParse(M.tb_Joint4.Text, out double a4); + double.TryParse(M.tb_Joint5.Text, out double a5); + double.TryParse(M.tb_Joint6.Text, out double a6); + + App.Core.Jyker.Move(new double[6] { a1, a2, a3, a4, a5, a6 }); + } + + + } +} diff --git a/HMIcode/SmallProject/SmallProject/WindowController/JykerStatusContrl.cs b/HMIcode/SmallProject/SmallProject/WindowController/JykerStatusContrl.cs new file mode 100644 index 0000000..6f1aa0b --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/WindowController/JykerStatusContrl.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Controls; + +namespace SmallProject.WindowController +{ + public class JykerStatusContrl + { + private MainWindow M; + + public JykerStatusContrl(MainWindow m) + { + M = m; + m.bt_ArmLoopStart.Click += Bt_ArmLoopStart_Click; + m.bt_ArmLoopEnd.Click += Bt_ArmLoopEnd_Click; + } + + + + //监听机械臂各个轴的状态 + private void Bt_ArmLoopStart_Click(object sender, System.Windows.RoutedEventArgs e) + { + App.Core.Loop += LoopStatus; + App.Core.Loop += App.Core.Jyker.LoopStatus; + M.bt_ArmLoopStart.IsEnabled = false; + M.bt_ArmLoopEnd.IsEnabled = true; + } + //结束监听机械臂各个轴的状态 + private void Bt_ArmLoopEnd_Click(object sender, System.Windows.RoutedEventArgs e) + { + App.Core.Loop -= LoopStatus; + App.Core.Loop -= App.Core.Jyker.LoopStatus; + M.bt_ArmLoopStart.IsEnabled = true; + M.bt_ArmLoopEnd.IsEnabled = false; + } + + //循环获取电机信息 + public void LoopStatus() + { + Task.Run(() => + { + while (true) + { + M.Dispatcher.Invoke(() => + { + for (int i = 0; i < 6; i++) + { + var pb = M.FindName($"pb_pJ{i + 1}") as ProgressBar; + var tb = M.FindName($"tb_pJ{i + 1}") as TextBlock; + if(pb!=null&&tb!=null) + { + pb.Value = App.Core.Jyker.motorJ[i].Current*1000; + tb.Text = App.Core.Jyker.motorJ[i].Current*1000 + ""; + } + } + + }); + Thread.Sleep(50); + } + }); + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/WindowController/JykerViewContrl.cs b/HMIcode/SmallProject/SmallProject/WindowController/JykerViewContrl.cs new file mode 100644 index 0000000..1a96809 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/WindowController/JykerViewContrl.cs @@ -0,0 +1,205 @@ +using Emgu.CV; +using HalconDotNet; +using Masuit.Tools.Strings; +using Microsoft.VisualBasic.Logging; +using NAudio.CoreAudioApi; +using OpenCvSharp; +using OpenCvSharp.Extensions; +using SmallProject.Aliyun.Models; +using SmallProject.Logger; +using SmallProject.Utils; +using SmallProject.YOLO; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using System.Windows.Controls; +using System.Windows.Media.Media3D; +using Yolov5Net.Scorer; +using static System.Formats.Asn1.AsnWriter; + +namespace SmallProject.WindowController +{ + class JykerViewContrl + { + private MainWindow M; + bool IsVideo = false; + Bitmap bitmap; + Dictionary shapeModels = new Dictionary(); + public JykerViewContrl(MainWindow m) + { + M = m; + M.bt_StartDetect.Click += Bt_StartDetect_Click; + //加载model + LoadModels(); + } + + private void LoadModels() + { + string path = App.JConfig.FindModelPath; + string searchPattern = "*.model"; + string[] files = Directory.GetFiles(path, searchPattern, SearchOption.AllDirectories); + + if (files.Length > 0) + { + foreach (string file in files) + { + HOperatorSet.ReadShapeModel(file, out HTuple modelID); + shapeModels.Add(modelID, file.Replace(".model","")); + } + } + + } + + private void Bt_StartDetect_Click(object sender, System.Windows.RoutedEventArgs e) + { + + if (!IsVideo) + { + IsVideo = true; + Task.Run(() => { + try + { + // 创建视频捕获对象并打开默认摄像头(0) + using (var capture = new OpenCvSharp.VideoCapture(1)) + { + if (!capture.IsOpened()) + { + JLog.Info("好像没有找到摄像头哦"); + return; + } + + while (true) + { + if (!IsVideo) + { + M.Dispatcher.Invoke(() => + { + M.bt_StartDetect.Content = "开启视频"; + }); + break; + } + M.Dispatcher.Invoke(() => + { + M.bt_StartDetect.Content = "运行中"; + }); + + OpenCvSharp.Mat frame = new OpenCvSharp.Mat(); + + // 从摄像头中读取当前帧图像 + bool success = capture.Read(frame); + + if (!success || frame.Empty()) + continue; + + // 在这里进行处理或显示图像 + bitmap = BitmapConverter.ToBitmap(frame); + M.Dispatcher.Invoke(() => + { + + if(shapeModels.Count>0) + { + var hv_ModelIDs = new HTuple(); + var NumMatches = new HTuple(); + foreach (var mod in shapeModels) + { + hv_ModelIDs = hv_ModelIDs.TupleConcat(mod.Key); + NumMatches = NumMatches.TupleConcat(1); + } + var Hobj = PicUtil.BitmapToHobject(bitmap); + //Image, ModelIDs, -0.39, 0.78, 0.9, 1.1, 0.5, 1, \ + //0.5, 'least_squares', 0, 0.9, Row, Column, Angle,\ + //Scale, Score, Model + HOperatorSet.FindScaledShapeModels(Hobj, hv_ModelIDs, -0.39 + , 0.78, 0.2, 0.9, 1.1, NumMatches, 0.5, "interpolation", 0, 0.9, out HTuple hv_Row, + out HTuple hv_Column, out HTuple hv_Angle, out HTuple hv_Scale, out HTuple hv_Score, out HTuple modelID); + if(hv_Scale.Length>0) + { + + //border.findShapeModelEntity.Angle = Math.Abs(((double)hv_Angle) / Math.PI * 180); + //border.findShapeModelEntity.Score = (double)hv_Score; + //border.findShapeModelEntity.X = hv_Column - width / 2; + //border.findShapeModelEntity.Y = hv_Row - height / 2; + //border.findShapeModelEntity.Width = width; + //border.findShapeModelEntity.Height = height; + var name = shapeModels[modelID]; + HOperatorSet.GetImageSize(Hobj, out HTuple width, out HTuple height); + float x, y, w, h; + x = hv_Column - width / 2; + y = hv_Row - height / 2; + w = width; + h = height; + DrawToBitmap.DrawRectangle(bitmap, x, y, w, h, + text: $"{name}({hv_Score})"); + } + } + + + + + //AreaDetectYolo8 areaDetectBarError = new AreaDetectYolo8("yolov8n"); + //var resList = areaDetectBarError.getPrediction(bitmap); + //if (resList.Count() > 0) + //{ + // foreach (var item in resList) + // { + // //if (item.Label.Id != 0) continue; + // DrawToBitmap.DrawRectangle(bitmap, item.Rectangle.X, item.Rectangle.Y, item.Rectangle.Width, item.Rectangle.Height, + // text: $"{item.Label.Title}({item.Score})"); + // } + //} + + M.ImageBig.Source = PicUtil.ToBitmapSource(bitmap); + + if (!string.IsNullOrEmpty(App.Core.ViewName)) + { + string dir = App.JConfig.FindModelPath; + if(!Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + PicUtil.SaveImageToFile(PicUtil.ToBitmapSource(bitmap), $"{dir}/{App.Core.ViewName}.jpg"); + HOperatorSet.ReadImage(out HObject part, $"{dir}/{App.Core.ViewName}.jpg"); + HOperatorSet.CreateScaledShapeModel(part, 5 + , (new HTuple(-45)).TupleRad() + , (new HTuple(90)).TupleRad(), "auto" + , 0.8,1.0, "auto", "none", "ignore_global_polarity", 40, 10, out HTuple hv_ModelID); + HOperatorSet.WriteShapeModel(hv_ModelID, $"{dir}/{App.Core.ViewName}.model"); + shapeModels.Add(hv_ModelID, App.Core.ViewName); + App.Core.ViewName = ""; + } + }); + + Thread.Sleep(50); + } + } + } + catch (Exception) + { + M.Dispatcher.Invoke(() => + { + M.bt_StartDetect.Content = "开启视频"; + }); + } + + }); + } + else + { + IsVideo = false; + M.Dispatcher.Invoke(() => { + M.bt_StartDetect.Content = "开启视频"; + }); + } + } + + + + } +} diff --git a/HMIcode/SmallProject/SmallProject/WindowController/LogContrl.cs b/HMIcode/SmallProject/SmallProject/WindowController/LogContrl.cs new file mode 100644 index 0000000..c4f25e6 --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/WindowController/LogContrl.cs @@ -0,0 +1,45 @@ +using SmallProject.Logger; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using static System.Net.Mime.MediaTypeNames; + +namespace SmallProject.WindowController +{ + internal class LogContrl + { + private MainWindow M; + + public LogContrl(MainWindow m) + { + M = m; + JLog.MessageEvent += Log_MessageEvent; + } + int richTextLine = 0; + //打印日志到桌面 + private void Log_MessageEvent(string text) + { + try + { + var str = $"{DateTime.Now.ToString("HH:mm:ss")}:{text}\n"; + M.Dispatcher.Invoke(() => + { + M.tbLog.AppendText(str); + M.tbLog.ScrollToEnd(); + if (richTextLine >= 3000) + { + M.tbLog.Text = string.Empty; + richTextLine = 0; + } + richTextLine++; + }); + } + catch (Exception) + { + + } + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/WindowController/LogLiveContrl.cs b/HMIcode/SmallProject/SmallProject/WindowController/LogLiveContrl.cs new file mode 100644 index 0000000..688f17f --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/WindowController/LogLiveContrl.cs @@ -0,0 +1,48 @@ +using SmallProject.BiliBili; +using SmallProject.Logger; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SmallProject.WindowController +{ + internal class LogLiveContrl + { + private MainWindow M; + + public LogLiveContrl(MainWindow m) + { + M = m; + JLog.MessageEventLive += Log_MessageEvent; + + BStationLive bStationLive = new BStationLive(); + bStationLive.Init(); + } + int richTextLine = 0; + //打印日志到桌面 + private void Log_MessageEvent(string text) + { + try + { + var str = $"{text}\n"; + M.Dispatcher.Invoke(() => + { + M.tbLogLive.AppendText(str); + M.tbLogLive.ScrollToEnd(); + if (richTextLine >= 3000) + { + M.tbLogLive.Text = string.Empty; + richTextLine = 0; + } + richTextLine++; + }); + } + catch (Exception) + { + + } + } + } +} diff --git a/HMIcode/SmallProject/SmallProject/YOLO/YoloV8PoseOutput.cs b/HMIcode/SmallProject/SmallProject/YOLO/YoloV8PoseOutput.cs new file mode 100644 index 0000000..533173f --- /dev/null +++ b/HMIcode/SmallProject/SmallProject/YOLO/YoloV8PoseOutput.cs @@ -0,0 +1,287 @@ +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using OpenCvSharp; +using OpenCvSharp.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Ink; + +namespace SmallProject.YOLO +{ + public class YoloV8PoseOutput + { + public class Prediction + { + public float X { get; set; } + public float Y { get; set; } + public float Width { get; set; } + public float Height { get; set; } + public float Confidence { get; set; } + public int Class { get; set; } + public Keypoint[] Keypoints { get; set; } + } + + public class Keypoint + { + public float X { get; set; } + public float Y { get; set; } + public float Confidence { get; set; } + } + + //加载模型并运算 + public static List GetPredictions(System.Drawing.Bitmap bitmap, out System.Drawing.Bitmap outImage) + { + // 设置目标尺寸 (如 YOLOv8 需要 640x640) + int targetWidth = 640; + int targetHeight = 640; + outImage = bitmap; + // 预处理图像 + DenseTensor inputTensor = Preprocess(bitmap, targetWidth, targetHeight + , out float scalarR, out float paddingWidth, out float paddingHeight); + + // 加载 ONNX 模型 + string modelPath = "yolov8n-pose.onnx"; + using (var session = new InferenceSession(modelPath)) + { + // 创建模型输入 + var inputs = new List + { + NamedOnnxValue.CreateFromTensor("images", inputTensor) + }; + + // 运行推理 + using (var results = session.Run(inputs)) + { + // 获取输出 + var outputName = session.OutputMetadata.Keys.First(); + var outputTensor = results.First(r => r.Name == outputName).AsTensor(); + var outputArray = outputTensor.ToArray(); + // 解析输出 (假设 numPredictions = 1 和 numKeypoints = 17) + int numPredictions = outputTensor.Dimensions[2]; + int numKeypoints = (outputTensor.Dimensions[1] - 5) / 3; // 根据实际模型输出调整 + Prediction[] predictions = ParseOutput(outputArray, numPredictions, numKeypoints, scalarR, paddingWidth, paddingHeight); + + // 在图像上绘制检测结果 + DrawPredictions(bitmap, predictions); + + // 保存或显示结果图像 + //bitmap.Save("annotated_image.jpg"); + + return predictions.ToList(); + } + } + } + + //原图缩放成 640*640 + private static Mat Letterbox(Mat image, out float scalarR, out float paddingWidth, out float paddingHeight + , Size newShape = default(Size), Scalar color = default(Scalar), bool scaleup = true) + { + if (newShape == default(Size)) + { + newShape = new Size(640, 640); + } + if (color == default(Scalar)) + { + color = new Scalar(114, 114, 114); // 默认灰色填充 + } + + // 原图像大小 + int originalHeight = image.Height; + int originalWidth = image.Width; + + // 缩放比例 + scalarR = Math.Min((float)newShape.Width / originalWidth, (float)newShape.Height / originalHeight); + + // 只进行下采样, 因为上采样会让图片模糊 + if (!scaleup) + { + scalarR = (float)Math.Min(scalarR, 1.0); + } + + // 计算调整后的图像大小 + int newUnpadWidth = (int)Math.Round(originalWidth * scalarR); + int newUnpadHeight = (int)Math.Round(originalHeight * scalarR); + + // 计算填充量 + paddingWidth = (newShape.Width - newUnpadWidth) / 2.0f; // width padding + paddingHeight = (newShape.Height - newUnpadHeight) / 2.0f; // height padding + + // 调整图像大小 + Mat resizedImage = new Mat(); + Cv2.Resize(image, resizedImage, new Size(newUnpadWidth, newUnpadHeight)); + + // 计算四周的填充 + int top = (int)Math.Round(paddingHeight - 0.1); + int bottom = (int)Math.Round(paddingHeight + 0.1); + int left = (int)Math.Round(paddingWidth - 0.1); + int right = (int)Math.Round(paddingWidth + 0.1); + + // 添加填充 + Mat paddedImage = new Mat(); + Cv2.CopyMakeBorder(resizedImage, paddedImage, top, bottom, left, right, BorderTypes.Constant, color); + + return paddedImage; + } + + //过滤output数据 + private static Prediction[] ParseOutput(float[] output, int numPredictions, int numKeypoints, float scalarR, float paddingWidth, float paddingHeight) + { + int numAttributes = 4 + 1 + (3 * numKeypoints); // 4 for bbox, 1 for confidence, 1 for class, 3 * numKeypoints for keypoints + List predictions = new List(); + + for (int i = 0; i < numPredictions; i++) + { + + Prediction prediction = new Prediction + { + X = (output[i] - paddingWidth) / scalarR, + Y = (output[i + 1 * numPredictions] - paddingHeight) / scalarR, + Width = (output[i + 2 * numPredictions]) / scalarR, + Height = (output[i + 3 * numPredictions]) / scalarR, + Confidence = output[i + 4 * numPredictions], + Keypoints = new Keypoint[numKeypoints] + }; + + //只要 conf>0.7 的 + if (prediction.Confidence < 0.7) continue; + + for (int j = 0; j < numKeypoints; j++) + { + int keypointOffset = i + (5 + (j * 3)) * numPredictions; + prediction.Keypoints[j] = new Keypoint + { + X = (output[keypointOffset] - paddingWidth) / scalarR, + Y = (output[keypointOffset + 1 * numPredictions] - paddingHeight) / scalarR, + Confidence = output[keypointOffset + 2 * numPredictions] + }; + } + + predictions.Add(prediction); + } + //极大值抑制 + predictions = NonMaximumSuppression(predictions); + return predictions.ToArray(); + } + + //在图上画上检测结果 + private static void DrawPredictions(System.Drawing.Bitmap bitmap, Prediction[] predictions) + { + using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bitmap)) + { + using (System.Drawing.Pen pen = new System.Drawing.Pen(System.Drawing.Color.Red, 2)) + { + using (System.Drawing.SolidBrush brush = new System.Drawing.SolidBrush(System.Drawing.Color.Blue)) + { + foreach (var pred in predictions) + { + // Draw bounding box + g.DrawRectangle(pen, pred.X - pred.Width / 2, pred.Y - pred.Height / 2, pred.Width, pred.Height); + + // Draw keypoints + foreach (var keypoint in pred.Keypoints) + { + if (keypoint.Confidence > 0.5) // Optional: Draw only confident keypoints + { + g.FillEllipse(brush, keypoint.X - 3, keypoint.Y - 3, 6, 6); + } + } + + // 绘制骨架连接 + int[][] skeleton = new int[][] + { + new int[]{0, 1}, new int[]{0, 2}, new int[]{1, 3}, new int[]{2, 4}, + new int[]{5, 6}, new int[]{5, 7}, new int[]{7, 9}, new int[]{6, 8}, new int[]{8, 10}, + new int[]{11, 12}, new int[]{11, 13}, new int[]{13, 15}, + new int[]{12, 14}, new int[]{ 14, 16} + }; + + foreach (var bone in skeleton) + { + int kpt1 = bone[0], kpt2 = bone[1]; + float x1 = pred.Keypoints[kpt1].X; + float y1 = pred.Keypoints[kpt1].Y; + float x2 = pred.Keypoints[kpt2].X; + float y2 = pred.Keypoints[kpt2].Y; + + g.DrawLine(pen, x1, y1, x2, y2); + } + } + } + } + } + } + + //图像归一化 + private static DenseTensor Preprocess(System.Drawing.Bitmap bitmap, int targetWidth, int targetHeight + , out float scalarR, out float paddingWidth, out float paddingHeight) + { + // 1. 调整图像大小 + var mat = Letterbox(BitmapConverter.ToMat(bitmap), out scalarR, out paddingWidth, out paddingHeight); + var resizedBitmap = BitmapConverter.ToBitmap(mat); + // 2. 创建浮点数组存储图像数据 (channels, height, width) + int channels = 3; // RGB + var tensor = new DenseTensor(new[] { 1, channels, targetHeight, targetWidth }); + + // 3. 逐像素填充数组,归一化为 [0, 1] + for (int y = 0; y < targetHeight; y++) + { + for (int x = 0; x < targetWidth; x++) + { + System.Drawing.Color color = resizedBitmap.GetPixel(x, y); + tensor[0, 0, y, x] = color.B / 255f; // Blue channel + tensor[0, 1, y, x] = color.R / 255f; // Red channel + tensor[0, 2, y, x] = color.G / 255f; // Green channel + + } + } + + return tensor; + } + + //极大值抑制处理图像框 + private static List NonMaximumSuppression(List predictions, float iouThreshold = 0.5f) + { + // 按置信度降序排序 + var sortedPredictions = predictions.OrderByDescending(p => p.Confidence).ToList(); + List result = new List(); + + while (sortedPredictions.Count > 0) + { + // 取出置信度最高的检测框 + var bestPrediction = sortedPredictions[0]; + result.Add(bestPrediction); + sortedPredictions.RemoveAt(0); + + // 移除与当前检测框重叠度大于阈值的检测框 + sortedPredictions = sortedPredictions.Where(p => IoU(bestPrediction, p) < iouThreshold).ToList(); + } + + return result; + } + + //计算图像相交区域 + private static float IoU(Prediction boxA, Prediction boxB) + { + // 计算重叠区域的坐标 + float x1 = Math.Max(boxA.X, boxB.X); + float y1 = Math.Max(boxA.Y, boxB.Y); + float x2 = Math.Min(boxA.X + boxA.Width, boxB.X + boxB.Width); + float y2 = Math.Min(boxA.Y + boxA.Height, boxB.Y + boxB.Height); + + // 计算重叠区域的面积 + float interArea = Math.Max(0, x2 - x1) * Math.Max(0, y2 - y1); + + // 计算两个检测框的面积 + float boxAArea = boxA.Width * boxA.Height; + float boxBArea = boxB.Width * boxB.Height; + + // 计算交并比(IoU) + return interArea / (boxAArea + boxBArea - interArea); + } + + + } +} diff --git a/HMIcode/SmallProject/SmallProject/yolov8n-pose.onnx b/HMIcode/SmallProject/SmallProject/yolov8n-pose.onnx new file mode 100644 index 0000000..dd34be7 Binary files /dev/null and b/HMIcode/SmallProject/SmallProject/yolov8n-pose.onnx differ diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/AreaDetect.cs b/HMIcode/SmallProject/Yolov5Net.Scorer/AreaDetect.cs new file mode 100644 index 0000000..6b1a729 --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/AreaDetect.cs @@ -0,0 +1,74 @@ +using Emgu.CV; +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Yolov5Net.Scorer.Models; +using static System.Net.Mime.MediaTypeNames; + +namespace Yolov5Net.Scorer +{ + public enum AreaDetectType + { + CPU = 0, + GPU = 1 + } + + public class AreaDetect + { + + private YoloScorer scorer = null; + + /// + /// 初始化并加载网络文件 + /// + /// weights 文件名 + /// 0: cuda, -1: cpu + public AreaDetect(string onnxName, AreaDetectType type = AreaDetectType.CPU) + { + string fileName = string.Format("Assets/Weights/{0}.onnx", onnxName); + + if (type == AreaDetectType.CPU) + { + //SessionOptions options = new SessionOptions(); + //options.AppendExecutionProvider_CPU(); + scorer = new YoloScorer(fileName); + } + else if (type == AreaDetectType.GPU) + { + scorer = new YoloScorer(fileName, + Microsoft.ML.OnnxRuntime.SessionOptions.MakeSessionOptionWithCudaProvider(0)); + } + + } + + ///// + ///// 获取目标和坐标 + ///// + ///// + ///// + //public List getPrediction(Mat captureImg) + //{ + // List predictions = scorer.Predict(captureImg); + // return predictions; + //} + + /// + /// 获取目标和坐标 + /// + /// + /// + public List getPrediction(System.Drawing.Image captureImg) + { + List predictions = scorer.Predict(captureImg); + return predictions; + } + + + } + +} diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/AreaDetectYolo8.cs b/HMIcode/SmallProject/Yolov5Net.Scorer/AreaDetectYolo8.cs new file mode 100644 index 0000000..0b2fc80 --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/AreaDetectYolo8.cs @@ -0,0 +1,71 @@ +using Emgu.CV; +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Yolov5Net.Scorer.Models; + +namespace Yolov5Net.Scorer +{ + + public class AreaDetectYolo8 + { + + private YoloScorer scorer = null; + + /// + /// 初始化并加载网络文件 + /// + /// weights 文件名 + /// 0: cuda, -1: cpu + public AreaDetectYolo8(string onnxName, AreaDetectType type = AreaDetectType.CPU) + { + string fileName = string.Format("Assets/Weights/{0}.onnx", onnxName); + + if (type == AreaDetectType.CPU) + { + //SessionOptions options = new SessionOptions(); + //options.AppendExecutionProvider_CPU(); + scorer = new YoloScorer(fileName); + } + else if (type == AreaDetectType.GPU) + { + scorer = new YoloScorer(fileName, + Microsoft.ML.OnnxRuntime.SessionOptions.MakeSessionOptionWithCudaProvider(0)); + } + + } + + + ///// + ///// 获取目标和坐标 + ///// + ///// + ///// + //public List getPrediction(Mat captureImg) + //{ + // List predictions = scorer.Predict(captureImg); + // return predictions; + //} + + /// + /// 获取目标和坐标 + /// + /// + /// + public List getPrediction(Image captureImg) + { + List predictions = scorer.Predictyolo8(captureImg); + return predictions; + } + + } + +} diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/Assets/Weights/best.onnx b/HMIcode/SmallProject/Yolov5Net.Scorer/Assets/Weights/best.onnx new file mode 100644 index 0000000..187625f Binary files /dev/null and b/HMIcode/SmallProject/Yolov5Net.Scorer/Assets/Weights/best.onnx differ diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/Assets/Weights/yolov8n.onnx b/HMIcode/SmallProject/Yolov5Net.Scorer/Assets/Weights/yolov8n.onnx new file mode 100644 index 0000000..c36e47e Binary files /dev/null and b/HMIcode/SmallProject/Yolov5Net.Scorer/Assets/Weights/yolov8n.onnx differ diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/Extensions/RectangleExtensions.cs b/HMIcode/SmallProject/Yolov5Net.Scorer/Extensions/RectangleExtensions.cs new file mode 100644 index 0000000..7c9cdda --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/Extensions/RectangleExtensions.cs @@ -0,0 +1,12 @@ +using System.Drawing; + +namespace Yolov5Net.Scorer.Extensions +{ + public static class RectangleExtensions + { + public static float Area(this RectangleF source) + { + return source.Width * source.Height; + } + } +} diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/Models/Abstract/YoloModel.cs b/HMIcode/SmallProject/Yolov5Net.Scorer/Models/Abstract/YoloModel.cs new file mode 100644 index 0000000..5a83e67 --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/Models/Abstract/YoloModel.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Yolov5Net.Scorer.Models.Abstract +{ + /// + /// Model descriptor. + /// + public abstract class YoloModel + { + public abstract int Width { get; set; } + public abstract int Height { get; set; } + public abstract int Depth { get; set; } + + public abstract int Dimensions { get; set; } + + public abstract int[] Strides { get; set; } + public abstract int[][][] Anchors { get; set; } + public abstract int[] Shapes { get; set; } + + public abstract float Confidence { get; set; } + public abstract float MulConfidence { get; set; } + public abstract float Overlap { get; set; } + + public abstract string[] Outputs { get; set; } + public abstract List Labels { get; set; } + public abstract bool UseDetect { get; set; } + } +} diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/Models/YoloCocoP5Model.cs b/HMIcode/SmallProject/Yolov5Net.Scorer/Models/YoloCocoP5Model.cs new file mode 100644 index 0000000..9e54694 --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/Models/YoloCocoP5Model.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using Yolov5Net.Scorer.Models.Abstract; + +namespace Yolov5Net.Scorer.Models +{ + public class YoloCocoP5Model : YoloModel + { + public override int Width { get; set; } = 640; + public override int Height { get; set; } = 640; + public override int Depth { get; set; } = 3; + + public override int Dimensions { get; set; } =11; + //public override int Dimensions { get; set; } = 9; + + public override int[] Strides { get; set; } = new int[] { 8, 16, 32 }; + + public override int[][][] Anchors { get; set; } = new int[][][] + { + new int[][] { new int[] { 010, 13 }, new int[] { 016, 030 }, new int[] { 033, 023 } }, + new int[][] { new int[] { 030, 61 }, new int[] { 062, 045 }, new int[] { 059, 119 } }, + new int[][] { new int[] { 116, 90 }, new int[] { 156, 198 }, new int[] { 373, 326 } } + }; + + public override int[] Shapes { get; set; } = new int[] { 80, 40, 20 }; + + public override float Confidence { get; set; } = 0.0f; + public override float MulConfidence { get; set; } = 0.25f; + public override float Overlap { get; set; } = 0.45f; + + //public override string[] Outputs { get; set; } = new[] { "output" }; + public override string[] Outputs { get; set; } = new[] { "output0" }; + + public override List Labels { get; set; } = new List() + { + new YoloLabel { Id = 0, Name = "0", Title = "波垫" }, + new YoloLabel { Id = 1, Name = "1", Title = "无波垫" }, + new YoloLabel { Id = 2, Name = "2", Title = "焊点" }, + new YoloLabel { Id = 3, Name = "3", Title = "空焊" }, + new YoloLabel { Id = 4, Name = "4", Title = "虚焊" }, + new YoloLabel { Id = 5, Name = "5", Title = "锡珠" }, + }; + + public override bool UseDetect { get; set; } = true; + + public YoloCocoP5Model() + { + + } + } +} diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/Models/YoloCocoP6Model.cs b/HMIcode/SmallProject/Yolov5Net.Scorer/Models/YoloCocoP6Model.cs new file mode 100644 index 0000000..de08351 --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/Models/YoloCocoP6Model.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using Yolov5Net.Scorer.Models.Abstract; + +namespace Yolov5Net.Scorer.Models +{ + public class YoloCocoP6Model : YoloModel + { + public override int Width { get; set; } = 1280; + public override int Height { get; set; } = 1280; + public override int Depth { get; set; } = 3; + + public override int Dimensions { get; set; } = 85; + + public override int[] Strides { get; set; } = new int[] { 8, 16, 32, 64 }; + + public override int[][][] Anchors { get; set; } = new int[][][] + { + new int[][] { new int[] { 019, 027 }, new int[] { 044, 040 }, new int[] { 038, 094 } }, + new int[][] { new int[] { 096, 068 }, new int[] { 086, 152 }, new int[] { 180, 137 } }, + new int[][] { new int[] { 140, 301 }, new int[] { 303, 264 }, new int[] { 238, 542 } }, + new int[][] { new int[] { 436, 615 }, new int[] { 739, 380 }, new int[] { 925, 792 } } + }; + + public override int[] Shapes { get; set; } = new int[] { 160, 80, 40, 20 }; + + public override float Confidence { get; set; } = 0.20f; + public override float MulConfidence { get; set; } = 0.25f; + public override float Overlap { get; set; } = 0.45f; + + public override string[] Outputs { get; set; } = new[] { "output" }; + + public override List Labels { get; set; } = new List() + { + new YoloLabel { Id = 1, Name = "person" }, + new YoloLabel { Id = 2, Name = "bicycle" }, + new YoloLabel { Id = 3, Name = "car" }, + new YoloLabel { Id = 4, Name = "motorcycle" }, + new YoloLabel { Id = 5, Name = "airplane" }, + new YoloLabel { Id = 6, Name = "bus" }, + new YoloLabel { Id = 7, Name = "train" }, + new YoloLabel { Id = 8, Name = "truck" }, + new YoloLabel { Id = 9, Name = "boat" }, + new YoloLabel { Id = 10, Name = "traffic light" }, + new YoloLabel { Id = 11, Name = "fire hydrant" }, + new YoloLabel { Id = 12, Name = "stop sign" }, + new YoloLabel { Id = 13, Name = "parking meter" }, + new YoloLabel { Id = 14, Name = "bench" }, + new YoloLabel { Id = 15, Name = "bird" }, + new YoloLabel { Id = 16, Name = "cat" }, + new YoloLabel { Id = 17, Name = "dog" }, + new YoloLabel { Id = 18, Name = "horse" }, + new YoloLabel { Id = 19, Name = "sheep" }, + new YoloLabel { Id = 20, Name = "cow" }, + new YoloLabel { Id = 21, Name = "elephant" }, + new YoloLabel { Id = 22, Name = "bear" }, + new YoloLabel { Id = 23, Name = "zebra" }, + new YoloLabel { Id = 24, Name = "giraffe" }, + new YoloLabel { Id = 25, Name = "backpack" }, + new YoloLabel { Id = 26, Name = "umbrella" }, + new YoloLabel { Id = 27, Name = "handbag" }, + new YoloLabel { Id = 28, Name = "tie" }, + new YoloLabel { Id = 29, Name = "suitcase" }, + new YoloLabel { Id = 30, Name = "frisbee" }, + new YoloLabel { Id = 31, Name = "skis" }, + new YoloLabel { Id = 32, Name = "snowboard" }, + new YoloLabel { Id = 33, Name = "sports ball" }, + new YoloLabel { Id = 34, Name = "kite" }, + new YoloLabel { Id = 35, Name = "baseball bat" }, + new YoloLabel { Id = 36, Name = "baseball glove" }, + new YoloLabel { Id = 37, Name = "skateboard" }, + new YoloLabel { Id = 38, Name = "surfboard" }, + new YoloLabel { Id = 39, Name = "tennis racket" }, + new YoloLabel { Id = 40, Name = "bottle" }, + new YoloLabel { Id = 41, Name = "wine glass" }, + new YoloLabel { Id = 42, Name = "cup" }, + new YoloLabel { Id = 43, Name = "fork" }, + new YoloLabel { Id = 44, Name = "knife" }, + new YoloLabel { Id = 45, Name = "spoon" }, + new YoloLabel { Id = 46, Name = "bowl" }, + new YoloLabel { Id = 47, Name = "banana" }, + new YoloLabel { Id = 48, Name = "apple" }, + new YoloLabel { Id = 49, Name = "sandwich" }, + new YoloLabel { Id = 50, Name = "orange" }, + new YoloLabel { Id = 51, Name = "broccoli" }, + new YoloLabel { Id = 52, Name = "carrot" }, + new YoloLabel { Id = 53, Name = "hot dog" }, + new YoloLabel { Id = 54, Name = "pizza" }, + new YoloLabel { Id = 55, Name = "donut" }, + new YoloLabel { Id = 56, Name = "cake" }, + new YoloLabel { Id = 57, Name = "chair" }, + new YoloLabel { Id = 58, Name = "couch" }, + new YoloLabel { Id = 59, Name = "potted plant" }, + new YoloLabel { Id = 60, Name = "bed" }, + new YoloLabel { Id = 61, Name = "dining table" }, + new YoloLabel { Id = 62, Name = "toilet" }, + new YoloLabel { Id = 63, Name = "tv" }, + new YoloLabel { Id = 64, Name = "laptop" }, + new YoloLabel { Id = 65, Name = "mouse" }, + new YoloLabel { Id = 66, Name = "remote" }, + new YoloLabel { Id = 67, Name = "keyboard" }, + new YoloLabel { Id = 68, Name = "cell phone" }, + new YoloLabel { Id = 69, Name = "microwave" }, + new YoloLabel { Id = 70, Name = "oven" }, + new YoloLabel { Id = 71, Name = "toaster" }, + new YoloLabel { Id = 72, Name = "sink" }, + new YoloLabel { Id = 73, Name = "refrigerator" }, + new YoloLabel { Id = 74, Name = "book" }, + new YoloLabel { Id = 75, Name = "clock" }, + new YoloLabel { Id = 76, Name = "vase" }, + new YoloLabel { Id = 77, Name = "scissors" }, + new YoloLabel { Id = 78, Name = "teddy bear" }, + new YoloLabel { Id = 79, Name = "hair drier" }, + new YoloLabel { Id = 80, Name = "toothbrush" } + }; + + public override bool UseDetect { get; set; } = true; + + public YoloCocoP6Model() + { + + } + } +} diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/Models/Yolov8Model.cs b/HMIcode/SmallProject/Yolov5Net.Scorer/Models/Yolov8Model.cs new file mode 100644 index 0000000..b262a52 --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/Models/Yolov8Model.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Yolov5Net.Scorer.Models.Abstract; + +namespace Yolov5Net.Scorer.Models +{ + public class Yolov8Model : YoloModel + { + public override int Width { get; set; } = 640; + public override int Height { get; set; } = 640; + public override int Depth { get; set; } = 3; + + public override int Dimensions { get; set; } = 10; + //public override int Dimensions { get; set; } = 9; + + public override int[] Strides { get; set; } = new int[] { 8, 16, 32 }; + + public override int[][][] Anchors { get; set; } = new int[][][] + { + new int[][] { new int[] { 010, 13 }, new int[] { 016, 030 }, new int[] { 033, 023 } }, + new int[][] { new int[] { 030, 61 }, new int[] { 062, 045 }, new int[] { 059, 119 } }, + new int[][] { new int[] { 116, 90 }, new int[] { 156, 198 }, new int[] { 373, 326 } } + }; + + public override int[] Shapes { get; set; } = new int[] { 80, 40, 20 }; + + public override float Confidence { get; set; } = 0.0f; + public override float MulConfidence { get; set; } = 0.25f; + public override float Overlap { get; set; } = 0.45f; + + //public override string[] Outputs { get; set; } = new[] { "output" }; + public override string[] Outputs { get; set; } = new[] { "output0" }; + + public override List Labels { get; set; } = new List() + { + new YoloLabel { Id = 0, Name = "0", Title = "人" }, + new YoloLabel { Id = 1, Name = "1", Title = "自行车" }, + new YoloLabel { Id = 2, Name = "2", Title = "汽车" }, + new YoloLabel { Id = 3, Name = "3", Title = "摩托车" }, + new YoloLabel { Id = 4, Name = "4", Title = "飞机" }, + new YoloLabel { Id = 5, Name = "5", Title = "公交车" }, + new YoloLabel { Id = 6, Name = "6", Title = "火车" }, + new YoloLabel { Id = 7, Name = "7", Title = "卡车" }, + new YoloLabel { Id = 8, Name = "8", Title = "船" }, + new YoloLabel { Id = 9, Name = "9", Title = "交通灯" }, + new YoloLabel { Id = 10, Name = "10", Title = "消防栓" }, + new YoloLabel { Id = 11, Name = "11", Title = "停车标志" }, + new YoloLabel { Id = 12, Name = "12", Title = "停车计时器" }, + new YoloLabel { Id = 13, Name = "13", Title = "长椅" }, + new YoloLabel { Id = 14, Name = "14", Title = "鸟" }, + new YoloLabel { Id = 15, Name = "15", Title = "猫" }, + new YoloLabel { Id = 16, Name = "16", Title = "狗" }, + new YoloLabel { Id = 17, Name = "17", Title = "马" }, + new YoloLabel { Id = 18, Name = "18", Title = "羊" }, + new YoloLabel { Id = 19, Name = "19", Title = "牛" }, + new YoloLabel { Id = 20, Name = "20", Title = "大象" }, + new YoloLabel { Id = 21, Name = "21", Title = "熊" }, + new YoloLabel { Id = 22, Name = "22", Title = "斑马" }, + new YoloLabel { Id = 23, Name = "23", Title = "长颈鹿" }, + new YoloLabel { Id = 24, Name = "24", Title = "背包" }, + new YoloLabel { Id = 25, Name = "25", Title = "雨伞" }, + new YoloLabel { Id = 26, Name = "26", Title = "手提包" }, + new YoloLabel { Id = 27, Name = "27", Title = "领带" }, + new YoloLabel { Id = 28, Name = "28", Title = "行李箱" }, + new YoloLabel { Id = 29, Name = "29", Title = "飞盘" }, + new YoloLabel { Id = 30, Name = "30", Title = "滑雪板" }, + new YoloLabel { Id = 31, Name = "31", Title = "单板滑雪板" }, + new YoloLabel { Id = 32, Name = "32", Title = "运动球" }, + new YoloLabel { Id = 33, Name = "33", Title = "风筝" }, + new YoloLabel { Id = 34, Name = "34", Title = "棒球棒" }, + new YoloLabel { Id = 35, Name = "35", Title = "棒球手套" }, + new YoloLabel { Id = 36, Name = "36", Title = "滑板" }, + new YoloLabel { Id = 37, Name = "37", Title = "冲浪板" }, + new YoloLabel { Id = 38, Name = "38", Title = "网球拍" }, + new YoloLabel { Id = 39, Name = "39", Title = "瓶子" }, + new YoloLabel { Id = 40, Name = "40", Title = "酒杯" }, + new YoloLabel { Id = 41, Name = "41", Title = "杯子" }, + new YoloLabel { Id = 42, Name = "42", Title = "叉子" }, + new YoloLabel { Id = 43, Name = "43", Title = "刀" }, + new YoloLabel { Id = 44, Name = "44", Title = "勺子" }, + new YoloLabel { Id = 45, Name = "45", Title = "碗" }, + new YoloLabel { Id = 46, Name = "46", Title = "香蕉" }, + new YoloLabel { Id = 47, Name = "47", Title = "苹果" }, + new YoloLabel { Id = 48, Name = "48", Title = "三明治" }, + new YoloLabel { Id = 49, Name = "49", Title = "橙子" }, + new YoloLabel { Id = 50, Name = "50", Title = "西兰花" }, + new YoloLabel { Id = 51, Name = "51", Title = "胡萝卜" }, + new YoloLabel { Id = 52, Name = "52", Title = "热狗" }, + new YoloLabel { Id = 53, Name = "53", Title = "披萨" }, + new YoloLabel { Id = 54, Name = "54", Title = "甜甜圈" }, + new YoloLabel { Id = 55, Name = "55", Title = "蛋糕" }, + new YoloLabel { Id = 56, Name = "56", Title = "椅子" }, + new YoloLabel { Id = 57, Name = "57", Title = "沙发" }, + new YoloLabel { Id = 58, Name = "58", Title = "盆栽植物" }, + new YoloLabel { Id = 59, Name = "59", Title = "床" }, + new YoloLabel { Id = 60, Name = "60", Title = "餐桌" }, + new YoloLabel { Id = 61, Name = "61", Title = "马桶" }, + new YoloLabel { Id = 62, Name = "62", Title = "电视" }, + new YoloLabel { Id = 63, Name = "63", Title = "笔记本电脑" }, + new YoloLabel { Id = 64, Name = "64", Title = "鼠标" }, + new YoloLabel { Id = 65, Name = "65", Title = "遥控器" }, + new YoloLabel { Id = 66, Name = "66", Title = "键盘" }, + new YoloLabel { Id = 67, Name = "67", Title = "手机" }, + new YoloLabel { Id = 68, Name = "68", Title = "微波炉" }, + new YoloLabel { Id = 69, Name = "69", Title = "烤箱" }, + new YoloLabel { Id = 70, Name = "70", Title = "烤面包机" }, + new YoloLabel { Id = 71, Name = "71", Title = "水槽" }, + new YoloLabel { Id = 72, Name = "72", Title = "冰箱" }, + new YoloLabel { Id = 73, Name = "73", Title = "书" }, + new YoloLabel { Id = 74, Name = "74", Title = "时钟" }, + new YoloLabel { Id = 75, Name = "75", Title = "花瓶" }, + new YoloLabel { Id = 76, Name = "76", Title = "剪刀" }, + new YoloLabel { Id = 77, Name = "77", Title = "泰迪熊" }, + new YoloLabel { Id = 78, Name = "78", Title = "吹风机" }, + new YoloLabel { Id = 79, Name = "79", Title = "牙刷" } + }; + + public override bool UseDetect { get; set; } = true; + + + } +} diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/YoloLabel.cs b/HMIcode/SmallProject/Yolov5Net.Scorer/YoloLabel.cs new file mode 100644 index 0000000..97458d2 --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/YoloLabel.cs @@ -0,0 +1,21 @@ +using System.Drawing; + +namespace Yolov5Net.Scorer +{ + /// + /// Label of detected object. + /// + public class YoloLabel + { + public int Id { get; set; } + public string Name { get; set; } + public string Title { get; set; } + public YoloLabelKind Kind { get; set; } + public Color Color { get; set; } + + public YoloLabel() + { + Color = Color.Yellow; + } + } +} diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/YoloLabelKind.cs b/HMIcode/SmallProject/Yolov5Net.Scorer/YoloLabelKind.cs new file mode 100644 index 0000000..163be0c --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/YoloLabelKind.cs @@ -0,0 +1,10 @@ +namespace Yolov5Net.Scorer +{ + /// + /// Enum to specify type of detected object. + /// + public enum YoloLabelKind + { + Generic + } +} diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/YoloPrediction.cs b/HMIcode/SmallProject/Yolov5Net.Scorer/YoloPrediction.cs new file mode 100644 index 0000000..0d9e564 --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/YoloPrediction.cs @@ -0,0 +1,26 @@ +using System.Drawing; + +namespace Yolov5Net.Scorer +{ + /// + /// Object prediction. + /// + public class YoloPrediction + { + public YoloLabel Label { get; set; } + public RectangleF Rectangle { get; set; } + public float Score { get; set; } + + public YoloPrediction() { } + + public YoloPrediction(YoloLabel label, float confidence) : this(label) + { + Score = confidence; + } + + public YoloPrediction(YoloLabel label) + { + Label = label; + } + } +} diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/YoloScorer.cs b/HMIcode/SmallProject/Yolov5Net.Scorer/YoloScorer.cs new file mode 100644 index 0000000..08260ea --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/YoloScorer.cs @@ -0,0 +1,709 @@ +using Emgu.CV; +using Emgu.CV.Flann; +using Emgu.CV.Structure; +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Yolov5Net.Scorer.Extensions; +using Yolov5Net.Scorer.Models.Abstract; + +namespace Yolov5Net.Scorer +{ + /// + /// Yolov5 scorer. + /// + public class YoloScorer : IDisposable where T : YoloModel + { + private readonly T _model; + + private readonly InferenceSession _inferenceSession; + + /// + /// Outputs value between 0 and 1. + /// + private float Sigmoid(float value) + { + return 1 / (1 + (float)Math.Exp(-value)); + } + + /// + /// Converts xywh bbox format to xyxy. + /// + private float[] Xywh2xyxy(float[] source) + { + var result = new float[4]; + + result[0] = source[0] - source[2] / 2f; + result[1] = source[1] - source[3] / 2f; + result[2] = source[0] + source[2] / 2f; + result[3] = source[1] + source[3] / 2f; + + return result; + } + + /// + /// Returns value clamped to the inclusive range of min and max. + /// + public float Clamp(float value, float min, float max) + { + return (value < min) ? min : (value > max) ? max : value; + } + + /// + /// Resizes image keeping ratio to fit model input size. + /// + private Bitmap ResizeImage(Image image) + { + PixelFormat format = image.PixelFormat; + + var output = new Bitmap(_model.Width, _model.Height, format); + + var (w, h) = (image.Width, image.Height); // image width and height + var (xRatio, yRatio) = (_model.Width / (float)w, _model.Height / (float)h); // x, y ratios + var ratio = Math.Min(xRatio, yRatio); // ratio = resized / original + var (width, height) = ((int)(w * ratio), (int)(h * ratio)); // roi width and height + var (x, y) = ((_model.Width / 2) - (width / 2), (_model.Height / 2) - (height / 2)); // roi x and y coordinates + var roi = new Rectangle(x, y, width, height); // region of interest + + using (var graphics = Graphics.FromImage(output)) + { + graphics.Clear(Color.FromArgb(0, 0, 0, 0)); // clear canvas + + graphics.SmoothingMode = SmoothingMode.None; // no smoothing + graphics.InterpolationMode = InterpolationMode.Bilinear; // bilinear interpolation + graphics.PixelOffsetMode = PixelOffsetMode.Half; // half pixel offset + + graphics.DrawImage(image, roi); // draw scaled + } + + return output; + } + + /// + /// Extracts pixels into tensor for net input. + /// + private Tensor ExtractPixels(Image image) + { + var bitmap = (Bitmap)image; + + var rectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height); + BitmapData bitmapData = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, bitmap.PixelFormat); + int bytesPerPixel = Image.GetPixelFormatSize(bitmap.PixelFormat) / 8; + + var tensor = new DenseTensor(new[] { 1, 3, _model.Height, _model.Width }); + + unsafe // speed up conversion by direct work with memory + { + Parallel.For(0, bitmapData.Height, (y) => + { + byte* row = (byte*)bitmapData.Scan0 + (y * bitmapData.Stride); + + Parallel.For(0, bitmapData.Width, (x) => + { + tensor[0, 0, y, x] = row[x * bytesPerPixel + 2] / 255.0F; // r + tensor[0, 1, y, x] = row[x * bytesPerPixel + 1] / 255.0F; // g + tensor[0, 2, y, x] = row[x * bytesPerPixel + 0] / 255.0F; // b + }); + }); + + bitmap.UnlockBits(bitmapData); + } + + return tensor; + } + + private Tensor ExtractPixels(Mat image) + { + Stopwatch sw = new Stopwatch(); + sw.Restart(); + + + + //var aa =image.GetData().ToTensor(); + //aa. + //return image.GetData().ToTensor(); + //var bitmap = (Bitmap)image; + + //var rectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height); + //BitmapData bitmapData = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, bitmap.PixelFormat); + //int bytesPerPixel = Image.GetPixelFormatSize(bitmap.PixelFormat) / 8; + + int bytesPerPixel = 3; + + var tensor = new DenseTensor(new[] { 1, 3, _model.Height, _model.Width }); + + unsafe // speed up conversion by direct work with memory + { + + + + + // Parallel.For(0, 64, (y) => + // { + + + // Parallel.For(0, image.Width / 2, (x) => + // { + // tensor[0, 0, y , x] = 0F; // r + // tensor[0, 1, y , x] = 0F; // g + // tensor[0, 2, x] = 0F; // b + // }); + // }); + + + + + + Parallel.For(0, image.Height / 2, (y) => + { + //byte* row = (byte*)bitmapData.Scan0 + (y * bitmapData.Stride); + + byte* row = (byte*)image.GetDataPointer(2 * y); + + Parallel.For(0, image.Width / 2, (x) => + { + //tensor[0, 0, y, x] = row[x * bytesPerPixel + 2] / 255.0F; // r + //tensor[0, 1, y, x] = row[x * bytesPerPixel + 1] / 255.0F; // g + //tensor[0, 2, y, x] = row[x * bytesPerPixel + 0] / 255.0F; // b + + //tensor[0, 0, y, x] = (float)image[y, x].Red / 255.0F; // r + //tensor[0, 1, y, x] = (float)image[y, x].Green / 255.0F; // g + //tensor[0, 2, y, x] = (float)image[y, x].Blue / 255.0F; // b + + tensor[0, 0, y + 64, x] = row[2 * x * bytesPerPixel + 2] / 255.0F; // r + tensor[0, 1, y + 64, x] = row[2 * x * bytesPerPixel + 1] / 255.0F; // g + tensor[0, 2, y + 64, x] = row[2 * x * bytesPerPixel + 0] / 255.0F; // b + + }); + }); + + // //bitmap.UnlockBits(bitmapData); + } + + + sw.Stop(); + Debug.WriteLine("toTensor:" + sw.ElapsedMilliseconds); + + + return tensor; + } + + + private Tensor ExtractPixels2(Image image) + { + var bitmap = (Bitmap)image; + + var rectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height); + BitmapData bitmapData = bitmap.LockBits(rectangle, ImageLockMode.ReadOnly, bitmap.PixelFormat); + int bytesPerPixel = Image.GetPixelFormatSize(bitmap.PixelFormat) / 8; + + var tensor = new DenseTensor(new[] { 1, 3, _model.Height, _model.Width }); + + unsafe // speed up conversion by direct work with memory + { + Parallel.For(0, bitmapData.Height, (y) => + { + byte* row = (byte*)bitmapData.Scan0 + (y * bitmapData.Stride); + + Parallel.For(0, bitmapData.Width, (x) => + { + tensor[0, 0, y, x] = row[x * bytesPerPixel + 2] / 255.0F; // r + tensor[0, 1, y, x] = row[x * bytesPerPixel + 1] / 255.0F; // g + tensor[0, 2, y, x] = row[x * bytesPerPixel + 0] / 255.0F; // b + }); + }); + + bitmap.UnlockBits(bitmapData); + } + + return tensor; + } + + /// + /// Runs inference session. + /// + private DenseTensor[] Inference(Image image) + { + Bitmap resized = null; + + if (image.Width != _model.Width || image.Height != _model.Height) + { + resized = ResizeImage(image); // fit image size to specified input size + } + + //resized.Save(@"C:\Users\pstech\Desktop\其他项目\焊接检测\resized.jpg"); + + + var inputs = new List // add image as onnx input + { + NamedOnnxValue.CreateFromTensor("images", ExtractPixels(resized ?? image)) + }; + + IDisposableReadOnlyCollection result = _inferenceSession.Run(inputs); // run inference + + var output = new List>(); + + foreach (var item in _model.Outputs) // add outputs for processing + { + output.Add(result.First(x => x.Name == item).Value as DenseTensor); + }; + + return output.ToArray(); + } + + private DenseTensor[] Inference(Mat image) + { + Mat resized = null; + + //if (image.Width != _model.Width || image.Height != _model.Height) + //{ + // resized = ResizeImage(image); // fit image size to specified input size + //} + + var inputs = new List // add image as onnx input + { + NamedOnnxValue.CreateFromTensor("images", ExtractPixels(resized ?? image)) + }; + + IDisposableReadOnlyCollection result = _inferenceSession.Run(inputs); // run inference + + var output = new List>(); + + foreach (var item in _model.Outputs) // add outputs for processing + { + output.Add(result.First(x => x.Name == item).Value as DenseTensor); + }; + + return output.ToArray(); + } + /// + /// Parses net output (detect) to predictions. + /// + private List ParseDetect(DenseTensor output, Image image) + { + var result = new ConcurrentBag(); + + var (w, h) = (image.Width, image.Height); // image w and h + var (xGain, yGain) = (_model.Width / (float)w, _model.Height / (float)h); // x, y gains + var gain = Math.Min(xGain, yGain); // gain = resized / original + + var (xPad, yPad) = ((_model.Width - w * gain) / 2, (_model.Height - h * gain) / 2); // left, right pads + + Parallel.For(0, (int)output.Length / _model.Dimensions, (i) => + { + if (output[0, i, 4] <= _model.Confidence) return; // skip low obj_conf results + + Parallel.For(5, _model.Dimensions, (j) => + { + output[0, i, j] = output[0, i, j] * output[0, i, 4]; // mul_conf = obj_conf * cls_conf + }); + + Parallel.For(5, _model.Dimensions, (k) => + { + if (output[0, i, k] <= _model.MulConfidence) return; // skip low mul_conf results + + float xMin = ((output[0, i, 0] - output[0, i, 2] / 2) - xPad) / gain; // unpad bbox tlx to original + float yMin = ((output[0, i, 1] - output[0, i, 3] / 2) - yPad) / gain; // unpad bbox tly to original + float xMax = ((output[0, i, 0] + output[0, i, 2] / 2) - xPad) / gain; // unpad bbox brx to original + float yMax = ((output[0, i, 1] + output[0, i, 3] / 2) - yPad) / gain; // unpad bbox bry to original + + xMin = Clamp(xMin, 0, w - 0); // clip bbox tlx to boundaries + yMin = Clamp(yMin, 0, h - 0); // clip bbox tly to boundaries + xMax = Clamp(xMax, 0, w - 1); // clip bbox brx to boundaries + yMax = Clamp(yMax, 0, h - 1); // clip bbox bry to boundaries + + YoloLabel label = _model.Labels[k - 5]; + + var prediction = new YoloPrediction(label, output[0, i, k]) + { + Rectangle = new RectangleF(xMin, yMin, xMax - xMin, yMax - yMin) + }; + + result.Add(prediction); + }); + }); + + return result.ToList(); + } + private List ParseDetect(DenseTensor output, Mat image) + { + var result = new ConcurrentBag(); + + var (w, h) = (image.Width, image.Height); // image w and h + var (xGain, yGain) = (_model.Width / (float)w, _model.Height / (float)h); // x, y gains + var gain = Math.Min(xGain, yGain); // gain = resized / original + + var (xPad, yPad) = ((_model.Width - w * gain) / 2, (_model.Height - h * gain) / 2); // left, right pads + + Parallel.For(0, (int)output.Length / _model.Dimensions, (i) => + { + if (output[0, i, 4] <= _model.Confidence) return; // skip low obj_conf results + + Parallel.For(5, _model.Dimensions, (j) => + { + output[0, i, j] = output[0, i, j] * output[0, i, 4]; // mul_conf = obj_conf * cls_conf + }); + + Parallel.For(5, _model.Dimensions, (k) => + { + if (output[0, i, k] <= _model.MulConfidence) return; // skip low mul_conf results + + float xMin = ((output[0, i, 0] - output[0, i, 2] / 2) - xPad) / gain; // unpad bbox tlx to original + float yMin = ((output[0, i, 1] - output[0, i, 3] / 2) - yPad) / gain; // unpad bbox tly to original + float xMax = ((output[0, i, 0] + output[0, i, 2] / 2) - xPad) / gain; // unpad bbox brx to original + float yMax = ((output[0, i, 1] + output[0, i, 3] / 2) - yPad) / gain; // unpad bbox bry to original + + xMin = Clamp(xMin, 0, w - 0); // clip bbox tlx to boundaries + yMin = Clamp(yMin, 0, h - 0); // clip bbox tly to boundaries + xMax = Clamp(xMax, 0, w - 1); // clip bbox brx to boundaries + yMax = Clamp(yMax, 0, h - 1); // clip bbox bry to boundaries + + YoloLabel label = _model.Labels[k - 5]; + + var prediction = new YoloPrediction(label, output[0, i, k]) + { + Rectangle = new RectangleF(xMin, yMin, xMax - xMin, yMax - yMin) + }; + + result.Add(prediction); + }); + }); + + return result.ToList(); + } + + /// + /// Parses net outputs (sigmoid) to predictions. + /// + private List ParseSigmoid(DenseTensor[] output, Image image) + { + var result = new ConcurrentBag(); + + var (w, h) = (image.Width, image.Height); // image w and h + var (xGain, yGain) = (_model.Width / (float)w, _model.Height / (float)h); // x, y gains + var gain = Math.Min(xGain, yGain); // gain = resized / original + + var (xPad, yPad) = ((_model.Width - w * gain) / 2, (_model.Height - h * gain) / 2); // left, right pads + + Parallel.For(0, output.Length, (i) => // iterate model outputs + { + int shapes = _model.Shapes[i]; // shapes per output + + Parallel.For(0, _model.Anchors[0].Length, (a) => // iterate anchors + { + Parallel.For(0, shapes, (y) => // iterate shapes (rows) + { + Parallel.For(0, shapes, (x) => // iterate shapes (columns) + { + int offset = (shapes * shapes * a + shapes * y + x) * _model.Dimensions; + + float[] buffer = output[i].Skip(offset).Take(_model.Dimensions).Select(Sigmoid).ToArray(); + + if (buffer[4] <= _model.Confidence) return; // skip low obj_conf results + + List scores = buffer.Skip(5).Select(b => b * buffer[4]).ToList(); // mul_conf = obj_conf * cls_conf + + float mulConfidence = scores.Max(); // max confidence score + + if (mulConfidence <= _model.MulConfidence) return; // skip low mul_conf results + + float rawX = (buffer[0] * 2 - 0.5f + x) * _model.Strides[i]; // predicted bbox x (center) + float rawY = (buffer[1] * 2 - 0.5f + y) * _model.Strides[i]; // predicted bbox y (center) + + float rawW = (float)Math.Pow(buffer[2] * 2, 2) * _model.Anchors[i][a][0]; // predicted bbox w + float rawH = (float)Math.Pow(buffer[3] * 2, 2) * _model.Anchors[i][a][1]; // predicted bbox h + + float[] xyxy = Xywh2xyxy(new float[] { rawX, rawY, rawW, rawH }); + + float xMin = Clamp((xyxy[0] - xPad) / gain, 0, w - 0); // unpad, clip tlx + float yMin = Clamp((xyxy[1] - yPad) / gain, 0, h - 0); // unpad, clip tly + float xMax = Clamp((xyxy[2] - xPad) / gain, 0, w - 1); // unpad, clip brx + float yMax = Clamp((xyxy[3] - yPad) / gain, 0, h - 1); // unpad, clip bry + + YoloLabel label = _model.Labels[scores.IndexOf(mulConfidence)]; + + var prediction = new YoloPrediction(label, mulConfidence) + { + Rectangle = new RectangleF(xMin, yMin, xMax - xMin, yMax - yMin) + }; + + result.Add(prediction); + }); + }); + }); + }); + + return result.ToList(); + } + + private List ParseSigmoid(DenseTensor[] output, Mat image) + { + var result = new ConcurrentBag(); + + var (w, h) = (image.Width, image.Height); // image w and h + var (xGain, yGain) = (_model.Width / (float)w, _model.Height / (float)h); // x, y gains + var gain = Math.Min(xGain, yGain); // gain = resized / original + + var (xPad, yPad) = ((_model.Width - w * gain) / 2, (_model.Height - h * gain) / 2); // left, right pads + + Parallel.For(0, output.Length, (i) => // iterate model outputs + { + int shapes = _model.Shapes[i]; // shapes per output + + Parallel.For(0, _model.Anchors[0].Length, (a) => // iterate anchors + { + Parallel.For(0, shapes, (y) => // iterate shapes (rows) + { + Parallel.For(0, shapes, (x) => // iterate shapes (columns) + { + int offset = (shapes * shapes * a + shapes * y + x) * _model.Dimensions; + + float[] buffer = output[i].Skip(offset).Take(_model.Dimensions).Select(Sigmoid).ToArray(); + + if (buffer[4] <= _model.Confidence) return; // skip low obj_conf results + + List scores = buffer.Skip(5).Select(b => b * buffer[4]).ToList(); // mul_conf = obj_conf * cls_conf + + float mulConfidence = scores.Max(); // max confidence score + + if (mulConfidence <= _model.MulConfidence) return; // skip low mul_conf results + + float rawX = (buffer[0] * 2 - 0.5f + x) * _model.Strides[i]; // predicted bbox x (center) + float rawY = (buffer[1] * 2 - 0.5f + y) * _model.Strides[i]; // predicted bbox y (center) + + float rawW = (float)Math.Pow(buffer[2] * 2, 2) * _model.Anchors[i][a][0]; // predicted bbox w + float rawH = (float)Math.Pow(buffer[3] * 2, 2) * _model.Anchors[i][a][1]; // predicted bbox h + + float[] xyxy = Xywh2xyxy(new float[] { rawX, rawY, rawW, rawH }); + + float xMin = Clamp((xyxy[0] - xPad) / gain, 0, w - 0); // unpad, clip tlx + float yMin = Clamp((xyxy[1] - yPad) / gain, 0, h - 0); // unpad, clip tly + float xMax = Clamp((xyxy[2] - xPad) / gain, 0, w - 1); // unpad, clip brx + float yMax = Clamp((xyxy[3] - yPad) / gain, 0, h - 1); // unpad, clip bry + + YoloLabel label = _model.Labels[scores.IndexOf(mulConfidence)]; + + var prediction = new YoloPrediction(label, mulConfidence) + { + Rectangle = new RectangleF(xMin, yMin, xMax - xMin, yMax - yMin) + }; + + result.Add(prediction); + }); + }); + }); + }); + + return result.ToList(); + } + + private List ParseDetectyolo8(DenseTensor output, Image image) + { + var result = new ConcurrentBag(); + + var (w, h) = (image.Width, image.Height); // image w and h + var lenth = Math.Max(w, h); + var (xGain, yGain) = ( (float)lenth / _model.Width, (float)lenth / _model.Height); // x, y gains + var gain = Math.Max(xGain, yGain); // gain = resized / original + var (xPad, yPad) = ((_model.Width - w / gain) / 2, (_model.Height - h / gain) / 2); // left, right pads + + //for (int i = 0; i < 8400; i++) + //{ + // var index = 0; + // var score = output[0, 4, i]; + // for (int k = 5; k < _model.Dimensions; k++) + // { + // if (output[0, k, i] > score) + // { + // score = output[0, k, i]; + // index = k - 4; + // } + // } + + // if (score <= _model.MulConfidence) continue; // skip low mul_conf results + + // float x1 = (float)(output[0, 0, i] - 0.5 * output[0, 2, i]) / gain; + // float y1 = (float)(output[0, 1, i] - 0.5 * output[0, 3, i]) / gain; + // float width = output[0, 2, i] / gain; + // float height = output[0, 3, i] / gain; + + // YoloLabel label = _model.Labels[index]; + + // var prediction = new YoloPrediction(label, score) + // { + // Rectangle = new RectangleF(x1, y1, width, height) + // }; + + // result.Add(prediction); + //} + + Parallel.For(0, 8400, (i) => + { + + var index = 0; + var score = output[0, 4, i]; + for (int k = 5; k < _model.Dimensions; k++) + { + if (output[0, k, i] > score) + { + score = output[0, k, i]; + index = k - 4; + } + } + + if (score <= _model.MulConfidence) return; // skip low mul_conf results + + float x1 = (float)(output[0, 0, i] - 0.5 * output[0, 2, i]) - xPad; + float y1 = (float)(output[0, 1, i] - 0.5 * output[0, 3, i]) - yPad; + float width = output[0, 2, i] ; + float height = output[0, 3, i] ; + + x1 *= gain; + y1 *= gain; + width *= gain; + height *= gain; + + YoloLabel label = _model.Labels[index]; + + var prediction = new YoloPrediction(label, score) + { + Rectangle = new RectangleF(x1, y1, width, height) + }; + + result.Add(prediction); + }); + + return result.ToList(); + } + + /// + /// Parses net outputs (sigmoid or detect layer) to predictions. + /// + private List ParseOutput(DenseTensor[] output, Image image) + { + return _model.UseDetect ? ParseDetect(output[0], image) : ParseSigmoid(output, image); + } + private List ParseOutput(DenseTensor[] output, Mat image) + { + return _model.UseDetect ? ParseDetect(output[0], image) : ParseSigmoid(output, image); + } + private List ParseOutputyolo8(DenseTensor[] output, Image image) + { + return _model.UseDetect ? ParseDetectyolo8(output[0], image) : ParseSigmoid(output, image); + } + /// + /// Removes overlaped duplicates (nms). + /// + private List Supress(List items) + { + var result = new List(items); + + foreach (var item in items) // iterate every prediction + { + foreach (var current in result.ToList()) // make a copy for each iteration + { + if (current == item) continue; + + var (rect1, rect2) = (item.Rectangle, current.Rectangle); + + RectangleF intersection = RectangleF.Intersect(rect1, rect2); + + float intArea = intersection.Area(); // intersection area + float unionArea = rect1.Area() + rect2.Area() - intArea; // union area + float overlap = intArea / unionArea; // overlap ratio + + if (overlap >= _model.Overlap) + { + if (item.Score >= current.Score) + { + result.Remove(current); + } + } + } + } + + return result; + } + + /// + /// Runs object detection. + /// + public List Predict(Image image) + { + return Supress(ParseOutput(Inference(image), image)); + } + public List Predict(Mat image) + { + return Supress(ParseOutput(Inference(image), image)); + } + public List Predictyolo8(Image image) + { + return Supress(ParseOutputyolo8(Inference(image), image)); + } + /// + /// Creates new instance of YoloScorer. + /// + public YoloScorer() + { + _model = Activator.CreateInstance(); + } + + /// + /// Creates new instance of YoloScorer with weights path and options. + /// + public YoloScorer(string weights, SessionOptions opts = null) : this() + { + _inferenceSession = new InferenceSession(File.ReadAllBytes(weights), opts ?? new SessionOptions()); + } + + /// + /// Creates new instance of YoloScorer with weights stream and options. + /// + public YoloScorer(Stream weights, SessionOptions opts = null) : this() + { + using (var reader = new BinaryReader(weights)) + { + _inferenceSession = new InferenceSession(reader.ReadBytes((int)weights.Length), opts ?? new SessionOptions()); + } + } + + /// + /// Creates new instance of YoloScorer with weights bytes and options. + /// + public YoloScorer(byte[] weights, SessionOptions opts = null) : this() + { + _inferenceSession = new InferenceSession(weights, opts ?? new SessionOptions()); + } + + /// + /// Disposes YoloScorer instance. + /// + public void Dispose() + { + _inferenceSession.Dispose(); + } + + /// + /// 获取名称 + /// + /// + public List GetLabels() + { + if (_model != null) + { + return _model.Labels; + } + return null; + } + + } +} diff --git a/HMIcode/SmallProject/Yolov5Net.Scorer/Yolov5Net.Scorer.csproj b/HMIcode/SmallProject/Yolov5Net.Scorer/Yolov5Net.Scorer.csproj new file mode 100644 index 0000000..4f7c31e --- /dev/null +++ b/HMIcode/SmallProject/Yolov5Net.Scorer/Yolov5Net.Scorer.csproj @@ -0,0 +1,104 @@ + + + + Yolov5Net + 1.0.9.0 + 1.0.9.0 + net472 + Yolov5Net + YOLOv5 object detection with C#, ML.NET, ONNX. + MIT + c-sharp machine-learning object-detection yolo yolov5 ml-net onnx + https://github.com/mentalstack/yolov5-net + https://github.com/mentalstack/yolov5-net + en + © Mentalstack 2016-2021 + Mentalstack + 1.0.9 + Library + AnyCPU;x64 + + + + true + + + + true + + + + true + false + + + + true + false + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + +