refactor: improve COM flow and light timing
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
Архитектура COM и роли - ComMultiManager переименован в ComHub и переведен на фабричный паттерн обработчиков по ROLE. - Добавлен единый контракт обработчиков COM-контроллеров: - IComControllerHandler - DefaultComControllerHandler - Ответственность за оркестрацию перенесена в ComSystemManager, чтобы инициализация COM и жизненный цикл контроллеров управлялись централизованно. - ComWrapper сохранен как слой совместимости на время миграции логики в новую схему. - PrizeStandController отрефакторен под новый role-handler pipeline вместо старой прямой привязки. - Добавлен удобный доступ к PrizeStandController из менеджера системы там, где это требуется игровому коду. Handshake/авторизация платы (ComDeviceClient) - Авторизация платы переработана в строгую поэтапную последовательность: 1) ПК отправляет 07 2) ожидает CHAL 3) отправляет 08 в ответ на CHAL 4) ожидает RAES - Парсер стадий теперь принимает только ожидаемые токены на каждой стадии: - WaitingChal -> только CHAL - WaitingRaes -> только RAES - Любой посторонний вывод из serial во время handshake игнорируется (без немедленного падения), что устраняет ложные ошибки на нерелевантных COM-устройствах. - Если ожидаемый ответ после 07 не приходит, порт быстро пропускается как нерелевантный, без эскалации в критическую ошибку. - Для случаев с неподходящими/сторонними устройствами уровень логирования снижен с ошибок до предупреждений там, где это уместно. - Добавлена подробная диагностика авторизации (переходы стадий, принятые значимые кадры, контекст таймаутов) для упрощения отладки. Тестовая утилита handshake - Добавлен Assets/Temp/Handshake_Test.cs как изолированный диагностический скрипт. - Включены очень подробные логи по каждому шагу handshake и по каждому решению парсера. - Позволяет отдельно гонять тестовый сценарий и сравнивать поведение платы, не ломая production-поток. Стабильность COM и поведение reconnect - Подправлена логика keep-alive, чтобы исключить агрессивные реконнекты. - Reconnect теперь срабатывает только после нескольких последовательных пропусков INPUT-ответа, что снижает случайные обрывы COM и прерывания подсветки. Поведение подсветки во время выполнения - Циклы подсветки сделаны независимыми от Time.timeScale: - LightController: WaitForSeconds -> WaitForSecondsRealtime - LightSequence: WaitForSeconds -> WaitForSecondsRealtime - Это устраняет случайные паузы/замедления световых циклов при изменении игрового времени (пауза, slow-mo, временные изменения scale). Итоговый эффект - COM-стек стал расширяемым по ролям и проще в сопровождении. - Handshake стал детерминированным, устойчивым к serial-шуму и безопасным при работе со сторонними устройствами. - Циклы подсветки стабилизированы для длительных сессий и больше не зависят от масштабирования игрового времени.
This commit is contained in:
parent
7e675618c8
commit
2963e342c2
@ -11,7 +11,6 @@ GameObject:
|
||||
- component: {fileID: 6708844088101150}
|
||||
- component: {fileID: -6830167632558745604}
|
||||
- component: {fileID: 7290507550622501242}
|
||||
- component: {fileID: 4577859410310799261}
|
||||
- component: {fileID: -3401184227323537348}
|
||||
- component: {fileID: -6245296037342915518}
|
||||
- component: {fileID: 6566017136146807207}
|
||||
@ -66,30 +65,6 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.Settings.SettingsManager
|
||||
database: {fileID: 11400000, guid: 29cc91230b52fab4494eb90856f51c2c, type: 2}
|
||||
--- !u!114 &4577859410310799261
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4763496076470024150}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d793a5f3669a676418d43519feaf3a8d, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.Controls.ComWrapper
|
||||
saveMoneyBetweenSessions: 0
|
||||
enablePrizeChecking: 0
|
||||
keepAlive: 1
|
||||
enableEncryptedHandshake: 1
|
||||
handshakeTimeoutMs: 5000
|
||||
roleRequestCommand: 97
|
||||
roleResponsePrefix: ROLE
|
||||
roleRequestTimeoutMs: 1500
|
||||
mainControllerRole: main_controller
|
||||
useAllDetectedPorts: 1
|
||||
explicitPorts: []
|
||||
defaultDevicePort:
|
||||
--- !u!114 &-3401184227323537348
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
@ -110,10 +85,17 @@ MonoBehaviour:
|
||||
roleRequestCommand: 97
|
||||
roleResponsePrefix: ROLE
|
||||
roleRequestTimeoutMs: 1500
|
||||
mainControllerRole: main_controller
|
||||
mainControllerRole: main
|
||||
useAllDetectedPorts: 1
|
||||
explicitPorts: []
|
||||
defaultDevicePort:
|
||||
roleHandlerBindings:
|
||||
- role: prize_stand
|
||||
handlerType: 1
|
||||
prizeStandBoxesCount: 24
|
||||
- role: prize_stand_controller
|
||||
handlerType: 1
|
||||
prizeStandBoxesCount: 24
|
||||
--- !u!114 &-6245296037342915518
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@ -8504,6 +8504,59 @@ Transform:
|
||||
m_CorrespondingSourceObject: {fileID: 4000011826580284, guid: 93b90907e99e38a448aca4707806a9be, type: 3}
|
||||
m_PrefabInstance: {fileID: 1487759007}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!1 &1500633490
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1500633492}
|
||||
- component: {fileID: 1500633491}
|
||||
m_Layer: 0
|
||||
m_Name: Handshake_Test
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 0
|
||||
--- !u!114 &1500633491
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1500633490}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 50f614532ed49204cb6644534a1c5d81, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Handshake_Test
|
||||
portName: COM3
|
||||
baudRate: 115200
|
||||
readTimeoutMs: 5
|
||||
writeTimeoutMs: 10
|
||||
openOnStart: 1
|
||||
autoRunHandshake: 1
|
||||
handshakeTimeoutMs: 10000
|
||||
closePortOnDisable: 1
|
||||
logRawReadLoop: 1
|
||||
--- !u!4 &1500633492
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1500633490}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: -2.75464, y: -3.18366, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1513556535
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@ -289291,3 +289344,4 @@ SceneRoots:
|
||||
- {fileID: 2033907137}
|
||||
- {fileID: 1553660625}
|
||||
- {fileID: 348531057}
|
||||
- {fileID: 1500633492}
|
||||
|
||||
@ -3929,7 +3929,6 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1669870415}
|
||||
- {fileID: 1962581740}
|
||||
m_Father: {fileID: 139708007}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
@ -4351,7 +4350,6 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1877012750}
|
||||
- {fileID: 1906929718}
|
||||
m_Father: {fileID: 139708007}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
@ -4945,42 +4943,6 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 683903126}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &700047461
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 700047462}
|
||||
m_Layer: 5
|
||||
m_Name: VFX
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 0
|
||||
--- !u!224 &700047462
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 700047461}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 997740367}
|
||||
m_Father: {fileID: 1460883867}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &705422857
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@ -7459,124 +7421,6 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 996229917}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &997740366
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 997740367}
|
||||
- component: {fileID: 997740369}
|
||||
- component: {fileID: 997740368}
|
||||
m_Layer: 5
|
||||
m_Name: Visual Effect
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &997740367
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 997740366}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 100, y: 100, z: 100}
|
||||
m_ConstrainProportionsScale: 1
|
||||
m_Children: []
|
||||
m_Father: {fileID: 700047462}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!73398921 &997740368
|
||||
VFXRenderer:
|
||||
serializedVersion: 1
|
||||
m_ObjectHideFlags: 2
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 997740366}
|
||||
m_Enabled: 0
|
||||
m_CastShadows: 0
|
||||
m_ReceiveShadows: 0
|
||||
m_DynamicOccludee: 1
|
||||
m_StaticShadowCaster: 0
|
||||
m_MotionVectors: 0
|
||||
m_LightProbeUsage: 0
|
||||
m_ReflectionProbeUsage: 0
|
||||
m_RayTracingMode: 0
|
||||
m_RayTraceProcedural: 0
|
||||
m_RayTracingAccelStructBuildFlagsOverride: 0
|
||||
m_RayTracingAccelStructBuildFlags: 1
|
||||
m_SmallMeshCulling: 1
|
||||
m_ForceMeshLod: -1
|
||||
m_MeshLodSelectionBias: 0
|
||||
m_RenderingLayerMask: 4294967295
|
||||
m_RendererPriority: 0
|
||||
m_StaticBatchInfo:
|
||||
firstSubMesh: 0
|
||||
subMeshCount: 0
|
||||
m_StaticBatchRoot: {fileID: 0}
|
||||
m_ProbeAnchor: {fileID: 0}
|
||||
m_LightProbeVolumeOverride: {fileID: 0}
|
||||
m_ScaleInLightmap: 1
|
||||
m_ReceiveGI: 1
|
||||
m_PreserveUVs: 0
|
||||
m_IgnoreNormalsForChartDetection: 0
|
||||
m_ImportantGI: 0
|
||||
m_StitchLightmapSeams: 1
|
||||
m_SelectedEditorRenderState: 3
|
||||
m_MinimumChartSize: 4
|
||||
m_AutoUVMaxDistance: 0.5
|
||||
m_AutoUVMaxAngle: 89
|
||||
m_LightmapParameters: {fileID: 0}
|
||||
m_GlobalIlluminationMeshLod: 0
|
||||
m_SortingLayerID: 981438795
|
||||
m_SortingLayer: 2
|
||||
m_SortingOrder: 0
|
||||
m_MaskInteraction: 0
|
||||
--- !u!2083052967 &997740369
|
||||
VisualEffect:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 997740366}
|
||||
m_Enabled: 1
|
||||
m_Asset: {fileID: 8926484042661614526, guid: 6c4fdce0f9a1b184d9c3660014bdfdb5, type: 3}
|
||||
m_InitialEventName: OnPlay
|
||||
m_InitialEventNameOverriden: 0
|
||||
m_StartSeed: 0
|
||||
m_ResetSeedOnPlay: 1
|
||||
m_AllowInstancing: 1
|
||||
m_ResourceVersion: 1
|
||||
m_PropertySheet:
|
||||
m_Float:
|
||||
m_Array: []
|
||||
m_Vector2f:
|
||||
m_Array: []
|
||||
m_Vector3f:
|
||||
m_Array: []
|
||||
m_Vector4f:
|
||||
m_Array: []
|
||||
m_Uint:
|
||||
m_Array: []
|
||||
m_Int:
|
||||
m_Array: []
|
||||
m_Matrix4x4f:
|
||||
m_Array: []
|
||||
m_AnimationCurve:
|
||||
m_Array: []
|
||||
m_Gradient:
|
||||
m_Array: []
|
||||
m_NamedObject:
|
||||
m_Array: []
|
||||
m_Bool:
|
||||
m_Array: []
|
||||
--- !u!1 &1010062389
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@ -9936,8 +9780,8 @@ MonoBehaviour:
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: -5002131913879957837, guid: 67ae0b8315a808542a646c540231aa88, type: 3}
|
||||
m_Type: 0
|
||||
m_Sprite: {fileID: 21300000, guid: 67ae0b8315a808542a646c540231aa88, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
@ -10384,7 +10228,6 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 1412406034}
|
||||
- {fileID: 700047462}
|
||||
m_Father: {fileID: 139708007}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
@ -13553,7 +13396,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0.00094176567}
|
||||
m_AnchoredPosition: {x: 0, y: 0.00095310435}
|
||||
m_SizeDelta: {x: 0, y: 300}
|
||||
m_Pivot: {x: 0, y: 1}
|
||||
--- !u!114 &1792218764
|
||||
@ -13925,7 +13768,6 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 374987852}
|
||||
- {fileID: 2141805163}
|
||||
m_Father: {fileID: 139708007}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
@ -14825,74 +14667,6 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1906471957}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!1 &1906929717
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1906929718}
|
||||
- component: {fileID: 1906929719}
|
||||
m_Layer: 5
|
||||
m_Name: SettingsUIGenerator
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1906929718
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1906929717}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 650017796}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1906929719
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1906929717}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 2bec3b41f31ece6499468cf44ed8ed4e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.SettingsUIGenerator
|
||||
database: {fileID: 11400000, guid: 29cc91230b52fab4494eb90856f51c2c, type: 2}
|
||||
contentRoot: {fileID: 0}
|
||||
groupByGroup: 1
|
||||
collapsibleGroups: 1
|
||||
groupPanelPrefab: {fileID: 2186865876444500735, guid: 1983dbab136fb73419f146d56772a8c8, type: 3}
|
||||
defaultGroupsExpanded: 1
|
||||
rememberGroupFoldout: 1
|
||||
groupOrder:
|
||||
- "\u041C\u0443\u0437\u044B\u043A\u0430 \u0438 \u0437\u0432\u0443\u043A\u0438"
|
||||
sortByNameInsideGroup: 0
|
||||
toggleRowPrefab: {fileID: -3497382869404314779, guid: 3f18baed1f4d89f4ab9b4caf0c789a38, type: 3}
|
||||
sliderRowPrefab: {fileID: 5445087308684328405, guid: a85153c0090994b438ad050a1dbcc306, type: 3}
|
||||
valueSelectorRowPrefab: {fileID: 5605032421712587294, guid: ef3fe155416356b4babfbdf354df7056, type: 3}
|
||||
dropdownRowPrefab: {fileID: 1137542491512761884, guid: 1aa96bf602ff6b243b6f9844b30d87f1, type: 3}
|
||||
inputRowPrefab: {fileID: 895630567611506380, guid: b9bddf8fad39ed546a60c1ca412c134d, type: 3}
|
||||
buttonRowPrefab: {fileID: 6489304585556535039, guid: af71450598473d841b596834d93a58db, type: 3}
|
||||
optionPickerRowPrefab: {fileID: 0}
|
||||
buildOnEnable: 1
|
||||
clearBeforeBuild: 1
|
||||
showDescription: 1
|
||||
--- !u!1 &1925001478
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@ -15184,74 +14958,6 @@ MonoBehaviour:
|
||||
m_StringArgument:
|
||||
m_BoolArgument: 0
|
||||
m_CallState: 2
|
||||
--- !u!1 &1935615349
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1935615350}
|
||||
- component: {fileID: 1935615351}
|
||||
m_Layer: 5
|
||||
m_Name: SettingsUIGenerator
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1935615350
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1935615349}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 2009866677}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1935615351
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1935615349}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 2bec3b41f31ece6499468cf44ed8ed4e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.SettingsUIGenerator
|
||||
database: {fileID: 11400000, guid: 29cc91230b52fab4494eb90856f51c2c, type: 2}
|
||||
contentRoot: {fileID: 0}
|
||||
groupByGroup: 1
|
||||
collapsibleGroups: 1
|
||||
groupPanelPrefab: {fileID: 2186865876444500735, guid: 1983dbab136fb73419f146d56772a8c8, type: 3}
|
||||
defaultGroupsExpanded: 1
|
||||
rememberGroupFoldout: 1
|
||||
groupOrder:
|
||||
- "\u041C\u0443\u0437\u044B\u043A\u0430 \u0438 \u0437\u0432\u0443\u043A\u0438"
|
||||
sortByNameInsideGroup: 0
|
||||
toggleRowPrefab: {fileID: -3497382869404314779, guid: 3f18baed1f4d89f4ab9b4caf0c789a38, type: 3}
|
||||
sliderRowPrefab: {fileID: 5445087308684328405, guid: a85153c0090994b438ad050a1dbcc306, type: 3}
|
||||
valueSelectorRowPrefab: {fileID: 5605032421712587294, guid: ef3fe155416356b4babfbdf354df7056, type: 3}
|
||||
dropdownRowPrefab: {fileID: 1137542491512761884, guid: 1aa96bf602ff6b243b6f9844b30d87f1, type: 3}
|
||||
inputRowPrefab: {fileID: 895630567611506380, guid: b9bddf8fad39ed546a60c1ca412c134d, type: 3}
|
||||
buttonRowPrefab: {fileID: 6489304585556535039, guid: af71450598473d841b596834d93a58db, type: 3}
|
||||
optionPickerRowPrefab: {fileID: 0}
|
||||
buildOnEnable: 1
|
||||
clearBeforeBuild: 1
|
||||
showDescription: 1
|
||||
--- !u!1 &1959311158
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@ -15422,74 +15128,6 @@ Transform:
|
||||
- {fileID: 1594047442}
|
||||
m_Father: {fileID: 1381629685}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1962581739
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1962581740}
|
||||
- component: {fileID: 1962581741}
|
||||
m_Layer: 5
|
||||
m_Name: SettingsUIGenerator
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1962581740
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1962581739}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 554903410}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &1962581741
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1962581739}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 2bec3b41f31ece6499468cf44ed8ed4e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.SettingsUIGenerator
|
||||
database: {fileID: 11400000, guid: 29cc91230b52fab4494eb90856f51c2c, type: 2}
|
||||
contentRoot: {fileID: 0}
|
||||
groupByGroup: 1
|
||||
collapsibleGroups: 1
|
||||
groupPanelPrefab: {fileID: 2186865876444500735, guid: 1983dbab136fb73419f146d56772a8c8, type: 3}
|
||||
defaultGroupsExpanded: 1
|
||||
rememberGroupFoldout: 1
|
||||
groupOrder:
|
||||
- "\u041C\u0443\u0437\u044B\u043A\u0430 \u0438 \u0437\u0432\u0443\u043A\u0438"
|
||||
sortByNameInsideGroup: 0
|
||||
toggleRowPrefab: {fileID: -3497382869404314779, guid: 3f18baed1f4d89f4ab9b4caf0c789a38, type: 3}
|
||||
sliderRowPrefab: {fileID: 5445087308684328405, guid: a85153c0090994b438ad050a1dbcc306, type: 3}
|
||||
valueSelectorRowPrefab: {fileID: 5605032421712587294, guid: ef3fe155416356b4babfbdf354df7056, type: 3}
|
||||
dropdownRowPrefab: {fileID: 1137542491512761884, guid: 1aa96bf602ff6b243b6f9844b30d87f1, type: 3}
|
||||
inputRowPrefab: {fileID: 895630567611506380, guid: b9bddf8fad39ed546a60c1ca412c134d, type: 3}
|
||||
buttonRowPrefab: {fileID: 6489304585556535039, guid: af71450598473d841b596834d93a58db, type: 3}
|
||||
optionPickerRowPrefab: {fileID: 0}
|
||||
buildOnEnable: 1
|
||||
clearBeforeBuild: 1
|
||||
showDescription: 1
|
||||
--- !u!1 &1986812974
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@ -15860,7 +15498,6 @@ RectTransform:
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 765378070}
|
||||
- {fileID: 1935615350}
|
||||
m_Father: {fileID: 139708007}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
@ -16798,74 +16435,6 @@ MonoBehaviour:
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &2141805162
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2141805163}
|
||||
- component: {fileID: 2141805164}
|
||||
m_Layer: 5
|
||||
m_Name: SettingsUIGenerator
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &2141805163
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2141805162}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1828951630}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 100, y: 100}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &2141805164
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2141805162}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 2bec3b41f31ece6499468cf44ed8ed4e, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.SettingsUIGenerator
|
||||
database: {fileID: 11400000, guid: 29cc91230b52fab4494eb90856f51c2c, type: 2}
|
||||
contentRoot: {fileID: 0}
|
||||
groupByGroup: 1
|
||||
collapsibleGroups: 1
|
||||
groupPanelPrefab: {fileID: 2186865876444500735, guid: 1983dbab136fb73419f146d56772a8c8, type: 3}
|
||||
defaultGroupsExpanded: 1
|
||||
rememberGroupFoldout: 1
|
||||
groupOrder:
|
||||
- "\u041C\u0443\u0437\u044B\u043A\u0430 \u0438 \u0437\u0432\u0443\u043A\u0438"
|
||||
sortByNameInsideGroup: 0
|
||||
toggleRowPrefab: {fileID: -3497382869404314779, guid: 3f18baed1f4d89f4ab9b4caf0c789a38, type: 3}
|
||||
sliderRowPrefab: {fileID: 5445087308684328405, guid: a85153c0090994b438ad050a1dbcc306, type: 3}
|
||||
valueSelectorRowPrefab: {fileID: 5605032421712587294, guid: ef3fe155416356b4babfbdf354df7056, type: 3}
|
||||
dropdownRowPrefab: {fileID: 1137542491512761884, guid: 1aa96bf602ff6b243b6f9844b30d87f1, type: 3}
|
||||
inputRowPrefab: {fileID: 895630567611506380, guid: b9bddf8fad39ed546a60c1ca412c134d, type: 3}
|
||||
buttonRowPrefab: {fileID: 6489304585556535039, guid: af71450598473d841b596834d93a58db, type: 3}
|
||||
optionPickerRowPrefab: {fileID: 0}
|
||||
buildOnEnable: 1
|
||||
clearBeforeBuild: 1
|
||||
showDescription: 1
|
||||
--- !u!1 &1000009598396499
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@ -4,6 +4,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using RR.Game2D.Core.Managers;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RR.Common.Controls
|
||||
{
|
||||
@ -37,25 +38,27 @@ namespace RR.Common.Controls
|
||||
private int _handle;
|
||||
private long _lastCoinTime;
|
||||
private int _inputToggle;
|
||||
private int _missedInputResponses;
|
||||
|
||||
private bool _isAuthorized;
|
||||
private bool _isOperational;
|
||||
private bool _handshakeInProgress;
|
||||
|
||||
private string _requestHex = string.Empty;
|
||||
private string _lastHandshakeResponse = string.Empty;
|
||||
private string _lastInputBits = DefaultInputBits;
|
||||
private byte[] _aesKey32;
|
||||
|
||||
private HandshakeStage _handshakeStage = HandshakeStage.None;
|
||||
private TaskCompletionSource<bool> _handshakeCompletion;
|
||||
private TaskCompletionSource<bool> _chalReceivedCompletion;
|
||||
private TaskCompletionSource<string> _roleCompletion;
|
||||
|
||||
private enum HandshakeStage
|
||||
{
|
||||
None,
|
||||
WaitingRaes,
|
||||
WaitingChal,
|
||||
WaitingReady,
|
||||
WaitingRaes,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
@ -69,6 +72,7 @@ namespace RR.Common.Controls
|
||||
|
||||
public event Action<string> CoinAccepted;
|
||||
public event Action<string, string> InputReceived;
|
||||
public event Action<string, string> RawLineReceived;
|
||||
public event Action<string> Authorized;
|
||||
public event Action<string> Disconnected;
|
||||
|
||||
@ -146,8 +150,10 @@ namespace RR.Common.Controls
|
||||
_handshakeInProgress = false;
|
||||
_handshakeStage = HandshakeStage.None;
|
||||
_handshakeCompletion?.TrySetCanceled();
|
||||
_chalReceivedCompletion?.TrySetCanceled();
|
||||
_roleCompletion?.TrySetCanceled();
|
||||
ControllerRole = string.Empty;
|
||||
_keepAliveTask = null;
|
||||
|
||||
Disconnected?.Invoke(DeviceId);
|
||||
|
||||
@ -230,7 +236,10 @@ namespace RR.Common.Controls
|
||||
string bits = DecimalToBinary(rawData);
|
||||
_lastInputBits = bits;
|
||||
InputReceived?.Invoke(DeviceId, bits);
|
||||
return;
|
||||
}
|
||||
|
||||
RawLineReceived?.Invoke(DeviceId, line);
|
||||
}
|
||||
|
||||
private async Task RunHandshakeAsync(CancellationToken token)
|
||||
@ -238,59 +247,88 @@ namespace RR.Common.Controls
|
||||
_handshakeInProgress = true;
|
||||
_isAuthorized = false;
|
||||
_isOperational = false;
|
||||
_handshakeStage = HandshakeStage.WaitingRaes;
|
||||
_handshakeStage = HandshakeStage.WaitingChal;
|
||||
_lastHandshakeResponse = string.Empty;
|
||||
_handshakeCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
SendRaw("00", true);
|
||||
await Task.Delay(1000, token);
|
||||
_chalReceivedCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] Handshake started. Stage={_handshakeStage}");
|
||||
|
||||
_requestHex = GenerateRandomBlock16Hex();
|
||||
// Порядок handshake по протоколу платы:
|
||||
// PC -> 07[random], board -> CHAL, PC -> 08[enc(CHAL)], board -> RAES, board -> AUTH_CNT_OK.
|
||||
SendRaw("07" + _requestHex, true);
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] -> 07{_requestHex}");
|
||||
|
||||
// Если на 07 не получили ожидаемый CHAL быстро, сразу пропускаем устройство.
|
||||
int chalFastTimeoutMs = Math.Min(1200, Math.Max(250, _handshakeTimeoutMs / 4));
|
||||
Task chalFastTimeoutTask = Task.Delay(chalFastTimeoutMs, token);
|
||||
Task chalFastFinished = await Task.WhenAny(_chalReceivedCompletion.Task, chalFastTimeoutTask);
|
||||
if (chalFastFinished != _chalReceivedCompletion.Task)
|
||||
{
|
||||
Debug.LogWarning($"[COM AUTH][{DeviceId}] No CHAL after 07 in {chalFastTimeoutMs}ms. Skipping device.");
|
||||
FailHandshake($"Нет ожидаемого CHAL на 07 за {chalFastTimeoutMs}ms");
|
||||
return;
|
||||
}
|
||||
|
||||
Task timeoutTask = Task.Delay(_handshakeTimeoutMs, token);
|
||||
Task finished = await Task.WhenAny(_handshakeCompletion.Task, timeoutTask);
|
||||
|
||||
if (finished == _handshakeCompletion.Task && _handshakeCompletion.Task.Result)
|
||||
{
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] Handshake completed successfully");
|
||||
MarkOperational();
|
||||
return;
|
||||
}
|
||||
|
||||
FailHandshake("Таймаут рукопожатия");
|
||||
Debug.LogWarning($"[COM AUTH][{DeviceId}] Handshake timeout. Last board response: {_lastHandshakeResponse}");
|
||||
FailHandshake($"Таймаут рукопожатия. Последний ответ платы: {_lastHandshakeResponse}");
|
||||
}
|
||||
|
||||
private void TryProcessHandshakeLine(string line)
|
||||
{
|
||||
if (line.Equals("READY", StringComparison.OrdinalIgnoreCase))
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] <- {line}; stage={_handshakeStage}");
|
||||
_lastHandshakeResponse = line;
|
||||
|
||||
// Во время handshake обрабатываем только ожидаемые сообщения:
|
||||
// WaitingChal -> CHAL
|
||||
// WaitingRaes -> RAES
|
||||
// Все остальные строки игнорируем.
|
||||
if (_handshakeStage == HandshakeStage.WaitingChal)
|
||||
{
|
||||
if (_handshakeStage != HandshakeStage.WaitingReady)
|
||||
if (line.StartsWith("CHAL", StringComparison.Ordinal))
|
||||
{
|
||||
FailHandshake($"READY в неверной фазе: {_handshakeStage}");
|
||||
return;
|
||||
HandleChal(line);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] Ignored while WaitingChal: {line}");
|
||||
}
|
||||
|
||||
_handshakeStage = HandshakeStage.Completed;
|
||||
_isAuthorized = true;
|
||||
_handshakeCompletion?.TrySetResult(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.StartsWith("RAES", StringComparison.Ordinal))
|
||||
if (_handshakeStage == HandshakeStage.WaitingRaes)
|
||||
{
|
||||
HandleRaes(line);
|
||||
if (line.StartsWith("RAES", StringComparison.Ordinal))
|
||||
{
|
||||
HandleRaes(line);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] Ignored while WaitingRaes: {line}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.StartsWith("CHAL", StringComparison.Ordinal))
|
||||
{
|
||||
HandleChal(line);
|
||||
}
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] Ignored in stage {_handshakeStage}: {line}");
|
||||
}
|
||||
|
||||
private void HandleRaes(string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] Handling RAES...");
|
||||
if (_handshakeStage != HandshakeStage.WaitingRaes)
|
||||
{
|
||||
FailHandshake($"RAES в неверной фазе: {_handshakeStage}");
|
||||
@ -317,7 +355,10 @@ namespace RR.Common.Controls
|
||||
return;
|
||||
}
|
||||
|
||||
_handshakeStage = HandshakeStage.WaitingChal;
|
||||
_handshakeStage = HandshakeStage.Completed;
|
||||
_isAuthorized = true;
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] RAES verified. Raw RAES={line}; decrypted={decrypted}; handshake complete");
|
||||
_handshakeCompletion?.TrySetResult(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -329,12 +370,15 @@ namespace RR.Common.Controls
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] Handling CHAL...");
|
||||
if (_handshakeStage != HandshakeStage.WaitingChal)
|
||||
{
|
||||
FailHandshake($"CHAL в неверной фазе: {_handshakeStage}");
|
||||
return;
|
||||
}
|
||||
|
||||
_chalReceivedCompletion?.TrySetResult(true);
|
||||
|
||||
if (line.Length < 36)
|
||||
{
|
||||
FailHandshake("Некорректная длина CHAL");
|
||||
@ -350,7 +394,8 @@ namespace RR.Common.Controls
|
||||
|
||||
string encrypted = EncryptHexBlock16(hex);
|
||||
SendRaw("08" + encrypted, true);
|
||||
_handshakeStage = HandshakeStage.WaitingReady;
|
||||
_handshakeStage = HandshakeStage.WaitingRaes;
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] -> 08{encrypted}; stage={_handshakeStage}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -365,10 +410,12 @@ namespace RR.Common.Controls
|
||||
_isOperational = true;
|
||||
_handshakeStage = HandshakeStage.Completed;
|
||||
ControllerRole = string.Empty;
|
||||
_missedInputResponses = 0;
|
||||
Debug.Log($"[COM AUTH][{DeviceId}] Device marked operational");
|
||||
|
||||
SendRaw("2171", true);
|
||||
|
||||
if (_keepAlive)
|
||||
if (_keepAlive && (_keepAliveTask == null || _keepAliveTask.IsCompleted))
|
||||
{
|
||||
_keepAliveTask = Task.Run(() => KeepAliveLoopAsync(_lifecycleCts.Token), _lifecycleCts.Token);
|
||||
}
|
||||
@ -383,7 +430,12 @@ namespace RR.Common.Controls
|
||||
_handshakeInProgress = false;
|
||||
_handshakeStage = HandshakeStage.Failed;
|
||||
_handshakeCompletion?.TrySetResult(false);
|
||||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] {message}");
|
||||
Debug.LogWarning($"[COM AUTH][{DeviceId}] Handshake failed: {message}");
|
||||
GameManager.Logger?.LogWarning("ComDeviceClient", $"[{DeviceId}] {message}");
|
||||
|
||||
// Если порт отвечает не по ожидаемому протоколу платы,
|
||||
// отключаем устройство, чтобы менеджер выбрал другой порт.
|
||||
Stop(false);
|
||||
}
|
||||
|
||||
private async Task KeepAliveLoopAsync(CancellationToken token)
|
||||
@ -406,7 +458,18 @@ namespace RR.Common.Controls
|
||||
await Task.Delay(1000, token);
|
||||
if (_inputToggle == 0)
|
||||
{
|
||||
await ReconnectAsync(token);
|
||||
_missedInputResponses++;
|
||||
if (_missedInputResponses >= 3)
|
||||
{
|
||||
GameManager.Logger?.LogWarning("ComDeviceClient",
|
||||
$"[{DeviceId}] Нет INPUT-ответа {_missedInputResponses} цикла подряд, выполняю reconnect");
|
||||
_missedInputResponses = 0;
|
||||
await ReconnectAsync(token);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_missedInputResponses = 0;
|
||||
}
|
||||
|
||||
await Task.Delay(7000, token);
|
||||
@ -443,6 +506,9 @@ namespace RR.Common.Controls
|
||||
|
||||
if (!OpenPort())
|
||||
{
|
||||
// Порт недоступен при переподключении: отключаем устройство и даем менеджеру
|
||||
// переключиться на другие доступные порты.
|
||||
Stop(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -6,9 +6,10 @@ using RR.Game2D.Core.Managers;
|
||||
|
||||
namespace RR.Common.Controls
|
||||
{
|
||||
internal sealed class ComMultiManager
|
||||
internal sealed class ComHub
|
||||
{
|
||||
private readonly Dictionary<string, ComDeviceClient> _devices = new();
|
||||
private readonly Dictionary<string, IComControllerHandler> _handlers = new();
|
||||
private readonly ConcurrentQueue<string> _coinQueue = new();
|
||||
private readonly ConcurrentQueue<DeviceInputMessage> _inputQueue = new();
|
||||
private readonly ConcurrentQueue<string> _authorizedQueue = new();
|
||||
@ -23,10 +24,11 @@ namespace RR.Common.Controls
|
||||
private readonly string _roleResponsePrefix;
|
||||
private readonly int _roleRequestTimeoutMs;
|
||||
private readonly string _mainControllerRole;
|
||||
private readonly Func<string, string, IComControllerHandler> _handlerFactory;
|
||||
|
||||
private string _defaultDeviceId = string.Empty;
|
||||
|
||||
public ComMultiManager(
|
||||
public ComHub(
|
||||
bool useAllDetectedPorts,
|
||||
string[] explicitPorts,
|
||||
string defaultDevicePort,
|
||||
@ -36,7 +38,8 @@ namespace RR.Common.Controls
|
||||
string roleRequestCommand,
|
||||
string roleResponsePrefix,
|
||||
int roleRequestTimeoutMs,
|
||||
string mainControllerRole)
|
||||
string mainControllerRole,
|
||||
Func<string, string, IComControllerHandler> handlerFactory)
|
||||
{
|
||||
_useAllDetectedPorts = useAllDetectedPorts;
|
||||
_explicitPorts = explicitPorts ?? Array.Empty<string>();
|
||||
@ -48,6 +51,7 @@ namespace RR.Common.Controls
|
||||
_roleResponsePrefix = roleResponsePrefix ?? "ROLE";
|
||||
_roleRequestTimeoutMs = roleRequestTimeoutMs;
|
||||
_mainControllerRole = string.IsNullOrWhiteSpace(mainControllerRole) ? "main_controller" : mainControllerRole;
|
||||
_handlerFactory = handlerFactory;
|
||||
}
|
||||
|
||||
public string DefaultDeviceId => _defaultDeviceId;
|
||||
@ -61,7 +65,7 @@ namespace RR.Common.Controls
|
||||
string[] allPorts = ComNativeWrapper.ListPorts();
|
||||
if (allPorts.Length == 0)
|
||||
{
|
||||
GameManager.Logger?.LogWarning("ComMultiManager", "Не найдено COM-портов");
|
||||
GameManager.Logger?.LogWarning("ComHub", "Не найдено COM-портов");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -82,24 +86,50 @@ namespace RR.Common.Controls
|
||||
client.Stop(sendShutdownCommands);
|
||||
}
|
||||
|
||||
foreach (IComControllerHandler handler in _handlers.Values)
|
||||
{
|
||||
handler.Dispose();
|
||||
}
|
||||
|
||||
_devices.Clear();
|
||||
_handlers.Clear();
|
||||
_defaultDeviceId = string.Empty;
|
||||
|
||||
while (_coinQueue.TryDequeue(out _)) { }
|
||||
while (_inputQueue.TryDequeue(out _)) { }
|
||||
while (_authorizedQueue.TryDequeue(out _)) { }
|
||||
while (_coinQueue.TryDequeue(out _))
|
||||
{
|
||||
}
|
||||
|
||||
while (_inputQueue.TryDequeue(out _))
|
||||
{
|
||||
}
|
||||
|
||||
while (_authorizedQueue.TryDequeue(out _))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public bool SendToDefault(string message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_defaultDeviceId)) return false;
|
||||
if (string.IsNullOrEmpty(_defaultDeviceId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return SendToDevice(_defaultDeviceId, message);
|
||||
}
|
||||
|
||||
public bool SendToDevice(string deviceId, string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId)) return false;
|
||||
if (!_devices.TryGetValue(deviceId, out ComDeviceClient client)) return false;
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_devices.TryGetValue(deviceId, out ComDeviceClient client))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
client.Send(message);
|
||||
return true;
|
||||
}
|
||||
@ -114,7 +144,11 @@ namespace RR.Common.Controls
|
||||
|
||||
public string GetLastInputState(string deviceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId)) return "11111111111111111111111111";
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
return "11111111111111111111111111";
|
||||
}
|
||||
|
||||
return _devices.TryGetValue(deviceId, out ComDeviceClient client)
|
||||
? client.LastInputBits
|
||||
: "11111111111111111111111111";
|
||||
@ -128,18 +162,49 @@ namespace RR.Common.Controls
|
||||
|
||||
public string GetDeviceRole(string deviceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId)) return string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return _devices.TryGetValue(deviceId, out ComDeviceClient client)
|
||||
? client.ControllerRole
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
public PrizeStandController GetPrizeStandController(string deviceId = null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(deviceId))
|
||||
{
|
||||
return _handlers.TryGetValue(deviceId, out IComControllerHandler handler)
|
||||
? handler as PrizeStandController
|
||||
: null;
|
||||
}
|
||||
|
||||
foreach (IComControllerHandler handler in _handlers.Values)
|
||||
{
|
||||
if (handler is PrizeStandController standController)
|
||||
{
|
||||
return standController;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string[] SelectPorts(string[] allPorts)
|
||||
{
|
||||
if (_useAllDetectedPorts) return allPorts;
|
||||
if (_explicitPorts.Length == 0) return Array.Empty<string>();
|
||||
if (_useAllDetectedPorts)
|
||||
{
|
||||
return allPorts;
|
||||
}
|
||||
|
||||
HashSet<string> explicitSet = new HashSet<string>(_explicitPorts.Where(p => !string.IsNullOrWhiteSpace(p)));
|
||||
if (_explicitPorts.Length == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
HashSet<string> explicitSet = new(_explicitPorts.Where(p => !string.IsNullOrWhiteSpace(p)));
|
||||
return allPorts.Where(explicitSet.Contains).ToArray();
|
||||
}
|
||||
|
||||
@ -156,7 +221,7 @@ namespace RR.Common.Controls
|
||||
|
||||
private void TryCreateDevice(string port)
|
||||
{
|
||||
ComDeviceClient client = new ComDeviceClient(
|
||||
ComDeviceClient client = new(
|
||||
port,
|
||||
_enableEncryptedHandshake,
|
||||
_handshakeTimeoutMs,
|
||||
@ -180,6 +245,7 @@ namespace RR.Common.Controls
|
||||
{
|
||||
client.CoinAccepted += OnDeviceCoinAccepted;
|
||||
client.InputReceived += OnDeviceInputReceived;
|
||||
client.RawLineReceived += OnDeviceRawLineReceived;
|
||||
client.Authorized += OnDeviceAuthorized;
|
||||
client.Disconnected += OnDeviceDisconnected;
|
||||
}
|
||||
@ -188,20 +254,41 @@ namespace RR.Common.Controls
|
||||
{
|
||||
client.CoinAccepted -= OnDeviceCoinAccepted;
|
||||
client.InputReceived -= OnDeviceInputReceived;
|
||||
client.RawLineReceived -= OnDeviceRawLineReceived;
|
||||
client.Authorized -= OnDeviceAuthorized;
|
||||
client.Disconnected -= OnDeviceDisconnected;
|
||||
}
|
||||
|
||||
private void OnDeviceCoinAccepted(string deviceId)
|
||||
{
|
||||
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
|
||||
{
|
||||
handler.HandleCoinAccepted();
|
||||
return;
|
||||
}
|
||||
|
||||
_coinQueue.Enqueue(deviceId);
|
||||
}
|
||||
|
||||
private void OnDeviceInputReceived(string deviceId, string inputBits)
|
||||
{
|
||||
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
|
||||
{
|
||||
handler.HandleInputReceived(inputBits);
|
||||
return;
|
||||
}
|
||||
|
||||
_inputQueue.Enqueue(new DeviceInputMessage(deviceId, inputBits));
|
||||
}
|
||||
|
||||
private void OnDeviceRawLineReceived(string deviceId, string line)
|
||||
{
|
||||
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
|
||||
{
|
||||
handler.HandleRawLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDeviceAuthorized(string deviceId)
|
||||
{
|
||||
if (_devices.TryGetValue(deviceId, out ComDeviceClient client) &&
|
||||
@ -214,20 +301,48 @@ namespace RR.Common.Controls
|
||||
_defaultDeviceId = deviceId;
|
||||
}
|
||||
|
||||
if (_handlers.TryGetValue(deviceId, out IComControllerHandler existing))
|
||||
{
|
||||
existing.Dispose();
|
||||
}
|
||||
|
||||
_handlers[deviceId] = CreateHandlerForDevice(deviceId);
|
||||
_authorizedQueue.Enqueue(deviceId);
|
||||
}
|
||||
|
||||
private void OnDeviceDisconnected(string deviceId)
|
||||
{
|
||||
if (!_devices.TryGetValue(deviceId, out ComDeviceClient client)) return;
|
||||
if (!_devices.TryGetValue(deviceId, out ComDeviceClient client))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnsubscribeDevice(client);
|
||||
_devices.Remove(deviceId);
|
||||
|
||||
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
|
||||
{
|
||||
handler.Dispose();
|
||||
_handlers.Remove(deviceId);
|
||||
}
|
||||
|
||||
if (_defaultDeviceId == deviceId)
|
||||
{
|
||||
_defaultDeviceId = _devices.Keys.FirstOrDefault() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private IComControllerHandler CreateHandlerForDevice(string deviceId)
|
||||
{
|
||||
string role = GetDeviceRole(deviceId);
|
||||
IComControllerHandler customHandler = _handlerFactory?.Invoke(deviceId, role);
|
||||
if (customHandler != null)
|
||||
{
|
||||
return customHandler;
|
||||
}
|
||||
|
||||
return new DefaultComControllerHandler(deviceId, _coinQueue, _inputQueue);
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct DeviceInputMessage
|
||||
@ -242,3 +357,5 @@ namespace RR.Common.Controls
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +1,305 @@
|
||||
using System;
|
||||
using RR.Game2D.Core.Managers;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace RR.Common.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Новый публичный компонент COM-системы.
|
||||
/// Наследует реализацию от <see cref="ComWrapper"/> для плавной миграции.
|
||||
/// Публичный компонент COM-системы.
|
||||
/// </summary>
|
||||
[AddComponentMenu("RR/Controls/Com System Manager")]
|
||||
public class ComSystemManager : ComWrapper
|
||||
public class ComSystemManager : MonoBehaviour
|
||||
{
|
||||
private enum ControllerHandlerType
|
||||
{
|
||||
Default = 0,
|
||||
PrizeStand = 1
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private sealed class RoleHandlerBinding
|
||||
{
|
||||
public string role;
|
||||
public ControllerHandlerType handlerType = ControllerHandlerType.Default;
|
||||
[Min(0)] public int prizeStandBoxesCount;
|
||||
}
|
||||
|
||||
[Header("Основные настройки")]
|
||||
[SerializeField] private bool saveMoneyBetweenSessions;
|
||||
[SerializeField] private bool enablePrizeChecking;
|
||||
[SerializeField] private bool keepAlive = true;
|
||||
|
||||
[Header("Авторизация")]
|
||||
[SerializeField] private bool enableEncryptedHandshake = true;
|
||||
[SerializeField] private int handshakeTimeoutMs = 5000;
|
||||
|
||||
[Header("Роль контроллера (после handshake)")]
|
||||
[SerializeField] private string roleRequestCommand = "97";
|
||||
[SerializeField] private string roleResponsePrefix = "ROLE";
|
||||
[SerializeField] private int roleRequestTimeoutMs = 1500;
|
||||
[SerializeField] private string mainControllerRole = "main_controller";
|
||||
|
||||
[Header("Мульти-устройства")]
|
||||
[SerializeField] private bool useAllDetectedPorts = true;
|
||||
[SerializeField] private string[] explicitPorts = Array.Empty<string>();
|
||||
[SerializeField] private string defaultDevicePort = "";
|
||||
|
||||
[Header("Фабрика обработчиков по ROLE")]
|
||||
[SerializeField] private RoleHandlerBinding[] roleHandlerBindings =
|
||||
{
|
||||
new RoleHandlerBinding { role = "prize_stand", handlerType = ControllerHandlerType.PrizeStand, prizeStandBoxesCount = 24 },
|
||||
new RoleHandlerBinding { role = "prize_stand_controller", handlerType = ControllerHandlerType.PrizeStand, prizeStandBoxesCount = 24 }
|
||||
};
|
||||
|
||||
public static UnityEvent<int> OnCreditAdded;
|
||||
public static UnityEvent<int> OnCreditRemoved;
|
||||
public static UnityEvent<string> OnInputReceived;
|
||||
public static Action OnInitialize;
|
||||
|
||||
public static UnityEvent<string, string> OnDeviceInputReceived;
|
||||
public static UnityEvent<string> OnDeviceAuthorized;
|
||||
|
||||
private static ComSystemManager _instance;
|
||||
private static int _creditsCount;
|
||||
|
||||
private ComHub _manager;
|
||||
private LightController _light;
|
||||
private bool _initializeInvoked;
|
||||
|
||||
public LightController Light => _light;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_instance = this;
|
||||
|
||||
OnCreditAdded = new UnityEvent<int>();
|
||||
OnCreditRemoved = new UnityEvent<int>();
|
||||
OnInputReceived = new UnityEvent<string>();
|
||||
OnDeviceInputReceived = new UnityEvent<string, string>();
|
||||
OnDeviceAuthorized = new UnityEvent<string>();
|
||||
|
||||
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
|
||||
_creditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
|
||||
|
||||
_light = GetComponent<LightController>() ?? gameObject.AddComponent<LightController>();
|
||||
|
||||
_manager = new ComHub(
|
||||
useAllDetectedPorts,
|
||||
explicitPorts,
|
||||
defaultDevicePort,
|
||||
enableEncryptedHandshake,
|
||||
handshakeTimeoutMs,
|
||||
keepAlive || enablePrizeChecking,
|
||||
roleRequestCommand,
|
||||
roleResponsePrefix,
|
||||
roleRequestTimeoutMs,
|
||||
mainControllerRole,
|
||||
CreateRoleHandler);
|
||||
|
||||
_manager.InitializeDevices();
|
||||
}
|
||||
|
||||
public static void AddCredit()
|
||||
{
|
||||
_creditsCount++;
|
||||
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
|
||||
OnCreditAdded?.Invoke(_creditsCount);
|
||||
}
|
||||
|
||||
public static bool Pay(int amount = 1)
|
||||
{
|
||||
if (_creditsCount < amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_creditsCount -= amount;
|
||||
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
|
||||
|
||||
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
|
||||
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
|
||||
|
||||
OnCreditRemoved?.Invoke(_creditsCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static int GetCredits() => _creditsCount;
|
||||
|
||||
public static void SetCredits(int amount)
|
||||
{
|
||||
int oldCredits = _creditsCount;
|
||||
_creditsCount = Mathf.Max(0, amount);
|
||||
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
|
||||
|
||||
if (_creditsCount > oldCredits)
|
||||
{
|
||||
OnCreditAdded?.Invoke(_creditsCount);
|
||||
}
|
||||
else if (_creditsCount < oldCredits)
|
||||
{
|
||||
OnCreditRemoved?.Invoke(_creditsCount);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Send(string message)
|
||||
{
|
||||
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
|
||||
_instance._manager?.SendToDefault(message);
|
||||
}
|
||||
|
||||
public static void SendToDevice(string deviceId, string message)
|
||||
{
|
||||
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
|
||||
_instance._manager?.SendToDevice(deviceId, message);
|
||||
}
|
||||
|
||||
public static void SendToAll(string message)
|
||||
{
|
||||
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
|
||||
_instance._manager?.SendToAll(message);
|
||||
}
|
||||
|
||||
public static string GetLastInputState()
|
||||
{
|
||||
if (_instance?._manager == null) return "11111111111111111111111111";
|
||||
string defaultId = _instance._manager.DefaultDeviceId;
|
||||
return string.IsNullOrWhiteSpace(defaultId)
|
||||
? "11111111111111111111111111"
|
||||
: _instance._manager.GetLastInputState(defaultId);
|
||||
}
|
||||
|
||||
public static string GetLastInputState(string deviceId)
|
||||
{
|
||||
return _instance?._manager?.GetLastInputState(deviceId) ?? "11111111111111111111111111";
|
||||
}
|
||||
|
||||
public static string[] GetConnectedDeviceIds()
|
||||
{
|
||||
return _instance?._manager?.ConnectedDeviceIds ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public static string GetDefaultDeviceId()
|
||||
{
|
||||
return _instance?._manager?.DefaultDeviceId ?? string.Empty;
|
||||
}
|
||||
|
||||
public static string GetDeviceRole(string deviceId)
|
||||
{
|
||||
return _instance?._manager?.GetDeviceRole(deviceId) ?? string.Empty;
|
||||
}
|
||||
|
||||
public static PrizeStandController GetPrizeStandController(string deviceId = null)
|
||||
{
|
||||
return _instance?._manager?.GetPrizeStandController(deviceId);
|
||||
}
|
||||
|
||||
private IComControllerHandler CreateRoleHandler(string deviceId, string role)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(role) || roleHandlerBindings == null || roleHandlerBindings.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
RoleHandlerBinding binding = FindRoleBinding(role);
|
||||
if (binding == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (binding.handlerType == ControllerHandlerType.PrizeStand)
|
||||
{
|
||||
PrizeStandController controller = new PrizeStandController(
|
||||
deviceId,
|
||||
(id, message) => _manager != null && _manager.SendToDevice(id, message),
|
||||
message => GameManager.Logger?.Log("PrizeStandController", $"[{deviceId}] {message}"));
|
||||
|
||||
controller.SetBoxesCount(binding.prizeStandBoxesCount);
|
||||
return controller;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private RoleHandlerBinding FindRoleBinding(string role)
|
||||
{
|
||||
for (int i = 0; i < roleHandlerBindings.Length; i++)
|
||||
{
|
||||
RoleHandlerBinding binding = roleHandlerBindings[i];
|
||||
if (binding == null || string.IsNullOrWhiteSpace(binding.role))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(binding.role.Trim(), role.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return binding;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Blink() => _light.Blink();
|
||||
|
||||
public void StopBlink(bool enableHat = true) => _light.StopBlink(enableHat);
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_manager == null) return;
|
||||
|
||||
int coinsProcessed = 0;
|
||||
while (_manager.TryDequeueCoin(out _))
|
||||
{
|
||||
AddCredit();
|
||||
coinsProcessed++;
|
||||
}
|
||||
|
||||
while (_manager.TryDequeueInput(out DeviceInputMessage msg))
|
||||
{
|
||||
OnDeviceInputReceived?.Invoke(msg.DeviceId, msg.InputBits);
|
||||
|
||||
if (msg.DeviceId == _manager.DefaultDeviceId)
|
||||
{
|
||||
OnInputReceived?.Invoke(msg.InputBits);
|
||||
}
|
||||
}
|
||||
|
||||
while (_manager.TryDequeueAuthorized(out string deviceId))
|
||||
{
|
||||
OnDeviceAuthorized?.Invoke(deviceId);
|
||||
|
||||
if (!_initializeInvoked)
|
||||
{
|
||||
_initializeInvoked = true;
|
||||
OnInitialize?.Invoke();
|
||||
}
|
||||
|
||||
if (deviceId == _manager.DefaultDeviceId)
|
||||
{
|
||||
_light.Blink();
|
||||
}
|
||||
}
|
||||
|
||||
if (coinsProcessed > 0)
|
||||
{
|
||||
GameManager.Logger?.Log("ComSystemManager", $"Обработано монет: {coinsProcessed}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_manager?.ShutdownAll(false);
|
||||
_manager = null;
|
||||
_initializeInvoked = false;
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
_manager?.ShutdownAll(true);
|
||||
_manager = null;
|
||||
_initializeInvoked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,231 +1,14 @@
|
||||
using System;
|
||||
using RR.Game2D.Core.Managers;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace RR.Common.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Совместимый фасад над мульти-устройственной COM-системой.
|
||||
/// Старый API сохранен для текущего проекта.
|
||||
/// Устаревшее имя компонента COM-системы.
|
||||
/// Используйте <see cref="ComSystemManager"/>.
|
||||
/// </summary>
|
||||
public class ComWrapper : MonoBehaviour
|
||||
[Obsolete("Use ComSystemManager instead")]
|
||||
public class ComWrapper : ComSystemManager
|
||||
{
|
||||
[Header("Основные настройки")]
|
||||
[SerializeField] private bool saveMoneyBetweenSessions;
|
||||
[SerializeField] private bool enablePrizeChecking;
|
||||
[SerializeField] private bool keepAlive = true;
|
||||
|
||||
[Header("Авторизация")]
|
||||
[SerializeField] private bool enableEncryptedHandshake = true;
|
||||
[SerializeField] private int handshakeTimeoutMs = 5000;
|
||||
|
||||
[Header("Роль контроллера (после handshake)")]
|
||||
[SerializeField] private string roleRequestCommand = "97";
|
||||
[SerializeField] private string roleResponsePrefix = "ROLE";
|
||||
[SerializeField] private int roleRequestTimeoutMs = 1500;
|
||||
[SerializeField] private string mainControllerRole = "main_controller";
|
||||
|
||||
[Header("Мульти-устройства")]
|
||||
[SerializeField] private bool useAllDetectedPorts = true;
|
||||
[SerializeField] private string[] explicitPorts = Array.Empty<string>();
|
||||
[SerializeField] private string defaultDevicePort = "";
|
||||
|
||||
public static UnityEvent<int> OnCreditAdded;
|
||||
public static UnityEvent<int> OnCreditRemoved;
|
||||
public static UnityEvent<string> OnInputReceived;
|
||||
public static Action OnInitialize;
|
||||
|
||||
public static UnityEvent<string, string> OnDeviceInputReceived;
|
||||
public static UnityEvent<string> OnDeviceAuthorized;
|
||||
|
||||
private static ComWrapper _instance;
|
||||
private static int _creditsCount;
|
||||
|
||||
private ComMultiManager _manager;
|
||||
private LightController _light;
|
||||
private bool _initializeInvoked;
|
||||
|
||||
public LightController Light => _light;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_instance = this;
|
||||
|
||||
OnCreditAdded = new UnityEvent<int>();
|
||||
OnCreditRemoved = new UnityEvent<int>();
|
||||
OnInputReceived = new UnityEvent<string>();
|
||||
OnDeviceInputReceived = new UnityEvent<string, string>();
|
||||
OnDeviceAuthorized = new UnityEvent<string>();
|
||||
|
||||
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
|
||||
_creditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
|
||||
|
||||
_light = GetComponent<LightController>() ?? gameObject.AddComponent<LightController>();
|
||||
|
||||
_manager = new ComMultiManager(
|
||||
useAllDetectedPorts,
|
||||
explicitPorts,
|
||||
defaultDevicePort,
|
||||
enableEncryptedHandshake,
|
||||
handshakeTimeoutMs,
|
||||
keepAlive || enablePrizeChecking,
|
||||
roleRequestCommand,
|
||||
roleResponsePrefix,
|
||||
roleRequestTimeoutMs,
|
||||
mainControllerRole);
|
||||
|
||||
_manager.InitializeDevices();
|
||||
}
|
||||
|
||||
public static void AddCredit()
|
||||
{
|
||||
_creditsCount++;
|
||||
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
|
||||
OnCreditAdded?.Invoke(_creditsCount);
|
||||
}
|
||||
|
||||
public static bool Pay(int amount = 1)
|
||||
{
|
||||
if (_creditsCount < amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_creditsCount -= amount;
|
||||
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
|
||||
|
||||
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
|
||||
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
|
||||
|
||||
OnCreditRemoved?.Invoke(_creditsCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static int GetCredits() => _creditsCount;
|
||||
|
||||
public static void SetCredits(int amount)
|
||||
{
|
||||
int oldCredits = _creditsCount;
|
||||
_creditsCount = Mathf.Max(0, amount);
|
||||
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
|
||||
|
||||
if (_creditsCount > oldCredits)
|
||||
{
|
||||
OnCreditAdded?.Invoke(_creditsCount);
|
||||
}
|
||||
else if (_creditsCount < oldCredits)
|
||||
{
|
||||
OnCreditRemoved?.Invoke(_creditsCount);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Send(string message)
|
||||
{
|
||||
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
|
||||
_instance._manager?.SendToDefault(message);
|
||||
}
|
||||
|
||||
public static void SendToDevice(string deviceId, string message)
|
||||
{
|
||||
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
|
||||
_instance._manager?.SendToDevice(deviceId, message);
|
||||
}
|
||||
|
||||
public static void SendToAll(string message)
|
||||
{
|
||||
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
|
||||
_instance._manager?.SendToAll(message);
|
||||
}
|
||||
|
||||
public static string GetLastInputState()
|
||||
{
|
||||
if (_instance?._manager == null) return "11111111111111111111111111";
|
||||
string defaultId = _instance._manager.DefaultDeviceId;
|
||||
return string.IsNullOrWhiteSpace(defaultId)
|
||||
? "11111111111111111111111111"
|
||||
: _instance._manager.GetLastInputState(defaultId);
|
||||
}
|
||||
|
||||
public static string GetLastInputState(string deviceId)
|
||||
{
|
||||
return _instance?._manager?.GetLastInputState(deviceId) ?? "11111111111111111111111111";
|
||||
}
|
||||
|
||||
public static string[] GetConnectedDeviceIds()
|
||||
{
|
||||
return _instance?._manager?.ConnectedDeviceIds ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
public static string GetDefaultDeviceId()
|
||||
{
|
||||
return _instance?._manager?.DefaultDeviceId ?? string.Empty;
|
||||
}
|
||||
|
||||
public static string GetDeviceRole(string deviceId)
|
||||
{
|
||||
return _instance?._manager?.GetDeviceRole(deviceId) ?? string.Empty;
|
||||
}
|
||||
|
||||
public void Blink() => _light.Blink();
|
||||
|
||||
public void StopBlink(bool enableHat = true) => _light.StopBlink(enableHat);
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_manager == null) return;
|
||||
|
||||
int coinsProcessed = 0;
|
||||
while (_manager.TryDequeueCoin(out _))
|
||||
{
|
||||
AddCredit();
|
||||
coinsProcessed++;
|
||||
}
|
||||
|
||||
while (_manager.TryDequeueInput(out DeviceInputMessage msg))
|
||||
{
|
||||
OnDeviceInputReceived?.Invoke(msg.DeviceId, msg.InputBits);
|
||||
|
||||
if (msg.DeviceId == _manager.DefaultDeviceId)
|
||||
{
|
||||
OnInputReceived?.Invoke(msg.InputBits);
|
||||
}
|
||||
}
|
||||
|
||||
while (_manager.TryDequeueAuthorized(out string deviceId))
|
||||
{
|
||||
OnDeviceAuthorized?.Invoke(deviceId);
|
||||
|
||||
if (!_initializeInvoked)
|
||||
{
|
||||
_initializeInvoked = true;
|
||||
OnInitialize?.Invoke();
|
||||
}
|
||||
|
||||
if (deviceId == _manager.DefaultDeviceId)
|
||||
{
|
||||
_light.Blink();
|
||||
}
|
||||
}
|
||||
|
||||
if (coinsProcessed > 0)
|
||||
{
|
||||
GameManager.Logger?.Log("ComWrapper", $"Обработано монет: {coinsProcessed}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
_manager?.ShutdownAll(false);
|
||||
_manager = null;
|
||||
_initializeInvoked = false;
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
_manager?.ShutdownAll(true);
|
||||
_manager = null;
|
||||
_initializeInvoked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace RR.Common.Controls
|
||||
{
|
||||
internal sealed class DefaultComControllerHandler : IComControllerHandler
|
||||
{
|
||||
private readonly string _deviceId;
|
||||
private readonly ConcurrentQueue<string> _coinQueue;
|
||||
private readonly ConcurrentQueue<DeviceInputMessage> _inputQueue;
|
||||
|
||||
public DefaultComControllerHandler(
|
||||
string deviceId,
|
||||
ConcurrentQueue<string> coinQueue,
|
||||
ConcurrentQueue<DeviceInputMessage> inputQueue)
|
||||
{
|
||||
_deviceId = deviceId;
|
||||
_coinQueue = coinQueue;
|
||||
_inputQueue = inputQueue;
|
||||
}
|
||||
|
||||
public void HandleCoinAccepted()
|
||||
{
|
||||
_coinQueue.Enqueue(_deviceId);
|
||||
}
|
||||
|
||||
public void HandleInputReceived(string inputBits)
|
||||
{
|
||||
_inputQueue.Enqueue(new DeviceInputMessage(_deviceId, inputBits));
|
||||
}
|
||||
|
||||
public void HandleRawLine(string line)
|
||||
{
|
||||
// Для контроллера по умолчанию обработка сырых строк не требуется.
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3aed53dc839b7134c91148aca38412db
|
||||
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace RR.Common.Controls
|
||||
{
|
||||
internal interface IComControllerHandler : IDisposable
|
||||
{
|
||||
void HandleCoinAccepted();
|
||||
void HandleInputReceived(string inputBits);
|
||||
void HandleRawLine(string line);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ff383aa1ebc9874c9019bf48d7bd5b8
|
||||
@ -160,7 +160,7 @@ namespace RR.Common.Controls
|
||||
? Mathf.Max(0.05f, interval + Random.Range(-blinkIntervalVariance, blinkIntervalVariance))
|
||||
: interval;
|
||||
|
||||
yield return new WaitForSeconds(wait);
|
||||
yield return new WaitForSecondsRealtime(wait);
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,3 +179,4 @@ namespace RR.Common.Controls
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -82,7 +82,7 @@ namespace RR.Common.Controls
|
||||
_controller.SendCommand(step.Mode, step.DurationSeconds);
|
||||
|
||||
if (step.Mode != LightMode.Off && step.DurationSeconds > 0)
|
||||
yield return new WaitForSeconds(step.DurationSeconds);
|
||||
yield return new WaitForSecondsRealtime(step.DurationSeconds);
|
||||
}
|
||||
}
|
||||
while (_looping);
|
||||
@ -91,3 +91,4 @@ namespace RR.Common.Controls
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,315 +1,294 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using RR.Common.Controls;
|
||||
using UnityEngine;
|
||||
|
||||
//Зависит от ComNativeWrapper и одноименного плагина
|
||||
//рекомендуется соединение с таймаутом не меньше 10мс. Иначе вероятно busy loop и много алокаций.
|
||||
public static class PrizeStandController
|
||||
namespace RR.Common.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// По завершении инициализации.
|
||||
/// bool - успех/неуспех.
|
||||
/// string - сообщение в случае неуспеха
|
||||
/// </summary>
|
||||
public static event Action<bool, string> InitializationComplete;
|
||||
/// <summary>
|
||||
/// По завершении операции открытия.
|
||||
/// bool - успех/неуспех.
|
||||
/// string - сообщение в случае неуспеха
|
||||
/// </summary>
|
||||
public static event Action<bool, string> OpeningFinished;
|
||||
|
||||
private const string REQUEST_STAND_STATUS = "25";
|
||||
private const string OPEN_BOX_PREFIX = "2501";
|
||||
private const float STATUS_REQUEST_TIMEOUT = 3;
|
||||
private const float WAIT_OPEN_STATUS_TIMEOUT = 5;
|
||||
|
||||
private static readonly IReadOnlyList<BoxStatus> READY_TO_OPEN_STATUSES = new[] { BoxStatus.OK, BoxStatus.OKplusCard };
|
||||
|
||||
private static int handle = -1;
|
||||
private static int boxesCount;
|
||||
private static readonly SemaphoreSlim _lock = new(1, 1);
|
||||
private static bool initialized = false;
|
||||
|
||||
private static ILogger _logger;
|
||||
|
||||
#region PUBLIC API
|
||||
/// <summary>
|
||||
/// Инициализировать модуль витрины.
|
||||
/// </summary>
|
||||
/// <param name="handleId">Id хэндла полученное после ComNativeWrapper.Serial_Open</param>
|
||||
/// <param name="countOfBoxes">Количество физических коробок витрины</param>
|
||||
/// <param name="logger">(опционально) Логгер</param>
|
||||
public static void Initialize(int handleId, int countOfBoxes, ILogger logger)
|
||||
// Зависит от ComHub и обрабатывает контроллеры витрины по ROLE.
|
||||
public sealed class PrizeStandController : IComControllerHandler
|
||||
{
|
||||
bool hasError = false;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
public static readonly string[] SupportedRoles = { "prize_stand", "prize_stand_controller" };
|
||||
|
||||
//Проверка хэндла. h<=0 - ошибка открытия порта
|
||||
if (handleId <= 0)
|
||||
/// <summary>
|
||||
/// По завершении операции открытия.
|
||||
/// bool - успех/неуспех.
|
||||
/// string - сообщение в случае неуспеха
|
||||
/// </summary>
|
||||
public event Action<bool, string> OpeningFinished;
|
||||
|
||||
private const string RequestStandStatus = "25";
|
||||
private const string OpenBoxPrefix = "2501";
|
||||
private const float StatusRequestTimeout = 3f;
|
||||
private const float WaitOpenStatusTimeout = 5f;
|
||||
|
||||
private static readonly IReadOnlyList<BoxStatus> ReadyToOpenStatuses =
|
||||
new[] { BoxStatus.OK, BoxStatus.OKplusCard };
|
||||
|
||||
private readonly string _deviceId;
|
||||
private readonly Func<string, string, bool> _sendToDevice;
|
||||
private readonly Action<string> _log;
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
private readonly ConcurrentQueue<BoxStatusEntry> _statusQueue = new();
|
||||
|
||||
private int _boxesCount;
|
||||
|
||||
public PrizeStandController(string deviceId, Func<string, string, bool> sendToDevice, Action<string> log)
|
||||
{
|
||||
hasError = true;
|
||||
sb.AppendLine($"[Модуль витрины] Переданный handle={handleId}. Ожидалось значение>0.");
|
||||
}
|
||||
else handle = handleId;
|
||||
|
||||
//Проверка количества коробок
|
||||
if (countOfBoxes <= 0)
|
||||
{
|
||||
hasError = true;
|
||||
sb.AppendLine($"[Модуль витрины] Попытка задать количество коробок={countOfBoxes}. Ожидалось значение>0.");
|
||||
}
|
||||
else boxesCount = countOfBoxes;
|
||||
|
||||
//Установка логгера
|
||||
_logger = logger;
|
||||
if (_logger == null)
|
||||
sb.AppendLine($"[Модуль витрины] Получен логгер NULL. Если нужно логгирование, передайте дествующий логгер.");
|
||||
|
||||
//Завершение
|
||||
if (hasError)
|
||||
{
|
||||
initialized = false;
|
||||
handle = -1;
|
||||
boxesCount = 0;
|
||||
|
||||
InitializationComplete?.Invoke(false, sb.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
initialized = true;
|
||||
InitializationComplete?.Invoke(true, sb.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Открыть коробку по номеру
|
||||
/// </summary>
|
||||
/// <param name="boxNumber">Номер коробки (считаем с 1)</param>
|
||||
public static async Task OpenBoxAsync(int boxNumber)
|
||||
{
|
||||
if(!await _lock.WaitAsync(0))
|
||||
{
|
||||
OpeningFinished?.Invoke(false, "Выдача уже в процессе");
|
||||
return;
|
||||
_deviceId = deviceId;
|
||||
_sendToDevice = sendToDevice;
|
||||
_log = log;
|
||||
}
|
||||
|
||||
try
|
||||
public void SetBoxesCount(int boxesCount)
|
||||
{
|
||||
//Валидация
|
||||
var validationResult = await CanExecuteOpenAsync(boxNumber);
|
||||
if (!validationResult.OK)
|
||||
_boxesCount = Math.Max(0, boxesCount);
|
||||
}
|
||||
|
||||
public async Task OpenBoxAsync(int boxNumber)
|
||||
{
|
||||
if (!await _lock.WaitAsync(0))
|
||||
{
|
||||
OpeningFinished?.Invoke(false, validationResult.Message);
|
||||
OpeningFinished?.Invoke(false, "Выдача уже в процессе");
|
||||
return;
|
||||
}
|
||||
|
||||
SendOpenCommand(boxNumber);
|
||||
try
|
||||
{
|
||||
var validationResult = await CanExecuteOpenAsync(boxNumber);
|
||||
if (!validationResult.OK)
|
||||
{
|
||||
OpeningFinished?.Invoke(false, validationResult.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
//Обработка результатов
|
||||
BoxStatus openStatus = await ReadBoxStatus(boxNumber, 1, WAIT_OPEN_STATUS_TIMEOUT);
|
||||
if (openStatus == BoxStatus.OPENED)
|
||||
{
|
||||
OpeningFinished?.Invoke(true, $"Ячейка {boxNumber} успешно открыта");
|
||||
ClearStatusQueue();
|
||||
SendCommand($"{OpenBoxPrefix}{boxNumber:D2}");
|
||||
|
||||
BoxStatus openStatus = await ReadBoxStatusAsync(boxNumber, 1, WaitOpenStatusTimeout);
|
||||
if (openStatus == BoxStatus.OPENED)
|
||||
{
|
||||
OpeningFinished?.Invoke(true, $"Ячейка {boxNumber} успешно открыта");
|
||||
}
|
||||
else
|
||||
{
|
||||
_log?.Invoke($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}");
|
||||
OpeningFinished?.Invoke(false, $"Ошибка при открытии ячейки номер {boxNumber}");
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}");
|
||||
OpeningFinished?.Invoke(false, $"Ошибка при открытии ячейки номер {boxNumber}");
|
||||
_log?.Invoke($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}: {ex.Message}");
|
||||
OpeningFinished?.Invoke(false, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}: {ex.Message}");
|
||||
OpeningFinished?.Invoke(false, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region HELPERS
|
||||
private static void SendOpenCommand(int boxNumber)
|
||||
=> ComNativeWrapper.Serial_WriteStringUTF8(handleId: handle,
|
||||
utf8: $"{OPEN_BOX_PREFIX}{boxNumber:D2}", //D2 приводит в формат XX (2 знака, ноль впереди)
|
||||
appendNewline: 1);
|
||||
private static void SendRequestStatusCommand()
|
||||
=> ComNativeWrapper.Serial_WriteStringUTF8(handle, REQUEST_STAND_STATUS, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Проверка может ли модуль произвести выдачу
|
||||
/// </summary>
|
||||
private static async Task<(bool OK, string Message)> CanExecuteOpenAsync(int boxNumber)
|
||||
{
|
||||
bool isOk = true;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine($"[Модуль витрины. Проверка перед открытием]");
|
||||
|
||||
//Проверка инициализации
|
||||
if (!initialized)
|
||||
public void HandleCoinAccepted()
|
||||
{
|
||||
isOk = false;
|
||||
sb.AppendLine("- Модуль не инициализирован.");
|
||||
// Для витрины монеты не обрабатываются.
|
||||
}
|
||||
|
||||
bool boxNumberIsValid = boxNumber > 0 && boxNumber <= boxesCount;
|
||||
if (!boxNumberIsValid)
|
||||
public void HandleInputReceived(string inputBits)
|
||||
{
|
||||
isOk = false;
|
||||
sb.AppendLine($"- Номер коробки={boxNumber} невалидный. Требуется от 1 до {boxesCount}.");
|
||||
// Для витрины INPUT-пакеты не используются.
|
||||
}
|
||||
|
||||
//Не лезем в порт с запросом статуса коробки, если то, что выше дало НеОК
|
||||
if (isOk)
|
||||
public void HandleRawLine(string line)
|
||||
{
|
||||
SendRequestStatusCommand();
|
||||
BoxStatus boxStatus = await ReadBoxStatus(boxNumber, boxesCount, STATUS_REQUEST_TIMEOUT);
|
||||
var parseResult = ParseBoxStatus(line);
|
||||
if (parseResult.BoxNumber < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!READY_TO_OPEN_STATUSES.Contains(boxStatus))
|
||||
_statusQueue.Enqueue(new BoxStatusEntry(parseResult.BoxNumber, parseResult.Status));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_lock.Dispose();
|
||||
ClearStatusQueue();
|
||||
}
|
||||
|
||||
public static bool IsSupportedRole(string role)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(role))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return SupportedRoles.Any(r => string.Equals(r, role, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private async Task<(bool OK, string Message)> CanExecuteOpenAsync(int boxNumber)
|
||||
{
|
||||
bool isOk = true;
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine("[Модуль витрины. Проверка перед открытием]");
|
||||
|
||||
if (boxNumber <= 0)
|
||||
{
|
||||
isOk = false;
|
||||
sb.AppendLine($"- Статус коробки {boxNumber}: {boxStatus} не позволяет провести открытие.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"- Невозможно проверить статус коробки {boxNumber} из-за ошибок выше.");
|
||||
}
|
||||
|
||||
return (isOk, sb.ToString());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Читать статус заданной ячейки витрины. Выход когда прочитали заданное количество вхождений, или по таймауту.
|
||||
/// </summary>
|
||||
/// <param name="expectedBoxNumber">Номер ячейки которую мы ждем</param>
|
||||
/// <param name="expectedEntriesCount">Сколько ячеек мы ждем</param>
|
||||
/// <param name="timeout">Таймаут операции(сек.)</param>
|
||||
/// <returns></returns>
|
||||
private static async Task<BoxStatus> ReadBoxStatus(int expectedBoxNumber, int expectedEntriesCount, float timeout)
|
||||
{
|
||||
byte[] buffer = new byte[1024];
|
||||
|
||||
//Изначально предполагаем что статус нужной коробки не пришел
|
||||
BoxStatus status = BoxStatus.NotFound;
|
||||
|
||||
int boxesRecieved = 0;
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
while (sw.Elapsed.TotalSeconds < timeout && boxesRecieved < expectedEntriesCount)
|
||||
{
|
||||
//ждать пока всё не считаем
|
||||
int entryLength = await Task.Run(() => ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle, buffer, buffer.Length));
|
||||
|
||||
if (entryLength == 0) //ничего не пришло
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (entryLength < 0) //ошибка
|
||||
{
|
||||
_logger?.Log($"Ошибка при считывании статуса ячеек={entryLength} :: {ComNativeWrapper.GetLastError()}");
|
||||
continue;
|
||||
sb.AppendLine("- Номер коробки должен быть больше 0.");
|
||||
}
|
||||
|
||||
|
||||
string entry = Encoding.UTF8.GetString(buffer, 0, entryLength).Trim();
|
||||
var ParseResult = ParseBoxStatus(entry);
|
||||
|
||||
if(ParseResult.BoxNumber == -1)
|
||||
if (_boxesCount > 0 && boxNumber > _boxesCount)
|
||||
{
|
||||
_logger?.Log($"[Модуль витрины. Ошибка получения статуса ячейки]\n{ParseResult.Message}");
|
||||
isOk = false;
|
||||
sb.AppendLine($"- Номер коробки={boxNumber} невалидный. Требуется от 1 до {_boxesCount}.");
|
||||
}
|
||||
else
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
boxesRecieved++;
|
||||
if (ParseResult.BoxNumber == expectedBoxNumber)
|
||||
status = ParseResult.Status;
|
||||
}
|
||||
}
|
||||
ClearStatusQueue();
|
||||
SendCommand(RequestStandStatus);
|
||||
|
||||
if (status == BoxStatus.NotFound)
|
||||
_logger?.Log($"[Модуль витрины. Не дождался ответа по статусу ячейки]\n" +
|
||||
$"Ожидаемый номер: {expectedBoxNumber}\n" +
|
||||
$"Получено статусов ячеек: {boxesRecieved}/{expectedEntriesCount}");
|
||||
return status;
|
||||
}
|
||||
int expectedEntriesCount = _boxesCount > 0 ? _boxesCount : 1;
|
||||
BoxStatus boxStatus = await ReadBoxStatusAsync(boxNumber, expectedEntriesCount, StatusRequestTimeout);
|
||||
|
||||
private static (int BoxNumber, BoxStatus Status, string Message) ParseBoxStatus(string entryString)
|
||||
{
|
||||
int number = -1;
|
||||
BoxStatus status = BoxStatus.ParsingError;
|
||||
string message = "";
|
||||
|
||||
|
||||
|
||||
if (entryString.StartsWith("BOX"))
|
||||
{
|
||||
var noPrefixEntry = entryString.Substring(3); //Убираем "BOX"
|
||||
string[] parts = noPrefixEntry.Split('_'); //ожидаем массив [<номер_коробки>, <статус_коробки>]
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
//Парсим номер коробки
|
||||
bool boxNumberParsed = (int.TryParse(parts[0], out int parsedNumber));
|
||||
|
||||
if (boxNumberParsed)
|
||||
if (!ReadyToOpenStatuses.Contains(boxStatus))
|
||||
{
|
||||
number = parsedNumber;
|
||||
status = MapStatusFromString(parts[1]);
|
||||
isOk = false;
|
||||
sb.AppendLine($"- Статус коробки {boxNumber}: {boxStatus} не позволяет провести открытие.");
|
||||
}
|
||||
else message = $"Не удалось спарсить номер коробки: {entryString}";
|
||||
}
|
||||
else
|
||||
{
|
||||
message = $"Ошибка парсинга. Часть строки не пришла: {entryString}";
|
||||
sb.AppendLine($"- Невозможно проверить статус коробки {boxNumber} из-за ошибок выше.");
|
||||
}
|
||||
|
||||
return (isOk, sb.ToString());
|
||||
}
|
||||
|
||||
private async Task<BoxStatus> ReadBoxStatusAsync(int expectedBoxNumber, int expectedEntriesCount, float timeout)
|
||||
{
|
||||
BoxStatus status = BoxStatus.NotFound;
|
||||
int boxesReceived = 0;
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
|
||||
while (sw.Elapsed.TotalSeconds < timeout && boxesReceived < expectedEntriesCount)
|
||||
{
|
||||
while (_statusQueue.TryDequeue(out BoxStatusEntry entry))
|
||||
{
|
||||
boxesReceived++;
|
||||
if (entry.BoxNumber == expectedBoxNumber)
|
||||
{
|
||||
status = entry.Status;
|
||||
}
|
||||
|
||||
if (boxesReceived >= expectedEntriesCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (boxesReceived >= expectedEntriesCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(10);
|
||||
}
|
||||
|
||||
if (status == BoxStatus.NotFound)
|
||||
{
|
||||
_log?.Invoke("[Модуль витрины. Не дождался ответа по статусу ячейки]\n" +
|
||||
$"Ожидаемый номер: {expectedBoxNumber}\n" +
|
||||
$"Получено статусов ячеек: {boxesReceived}/{expectedEntriesCount}");
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
private void SendCommand(string command)
|
||||
{
|
||||
if (!_sendToDevice(_deviceId, command))
|
||||
{
|
||||
throw new InvalidOperationException($"Не удалось отправить команду '{command}' в устройство {_deviceId}");
|
||||
}
|
||||
}
|
||||
else message = $"Неожиданный префикс при парсинге ячеек: {entryString}";
|
||||
|
||||
private void ClearStatusQueue()
|
||||
{
|
||||
while (_statusQueue.TryDequeue(out _))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return (number, status, message);
|
||||
private static (int BoxNumber, BoxStatus Status) ParseBoxStatus(string entryString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entryString) || !entryString.StartsWith("BOX", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (-1, BoxStatus.ParsingError);
|
||||
}
|
||||
|
||||
string noPrefixEntry = entryString.Substring(3);
|
||||
string[] parts = noPrefixEntry.Split('_');
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
return (-1, BoxStatus.ParsingError);
|
||||
}
|
||||
|
||||
if (!int.TryParse(parts[0], out int parsedNumber))
|
||||
{
|
||||
return (-1, BoxStatus.ParsingError);
|
||||
}
|
||||
|
||||
return (parsedNumber, MapStatusFromString(parts[1]));
|
||||
}
|
||||
|
||||
private static BoxStatus MapStatusFromString(string statusString)
|
||||
{
|
||||
return statusString switch
|
||||
{
|
||||
"OK" => BoxStatus.OK,
|
||||
"OK+CARD" => BoxStatus.OKplusCard,
|
||||
"EMPTY" => BoxStatus.Empty,
|
||||
"OPEN" => BoxStatus.Open,
|
||||
"OPENED" => BoxStatus.OPENED,
|
||||
"OPEN_ERROR" => BoxStatus.OPEN_ERROR,
|
||||
"ERROR" => BoxStatus.Error,
|
||||
_ => BoxStatus.ParsingError
|
||||
};
|
||||
}
|
||||
|
||||
private readonly struct BoxStatusEntry
|
||||
{
|
||||
public readonly int BoxNumber;
|
||||
public readonly BoxStatus Status;
|
||||
|
||||
public BoxStatusEntry(int boxNumber, BoxStatus status)
|
||||
{
|
||||
BoxNumber = boxNumber;
|
||||
Status = status;
|
||||
}
|
||||
}
|
||||
}
|
||||
private static BoxStatus MapStatusFromString(string statusString) => statusString switch
|
||||
|
||||
public enum BoxStatus
|
||||
{
|
||||
"OK" => BoxStatus.OK,
|
||||
"OK+CARD" => BoxStatus.OKplusCard,
|
||||
"EMPTY" => BoxStatus.Empty,
|
||||
"OPEN" => BoxStatus.Open,
|
||||
// Статусы ДО открытия
|
||||
OK,
|
||||
OKplusCard,
|
||||
Empty,
|
||||
Open,
|
||||
|
||||
"OPENED" => BoxStatus.OPENED,
|
||||
"OPEN_ERROR" => BoxStatus.OPEN_ERROR,
|
||||
// Статусы ПОСЛЕ открытия
|
||||
OPENED,
|
||||
OPEN_ERROR,
|
||||
|
||||
"ERROR" => BoxStatus.Error,
|
||||
|
||||
_ => BoxStatus.ParsingError
|
||||
};
|
||||
#endregion
|
||||
// Служебные статусы
|
||||
ParsingError,
|
||||
NotFound,
|
||||
Error
|
||||
}
|
||||
}
|
||||
|
||||
enum BoxStatus
|
||||
{
|
||||
//Статусы ДО открытия
|
||||
OK, //Готов к выдаче
|
||||
OKplusCard, //Готов к выдаче + готов выдать по карте
|
||||
Empty, //Пусто, уже выдали
|
||||
Open, //Дверка УЖЕ открыта. Приз предположительно пустой
|
||||
|
||||
//Статусы ПОСЛЕ открытия
|
||||
OPENED, //Всё хорошо, открылся
|
||||
OPEN_ERROR, //Ошибка открытия
|
||||
|
||||
//Служебные статусы
|
||||
ParsingError, //При парсинге что то пошло не так, или пришла непредусмотренная фигня
|
||||
NotFound, //Коробка с требуемым номером не найдена
|
||||
Error //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок)
|
||||
}
|
||||
|
||||
@ -34,8 +34,8 @@ namespace RR.Game2D.Core.Managers
|
||||
// Plate control for game
|
||||
public ComSystemManager Plate { get; private set; }
|
||||
// Light control for game
|
||||
public LightController Light => Plate != null ? Plate.Light : null;
|
||||
|
||||
public LightController Light { get; private set; }
|
||||
|
||||
// Globals
|
||||
[Header("Globals")]
|
||||
[SerializeField] private string globalsPrefabResourcePath = "Prefabs/Global/Globals";
|
||||
@ -158,10 +158,16 @@ namespace RR.Game2D.Core.Managers
|
||||
if (Inactivity == null) Inactivity = gameObject.AddComponent<InactivityManager>();
|
||||
if (Inactivity == null) Inactivity = InactivityManager.Instance;
|
||||
|
||||
// Apply loaded settings (new system applies automatically in Awake)
|
||||
|
||||
if (Plate == null) Plate = globals.GetComponent<ComSystemManager>();
|
||||
|
||||
if (Plate != null) Light = Plate.Light;
|
||||
if (Light) Light.SetLight(LightMode.DiagonalWave)
|
||||
.Then(LightMode.DiagonalWave2)
|
||||
.Then(LightMode.Rainbow)
|
||||
.Then(LightMode.FallingRows)
|
||||
.Then(LightMode.SeaWave)
|
||||
.Loop();
|
||||
|
||||
Payment.OnCreditAdded.AddListener(OnCreditAdded);
|
||||
|
||||
_gameStartTime = Time.unscaledTime;
|
||||
@ -174,8 +180,11 @@ namespace RR.Game2D.Core.Managers
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (!Light) return;
|
||||
Light.StopBlink(false);
|
||||
Light.StopSequence();
|
||||
|
||||
if (Instance == this) Instance = null;
|
||||
Plate?.StopBlink(false);
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
@ -331,8 +340,13 @@ public void TogglePause() => SetPause(!IsPaused);
|
||||
_gameStartTime = Time.time;
|
||||
Logger.Log(gameObject.name,$"Game start time = {_gameStartTime}");
|
||||
|
||||
if (Plate) Plate.StopBlink();
|
||||
Light?.SetLight(LightMode.Rainbow, 99);
|
||||
// Активируем шапку и эффекты при начале игры
|
||||
if (Light)
|
||||
{
|
||||
Light.StopBlink();
|
||||
if (Light.IsSequencePlaying) Light.StopSequence();
|
||||
Light.SetLight(LightMode.Rainbow).Loop();
|
||||
}
|
||||
|
||||
// Включаем отслеживание бездействия
|
||||
if (Inactivity != null) Inactivity.EnableInactivityCheck();
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"com.github-glitchenzo.nugetforunity": "https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity",
|
||||
"com.quaza.unitymcp": "https://github.com/quazaai/UnityMCPIntegration.git",
|
||||
"com.unity.ai.inference": "2.5.0",
|
||||
"com.unity.cinemachine": "3.1.5",
|
||||
"com.unity.collab-proxy": "2.11.3",
|
||||
|
||||
@ -7,13 +7,6 @@
|
||||
"dependencies": {},
|
||||
"hash": "34432703a177c97631a4eb1d908f086037c67969"
|
||||
},
|
||||
"com.quaza.unitymcp": {
|
||||
"version": "https://github.com/quazaai/UnityMCPIntegration.git",
|
||||
"depth": 0,
|
||||
"source": "git",
|
||||
"dependencies": {},
|
||||
"hash": "d1515b326e2fc14b39de5a08534159ba29963c7d"
|
||||
},
|
||||
"com.unity.2d.animation": {
|
||||
"version": "13.0.4",
|
||||
"depth": 1,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user