diff --git a/ClumsyPilot/ClumsyPilot.csproj b/ClumsyPilot/ClumsyPilot.csproj index ceb2716..2553db3 100644 --- a/ClumsyPilot/ClumsyPilot.csproj +++ b/ClumsyPilot/ClumsyPilot.csproj @@ -21,9 +21,6 @@ - - ref\CommonUsage.dll - ref\LessokajiWeaverUtilities.dll @@ -39,6 +36,9 @@ ref\RefFundamentalLib.dll + + ..\ref\CommonUsage.dll + diff --git a/ClumsyPilot/build/Clumsy/ClumsyPilot.dll b/ClumsyPilot/build/Clumsy/ClumsyPilot.dll index 3d48544..bf4e181 100644 Binary files a/ClumsyPilot/build/Clumsy/ClumsyPilot.dll and b/ClumsyPilot/build/Clumsy/ClumsyPilot.dll differ diff --git a/ClumsyPilot/build/Clumsy/ClumsyPilot.pdb b/ClumsyPilot/build/Clumsy/ClumsyPilot.pdb index fc4ea1f..f62506f 100644 Binary files a/ClumsyPilot/build/Clumsy/ClumsyPilot.pdb and b/ClumsyPilot/build/Clumsy/ClumsyPilot.pdb differ diff --git a/ClumsyPilot/build/Clumsy/CommonUsage.dll b/ClumsyPilot/build/Clumsy/CommonUsage.dll index effb81e..cdb9124 100644 Binary files a/ClumsyPilot/build/Clumsy/CommonUsage.dll and b/ClumsyPilot/build/Clumsy/CommonUsage.dll differ diff --git a/ClumsyPilot/obj/Debug/ClumsyPilot.AssemblyInfo.cs b/ClumsyPilot/obj/Debug/ClumsyPilot.AssemblyInfo.cs index 1f11488..79a3c54 100644 --- a/ClumsyPilot/obj/Debug/ClumsyPilot.AssemblyInfo.cs +++ b/ClumsyPilot/obj/Debug/ClumsyPilot.AssemblyInfo.cs @@ -13,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("ClumsyPilot")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+e6b99c45b352f24ff58092f7855d0eadd8828d42")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] [assembly: System.Reflection.AssemblyProductAttribute("ClumsyPilot")] [assembly: System.Reflection.AssemblyTitleAttribute("ClumsyPilot")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/ClumsyPilot/obj/Debug/ClumsyPilot.AssemblyInfoInputs.cache b/ClumsyPilot/obj/Debug/ClumsyPilot.AssemblyInfoInputs.cache index 29381a2..8fb9fb2 100644 --- a/ClumsyPilot/obj/Debug/ClumsyPilot.AssemblyInfoInputs.cache +++ b/ClumsyPilot/obj/Debug/ClumsyPilot.AssemblyInfoInputs.cache @@ -1 +1 @@ -3e5507bf56e38facdc245d5ff407e181c060d8094f38b94ad743b8c3305523f2 +5d77794fa0720c6591db5b06ac60427413c18989a6f7b64420ccb07d122d85bc diff --git a/ClumsyPilot/obj/Debug/ClumsyPilot.GeneratedMSBuildEditorConfig.editorconfig b/ClumsyPilot/obj/Debug/ClumsyPilot.GeneratedMSBuildEditorConfig.editorconfig index f169771..a3aa451 100644 --- a/ClumsyPilot/obj/Debug/ClumsyPilot.GeneratedMSBuildEditorConfig.editorconfig +++ b/ClumsyPilot/obj/Debug/ClumsyPilot.GeneratedMSBuildEditorConfig.editorconfig @@ -1,6 +1,6 @@ is_global = true build_property.RootNamespace = MultiWheelC -build_property.ProjectDir = d:\MyParking\ClumsyPilot\ +build_property.ProjectDir = D:\Users\Desktop\入职培训\停车机器人\MyParking\ClumsyPilot\ build_property.EnableComHosting = build_property.EnableGeneratedComInterfaceComImportInterop = build_property.CsWinRTUseWindowsUIXamlProjections = false diff --git a/ClumsyPilot/obj/Debug/ClumsyPilot.assets.cache b/ClumsyPilot/obj/Debug/ClumsyPilot.assets.cache index 9dedb7e..3d28e6f 100644 Binary files a/ClumsyPilot/obj/Debug/ClumsyPilot.assets.cache and b/ClumsyPilot/obj/Debug/ClumsyPilot.assets.cache differ diff --git a/ClumsyPilot/obj/Debug/ClumsyPilot.csproj.AssemblyReference.cache b/ClumsyPilot/obj/Debug/ClumsyPilot.csproj.AssemblyReference.cache index 787d026..0de62a8 100644 Binary files a/ClumsyPilot/obj/Debug/ClumsyPilot.csproj.AssemblyReference.cache and b/ClumsyPilot/obj/Debug/ClumsyPilot.csproj.AssemblyReference.cache differ diff --git a/ClumsyPilot/obj/Debug/ClumsyPilot.csproj.CoreCompileInputs.cache b/ClumsyPilot/obj/Debug/ClumsyPilot.csproj.CoreCompileInputs.cache index 7c7bf92..d102794 100644 --- a/ClumsyPilot/obj/Debug/ClumsyPilot.csproj.CoreCompileInputs.cache +++ b/ClumsyPilot/obj/Debug/ClumsyPilot.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -e5afef3021b83202643b31f468b83ac62f224cd367fe52dc0a1cbb98570c91e1 +e972a413d047c4137a8ce86cbff54a8d2e2558806d9d974d3d6312467ee8ba4d diff --git a/ClumsyPilot/obj/Debug/ClumsyPilot.dll b/ClumsyPilot/obj/Debug/ClumsyPilot.dll index 8837123..bf4e181 100644 Binary files a/ClumsyPilot/obj/Debug/ClumsyPilot.dll and b/ClumsyPilot/obj/Debug/ClumsyPilot.dll differ diff --git a/ClumsyPilot/obj/Debug/ClumsyPilot.pdb b/ClumsyPilot/obj/Debug/ClumsyPilot.pdb index ab65ced..f62506f 100644 Binary files a/ClumsyPilot/obj/Debug/ClumsyPilot.pdb and b/ClumsyPilot/obj/Debug/ClumsyPilot.pdb differ diff --git a/CommonUsage-MultiVehicleSync/commonusage/.gitattributes b/CommonUsage-MultiVehicleSync/commonusage/.gitattributes new file mode 100644 index 0000000..c6c8654 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/.gitattributes @@ -0,0 +1,2 @@ +*.cs text eol=crlf +.gitattributes text eol=lf diff --git a/CommonUsage-MultiVehicleSync/commonusage/.gitignore b/CommonUsage-MultiVehicleSync/commonusage/.gitignore new file mode 100644 index 0000000..1ee5385 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/.gitignore @@ -0,0 +1,362 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd diff --git a/CommonUsage-MultiVehicleSync/commonusage/Chassis/AbstractChassis.cs b/CommonUsage-MultiVehicleSync/commonusage/Chassis/AbstractChassis.cs new file mode 100644 index 0000000..41b0e23 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Chassis/AbstractChassis.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Numerics; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using FundamentalLib; +using Newtonsoft.Json; + +namespace CommonUsage.Chassis +{ + public abstract class AbstractChassis + { + protected AbstractChassis() + { + Valid = false; + } + + public abstract void Initialize(); + + public abstract void Visualize(); + + public abstract void AfterDirectionChanged(); + + /// + /// 当前行进方向。 + /// + [Obsolete] + public float DirectionAngle + { + get => _originBiasTh; + set + { + _originBiasTh = value; + if (_originBiasTh != _lastDirectionAngle) AfterDirectionChanged(); + _lastDirectionAngle = _originBiasTh; + } + } + + protected float _originBiasX = 0f, _originBiasY = 0f, _originBiasTh; + + public Vector3 GetOriginBias() + { + return new Vector3(_originBiasX, _originBiasY, _originBiasTh); + } + + public class CarSpeed + { + public float Vx,Vy,Vw; + } + + public abstract CarSpeed GetCarSpeed(bool isActual = false); + public List GetGeometricControlPoints() + { + return GeometricControlPoints; + } + + public void ComputeWheelsGeometrically(float speed) + { + // 打印调用位置信息 + var stackTrace = new StackTrace(true); + var callerFrame = stackTrace.GetFrame(1); // 获取调用者的帧 + if (callerFrame != null) + { + var fileName = callerFrame.GetFileName(); + var lineNumber = callerFrame.GetFileLineNumber(); + DLog.Log($"s:{speed:0.000} from {fileName} ln.{lineNumber}", $"WheelComputeCaller"); + } + + DefineGeometricWheelComputation(speed); + } + + protected abstract void DefineGeometricWheelComputation(float speed); + + public void DriveStop() + { + PredefinedDriveStop(); + CustomDriveStop?.Invoke(); + } + + public abstract void PredefinedDriveStop(); + + public Action CustomDriveStop; + + public abstract bool ComputeRotateWheels(float rotSpeed); + + public abstract float CalculateTurningSpeedDecayFac(float turn); + + public enum ChassisState + { + Standby, + Running, + AbnormalFeedback, + ExceedMotionAbility, + } + + protected ChassisState State; + + protected string StateDescription; + + public (ChassisState State, string Description) GetChassisState() + { + return (State, StateDescription); + } + + public bool Debug = false; + public float AccPerSecond = 0.2f; + public float DeAccPerSecond = 0.2f; + public float MaxSpeed = 1; // m/s + public float MinTurnSpeedFac = 0.5f; + public float MaxTurnThreshold = 90f; + + public float GcpThetaPerSecond = 10f; + + public DateTime LastMoveTime = DateTime.MinValue; + + protected bool Valid = false; + protected List GeometricControlPoints = new(); + protected bool RotatingActive = false; + protected bool GoingActive = false; + protected bool GoingWheelAligned = false; + + private float _lastDirectionAngle = 0; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Chassis/DiffSteerWheel.cs b/CommonUsage-MultiVehicleSync/commonusage/Chassis/DiffSteerWheel.cs new file mode 100644 index 0000000..4f179ae --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Chassis/DiffSteerWheel.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace CommonUsage.Chassis +{ + public class DiffSteerWheel:SteerWheel + { + public DiffSteerWheel(float wheelDistance,Vector2 position, float angleLowerLimit, float angleUpperLimit, Action speedWriter, + Func speedReader, Action angleWriter, Func angleReader, Action leftSpeedWriter, Action rightSpeedWriter, + float angleLimitMarginDeg = 15f) : base(position, + angleLowerLimit, angleUpperLimit, speedWriter, speedReader, angleWriter, angleReader, angleLimitMarginDeg) + { + _leftSpeedWriter = leftSpeedWriter; + _rightSpeedWriter = rightSpeedWriter; + WheelDistance = wheelDistance; + } + + public float GetLeftSendSpeed() + { + return _leftSendSpeed; + } + + public float GetRightSendSpeed() + { + return _rightSendSpeed; + } + + public void WriteLeftSpeed(float speed) + { + _leftSpeedWriter(_leftSendSpeed = speed); + } + + public void WriteRightSpeed(float speed) + { + _rightSpeedWriter(_rightSendSpeed = speed); + } + + public float WheelDistance; + private readonly Action _leftSpeedWriter; + private readonly Action _rightSpeedWriter; + + private float _leftSendSpeed; + private float _rightSendSpeed; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Chassis/DifferentialChassis.cs b/CommonUsage-MultiVehicleSync/commonusage/Chassis/DifferentialChassis.cs new file mode 100644 index 0000000..e6a7279 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Chassis/DifferentialChassis.cs @@ -0,0 +1,170 @@ +using FundamentalLib; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; +using System.Diagnostics; + +namespace CommonUsage.Chassis +{ + public class DifferentialChassis : AbstractChassis + { + public void SetLeftRightWheels(Wheel wheelL, Wheel wheelR) + { + _leftWheel = wheelL; + _rightWheel = wheelR; + _halfWheelTrack = Math.Abs(_leftWheel.Position.Y); + } + + public override void Visualize() + { + + } + + public override CarSpeed GetCarSpeed(bool isActual = false) + { + if (!isActual) + { + return new CarSpeed() + { + Vx = (_speedL + _speedR) / 2f, + Vw = (_speedR - _speedL) / Math.Abs(_leftWheel.Position.Y - _rightWheel.Position.Y) / + (float)Math.PI * 180f * 1000f, + Vy = 0 + }; + } + else + { + return new CarSpeed() + { + Vx = GetLinearSpeed(), + Vw = (_rightWheel.ReadSpeed() - _leftWheel.ReadSpeed()) / + Math.Abs(_leftWheel.Position.Y - _rightWheel.Position.Y) / + (float)Math.PI * 180f * 1000f, + Vy = 0 + }; + } + + } + + public (Wheel,Wheel) GetWheels() + { + return (_leftWheel, _rightWheel); + } + + public float GetLinearSpeed() + { + return (_leftWheel.ReadSpeed() + _rightWheel.ReadSpeed()) / 2f; + } + + public override void Initialize() + { + GeometricControlPoints.Add(new GeometricControlPoint(new Vector2(0, 0))); + Valid = true; + } + + public override void AfterDirectionChanged() + { + + } + + public override void PredefinedDriveStop() + { + if (!Valid) return; + _sendSpeedL = _sendSpeedR = 0; + _speedL = _speedR = 0; + _leftWheel.WriteSpeed(_sendSpeedL); + _rightWheel.WriteSpeed(_sendSpeedR); + GoingActive = false; + RotatingActive = false; + } + + protected override void DefineGeometricWheelComputation(float speed) + { + var now = DateTime.Now; + if (!GoingActive) LastMoveTime = now; + + SendSpeed(speed, GeometricControlPoints[0].Theta, now - LastMoveTime); + + GoingActive = true; + RotatingActive = false; + } + + public override bool ComputeRotateWheels(float rotSpeed) + { + if (!RotatingActive) LastMoveTime = DateTime.Now; + + SendSpeed(0, rotSpeed); + + GoingActive = false; + RotatingActive = true; + return true; + } + + public override float CalculateTurningSpeedDecayFac(float turn) + { + return 1 - Math.Min(turn, MaxTurnThreshold) / MaxTurnThreshold * MinTurnSpeedFac; + } + + public void SendSpeed(float linearSpeed, float angularSpeed, TimeSpan? deltaTime = null) + { + var edgeLinearSpeed = (float)(angularSpeed / 180f * Math.PI * _halfWheelTrack / 1000); + var vl = linearSpeed - edgeLinearSpeed; + var vr = linearSpeed + edgeLinearSpeed; + _speedL = vl; + _speedR = vr; + AccumulateSpeed(vl, vr, deltaTime); + LastMoveTime = DateTime.Now; + } + + private void AccumulateSpeed(float vl, float vr, TimeSpan? deltaTime = null) + { + // var dTime = (float)(deltaTime ?? DateTime.Now - LastMoveTime).TotalSeconds; + // + // var speedSignL = Math.Sign(vl - _sendSpeedL); + // var accL = Math.Abs(vl) > Math.Abs(_sendSpeedL) ? AccPerSecond : DeAccPerSecond; + // _sendSpeedL += speedSignL * Math.Min(Math.Abs(vl - _sendSpeedL), accL * dTime); + // _leftWheel.WriteSpeed(_sendSpeedL); + // + // var speedSignR = Math.Sign(vr - _sendSpeedR); + // var accR = Math.Abs(vr) > Math.Abs(_sendSpeedR) ? AccPerSecond : DeAccPerSecond; + // _sendSpeedR += speedSignR * Math.Min(Math.Abs(vr - _sendSpeedR), accR * dTime); + // _rightWheel.WriteSpeed(_sendSpeedR); + + // if (Debug) + // Console.WriteLine($"DiffChassis, target:{v:0.00},send:{_sendSpeed:0.0}"); + // var dTime = (float)(deltaTime ?? DateTime.Now - LastMoveTime).TotalSeconds; + var dTime = (float)(deltaTime ?? DateTime.Now - LastMoveTime).TotalSeconds; + float diffL = vl - _sendSpeedL; + float diffR = vr - _sendSpeedR; + + float accL = Math.Abs(vl) > Math.Abs(_sendSpeedL) ? AccPerSecond : DeAccPerSecond; + float accR = Math.Abs(vr) > Math.Abs(_sendSpeedR) ? AccPerSecond : DeAccPerSecond; + + float maxDeltaL = accL * dTime; + float maxDeltaR = accR * dTime; + + float factorL = Math.Abs(diffL) > maxDeltaL ? maxDeltaL / Math.Abs(diffL) : 1.0f; + float factorR = Math.Abs(diffR) > maxDeltaR ? maxDeltaR / Math.Abs(diffR) : 1.0f; + + float factor = Math.Min(factorL, factorR); + + _sendSpeedL += diffL * factor; + _sendSpeedR += diffR * factor; + + _leftWheel.WriteSpeed(_sendSpeedL); + _rightWheel.WriteSpeed(_sendSpeedR); + // Console.WriteLine($"DiffChassis, target:{vl:0.00},send:{_sendSpeedL:0.00} dTime:{dTime} diffL:{diffL} factor:{factor}" ); + } + + private Wheel _leftWheel; + private Wheel _rightWheel; + private float _halfWheelTrack; // millimeter + + private float _sendSpeedL; + private float _sendSpeedR; + private int _direction = 1; // 1 forward, -1 backward + private float _speedL; + private float _speedR; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Chassis/MultiWheelChassis.cs b/CommonUsage-MultiVehicleSync/commonusage/Chassis/MultiWheelChassis.cs new file mode 100644 index 0000000..fa96e28 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Chassis/MultiWheelChassis.cs @@ -0,0 +1,1083 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Diagnostics; +using System.Text; +using CommonUsage.Mathematics; +using FundamentalLib; +using System.Drawing; + +namespace CommonUsage.Chassis +{ + public class MultiWheelChassis : AbstractChassis + { + public float ThConsistentThreshold = 1.0f; + + public float ControlPointRadius = 500f; + + // 原地旋转纠偏钳位:SendRotateMotion 中每轮纠偏速度幅值不超过 该比例×本轮旋转切向速度, + // 防止减速末段旋转切向变小时纠偏盖过它、使轮向矢量乱摆(频繁打方向/卡死)。<0 关闭钳位。 + public float RotateCompTangentFrac = 0.5f; + + // 上一次 SendRotateMotion 的舵轮对齐状态:false 表示舵轮未追上目标角(gate=0、车未真正转动)。 + // 供上层做积分抗饱和(卡死时冻结积分)。 + public bool LastRotateAligned { get; private set; } = true; + + public string LastMotionDecomposeFailureReason { get; private set; } = ""; + + // 为true时,在两个等价舵角都可用的情况下优先选择机械转动距离更小的方案。 + // 默认关闭以保持旧项目原有行为,由需要该策略的车型适配器主动开启。 + public bool PreferMinimumSteeringTravel { get; set; } + + public float MinimumTurningAngleForAckermann = 60f; + + + public MultiWheelChassis() : base() + { + + } + + public void AddWheel(SteerWheel wheel) + { + _steerWheels.Add(wheel); + } + + public override void Initialize() + { + _steerWheels = _steerWheels.OrderByDescending(sw => sw.Position.X).ToList(); + _targetSpeeds = new float[_steerWheels.Count].ToList(); + _sendSpeeds = new float[_steerWheels.Count].ToList(); + _tmpSpeeds = new float[_steerWheels.Count].ToList(); + _wheelDirs = Enumerable.Repeat(1, _steerWheels.Count).ToList(); + _sendAngle = new float[_steerWheels.Count].ToList(); + _debugSpeeds = new float[_steerWheels.Count].ToList(); + + if (_steerWheels.Count < 2) + { + Console.WriteLine($"steer wheel num: {_steerWheels.Count}. invalid!"); + Valid = false; + return; + } + + CalculateAxes(); + Valid = true; + } + + public Visualizer MainVisualizer = null; + + public override void Visualize() + { + MainVisualizer?.Clear(); + + if (_rotCenter != Vector2.Zero) + { + MainVisualizer?.DrawText(Color.Chartreuse, $"({_rotCenter.X:f1}, {_rotCenter.Y:f2})", _rotCenter); + } + + for (var i = 0; i < _steerWheels.Count; ++i) + { + var sw = _steerWheels[i]; + if (sw is DiffSteerWheel dsw) + { + var pointing = CommonMath.Transform2D(dsw.Position, dsw.ZeroDirection, new Vector2(200, 0)); + MainVisualizer?.DrawLine(Color.Gray, dsw.Position, pointing); + MainVisualizer?.DrawText(Color.Gray, $"{dsw.GetSendSpeed():f2} {sw.GetSendAngle():f2}", dsw.Position); + + var left = CommonMath.Transform2D(dsw.Position, dsw.GetAngleRelativeToChassis() + 90, new Vector2(dsw.WheelDistance, 0)); + MainVisualizer?.DrawLine(Color.GreenYellow, left, + CommonMath.Transform2D(left, dsw.GetAngleRelativeToChassis(), new Vector2(200, 0)), endArrow: true); + MainVisualizer?.DrawText(Color.GreenYellow, $"{dsw.GetLeftSendSpeed():0.00}", left); + + var right = CommonMath.Transform2D(dsw.Position, dsw.GetAngleRelativeToChassis() - 90, new Vector2(dsw.WheelDistance, 0)); + MainVisualizer?.DrawLine(Color.DeepPink, right, + CommonMath.Transform2D(right, dsw.GetAngleRelativeToChassis(), new Vector2(200, 0)), endArrow: true); + MainVisualizer?.DrawText(Color.DeepPink, $"{dsw.GetRightSendSpeed():0.00}", right); + } + else + { + MainVisualizer?.DrawLine(Color.Green, sw.Position, + CommonMath.Transform2D(sw.Position, sw.GetAngleRelativeToChassis(), new Vector2(200, 0)), endArrow: true); + MainVisualizer?.DrawText(Color.Chartreuse, $"{sw.GetSendSpeed():f2} {sw.GetAngleRelativeToChassis():f2}", sw.Position); + } + + if (_rotCenter != Vector2.Zero) + { + MainVisualizer?.DrawLine(Color.DodgerBlue, sw.Position, _rotCenter); + } + } + } + + public override void AfterDirectionChanged() + { + Hedingben.ToastText($"AfterDirectionChanged", "MultiWheelChassis-AfterDirectionChanged"); + + foreach (var sw in _steerWheels) + { + sw.Position = CommonMath.Transform2D(new Vector2(_originBiasX, _originBiasY), _originBiasTh, + sw.PhysicalPosition); + sw.ZeroDirection = _originBiasTh; + } + + CalculateAxes(); + GoingWheelAligned = false; + } + + protected override void DefineGeometricWheelComputation(float speed) + { + if (!GoingActive) ResetMotionState(); + var targetGcpTheta0 = GeometricControlPoints[0].Theta; + var targetGcpTheta1 = GeometricControlPoints[1].Theta; + var lastGcpTheta0Before = _lastGcpTheta0; + var lastGcpTheta1Before = _lastGcpTheta1; + + void Process(ref float current, float target, int id) + { + current += Math.Sign(target - current) * Math.Min(Math.Abs(target - current), + GcpThetaPerSecond * (float)(DateTime.Now - LastMoveTime).TotalSeconds); + // Hedingben.ToastText($"current:{current:0.00} target:{target:0.00}", $"gcp-accumulation-{id}"); + } + + Process(ref _lastGcpTheta0, GeometricControlPoints[0].Theta, 0); + Process(ref _lastGcpTheta1, GeometricControlPoints[1].Theta, 1); + if ((DateTime.Now - _geometricComputeLastLog).TotalMilliseconds >= 200) + { + _geometricComputeLastLog = DateTime.Now; + DLog.Log( + $"ComputeWheelsGeometrically speed:{speed:F3} " + + $"targetGcp:({targetGcpTheta0:F1},{targetGcpTheta1:F1}) " + + $"smoothBefore:({lastGcpTheta0Before:F1},{lastGcpTheta1Before:F1}) " + + $"smoothAfter:({_lastGcpTheta0:F1},{_lastGcpTheta1:F1}) " + + $"originBias:({_originBiasX:F1},{_originBiasY:F1},{_originBiasTh:F1}) goingActive:{GoingActive}", + "MultiWheelGoDiag"); + } + SendMotion(speed, _lastGcpTheta0, _lastGcpTheta1); + + GoingActive = true; + RotatingActive = false; + XYThActive = false; + } + + private float _lastGcpTheta0; + + private float _lastGcpTheta1; + + /// + /// 转速单位为度/s,逆时针为正。 + /// + /// + public override bool ComputeRotateWheels(float rotSpeed) + { + if (!RotatingActive) ResetMotionState(); + + var feasible = SendRotateMotion(rotSpeed); + + GoingActive = false; + RotatingActive = true; + XYThActive = false; + return feasible; + } + + public override void PredefinedDriveStop() + { + if (!Valid) return; + for (var i = 0; i < _steerWheels.Count; i++) + { + _sendSpeeds[i] = 0; + _debugSpeeds[i] = 0; + if (_steerWheels[i] is DiffSteerWheel dfw) + { + dfw.WriteLeftSpeed(0); + dfw.WriteRightSpeed(0); + } + else + { + _steerWheels[i].WriteSpeed(0); + } + } + GoingActive = false; + RotatingActive = false; + XYThActive = false; + } + + public void RampStop(TimeSpan? deltaTime = null) + { + if (!Valid) return; + + for (var i = 0; i < _steerWheels.Count; i++) + { + _debugSpeeds[i] = 0; + AccumulateSpeed(i, 0, false, Vector2.Zero, deltaTime); + } + + GoingActive = false; + RotatingActive = false; + XYThActive = false; + LastMoveTime = DateTime.Now; + } + + private struct WheelAngleCandidate + { + public bool Valid; + public float RawAngle; + public float NormalizedAngle; + public float Margin; + public int Direction; + public float Lower; + public float Upper; + } + + private WheelAngleCandidate BuildWheelAngleCandidate(SteerWheel sw, float rawAngle, int direction) + { + var lower = (float)CommonMath.RoundTh(sw.AngleLowerLimit); + var upper = (float)CommonMath.RoundTh(sw.AngleUpperLimit); + while (upper < lower) upper += 360; + + var normalized = (float)CommonMath.RoundTh(rawAngle); + while (normalized < lower) normalized += 360; + while (normalized > upper && normalized - 360 >= lower) normalized -= 360; + + var margin = Math.Min(normalized - lower, upper - normalized); + var marginRequired = Math.Max(0, sw.AngleLimitMarginDeg); + return new WheelAngleCandidate + { + Valid = normalized >= lower && normalized <= upper && margin >= marginRequired, + RawAngle = rawAngle, + NormalizedAngle = normalized, + Margin = margin, + Direction = direction, + Lower = lower, + Upper = upper + }; + } + + private bool TryResolveWheelAngle(int wheelIndex, float targetAngle, string entry, + out float angle, out int direction, out string reason) + { + var sw = _steerWheels[wheelIndex]; + var forward = BuildWheelAngleCandidate(sw, targetAngle, 1); + var reverse = BuildWheelAngleCandidate(sw, targetAngle + 180, -1); + + WheelAngleCandidate? selected = null; + + if (PreferMinimumSteeringTravel && + forward.Valid && + reverse.Valid) + { + var actualAngle = sw.ReadAngle(); + + if (!float.IsNaN(actualAngle) && + !float.IsInfinity(actualAngle)) + { + // 将反馈角映射到与候选角相同的机械限位区间。 + // 这里比较的是受限机械舵角,不能使用圆周最短角度差。 + var actual = BuildWheelAngleCandidate( + sw, + actualAngle, + 1); + var forwardError = Math.Abs( + forward.NormalizedAngle - + actual.NormalizedAngle); + var reverseError = Math.Abs( + reverse.NormalizedAngle - + actual.NormalizedAngle); + + // 迟滞避免两个方案转动量接近时频繁翻转轮速方向。 + const float switchHysteresisDegrees = 5f; + + if (reverseError + + switchHysteresisDegrees < + forwardError) + { + selected = reverse; + } + else if (forwardError + + switchHysteresisDegrees < + reverseError) + { + selected = forward; + } + } + } + + // 未启用最小转舵策略、反馈无效或两个候选差异落在迟滞区时, + // 保持旧版轮速方向,避免破坏其他车型的既有行为。 + if (_wheelDirs != null && wheelIndex < _wheelDirs.Count) + { + if (selected == null && + _wheelDirs[wheelIndex] < 0 && + reverse.Valid) + { + selected = reverse; + } + else if (selected == null && + _wheelDirs[wheelIndex] >= 0 && + forward.Valid) + { + selected = forward; + } + } + + if (selected == null) + { + if (forward.Valid) selected = forward; + else if (reverse.Valid) selected = reverse; + } + + if (selected != null) + { + angle = selected.Value.NormalizedAngle; + direction = selected.Value.Direction; + reason = ""; + return true; + } + + angle = 0; + direction = 0; + var marginRequired = Math.Max(0, sw.AngleLimitMarginDeg); + reason = + $"{entry} infeasible wheel={wheelIndex} target={CommonMath.RoundTh(targetAngle):F1} " + + $"reverse={CommonMath.RoundTh(targetAngle + 180):F1} " + + $"limit=[{forward.Lower:F1},{forward.Upper:F1}] requiredMargin={marginRequired:F1} " + + $"forward(norm={forward.NormalizedAngle:F1},margin={forward.Margin:F1},valid={forward.Valid}) " + + $"reverse(norm={reverse.NormalizedAngle:F1},margin={reverse.Margin:F1},valid={reverse.Valid})"; + return false; + } + + private bool FailMotionDecomposition(string entry, string reason, TimeSpan? deltaTime) + { + LastMotionDecomposeFailureReason = $"{entry}: {reason}"; + Console.WriteLine(LastMotionDecomposeFailureReason); + DLog.Log(LastMotionDecomposeFailureReason, "MultiWheelMotionDecompose"); + Hedingben.ToastText($"Motion decompose failed: {reason}", "MultiWheelChassis-decompose"); + RampStop(deltaTime); + return false; + } + + private Vector2 _rotCenter; + + public Vector2 GetRotCenter() + { + return _rotCenter; + } + + public void SetOriginBias(float x, float y, float th) + { + _originBiasX = x; + _originBiasY = y; + _originBiasTh = th; + + foreach (var sw in _steerWheels) + { + sw.Position = CommonMath.Transform2D(new Vector2(_originBiasX, _originBiasY), _originBiasTh, + sw.PhysicalPosition); + sw.ZeroDirection = _originBiasTh; + } + + CalculateAxes(); + } + + public Visualizer SendMotionVisualizer = null; + + /// + /// frontTh和rearTh是基于当前行进方向,前后轴应该打的角度。 + /// + /// 单位m/s。 + /// 角度制。基于当前行进方向。 + /// 角度制。基于当前行进方向。 + /// 上次下发SendMotion到本次的间隔时间。 + /// 用于多车联动,大于0时生效。 + /// 用于多车联动,localControlRadius大于0时生效。 + /// 用于多车联动,localControlRadius大于0时生效。 + /// 用于多车联动,localControlRadius大于0时生效。 + /// + public bool SendMotion(float speed, float frontTh, float rearTh, TimeSpan? deltaTime = null, + float localControlRadius = 0, float localCompensateX = 0f, float localCompensateY = 0, float localCompensateTh = 0) + { + if (!Valid) return FailMotionDecomposition("SendMotion", "invalid chassis", deltaTime); + LastMotionDecomposeFailureReason = ""; + + var compInputSpeed = speed; + var compInputFrontTh = frontTh; + var compInputRearTh = rearTh; + var compMotionTh = 0f; + var compAlong = 0f; + var compSide = 0f; + var compDSpeed = 0f; + var compDTh = 0f; + + if (localControlRadius > 0) + { + var motionTh = AverageAngle(frontTh, rearTh); + var radMotionTh = motionTh / 180f * Math.PI; + var alongComp = (float)(localCompensateX * Math.Cos(radMotionTh) + + localCompensateY * Math.Sin(radMotionTh)); + var sideComp = (float)(-localCompensateX * Math.Sin(radMotionTh) + + localCompensateY * Math.Cos(radMotionTh)); + var dSpeed = alongComp / 1000f; + speed += dSpeed; + + var speedSign = Math.Abs(compInputSpeed) > 1e-4f ? Math.Sign(compInputSpeed) : 1; + var steerBaseSpeed = Math.Max(Math.Abs(speed), 0.05f); + var dTh = (float)(Math.Atan2(sideComp / 1000f, steerBaseSpeed) / Math.PI * 180f) * speedSign; + compMotionTh = motionTh; + compAlong = alongComp; + compSide = sideComp; + compDSpeed = dSpeed; + compDTh = dTh; + Hedingben.ToastText($"s:{compInputSpeed:F2} d:{dSpeed:F3}", "MultiWheelChassis-dSpeed"); + Hedingben.ToastText($"F:{frontTh:F1} R:{rearTh:F1} d:{dTh:F1}", "MultiWheelChassis-dTh"); + frontTh += dTh; + rearTh += dTh; + + if (Math.Abs(localCompensateX) > 1e-3f || Math.Abs(localCompensateY) > 1e-3f || + Math.Abs(localCompensateTh) > 1e-3f) + DLog.Log( + $"SendMotionComp in speed:{compInputSpeed:F3} fTh:{compInputFrontTh:F1} rTh:{compInputRearTh:F1} " + + $"motionTh:{motionTh:F1} biasTh:{_originBiasTh:F1} comp({localCompensateX:F1},{localCompensateY:F1},{localCompensateTh:F2}) " + + $"along:{alongComp:F1} side:{sideComp:F1} dSpeed:{dSpeed:F3} dTh:{dTh:F1} " + + $"out speed:{speed:F3} fTh:{frontTh:F1} rTh:{rearTh:F1}", + "MultiWheelMotionComp"); + } + + float scaleFactor = 1.0f; + float currentFrontTh = frontTh; + float currentRearTh = rearTh; + bool needAdjust = true; + const float minScaleFactor = 0.1f; + var finalAngles = new List(); + + var axisDiffFlag = false; + var myRotCenter = new Vector2(); + var lastInvalidReason = ""; + + float AverageAngle(float left, float right) + { + var leftRad = left / 180f * Math.PI; + var rightRad = right / 180f * Math.PI; + var x = Math.Cos(leftRad) + Math.Cos(rightRad); + var y = Math.Sin(leftRad) + Math.Sin(rightRad); + if (Math.Abs(x) < 1e-6 && Math.Abs(y) < 1e-6) + return (float)CommonMath.RoundTh(left); + return (float)CommonMath.RoundTh(Math.Atan2(y, x) / Math.PI * 180f); + } + + float Interpolate(float left, float current, float right) + { + if (current <= left) return 0; + if (current >= right) return 1; + return (current - left) / (right - left); + } + + while (needAdjust && scaleFactor > minScaleFactor) + { + needAdjust = false; + var tmpAngles = new List(); + + // ... 现有的插值函数和旋转中心计算代码 ... + var thDiff = CommonMath.ThDiff(currentFrontTh, currentRearTh); + axisDiffFlag = Math.Abs(thDiff) > ThConsistentThreshold; + + var sign = -1; + if (axisDiffFlag) + { + // Vector2 pFront = new(_frontBase, 0), pRear = new(_rearBase, 0), + Vector2 pFront = new(ControlPointRadius, 0), pRear = new(-ControlPointRadius, 0), + normFront = CommonMath.Transform2D(pFront, currentFrontTh + 90, Vector2.UnitX), + normRear = CommonMath.Transform2D(pRear, currentRearTh + 90, Vector2.UnitX); + var (intersect, center) = + CommonMath.TwoLinesIntersection(pFront, normFront, pRear, normRear); + if (!intersect) + return FailMotionDecomposition("SendMotion", + $"front/rear axes do not intersect frontTh={currentFrontTh:F1} rearTh={currentRearTh:F1}", + deltaTime); + myRotCenter = center; + sign = myRotCenter.Y > 1 ? 1 : -1; + + if (localControlRadius > 0 && localCompensateTh != 0) + { + void VisPointing(Color color, Vector2 point, float th, int width) + { + var localFrontPointing = CommonMath.Transform2D(point, th, new Vector2(200, 0)); + SendMotionVisualizer?.DrawLine(color, point, localFrontPointing, width: width); + } + + var localPFront = CommonMath.Transform2D(new Vector2(_originBiasX, _originBiasY), _originBiasTh, + new Vector2(localControlRadius, 0)); + var localPRear = CommonMath.Transform2D(new Vector2(_originBiasX, _originBiasY), _originBiasTh, + new Vector2(-localControlRadius, 0)); + // SendMotionVisualizer?.DrawLine(Color.DarkRed, myRotCenter, localPFront); + // SendMotionVisualizer?.DrawLine(Color.DarkRed, myRotCenter, localPRear); + + // 计算_rotCenter在"localPRear指向localPFront的向量"的左侧还是右侧 + // 使用叉积判断:叉积 > 0 表示在左侧,< 0 表示在右侧 + var vecRearToFront = new Vector2(localPFront.X - localPRear.X, localPFront.Y - localPRear.Y); + var vecRearToCenter = new Vector2(myRotCenter.X - localPRear.X, myRotCenter.Y - localPRear.Y); + var crossProduct = vecRearToFront.X * vecRearToCenter.Y - vecRearToFront.Y * vecRearToCenter.X; + var side = crossProduct > 0 ? 1 : -1; + + var localFrontTh = + (float)(Math.Atan2(localPFront.Y - myRotCenter.Y, localPFront.X - myRotCenter.X) / Math.PI * + 180) + 90 * side; + var localRearTh = + (float)(Math.Atan2(localPRear.Y - myRotCenter.Y, localPRear.X - myRotCenter.X) / Math.PI * + 180) + 90 * side; + var sameDirectionError = + Math.Abs(CommonMath.ThDiff(localFrontTh, currentFrontTh)) + + Math.Abs(CommonMath.ThDiff(localRearTh, currentRearTh)); + var reverseDirectionError = + Math.Abs(CommonMath.ThDiff(localFrontTh + 180, currentFrontTh)) + + Math.Abs(CommonMath.ThDiff(localRearTh + 180, currentRearTh)); + if (reverseDirectionError < sameDirectionError) + { + localFrontTh = (float)CommonMath.RoundTh(localFrontTh + 180); + localRearTh = (float)CommonMath.RoundTh(localRearTh + 180); + } + // VisPointing(Color.DarkRed, localPFront, localFrontTh, 1); + // VisPointing(Color.DarkRed, localPRear, localRearTh, 1); + + var newLocalFrontTh = localFrontTh + localCompensateTh * Math.Sign(speed); + var newLocalRearTh = localRearTh - localCompensateTh * Math.Sign(speed); + // VisPointing(Color.Red, localPFront, newLocalFrontTh, 2); + // VisPointing(Color.Red, localPRear, newLocalRearTh, 2); + + var localThDiff = CommonMath.ThDiff(newLocalFrontTh, newLocalRearTh); + axisDiffFlag = Math.Abs(localThDiff) > ThConsistentThreshold; + + if (axisDiffFlag) + { + var localNormFront = CommonMath.Transform2D(localPFront, newLocalFrontTh + 90, Vector2.UnitX); + var localNormRear = CommonMath.Transform2D(localPRear, newLocalRearTh + 90, Vector2.UnitX); + var (localIntersect, localCenter) = + CommonMath.TwoLinesIntersection(localPFront, localNormFront, localPRear, localNormRear); + if (!localIntersect) + return FailMotionDecomposition("SendMotion", + $"compensated axes do not intersect frontTh={newLocalFrontTh:F1} rearTh={newLocalRearTh:F1}", + deltaTime); + myRotCenter = localCenter; + sign = myRotCenter.Y > 1 ? 1 : -1; + + // SendMotionVisualizer?.DrawLine(Color.Red, myRotCenter, localPFront, width: 2); + // SendMotionVisualizer?.DrawLine(Color.Red, myRotCenter, localPRear, width: 2); + } + else + { + currentFrontTh = newLocalFrontTh; + currentRearTh = newLocalRearTh; + thDiff = CommonMath.ThDiff(currentFrontTh, currentRearTh); + } + } + } + + _rotCenter = myRotCenter; + Hedingben.ToastText($"fTh:{frontTh:0.0}, rTh:{rearTh:0.0}, DA:{_originBiasTh:0.00}, " + + $"rc:({_rotCenter.X:0.0},{_rotCenter.Y:0.0}), _rb:{_rearBase}, _fb:{_frontBase}", "rotCenter"); + var r0 = _rotCenter.Length(); + + // 计算所有轮子的角度 + bool anyWheelInvalid = false; + for (var i = 0; i < _steerWheels.Count; i++) + { + var sw = _steerWheels[i]; + float th; + + if (axisDiffFlag) + { + var sTh = (float)(Math.Atan2(sw.Position.Y - _rotCenter.Y, sw.Position.X - _rotCenter.X) / + Math.PI * 180) + sign * 90; + th = CommonMath.ThDiff(sTh, sw.ZeroDirection); + var r1 = Vector2.Distance(sw.Position, _rotCenter); + _tmpSpeeds[i] = r1 / (r0 + 0.00001f); + } + else + { + var axisTh = currentRearTh + thDiff * Interpolate(_rearBase, _wheelBases[i], _frontBase); + _tmpSpeeds[i] = 1; + th = CommonMath.ThDiff(axisTh, sw.ZeroDirection); + } + + if (!TryResolveWheelAngle(i, th, "SendMotion", out var resolvedTh, out var resolvedDir, + out var resolveReason)) + { + anyWheelInvalid = true; + needAdjust = true; + lastInvalidReason = resolveReason; + break; + } + + _wheelDirs[i] = resolvedDir; + tmpAngles.Add(resolvedTh); + } + + if (needAdjust && anyWheelInvalid) + { + return FailMotionDecomposition("SendMotion", lastInvalidReason, deltaTime); + } + + finalAngles = tmpAngles; + break; + } + + if (finalAngles.Count == _steerWheels.Count) + { + for (var i = 0; i < _steerWheels.Count; i++) + { + SendTh(i, finalAngles[i]); + } + } + else + { + return FailMotionDecomposition("SendMotion", + $"unable to find valid wheel angles frontTh={frontTh:F1} rearTh={rearTh:F1}", + deltaTime); + } + + // // todo: find better way to limit max speed + // var totalTurn = Math.Abs(frontTh) + Math.Abs(rearTh); + // var turnThresholdSpeed = CalculateTurningSpeedDecayFac(totalTurn) * MaxSpeed; + for (int i = 0; i < _steerWheels.Count; i++) + { + _debugSpeeds[i] = _tmpSpeeds[i] * speed * _wheelDirs[i]; + } + var speedBeforeAlignGate = speed; + + if (!GoingWheelAligned) + { + Hedingben.ToastText("Wheel Not Aligned", "MultiWheelChassis-SendMotion-notAligned"); + speed = 0; + var aligned = _steerWheels.Select((sw, i) => (sw, i)).All(ww => + Math.Abs(CommonMath.ThDiff(ww.sw.ReadAngle(), _sendAngle[ww.i])) < 1); + GoingWheelAligned = aligned; + } + + for (var i = 0; i < _steerWheels.Count; i++) + { + AccumulateSpeed(i, _tmpSpeeds[i] * speed * _wheelDirs[i], axisDiffFlag, _rotCenter, deltaTime); + } + + if ((DateTime.Now - _sendMotionDetailLastLog).TotalMilliseconds >= 200) + { + _sendMotionDetailLastLog = DateTime.Now; + var sb = new StringBuilder(); + sb.Append($"SendMotionDetail speed:{speed:F3} speedBeforeGate:{speedBeforeAlignGate:F3} fTh:{frontTh:F1} rTh:{rearTh:F1} " + + $"curF:{currentFrontTh:F1} curR:{currentRearTh:F1} " + + $"originBias:({_originBiasX:F1},{_originBiasY:F1},{_originBiasTh:F1}) " + + $"in(speed:{compInputSpeed:F3},f:{compInputFrontTh:F1},r:{compInputRearTh:F1}) " + + $"comp({localCompensateX:F1},{localCompensateY:F1},{localCompensateTh:F2}) " + + $"motionTh:{compMotionTh:F1} along:{compAlong:F1} side:{compSide:F1} dSpeed:{compDSpeed:F3} dTh:{compDTh:F1} " + + $"axisDiff:{axisDiffFlag} rotCenter:({_rotCenter.X:F1},{_rotCenter.Y:F1}) goingAligned:{GoingWheelAligned}"); + for (var i = 0; i < _steerWheels.Count; i++) + { + var sw = _steerWheels[i]; + var readTh = sw.ReadAngle(); + var targetTh = _sendAngle[i]; + var errTh = CommonMath.ThDiff(readTh, targetTh); + sb.Append($" | w{i} tgt:{targetTh:F1} read:{readTh:F1} err:{errTh:F1} " + + $"zero:{sw.ZeroDirection:F1} rel:{sw.GetAngleRelativeToChassis():F1} " + + $"dir:{_wheelDirs[i]} tmp:{_tmpSpeeds[i]:F2} targetV:{_targetSpeeds[i]:F3} sendV:{_sendSpeeds[i]:F3} " + + $"lim:[{sw.AngleLowerLimit:F1},{sw.AngleUpperLimit:F1}]"); + if (sw is DiffSteerWheel dsw) + { + sb.Append($" diffSend(L:{dsw.GetLeftSendSpeed():F3},R:{dsw.GetRightSendSpeed():F3})"); + } + else + { + sb.Append($" wheelSend:{sw.GetSendSpeed():F3}"); + } + } + DLog.Log(sb.ToString(), "MultiWheelMotionDetail"); + } + + LastMoveTime = DateTime.Now; + LastMotionDecomposeFailureReason = ""; + return true; + } + + /// + /// 绕"已被 SetOriginBias 偏置到车队中心的原点"做原地旋转,可叠加一个车体系小幅纠偏旋量。 + /// + /// 绕车队中心角速度(deg/s,逆时针为正)。 + /// 车体系纵向(前+)修正速度(mm/s),多车联动维持队形用。 + /// 车体系横向(左+)修正速度(mm/s)。 + /// 绕本车几何中心附加角速度(deg/s),修正朝向偏差。 + public bool SendRotateMotion(float rotSpeed, TimeSpan? deltaTime = null, + float localCompensateX = 0f, float localCompensateY = 0f, float localCompensateTh = 0f) + { + if (!Valid) return FailMotionDecomposition("SendRotateMotion", "invalid chassis", deltaTime); + LastMotionDecomposeFailureReason = ""; + if (Math.Abs(rotSpeed) < 1e-6f && + Math.Abs(localCompensateX) < 1e-6f && + Math.Abs(localCompensateY) < 1e-6f && + Math.Abs(localCompensateTh) < 1e-6f) + { + LastRotateAligned = true; + RampStop(deltaTime); + return true; + } + + var ths = new float[_steerWheels.Count]; + var dirs = new int[_steerWheels.Count]; + // 每轮合速度大小(m/s),含"绕队心旋转 + 车体平移纠偏 + 绕本车中心微转纠偏"三项矢量和。 + var speedMags = new float[_steerWheels.Count]; + var allWheelAligned = true; + + // 把车体系纠偏旋量换算到 sw.Position 所在的偏置帧 F(原点=车队中心, 朝向随 _originBiasTh)。 + // body->F 旋转为 R(_originBiasTh),与 SetOriginBias 里 Transform2D 的约定一致。 + var radBias = _originBiasTh / 180.0 * Math.PI; + var cosB = Math.Cos(radBias); + var sinB = Math.Sin(radBias); + var compVxF = localCompensateX * cosB - localCompensateY * sinB; // mm/s,F帧 + var compVyF = localCompensateX * sinB + localCompensateY * cosB; + var rotRad = rotSpeed / 180.0 * Math.PI; // 绕队心(F原点)角速度 rad/s + var compRad = localCompensateTh / 180.0 * Math.PI; // 绕本车几何中心附加角速度 rad/s + var biasX = (double)_originBiasX; // 本车几何中心在 F 中的位置 + var biasY = (double)_originBiasY; + + for (var i = 0; i < _steerWheels.Count; i++) + { + var sw = _steerWheels[i]; + var px = (double)sw.Position.X; + var py = (double)sw.Position.Y; + // 合成轮速矢量(mm/s, F帧):v = ω_rot ẑ×p + [R(bias)·v_comp + ω_comp ẑ×(p - 本车中心)] + // 旋转切向项与纠偏项分开算,便于把纠偏钳到旋转切向的一定比例。 + var vxRot = -rotRad * py; + var vyRot = rotRad * px; + var vxComp = compVxF - compRad * (py - biasY); + var vyComp = compVyF + compRad * (px - biasX); + // 纠偏钳位:|纠偏| ≤ RotateCompTangentFrac × |旋转切向|,限制合矢量相对纯切向的最大偏角 + // (frac=0.5 → 偏角≤26.6°),避免减速末段纠偏盖过旋转切向导致舵轮 180° 乱翻。 + if (RotateCompTangentFrac >= 0f) + { + var rotMag = Math.Sqrt(vxRot * vxRot + vyRot * vyRot); + var compMag = Math.Sqrt(vxComp * vxComp + vyComp * vyComp); + var compLimit = RotateCompTangentFrac * rotMag; + if (compMag > compLimit && compMag > 1e-6) + { + var s = compLimit / compMag; + vxComp *= s; + vyComp *= s; + } + } + var vx = vxRot + vxComp; + var vy = vyRot + vyComp; + var vmag = Math.Sqrt(vx * vx + vy * vy); + speedMags[i] = (float)(vmag / 1000.0); + + // 轮子应指向的运动方向(F帧)。纠偏后合速度≈0 时退化为纯旋转切向,避免 atan2 抖动。 + var angleF = vmag < 1e-6 + ? Math.Atan2(px, -py) / Math.PI * 180 + : Math.Atan2(vy, vx) / Math.PI * 180; + var tangent = CommonMath.ThDiff((float)angleF, sw.ZeroDirection); + + if (!TryResolveWheelAngle(i, tangent, "SendRotateMotion", out ths[i], out dirs[i], + out var resolveReason)) + return FailMotionDecomposition("SendRotateMotion", resolveReason, deltaTime); + _wheelDirs[i] = dirs[i]; + } + + for (var i = 0; i < _steerWheels.Count; ++i) + SendTh(i, ths[i]); + var slowFac = 1f; + var maxDth = 0f; + for (var i = 0; i < _steerWheels.Count; ++i) + { + var dth = Math.Abs(CommonMath.ThDiff(ths[i], _steerWheels[i].ReadAngle())); + maxDth = Math.Max(maxDth, dth); + slowFac = Math.Min(slowFac, CommonMath.gaussmf(dth, rotSync, 0)); + // if (dth > 5) + // { + // allWheelAligned = false; + // break; + // } + } + LastRotateAligned = allWheelAligned; // 供上层做积分抗饱和 + + // 对齐门控:未对齐则整体停车;对齐后用 slowFac 平滑提速。旋转与纠偏被同等缩放,保持几何一致。 + var gateFac = allWheelAligned ? slowFac : 0f; + for (var i = 0; i < _steerWheels.Count; ++i) + AccumulateSpeed(i, speedMags[i] * gateFac * dirs[i], true, Vector2.Zero, deltaTime); + + // === 原地旋转诊断(节流 ~300ms)=== + // 关注 allWheelAligned(false 说明舵轮追不上目标角)、dirs/半径,以及叠加的纠偏旋量 comp(vx,vy,om)。 + if ((DateTime.Now - _rotDbgLast).TotalMilliseconds >= 300) + { + _rotDbgLast = DateTime.Now; + var sb = new StringBuilder(); + sb.Append($"SendRotateMotion in:{rotSpeed:F1} aligned:{allWheelAligned} maxDth:{maxDth:F1} slowFac:{slowFac:F2} gate:{gateFac:F2} biasTh:{_originBiasTh:F1} comp(vx:{localCompensateX:F1} vy:{localCompensateY:F1} om:{localCompensateTh:F1})"); + for (var i = 0; i < _steerWheels.Count; ++i) + { + var sw = _steerWheels[i]; + sb.Append($" | w{i} tgt:{ths[i]:F1} read:{sw.ReadAngle():F1} dth:{Math.Abs(CommonMath.ThDiff(ths[i], sw.ReadAngle())):F1} dir:{dirs[i]} R:{sw.Position.Length():F0} v:{speedMags[i] * gateFac * dirs[i]:F3} send:{sw.GetSendSpeed():F2}"); + } + DLog.Log(sb.ToString(), "RotateDbg"); + } + + LastMoveTime = DateTime.Now; + LastMotionDecomposeFailureReason = ""; + return true; + } + + private DateTime _rotDbgLast = DateTime.MinValue; + + public override float CalculateTurningSpeedDecayFac(float turn) + { + return 1 - Math.Min(turn, MaxTurnThreshold) / MaxTurnThreshold * MinTurnSpeedFac; + } + + [Obsolete] + public List GetSteerWheels() + { + return _steerWheels.ToList(); + } + + private void SendTh(int i, float targetTh) + { + _steerWheels[i].WriteAngle(targetTh); + _sendAngle[i] = targetTh; + } + + private (int direction, float rangeFront, float rangeRear) DetermineWheelDirection(SteerWheel wheel) + { + var lower = CommonMath.RoundTh(wheel.AngleLowerLimit + wheel.ZeroDirection); + var upper = CommonMath.RoundTh(wheel.AngleUpperLimit + wheel.ZeroDirection); + while (upper < lower) upper += 360; + + var frontDir = 0; + var rearDir = 180; + + // 计算前向和后向的可用范围 + var rangeFront = Math.Min(upper - frontDir, frontDir - lower); + var rangeRear = Math.Min(upper - rearDir, rearDir - lower); + + // 选择范围更大的方向 + if (rangeFront >= rangeRear && rangeFront > 0) + { + return (1, rangeFront, rangeRear); + } + else if (rangeRear > rangeFront && rangeRear > 0) + { + return (-1, rangeFront, rangeRear); + } + + // 如果两个方向都不可行 + return (0, rangeFront, rangeRear); + } + + private void AccumulateSpeed(int i, float v, bool axisDiff, Vector2 rotCenter, TimeSpan? deltaTime = null) + { + _targetSpeeds[i] = v; + var speedSign = Math.Sign(_targetSpeeds[i] - _sendSpeeds[i]); + var acc = Math.Abs(_targetSpeeds[i]) > Math.Abs(_sendSpeeds[i]) ? AccPerSecond : DeAccPerSecond; + _sendSpeeds[i] += speedSign * Math.Min(Math.Abs(_targetSpeeds[i] - _sendSpeeds[i]), + acc * (float)(deltaTime ?? DateTime.Now - LastMoveTime).TotalSeconds); + if (_steerWheels[i] is DiffSteerWheel dsw) + { + if (axisDiff) + { + var wheelRadius = Vector2.Distance(rotCenter, dsw.Position); + var wheelDir = _sendAngle[i]; + + var left = CommonMath.Transform2D(dsw.Position, wheelDir + 90, new Vector2(dsw.WheelDistance, 0)); + var right = CommonMath.Transform2D(dsw.Position, wheelDir - 90, new Vector2(dsw.WheelDistance, 0)); + dsw.WriteLeftSpeed(_sendSpeeds[i] / wheelRadius * Vector2.Distance(rotCenter, left)); + dsw.WriteRightSpeed(_sendSpeeds[i] / wheelRadius * Vector2.Distance(rotCenter, right)); + } + else + { + dsw.WriteLeftSpeed(_sendSpeeds[i]); + dsw.WriteRightSpeed(_sendSpeeds[i]); + } + } + else + { + _steerWheels[i].WriteSpeed(_sendSpeeds[i]); + } + + if (Debug) + Console.WriteLine($"Ackermann wheel{i}: target:{_targetSpeeds[i]:0.00},send:{_sendSpeeds[i]:0.0}"); + } + + private void CalculateAxes() + { + var axes = _steerWheels + .Select(sw => (sw, Vector2.Dot(sw.Position, new Vector2(1, 0)))) + .OrderByDescending(ax => ax.Item2).ToList(); + _wheelBases = axes.Select(ax => ax.Item2).ToList(); + _steerWheels = axes.Select(ax => ax.sw).ToList(); + GeometricControlPoints = new List() + { + new (new Vector2(ControlPointRadius, 0)), + new (new Vector2(-ControlPointRadius, 0)) + }; + _frontBase = _wheelBases.First(); + _rearBase = _wheelBases.Last(); + } + + private List _steerWheels = new (); + private float _frontBase, _rearBase; + private List _wheelBases; + private List _sendSpeeds; + private List _targetSpeeds; + private List _sendAngle; + private List _debugSpeeds; + private DateTime _sendMotionDetailLastLog = DateTime.MinValue; + private DateTime _geometricComputeLastLog = DateTime.MinValue; + + private List _tmpSpeeds; + // add TimeStamp, prevent the wheel from swaying. + // for example, target angle is 90, current wheel is around 0. if no TimeStamp, + // wheel will swing between 90 and -90. + private List _wheelDirs; + + // call this before a new continuous motion happens + private void ResetMotionState() + { + LastMoveTime = DateTime.Now; + GoingWheelAligned = false; + _wheelDirs = Enumerable.Repeat(1, _steerWheels.Count).ToList(); + } + + public void AddTestFunction() + { + + } + /// + /// 得到相对舵轮在车体坐标系下的分解速度 + /// + /// + /// + /// + /// 单位为°/s + /// + private Vector2 VectorVelocity(Vector2 pos, float vx, float vy, float vth, int i) + { + var vRotX = -vth / 180 * (float)Math.PI * pos.Y / 1000; + var vRotY = vth / 180 * (float)Math.PI * pos.X / 1000; + Hedingben.ToastText($"vRot:({vRotX:F3},{vRotY:F3}) pos:({pos.X:F1},{pos.Y:F1})", + $"SendXYThSpeed-VectorVelocity{i}"); + return new Vector2(vx + vRotX, vy + vRotY); + } + /// + /// 获得车轮应该打的角度和速度 + /// + /// + /// + /// + /// 单位为°/s + /// + private (float angle, float speed) AngleAndSpeed(Vector2 pos, float vx, float vy, float vth, int i) + { + var v = VectorVelocity(pos, vx, vy, vth, i); + return ((float)(Math.Atan2(v.Y, v.X) / Math.PI * 180), v.Length()); + } + + private float w = 0; + private float wAcc = 0.1f; + private float rotSync = 3f; + private bool XYThActive = false; + + public bool SendXYThSpeed(float vx, float vy, float vth, TimeSpan? deltaTime = null) + { + if (!Valid) return FailMotionDecomposition("SendXYThSpeed", "invalid chassis", deltaTime); + if (!XYThActive) + { + ResetMotionState(); + w = 0; + } + XYThActive = true; + GoingActive = false; + RotatingActive = false; + + if (Math.Abs(vx) < 1e-6f && Math.Abs(vy) < 1e-6f && Math.Abs(vth) < 1e-6f) + { + RampStop(deltaTime); + LastMotionDecomposeFailureReason = ""; + return true; + } + + w = w + wAcc; + if (w > 1) w = 1; + float[] sendSpeed = new float[_steerWheels.Count]; + for (var i = 0; i < _steerWheels.Count; i++) + { + var sw = _steerWheels[i]; + var (angle, speed) = AngleAndSpeed(sw.Position, vx, vy, vth, i); + var actualTh = sw.ReadAngle(); + if (!TryResolveWheelAngle(i, CommonMath.ThDiff(angle, sw.ZeroDirection), "SendXYThSpeed", + out var useAngle, out var dir, out var resolveReason)) + return FailMotionDecomposition("SendXYThSpeed", resolveReason, deltaTime); + speed *= dir; + _wheelDirs[i] = dir; + + sendSpeed[i] = speed; + if (speed!=0) + SendTh(i, useAngle); + w = (float)Math.Min(w, CommonMath.gaussmf(CommonMath.ThDiff(actualTh, _sendAngle[i]), rotSync, 0)); + + Hedingben.ToastText($"w:{w:F2} s:{speed:F3} th:{_sendAngle[i]:F1} actualTh:{actualTh:F1}", $"SendXYThSpeed-{i}"); + } + for (var i = 0; i < _steerWheels.Count; i++) + AccumulateSpeed(i, w * sendSpeed[i],false,new Vector2(0f,0f), deltaTime); + //todo 计算rotCenter填入 + LastMoveTime = DateTime.Now; + LastMotionDecomposeFailureReason = ""; + return true; + } + + public override CarSpeed GetCarSpeed(bool isActual = false) + { + List vx = new List(); + List vy = new List(); + List vth = new List(); + for (var i = 0; i < _steerWheels.Count; ++i) + { + var sw1 = _steerWheels[i]; + Vector2 a = sw1.Position / 1000; + //var tha = _sendAngle[i] / 180 * (float)Math.PI; + var tha = isActual ? sw1.ReadAngle() / 180 * (float)Math.PI : _sendAngle[i] / 180 * (float)Math.PI; + //var speeda = _sendSpeeds[i]; + //var speeda = isActual ? sw1.ReadSpeed() : _sendSpeeds[i]; + var speeda = isActual ? sw1.ReadSpeed() : _debugSpeeds[i]; + Vector2 va = new Vector2(speeda * (float)Math.Cos(tha), speeda * (float)Math.Sin(tha)); + // painter.DrawLine(Color.Green, sw1.Position, + // LessMath.Transform2D(sw1.Position, sw1.ReadAngle(), new Vector2(200, 0)), endArrow: true); + for (var j = i + 1; j < _steerWheels.Count; ++j) + { + var sw2 = _steerWheels[j]; + Vector2 b = sw2.Position / 1000; + //var thb = _sendAngle[j] / 180 * (float)Math.PI; + var thb = isActual ? sw2.ReadAngle() / 180 * (float)Math.PI : _sendAngle[j] / 180 * (float)Math.PI; + //var speedb = _sendSpeeds[j]; + //var speedb = isActual ? sw2.ReadSpeed() : _sendSpeeds[j]; + var speedb = isActual ? sw2.ReadSpeed() : _debugSpeeds[j]; + Vector2 vb = new Vector2(speedb * (float)Math.Cos(thb), speedb * (float)Math.Sin(thb)); + var (tempvx, tempvy, tempvth) = CenterVelocityFromPoints(a, va, b, vb); + vx.Add(tempvx); + vy.Add(tempvy); + vth.Add(tempvth); + } + } + return new CarSpeed() + { Vx = vx.Average(), Vy = vy.Average(), Vw = (float)(vth.Average() / Math.PI * 180f) }; + } + + private static (float, float, float) CenterVelocityFromPoints(Vector2 a, Vector2 va, Vector2 b, Vector2 vb) + { + float vth_x = 0, vth_y = 0, vth = 0, vx = 0, vy = 0; + var eps = 0.0000001; + if (Math.Abs(a.Y - b.Y) > eps) + { + vth_x = (va.X - vb.X) / (b.Y - a.Y); + } + + if (Math.Abs(a.X - b.X) > eps) + { + vth_y = (va.Y - vb.Y) / (a.X - b.X); + } + vth = vth_x == 0 ? vth_y : vth_x; + vx = va.X + vth * a.Y; + vy = va.Y - vth * a.X; + return (vx, vy, vth); + } + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Chassis/SingleSteerChassis.cs b/CommonUsage-MultiVehicleSync/commonusage/Chassis/SingleSteerChassis.cs new file mode 100644 index 0000000..cb0df3e --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Chassis/SingleSteerChassis.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Diagnostics; +using CommonUsage.Mathematics; +using FundamentalLib; + +namespace CommonUsage.Chassis +{ + public class SingleSteerChassis : AbstractChassis + { + public void SetSteerWheel(SteerWheel wheel) + { + _steerWheel = wheel; + } + + public SteerWheel GetSteerWheel() + { + return _steerWheel; + } + + public override void Visualize() + { + + } + + public override CarSpeed GetCarSpeed(bool isActual = false) + { + if (!isActual) + { + var sendAngle = _steerWheel.GetSendAngle(); + var sendAngleRad = _steerWheel.GetSendAngle() / 180f * Math.PI; + // VSteer* Cos = v; + var vsteer = _sendSpeed / ((Math.Cos(Math.Abs(sendAngleRad)) + 0.000001)); + var vsteerY = vsteer * Math.Sin(sendAngleRad); + // Console.WriteLine($"{vsteer} {vsteerY} {sendAngleRad} {_sendSpeed}"); + return new CarSpeed() + { + Vx = (float)(_sendSpeed * Math.Cos(Math.Abs(sendAngle) / 180f * Math.PI)), + Vy = 0, + Vw = (float)(_sendSpeed * Math.Sin(Math.Abs(sendAngle) / 180f * Math.PI) / + Math.Abs(_steerWheel.Position.X / 1000f) / Math.PI * 180f) + //阿克曼 + // Vx = (float)(_sendSpeed), + // Vy = 0, + // Vw = (float)(vsteerY / Math.Abs(_steerWheel.Position.X / 1000f) / Math.PI * 180f) + }; + } + else + { + return new CarSpeed() + { + Vx = (float)(_steerWheel.ReadSpeed() * + Math.Cos(Math.Abs(_steerWheel.ReadAngle()) / 180f * Math.PI)), + Vy = 0, + Vw = (float)(_steerWheel.ReadSpeed() * + Math.Sin(Math.Abs(_steerWheel.ReadAngle()) / 180f * Math.PI) / + Math.Abs(_steerWheel.Position.X / 1000f) / Math.PI * 180f) + }; + } + + } + + public override void Initialize() + { + GeometricControlPoints = new List() + { + new (_steerWheel.Position), + new (Vector2.Zero) + }; + Valid = true; + } + + public override void AfterDirectionChanged() + { + if (Math.Abs(CommonMath.ThDiff(0, _originBiasTh)) > 90) + { + GeometricControlPoints = new List() + { + new (-_steerWheel.Position), + }; + _direction = -1; + } + else + { + GeometricControlPoints = new List() + { + new (_steerWheel.Position), + }; + _direction = 1; + } + } + + public override void PredefinedDriveStop() + { + if (!Valid) return; + _sendSpeed = 0; + _steerWheel.WriteSpeed(_sendSpeed); + GoingActive = false; + RotatingActive = false; + } + + protected override void DefineGeometricWheelComputation(float speed) + { + var now = DateTime.Now; + + if (!GoingActive) + { + LastMoveTime = now; + GoingWheelAligned = false; + } + + SendSteerMotion(speed * _direction, GeometricControlPoints[0].Theta, now - LastMoveTime); + + GoingActive = true; + RotatingActive = false; + } + + public override bool ComputeRotateWheels(float rotSpeed) + { + if (!RotatingActive) + { + LastMoveTime = DateTime.Now; + GoingWheelAligned = false; + } + + SendSteerMotion(rotSpeed, 90); + + GoingActive = false; + RotatingActive = true; + return true; + } + + public void SendSteerMotion(float speed, float theta, TimeSpan? deltaTime = null) + { + _steerWheel.WriteAngle(theta); + + if (!GoingWheelAligned && Math.Abs(CommonMath.ThDiff(_steerWheel.ReadAngle(), theta)) < 1) + GoingWheelAligned = true; + if (!GoingWheelAligned) speed = 0; + + var turnThresholdSpeed = CalculateTurningSpeedDecayFac(Math.Abs(theta)) * MaxSpeed; + AccumulateSpeed(Math.Min(turnThresholdSpeed, Math.Abs(speed)) * Math.Sign(speed), deltaTime); + + LastMoveTime = DateTime.Now; + } + + public override float CalculateTurningSpeedDecayFac(float turn) + { + return 1 - Math.Min(turn, MaxTurnThreshold) / MaxTurnThreshold * MinTurnSpeedFac; + } + + private void AccumulateSpeed(float v, TimeSpan? deltaTime = null) + { + // _targetSpeed = v; + var speedSign = Math.Sign(v - _sendSpeed); + var acc = Math.Abs(v) > Math.Abs(_sendSpeed) ? AccPerSecond : DeAccPerSecond; + _sendSpeed += speedSign * Math.Min(Math.Abs(v - _sendSpeed), + acc * (float)(deltaTime ?? DateTime.Now - LastMoveTime).TotalSeconds); + _steerWheel.WriteSpeed(_sendSpeed); + + if (Debug) + Console.WriteLine($"SingleSteer, target:{v:0.00},send:{_sendSpeed:0.0}"); + } + + private SteerWheel _steerWheel; + private float _sendSpeed; + private int _direction = 1; // 1 forward, -1 backward + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Chassis/SteerWheel.cs b/CommonUsage-MultiVehicleSync/commonusage/Chassis/SteerWheel.cs new file mode 100644 index 0000000..00bc7c4 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Chassis/SteerWheel.cs @@ -0,0 +1,100 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; +using CommonUsage.Mathematics; + +namespace CommonUsage.Chassis +{ + public class SteerWheel : Wheel + { + public SteerWheel(Vector2 position, float angleLowerLimit, float angleUpperLimit, Action speedWriter, + Func speedReader, Action angleWriter, Func angleReader, float angleLimitMarginDeg = 15f) : base(position, speedWriter, + speedReader) + { + _angleLowerLimit = angleLowerLimit; + _angleUpperLimit = angleUpperLimit; + _angleWriter = angleWriter; + _angleReader = angleReader; + _centerDistance = position.Length(); + AngleLimitMarginDeg = angleLimitMarginDeg; + } + + public float AngleLimitMarginDeg = 15f; + + public bool TrySetDirection(bool allowReverse, ref float desireDirection, ref int dir) + { + if (TryNormalizeAngleInLimit(desireDirection, out var normalized)) + { + dir = 1; + desireDirection = normalized; + return true; + } + + if (!allowReverse) return false; + + var oppositeTh = (float)CommonMath.RoundTh(desireDirection + 180); + if (TryNormalizeAngleInLimit(oppositeTh, out normalized)) + { + dir = -1; + desireDirection = normalized; + return true; + } + + dir = 0; + return false; + } + + private bool TryNormalizeAngleInLimit(float angle, out float normalized) + { + var lower = CommonMath.RoundTh(_angleLowerLimit); + var upper = CommonMath.RoundTh(_angleUpperLimit); + while (upper < lower) upper += 360; + + normalized = (float)CommonMath.RoundTh(angle); + while (normalized < lower) normalized += 360; + while (normalized > upper && normalized - 360 >= lower) normalized -= 360; + + var margin = Math.Min(normalized - lower, upper - normalized); + return normalized >= lower && normalized <= upper && margin >= Math.Max(0, AngleLimitMarginDeg); + } + + public float ReadAngle() + { + return _angleReader(); + } + + public void WriteAngle(float angle) + { + _angleWriter.Invoke(_sendAngle = Math.Max(_angleLowerLimit, Math.Min(angle, _angleUpperLimit))); + } + + public float GetSendAngle() + { + return _sendAngle; + } + + public float GetAngleRelativeToChassis() + { + return ZeroDirection + _sendAngle; + } + + public float CenterDistance() + { + return _centerDistance; + } + + public float AngleLowerLimit => _angleLowerLimit; + + public float AngleUpperLimit => _angleUpperLimit; + + [JsonIgnore] private readonly Action _angleWriter; + [JsonIgnore] private readonly Func _angleReader; + + private float _angleLowerLimit = -90, _angleUpperLimit = 90; + private float _centerDistance; + + public float _sendAngle = 0f; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Chassis/Wheel.cs b/CommonUsage-MultiVehicleSync/commonusage/Chassis/Wheel.cs new file mode 100644 index 0000000..dea87e1 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Chassis/Wheel.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; +using Newtonsoft.Json; + +namespace CommonUsage.Chassis +{ + public class Wheel + { + public Wheel(Vector2 position, Action speedWriter, Func speedReader) + { + PhysicalPosition = Position = position; + SpeedWriter = speedWriter; + SpeedReader = speedReader; + } + + public void WriteSpeed(float speed) + { + SpeedWriter.Invoke(_sendSpeed = speed); + } + + public float GetSendSpeed() + { + return _sendSpeed; + } + + public float ReadSpeed() + { + return SpeedReader(); + } + + // PhysicalPosition ===(chassis transform)===> Position + // useful in dual agv coordination + public readonly Vector2 PhysicalPosition; + public Vector2 Position; + + public float ZeroDirection = 0; + + [JsonIgnore] public readonly Action SpeedWriter; + [JsonIgnore] public readonly Func SpeedReader; + + public float _sendSpeed; + } + + public class GeometricControlPoint + { + public GeometricControlPoint(Vector2 position) + { + Position = position; + } + + public Vector2 Position; + public float Theta; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/CommonUsage.csproj b/CommonUsage-MultiVehicleSync/commonusage/CommonUsage.csproj new file mode 100644 index 0000000..d5f4d92 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/CommonUsage.csproj @@ -0,0 +1,48 @@ + + + + netstandard2.0 + + + + latest + True + + + + embedded + + + + embedded + + + + + + + + + + + + + + + + + + ..\..\MedullaAdapter\ref\RefFundamentalLib.dll + + + ..\..\ClumsyPilot\ref\RefClumsyCore.dll + + + + + + + + + diff --git a/CommonUsage-MultiVehicleSync/commonusage/CommonUsageSln.sln b/CommonUsage-MultiVehicleSync/commonusage/CommonUsageSln.sln new file mode 100644 index 0000000..227bf56 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/CommonUsageSln.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33424.131 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommonUsage", "CommonUsage.csproj", "{E1C5DEA8-3785-40A9-9965-E51E65BF5947}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E1C5DEA8-3785-40A9-9965-E51E65BF5947}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1C5DEA8-3785-40A9-9965-E51E65BF5947}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1C5DEA8-3785-40A9-9965-E51E65BF5947}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1C5DEA8-3785-40A9-9965-E51E65BF5947}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {709F9C19-45DB-46AD-B70A-0E1E3C6CFB0C} + EndGlobalSection +EndGlobal diff --git a/CommonUsage-MultiVehicleSync/commonusage/Deps/RefFundamentalLib.dll b/CommonUsage-MultiVehicleSync/commonusage/Deps/RefFundamentalLib.dll new file mode 100644 index 0000000..8ada7ed Binary files /dev/null and b/CommonUsage-MultiVehicleSync/commonusage/Deps/RefFundamentalLib.dll differ diff --git a/CommonUsage-MultiVehicleSync/commonusage/Geometries/AbstractGeometry.cs b/CommonUsage-MultiVehicleSync/commonusage/Geometries/AbstractGeometry.cs new file mode 100644 index 0000000..28ed7b9 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Geometries/AbstractGeometry.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Numerics; +using System.Text; +using static CommonUsage.Geometries.CircularArc; + +namespace CommonUsage.Geometries +{ + /// + /// 便于直接创建几何形状并求几何形状的切点、切线等。 + /// + public abstract class AbstractGeometry + { + protected AbstractGeometry() + { + PaddingType = Padding.StartExtendEndExtend; + VisualizeOption = new VisualizeOption(Color.Red, Color.Gray); + } + + public Padding PaddingType; + + public abstract (Vector2 Pt, float Angle, float Bias, float Position) QueryTangentPoint(Vector2 point); + + public abstract void Visualize(Action processDot, Action processLine, + bool visExtendedPart = false); + + public VisualizeOption VisualizeOption; + + /// + /// 查询指定位置的曲率。 + /// + /// 从起点到查询位置的距离。 + /// + public abstract float QueryCurvature(float position); + + public abstract float Length(); + } + + public class VisualizeOption + { + public VisualizeOption(Color mainColor, Color auxiliaryColor) + { + MainColor = mainColor; + AuxiliaryColor = auxiliaryColor; + } + + public Color MainColor; + public Color AuxiliaryColor; + public bool DrawAuxiliary = true; + public bool VisualizeDirection = true; + } + + public enum Padding + { + StartLineEndLine = 0b_0001_0001, + StartLineEndExtend = 0b_0001_0010, + StartExtendEndLine = 0b_0010_0001, + StartExtendEndExtend = 0b_0010_0010, + } + + public class VisDot + { + public VisDot(Vector2 point, Color color) + { + Point = point; + Color = color; + } + + public Vector2 Point; + public Color Color; + } + + public class VisLine + { + public VisLine(Vector2 start, Vector2 end, bool startArrow, bool endArrow, Color color, float width = 1) + { + Start = start; + End = end; + StartArrow = startArrow; + EndArrow = endArrow; + Color = color; + Width = width; + } + + public Vector2 Start; + public Vector2 End; + public bool StartArrow = false; + public bool EndArrow = false; + public Color Color; + public float Width; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Geometries/BezierCurve.cs b/CommonUsage-MultiVehicleSync/commonusage/Geometries/BezierCurve.cs new file mode 100644 index 0000000..c4c8bbe --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Geometries/BezierCurve.cs @@ -0,0 +1,334 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Reflection; +using CommonUsage.Mathematics; + +namespace CommonUsage.Geometries +{ + public class BezierCurve : AbstractGeometry + { + + public BezierCurve(List controlPoints, int resolution = 100) + { + // Console.WriteLine($"BezierCurve1"); + // Console.WriteLine(string.Join(" ",controlPoints.Select(p=>$"{p.X:f2},{p.Y:f2}"))); + _controlPoints = controlPoints; + _resolution = resolution; + InitializeBezier(); + } + + public override void Visualize(Action processDot, Action processLine, bool visExtendedPart = false) + { + if (VisualizeOption.DrawAuxiliary) + for (var i = 0; i < _controlPoints.Count - 1; ++i) + { + processLine(new VisLine(_controlPoints[i], _controlPoints[i + 1], + false, false, VisualizeOption.AuxiliaryColor)); + if (i == 0) continue; + processDot(new VisDot(_controlPoints[i], VisualizeOption.AuxiliaryColor)); + } + + for (var i = 0; i < _bezierPoints.Count - 1; ++i) + { + if (Direction == -1) + { + processLine(new VisLine(_bezierPoints[i + 1], _bezierPoints[i], + false, i == (int)(_bezierPoints.Count / 2), VisualizeOption.MainColor, 2)); + } + else + { + processLine(new VisLine(_bezierPoints[i], _bezierPoints[i + 1], + false, i == (int)(_bezierPoints.Count / 2), VisualizeOption.MainColor, 2)); + } + + + } + } + + public (Vector2 Point, int Id) QueryPoint(Vector2 point) + { + var p = new Vector2(); + var id = -1; + var bestDistance = float.MaxValue; + + var hashes = _bias.Select(bb => CalculateHash(point, 100, bb.X, bb.Y)).ToList(); + + void TryQuery(Dictionary> dict, List hashList) + { + foreach (var hash in hashList) + { + if (!dict.TryGetValue(hash, out var ll)) continue; + foreach (var (q, qId) in ll) + { + var d = Vector2.Distance(q, point); + if (d < bestDistance) + { + p = q; + id = qId; + bestDistance = d; + } + } + } + } + //map目前有bug,取消cpu占用也不严重,必要时候在优化 + // TryQuery(_pointsMappingSmall, hashes); + // + // if (id == -1) + // { + // hashes = _bias.Select(bb => CalculateHash(point, 1000, bb.X, bb.Y)).ToList(); + // TryQuery(_pointsMappingBig, hashes); + // } + + if (id == -1) + { + // todo: improve the way to find closest point if mappings fail + (p, id) = _bezierPoints.Select((p, i) => (p, i)) + .OrderBy(pair => CommonMath.dist(pair.p.X, pair.p.Y, point.X, point.Y)).First(); + } + + return (p, id); + } + + public override (Vector2 Pt, float Angle, float Bias, float Position) QueryTangentPoint(Vector2 point) + { + var (p, id) = QueryPoint(point); + var tangent = _tangents[id]; + var (bias, lp, fd) = CommonMath.Project2DLine(point, p, tangent); + var next = fd > 0 ? id + 1 : id - 1; + if (id == 0) next = 1; + // Console.WriteLine($"id:{id} next:{next} tangent:{tangent} _tangents.Count:{_tangents.Count}"); + if (next > 0 && next < _tangents.Count)//线性插值 + { + var (_, _, t) = CommonMath.Project2DLine(point, _bezierPoints[id], _bezierPoints[next]); + var partial = t / Vector2.Distance(_bezierPoints[id], _bezierPoints[next]); + if (partial >= 0 && partial <= 1) + { + tangent = CommonMath.RoundTh(_tangents[id] + + partial * CommonMath.RoundTh(_tangents[next] - _tangents[id])); + if (CommonMath.RoundTh(_tangents[next] - _tangents[id]) > 5) + Console.WriteLine($"bezier tangents bug, tanget: {id}:{_tangents[id]} {next}:{_tangents[next]}"); + } + // else Console.WriteLine("bezier tangents bug"); + } + return (lp, tangent, bias, fd + _sumDistances[id]); + } + + public override float Length() + { + return _length; + } + + public override float QueryCurvature(float position) + { + int id = _sumDistances.Count - 1; + if (position <= 0) id = 0; + else + { + for (int i = 1; i < _sumDistances.Count; i++) + { + if (position > _sumDistances[i - 1] && position <= _sumDistances[i]) + { + id = i; + break; + } + } + } + + var result = _curvatures[id]; + if (id > 0 && id < _sumDistances.Count - 1)//插值 + { + var partial = (position - _sumDistances[id - 1]) / (_sumDistances[id] - _sumDistances[id - 1]); + if (partial >= 0 && partial <= 1) result = (1 - partial) * _curvatures[id - 1] + partial * _curvatures[id]; + else Console.WriteLine("bezier curvature bug"); + } + + return result; + } + + public Vector3 QueryBezierPointsById(int id) + { + if (id < 0 || id > Resolution) + { + Console.WriteLine($"QueryBezierPointsById out of range, Resolution:{Resolution},id:{id}."); + return new Vector3(0, 0, 0); + } + + return new Vector3(_bezierPoints[id].X, _bezierPoints[id].Y, _tangents[id]); + } + + public List ControlPoints => _controlPoints; + + public int Resolution => _resolution; + /// + /// 仅用于simple显示路径方向 + /// + public int Direction = 1; + public int Order => _order; + + // public List Tangents => _tangents; + + public void UpdateControlPoint(int id, Vector2 point) + { + _controlPoints[id] = point; + InitializeBezier(); + } + + public void AddControlPoint(int id, Vector2 point) + { + _controlPoints.Insert(id, point); + InitializeBezier(); + } + + public void RemoveControlPoint(int id) + { + _controlPoints.RemoveAt(id); + InitializeBezier(); + } + + public Vector2 GetMidPoint() + { + return _bezierPoints[(int)Math.Ceiling(_resolution / 2d)]; + } + + private void InitializeBezier() + { + _order = _controlPoints.Count - 1; + // _bezierPoints = new List(); + var delta = 1.0f / _resolution; + // for (int t = 0; t <= _resolution; t += 1)//下面循环算了,没必要先递归算一遍 + // _bezierPoints.Add(new Vector2(DeCasteljauX(_order, 0, t*delta), DeCasteljauY(_order, 0, t*delta))); + var allPoints = new List>>(); + for (var i = 0; i < _order; i++) + { + var size = allPoints.Count; + var morePoints = new List>(); + for (var j = 0; j < _order - i; j++) + { + var points = new List(); + for (int t = 0; t <= _resolution; t += 1) + { + float p0x; + float p1x; + float p0y; + float p1y; + var z = t; + if (size > 0) + { + p0x = allPoints[i - 1][j][z].X; + p1x = allPoints[i - 1][j + 1][z].X; + p0y = allPoints[i - 1][j][z].Y; + p1y = allPoints[i - 1][j + 1][z].Y; + } + else + { + p0x = _controlPoints[j].X; + p1x = _controlPoints[j + 1].X; + p0y = _controlPoints[j].Y; + p1y = _controlPoints[j + 1].Y; + } + + var part = t * delta; + points.Add(new Vector2((1 - part) * p0x + part * p1x, (1 - part) * p0y + part * p1y)); + } + morePoints.Add(points); + } + allPoints.Add(morePoints); + } + + _bezierPoints = allPoints.Last().Last(); + _tangentInfo = allPoints; + _tangents = Enumerable.Repeat(0f, _bezierPoints.Count).ToList(); + _curvatures = Enumerable.Repeat(0f, _bezierPoints.Count).ToList(); + var p2 = allPoints[Order - 2]; + for (var id = 0; id < _bezierPoints.Count; ++id) + { + _tangents[id] = + (float)(Math.Atan2(p2[1][id].Y - p2[0][id].Y, p2[1][id].X - p2[0][id].X) / Math.PI * 180); + if (id != 0) _curvatures[id] = (float)((CommonMath.ThDiff(_tangents[id], _tangents[id - 1]) / 180 * Math.PI) + / (Vector2.Distance(_bezierPoints[id], _bezierPoints[id - 1]) / 1000)); + } + // Console.WriteLine($"{string.Join("\n", _tangents.Select((val, i) => $"{i}: {val}"))}"); + _tangents[0] = _tangents[1]; // todo: here is temporary fix + _curvatures[0] = _curvatures[1]; + for (var id = 1; id < _bezierPoints.Count - 1; ++id)//前移0.5 + _curvatures[id] = (_curvatures[id] + _curvatures[id + 1]) / 2; + + _remainDistances = Enumerable.Repeat(0f, _bezierPoints.Count).ToList(); + _sumDistances = Enumerable.Repeat(0f, _bezierPoints.Count).ToList(); + for (var i = _bezierPoints.Count - 2; i >= 0; --i) + { + _remainDistances[i] = + _remainDistances[i + 1] + Vector2.Distance(_bezierPoints[i], _bezierPoints[i + 1]); + } + for (var i = 1; i < _bezierPoints.Count; ++i) + { + _sumDistances[i] = + _sumDistances[i - 1] + Vector2.Distance(_bezierPoints[i], _bezierPoints[i - 1]); + } + _length = _sumDistances.Last(); + + _minX = _bezierPoints.Min(pp => pp.X); + _minY = _bezierPoints.Min(pp => pp.Y); + + var tmpList = _bezierPoints.Select((point, index) => (point, index)).ToList(); + return; + void GenerateGridMapping(ref Dictionary> dict, float gSize) + { + dict = new Dictionary>(); + foreach (var (point, index) in tmpList) + { + var hash = CalculateHash(point, gSize); + if (dict.TryGetValue(hash, out var ll)) + ll.Add((point, index)); + else dict[hash] = new List<(Vector2 Point, int Id)>() { (point, index) }; + } + } + + GenerateGridMapping(ref _pointsMappingSmall, 100); + GenerateGridMapping(ref _pointsMappingBig, 1000); + } + + private uint CalculateHash(Vector2 point, float gridSize, int xBias = 0, int yBias = 0) + { + return (uint)(((int)((point.X - _minX) / gridSize) + xBias) << 16 + (((int)((point.Y - _minY) / gridSize) + yBias) & 0xffff)); + } + + private readonly List<(int X, int Y)> _bias = new() + { + new(-1, -1), new(-1, 0), new(-1, 1), + new(0, -1), new(0, 0), new(0, 1), + new(1, -1), new(1, 0), new(1, 1), + }; + + private float DeCasteljauX(int i, int j, float t) + { + if (i == 1) + return (1 - t) * _controlPoints[j].X + t * _controlPoints[j + 1].X; + return (1 - t) * DeCasteljauX(i - 1, j, t) + t * DeCasteljauX(i - 1, j + 1, t); + } + + private float DeCasteljauY(int i, int j, float t) + { + if (i == 1) + return (1 - t) * _controlPoints[j].Y + t * _controlPoints[j + 1].Y; + return (1 - t) * DeCasteljauY(i - 1, j, t) + t * DeCasteljauY(i - 1, j + 1, t); + } + + private int _order; + private int _resolution; + private List _controlPoints; + private List _bezierPoints; + private List>> _tangentInfo; + private List _tangents; + private List _remainDistances; + private List _sumDistances; + private List _curvatures; + private Dictionary> _pointsMappingSmall; + private Dictionary> _pointsMappingBig; + private float _minX, _minY; + private float _length; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Geometries/CircularArc.cs b/CommonUsage-MultiVehicleSync/commonusage/Geometries/CircularArc.cs new file mode 100644 index 0000000..2126385 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Geometries/CircularArc.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Numerics; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using CommonUsage.Mathematics; + +namespace CommonUsage.Geometries +{ + public class CircularArc : AbstractGeometry + { + /// + /// 以center为圆心、radius为半径,从angleStart逆时针转到angleEnd所构成的圆弧。direction表示圆弧走向。 + /// + /// + /// + /// + /// + /// 表示圆弧走向,1为angleStart到angleEnd,-1为angleEnd到angleStart + public CircularArc(Vector2 center, float radius, float angleStart, float angleEnd, int direction, Padding paddingType) + { + _center = center; + _radius = radius; + _angleStart = angleStart; + _angleEnd = angleEnd; + _direction = direction; + PaddingType = paddingType; + + ChangeShape(); + CalculateVisPoints(); + } + + public override void Visualize(Action processDot, Action processLine, + bool visExtendedPart = false) + { + lock (_visPoints) + { + if (visExtendedPart) + { + + } + + for (var i = 0; i < _visPoints.Length - 1; ++i) + { + if ((i == 0 || i == _visPoints.Length - 2) && !visExtendedPart) continue; + var color = Color.Red; + if (i == 0 || i == _visPoints.Length - 2) color = Color.Gray; + processLine(new VisLine(_visPoints[i], _visPoints[i + 1], + false, i == (_visPoints.Length - 1) / 2, color)); + } + + if (visExtendedPart) + { + + } + } + } + + public void SwitchSide() + { + (_angleStart, _angleEnd) = (_angleEnd, _angleStart); + ChangeShape(); + CalculateVisPoints(); + } + + public float VisAngleResolution = 1; + + public Vector2 Center + { + get => _center; + set + { + _center = value; + ChangeShape(); + CalculateVisPoints(); + } + } + + public float Radius + { + get => _radius; + set + { + _radius = value; + ChangeShape(); + CalculateVisPoints(); + } + } + + public float AngleStart + { + get => _angleStart; + set + { + _angleStart = value; + ChangeShape(); + CalculateVisPoints(); + } + } + + public float AngleEnd + { + get => _angleEnd; + set + { + _angleEnd = value; + ChangeShape(); + CalculateVisPoints(); + } + } + + public int Direction + { + get => _direction; + set + { + _direction = value; + ChangeShape(); + CalculateVisPoints(); + } + } + + public float AngleRange => _totalTh; + + public Vector2 PointStart => _center + new Vector2(_radius * (float)Math.Cos(_angleStart / 180 * Math.PI), + _radius * (float)Math.Sin(_angleStart / 180 * Math.PI)); + + public Vector2 PointEnd => _center + new Vector2(_radius * (float)Math.Cos(_angleEnd / 180 * Math.PI), + _radius * (float)Math.Sin(_angleEnd / 180 * Math.PI)); + + public Vector2 Src => _src; + + public Vector2 Dst => _dst; + + public float TangentSrc => _tangentSrc; + + public float TangentDst => _tangentDst; + + public override (Vector2 Pt, float Angle, float Bias, float Position) QueryTangentPoint(Vector2 point) + { + var queryTh = (float)(Math.Atan2(point.Y - _center.Y, point.X - _center.X) / Math.PI * 180); + + var p = new Vector2(); + var tangent = 0f; + var pd = 0f; + var bestBias = float.MaxValue; + + if ((((int)PaddingType >> 4) & 0x1) == 1) + { + var (bias1, hPnt1, fd1) = CommonMath.Project2DLine(point, _beforeStartSrc, _src); + if (fd1 <= 1000) + { + p = hPnt1; + tangent = (_direction >= 0 ? _angleStart : _angleEnd) + 90 * _direction; + pd = fd1; + bestBias = bias1; + } + } + + if (((int)PaddingType & 0x1) == 1) + { + var (bias2, hPnt2, fd2) = CommonMath.Project2DLine(point, _dst, _afterEndDst); + if (fd2 >= 0 && Math.Abs(bias2) < Math.Abs(bestBias)) + { + p = hPnt2; + tangent = (_direction >= 0 ? _angleEnd : _angleStart) + 90 * _direction; + pd = _totalLen + fd2; + bestBias = bias2; + } + } + + var th1 = _direction == 1 ? CommonMath.ThDiff(queryTh, _angleStart) : CommonMath.ThDiff(_angleEnd, queryTh); + // todo: urgent bug! should use better strategy to prevent sign problem + if (th1 < -55) th1 += 360; + var arcBias = (_radius - Vector2.Distance(point, _center)) * _direction; + if (Math.Abs(arcBias) < Math.Abs(bestBias)) + { + p = _center + _radius * new Vector2((float)Math.Cos(queryTh / 180 * Math.PI), + (float)Math.Sin(queryTh / 180 * Math.PI)); + tangent = queryTh + 90 * _direction; + pd = _radius * th1 / 180 * (float)Math.PI; + bestBias = arcBias; + } + + return (p, tangent, bestBias, pd); + } + + public override float QueryCurvature(float position) + { + // var theta = (float)(_angleEnd - position / _radius / Math.PI * 180f + Math.PI); + // return Vectoriel.FromAngleLen(theta, 1f / _radius); + return 1000f / _radius * _direction; + } + + public override float Length() + { + return _totalLen; + } + + private void CalculateVisPoints() + { + lock (_visPoints) + { + // todo: overlapping start and end is problematic + var ptCnt = (int)Math.Ceiling((_angleEnd + 360 - _angleStart) % 360 / VisAngleResolution); + _visPoints = new Vector2[ptCnt + 2]; + + var starting = _angleStart; + if (_direction == -1) starting = _angleEnd; + _visPoints[0] = _beforeStartSrc; + + for (var j = 0; j < ptCnt; ++j) + { + var th = starting + j * VisAngleResolution * _direction; + var radAngle = (float)(th / 180f * Math.PI); + _visPoints[j + 1] = Center + new Vector2((float)Math.Cos(radAngle), (float)Math.Sin(radAngle)) * Radius; + } + + _visPoints[ptCnt + 1] = _afterEndDst; + } + } + + private void ChangeShape() + { + _totalTh = CommonMath.ThDiff(_angleEnd, _angleStart); + if (_totalTh < 0) _totalTh += 360; + _totalLen = _radius * _totalTh / 180 * (float)Math.PI; + + var radAngleStart = _angleStart / 180 * Math.PI; + var radAngleEnd = _angleEnd / 180 * Math.PI; + double srcAngle = radAngleStart, dstAngle = radAngleEnd; + if (_direction == -1) (srcAngle, dstAngle) = (dstAngle, srcAngle); + _src = _center + new Vector2((float)Math.Cos(srcAngle), (float)Math.Sin(srcAngle)) * _radius; + _dst = _center + new Vector2((float)Math.Cos(dstAngle), (float)Math.Sin(dstAngle)) * _radius; + _beforeStartSrc = CommonMath.Transform2D(_src, + (_direction >= 0 ? _angleStart : _angleEnd) + 90 * _direction, new Vector2(-1000, 0)); + _afterEndDst = CommonMath.Transform2D(_dst, (_direction >= 0 ? _angleEnd : _angleStart) + 90 * _direction, + new Vector2(1000, 0)); + _tangentSrc = QueryTangentPoint(_src).Angle; + _tangentDst = QueryTangentPoint(_dst).Angle; + } + + private Vector2 _center; + private float _radius, _angleStart, _angleEnd; + private int _direction; + + private float _totalTh, _totalLen; + private Vector2 _src, _dst; + private float _tangentSrc, _tangentDst; + private Vector2 _beforeStartSrc, _afterEndDst; + + private Vector2[] _visPoints = Array.Empty(); + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Geometries/NurbsCurve.cs b/CommonUsage-MultiVehicleSync/commonusage/Geometries/NurbsCurve.cs new file mode 100644 index 0000000..aa45227 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Geometries/NurbsCurve.cs @@ -0,0 +1,349 @@ +using CommonUsage.Mathematics; +using System.Collections.Generic; +using System.Numerics; +using System; +using System.Linq; + +namespace CommonUsage.Geometries +{ + public class NurbsCurve : AbstractGeometry + { + + public NurbsCurve(List controlPoints, List weights, List knotVector, int frame = 100) + { + _controlPoints = controlPoints; + _weights = weights; + _knotVector = knotVector; + _frame = frame; + InitializeNurbs(); + } + + public override void Visualize(Action processDot, Action processLine, bool visExtendedPart = false) + { + if (VisualizeOption.DrawAuxiliary) + for (var i = 0; i < _controlPoints.Count - 1; ++i) + { + processLine(new VisLine(_controlPoints[i], _controlPoints[i + 1], + false, false, VisualizeOption.AuxiliaryColor)); + if (i == 0) continue; + processDot(new VisDot(_controlPoints[i], VisualizeOption.AuxiliaryColor)); + } + + for (var i = 0; i < _nurbsPoints.Count - 1; ++i) + { + if (Direction == -1) + { + processLine(new VisLine(_nurbsPoints[i + 1], _nurbsPoints[i], + false, i == (int)(_nurbsPoints.Count / 2), VisualizeOption.MainColor, 2)); + } + else + { + processLine(new VisLine(_nurbsPoints[i], _nurbsPoints[i + 1], + false, i == (int)(_nurbsPoints.Count / 2), VisualizeOption.MainColor, 2)); + } + } + } + public (Vector2 Point, int Id) QueryPoint(Vector2 point) + { + var p = new Vector2(); + var id = -1; + var bestDistance = float.MaxValue; + + var hashes = _bias.Select(bb => CalculateHash(point, 100, bb.X, bb.Y)).ToList(); + + void TryQuery(Dictionary> dict, List hashList) + { + foreach (var hash in hashList) + { + if (!dict.TryGetValue(hash, out var ll)) continue; + foreach (var (q, qId) in ll) + { + var d = Vector2.Distance(q, point); + if (d < bestDistance) + { + p = q; + id = qId; + bestDistance = d; + } + } + } + } + + if (id == -1) + { + // todo: improve the way to find closest point if mappings fail + (p, id) = _nurbsPoints.Select((p, i) => (p, i)) + .OrderBy(pair => CommonMath.dist(pair.p.X, pair.p.Y, point.X, point.Y)).First(); + } + + return (p, id); + } + + private uint CalculateHash(Vector2 point, float gridSize, int xBias = 0, int yBias = 0) + { + return (uint)(((int)((point.X - _minX) / gridSize) + xBias) << 16 + (((int)((point.Y - _minY) / gridSize) + yBias) & 0xffff)); + } + + private readonly List<(int X, int Y)> _bias = new() + { + new(-1, -1), new(-1, 0), new(-1, 1), + new(0, -1), new(0, 0), new(0, 1), + new(1, -1), new(1, 0), new(1, 1), + }; + + public override (Vector2 Pt, float Angle, float Bias, float Position) QueryTangentPoint(Vector2 point) + { + var (p, id) = QueryPoint(point); + var tangent = _tangents[id]; + var (bias, lp, fd) = CommonMath.Project2DLine(point, p, tangent); + var next = fd > 0 ? id + 1 : id - 1; + if (next > 0 && next < _tangents.Count)//线性插值 + { + var (_, _, t) = CommonMath.Project2DLine(point, _nurbsPoints[id], _nurbsPoints[next]); + var partial = t / Vector2.Distance(_nurbsPoints[id], _nurbsPoints[next]); + if (partial >= 0 && partial <= 1) + { + tangent = CommonMath.RoundTh(_tangents[id] + + partial * CommonMath.RoundTh(_tangents[next] - _tangents[id])); + if (CommonMath.RoundTh(_tangents[next] - _tangents[id]) > 5) + Console.WriteLine($"Nurbs tangents bug, tanget: {id}:{_tangents[id]} {next}:{_tangents[next]}"); + } + else Console.WriteLine("Nurbs tangents bug"); + } + return (lp, tangent, bias, fd + _sumDistances[id]); + } + + public override float QueryCurvature(float position) + { + int id = _sumDistances.Count - 1; + if (position <= 0) id = 0; + else + { + for (int i = 1; i < _sumDistances.Count; i++) + { + if (position > _sumDistances[i - 1] && position <= _sumDistances[i]) + { + id = i; + break; + } + } + } + + var result = _curvatures[id]; + if (id > 0 && id < _sumDistances.Count - 1)//插值 + { + var partial = (position - _sumDistances[id - 1]) / (_sumDistances[id] - _sumDistances[id - 1]); + if (partial >= 0 && partial <= 1) result = (1 - partial) * _curvatures[id - 1] + partial * _curvatures[id]; + else Console.WriteLine("Nurbs curvature bug"); + } + + return result; + } + + public Vector3 QueryNurbsPointsById(int id) + { + if (id < 0 || id > Frame) + { + Console.WriteLine($"QueryBezierPointsById out of range, Resolution:{Frame},id:{id}."); + return new Vector3(0, 0, 0); + } + + return new Vector3(_nurbsPoints[id].X, _nurbsPoints[id].Y, _tangents[id]); + } + public override float Length() + { + return _length; + } + + public int Order => _order; + public List ControlPoints => _controlPoints; + public List Weights => _weights; + public List KnotVector => _knotVector; + public int Frame => _frame; + + public int Direction = 1; + + + public void UpdateControlPoint(int id, Vector2 point) + { + _controlPoints[id] = point; + InitializeNurbs(); + } + public void UpdateNurbsWeihgts(int id, float weight) + { + _weights[id] = weight; + InitializeNurbs(); + + } + public void AddControlPoint(int id, Vector2 point) + { + _controlPoints.Insert(id, point); + InitializeNurbs(); + } + + public void RemoveControlPoint(int id) + { + _controlPoints.RemoveAt(id); + InitializeNurbs(); + } + + public Vector2 GetMidPoint() + { + return _nurbsPoints[(int)Math.Ceiling(_frame / 2d)]; + } + + private void InitializeNurbs() + { + _order = _controlPoints.Count - 1; + List> allpoints = new List>(); + List nurbsCurvePoints = new List(); + float delta = 1.0f / Frame; + + for (float t = 0; t <= 1; t += delta) + { + var (point, tangent) = DeBoorAlgorithm(t); + var points = new List + { + point, + point + tangent // Tangent endpoint + }; + allpoints.Add(points); + nurbsCurvePoints.Add(point); // Store the curve point separately + } + + _nurbsPoints = nurbsCurvePoints; + _tangents = Enumerable.Repeat(0f, _nurbsPoints.Count).ToList(); + _curvatures = Enumerable.Repeat(0f, _nurbsPoints.Count).ToList(); + + for (var id = 0; id < _nurbsPoints.Count - 1; ++id) + { + Vector2 p1 = _nurbsPoints[id]; + Vector2 p2 = _nurbsPoints[id + 1]; + + float tangentAngle = (float)Math.Atan2(p2.Y - p1.Y, p2.X - p1.X) * 180 / (float)Math.PI; + _tangents[id] = tangentAngle; + + // Calculate curvature using finite differences of tangent (second derivative approximation) + if (id > 0) + { + float previousTangent = _tangents[id - 1]; + float curvature = (float)(CommonMath.ThDiff(tangentAngle, previousTangent) * Math.PI / 180) / + (Vector2.Distance(p1, p2) / 1000); + _curvatures[id] = curvature; + } + } + + _curvatures.Insert(0, _curvatures[0]); + for (var id = 1; id < _curvatures.Count - 1; ++id) + { + _curvatures[id] = (_curvatures[id] + _curvatures[id + 1]) / 2; + } + + _remainDistances = Enumerable.Repeat(0f, _nurbsPoints.Count).ToList(); + _sumDistances = Enumerable.Repeat(0f, _nurbsPoints.Count).ToList(); + _remainDistances[_nurbsPoints.Count - 1] = 0; + + for (var i = _nurbsPoints.Count - 2; i >= 0; --i) + { + _remainDistances[i] = _remainDistances[i + 1] + Vector2.Distance(_nurbsPoints[i], _nurbsPoints[i + 1]); + } + + _sumDistances[0] = 0; + for (var i = 1; i < _nurbsPoints.Count; ++i) + { + _sumDistances[i] = _sumDistances[i - 1] + Vector2.Distance(_nurbsPoints[i], _nurbsPoints[i - 1]); + } + + _length = _sumDistances.Last(); + + _minX = _nurbsPoints.Min(pp => pp.X); + _minY = _nurbsPoints.Min(pp => pp.Y); + + var tmpList = _nurbsPoints.Select((point, index) => (point, index)).ToList(); + // GenerateGridMapping(ref _pointsMappingSmall, 100, tmpList); + // GenerateGridMapping(ref _pointsMappingBig, 1000, tmpList); + } + + + + + private float CalculateLength() + { + return _nurbsPoints.Zip(_nurbsPoints.Skip(1), Vector2.Distance).Sum(); + } + + private (Vector2, Vector2) DeBoorAlgorithm(float t) + { + Vector2 numerator = Vector2.Zero; + Vector2 tangentNumerator = Vector2.Zero; + float denominator = 0f; + + // Calculate the point on the curve + for (int i = 0; i < ControlPoints.Count; ++i) + { + float basis = BasisFunction(i, _order, t) * Weights[i]; + numerator += basis * ControlPoints[i]; + denominator += basis; + } + + Vector2 point = numerator / denominator; + + // Calculate the tangent vector using the analytical derivative + for (int i = 0; i < ControlPoints.Count; ++i) + { + float basisDerivative = BasisFunctionDerivative(i, _order, t) * Weights[i]; + tangentNumerator += basisDerivative * ControlPoints[i]; + } + Vector2 tangent = tangentNumerator / denominator; + + return (point, tangent); + } + + private float BasisFunction(int i, int p, float t) + { + if (p == 0) + return (KnotVector[i] <= t && t < KnotVector[i + 1]) ? 1.0f : 0.0f; + + float denom1 = KnotVector[i + p] - KnotVector[i]; + float term1 = denom1 == 0 ? 0 : ((t - KnotVector[i]) / denom1) * BasisFunction(i, p - 1, t); + + float denom2 = KnotVector[i + p + 1] - KnotVector[i + 1]; + float term2 = denom2 == 0 ? 0 : ((KnotVector[i + p + 1] - t) / denom2) * BasisFunction(i + 1, p - 1, t); + + return term1 + term2; + } + + private float BasisFunctionDerivative(int i, int k, float t) + { + if (k == 0) return 0; + + float denom1 = KnotVector[i + k] - KnotVector[i]; + float denom2 = KnotVector[i + k + 1] - KnotVector[i + 1]; + + float term1 = denom1 != 0 ? BasisFunction(i, k - 1, t) / denom1 : 0; + float term2 = denom1 != 0 ? (t - KnotVector[i]) * BasisFunctionDerivative(i, k - 1, t) / denom1 : 0; + + float term3 = denom2 != 0 ? -BasisFunction(i + 1, k - 1, t) / denom2 : 0; + float term4 = denom2 != 0 ? (KnotVector[i + k + 1] - t) * BasisFunctionDerivative(i + 1, k - 1, t) / denom2 : 0; + + return term1 + term2 + term3 + term4; + } + + private int _order; + private List _controlPoints; + private List _weights; + private List _knotVector; + private int _frame; + private List _nurbsPoints; + private List>> _tangentPoints; + private List _curvatures; + private List _sumDistances; + private List _remainDistances; + private float _minX, _minY; + private float _length; + private Dictionary> _pointsMappingSmall; + private Dictionary> _pointsMappingBig; + private List _tangents; + + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Geometries/Vectoriel.cs b/CommonUsage-MultiVehicleSync/commonusage/Geometries/Vectoriel.cs new file mode 100644 index 0000000..86b72a9 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Geometries/Vectoriel.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace CommonUsage.Geometries +{ + /// + /// MDCS数学类:向量。 + /// + public class Vectoriel + { + public Vectoriel() + { + _vec2 = Vector2.Zero; + _dir2 = Vector2.Normalize(_vec2); + _len = _vec2.Length(); + _angle = (float)(Math.Atan2(_vec2.Y, _vec2.X) / Math.PI * 180f); + } + + public Vectoriel(Vector2 vec) + { + _vec2 = vec; + _dir2 = Vector2.Normalize(_vec2); + _len = _vec2.Length(); + _angle = (float)(Math.Atan2(_vec2.Y, _vec2.X) / Math.PI * 180f); + } + + /// + /// 通过笛卡尔坐标系X和Y值构建向量。 + /// + /// + /// + /// + public static Vectoriel FromXY(float x, float y) + { + return new Vectoriel(new Vector2(x, y)); + } + + /// + /// 通过极坐标系的角度和距离值构建向量。 + /// + /// + /// + /// + public static Vectoriel FromAngleLen(float angle, float len) + { + var rad = angle / 180f * Math.PI; + return new Vectoriel(new Vector2((float)Math.Cos(rad), (float)Math.Sin(rad)) * len); + } + + public static implicit operator Vector2(Vectoriel vec) + { + return vec._vec2; + } + + public static explicit operator Vectoriel(Vector2 vec) + { + return FromXY(vec.X, vec.Y); + } + + public Vector2 Direction => _dir2; + + public float Length => _len; + + public float Angle => _angle; + + private Vector2 _vec2, _dir2; + private float _len, _angle; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Mathematics/CommonMath.cs b/CommonUsage-MultiVehicleSync/commonusage/Mathematics/CommonMath.cs new file mode 100644 index 0000000..df49d5b --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Mathematics/CommonMath.cs @@ -0,0 +1,774 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace CommonUsage.Mathematics +{ + + using T3 = Tuple; + using D3 = Tuple; + + public class CommonMath + { + public class PrimeEnumerator + { + public PrimeEnumerator(List items, Func process) + { + _n = items.Count; + _items = items; + _process = process; + + foreach (var pNum in _primes) + { + if (_n % pNum != 0) + { + _a = pNum; + _b = 11; + break; + } + } + } + + public void Enumerate() + { + using (var enumerator = Get().GetEnumerator()) + { + while (enumerator.MoveNext()) { } + } + } + + private readonly int _n, _a, _b; + private readonly int[] _primes = new[] { 29, 23, 19, 17, 13 }; + private List _items; + private readonly Func _process; + + private IEnumerable Get() + { + for (var i = 0; i < _n; ++i) + { + var id = (i * _a + _b) % _n; + yield return _process(_items[id]); + } + } + } + + private static IEnumerable> GetPermutationsInternal(IEnumerable list, int length) + { + if (length == 1) return list.Select(t => new T[] { t }); + + return GetPermutationsInternal(list, length - 1) + .SelectMany(t => list.Where(e => !t.Contains(e)), + (t1, t2) => t1.Concat(new T[] { t2 })); + } + + /// + /// 得到一组数据的所有排列。 + /// + /// 元素数据类型 + /// 所有待选元素 + /// 所选出的元素数量 + /// + public static List> GetPermutations(List list, int selectNum) + { + return GetPermutationsInternal(list, selectNum).Select(ll => ll.ToList()).ToList(); + } + + public static (float bias, Vector2 hPnt, float d) Project2DLine(Vector2 pnt, Vector2 segSt, + Vector2 segEnd) + { + var dir = Vector2.Normalize(segEnd - segSt); + var fd = Vector2.Dot(pnt - segSt, dir); + var hPnt = segSt + fd * dir; + var bias = dir.X * (pnt.Y-segSt.Y) - (pnt.X-segSt.X) * dir.Y; + return (bias, hPnt, fd); + } + + public static (float bias, Vector2 hPnt, float fd) Project2DLine(Vector2 pnt, Vector2 segSt, float tangent) + { + var dir = new Vector2((float)System.Math.Cos(tangent / 180 * System.Math.PI), (float)System.Math.Sin(tangent / 180 * System.Math.PI)); + var fd = Vector2.Dot(pnt - segSt, dir); + var hPnt = segSt + fd * dir; + var bias = dir.X * (pnt.Y - segSt.Y) - (pnt.X - segSt.X) * dir.Y; + return (bias, hPnt, fd); + } + + public class LineEqu + { + public double A, B, C, ln, dAB; + public double px1, px2, py1, py2; + public float midX; + public float midY; + } + // Fit line with PCA. + public LineEqu CalcLine(IEnumerable tls) + { + var lidarPoint2Ds = tls as Vector2[] ?? tls.ToArray(); + float fx = lidarPoint2Ds.Average(f => f.X); + float fy = lidarPoint2Ds.Average(f => f.Y); + float fxx = lidarPoint2Ds.Average(f => f.X * f.X); + float fxy = lidarPoint2Ds.Average(f => f.X * f.Y); + float fyy = lidarPoint2Ds.Average(f => f.Y * f.Y); + float a = fxx - fx * fx, b = fxy - fx * fy, c = fyy - fy * fy; + double sqt = System.Math.Sqrt((a - c) * (a - c) + 4 * b * b); + double l1 = a + c + sqt; + double l2 = a + c - sqt; + double dx, dy; + if (System.Math.Abs(a - l1 / 2) > System.Math.Abs(c - l1 / 2)) + { + dy = l1 / 2 - a; dx = b; + } + else + { + dx = l1 / 2 - c; dy = b; + } + double norm = System.Math.Sqrt(dx * dx + dy * dy); + dx /= norm; dy /= norm; + double A = dy, B = -dx, C = dx * fy - dy * fx; + double dAB = System.Math.Sqrt(A * A + B * B); + + return new CommonMath.LineEqu + { + A = A, + B = B, + C = C, + ln = lidarPoint2Ds.Average(p => System.Math.Abs(p.X * A + p.Y * B + C) / dAB), + midX = fx, + midY = fy + }; + } + + public static double QuadInterp3(double[] confsF) + { + if (confsF[0] > confsF[1] && confsF[0] > confsF[2]) + { + //printf("left overflow...\n"); + return -1; + } + + if (confsF[1] > confsF[0] && confsF[1] > confsF[2]) + { + return (-(confsF[2] - confsF[0]) / 2.0f / (confsF[0] + confsF[2] - 2.0f * confsF[1] + 0.0001f)); + } + if (confsF[2] > confsF[0] && confsF[2] > confsF[1]) + { + //printf("right overflow...\n"); + return 1; + } + return 0; + } + + public static double cross(PointF O, PointF A, PointF B) + { + return (A.X - O.X) * (B.Y - O.Y) - (A.Y - O.Y) * (B.X - O.X); + } + + public static List GetConvexHull(List points) + { + if (points == null) + return null; + + if (points.Count() <= 1) + return points; + + int n = points.Count(), k = 0; + List H = new List(new PointF[2 * n]); + + points.Sort((a, b) => + a.X == b.X ? a.Y.CompareTo(b.Y) : a.X.CompareTo(b.X)); + + // Build lower hull + for (int i = 0; i < n; ++i) + { + while (k >= 2 && cross(H[k - 2], H[k - 1], points[i]) <= 0) + k--; + H[k++] = points[i]; + } + + // Build upper hull + for (int i = n - 2, t = k + 1; i >= 0; i--) + { + while (k >= t && cross(H[k - 2], H[k - 1], points[i]) <= 0) + k--; + H[k++] = points[i]; + } + + return H.Take(k - 1).ToList(); + } + + public static bool IsPointInPolygon4(PointF[] polygon, PointF testPoint) + { + // ray casting odd even test. + bool result = false; + int j = polygon.Count() - 1; + for (int i = 0; i < polygon.Count(); i++) + { + if (polygon[i].Y < testPoint.Y && polygon[j].Y >= testPoint.Y || + polygon[j].Y < testPoint.Y && polygon[i].Y >= testPoint.Y) + { + if (polygon[i].X + (testPoint.Y - polygon[i].Y) / (polygon[j].Y - polygon[i].Y) * + (polygon[j].X - polygon[i].X) < testPoint.X) + { + result = !result; + } + } + + j = i; + } + + return result; + } + + public static double Exp(double val) + { + if (val < -20) return 0.0000001; + if (val > 20) return 99999999999999; + long tmp = (long)(1512775 * val + 1072632447); + return BitConverter.Int64BitsToDouble(tmp << 32); + } + public static double gaussmf(double x, double sig, double c) + { + return Exp(-(x - c) * (x - c) / (2 * sig * sig)); + } + + public static float Exp(float x) + { + if (x < -10) return 0; + if (x > 10) return 99999999999999; + x = 1.0f + x / 64f; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + x *= x; + return x; + } + public static float gaussmf(float x, float sig, float c) + { + return Exp(-(x - c) * (x - c) / (2 * sig * sig)); + } + + public static D3 Transform2D(D3 src, D3 t) + { + var rth = src.Item3 / 180.0 * System.Math.PI; + var p1dtx = (src.Item1 + System.Math.Cos(rth) * t.Item1 - + System.Math.Sin(rth) * t.Item2); + var p1dty = (src.Item2 + System.Math.Sin(rth) * t.Item1 + + System.Math.Cos(rth) * t.Item2); + var p1dtth = src.Item3 + t.Item3; + return Tuple.Create(p1dtx, p1dty, p1dtth); + } + + public struct LngLatToXY + { + public double scale; + public double rad; + public double biasX, biasY; + } + + public static LngLatToXY GetTransformLngLatToXY(Vector2 lnglat1, Vector2 xy1, Vector2 lnglat2, Vector2 xy2) + { + var scale = (xy1 - xy2).Length() / (lnglat1 - lnglat2).Length(); + var dxy = (xy1 - xy2); + var dlnglat = lnglat1 - lnglat2; + var rad = System.Math.Atan2(dxy.X, dxy.Y) - System.Math.Atan2(dlnglat.X, dlnglat.Y); + var intm = lnglat1 * scale; + var biasX = xy1.X - (intm.X * System.Math.Cos(rad) - intm.Y * System.Math.Sin(rad)); + var biasY = xy1.Y - (intm.X * System.Math.Sin(rad) + intm.Y * System.Math.Cos(rad)); + return new LngLatToXY {rad = rad, biasX = biasX, biasY = biasY, scale = scale}; + } + + public Vector2 TransformLngLatToXY(Vector2 lnglat, LngLatToXY t) + { + var intm = lnglat * (float) t.scale; + return new Vector2((float) (intm.X * System.Math.Cos(t.rad) - intm.Y * System.Math.Sin(t.rad) + t.biasX), + (float) (intm.X * System.Math.Sin(t.rad) + intm.Y * System.Math.Cos(t.rad) + t.biasY)); + } + + public static D3 ReverseTransform(D3 dest, D3 t) + { + var rth = (dest.Item3 - t.Item3) / 180.0 * System.Math.PI; + var nxT = (dest.Item1 - System.Math.Cos(rth) * t.Item1 + + System.Math.Sin(rth) * t.Item2); + var nyT = (dest.Item2 - System.Math.Sin(rth) * t.Item1 - + System.Math.Cos(rth) * t.Item2); + var pth = dest.Item3 - t.Item3; + return Tuple.Create(nxT, nyT, pth); + } + + public static D3 SolveTransform2D(D3 src, D3 dest) + { + var th = dest.Item3 - src.Item3; + th = (th - System.Math.Round((th) / 360.0f) * 360); + var rth = src.Item3 / 180.0 * System.Math.PI; + var x = ((dest.Item1 - src.Item1) * System.Math.Cos(rth) + + (dest.Item2 - src.Item2) * System.Math.Sin(rth)); + var y = (-(dest.Item1 - src.Item1) * System.Math.Sin(rth) + + (dest.Item2 - src.Item2) * System.Math.Cos(rth)); + return Tuple.Create(x, y, th); + } + + public static T3 Transform2D(T3 src, T3 t) + { + var rth = src.Item3 / 180.0 * System.Math.PI; + var p1dtx = (float)(src.Item1 + System.Math.Cos(rth) * t.Item1 - + System.Math.Sin(rth) * t.Item2); + var p1dty = (float)(src.Item2 + System.Math.Sin(rth) * t.Item1 + + System.Math.Cos(rth) * t.Item2); + var p1dtth = src.Item3 + t.Item3; + return Tuple.Create(p1dtx, p1dty, p1dtth); + } + + public static Vector2 Transform2D(Vector3 src, Vector3 t) + { + var tup = Transform2D(Tuple.Create(src.X, src.Y, src.Z), Tuple.Create(t.X, t.Y, t.Z)); + return new Vector2(tup.Item1, tup.Item2); + } + + public static Vector2 Transform2D(Vector2 srcPos, float srcTh, Vector2 dt, float dth = 0) + { + var tup = Transform2D(Tuple.Create(srcPos.X, srcPos.Y, srcTh), Tuple.Create(dt.X, dt.Y, dth)); + return new Vector2(tup.Item1, tup.Item2); + } + + public static T3 ReverseTransform(T3 dest, T3 t) + { + var rth = (dest.Item3 - t.Item3) / 180.0 * System.Math.PI; + var nxT = (float)(dest.Item1 - System.Math.Cos(rth) * t.Item1 + + System.Math.Sin(rth) * t.Item2); + var nyT = (float)(dest.Item2 - System.Math.Sin(rth) * t.Item1 - + System.Math.Cos(rth) * t.Item2); + var pth = dest.Item3 - t.Item3; + return Tuple.Create(nxT, nyT, pth); + } + + public static T3 SolveTransform2D(T3 src, T3 dest) + { + var th = dest.Item3 - src.Item3; + th = (float)(th - System.Math.Round((th) / 360.0f) * 360); + var rth = src.Item3 / 180.0 * System.Math.PI; + var x = (float)((dest.Item1 - src.Item1) * System.Math.Cos(rth) + + (dest.Item2 - src.Item2) * System.Math.Sin(rth)); + var y = (float)(-(dest.Item1 - src.Item1) * System.Math.Sin(rth) + + (dest.Item2 - src.Item2) * System.Math.Cos(rth)); + return Tuple.Create(x, y, th); + } + + public static Vector2 SolveTransform2D(Vector2 srcPos, float srcTh, Vector2 dt, float dth = 0) + { + var tup = SolveTransform2D(Tuple.Create(srcPos.X, srcPos.Y, srcTh), Tuple.Create(dt.X, dt.Y, dth)); + return new Vector2(tup.Item1, tup.Item2); + } + + public static double dist(double x1, double y1, double x2, double y2) + { + return System.Math.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); + } + + [StructLayout(LayoutKind.Explicit)] + private struct FloatIntUnion + { + [FieldOffset(0)] public float f; + + [FieldOffset(0)] public int tmp; + } + + public static float Sqrt(float z) + { + FloatIntUnion u; + u.tmp = 0; + u.f = z; + u.tmp -= 1 << 23; /* Subtract 2^m. */ + u.tmp >>= 1; /* Divide by 2. */ + u.tmp += 1 << 29; /* Add ((b + 1) / 2) * 2^m. */ + return u.f; + } + + public static float dist2(float x1, float y1, float x2, float y2) + { + return ((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); + } + + + public static float d2(float x1, float y1, float x2, float y2) + { + return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2); + } + + public static float ThAverage(List angles) + { + var anchor = angles[0]; + var diff = 0f; + foreach (var angle in angles) + diff += ThDiff(angle, anchor); + return RoundTh(anchor + diff / angles.Count); + } + + public static float ThDiff(float th1, float th2) + { + return (float)(th1 - th2 - + System.Math.Round((th1 - th2) / 360.0f) * 360); + } + + public static double ThDiff(double th1, double th2) + { + return th1 - th2 - + System.Math.Round((th1 - th2) / 360.0f) * 360; + } + + public static double refine(double x) + { + if (x < 1 && x > -1) return x; + if (x > 1) + return (2 / (1 + System.Math.Exp(-((x - 1) * 2)))); + return (2 / (1 + System.Math.Exp(-((x + 1) * 2)))) - 2; + } + + /// + /// 求点p到两点式直线p1p2的距离 + /// + /// 点p的x坐标 + /// 点p的y坐标 + /// 直线点p1的x坐标 + /// 直线点p1的y坐标 + /// 直线点p2的x坐标 + /// 直线点p2的y坐标 + /// + public static double Point2LineDist(double x, double y, double x1, double y1, double x2, double y2) + { + double a1 = -(y1 - y2) / 10; + double b1 = (x1 - x2) / 10; + double c1 = (x1 * (y1 - y2) - y1 * (x1 - x2)) / 10; + return System.Math.Abs(a1 * x + b1 * y + c1) / System.Math.Sqrt(a1 * a1 + b1 * b1); + } + + public static double Point2LineDist(Vector2 p, LineSegment ll) + { + double a1 = -(ll.Src.Y - ll.Dst.Y) / 10; + double b1 = (ll.Src.X - ll.Dst.X) / 10; + double c1 = (ll.Src.X * (ll.Src.Y - ll.Dst.Y) - ll.Src.Y * (ll.Src.X - ll.Dst.X)) / 10; + return System.Math.Abs(a1 * p.X + b1 * p.Y + c1) / System.Math.Sqrt(a1 * a1 + b1 * b1); + } + + /// + /// 两条两点式直线间的夹角 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// 角度制 + public static double AngleBetweenLines(double x1, double y1, double x2, double y2, double x3, double y3, + double x4, double y4) + { + var vec1 = new Vector2((float)(x2 - x1), (float)(y2 - y1)); + var vec2 = new Vector2((float)(x4 - x3), (float)(y4 - y3)); + return System.Math.Acos(System.Math.Abs(Vector2.Dot(vec1, vec2) / vec1.Length() / vec2.Length())) / System.Math.PI * 180; + } + + public static double AngleBetweenLines(LineSegment ls1, LineSegment ls2) + { + return AngleBetweenLines(ls1.Src.X, ls1.Src.Y, ls1.Dst.X, ls1.Dst.Y, ls2.Src.X, ls2.Src.Y, ls2.Dst.X, + ls2.Dst.Y); + } + + /// + /// 两向量间夹角 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// 角度制 + public static double AngleBetweenVectors(double x1, double y1, double x2, double y2, double x3, double y3, + double x4, double y4) + { + var vec1 = new Vector2((float)(x2 - x1), (float)(y2 - y1)); + var vec2 = new Vector2((float)(x4 - x3), (float)(y4 - y3)); + return System.Math.Acos(Vector2.Dot(vec1, vec2) / vec1.Length() / vec2.Length()) / System.Math.PI * 180; + } + + /// + /// 两向量间夹角. + /// + /// + /// + /// 角度制 + public static double AngleBetweenVectors(Vector2 vec1, Vector2 vec2) + { + return System.Math.Acos(Vector2.Dot(vec1, vec2) / vec1.Length() / vec2.Length()) / System.Math.PI * 180; + } + + public static double AngleBetweenVectors(Vector3 vector1, Vector3 vector2) + { + float dotProduct = Vector3.Dot(vector1, vector2); + float magnitude1 = vector1.Length(); + float magnitude2 = vector2.Length(); + float cosine = dotProduct / (magnitude1 * magnitude2); + + return System.Math.Acos(cosine) / System.Math.PI * 180; + } + + /// + /// 求点到直线的垂足 + /// + /// + /// + /// + /// + /// + /// + /// + public static (double, double) PerpendicularPoint(double x, double y, double x1, double y1, double x2, double y2) + { + double lx = x2 - x1, ly = y2 - y1, dAB = lx * lx + ly * ly; + var u = ((x - x1) * lx + (y - y1) * ly) / dAB; + return new(x1 + u * lx, y1 + u * ly); + } + + public static Vector2 PerpendicularPoint(Vector2 p, LineSegment ls) + { + double lx = ls.Dst.X - ls.Src.X, ly = ls.Dst.Y - ls.Src.Y, dAB = lx * lx + ly * ly; + var u = ((p.X - ls.Src.X) * lx + (p.Y - ls.Src.Y) * ly) / dAB; + return new Vector2((float)(ls.Src.X + u * lx), (float)(ls.Src.Y + u * ly)); + } + + public static double PerpendicularPosition(double x, double y, double x1, double y1, double x2, double y2) + { + double lx = x2 - x1, ly = y2 - y1; + var dAB = CommonMath.Sqrt((float)(lx * lx + ly * ly)); + lx /= dAB; + ly /= dAB; + return (x - x1) * lx + (y - y1) * ly; + } + + + /// + /// 最小二乘法拟合直线,得到两点式。 + /// + /// 待拟合的点集,应至少有2个点。 + /// 检查是否所有点距直线的距离均小于maxDist2Line,若为-1则不检查。 + /// 返回两点式的两个端点坐标。若坐标为全0,则拟合失败。 + public static (bool, Vector2, Vector2) FitLineSegment(List pts, double maxDist2Line = -1) + { + if (pts.Count < 2) + { + Console.WriteLine($"Points too Few! {pts.Count}! Cannot perform line fitting!", + MethodBase.GetCurrentMethod()?.Name ?? "FitLine"); + return (false, Vector2.Zero, Vector2.Zero); + }; + + // y = kx + b + double A = 0, B = 0, C = 0, D = 0; + foreach (var p in pts) + { + A += p.X * p.X; + B += p.X; + C += p.X * p.Y; + D += p.Y; + } + + var tmp = A * pts.Count - B * B; + var k = (C * pts.Count - B * D) / tmp; + var b = (A * D - C * B) / tmp; + + double x1 = 0, + y1 = k * x1 + b, + x2 = 1000, + y2 = k * x2 + b; + + double CalcDist(ref bool fail, ref Vector2 endP, ref Vector2 endQ) + { + double distSum = 0; + double lx = x2 - x1, ly = y2 - y1, dAB = lx * lx + ly * ly; + double minU = double.MaxValue, maxU = double.MinValue; + + foreach (var p in pts) + { + var u = ((p.X - x1) * lx + (p.Y - y1) * ly) / dAB; + var perp = new Vector2((float)(x1 + u * lx), (float)(y1 + u * ly)); + if (u < minU) + { + endP = perp; + minU = u; + } + if (u > maxU) + { + endQ = perp; + maxU = u; + } + + var curDist = dist(perp.X, perp.Y, p.X, p.Y); + if (maxDist2Line > -1 && curDist > maxDist2Line) fail = true; + distSum += curDist; + } + + return distSum; + } + + var kbFail = false; + Vector2 endP1 = new Vector2(), endQ1 = new Vector2(); + double kbDist = CalcDist(ref kbFail, ref endP1, ref endQ1); + + // x = my + n + A = 0; + B = 0; + C = 0; + D = 0; + foreach (var p in pts) + { + A += p.X * p.Y; + B += p.Y * p.Y; + C += p.Y; + D += p.X; + } + + tmp = C * C - B * pts.Count; + var m = (C * D - A * pts.Count) / tmp; + var n = (A * C - B * D) / tmp; + y1 = 0; + x1 = m * y1 + n; + y2 = 1000; + x2 = m * y2 + n; + + var mnFail = false; + Vector2 endP2 = new Vector2(), endQ2 = new Vector2(); + double mnDist = CalcDist(ref mnFail, ref endP2, ref endQ2); + + Vector2 endP = endP1, endQ = endQ1; + if (mnDist < kbDist) + { + if (mnFail) return (false, new Vector2(), new Vector2()); + endP = endP2; + endQ = endQ2; + } + else if (kbFail) return (false, new Vector2(), new Vector2()); + + return (true, endP, endQ); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int toId(int x, int y, int z) + { + return (x * 1140671485 + 12820163) ^ (y * 134775813 + 1) ^ (z * 1103515245 + 12345); + } + + public class Clustering + { + public int numIteration = 3; + public int itemNumThreshold = 10; + + public Func inRange; + public Func, T> average; + private readonly List _inputData; + private Dictionary> _clusters = new Dictionary>(); + + public Clustering(List data, Func inRange, Func, T> average) + { + _inputData = data; + this.inRange = inRange; + this.average = average; + } + + public Dictionary> GetClusters() + { + var tmp = new List<(T center, List items)>(); + + for (var iter = 0; iter < numIteration; iter++) + { + tmp = tmp.Where(cluster => cluster.items.Count > itemNumThreshold) + .Select(cluster => (average(cluster.items), new List())).ToList(); + + foreach (var data in _inputData) + { + var added = false; + foreach (var cluster in tmp) + { + if (inRange(cluster.center, data)) + { + cluster.items.Add(data); + added = true; + break; + } + } + + if (!added) + tmp.Add((data, new List() { data })); + } + } + + _clusters = tmp.Where(cluster => cluster.items.Count > itemNumThreshold) + .ToDictionary(cluster => cluster.center, cluster => cluster.items); + return _clusters; + } + } + + public static (bool, Vector2) TwoLinesIntersection(Vector2 A, Vector2 B, Vector2 C, Vector2 D) + { + // Line AB represented as a1x + b1y = c1 + double a1 = B.Y - A.Y; + double b1 = A.X - B.X; + double c1 = a1 * (A.X) + b1 * (A.Y); + + // Line CD represented as a2x + b2y = c2 + double a2 = D.Y - C.Y; + double b2 = C.X - D.X; + double c2 = a2 * (C.X) + b2 * (C.Y); + + double determinant = a1 * b2 - a2 * b1; + + if (determinant == 0) + { + // The lines are parallel. This is simplified + // by returning a pair of FLT_MAX + return new(false, new Vector2()); + } + else + { + double x = (b2 * c1 - b1 * c2) / determinant; + double y = (a1 * c2 - a2 * c1) / determinant; + return (true, new Vector2((float)x, (float)y)); + } + } + + public static bool IsAtLeft(Vector2 anchor, Vector2 dest, Vector2 p) + { + var v1 = new Vector3(anchor - p, 0); + var v2 = new Vector3(dest - p, 0); + return Vector3.Cross(v1, v2).Z > 0; + } + + /// + /// 将角度转化至-180到180度的范围内。 + /// + /// + /// + public static double RoundTh(double th) + { + return th - System.Math.Round(th / 360) * 360; + } + + /// + /// 将角度转化至-180到180度的范围内。 + /// + /// + /// + public static float RoundTh(float th) + { + return th - (float)System.Math.Round(th / 360f) * 360f; + } + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Mathematics/LineSegment.cs b/CommonUsage-MultiVehicleSync/commonusage/Mathematics/LineSegment.cs new file mode 100644 index 0000000..87e0405 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Mathematics/LineSegment.cs @@ -0,0 +1,161 @@ +using System; +using System.Numerics; + +namespace CommonUsage.Mathematics +{ + /// + /// 表示一条线段。 + /// + public class LineSegment + { + /// + /// 默认构造函数。所有坐标初始化为0。 + /// + public LineSegment() + { + + } + + /// + /// 使用两个端点初始化一段2D线段。 + /// + /// 线段起点。 + /// 线段终点。 + public LineSegment(Vector2 src, Vector2 dst) + { + Src = src; + Dst = dst; + } + + /// + /// 使用两个端点初始化一段3D线段。 + /// + /// 线段起点。 + /// 线段终点。 + public LineSegment(Vector3 src, Vector3 dst) + { + Src3D = src; + Dst3D = dst; + } + + /// + /// 使用两个端点初始化一条线段,2D。 + /// + /// 线段起点x坐标。 + /// 线段起点y坐标。 + /// 线段终点x坐标。 + /// 线段终点y坐标。 + public LineSegment(double x1, double y1, double x2, double y2) + { + Src = new Vector2((float)x1, (float)y1); + Dst = new Vector2((float)x2, (float)y2); + } + + /// + /// 使用两个端点初始化一条线段,2D。 + /// + /// 线段起点x坐标。 + /// 线段起点y坐标。 + /// 线段起点y坐标。 + /// 线段终点x坐标。 + /// 线段终点y坐标。 + /// 线段终点y坐标。 + public LineSegment(double x1, double y1, double z1, double x2, double y2, double z2) + { + Src3D = new Vector3((float)x1, (float)y1, (float)z1); + Dst3D = new Vector3((float)x2, (float)y2, (float)z2); + } + + /// + /// 返回线段长度,2D。 + /// + /// + public double Length() + { + return Vector2.Distance(Src, Dst); + } + + /// + /// 返回线段长度,3D。 + /// + /// + public double Length3D() + { + return Vector3.Distance(Src3D, Dst3D); + } + + /// + /// 返回线段与x轴正方向夹角度数,角度制。 + /// + /// + public double Angle() + { + return Math.Atan2(Dst.Y - Src.Y, Dst.X - Src.X) / Math.PI * 180; + } + + /// + /// 返回一段方向相反的线段。 + /// + /// + public LineSegment Reverse() + { + return new LineSegment(Dst3D, Src3D); + } + + /// + /// 线段2D起点。 + /// + public Vector2 Src + { + get => new(_srcX, _srcY); + set + { + _srcX = value.X; + _srcY = value.Y; + } + } + + /// + /// 线段2D终点。 + /// + public Vector2 Dst + { + get => new(_dstX, _dstY); + set + { + _dstX = value.X; + _dstY = value.Y; + } + } + + /// + /// 线段3D起点。 + /// + public Vector3 Src3D + { + get => new(_srcX, _srcY, _srcZ); + set + { + _srcX = value.X; + _srcY = value.Y; + _srcZ = value.Z; + } + } + + /// + /// 线段3D终点。 + /// + public Vector3 Dst3D + { + get => new(_dstX, _dstY, _dstZ); + set + { + _dstX = value.X; + _dstY = value.Y; + _dstZ = value.Z; + } + } + + private float _srcX, _srcY, _srcZ, _dstX, _dstY, _dstZ; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Properties/launchSettings.json b/CommonUsage-MultiVehicleSync/commonusage/Properties/launchSettings.json new file mode 100644 index 0000000..53c176e --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "CommonUsage": { + "commandName": "Project" + }, + "配置文件 1": { + "commandName": "Executable", + "executablePath": "D:\\Code\\Core\\Medulla\\build\\Medulla.exe", + "workingDirectory": "D:\\Code\\Core\\Medulla\\build\\" + } + } +} \ No newline at end of file diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/CommunicationProtocolFactory.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/CommunicationProtocolFactory.cs new file mode 100644 index 0000000..3376c97 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/CommunicationProtocolFactory.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050 +{ + public class CommunicationProtocolFactory + { + public static IVDACommunicationProtocol CreateProtocol(string protocolType, string host, int port) + { + return protocolType.ToLower() switch + { + "http" => new HTTPCommunication(host, port), + "mqtt" => new MQTTCommunication(host, port), + _ => throw new NotSupportedException($"Protocol {protocolType} is not supported") + }; + } + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/HTTPCommunication.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/HTTPCommunication.cs new file mode 100644 index 0000000..5ff34a8 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/HTTPCommunication.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using CommonUsage.Protocols.VDA5050.Messages; +using FundamentalLib; +using Newtonsoft.Json; + +namespace CommonUsage.Protocols.VDA5050 +{ + public class HTTPCommunication : IVDACommunicationProtocol + { + private readonly string _host; + private readonly int _port; + + public HTTPCommunication(string host, int port) + { + _host = host; + _port = port; + } + + public async Task PublishConnectionStatus(string status) + { + var message = new connectionMessage() + { + serialNumber = "test-01", + headerId = 1, + timestamp = DateTime.Now, + connectionState = status + }; + + await SendMessageAsync(message, "vda5050/connection"); + } + + public void SetupOrderListener(Action orderReceived) + { + PicoHttpServer.AddPostTextHandler("/order", new { }, (_, str) => + { + var order = JsonConvert.DeserializeObject(str); + orderReceived(order); + return ""; + }); + } + + public void SetUpInstanActionListener(Action onInstanceActionReceived) + { + PicoHttpServer.AddPostTextHandler("/instanceAction", new { }, (_, str) => + { + var instanceAction = JsonConvert.DeserializeObject(str); + onInstanceActionReceived(instanceAction); + return ""; + }); + } + + public void SetUpImmediateCommandListener(Action onChangeCarFieldsReceived) + { + throw new NotImplementedException(); + } + + public async Task SendMessageAsync(T message, string topic) + { + try + { + var url = $"http://{_host}:{_port}/{topic}"; + using var client = new HttpClient(); + var response = await client.PostAsync(url, new StringContent(JsonConvert.SerializeObject(message), Encoding.UTF8, "application/json")); + + if (!response.IsSuccessStatusCode) + { + Console.WriteLine($" >> Sending Message: Failed to send message. Status Code: {response.StatusCode}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error in sending message: {ex.Message}"); + } + } + + public async Task SendVisualizationMessageAsync(T msg, string topic) + { + throw new NotImplementedException(); + } + + public void SetupTestListener(Action testMsg) + { + throw new NotImplementedException(); + } + public async Task PublishFactSheet(factsheetMessage message) + { + throw new NotImplementedException(); + } + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/IVDACommunicationProtocol.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/IVDACommunicationProtocol.cs new file mode 100644 index 0000000..f8e998f --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/IVDACommunicationProtocol.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using CommonUsage.Protocols.VDA5050.Messages; + +namespace CommonUsage.Protocols.VDA5050 +{ + public interface IVDACommunicationProtocol + { + Task PublishConnectionStatus(string status); + Task PublishFactSheet(factsheetMessage msg); + void SetupOrderListener(Action orderReceived); + void SetUpInstanActionListener(Action onInstanceActionReceived); + void SetUpImmediateCommandListener(Action onChangeCarFieldsReceived); + Task SendMessageAsync(T message, string topic); + Task SendVisualizationMessageAsync(T message, string topic); + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/MQTTCommunication.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/MQTTCommunication.cs new file mode 100644 index 0000000..133fd46 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/MQTTCommunication.cs @@ -0,0 +1,319 @@ +using System.IO; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using CommonUsage.Protocols.VDA5050.Messages; +using MQTTnet; +using MQTTnet.Client; +using MQTTnet.Extensions.ManagedClient; +using MQTTnet.Packets; +using MQTTnet.Protocol; +using MQTTnet.Server; +using Newtonsoft.Json; +using FundamentalLib; +using CommonUsage.Protocols.VDA5050.Objects; +using FundamentalLib.MiscHelpers; + +namespace CommonUsage.Protocols.VDA5050 +{ + public class MQTTCommunication : IVDACommunicationProtocol + { + private readonly string _host; + private readonly int _port; + private readonly string _orderTopic = "vda5050/frldAGV/order"; + private readonly string _connectionTopic = "vda5050/frldAGV/connection"; + private readonly string _instanceAction = "vda5050/frldAGV/instantActions"; + private readonly string _factsheet = "vda5050/frldAGV/factsheet"; + private readonly string _changeCarFields = "vda5050/frldAGV/changeCarFields"; + + private IManagedMqttClient _client; + private IManagedMqttClient _visualizationClient; + + public MQTTCommunication(string host, int port) + { + _host = host; + _port = port; + InitializeClient(); + InitializeVisualizationClient(); + } + + private void InitializeClient() + { + var willMessage = new connectionMessage() + { + headerId = 1, + timestamp = DateTime.Now, + version = "00", + manufacturer = "frld", + serialNumber = "test-01", + connectionState = "CONNECTIONBROKEN" + }; + + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithClientId("AGV-Client-frldAGV") + .WithTcpServer(_host, _port) + .WithWillTopic(_connectionTopic) + .WithWillPayload(JsonConvert.SerializeObject(willMessage)) + .WithWillRetain(true) + .Build(); + + var managedMqttClientOptions = new ManagedMqttClientOptionsBuilder() + .WithClientOptions(mqttClientOptions) + .WithMaxPendingMessages(20) + .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage) + .Build(); + + _client = new MqttFactory().CreateManagedMqttClient(); + _client.StartAsync(managedMqttClientOptions).GetAwaiter().GetResult(); + Console.WriteLine($" >> MQTT client initialized and connected to broker at {_host} - {_port}"); + } + + private void InitializeVisualizationClient() + { + var mqttClientOptions = new MqttClientOptionsBuilder() + .WithClientId("AGV-Visualization") + .WithTcpServer(_host, _port) + .Build(); + + var managedMqttClientOptions = new ManagedMqttClientOptionsBuilder() + .WithClientOptions(mqttClientOptions) + .WithMaxPendingMessages(10) // Prevent overloading + .WithPendingMessagesOverflowStrategy(MqttPendingMessagesOverflowStrategy.DropOldestQueuedMessage) + .Build(); + + _visualizationClient = new MqttFactory().CreateManagedMqttClient(); + _visualizationClient.StartAsync(managedMqttClientOptions).GetAwaiter().GetResult(); + Console.WriteLine("MQTT Visualization client initialized."); + } + + public void SetUpImmediateCommandListener(Action onChangeCarFieldsReceived) + { + Console.WriteLine($"Subscribed to the topic: {_changeCarFields}"); + _client.SubscribeAsync(_changeCarFields).GetAwaiter().GetResult(); + + _client.ApplicationMessageReceivedAsync += async e => + { + if (e.ApplicationMessage.Topic == _changeCarFields) + { + var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); + LogMessage($"RECEIVE-{e.ApplicationMessage.Topic}", e.ApplicationMessage.Topic, payload); + //var script = JsonConvert.DeserializeObject(payload); + Console.WriteLine($"Change car field: {payload}"); + onChangeCarFieldsReceived(payload); + } + }; + } + + + public void SetUpInstanActionListener(Action onInstanceActionReceived) + { + Console.WriteLine($"Subscribed to the topic: {_instanceAction}"); + + // Subscribe to the instanceAction topic + _client.SubscribeAsync(_instanceAction).GetAwaiter().GetResult(); + + _client.ApplicationMessageReceivedAsync += async e => + { + if (e.ApplicationMessage.Topic == _instanceAction) + { + var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); + LogMessage($"RECEIVE-{e.ApplicationMessage.Topic}", e.ApplicationMessage.Topic, payload); + var actions = JsonConvert.DeserializeObject(payload); + Console.WriteLine($"Received instance action: Header ID = {actions.headerId}, Timestamp = {actions.timestamp}"); + foreach (var action in actions.actions) + { + Console.WriteLine($"Action ID: {action.actionId}, Type: {action.actionType}"); + } + + onInstanceActionReceived(actions); + } + }; + } + + public async Task PublishConnectionStatus(string status) + { + var message = new connectionMessage() + { + headerId = 1, + timestamp = DateTime.Now, + version = "00", + manufacturer = "frld", + serialNumber = "test-01", + connectionState = status + }; + + var payload = JsonConvert.SerializeObject(message); + + var content = new MqttApplicationMessageBuilder() + .WithTopic(_connectionTopic) + .WithPayload(payload) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .WithRetainFlag(true) + .Build(); + + await _client.EnqueueAsync(content); + LogMessage($"SEND-{_connectionTopic}", _connectionTopic, payload); + + //await SendMessageAsync(message, _connectionTopic); + } + + public async Task PublishFactSheet(factsheetMessage message) + { + + //await SendMessageAsync(message, _factsheet); + var payload = JsonConvert.SerializeObject(message); + + var content = new MqttApplicationMessageBuilder() + .WithTopic(_factsheet) + .WithPayload(payload) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) + //.WithRetainFlag(true) + .Build(); + + await _client.EnqueueAsync(content); + LogMessage($"SEND-{_factsheet}", _factsheet, payload); + + } + + public void SetupOrderListener(Action orderReceived) + { + // Subscribe to the orders topic + _client.SubscribeAsync(_orderTopic, MqttQualityOfServiceLevel.AtMostOnce).GetAwaiter().GetResult(); + _client.ApplicationMessageReceivedAsync += async e => + { + if (e.ApplicationMessage.Topic == _orderTopic) + { + var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); + LogMessage($"RECEIVE-{e.ApplicationMessage.Topic}", e.ApplicationMessage.Topic, payload); + var order = JsonConvert.DeserializeObject(payload); + + // Save the order message to a file for debugging + // SaveOrderToFile(payload); + orderReceived(order); + } + await Task.CompletedTask; + }; + } + + private void SaveOrderToFile(string orderJson) + { + try + { + // Specify the file path (e.g., orders_log.txt in the current directory) + string filePath = "orders_log.txt"; + + // Append the order JSON along with a timestamp + File.AppendAllText(filePath, $"{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} - {orderJson}{Environment.NewLine}"); + } + catch (Exception ex) + { + // Handle any exceptions that occur while writing to the file + Console.WriteLine($"Failed to save order to file: {ex.Message}"); + } + } + + public async Task SendMessageAsync(T message, string topic) + { + + var payload = JsonConvert.SerializeObject(message); + + var content = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) + .Build(); + + await _client.EnqueueAsync(content); + + if (topic != "vda5050/frldAGV/visualization") + { + + LogMessage($"SEND-{topic}", topic, payload); + } + + } + + public async Task SendVisualizationMessageAsync(T message, string topic) + { + if (_visualizationClient == null) return; // Ensure client is initialized + + var payload = JsonConvert.SerializeObject(message); + + var content = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce) // QoS 0 for lightweight visualization + .Build(); + + if (_visualizationClient.PendingApplicationMessagesCount < 5) // Prevent flooding + { + await _visualizationClient.EnqueueAsync(content); + } + else + { + Console.WriteLine("Skipping visualization update to avoid MQTT congestion."); + } + } + + + public void LogMessage(string direction, string topic, string payload) + { + string formattedPayload = payload; + string logDirectory = "Logs"; // Directory for log files + var filePreName = direction; + filePreName = filePreName.Replace("/", "_"); + string logFilePath = Path.Combine(logDirectory, filePreName + $"-{DateTime.Now:yyyy-MM-dd}.log"); + + if(!Directory.Exists(logDirectory)) Directory.CreateDirectory(logDirectory); + // Try to parse the payload as JSON and pretty-print it + try + { + var jsonObject = JsonConvert.DeserializeObject(payload); + formattedPayload = JsonConvert.SerializeObject(jsonObject, Formatting.Indented); + } + catch (JsonReaderException) + { + // If the payload is not valid JSON, just leave it as is + formattedPayload = payload; + } + + string logMessage = $"[{DateTime.Now:HH:mm:ss}] [{direction}] Topic: {topic}, Payload:\n{formattedPayload}\n"; + RotateLogFile(logFilePath, logDirectory); + + try + { + File.AppendAllText(logFilePath, logMessage + Environment.NewLine); + } + catch (Exception ex) + { + Console.WriteLine($"Error writing to log file: {ex.Message}"); + } + // Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] [{direction}] Topic: {topic}, Payload: {formattedPayload}"); + // DLog.Log($"[{DateTime.Now:HH:mm:ss}] [{direction}] Topic: {topic}, Payload: {formattedPayload}"); + } + + + private void RotateLogFile(string logFilePath, string logDirectory) + { + const long maxFileSize = 10 * 1024 * 1024; // 10 MB in bytes + + FileInfo fileInfo = new FileInfo(logFilePath); + if (fileInfo.Exists && fileInfo.Length > maxFileSize) + { + string archivePath = Path.Combine(logDirectory, $"log_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log"); + + try + { + File.Move(logFilePath, archivePath); // Rename the current log file + Console.WriteLine($"Log file rotated: {archivePath}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error rotating log file: {ex.Message}"); + } + } + } + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/connectionMessage.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/connectionMessage.cs new file mode 100644 index 0000000..a4f5c70 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/connectionMessage.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Messages +{ + public class connectionMessage + { + public int headerId; + public DateTime timestamp; + public string version = ""; + public string manufacturer = ""; + public string serialNumber = ""; + public string connectionState = ""; // Enum: {'ONLINE', 'OFFLINE', 'CONNECTIONBROKEN'} + + } + +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/errorMessage.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/errorMessage.cs new file mode 100644 index 0000000..9888fe2 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/errorMessage.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Messages +{ + public class errorMessage + { + public string serialNumber = ""; + public string errorCode = ""; // Unique error code + public string description = ""; // Error description + public string severity = ""; // Enum {'WARNING', 'FATAL'} + public DateTime timestamp; // Time of the error + } + +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/factsheetMessage.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/factsheetMessage.cs new file mode 100644 index 0000000..bcbd349 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/factsheetMessage.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; +using CommonUsage.Protocols.VDA5050.Objects; + +namespace CommonUsage.Protocols.VDA5050.Messages +{ + public class factsheetMessage + { + public int headerId; + public DateTime timestamp; + public string version = ""; + public string manufacturer = ""; + public string serialNumber = ""; + + public typeSpecification typeSpecification; + public physicalParameters physicalParameters; + public protocolLimits protocolLimits; + public protocolFeatures protocolFeatures; + public agvGeometry agvGeometry; + } + +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/instanceAction.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/instanceAction.cs new file mode 100644 index 0000000..e5c5f8d --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/instanceAction.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; +using CommonUsage.Protocols.VDA5050.Objects; + +namespace CommonUsage.Protocols.VDA5050.Messages +{ + public class instanceAction + { + + public uint headerId { get; set; } // Incremented for each new message. + public string timestamp { get; set; } // ISO 8601 UTC timestamp. + public string version { get; set; } // Protocol version. + public string manufacturer { get; set; } // AGV manufacturer. + public string serialNumber { get; set; } // Unique AGV serial number. + + public List actions { get; set; } + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/orderMessage.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/orderMessage.cs new file mode 100644 index 0000000..68516c7 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/orderMessage.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; +using CommonUsage.Protocols.VDA5050.Objects; + +namespace CommonUsage.Protocols.VDA5050.Messages +{ + public class orderMessage + { + public uint headerId; + public string timestamp = ""; + public string version = ""; + public string manufacturer = ""; + public string serialNumber = ""; + + public string orderId { get; set; } + + public uint orderUpdateId { get; set; } + + public node[] nodes { get; set; } + + public edge[] edges { get; set; } + + //public action[] action { get; set; } + } +} + diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/stateMessage.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/stateMessage.cs new file mode 100644 index 0000000..44b0438 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/stateMessage.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using CommonUsage.Protocols.VDA5050.Objects; + +namespace CommonUsage.Protocols.VDA5050.Messages +{ + /// + /// 6.10 Topic: "state" (from AGV to master control) + /// todo: complete all fields required by VDA5050 + /// + public class stateMessage + { + public uint headerId; + public string timestamp = ""; + public string version = ""; + public string manufacturer = ""; + public string serialNumber = ""; + /// + /// Unique order identification of the current order or the previously finished order. + /// The orderId is kept until a new order is received. + /// Empty string (""), if no previous orderId is available. + /// + public string orderId = ""; + + /// + /// Order update identification to identify, that an order update has been accepted by the AGV. + /// "0" if no previous orderUpdateId is available. + /// + public uint orderUpdatedId = 0; + + public string lastNodeId; + + public uint lastNodeSequenceId; + + /// + /// Array of nodeState objects that need to be traversed for fulfilling the order (empty array if idle) + /// + public nodeState[] nodeStates = []; + + /// + /// Array of edgeState objects that need to be traversed for fulfilling the order (empty array if idle) + /// + public edgeState[] edgeStates = []; + + public agvPosition agvPosition; + + public velocity velocity; + public load[] loads = []; + + public bool driving; + + public bool paused; + + public bool newBaseRequest; + + public double distanceSinceLastNode; + + public batteryState batteryState; + + public actionState[] actionStates = Array.Empty(); + public string operatingMode = ""; + public List errors { get; set; } = new List(); // Array of errorState objects + + public info[] information = []; + public safetyState safetyState; + + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/visualizationMessage.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/visualizationMessage.cs new file mode 100644 index 0000000..c895311 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Messages/visualizationMessage.cs @@ -0,0 +1,12 @@ +using CommonUsage.Protocols.VDA5050.Objects; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Messages +{ + public class visualizationMessage + { + public agvPosition agvPosition; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/action.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/action.cs new file mode 100644 index 0000000..e4212b4 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/action.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class action + { + + public string actionId { get; set; } + + public string actionType { get; set; } + public string actionDescription { get; set; } + + public string blockingType { get; set; } + + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/actionState.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/actionState.cs new file mode 100644 index 0000000..fe454e2 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/actionState.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json.Converters; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class actionState + { + public actionState(action action,ActionStateEnum state) + { + actionId = action.actionId; + actionDescription = action.actionDescription; + actionType = action.actionType; + actionStatus = state; + } + + public actionState() + { + } + + public string actionId { get; set; } + + public string actionType { get; set; } + public string actionDescription { get; set; } + + [JsonConverter(typeof(StringEnumConverter))] + public ActionStateEnum actionStatus { get; set; } + + public string resultDescription { get; set; } + + public enum ActionStateEnum + { + WAITING, + INITIALIZING, + RUNNING, + PAUSED, + FINISHED, + FAILED + } + + } + +} \ No newline at end of file diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/agvGeometry.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/agvGeometry.cs new file mode 100644 index 0000000..d96e7b7 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/agvGeometry.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Text; +using static CommonUsage.Protocols.VDA5050.Objects.agvGeometry; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class agvGeometry + { + // Wheel Definitions + public List wheelDefinitions { get; set; } = new List(); + + // 2D Envelopes + public List envelopes2D { get; set; } = new List(); + + // 3D Envelopes + public List envelopes3D { get; set; } = new List(); + + public class wheelDefinition + { + public enum WheelType { DRIVE, CASTER, FIXED, MECANUM } + + public WheelType type { get; set; } + public bool isActiveDriven { get; set; } + public bool isActiveSteered { get; set; } + + // Wheel Position + public double positionX { get; set; } + public double positionY { get; set; } + public double positionTheta { get; set; } // Required for fixed wheels + + // Wheel Properties + public double diameter { get; set; } + public double width { get; set; } + public double centerDisplacement { get; set; } = 0; // Default to 0 if not defined + public string constraints { get; set; } + } + + public class envelope2D + { + public string set { get; set; } + public List polygonPoints { get; set; } = new List(); + public string description { get; set; } + + public class polygonPoint + { + public double x { get; set; } + public double y { get; set; } + } + } + + public class envelope3D + { + public string set { get; set; } + public string format { get; set; } + public object data { get; set; } // JSON object for 3D envelope data + public string url { get; set; } + public string description { get; set; } + } + } + +} + diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/agvPosition.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/agvPosition.cs new file mode 100644 index 0000000..8659cd3 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/agvPosition.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class agvPosition + { + public bool positionInitialized; + + public double x; + + public double y; + + public double theta; + public double localizationScore; + public double deviationRange; + public string mapId = ""; + public string mapDescription = ""; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/batteryState.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/batteryState.cs new file mode 100644 index 0000000..1d3392c --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/batteryState.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class batteryState + { + public double batteryCharge; + public double batteryVoltage; + public double batteryHealth; + public bool charging; + public int reach; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/boundingBoxReference.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/boundingBoxReference.cs new file mode 100644 index 0000000..be48032 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/boundingBoxReference.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class boundingBoxReference + { + public double X { get; set; } // Reference point X in AGV coordinate system + public double Y { get; set; } // Reference point Y in AGV coordinate system + public double Z { get; set; } // Reference point Z in AGV coordinate system + public double Theta { get; set; } // Orientation of the load bounding box + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/controlPoint.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/controlPoint.cs new file mode 100644 index 0000000..03300b8 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/controlPoint.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class controlPoint + { + public float x; + + public float y; + + public float weight; + public controlPoint(float x, float y, float weight) + { + this.x = x; + this.y = y; + this.weight = weight; + } + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/edge.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/edge.cs new file mode 100644 index 0000000..e207eb5 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/edge.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class edge : sequenceItem + { + public string edgeId; + + public string edgeDescription; + + public string startNodeId; + + public string endNodeId; + + public double maxSpeed; + + public double orientation; + + public trajectory? trajectory; + + public float[] trackTypeInfo; + // public List controlPoints; + // + // public List weights; + + public action[] action = []; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/edgeState.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/edgeState.cs new file mode 100644 index 0000000..e5d8af3 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/edgeState.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class edgeState : sequenceItem + { + public string edgeId; + + public string edgeDescription; + + public trajectory trajectory; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/errorReference.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/errorReference.cs new file mode 100644 index 0000000..f44d5c7 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/errorReference.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class errorReference + { + public string referenceKey { get; set; } // Type of reference (e.g., nodeId, edgeId, actionId) + public string referenceValue { get; set; } // Value corresponding to the referenceKey + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/errorState.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/errorState.cs new file mode 100644 index 0000000..227ed80 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/errorState.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class errorState + { + public List errorReferences { get; set; } = new List(); // Array of references + public string errorType { get; set; } // Required: Type/name of the error + public string errorDescription { get; set; } // Verbose description of the error + public string errorHint { get; set; } // Hint for resolving the error + public string errorLevel { get; set; } // Required: WARNING or FATAL + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/info.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/info.cs new file mode 100644 index 0000000..a638ec8 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/info.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class info + { + public string infoType { get; set; } // Type/name of the information + public List infoReferences { get; set; } = new List(); // List of references + public string infoDescription { get; set; } // Description of the information + public infoLevelEnum infoLevel { get; set; } // Debugging or visualization level + + public class infoReference + { + public string ReferenceKey { get; set; } // Reference type (e.g., headerId, orderId) + public string ReferenceValue { get; set; } // The actual referenced field value + } + + public enum infoLevelEnum + { + DEBUG, // Used for debugging + INFO // Used for visualization + } + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/load.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/load.cs new file mode 100644 index 0000000..91530d0 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/load.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class load + { + public string loadId { get; set; } // Unique ID (barcode, RFID, etc.) + public string loadType { get; set; } // Type of load + public string loadPosition { get; set; } // Load handling position (e.g., "front", "back") + public boundingBoxReference boundingBoxReference { get; set; } = new boundingBoxReference(); + public loadDimensions loadDimensions { get; set; } = new loadDimensions(); + public double weight { get; set; } // Weight of load in kg (0.0 to ∞) + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/loadDimensions.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/loadDimensions.cs new file mode 100644 index 0000000..90469c6 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/loadDimensions.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class loadDimensions + { + public double Length { get; set; } // Length of the bounding box + public double Width { get; set; } // Width of the bounding box + public double Height { get; set; } // Height of the bounding box (optional) + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/node.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/node.cs new file mode 100644 index 0000000..c6617cf --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/node.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class node : sequenceItem + { + public string nodeId; + + public string nodeDescription; + + public nodePosition nodePosition; + + public action[] actions = []; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/nodePosition.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/nodePosition.cs new file mode 100644 index 0000000..a1c362a --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/nodePosition.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + /// + /// Defines the position on a map in a global project-specific world coordinate system. + /// Each floor has its own map. + /// All maps shall use the same project-specific global origin. + /// + public class nodePosition + { + /// + /// X-position on the map in reference to the map coordinate system. + /// Precision is up to the specific implementation. + /// + public double x; + + /// + /// Y-position on the map in reference to the map coordinate system. + /// Precision is up to the specific implementation. + /// + public double y; + + /// + /// Range: [-Pi ... Pi] + /// Absolute orientation of the AGV on the node. + /// Optional: vehicle can plan the path by itself. If defined, the AGV has to assume the theta angle on this node. + /// If previous edge disallows rotation, the AGV shall rotate on the node. + /// If following edge has a differing orientation defined but disallows rotation, + /// the AGV is to rotate on the node to the edges desired rotation before entering the edge. + /// + public double theta; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/nodeState.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/nodeState.cs new file mode 100644 index 0000000..2a67bdf --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/nodeState.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class nodeState : sequenceItem + { + /// + /// Unique node identification. + /// + public string nodeId; + + /// + /// Additional information on the node. + /// + public string nodeDescription; + + /// + /// Node position. + /// The object is defined in 6.6 Topic: "order" (from master control to AGV) + /// Optional: Master control has this information. Can be sent additionally, e.g., for debugging purposes. + /// + public nodePosition nodePosition; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/physicalParameters.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/physicalParameters.cs new file mode 100644 index 0000000..3b4e0ec --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/physicalParameters.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class physicalParameters + { + public double speedMin; + public double speedMax; + public double angularSpeedMin; + public double angularSpeedMax; + public double accelerationMax; + public double decelerationMax; + public double heightMin; + public double heightMax; + public double width; + public double length; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/protocolFeatures.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/protocolFeatures.cs new file mode 100644 index 0000000..bcead23 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/protocolFeatures.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class protocolFeatures + { + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/protocolLimits.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/protocolLimits.cs new file mode 100644 index 0000000..64c7df7 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/protocolLimits.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class protocolLimits + { + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/safetyState.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/safetyState.cs new file mode 100644 index 0000000..5f67a98 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/safetyState.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class safetyState + { + public eStopEnum eStop { get; set; } // Emergency stop status + public bool fieldViolation { get; set; } // "true" if a safety field is violated, "false" otherwise + + public enum eStopEnum + { + AUTOACK, // Auto-acknowledged emergency stop (e.g., triggered by a bumper) + MANUAL, // Manually confirmed emergency stop + REMOTE, // Remote-confirmed emergency stop + NONE // No emergency stop activated + } + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/sequenceItem.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/sequenceItem.cs new file mode 100644 index 0000000..b77f535 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/sequenceItem.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class sequenceItem + { + public uint sequenceId; + + public bool released; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/trajectory.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/trajectory.cs new file mode 100644 index 0000000..1b792c4 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/trajectory.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class trajectory + { + public float degree; + + public float[] knotVector; + + public controlPoint[] controlPoints; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/typeSpecification.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/typeSpecification.cs new file mode 100644 index 0000000..1f8d6e9 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/typeSpecification.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class typeSpecification + { + public string agvKinemantic = ""; + public string agvClass = ""; + public double maxLoadMass; + public string[] localizationTypes; // Simplified description of localization type (e.g., NATURAL, REFLECTOR, RFID, DMC, GRID) + public string[] navigationTypes; // Path planning types (e.g., 'AUTONOMOUS', 'VIRTUAL_LINE_GUIDED') + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/velocity.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/velocity.cs new file mode 100644 index 0000000..d774ba1 --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/Objects/velocity.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050.Objects +{ + public class velocity + { + public double vx; + + public double vy; + + public double omega; + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/VDA5050Basic.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/VDA5050Basic.cs new file mode 100644 index 0000000..029348c --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/VDA5050Basic.cs @@ -0,0 +1,118 @@ +//using CommonUsage.Protocols.VDA5050.Messages; +//using System; +//using System.Collections.Generic; +//using System.Net.Http.Headers; +//using System.Runtime.CompilerServices; +//using System.Text; +//using System.Threading; +//using System.Threading.Tasks; +//using CommonUsage.Protocols.VDA5050.Objects; +//using ClumsyCore.Utilities; +//using System.Linq; +//using ClumsyCore.Pilot; +//using System.Numerics; + +//namespace CommonUsage.Protocols.VDA5050 +//{ +// public abstract class VDA5050Basic +// { +// protected static IVDACommunicationProtocol _communicationProtocol; + +// public void Enable(IVDACommunicationProtocol protocol) +// { +// _communicationProtocol = protocol; +// Task.Run(() => ManageConnection()); +// StartVisualizationLoop(); +// } +// private void StartVisualizationLoop() +// { +// Console.WriteLine("Visualization in CommonUsage"); +// new Thread(async () => +// { +// while (true) +// { +// var position = GetAGVPosition(); +// if (position != null) +// { +// var msg = new stateMessage() +// { +// serialNumber = "test-01", +// agvPosition = new() +// { +// x = position.Value.X, +// y = position.Value.Y, +// theta = position.Value.Theta, +// positionInitialized = true +// } + +// }; +// await _communicationProtocol.SendMessageAsync(msg, "vda5050/frldAGV/visualization"); +// } + +// Thread.Sleep(100); +// } +// }) +// { Name = "VDA5050TopicVisualization" }.Start(); +// } + +// private void ManageConnection() +// { +// while (true) +// { +// var status = CheckConnectionStatus() ? "ONLINE" : "OFFLINE"; +// _communicationProtocol.PublishConnectionStatus(status); +// Thread.Sleep(1000); +// } +// } + +// protected List OrganizeReceivedSequence(orderMessage order) +// { +// List receivedSequence = new(); + +// int ii = 0, jj = 0; +// while (true) +// { +// var edge = order.edges[ii]; +// var node = order.nodes[jj]; +// var takeEdge = edge.sequenceId < node.sequenceId; + +// if (takeEdge) +// { +// receivedSequence.Add(edge); +// ii++; +// if (ii == order.edges.Length) break; +// } +// else +// { +// receivedSequence.Add(node); +// jj++; +// if (jj == order.nodes.Length) break; +// } +// } +// for (var i = ii; i < order.edges.Length; ++i) receivedSequence.Add(order.edges[i]); +// for (var j = jj; j < order.nodes.Length; ++j) receivedSequence.Add(order.nodes[j]); + +// for (var i = 1; i < receivedSequence.Count; i++) +// { +// if (receivedSequence[i - 1].sequenceId + 1 != receivedSequence[i].sequenceId) +// throw new Exception("stateMessage not continuous!"); +// } + +// return receivedSequence; +// } + +// public virtual bool CheckConnectionStatus() +// { +// return false; +// } + +// protected abstract Vector3? GetAGVPosition(); +// public struct Vector3 +// { +// public double X; +// public double Y; +// public double Theta; +// } +// } + +//} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/VDA5050Helper.cs b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/VDA5050Helper.cs new file mode 100644 index 0000000..9f5217f --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Protocols/VDA5050/VDA5050Helper.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CommonUsage.Protocols.VDA5050 +{ + public class VDA5050Helper + { + + } +} diff --git a/CommonUsage-MultiVehicleSync/commonusage/Visualizer.cs b/CommonUsage-MultiVehicleSync/commonusage/Visualizer.cs new file mode 100644 index 0000000..f17c27d --- /dev/null +++ b/CommonUsage-MultiVehicleSync/commonusage/Visualizer.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Numerics; +using System.Text; + +namespace CommonUsage +{ + public class Visualizer + { + public Action LineAction; + + public Action TextAction; + + public Action Clear; + + public void DrawLine(Color color, Vector2 src, Vector2 dst, bool startArrow = false, bool endArrow = false, + int width = 1) + { + LineAction?.Invoke(color, src, dst, startArrow, endArrow, width); + } + + public void DrawText(Color color, string text, Vector2 pos) + { + TextAction?.Invoke(color, text, pos); + } + } +} diff --git a/MedullaAdapter/MedullaAdapter.csproj b/MedullaAdapter/MedullaAdapter.csproj index fbac02d..421f8ce 100644 --- a/MedullaAdapter/MedullaAdapter.csproj +++ b/MedullaAdapter/MedullaAdapter.csproj @@ -27,11 +27,6 @@ false - - ref\CommonUsage.dll - false - - ref\MDCSToolBox.dll false @@ -41,6 +36,9 @@ ref\CycleGUI.dll false + + ..\ref\CommonUsage.dll + diff --git a/MedullaAdapter/build/Medulla/plugins/CommonUsage.dll b/MedullaAdapter/build/Medulla/plugins/CommonUsage.dll new file mode 100644 index 0000000..cdb9124 Binary files /dev/null and b/MedullaAdapter/build/Medulla/plugins/CommonUsage.dll differ diff --git a/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.deps.json b/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.deps.json index f850763..c1ccd7d 100644 --- a/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.deps.json +++ b/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.deps.json @@ -7,9 +7,20 @@ "targets": { ".NETCoreApp,Version=v8.0": { "MedullaAdapter/1.0.0": { + "dependencies": { + "CommonUsage": "1.0.0.0" + }, "runtime": { "MedullaAdapter.dll": {} } + }, + "CommonUsage/1.0.0.0": { + "runtime": { + "CommonUsage.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" + } + } } } }, @@ -18,6 +29,11 @@ "type": "project", "serviceable": false, "sha512": "" + }, + "CommonUsage/1.0.0.0": { + "type": "reference", + "serviceable": false, + "sha512": "" } } } \ No newline at end of file diff --git a/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.dll b/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.dll index 52be01c..cfce787 100644 Binary files a/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.dll and b/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.dll differ diff --git a/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.pdb b/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.pdb index f3a1f58..9ab97e2 100644 Binary files a/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.pdb and b/MedullaAdapter/build/Medulla/plugins/MedullaAdapter.pdb differ diff --git a/Shared/MultiWheelChassisAdapter.cs b/Shared/MultiWheelChassisAdapter.cs index 0af8468..2ad3417 100644 --- a/Shared/MultiWheelChassisAdapter.cs +++ b/Shared/MultiWheelChassisAdapter.cs @@ -119,6 +119,10 @@ namespace MyParking.Shared // 禁用旧版DirectionAngle/ZeroDirection坐标偏置, // 保证SendXYThSpeed直接使用真实车体坐标系。 ResetToBodyFrame(); + + // 停车机器人优先保持当前机械舵角,通过反转轮速表达反向运动, + // 避免蟹行正反切换时四个舵轮无意义地旋转180°。 + _chassis.PreferMinimumSteeringTravel = true; } diff --git a/ref/CommonUsage.dll b/ref/CommonUsage.dll new file mode 100644 index 0000000..cdb9124 Binary files /dev/null and b/ref/CommonUsage.dll differ