This commit is contained in:
2026-02-02 10:00:26 +07:00
commit b9b2c6ef79
617 changed files with 133854 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,124 @@
@page "/"
<PageTitle>Home</PageTitle>
<div class="page-header">
<div class="page-title">BCG08-C1QM0371 | EcoLine</div>
<div class="page-subtitle"> WIRE DRAW ENCODERS</div>
</div>
<div class="product-container">
<div class="product-image">
<img src="Images/encoder.jpg" alt="BCG08-C1QM0371" />
</div>
<div class="product-info">
<AccordionItem Title="Tổng quan" IsOpen="@(OpenMainSection == "overview")"
OnToggle="@(() => ToggleSection("overview"))">
<p>
Bộ mã hóa kéo dây BCG08-C1QM0371 thuộc dòng EcoLine của SICK,
là thiết bị đo chiều dài và vị trí được thiết kế cho các ứng dụng công nghiệp.
Sản phẩm sử dụng cơ cấu kéo dây kết hợp encoder tuyệt đối,
cho phép đo chính xác vị trí tuyến tính trong các hệ thống chuyển động.
</p>
<p>
Thiết bị phù hợp lắp đặt trong các dây chuyền sản xuất, hệ thống nâng hạ,
máy công cụ và các ứng dụng tự động hóa yêu cầu độ tin cậy cao.
Với giao tiếp CANopen, BCG08-C1QM0371 dễ dàng tích hợp với PLC
và các hệ điều khiển công nghiệp phổ biến hiện nay.
</p>
</AccordionItem>
<AccordionItem Title="Thông số kỹ thuật" Level="0" IsOpen="@(OpenMainSection == "spec")"
OnToggle="@(() => ToggleSection("spec"))">
<AccordionItem Title="Hiệu năng" Level="1" IsOpen="IsPerformanceOpen"
OnToggle="@(() => IsPerformanceOpen = !IsPerformanceOpen)">
<ul>
<li>Phạm vi đo: 1m -> 3m </li>
<li>Bộ mã hoá: tuyệt đối</li>
<li>Độ phân giải: 0.01mm</li>
<li>Độ lặp lại: ≤ 0.2 mm</li>
<li>Độ tuyến tính: ≤ ± 2 mm</li>
<li>Độ trễ: ≤ 0.4 mm</li>
</ul>
</AccordionItem>
<AccordionItem Title="Giao tiếp" Level="1" IsOpen="IsCommunicationOpen"
OnToggle="@(() => IsCommunicationOpen = !IsCommunicationOpen)">
<ul>
<li>Giao thức truyền thông: Canopen</li>
</ul>
</AccordionItem>
<AccordionItem Title="Điện tử" Level="1" IsOpen="IsElectronicsOpen"
OnToggle="@(() => IsElectronicsOpen = !IsElectronicsOpen)">
<ul>
<li>Kiểu kết nối: đầu nối đực, M12 - 5 chân, chuẩn công nghiệp, dễ đấu PLC /CANopen</li>
<li>Điện áp cấp nguồn: 10V -> 30V</li>
<li>Công suất tiêu thụ: ≤ 1.5 W (khi không tải)</li>
</ul>
</AccordionItem>
<AccordionItem Title="Cơ khí" Level="1" IsOpen="IsMechanicalOpen"
OnToggle="@(() => IsMechanicalOpen = !IsMechanicalOpen)">
<ul>
<li>Khối lượng thiết bị: 0.37 kg</li>
<li>Vật liệu dây đo: Thép không gỉ bện mềm, chuẩn 1.4401/V4A</li>
<li>Đường kính dây thép đo: 0.55 mm</li>
<li>Khối lượng dây: 1.2 g/m</li>
<li>Vỏ & cơ cấu cuộn dây: Nhựa, Noryl</li>
<li>Lực lò xo kéo dây về: 3.3 N … 4.4 N</li>
<li>Chiều dài dây trên mỗi vòng quay: 230 mm</li>
<li>Tuổi thọ cơ cấu kéo dây: ~ 1,000,000 chu kỳ</li>
<li>Chiều dài dây thực tế: 3.2 m</li>
<li>Gia tốc của dây: 10m/s²</li>
<li>Tốc độ làm việc: 6 m/s</li>
<li>Encoder gắn bên trong: AHM36 CANopen, AHM36A-S3CC014x12, 1065999</li>
<li>Hộp kéo dây: MRA-G080-103D3, 5322778</li>
</ul>
</AccordionItem>
<AccordionItem Title="Dữ liệu môi trường" Level="1" IsOpen="IsEnvironmentOpen"
OnToggle="@(() => IsEnvironmentOpen = !IsEnvironmentOpen)">
<ul>
<li>Khả năng chống nhiễu điện từ: EN 61000-6-2, EN 61000-6-3</li>
<li>Cấp bảo vệ: IP50 (hộp kéo dây), IP66/IP67 (bộ mã hóa)</li>
<li>Nhiệt độ hoạt động: 30 °C ... +70 °C</li>
</ul>
</AccordionItem>
</AccordionItem>
<AccordionItem Title="Cấu hình" IsOpen="@(OpenMainSection == "config")"
OnToggle="@(() => ToggleSection("config"))">
<ul>
<li>Bitrate: 20k -> 1M bit/s</li>
<li>Node ID: 1 -> 127</li>
</ul>
</AccordionItem>
<AccordionItem Title="Tài liệu" IsOpen="@(OpenMainSection == "doc")"
OnToggle="@(() => ToggleSection("doc"))">
<ul>
<li><a href="https://www.sick.com/media/pdf/1/71/371/dataSheet_BCG08-C1QM0371_1068867_en.pdf" target="_blank">Datasheet PDF</a></li>
<li><a href="https://www.sick.com/media/docs/3/93/193/operating_instructions_ahs_ahm36_canopen_ahs_ahm36_canopen_inox_absolute_encoder_en_im0055193.pdf" target="_blank">Full Documentation</a></li>
</ul>
</AccordionItem>
</div>
<div class="go-to-monitor" style="margin-top: 2rem; text-align: right;">
<a href="/sick" class="btn btn-primary btn-lg">
Go to Monitor <i class="bi bi-arrow-right"></i>
</a>
</div>
</div>
@code {
string? OpenMainSection;
bool IsPerformanceOpen;
bool IsCommunicationOpen;
bool IsElectronicsOpen;
bool IsMechanicalOpen;
bool IsEnvironmentOpen;
void ToggleSection(string section)
{
OpenMainSection = OpenMainSection == section ? null : section;
}
}

View File

@@ -0,0 +1,5 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>

View File

@@ -0,0 +1,293 @@
@page "/sick"
@implements IDisposable
@inject IOptions<CanBusOptions> CanOptions
@inject ICanBusService CanService
@inject IJSRuntime JS
<div class="encoder-container">
<div class="encoder-card">
<!-- HEADER -->
<div class="encoder-header">
<div class="encoder-title">Sick Encoder Monitor</div>
<div class="encoder-actions">
<button class="btn btn-start @( _starting ? "loading" : "" )"
disabled="@(_starting || _applying)"
@onclick="OnStartClicked">
@(_starting ? "Starting..." : "Start")
</button>
<button class="btn btn-stop @( _stopping ? "loading" : "" )"
disabled="@(_stopping || _applying)"
@onclick="OnStopClicked">
@(_stopping ? "Stopping..." : "Stop")
</button>
</div>
</div>
<!-- TABLE -->
<table class="encoder-table">
<thead>
<tr>
<th>CAN-ID</th>
<th>Length</th>
<th>Data</th>
<th>Position (mm)</th>
<th>Time</th>
</tr>
</thead>
<tbody>
@foreach (var f in _frames.Values.OrderBy(f => f.CobId))
{
<tr>
<td class="encoder-id">@($"0x{f.CobId:X3}")</td>
<td>@f.Length</td>
<td class="encoder-data">@f.DataHex</td>
<td class="encoder-pos">
@(f.PositionMm.HasValue? f.PositionMm.Value.ToString("F2") : "-")
</td>
<td class="encoder-time">@f.Timestamp.ToString("HH:mm:ss.fff")</td>
</tr>
}
</tbody>
</table>
<!-- STATUS -->
<div class="encoder-status @StateClass"> Node State: @_nodeState | Bitrate: @(CurrentBitrate / 1000) kbit/s | Node ID: @CurrentNodeId</div>
<div class="bitrate-group">
<label class="bitrate-label">Bitrate</label>
<select @bind="_selectedBitrate" class="bitrate-select">
<option value="1000000">1000 kbit/s</option>
<option value="800000">800 kbit/s</option>
<option value="500000">500 kbit/s</option>
<option value="250000">250 kbit/s</option>
<option value="125000">125 kbit/s</option>
<option value="100000">100 kbit/s</option>
<option value="50000">50 kbit/s</option>
<option value="20000">20 kbit/s</option>
</select>
<button class="btn btn-apply @( _applying ? "loading" : "" )"
disabled="@(_starting || _applying)"
@onclick="OnApplyClicked">
@(_applying ? "Applying..." : "Apply Bitrate")
</button>
<label class="nodeid-label">Node ID (1127)</label>
<input type="number"
min="1"
max="127"
@bind="_inputNodeId"
placeholder="@CurrentNodeId"
class="nodeid-input" />
<button class="btn btn-apply"
disabled="@(!_inputNodeId.HasValue)"
@onclick="OnApplyNodeIdClicked">
Apply Node ID
</button>
</div>
</div>
</div>
@code {
private PositionPdo? _current;
private CanNodeState _nodeState = CanNodeState.Unknown;
private int CurrentBitrate => CanService.CurrentBitrate;
private readonly Dictionary<uint, CanFrame> _frames = new();
private int _selectedBitrate;
private bool _bitrateApplied = false;
private bool _starting;
private bool _stopping;
private bool _applying;
private int? _inputNodeId;
private byte CurrentNodeId => CanService.CurrentNodeId;
protected override void OnInitialized()
{
CanService.PositionReceived += OnPositionReceived;
CanService.NodeStateChanged += OnNodeStateChanged;
CanService.FrameReceived += OnFrameReceived;
CanService.NodeIdChanged += OnNodeIdChanged;
_selectedBitrate = CanService.CurrentBitrate;
}
private async Task Start()
{
// ❗ LUÔN init lại để đảm bảo COB-ID & state sạch
await CanService.InitAsync();
// ❗ Reset node theo Node ID MỚI
CanService.SendNmtReset(CurrentNodeId);
await Task.Delay(800);
CanService.SendNmtStart(CurrentNodeId);
// ❗ Start read với Node ID MỚI
CanService.Start();
}
private Task Stop()
{
CanService.SendNmtStop(CurrentNodeId);
return Task.CompletedTask;
}
private void OnPositionReceived(object? sender, PositionPdo e)
{
_ = InvokeAsync(() =>
{
_current = e;
StateHasChanged();
});
}
public void Dispose()
{
CanService.PositionReceived -= OnPositionReceived;
CanService.NodeStateChanged -= OnNodeStateChanged;
CanService.FrameReceived -= OnFrameReceived;
CanService.NodeIdChanged -= OnNodeIdChanged;
CanService.Stop();
}
private void OnNodeStateChanged(object? sender, CanNodeState state)
{
_ = InvokeAsync(() =>
{
_nodeState = state;
StateHasChanged();
});
}
private string StateClass => _nodeState switch
{
CanNodeState.Operational => "state-op",
CanNodeState.PreOperational => "state-preop",
CanNodeState.Stopped => "state-stop",
CanNodeState.Timeout => "state-timeout",
_ => "state-unknown"
};
private void OnFrameReceived(object? sender, CanFrame frame)
{
_ = InvokeAsync(() =>
{
// mỗi CAN-ID chỉ giữ frame mới nhất
_frames[frame.CobId] = frame;
StateHasChanged();
});
}
private async Task ApplyBitrate()
{
await CanService.ApplyBitrateAsync(
CurrentNodeId,
_selectedBitrate
);
// UI chỉ báo PreOp / Stopped
_nodeState = CanNodeState.PreOperational;
}
private async Task OnApplyNodeIdClicked()
{
if (!_inputNodeId.HasValue)
return;
byte newNodeId = (byte)_inputNodeId.Value;
byte oldNodeId = CanService.CurrentNodeId;
await CanService.ApplyNodeIdAsync(oldNodeId, newNodeId);
_inputNodeId = null; // reset input
}
private async Task OnStartClicked()
{
if (_starting) return;
_starting = true;
StateHasChanged();
try
{
// ✅ Nếu CHƯA Apply bitrate → dùng bitrate hiện tại
if (!_bitrateApplied)
{
_selectedBitrate = CanService.CurrentBitrate;
}
await Start();
}
finally
{
_starting = false;
StateHasChanged();
}
}
private async Task OnStopClicked()
{
if (_stopping) return;
_stopping = true;
StateHasChanged();
try
{
await Stop();
}
finally
{
_stopping = false;
StateHasChanged();
}
}
private async Task OnApplyClicked()
{
if (_applying) return;
_applying = true;
StateHasChanged();
try
{
await ApplyBitrate();
// ✅ Đánh dấu đã Apply
_bitrateApplied = true;
}
finally
{
_applying = false;
StateHasChanged();
}
}
private void OnNodeIdChanged(object? sender, byte newNodeId)
{
_ = InvokeAsync(() =>
{
// 🔥 XÓA TOÀN BỘ FRAME CŨ
_frames.Clear();
// reset input
_inputNodeId = null;
StateHasChanged();
});
}
}