Compare commits

...

No commits in common. 'main' and 'master' have entirely different histories.
main ... master

  1. 407
      .gitignore
  2. 3
      IOModuleTestBlazor.slnx
  3. 147
      IOModuleTestBlazor/CanWorker.cs
  4. 23
      IOModuleTestBlazor/Components/App.razor
  5. 23
      IOModuleTestBlazor/Components/Layout/MainLayout.razor
  6. 98
      IOModuleTestBlazor/Components/Layout/MainLayout.razor.css
  7. 30
      IOModuleTestBlazor/Components/Layout/NavMenu.razor
  8. 105
      IOModuleTestBlazor/Components/Layout/NavMenu.razor.css
  9. 31
      IOModuleTestBlazor/Components/Layout/ReconnectModal.razor
  10. 157
      IOModuleTestBlazor/Components/Layout/ReconnectModal.razor.css
  11. 63
      IOModuleTestBlazor/Components/Layout/ReconnectModal.razor.js
  12. 640
      IOModuleTestBlazor/Components/Pages/CanMonitor.razor
  13. 36
      IOModuleTestBlazor/Components/Pages/Error.razor
  14. 7
      IOModuleTestBlazor/Components/Pages/Home.razor
  15. 5
      IOModuleTestBlazor/Components/Pages/NotFound.razor
  16. 90
      IOModuleTestBlazor/Components/Pages/Scott.razor
  17. 6
      IOModuleTestBlazor/Components/Routes.razor
  18. 13
      IOModuleTestBlazor/Components/_Imports.razor
  19. 15
      IOModuleTestBlazor/IOModuleTestBlazor.csproj
  20. 36
      IOModuleTestBlazor/Models/CanModels.cs
  21. 31
      IOModuleTestBlazor/Program.cs
  22. 15
      IOModuleTestBlazor/Properties/PublishProfiles/FolderProfile.pubxml
  23. 23
      IOModuleTestBlazor/Properties/launchSettings.json
  24. 172
      IOModuleTestBlazor/Services/CanService.cs
  25. 54
      IOModuleTestBlazor/Services/ICanService.cs
  26. 8
      IOModuleTestBlazor/appsettings.Development.json
  27. 16
      IOModuleTestBlazor/appsettings.json
  28. 60
      IOModuleTestBlazor/wwwroot/app.css
  29. BIN
      IOModuleTestBlazor/wwwroot/favicon.png
  30. 4085
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css
  31. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map
  32. 6
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css
  33. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map
  34. 4084
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css
  35. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
  36. 6
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
  37. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
  38. 597
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css
  39. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map
  40. 6
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css
  41. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
  42. 594
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
  43. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
  44. 6
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
  45. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
  46. 5402
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css
  47. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.css.map
  48. 6
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css
  49. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
  50. 5393
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
  51. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
  52. 6
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
  53. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
  54. 12057
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap.css
  55. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map
  56. 6
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css
  57. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map
  58. 12030
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css
  59. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.css.map
  60. 6
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css
  61. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
  62. 6314
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js
  63. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map
  64. 7
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js
  65. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map
  66. 4447
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js
  67. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.js.map
  68. 7
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js
  69. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.esm.min.js.map
  70. 4494
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.js
  71. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map
  72. 7
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js
  73. 1
      IOModuleTestBlazor/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map
  74. 3
      README.md

407
.gitignore vendored

@ -1,400 +1,17 @@
# ---> VisualStudio
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
# VS / Rider
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
.idea/
*.user
*.suo
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# Build output
bin/
obj/
# NuGet Packages
# NuGet
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
packages/
# OS
Thumbs.db
.DS_Store

@ -0,0 +1,3 @@
<Solution>
<Project Path="IOModuleTestBlazor/IOModuleTestBlazor.csproj" />
</Solution>

@ -0,0 +1,147 @@
using Peak.Can.Basic;
using IOModuleTestBlazor.Models;
using IOModuleTestBlazor.Services;
namespace IOModuleTestBlazor;
/// <summary>
/// Background service that initialises the PCAN adapter and streams CAN messages
/// to the rest of the app via <see cref="ICanService.MessageReceived"/>.
/// </summary>
public class CanWorker(
ILogger<CanWorker> logger,
IConfiguration configuration,
ICanService canService) : BackgroundService
{
public override Task StartAsync(CancellationToken cancellationToken)
{
// Seed filters and bitmasks from appsettings regardless of connection state
foreach (var f in configuration.GetSection("CanOptions:Filters").Get<CanFilter[]>() ?? [])
canService.AddFilter(f);
foreach (var b in configuration.GetSection("CanOptions:Bitmasks").Get<CanBitmask[]>() ?? [])
canService.AddBitmask(b);
// Auto-connect if config is present; failures are non-fatal so the app
// still starts and the user can configure the connection from the UI.
try
{
var channel = ResolveChannel();
var bitrate = ResolveBitrate();
canService.Initialize(channel, bitrate);
logger.LogInformation("CAN channel {Channel} ready at {Bitrate}", channel, bitrate);
}
catch (Exception ex)
{
logger.LogWarning(ex, "CAN auto-connect failed — configure via the UI.");
}
return base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Pick up any reinit request from the UI
if (canService.TryConsumePendingReinit(out var newChannel, out var newBitrate))
{
logger.LogInformation("Reinitializing CAN: {Channel} at {Bitrate}", newChannel, newBitrate);
if (canService.IsConnected) canService.Uninitialize();
try
{
canService.Initialize(newChannel, newBitrate);
logger.LogInformation("CAN reinitialized: {Channel} at {Bitrate}", newChannel, newBitrate);
}
catch (Exception ex)
{
logger.LogError(ex, "CAN reinitialization failed.");
}
continue;
}
if (!canService.IsConnected)
{
await Task.Delay(100, stoppingToken);
continue;
}
var result = canService.Read(out PcanMessage msg, out ulong timestamp);
if (result == PcanStatus.OK)
{
if (!canService.PassesFilter(msg.ID))
continue;
var data = new byte[msg.DLC];
for (int i = 0; i < msg.DLC; i++)
data[i] = msg.Data[i];
var signals = canService.ExtractSignals(msg.ID, data);
canService.PublishMessage(new CanMessageDto(
Id: msg.ID,
Data: data,
Dlc: msg.DLC,
TimestampUs: timestamp,
IsExtended: msg.MsgType == MessageType.Extended,
Signals: signals.Count > 0 ? signals : null
));
logger.LogDebug("[RX] ID=0x{Id:X3} DLC={Dlc} Data={Data}",
msg.ID, msg.DLC, Convert.ToHexString(data));
}
else if (result == PcanStatus.ReceiveQueueEmpty)
{
await Task.Delay(1, stoppingToken);
}
else
{
logger.LogError("CAN read error: {Error}", GetErrorText(result));
await Task.Delay(100, stoppingToken);
}
}
}
public override Task StopAsync(CancellationToken cancellationToken)
{
canService.Uninitialize();
logger.LogInformation("CAN channel uninitialized.");
return base.StopAsync(cancellationToken);
}
// ── Channel / bitrate resolution ──────────────────────────────────────────
private PcanChannel ResolveChannel()
{
var name = configuration["CanOptions:Channel"];
if (!string.IsNullOrWhiteSpace(name) &&
Enum.TryParse(name, ignoreCase: true, out PcanChannel configured))
return configured;
logger.LogInformation("No channel configured — scanning for available PCAN USB channels...");
var available = canService.GetAvailableChannels();
if (available.Count == 0)
throw new InvalidOperationException("No PCAN USB channels detected.");
logger.LogInformation("Auto-selected channel: {Channel}", available[0]);
return available[0];
}
private Bitrate ResolveBitrate()
{
var name = configuration["CanOptions:Bitrate"];
if (!string.IsNullOrWhiteSpace(name) &&
Enum.TryParse(name, ignoreCase: true, out Bitrate configured))
return configured;
logger.LogInformation("No bitrate configured — defaulting to 500 kbps.");
return Bitrate.Pcan500;
}
private static string GetErrorText(PcanStatus status)
{
try { Api.GetErrorText(status, out var text); return text; }
catch (PcanBasicException) { return status.ToString(); }
}
}

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["IOModuleTestBlazor.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>
</html>

@ -0,0 +1,23 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>

@ -0,0 +1,98 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

@ -0,0 +1,30 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">IOModuleTestBlazor</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="can-monitor">
<span class="bi bi-activity-nav-menu" aria-hidden="true"></span> CAN Monitor
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="scott">
<span aria-hidden="true">💩</span> Scott click here
</NavLink>
</div>
</nav>
</div>

@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

@ -0,0 +1,31 @@
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
<dialog id="components-reconnect-modal" data-nosnippet>
<div class="components-reconnect-container">
<div class="components-rejoining-animation" aria-hidden="true">
<div></div>
<div></div>
</div>
<p class="components-reconnect-first-attempt-visible">
Rejoining the server...
</p>
<p class="components-reconnect-repeated-attempt-visible">
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
</p>
<p class="components-reconnect-failed-visible">
Failed to rejoin.<br />Please retry or reload the page.
</p>
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
Retry
</button>
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<p class="components-resume-failed-visible">
Failed to resume the session.<br />Please retry or reload the page.
</p>
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
Resume
</button>
</div>
</dialog>

@ -0,0 +1,157 @@
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
display: none;
}
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: block;
}
#components-reconnect-modal {
background-color: white;
width: 20rem;
margin: 20vh auto;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
&[open]
{
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
animation-fill-mode: both;
}
}
#components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1;
}
@keyframes components-reconnect-modal-slideUp {
0% {
transform: translateY(30px) scale(0.95);
}
100% {
transform: translateY(0);
}
}
@keyframes components-reconnect-modal-fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes components-reconnect-modal-fadeOutOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.components-reconnect-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#components-reconnect-modal p {
margin: 0;
text-align: center;
}
#components-reconnect-modal button {
border: 0;
background-color: #6b9ed2;
color: white;
padding: 4px 24px;
border-radius: 4px;
}
#components-reconnect-modal button:hover {
background-color: #3b6ea2;
}
#components-reconnect-modal button:active {
background-color: #6b9ed2;
}
.components-rejoining-animation {
position: relative;
width: 80px;
height: 80px;
}
.components-rejoining-animation div {
position: absolute;
border: 3px solid #0087ff;
opacity: 1;
border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes components-rejoining-animation {
0% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 80px;
height: 80px;
opacity: 0;
}
}

@ -0,0 +1,63 @@
// Set up event handlers
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);
const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resume);
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
// Reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await Blazor.reconnect();
if (!successful) {
// We have been able to reach the server, but the circuit is no longer available.
// We'll reload the page so the user can continue using the app as quickly as possible.
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
} catch (err) {
// We got an exception, server is currently unavailable
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
} catch {
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}

@ -0,0 +1,640 @@
@page "/can-monitor"
@rendermode InteractiveServer
@implements IDisposable
@using Peak.Can.Basic
@inject ICanService CanService
<PageTitle>CAN Monitor</PageTitle>
<h1>CAN Monitor</h1>
@* ── Connection ──────────────────────────────────────────────────────────────── *@
<div class="card mb-3">
<div class="card-body py-2">
@if (CanService.IsConnected && !showConnectionForm)
{
<div class="d-flex align-items-center gap-3">
<span class="badge bg-success">● Connected</span>
<span class="font-monospace fw-semibold">@CanService.ChannelName</span>
<span class="text-muted">@BitrateLabel(CanService.CurrentBitrate)</span>
<button class="btn btn-sm btn-outline-secondary ms-auto"
@onclick="() => { showConnectionForm = true; ScanChannels(); }">
Change
</button>
</div>
}
else
{
<div class="row g-2 align-items-end">
<div class="col-auto">
<label class="form-label small mb-0">Channel</label>
<select class="form-select form-select-sm font-monospace" @bind="selectedChannel"
style="min-width:100px">
@if (availableChannels.Count == 0)
{
<option value="">— none found —</option>
}
@foreach (var ch in availableChannels)
{
<option value="@ch.ToString()">@ch.ToString()</option>
}
</select>
</div>
<div class="col-auto">
<label class="form-label small mb-0">Bitrate</label>
<select class="form-select form-select-sm" @bind="selectedBitrate"
style="min-width:120px">
@foreach (var (val, label) in BitrateOptions)
{
<option value="@val.ToString()">@label</option>
}
</select>
</div>
<div class="col-auto d-flex gap-2">
<button class="btn btn-sm btn-outline-secondary" @onclick="ScanChannels">Scan</button>
<button class="btn btn-sm btn-primary" @onclick="Connect"
disabled="@(availableChannels.Count == 0 || isConnecting)">
@(isConnecting ? "Connecting…" : CanService.IsConnected ? "Reconnect" : "Connect")
</button>
@if (CanService.IsConnected)
{
<button class="btn btn-sm btn-outline-secondary"
@onclick="() => showConnectionForm = false">Cancel</button>
}
</div>
</div>
@if (availableChannels.Count == 0)
{
<div class="text-warning small mt-1">
No PCAN USB channels detected. Plug in your adapter and click Scan.
</div>
}
@if (connectError != null)
{
<div class="text-danger small mt-1">@connectError</div>
}
}
</div>
</div>
@* ── Live messages ──────────────────────────────────────────────────────────── *@
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center py-0 ps-0">
<ul class="nav nav-tabs border-0 mb-0">
<li class="nav-item">
<button class="nav-link rounded-0 @(msgTab == "stream" ? "active" : "")"
@onclick='() => msgTab = "stream"'>
Stream
<span class="badge bg-secondary ms-1">@messages.Count</span>
</button>
</li>
<li class="nav-item">
<button class="nav-link rounded-0 @(msgTab == "latest" ? "active" : "")"
@onclick='() => msgTab = "latest"'>
Latest
<span class="badge bg-secondary ms-1">@latestMessages.Count</span>
</button>
</li>
</ul>
<button class="btn btn-sm btn-outline-secondary me-2" @onclick="ClearMessages">Clear</button>
</div>
@if (msgTab == "stream")
{
<div style="max-height:320px; overflow-y:auto;">
<table class="table table-sm table-hover font-monospace mb-0">
<thead class="table-light sticky-top">
<tr>
<th>Timestamp (µs)</th>
<th>ID</th>
<th>DLC</th>
<th>Data</th>
<th>Signals</th>
</tr>
</thead>
<tbody>
@lock (messages)
{
foreach (var m in messages)
{
<tr>
<td>@m.TimestampUs</td>
<td>@($"0x{m.Id:X3}")</td>
<td>@m.Dlc</td>
<td>@(m.Data.Length > 0 ? BitConverter.ToString(m.Data).Replace("-", " ") : "–")</td>
<td>@SignalsText(m)</td>
</tr>
}
}
</tbody>
</table>
</div>
}
else
{
<div style="max-height:320px; overflow-y:auto;">
<table class="table table-sm table-hover font-monospace mb-0">
<thead class="table-light sticky-top">
<tr>
<th>ID</th>
<th>DLC</th>
<th>Data</th>
<th>Signals</th>
<th>Timestamp (µs)</th>
<th class="text-end">Count</th>
</tr>
</thead>
<tbody>
@foreach (var m in GetLatestRows())
{
<tr>
<td>@($"0x{m.Id:X3}")</td>
<td>@m.Dlc</td>
<td>@(m.Data.Length > 0 ? BitConverter.ToString(m.Data).Replace("-", " ") : "–")</td>
<td>@SignalsText(m)</td>
<td>@m.TimestampUs</td>
<td class="text-end">@updateCounts.GetValueOrDefault(m.Id)</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
@* ── Filters + Send (two columns) ──────────────────────────────────────────── *@
<div class="row g-3 mb-3">
@* Filters *@
<div class="col-lg-7">
<div class="card h-100">
<div class="card-header">
Filters
<span class="text-muted small ms-2">
@(filters.Count == 0 ? "(no filters — all messages pass)" : $"({filters.Count} active)")
</span>
</div>
<div class="card-body">
@if (filters.Count > 0)
{
<table class="table table-sm mb-2">
<thead><tr><th>ID</th><th>Mask</th><th>Description</th><th></th></tr></thead>
<tbody>
@foreach (var f in filters)
{
<tr>
<td class="font-monospace">@($"0x{f.MessageId:X3}")</td>
<td class="font-monospace">@($"0x{f.Mask:X3}")</td>
<td>@f.Description</td>
<td>
<button class="btn btn-sm btn-outline-danger py-0 px-2"
@onclick="() => RemoveFilter(f.Id)">×</button>
</td>
</tr>
}
</tbody>
</table>
}
<div class="row g-1 align-items-end">
<div class="col-3">
<label class="form-label small mb-0">ID (hex)</label>
<input class="form-control form-control-sm font-monospace"
placeholder="e.g. 100" @bind="newFilterId" />
</div>
<div class="col-3">
<label class="form-label small mb-0">Mask (hex)</label>
<input class="form-control form-control-sm font-monospace"
placeholder="e.g. 7FF" @bind="newFilterMask" />
</div>
<div class="col-4">
<label class="form-label small mb-0">Description</label>
<input class="form-control form-control-sm"
placeholder="optional" @bind="newFilterDesc" />
</div>
<div class="col-2">
<button class="btn btn-sm btn-primary w-100" @onclick="AddFilter">Add</button>
</div>
</div>
@if (filterError != null)
{
<div class="text-danger small mt-1">@filterError</div>
}
@if (filters.Count > 0)
{
<button class="btn btn-sm btn-outline-danger mt-2" @onclick="ClearFilters">Clear all</button>
}
</div>
</div>
</div>
@* Send *@
<div class="col-lg-5">
<div class="card h-100">
<div class="card-header">Send Message</div>
<div class="card-body">
<div class="mb-2">
<label class="form-label small mb-0">CAN ID (hex)</label>
<input class="form-control form-control-sm font-monospace"
placeholder="e.g. 7FF" @bind="sendIdHex" />
</div>
<div class="mb-2">
<label class="form-label small mb-0">Data bytes (hex, space-separated)</label>
<input class="form-control form-control-sm font-monospace"
placeholder="e.g. DE AD BE EF" @bind="sendDataHex" />
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="extFrame" @bind="sendExtended" />
<label class="form-check-label small" for="extFrame">Extended frame (29-bit ID)</label>
</div>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-sm btn-success" @onclick="SendMessage">Send</button>
@if (sendFeedback != null)
{
<span class="small @(sendOk ? "text-success" : "text-danger")">@sendFeedback</span>
}
</div>
</div>
</div>
</div>
</div>
@* ── Bitmasks ───────────────────────────────────────────────────────────────── *@
<div class="card">
<div class="card-header">Signal Bitmasks</div>
<div class="card-body">
@if (bitmasks.Count == 0)
{
<p class="text-muted small mb-2">
No bitmasks configured. Add one to extract named signal values from a message's data bytes.<br />
<em>Formula: physicalValue = ((rawUint64 &amp; DataMask) &gt;&gt; RightShift) × Scale + Offset</em>
</p>
}
else
{
<table class="table table-sm mb-2">
<thead>
<tr>
<th>Msg ID</th><th>Signal</th><th>DataMask (64-bit)</th>
<th>Shift</th><th>Scale</th><th>Offset</th><th>Unit</th><th></th>
</tr>
</thead>
<tbody>
@foreach (var b in bitmasks)
{
<tr>
<td class="font-monospace">@($"0x{b.MessageId:X3}")</td>
<td>@b.SignalName</td>
<td class="font-monospace small">@($"0x{b.DataMask:X16}")</td>
<td>@b.RightShift</td>
<td>@b.Scale</td>
<td>@b.Offset</td>
<td>@b.Unit</td>
<td>
<button class="btn btn-sm btn-outline-danger py-0 px-2"
@onclick="() => RemoveBitmask(b.Id)">×</button>
</td>
</tr>
}
</tbody>
</table>
}
<div class="row g-1 align-items-end">
<div class="col-sm-2">
<label class="form-label small mb-0">Msg ID (hex)</label>
<input class="form-control form-control-sm font-monospace" placeholder="e.g. 100" @bind="newBmMsgId" />
</div>
<div class="col-sm-2">
<label class="form-label small mb-0">Signal name</label>
<input class="form-control form-control-sm" placeholder="e.g. EngineRPM" @bind="newBmName" />
</div>
<div class="col-sm-3">
<label class="form-label small mb-0">DataMask (64-bit hex)</label>
<input class="form-control form-control-sm font-monospace" placeholder="e.g. 00FF000000000000" @bind="newBmMask" />
</div>
<div class="col-sm-1">
<label class="form-label small mb-0">Shift</label>
<input class="form-control form-control-sm" @bind="newBmShift" />
</div>
<div class="col-sm-1">
<label class="form-label small mb-0">Scale</label>
<input class="form-control form-control-sm" @bind="newBmScale" />
</div>
<div class="col-sm-1">
<label class="form-label small mb-0">Offset</label>
<input class="form-control form-control-sm" @bind="newBmOffset" />
</div>
<div class="col-sm-1">
<label class="form-label small mb-0">Unit</label>
<input class="form-control form-control-sm" placeholder="rpm" @bind="newBmUnit" />
</div>
<div class="col-sm-1">
<button class="btn btn-sm btn-primary w-100" @onclick="AddBitmask">Add</button>
</div>
</div>
@if (bitmaskError != null)
{
<div class="text-danger small mt-1">@bitmaskError</div>
}
@if (bitmasks.Count > 0)
{
<button class="btn btn-sm btn-outline-danger mt-2" @onclick="ClearBitmasks">Clear all</button>
}
</div>
</div>
@code {
// ── Connection form ───────────────────────────────────────────────────────
private List<PcanChannel> availableChannels = [];
private string selectedChannel = "";
private string selectedBitrate = Bitrate.Pcan500.ToString();
private bool showConnectionForm;
private bool isConnecting;
private string? connectError;
private static readonly (Bitrate Value, string Label)[] BitrateOptions =
[
(Bitrate.Pcan1000, "1000 kbps"),
(Bitrate.Pcan800, "800 kbps"),
(Bitrate.Pcan500, "500 kbps"),
(Bitrate.Pcan250, "250 kbps"),
(Bitrate.Pcan125, "125 kbps"),
(Bitrate.Pcan100, "100 kbps"),
(Bitrate.Pcan50, "50 kbps"),
(Bitrate.Pcan20, "20 kbps"),
(Bitrate.Pcan10, "10 kbps"),
(Bitrate.Pcan5, "5 kbps"),
];
// ── Data ──────────────────────────────────────────────────────────────────
private readonly List<CanMessageDto> messages = [];
private readonly Dictionary<uint, CanMessageDto> latestMessages = new();
private readonly Dictionary<uint, int> updateCounts = new();
private string msgTab = "stream";
private const int MaxMessages = 200;
private List<CanFilter> filters = [];
private List<CanBitmask> bitmasks = [];
// ── Filter form ───────────────────────────────────────────────────────────
private string newFilterId = "";
private string newFilterMask = "7FF";
private string newFilterDesc = "";
private string? filterError;
// ── Bitmask form ──────────────────────────────────────────────────────────
private string newBmMsgId = "";
private string newBmName = "";
private string newBmMask = "";
private int newBmShift = 0;
private double newBmScale = 1.0;
private double newBmOffset = 0.0;
private string newBmUnit = "";
private string? bitmaskError;
// ── Send form ─────────────────────────────────────────────────────────────
private string sendIdHex = "";
private string sendDataHex = "";
private bool sendExtended;
private string? sendFeedback;
private bool sendOk;
// ── Lifecycle ─────────────────────────────────────────────────────────────
protected override void OnInitialized()
{
filters = CanService.Filters.ToList();
bitmasks = CanService.Bitmasks.ToList();
CanService.MessageReceived += OnMessageReceived;
ScanChannels();
if (CanService.IsConnected)
{
selectedChannel = CanService.ChannelName;
selectedBitrate = CanService.CurrentBitrate.ToString();
}
else
{
showConnectionForm = true;
}
}
private void OnMessageReceived(CanMessageDto msg)
{
lock (messages)
{
messages.Insert(0, msg);
if (messages.Count > MaxMessages)
messages.RemoveAt(messages.Count - 1);
}
lock (latestMessages)
{
if (!latestMessages.TryGetValue(msg.Id, out var existing) ||
!msg.Data.SequenceEqual(existing.Data))
{
latestMessages[msg.Id] = msg;
updateCounts[msg.Id] = updateCounts.GetValueOrDefault(msg.Id) + 1;
}
}
InvokeAsync(StateHasChanged);
}
public void Dispose() => CanService.MessageReceived -= OnMessageReceived;
// ── Connection ────────────────────────────────────────────────────────────
private void ScanChannels()
{
availableChannels = CanService.GetAvailableChannels().ToList();
if (availableChannels.Count > 0 &&
!availableChannels.Any(c => c.ToString() == selectedChannel))
selectedChannel = availableChannels[0].ToString();
}
private async Task Connect()
{
connectError = null;
if (!Enum.TryParse<PcanChannel>(selectedChannel, out var channel))
{ connectError = "Please select a channel."; return; }
if (!Enum.TryParse<Bitrate>(selectedBitrate, out var bitrate))
{ connectError = "Please select a bitrate."; return; }
isConnecting = true;
CanService.RequestReinitialize(channel, bitrate);
// Poll until the worker completes the reinit (typically < 50 ms)
for (int i = 0; i < 20; i++)
{
await Task.Delay(100);
if (CanService.IsConnected) break;
}
isConnecting = false;
if (CanService.IsConnected)
showConnectionForm = false;
else
connectError = "Connection failed. Check the adapter is plugged in and try again.";
StateHasChanged();
}
private static string BitrateLabel(Bitrate b) => b switch
{
Bitrate.Pcan1000 => "1000 kbps",
Bitrate.Pcan800 => "800 kbps",
Bitrate.Pcan500 => "500 kbps",
Bitrate.Pcan250 => "250 kbps",
Bitrate.Pcan125 => "125 kbps",
Bitrate.Pcan100 => "100 kbps",
Bitrate.Pcan50 => "50 kbps",
Bitrate.Pcan20 => "20 kbps",
Bitrate.Pcan10 => "10 kbps",
Bitrate.Pcan5 => "5 kbps",
_ => b.ToString()
};
// ── Messages ──────────────────────────────────────────────────────────────
private void ClearMessages()
{
lock (messages) messages.Clear();
lock (latestMessages) { latestMessages.Clear(); updateCounts.Clear(); }
}
// ── Filters ───────────────────────────────────────────────────────────────
private void AddFilter()
{
filterError = null;
if (!TryParseHex32(newFilterId, out uint msgId))
{ filterError = "Invalid ID — enter a hex value e.g. 100"; return; }
if (!TryParseHex32(newFilterMask, out uint mask))
{ filterError = "Invalid mask — enter a hex value e.g. 7FF"; return; }
var filter = new CanFilter { MessageId = msgId, Mask = mask, Description = newFilterDesc };
CanService.AddFilter(filter);
filters = CanService.Filters.ToList();
newFilterId = ""; newFilterMask = "7FF"; newFilterDesc = "";
}
private void RemoveFilter(Guid id)
{
CanService.RemoveFilter(id);
filters = CanService.Filters.ToList();
}
private void ClearFilters()
{
CanService.ClearFilters();
filters = [];
}
// ── Bitmasks ──────────────────────────────────────────────────────────────
private void AddBitmask()
{
bitmaskError = null;
if (!TryParseHex32(newBmMsgId, out uint msgId))
{ bitmaskError = "Invalid Msg ID"; return; }
if (string.IsNullOrWhiteSpace(newBmName))
{ bitmaskError = "Signal name is required"; return; }
if (!TryParseHex64(newBmMask, out ulong mask))
{ bitmaskError = "Invalid 64-bit mask — enter hex e.g. 00FF000000000000"; return; }
var bitmask = new CanBitmask
{
MessageId = msgId,
SignalName = newBmName.Trim(),
DataMask = mask,
RightShift = newBmShift,
Scale = newBmScale,
Offset = newBmOffset,
Unit = string.IsNullOrWhiteSpace(newBmUnit) ? null : newBmUnit
};
CanService.AddBitmask(bitmask);
bitmasks = CanService.Bitmasks.ToList();
newBmMsgId = ""; newBmName = ""; newBmMask = "";
newBmShift = 0; newBmScale = 1.0; newBmOffset = 0.0; newBmUnit = "";
}
private void RemoveBitmask(Guid id)
{
CanService.RemoveBitmask(id);
bitmasks = CanService.Bitmasks.ToList();
}
private void ClearBitmasks()
{
CanService.ClearBitmasks();
bitmasks = [];
}
// ── Send ──────────────────────────────────────────────────────────────────
private void SendMessage()
{
sendFeedback = null;
if (!TryParseHex32(sendIdHex, out uint id))
{ sendFeedback = "Invalid ID"; sendOk = false; return; }
byte[] data;
try
{
var hex = (sendDataHex ?? "").Replace(" ", "");
data = hex.Length > 0 ? Convert.FromHexString(hex) : [];
}
catch { sendFeedback = "Invalid hex data"; sendOk = false; return; }
if (data.Length > 8)
{ sendFeedback = "Max 8 bytes"; sendOk = false; return; }
var msg = new PcanMessage
{
ID = id,
MsgType = sendExtended ? MessageType.Extended : MessageType.Standard,
DLC = (byte)data.Length,
Data = new byte[8]
};
data.CopyTo(msg.Data, 0);
var result = CanService.Write(msg);
sendOk = result == PcanStatus.OK;
sendFeedback = sendOk ? "Sent!" : $"Error: {result}";
}
// ── Helpers ───────────────────────────────────────────────────────────────
private List<CanMessageDto> GetLatestRows()
{
lock (latestMessages)
return latestMessages.Values.OrderBy(m => m.Id).ToList();
}
private static string SignalsText(CanMessageDto m)
{
if (m.Signals is null || m.Signals.Count == 0) return "–";
return string.Join(", ", m.Signals.Select(kv => $"{kv.Key}={kv.Value:G4}"));
}
private static bool TryParseHex32(string? s, out uint value)
{
value = 0;
if (string.IsNullOrWhiteSpace(s)) return false;
var hex = s.Trim();
if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) hex = hex[2..];
return uint.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out value);
}
private static bool TryParseHex64(string? s, out ulong value)
{
value = 0;
if (string.IsNullOrWhiteSpace(s)) return false;
var hex = s.Trim();
if (hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) hex = hex[2..];
return ulong.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out value);
}
}

@ -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;
}

@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

@ -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>

@ -0,0 +1,90 @@
@page "/scott"
<PageTitle>Scott</PageTitle>
<div class="scott-wrapper">
<div class="poo-orbit">
<div class="poo-spin">
<div class="poo-wobble">
<div class="poo-pulse">
<span class="poo-text">💩POO💩</span>
</div>
</div>
</div>
</div>
</div>
<style>
.scott-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 70vh;
overflow: hidden;
background: radial-gradient(ellipse at center, #1a0033 0%, #000 100%);
border-radius: 12px;
margin-top: 2rem;
}
.poo-orbit {
animation: orbit 3s linear infinite;
}
.poo-spin {
animation: spin 0.4s linear infinite;
}
.poo-wobble {
animation: wobble 0.7s ease-in-out infinite alternate;
}
.poo-pulse {
animation: pulse 0.5s ease-in-out infinite alternate;
}
.poo-text {
font-size: clamp(4rem, 12vw, 9rem);
font-weight: 900;
font-family: Impact, sans-serif;
letter-spacing: 0.05em;
background: linear-gradient(
90deg,
#ff0000, #ff7700, #ffff00,
#00ff00, #0000ff, #8b00ff,
#ff0000
);
background-size: 300% auto;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: rainbow 0.8s linear infinite;
filter: drop-shadow(0 0 18px #ff00ff) drop-shadow(0 0 40px #00ffff);
display: inline-block;
text-shadow: none;
}
@@keyframes orbit {
0% { transform: rotate(0deg) translateX(40px) rotate(0deg); }
100% { transform: rotate(360deg) translateX(40px) rotate(-360deg); }
}
@@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@@keyframes wobble {
from { transform: skewX(-15deg) scaleY(0.9); }
to { transform: skewX(15deg) scaleY(1.15); }
}
@@keyframes pulse {
from { transform: scale(0.85); }
to { transform: scale(1.2); }
}
@@keyframes rainbow {
0% { background-position: 0% center; }
100% { background-position: 300% center; }
}
</style>

@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

@ -0,0 +1,13 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using IOModuleTestBlazor
@using IOModuleTestBlazor.Components
@using IOModuleTestBlazor.Components.Layout
@using IOModuleTestBlazor.Models
@using IOModuleTestBlazor.Services

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.4" />
<PackageReference Include="Peak.PCANBasic.NET" Version="4.10.1.968" />
</ItemGroup>
</Project>

@ -0,0 +1,36 @@
namespace IOModuleTestBlazor.Models;
public record CanMessageDto(
uint Id,
byte[] Data,
int Dlc,
ulong TimestampUs,
bool IsExtended,
IReadOnlyDictionary<string, double>? Signals
);
public class CanFilter
{
public Guid Id { get; set; }
public uint MessageId { get; set; }
public uint Mask { get; set; } = 0x7FF;
public string? Description { get; set; }
}
public class CanBitmask
{
public Guid Id { get; set; }
public uint MessageId { get; set; }
public string SignalName { get; set; } = "";
public ulong DataMask { get; set; }
public int RightShift { get; set; }
public double Scale { get; set; } = 1.0;
public double Offset { get; set; }
public string? Unit { get; set; }
}
public class CanStatus
{
public bool IsConnected { get; set; }
public string ChannelName { get; set; } = "";
}

@ -0,0 +1,31 @@
using IOModuleTestBlazor;
using IOModuleTestBlazor.Components;
using IOModuleTestBlazor.Services;
var builder = WebApplication.CreateBuilder(args);
// ── CAN bus ───────────────────────────────────────────────────────────────────
builder.Services.AddSingleton<ICanService, CanService>();
builder.Services.AddHostedService<CanWorker>();
// ── Blazor ────────────────────────────────────────────────────────────────────
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<DeleteExistingFiles>false</DeleteExistingFiles>
<ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>bin\Release\net10.0\publish\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId>
</PropertyGroup>
</Project>

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5042",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7074;http://localhost:5042",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

@ -0,0 +1,172 @@
using Peak.Can.Basic;
using IOModuleTestBlazor.Models;
namespace IOModuleTestBlazor.Services;
public class CanService : ICanService
{
private PcanChannel _channel;
private readonly List<CanFilter> _filters = [];
private readonly List<CanBitmask> _bitmasks = [];
private readonly object _lock = new();
public bool IsConnected { get; private set; }
public string ChannelName => IsConnected ? _channel.ToString() : "";
public Bitrate CurrentBitrate { get; private set; }
private (PcanChannel Channel, Bitrate Bitrate)? _pendingReinit;
public event Action<CanMessageDto>? MessageReceived;
public void PublishMessage(CanMessageDto dto) => MessageReceived?.Invoke(dto);
// ── Filters ───────────────────────────────────────────────────────────────
public IReadOnlyList<CanFilter> Filters
{
get { lock (_lock) return _filters.ToList(); }
}
public CanFilter AddFilter(CanFilter filter)
{
lock (_lock) _filters.Add(filter);
return filter;
}
public bool RemoveFilter(Guid id)
{
lock (_lock) return _filters.RemoveAll(f => f.Id == id) > 0;
}
public void ClearFilters()
{
lock (_lock) _filters.Clear();
}
// ── Bitmasks ──────────────────────────────────────────────────────────────
public IReadOnlyList<CanBitmask> Bitmasks
{
get { lock (_lock) return _bitmasks.ToList(); }
}
public CanBitmask AddBitmask(CanBitmask bitmask)
{
lock (_lock) _bitmasks.Add(bitmask);
return bitmask;
}
public bool RemoveBitmask(Guid id)
{
lock (_lock) return _bitmasks.RemoveAll(b => b.Id == id) > 0;
}
public void ClearBitmasks()
{
lock (_lock) _bitmasks.Clear();
}
// ── Helpers ───────────────────────────────────────────────────────────────
public bool PassesFilter(uint messageId)
{
lock (_lock)
{
if (_filters.Count == 0) return true;
return _filters.Any(f => (messageId & f.Mask) == (f.MessageId & f.Mask));
}
}
public IReadOnlyDictionary<string, double> ExtractSignals(uint messageId, byte[] data)
{
List<CanBitmask> applicable;
lock (_lock)
applicable = _bitmasks.Where(b => b.MessageId == messageId).ToList();
if (applicable.Count == 0)
return new Dictionary<string, double>();
Span<byte> padded = stackalloc byte[8];
var len = Math.Min(data.Length, 8);
for (int i = 0; i < len; i++)
padded[i] = data[i];
ulong raw = 0;
for (int i = 0; i < 8; i++)
raw = (raw << 8) | padded[i];
var signals = new Dictionary<string, double>(applicable.Count);
foreach (var bm in applicable)
{
ulong masked = (raw & bm.DataMask) >> bm.RightShift;
signals[bm.SignalName] = masked * bm.Scale + bm.Offset;
}
return signals;
}
// ── PCAN passthrough ──────────────────────────────────────────────────────
public IReadOnlyList<PcanChannel> GetAvailableChannels()
{
var available = new List<PcanChannel>();
foreach (var ch in Enum.GetValues<PcanChannel>()
.Where(c => c.ToString().StartsWith("Usb", StringComparison.OrdinalIgnoreCase))
.OrderBy(c => c.ToString()))
{
var result = Api.GetValue(ch, PcanParameter.ChannelCondition, out uint condition);
if (result == PcanStatus.OK && condition != 0)
available.Add(ch);
}
return available;
}
public void RequestReinitialize(PcanChannel channel, Bitrate bitrate)
{
lock (_lock) _pendingReinit = (channel, bitrate);
}
public bool TryConsumePendingReinit(out PcanChannel channel, out Bitrate bitrate)
{
lock (_lock)
{
if (_pendingReinit.HasValue)
{
channel = _pendingReinit.Value.Channel;
bitrate = _pendingReinit.Value.Bitrate;
_pendingReinit = null;
return true;
}
channel = default;
bitrate = default;
return false;
}
}
public void Initialize(PcanChannel channel, Bitrate bitrate)
{
_channel = channel;
var result = Api.Initialize(channel, bitrate);
if (result != PcanStatus.OK)
throw new InvalidOperationException($"CAN init failed: {GetErrorText(result)}");
IsConnected = true;
CurrentBitrate = bitrate;
}
public void Uninitialize()
{
Api.Uninitialize(_channel);
IsConnected = false;
}
public PcanStatus Read(out PcanMessage msg, out ulong timestamp)
=> Api.Read(_channel, out msg, out timestamp);
public PcanStatus Write(PcanMessage msg)
=> Api.Write(_channel, msg);
private static string GetErrorText(PcanStatus status)
{
try { Api.GetErrorText(status, out var text); return text; }
catch (PcanBasicException) { return status.ToString(); }
}
}

@ -0,0 +1,54 @@
using Peak.Can.Basic;
using IOModuleTestBlazor.Models;
namespace IOModuleTestBlazor.Services;
public interface ICanService
{
bool IsConnected { get; }
string ChannelName { get; }
Bitrate CurrentBitrate { get; }
/// <summary>
/// Raised on the worker thread each time a CAN message passes all filters.
/// Subscribers must marshal UI updates with InvokeAsync(StateHasChanged).
/// </summary>
event Action<CanMessageDto> MessageReceived;
// ── Filters ───────────────────────────────────────────────────────────────
IReadOnlyList<CanFilter> Filters { get; }
CanFilter AddFilter(CanFilter filter);
bool RemoveFilter(Guid id);
void ClearFilters();
// ── Bitmasks ──────────────────────────────────────────────────────────────
IReadOnlyList<CanBitmask> Bitmasks { get; }
CanBitmask AddBitmask(CanBitmask bitmask);
bool RemoveBitmask(Guid id);
void ClearBitmasks();
// ── Helpers used by the worker ────────────────────────────────────────────
bool PassesFilter(uint messageId);
IReadOnlyDictionary<string, double> ExtractSignals(uint messageId, byte[] data);
void PublishMessage(CanMessageDto dto);
// ── Channel management ────────────────────────────────────────────────────
/// <summary>Returns all PCAN USB channels that are currently available.</summary>
IReadOnlyList<PcanChannel> GetAvailableChannels();
/// <summary>
/// Asks the worker to reinitialise on the next loop iteration.
/// Non-blocking — the actual reconnect happens asynchronously.
/// </summary>
void RequestReinitialize(PcanChannel channel, Bitrate bitrate);
/// <summary>Called by the worker to pick up a pending reinit request.</summary>
bool TryConsumePendingReinit(out PcanChannel channel, out Bitrate bitrate);
// ── PCAN passthrough ──────────────────────────────────────────────────────
void Initialize(PcanChannel channel, Bitrate bitrate);
void Uninitialize();
PcanStatus Read(out PcanMessage msg, out ulong timestamp);
PcanStatus Write(PcanMessage msg);
}

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

@ -0,0 +1,16 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"IOModuleTestBlazor.CanWorker": "Debug"
}
},
"AllowedHosts": "*",
"CanOptions": {
"Channel": "",
"Bitrate": "Pcan500",
"Filters": [],
"Bitmasks": []
}
}

@ -0,0 +1,60 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,3 +0,0 @@
# IOModuleBlazorTest
Blazor app with can interface
Loading…
Cancel
Save