Files
BQP/srcs/RobotNet10/RobotApp/Communication/Olei.LidarSensor/ARCHITECTURE.md
2026-07-13 09:25:40 +07:00

413 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Olei LiDAR Sensor - Architecture
## System Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Olei LiDAR Sensor │
│ (LR-1F / LR-1BS Hardware) │
└───────────────────────┬─────────────────────────────────────┘
│ UDP/IP (Port 2368)
│ 1240 bytes packets
│ Little-endian format
┌─────────────────────────────────────────────────────────────┐
│ OleiLidarServer (Main Component) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Dedicated Receive Thread │ │
│ │ (ThreadPriority.AboveNormal) │ │
│ │ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ UdpClient.Receive() │ │ │
│ │ │ • Non-blocking receive │ │ │
│ │ │ • 1MB receive buffer │ │ │
│ │ └──────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ Quick Validation │ │ │
│ │ │ • Check Frame ID │ │ │
│ │ │ • Verify packet size │ │ │
│ │ └──────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ Get Packet from Pool │ │ │
│ │ │ • ConcurrentBag<Packet> │ │ │
│ │ │ • Reuse allocated objects │ │ │
│ │ └──────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ LidarPacketParser │ │ │
│ │ │ • Zero-allocation parsing │ │ │
│ │ │ • Span<byte> based │ │ │
│ │ │ • MemoryMarshal.Read<T> │ │ │
│ │ └──────────────┬───────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ Raise DataReceived Event │ │ │
│ │ │ • Thread-safe event invocation │ │ │
│ │ │ • Pass LidarDataPacket │ │ │
│ │ └──────────────────────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Resource Management │ │
│ │ • ArrayPool<byte> for buffers │ │
│ │ • ConcurrentBag for packet pooling │ │
│ │ • Interlocked counters for statistics │ │
│ └────────────────────────────────────────────────┘ │
│ │
└────────────────────┬────────────────────────────────────────┘
│ Events
│ • DataReceived
│ • ErrorOccurred
┌─────────────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ server.DataReceived += (sender, e) => │
│ { │
│ var packet = e.Packet; │
│ // Process LiDAR data │
│ e.Server.ReturnPacketToPool(packet); // IMPORTANT! │
│ }; │
│ │
└─────────────────────────────────────────────────────────────┘
```
## Data Flow
### Packet Structure (1240 bytes)
```
┌────────────────────────────────────────────────────────────────┐
│ PACKET (1240 bytes) │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ HEADER (40 bytes) │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ Offset │ Size │ Field │ │
│ ├────────┼──────┼──────────────────────────────────────────┤ │
│ │ 0 │ 4 │ Frame ID (0xFEF0010F) │ │
│ │ 4 │ 2 │ Protocol Version (0x0200) │ │
│ │ 6 │ 1 │ Distance Scale │ │
│ │ 7 │ 3 │ Brand Name │ │
│ │ 10 │ 12 │ Commercial Type │ │
│ │ 22 │ 2 │ Internal Type Code │ │
│ │ 24 │ 2 │ Hardware Version │ │
│ │ 26 │ 2 │ Software Version │ │
│ │ 28 │ 4 │ Time Stamp │ │
│ │ 32 │ 2 │ Rotation Rate & Direction │ │
│ │ 34 │ 1 │ Safe Zone Status │ │
│ │ 35 │ 1 │ Error Status │ │
│ │ 36 │ 4 │ NTP Timestamp (integer part) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ DATA BLOCKS (150 × 8 = 1200 bytes) │ │
│ ├──────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ Block 0: [Angle][Distance][Strength][Reserved] │ │
│ │ └─2B──┘└───2B───┘└───2B───┘└───2B───┘ │ │
│ │ │ │
│ │ Block 1: [Angle][Distance][Strength][Reserved] │ │
│ │ │ │
│ │ Block 2: [Angle][Distance][Strength][Reserved] │ │
│ │ │ │
│ │ ... │ │
│ │ │ │
│ │ Block 149: [Angle][Distance][Strength][Reserved] │ │
│ │ │ │
│ │ Each block represents one measurement point: │ │
│ │ • Angle: 0-35999 (0.01° resolution) │ │
│ │ • Distance: Raw value × Distance Scale │ │
│ │ • Strength: Signal strength (0-65535) │ │
│ │ • Invalid if Angle >= 0xFF00 │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
```
## Class Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ LidarHeader │
│ (struct, 40 bytes) │
├─────────────────────────────────────────────────────────────┤
│ + Id: uint │
│ + ProtocolVersion: ushort │
│ + DistanceScale: byte │
│ + TimeStamp: uint │
│ + RotationRateAndDirection: ushort │
│ + ErrorStatus: byte │
│ + ... │
├─────────────────────────────────────────────────────────────┤
│ + IsValidFrame: bool │
│ + RotationRate: ushort │
│ + IsCounterClockwise: bool │
│ + HasMotorFault: bool │
│ + HasAbnormalVoltage: bool │
│ + HasTemperatureFault: bool │
└─────────────────────────────────────────────────────────────┘
│ used by
┌─────────────────────────────────────────────────────────────┐
│ LidarDataPacket │
│ (class, 1240 bytes total) │
├─────────────────────────────────────────────────────────────┤
│ + Header: LidarHeader │
│ + DataBlocks: LidarDataBlock[150] │
│ + ReceivedTime: DateTime │
├─────────────────────────────────────────────────────────────┤
│ + GetValidDataBlocks(): IEnumerable<LidarDataBlock> │
│ + GetValidDataBlockCount(): int │
│ + GetValidPoints(): IEnumerable<(angle, distance)> │
│ + GetValidCartesianPoints(): IEnumerable<(x, y)> │
└─────────────────────────────────────────────────────────────┘
│ contains
┌─────────────────────────────────────────────────────────────┐
│ LidarDataBlock │
│ (struct, 8 bytes) │
├─────────────────────────────────────────────────────────────┤
│ + AngleRaw: ushort │
│ + DistanceRaw: ushort │
│ + SignalStrength: ushort │
│ + Reserved: ushort │
├─────────────────────────────────────────────────────────────┤
│ + IsValid: bool │
│ + GetAngleDegrees(): double │
│ + GetAngleRadians(): double │
│ + GetDistance(scale): double │
│ + GetX(scale): double │
│ + GetY(scale): double │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ LidarPacketParser (static) │
├─────────────────────────────────────────────────────────────┤
│ + TryParse(byte[], length, packet): bool │
│ + TryParse(ReadOnlySpan<byte>, packet): bool │
│ + IsValidPacket(ReadOnlySpan<byte>): bool │
│ + GetDistanceScale(ReadOnlySpan<byte>): byte │
│ + GetTimestamp(ReadOnlySpan<byte>): uint │
│ + GetErrorStatus(ReadOnlySpan<byte>): byte │
│ + HasErrors(ReadOnlySpan<byte>): bool │
└─────────────────────────────────────────────────────────────┘
│ used by
┌─────────────────────────────────────────────────────────────┐
│ OleiLidarServer │
│ : IDisposable │
├─────────────────────────────────────────────────────────────┤
│ - _udpClient: UdpClient │
│ - _receiveThread: Thread │
│ - _bufferPool: ArrayPool<byte> │
│ - _packetPool: ConcurrentBag<LidarDataPacket> │
│ - _isRunning: bool │
├─────────────────────────────────────────────────────────────┤
│ + DataReceived: event │
│ + ErrorOccurred: event │
├─────────────────────────────────────────────────────────────┤
│ + Start(): void │
│ + Stop(): void │
│ + ReturnPacketToPool(packet): void │
│ + GetStatistics(): string │
│ + ResetStatistics(): void │
│ + Dispose(): void │
├─────────────────────────────────────────────────────────────┤
│ - ReceiveLoop(): void │
│ - ProcessPacket(data): void │
└─────────────────────────────────────────────────────────────┘
```
## Threading Model
```
┌─────────────────────────────────────────────────────────────┐
│ Main Thread │
│ • Start() / Stop() server │
│ • Subscribe to events │
│ • Application logic │
└───────────────────────┬─────────────────────────────────────┘
│ Starts/Stops
┌─────────────────────────────────────────────────────────────┐
│ Dedicated Receive Thread │
│ (ThreadPriority.AboveNormal) │
│ │
│ while (_isRunning) │
│ { │
│ // Non-blocking check for available data │
│ if (Available > 0) │
│ { │
│ byte[] data = Receive(); │
│ ProcessPacket(data); // Parse & raise event │
│ } │
│ else │
│ { │
│ Thread.Sleep(1); // Prevent busy-wait │
│ } │
│ } │
│ │
└───────────────────────┬─────────────────────────────────────┘
│ Raises Event
┌─────────────────────────────────────────────────────────────┐
│ Event Handler Thread │
│ (Same as Receive Thread Context) │
│ │
│ server.DataReceived += (sender, e) => │
│ { │
│ // WARNING: This runs on receive thread! │
│ // Keep processing quick or dispatch to another thread│
│ ProcessData(e.Packet); │
│ e.Server.ReturnPacketToPool(e.Packet); │
│ }; │
│ │
└─────────────────────────────────────────────────────────────┘
```
## Memory Management Strategy
```
┌─────────────────────────────────────────────────────────────┐
│ Memory Pools │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────┐ │
│ │ ArrayPool<byte>.Shared │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Buffer 1 (2480 bytes) │ │ │
│ │ ├──────────────────────────┤ │ │
│ │ │ Buffer 2 (2480 bytes) │ │ │
│ │ ├──────────────────────────┤ │ │
│ │ │ Buffer 3 (2480 bytes) │ │ │
│ │ └──────────────────────────┘ │ │
│ │ │ │
│ │ • Rented in ReceiveLoop() │ │
│ │ • Returned after thread exits │ │
│ │ • Shared across application │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ ConcurrentBag<LidarDataPacket> │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Packet 1 (reusable) │ │ │
│ │ ├──────────────────────────┤ │ │
│ │ │ Packet 2 (reusable) │ │ │
│ │ ├──────────────────────────┤ │ │
│ │ │ Packet 3 (reusable) │ │ │
│ │ ├──────────────────────────┤ │ │
│ │ │ ... │ │ │
│ │ ├──────────────────────────┤ │ │
│ │ │ Packet N (max 50) │ │ │
│ │ └──────────────────────────┘ │ │
│ │ │ │
│ │ • Pre-allocated (10 initially) │ │
│ │ • Taken for each received packet │ │
│ │ • Must be returned by user! │ │
│ │ • Limited to 50 to prevent bloat │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Lifecycle:
1. Receive data → Get buffer from ArrayPool
2. Parse → Get packet from ConcurrentBag (or new)
3. Raise event → User processes packet
4. User returns → Packet back to ConcurrentBag
5. Thread exits → Buffer back to ArrayPool
Benefits:
✓ Minimal GC pressure after warm-up
✓ Constant memory usage
✓ Fast allocation/deallocation
✓ Thread-safe pools
```
## Performance Considerations
### Hot Path Optimization
```
Critical Path (minimize latency):
1. UdpClient.Receive() ← I/O bound
2. Quick validation ← Few CPU cycles
3. Get packet from pool ← Lock-free
4. MemoryMarshal.Read<T>() ← Zero-copy
5. Raise event ← Delegate invoke
Total latency target: < 1ms
```
### Memory Allocation Profile
```
Cold Start (first few packets):
• Buffer pool allocation
• Packet pool pre-allocation
• Event delegate allocation
Warm Running (steady state):
• Near-zero allocations
• No GC Gen0 collections (ideal)
• Flat memory profile
```
## Error Recovery
```
┌─────────────────────────────────────────────────────────────┐
│ Error Scenarios │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Invalid Packet │
│ → Quick validation fails │
│ → Increment error counter │
│ → Continue receiving │
│ │
│ 2. Socket Exception │
│ → Catch in ReceiveLoop │
│ → Raise ErrorOccurred event │
│ → Continue or exit based on error type │
│ │
│ 3. Parse Exception │
│ → Catch in ProcessPacket │
│ → Increment error counter │
│ → Return packet to pool │
│ → Continue receiving │
│ │
│ 4. Hardware Error (from LiDAR) │
│ → Parsed from Header.ErrorStatus │
│ → Accessible via HasMotorFault, etc. │
│ → User handles in DataReceived event │
│ │
└─────────────────────────────────────────────────────────────┘
```
## Summary
This architecture provides:
- **High throughput**: Dedicated thread, minimal latency
- **Low memory**: Pooling strategy, zero-allocation parsing
- **Thread safety**: Lock-free where possible, Interlocked for counters
- **Robustness**: Error handling at every level
- **Simplicity**: Event-based API, easy to use
- **Performance**: < 1ms latency, > 1000 packets/sec