diff --git a/angular-client/src/assets/icons/battery.svg b/angular-client/src/assets/icons/battery.svg new file mode 100644 index 00000000..d39a0234 --- /dev/null +++ b/angular-client/src/assets/icons/battery.svg @@ -0,0 +1,3 @@ + + + diff --git a/angular-client/src/components/select-dropdown/select-dropdown.component.ts b/angular-client/src/components/select-dropdown/select-dropdown.component.ts index 72406433..0b76b565 100644 --- a/angular-client/src/components/select-dropdown/select-dropdown.component.ts +++ b/angular-client/src/components/select-dropdown/select-dropdown.component.ts @@ -1,4 +1,4 @@ -import { Component, input, OnInit, ViewChild } from '@angular/core'; +import { Component, effect, input, ViewChild } from '@angular/core'; import { SelectChangeEvent, Select } from 'primeng/select'; import { ReactiveFormsModule, FormsModule } from '@angular/forms'; @@ -19,8 +19,7 @@ export interface DropdownOption { standalone: true, imports: [Select, ReactiveFormsModule, FormsModule] }) -export class SelectDropdownComponent implements OnInit { - constructor() {} +export class SelectDropdownComponent { options = input([ { name: 'default', @@ -37,13 +36,17 @@ export class SelectDropdownComponent implements OnInit { // eslint-disable-next-line @typescript-eslint/no-explicit-any @ViewChild('dropdownRef') dropdownRef: any; - ngOnInit() { - if (this.defaultValue()) { - const defaultOption = this.options().find((option) => option.name === this.defaultValue()); - if (defaultOption) { - this.selectedOption = defaultOption; + constructor() { + // React to defaultValue changes (e.g. from "Set ALL Maps" selector) + effect(() => { + const val = this.defaultValue(); + if (val) { + const match = this.options().find((option) => option.name === val); + if (match) { + this.selectedOption = match; + } } - } + }); } handleChangedOption(changeEvent: SelectChangeEvent) { diff --git a/angular-client/src/pages/bms-debug-page/bms-debug-page.component.css b/angular-client/src/pages/bms-debug-page/bms-debug-page.component.css index a22b24e3..02f56196 100644 --- a/angular-client/src/pages/bms-debug-page/bms-debug-page.component.css +++ b/angular-client/src/pages/bms-debug-page/bms-debug-page.component.css @@ -11,3 +11,28 @@ mat-grid-tile { overflow: visible !important; /* Allow dropdown to overflow */ } + +/* ── Section header row ── */ +.section-header { + display: flex; + align-items: center; + padding: clamp(8px, 1.25vw, 24px) 0 clamp(4px, 0.63vw, 12px); + gap: clamp(6px, 0.94vw, 18px); +} + +.section-title { + font-family: 'Roboto', sans-serif; + font-weight: 700; + font-size: clamp(18px, 2.66vw, 44px); + color: #efefef; +} + +.section-title-right { + margin-left: auto; +} + +.segment-rows { + display: flex; + flex-direction: column; + gap: clamp(2px, 0.35vw, 8px); +} diff --git a/angular-client/src/pages/bms-debug-page/bms-debug-page.component.html b/angular-client/src/pages/bms-debug-page/bms-debug-page.component.html index 17882a2a..95a14e76 100644 --- a/angular-client/src/pages/bms-debug-page/bms-debug-page.component.html +++ b/angular-client/src/pages/bms-debug-page/bms-debug-page.component.html @@ -2,7 +2,6 @@ @if (isMobile) { } @else { - @@ -10,23 +9,24 @@ - -
- - - -
-
- @for (segment of segments; track segment) { - - - - } +
+ + +
+ Cell-by-Cell Heat Map + + Segment Overview +
+ + +
@for (segment of segments; track segment) { - - - + } - +
} diff --git a/angular-client/src/pages/bms-debug-page/bms-debug-page.component.ts b/angular-client/src/pages/bms-debug-page/bms-debug-page.component.ts index 8cab349f..9a31d3f1 100644 --- a/angular-client/src/pages/bms-debug-page/bms-debug-page.component.ts +++ b/angular-client/src/pages/bms-debug-page/bms-debug-page.component.ts @@ -1,13 +1,18 @@ -import { Component, HostListener } from '@angular/core'; +import { Component, HostListener, inject, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; import { allSegments } from 'src/utils/bms.utils'; import { MatGridList, MatGridTile } from '@angular/material/grid-list'; import { BmsHeaderComponent } from './components/bms-header/bms-header.component'; import { BmsAtAGlanceComponent } from './components/bms-at-a-glance/bms-at-a-glance.component'; -import { AccHighVoltageComponent } from './components/acc-high-voltage/acc-high-voltage.component'; -import { AccLowVoltageComponent } from './components/acc-low-voltage/acc-low-voltage.component'; -import { AccHighTempComponent } from './components/acc-high-temp/acc-high-temp.component'; -import { SegmentSummaryComponent } from './components/segment-summary/segment-summary.component'; -import { CellByCellHeatMapComponent } from './components/cell-by-cell-heat-map/cell-by-cell-heat-map.component'; +import { SegmentRowComponent } from './components/segment-row/segment-row.component'; +import { HeatMapService, HeatMapView } from 'src/services/heat-map.service'; +import { + DropdownOption, + SelectorConfig, + SelectDropdownComponent +} from 'src/components/select-dropdown/select-dropdown.component'; + +const formatAllSelectorName = (name: string) => 'Set ALL Maps: ' + name; @Component({ selector: 'app-bms-debug-page', @@ -19,14 +24,14 @@ import { CellByCellHeatMapComponent } from './components/cell-by-cell-heat-map/c MatGridTile, BmsHeaderComponent, BmsAtAGlanceComponent, - AccHighVoltageComponent, - AccLowVoltageComponent, - AccHighTempComponent, - SegmentSummaryComponent, - CellByCellHeatMapComponent + SegmentRowComponent, + SelectDropdownComponent ] }) -export class BmsDebugPageComponent { +export class BmsDebugPageComponent implements OnInit, OnDestroy { + private heatMapService = inject(HeatMapService); + private subscription?: Subscription; + time = new Date(); newRunIsLoading = false; mobileThreshold = 768; @@ -34,8 +39,41 @@ export class BmsDebugPageComponent { isMobile = window.innerWidth < this.mobileThreshold; segments = allSegments; + /** "Set ALL Maps" dropdown — shown once at the section header level */ + private allViewOptions: DropdownOption[] = [ + { + name: formatAllSelectorName(HeatMapView.Voltage.toString()), + function: () => this.heatMapService.setAllSegViews(HeatMapView.Voltage) + }, + { + name: formatAllSelectorName(HeatMapView.Balancing.toString()), + function: () => this.heatMapService.setAllSegViews(HeatMapView.Balancing) + }, + { + name: formatAllSelectorName(HeatMapView.Temperature.toString()), + function: () => this.heatMapService.setAllSegViews(HeatMapView.Temperature) + } + ]; + allSegSelectorConfig: SelectorConfig = { + options: this.allViewOptions, + placeholder: 'Set ALL Maps' + }; + constructor() {} + ngOnInit(): void { + this.subscription = this.heatMapService.globalView$.subscribe((view) => { + this.allSegSelectorConfig = { + ...this.allSegSelectorConfig, + defaultValue: formatAllSelectorName(view) + }; + }); + } + + ngOnDestroy(): void { + this.subscription?.unsubscribe(); + } + @HostListener('window:resize', ['$event']) onResize() { this.isMobile = window.innerWidth <= this.mobileThreshold; diff --git a/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.css b/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.css index cc9d76d0..9eb8c49e 100644 --- a/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.css +++ b/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.css @@ -2,3 +2,11 @@ mat-grid-tile { overflow: visible !important; /* Allow dropdown to overflow */ } + +.segment-page-heatmap { + --hex-w: clamp(60px, 5.5vw, 85px); + --hex-h: calc(var(--hex-w) * 1.155); + --hex-gap: 3px; + --row-offset: calc(var(--hex-w) / 2 + 3px); + margin: 0 auto; +} diff --git a/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.html b/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.html index c6be1d68..aedd1b54 100644 --- a/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.html +++ b/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.html @@ -12,7 +12,16 @@ - + +
+ +
diff --git a/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.ts b/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.ts index ab8558f5..5dc5b8dc 100644 --- a/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.ts +++ b/angular-client/src/pages/bms-debug-page/bms-segment-view/bms-segment-view.component.ts @@ -1,13 +1,20 @@ -import { Component, HostListener, OnInit } from '@angular/core'; -import { inject } from '@angular/core'; +import { Component, HostListener, inject, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; +import { Subscription } from 'rxjs'; import { allSegments, Chip, Segment } from 'src/utils/bms.utils'; import { MatGridList, MatGridTile } from '@angular/material/grid-list'; import { BmsHeaderComponent } from '../components/bms-header/bms-header.component'; import { SegmentAtAGlanceComponent } from '../components/segment-at-a-glance/segment-at-a-glance.component'; -import { CellByCellHeatMapComponent } from '../components/cell-by-cell-heat-map/cell-by-cell-heat-map.component'; +import { SegmentHeatmapComponent } from '../components/segment-heatmap/segment-heatmap.component'; import { ChipDiagnosticsComponent } from '../components/chip-diagnostics/chip-diagnostics.component'; import { ChipFaultsComponent } from '../components/chip-faults/chip-faults.component'; +import { InfoBackgroundComponent } from '../../../components/info-background/info-background.component'; +import { DropdownOption, SelectorConfig } from 'src/components/select-dropdown/select-dropdown.component'; +import { HeatMapService, HeatMapView } from 'src/services/heat-map.service'; + +const formatAllSelectorName = (name: string) => { + return 'Set ALL Maps: ' + name; +}; @Component({ selector: 'bms-segment-view', @@ -19,23 +26,84 @@ import { ChipFaultsComponent } from '../components/chip-faults/chip-faults.compo MatGridTile, BmsHeaderComponent, SegmentAtAGlanceComponent, - CellByCellHeatMapComponent, + SegmentHeatmapComponent, + InfoBackgroundComponent, ChipDiagnosticsComponent, ChipFaultsComponent ] }) -export class BmsSegmentViewComponent implements OnInit { +export class BmsSegmentViewComponent implements OnInit, OnDestroy { private readonly route = inject(ActivatedRoute); private router = inject(Router); + private heatMapService = inject(HeatMapService); + private subscriptions: Subscription[] = []; changeTitleSize = window.innerWidth < 1060; segmentId!: Segment; chipAlpha: Chip = Chip.Alpha; chipBeta: Chip = Chip.Beta; + cellViewSelectOptions: DropdownOption[] = [ + { + name: HeatMapView.Temperature.toString(), + function: () => { + this.heatMapService.setCurrentView(this.segmentId, HeatMapView.Temperature); + } + }, + { + name: HeatMapView.Voltage.toString(), + function: () => { + this.heatMapService.setCurrentView(this.segmentId, HeatMapView.Voltage); + } + }, + { + name: HeatMapView.Balancing.toString(), + function: () => { + this.heatMapService.setCurrentView(this.segmentId, HeatMapView.Balancing); + } + } + ]; + + currentSegmentSelectorConfig: SelectorConfig = { + options: this.cellViewSelectOptions, + placeholder: 'Change View' + }; + + allSegSelectorConfig: SelectorConfig = { + options: this.cellViewSelectOptions.map((option) => ({ + name: formatAllSelectorName(option.name), + function: () => { + this.heatMapService.setAllSegViews(option.name as HeatMapView); + } + })), + placeholder: 'Change ALL Segments' + }; + ngOnInit(): void { this.subscribeToSegmentID(); } + getHeatmapTitle(): string { + return 'Segment ' + (this.segmentId + 1) + ': Cell-by-Cell'; + } + + private subscribeToView(): void { + const viewSub = this.heatMapService.getCurrentView(this.segmentId); + if (viewSub) { + this.subscriptions.push( + viewSub.subscribe((view) => { + this.allSegSelectorConfig = { + ...this.allSegSelectorConfig, + defaultValue: view !== undefined ? formatAllSelectorName(view.toString()) : 'Change ALL Segments' + }; + this.currentSegmentSelectorConfig = { + ...this.currentSegmentSelectorConfig, + defaultValue: view !== undefined ? view : 'Change View' + }; + }) + ); + } + } + // Update view width @HostListener('window:resize', ['$event']) onResize() { @@ -47,9 +115,14 @@ export class BmsSegmentViewComponent implements OnInit { this.route.paramMap.subscribe((params) => { const possibleSegId = Number(params.get('id')) - 1; allSegments.indexOf(possibleSegId) !== -1 ? (this.segmentId = possibleSegId) : this.router.navigate(['bms']); + this.subscribeToView(); }); } else { this.router.navigate(['bms']); } }; + + ngOnDestroy(): void { + this.subscriptions.forEach((s) => s.unsubscribe()); + } } diff --git a/angular-client/src/pages/bms-debug-page/components/cell-by-cell-heat-map/cell-by-cell-heat-map.component.html b/angular-client/src/pages/bms-debug-page/components/cell-by-cell-heat-map/cell-by-cell-heat-map.component.html index 5701e804..fb96ffa4 100644 --- a/angular-client/src/pages/bms-debug-page/components/cell-by-cell-heat-map/cell-by-cell-heat-map.component.html +++ b/angular-client/src/pages/bms-debug-page/components/cell-by-cell-heat-map/cell-by-cell-heat-map.component.html @@ -15,7 +15,7 @@ [currentView]="this.view" [boxShadowColor]="this.isSelectedCell(cell)" [sevenBoxLayout]="true" - [upperTitle]="cell.cellNumbers?.toString()" + [upperTitle]="cell.cellNumber.toString()" > } @@ -29,97 +29,59 @@ [currentView]="this.view" [boxShadowColor]="this.isSelectedCell(cell)" [sevenBoxLayout]="true" - [upperTitle]="cell.cellNumbers?.toString()" + [upperTitle]="cell.cellNumber.toString()" > } } @else if (this.view.toString() === 'Voltage') { - @for (cell of betaCells.slice().reverse(); track cell; let i = $index) { - @if (i !== 0) { - - } - + @for (cell of betaCells.slice().reverse(); track cell) { }
- @for (cell of alphaCells; track cell; let i = $index) { - + @for (cell of alphaCells; track cell) { } } @else if (this.view === 'Balancing') { - @for (cell of betaCells.slice().reverse(); track cell; let i = $index) { - @if (i !== 0) { - - } - + @for (cell of betaCells.slice().reverse(); track cell) { }
- @for (cell of alphaCells; track cell; let i = $index) { - + @for (cell of alphaCells; track cell) { } diff --git a/angular-client/src/pages/bms-debug-page/components/cell-by-cell-heat-map/cell-by-cell-heat-map.component.ts b/angular-client/src/pages/bms-debug-page/components/cell-by-cell-heat-map/cell-by-cell-heat-map.component.ts index 56b4d662..5b92cb80 100644 --- a/angular-client/src/pages/bms-debug-page/components/cell-by-cell-heat-map/cell-by-cell-heat-map.component.ts +++ b/angular-client/src/pages/bms-debug-page/components/cell-by-cell-heat-map/cell-by-cell-heat-map.component.ts @@ -1,7 +1,7 @@ import { Component, effect, inject, input, OnInit } from '@angular/core'; import { Segment } from 'src/utils/bms.utils'; import { HeatMapService, HeatMapView } from 'src/services/heat-map.service'; -import { AlphaCells, BetaCells, CellReading, CellService } from 'src/services/cell.service'; +import { CellReading, CellService } from 'src/services/cell.service'; import { DropdownOption, SelectorConfig } from 'src/components/select-dropdown/select-dropdown.component'; import { DialogService } from 'primeng/dynamicdialog'; import { CellViewComponent } from '../cell-view/cell-view.component'; @@ -25,8 +25,8 @@ export class CellByCellHeatMapComponent implements OnInit { private heatMapService = inject(HeatMapService); private dialogService = inject(DialogService); currentSegment = input.required(); - alphaCells!: Readonly; - betaCells!: Readonly; + alphaCells!: Readonly; + betaCells!: Readonly; view = HeatMapView.Voltage; selectedCell: CellReading | undefined = undefined; cellViewSelectOptions: DropdownOption[] = [ @@ -132,16 +132,8 @@ export class CellByCellHeatMapComponent implements OnInit { return this.selectedCell === cell; }; - averageVoltCellPair = (reading: CellReading): number | undefined => { - const { volt1, volt2 } = reading; - - if (volt1 === undefined) { - return volt2; - } else if (volt2 === undefined) { - return volt1; - } - - return (volt1 + volt2) / 2; + getCellVoltage = (reading: CellReading): number | undefined => { + return reading.voltage; }; // open cell-view dialog diff --git a/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.css b/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.css index e69de29b..6f600f02 100644 --- a/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.css +++ b/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.css @@ -0,0 +1,96 @@ +:host { + display: block; +} + +/* Grid: label column + N data columns side by side */ +.compare-grid { + display: flex; + gap: 0; +} + +/* Shared column structure */ +.label-col, +.data-col { + display: flex; + flex-direction: column; +} + +/* Left label column */ +.label-col { + min-width: 100px; + flex-shrink: 0; +} + +.corner-cell { + height: 58px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.row-label { + font-family: 'Roboto', sans-serif; + font-size: 18px; + font-weight: 600; + color: rgba(255, 255, 255, 0.4); + padding: 12px 16px 12px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + display: flex; + align-items: center; +} + +.row-label:last-child { + border-bottom: none; +} + +/* Data columns */ +.data-col { + min-width: 130px; + border-left: 1px solid rgba(255, 255, 255, 0.08); +} + +.col-header { + display: flex; + flex-direction: column; + padding: 8px 18px 8px; + height: 58px; + justify-content: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.03); +} + +.col-title { + font-family: 'Roboto', sans-serif; + font-size: 22px; + font-weight: 700; + color: #efefef; + line-height: 1.1; +} + +.col-subtitle { + font-family: 'Roboto', sans-serif; + font-size: 14px; + font-weight: 500; + color: rgba(255, 255, 255, 0.35); + line-height: 1; +} + +.data-value { + font-family: 'Roboto', sans-serif; + font-size: 20px; + font-weight: 600; + color: #efefef; + padding: 12px 18px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); + white-space: nowrap; +} + +.data-value:last-child { + border-bottom: none; +} + +.bal-yes { + color: #4169e1; +} + +.bal-no { + color: #ffd700; +} diff --git a/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.html b/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.html index 7d97102d..d527d706 100644 --- a/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.html +++ b/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.html @@ -1,51 +1,25 @@ - - - - - - - - - - - - - - - +
+ +
+
+ Voltage + Temp + Balancing +
+ @for (cell of cells; track cell.reading) { +
+
+ {{ cell.cellNum }} + {{ chipLabel(cell.reading.chip) }} · S{{ cell.segment + 1 }} +
+ {{ formatVoltage(cell.reading.voltage) }} + {{ formatTemp(cell.reading.temp) }} + {{ formatBool(cell.reading.balancing) }} +
+ } +
diff --git a/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.ts b/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.ts index 908caca2..29927f19 100644 --- a/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.ts +++ b/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.ts @@ -1,78 +1,50 @@ -import { Component, HostListener, inject, input } from '@angular/core'; +import { ChangeDetectorRef, Component, inject, OnDestroy } from '@angular/core'; import { DynamicDialogConfig } from 'primeng/dynamicdialog'; -import { CellReading } from 'src/services/cell.service'; -import { HeatMapService } from 'src/services/heat-map.service'; -import { chipToString, Segment } from 'src/utils/bms.utils'; -import { InfoBackgroundComponent } from '../../../../components/info-background/info-background.component'; - -import { InfoValueDisplayComponent } from '../../../../components/info-value-dispaly/info-value-display.component'; -import { DividerComponent } from '../../../../components/divider/divider'; -import HStackComponent from 'src/components/hstack/hstack.component'; -import VStackComponent from 'src/components/vstack/vstack.component'; +import { SelectedCellInfo } from 'src/services/heat-map.service'; +import { Chip, chipToString } from 'src/utils/bms.utils'; @Component({ selector: 'cell-view', templateUrl: './cell-view.component.html', styleUrl: './cell-view.component.css', standalone: true, - imports: [InfoBackgroundComponent, InfoValueDisplayComponent, DividerComponent, HStackComponent, VStackComponent] + imports: [] }) -export class CellViewComponent { - private heatMapService = inject(HeatMapService); - cellViewData: CellReading | undefined = undefined; - screenWidth = window.innerWidth; - forSegment = input.required(); - segment: Segment; +export class CellViewComponent implements OnDestroy { + private cdr = inject(ChangeDetectorRef); + private refreshInterval: ReturnType | undefined; public config = inject(DynamicDialogConfig); - // Update view width - @HostListener('window:resize', ['$event']) - onResize() { - this.screenWidth = window.innerWidth; - } + /** Shared array reference — additions/removals by SegmentHeatmapComponent + * are visible here because it's the same array object. */ + cells: SelectedCellInfo[]; constructor() { - this.segment = this.config.data.forSegment; - this.heatMapService.getSelectedCell(this.segment)?.subscribe((data) => { - this.cellViewData = data; - }); + this.cells = this.config.data.cells; + // Poll for MQTT value changes and selection array changes. + this.refreshInterval = setInterval(() => this.cdr.detectChanges(), 500); } - getTitle = (): string => { - const title = `Seg ${this.segment + 1}: Cell View`; - return title; - }; - - getUpperRightTitle = (): string => { - const smallChipLabel = this.screenWidth < 1200; - - const chipValue = - this.cellViewData?.chip !== undefined ? chipToString(this.cellViewData?.chip, smallChipLabel) : 'No Value'; - - const tempValue = - this.cellViewData?.temp !== undefined && this.cellViewData?.temp !== null - ? `${this.cellViewData?.temp?.toFixed(2)} °C` - : 'No Value'; - - const chipLabel = this.screenWidth <= 1100 ? `C:` : `Cell:`; - const tempLabel = this.screenWidth <= 1100 ? `T:` : `Temp:`; - const title = `${chipLabel} ${chipValue} | ${tempLabel} ${tempValue}`; + ngOnDestroy(): void { + if (this.refreshInterval) { + clearInterval(this.refreshInterval); + } + } - return title; - }; + chipLabel(chip: Chip): string { + return chipToString(chip, true); + } - getCellNumTitle = (): string => { - const cellNumLabel = this.screenWidth <= 1100 ? `Cell` : `Cell Number`; - return cellNumLabel; - }; + formatVoltage(v: number | undefined): string { + return v !== undefined ? `${v.toFixed(3)} V` : '-'; + } - getCellVoltageTitle = (): string => { - const cellVoltageLabel = this.screenWidth <= 1100 ? `Volts` : `Voltage`; - return cellVoltageLabel; - }; + formatTemp(t: number | undefined): string { + return t !== undefined && t !== null ? `${t.toFixed(1)} °C` : '-'; + } - getBalancingTitle = (): string => { - const balancingLabel = this.screenWidth <= 1100 ? `Bal.?` : `Balancing?`; - return balancingLabel; - }; + formatBool(b: boolean | undefined): string { + if (b === undefined) return '-'; + return b ? 'Yes' : 'No'; + } } diff --git a/angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.css b/angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.css new file mode 100644 index 00000000..7e18856b --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.css @@ -0,0 +1,191 @@ +:host { + display: block; + flex-shrink: 0; +} + +/* Pointy-top hexagon via clip-path */ +.hex-tile { + width: var(--hex-w, 42px); + height: var(--hex-h, 48px); + clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + transition: filter 0.15s ease; +} + +.hex-tile:hover { + filter: brightness(1.15); +} + +/* Selected: drop-shadow on host follows the clip-path shape */ +.hex-tile.selected { + filter: brightness(1.3); +} + +:host(.selected-cell) { + filter: drop-shadow(0 0 3px white) drop-shadow(0 0 1px white); + z-index: 1; +} + +.cell-number { + position: absolute; + top: 8%; + left: 50%; + transform: translateX(-50%); + font-size: calc(var(--hex-w, 42px) * 0.18); + font-weight: 600; + color: #2c2c2c; + line-height: 1; +} + +.cell-value-container { + display: flex; + align-items: baseline; + gap: 0; +} + +.cell-value { + font-size: calc(var(--hex-w, 42px) * 0.24); + font-weight: 700; + color: #2c2c2c; + line-height: 1; +} + +.cell-unit { + font-size: calc(var(--hex-w, 42px) * 0.15); + font-weight: 600; + color: #2c2c2c; + vertical-align: super; +} + +/* ── View Variants ── */ + +/* Voltage: number top-left, value center-bottom */ +.hex-tile.view-voltage .cell-number { + top: 22%; + left: 20%; + transform: none; + font-size: calc(var(--hex-w, 42px) * 0.2); + font-weight: 700; + opacity: 0.65; +} + +.hex-tile.view-voltage { + justify-content: flex-end; + padding-bottom: 16%; +} + +/* Balancing: number top-left, value centered */ +.hex-tile.view-balancing .cell-number { + top: 22%; + left: 20%; + transform: none; + font-size: calc(var(--hex-w, 42px) * 0.2); + font-weight: 700; + opacity: 0.65; +} + +.hex-tile.view-balancing { + justify-content: flex-end; + padding-bottom: 16%; +} + +/* Temperature: number top-left, value centered */ +.hex-tile.view-temperature .cell-number { + top: 22%; + left: 20%; + transform: none; + font-size: calc(var(--hex-w, 42px) * 0.2); + font-weight: 700; + opacity: 0.65; +} + +.hex-tile.view-temperature { + justify-content: flex-end; + padding-bottom: 16%; +} + +/* ── heatmap-cell variant: wider hex, number top-left above value ── */ + +:host(.heatmap-cell) .hex-tile { + width: var(--hex-w, 48px); + height: var(--hex-h, 48px); + align-items: flex-start; + justify-content: center; + padding: 0% 0 0 8%; + flex-direction: column; + gap: 0; +} + +:host(.heatmap-cell) .cell-number { + position: static; + font-size: calc(var(--hex-w, 48px) * 0.22); + font-weight: 800; + color: #1a1a1a; + opacity: 0.6; + line-height: 1; + margin-bottom: 1px; +} + +:host(.heatmap-cell) .cell-value-container { + gap: 1px; +} + +:host(.heatmap-cell) .cell-value { + font-size: calc(var(--hex-w, 48px) * 0.36); + font-weight: 700; + color: #1a1a1a; + line-height: 1; +} + +:host(.heatmap-cell) .cell-unit { + font-size: calc(var(--hex-w, 48px) * 0.2); + color: #1a1a1a; +} + +/* ── Double-hex variant: two merged pointy-top hex silhouettes ── */ + +:host(.heatmap-cell-double) .hex-tile { + width: calc(var(--hex-w, 48px) * 2 + var(--hex-gap, 2px)); + height: var(--hex-h, 48px); + /* Outline of two pointy-top hexagons sharing their inner vertical edge. + V-notches at top-center (50% 25%) and bottom-center (50% 75%) + reveal the two-hex silhouette. */ + clip-path: polygon(0% 25%, 25% 0%, 50% 25%, 75% 0%, 100% 25%, 100% 75%, 75% 100%, 50% 75%, 25% 100%, 0% 75%); + align-items: flex-start; + justify-content: center; + padding: 0% 0 0 5%; + flex-direction: column; + gap: 0; +} + +:host(.heatmap-cell-double) .cell-number { + position: static; + font-size: calc(var(--hex-w, 48px) * 0.22); + font-weight: 800; + color: #1a1a1a; + opacity: 0.6; + line-height: 1; + margin-bottom: 1px; +} + +:host(.heatmap-cell-double) .cell-value-container { + gap: 1px; + align-self: center; +} + +:host(.heatmap-cell-double) .cell-value { + font-size: calc(var(--hex-w, 48px) * 0.36); + font-weight: 700; + color: #1a1a1a; + line-height: 1; +} + +:host(.heatmap-cell-double) .cell-unit { + font-size: calc(var(--hex-w, 48px) * 0.2); + color: #1a1a1a; +} diff --git a/angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.html b/angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.html new file mode 100644 index 00000000..78375ff0 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.html @@ -0,0 +1,9 @@ +
+ {{ cellNumber() }} + + {{ displayValue() }} + @if (unitLabel()) { + {{ unitLabel() }} + } + +
diff --git a/angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.ts b/angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.ts new file mode 100644 index 00000000..28f0b812 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.ts @@ -0,0 +1,47 @@ +import { Component, input, computed } from '@angular/core'; +import { HeatMapView } from 'src/services/heat-map.service'; + +@Component({ + selector: 'hex-tile', + templateUrl: './hex-tile.component.html', + styleUrl: './hex-tile.component.css', + standalone: true, + host: { + '[class]': 'variant()', + '[class.selected-cell]': 'boxShadowColor()' + } +}) +export class HexTileComponent { + variant = input(''); + value = input(); + booleanValue = input(); + color = input.required(); + currentView = input(); + boxShadowColor = input(false); + cellNumber = input(); + + viewClass = computed(() => { + const view = this.currentView(); + let cls = 'hex-tile'; + if (view === HeatMapView.Voltage) cls += ' view-voltage'; + else if (view === HeatMapView.Balancing) cls += ' view-balancing'; + else if (view === HeatMapView.Temperature) cls += ' view-temperature'; + return cls; + }); + + displayValue = computed(() => { + if (this.booleanValue() !== undefined) { + return this.booleanValue() ? 'YES' : 'NO'; + } + const value = this.value(); + return value === undefined ? '-' : value.toFixed(2); + }); + + unitLabel = computed(() => { + if (this.booleanValue() !== undefined) return ''; + const view = this.currentView(); + if (view === HeatMapView.Temperature) return '°C'; + if (view === HeatMapView.Voltage) return 'V'; + return ''; + }); +} diff --git a/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.css b/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.css new file mode 100644 index 00000000..37192104 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.css @@ -0,0 +1,41 @@ +:host { + display: block; + --hex-w: clamp(28px, 4.1vw, 76px); + --hex-h: calc(var(--hex-w) * 1.155); + --hex-gap: 2px; + --row-offset: calc(var(--hex-w) / 2 + 2px); + --row-gap: calc(var(--hex-h) * -0.2); + flex: 0 1 auto; + width: fit-content; + min-width: fit-content; +} + +.hex-grid-zone { + display: flex; + flex-direction: column; + align-items: flex-start; + overflow: visible; + padding: 4px 0; + min-width: 0; +} + +.hex-row { + display: flex; + flex-wrap: nowrap; +} + +.hex-row hex-tile { + margin-right: var(--hex-gap); +} + +.hex-row hex-tile:last-child { + margin-right: 0; +} + +.hex-row.top-row { + margin-left: var(--row-offset); +} + +.hex-row.bottom-row { + margin-top: var(--row-gap); +} diff --git a/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.html b/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.html new file mode 100644 index 00000000..272fb5a6 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.html @@ -0,0 +1,30 @@ +
+
+ @for (cell of topRowCells; track $index) { + + } +
+
+ @for (cell of bottomRowCells; track $index) { + + } +
+
diff --git a/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.ts b/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.ts new file mode 100644 index 00000000..f857d79a --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.ts @@ -0,0 +1,196 @@ +import { Component, effect, inject, input, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { Chip, Segment } from 'src/utils/bms.utils'; +import { HeatMapService, HeatMapView, SelectedCellInfo } from 'src/services/heat-map.service'; +import { CellReading, CellService } from 'src/services/cell.service'; +import { ALPHA_THERM_CELL_MAP, BETA_THERM_CELL_MAP } from 'src/utils/bms.config'; +import { DialogService } from 'primeng/dynamicdialog'; +import { CellViewComponent } from '../cell-view/cell-view.component'; +import { HexTileComponent } from '../hex-tile/hex-tile.component'; + +export interface DisplayCell { + reading: CellReading; + value: number | undefined; + boolValue: boolean | undefined; + cellNum: string; + cellCount: number; +} + +@Component({ + selector: 'segment-heatmap', + templateUrl: './segment-heatmap.component.html', + styleUrl: './segment-heatmap.component.css', + standalone: true, + imports: [HexTileComponent] +}) +export class SegmentHeatmapComponent implements OnInit, OnDestroy { + private cellService = inject(CellService); + private heatMapService = inject(HeatMapService); + private dialogService = inject(DialogService); + private subscriptions: Subscription[] = []; + + segment = input.required(); + + alphaCells!: Readonly; + betaCells!: Readonly; + view = HeatMapView.Voltage; + + constructor() { + effect(() => { + this.alphaCells = this.cellService.getAlphaCellsBySegment(this.segment()); + this.betaCells = this.cellService.getBetaCellsBySegment(this.segment()); + }); + } + + ngOnInit(): void { + this.alphaCells = this.cellService.getAlphaCellsBySegment(this.segment()); + this.betaCells = this.cellService.getBetaCellsBySegment(this.segment()); + + const viewSub = this.heatMapService.getCurrentView(this.segment()); + if (viewSub) { + this.subscriptions.push( + viewSub.subscribe((view) => { + this.view = view; + }) + ); + } + } + + /** Map each CellReading 1:1 to a DisplayCell */ + private toDisplayCells(cells: Readonly): DisplayCell[] { + return cells.map((cell) => ({ + reading: cell, + value: this.getCellValue(cell), + boolValue: this.getCellBoolValue(cell), + cellNum: cell.cellNumber.toString(), + cellCount: 1 + })); + } + + /** Group cells by therm mapping into combined DisplayCells */ + private toThermDisplayCells(cells: Readonly, thermMap: number[][]): DisplayCell[] { + return thermMap.map((cellIndices) => { + const primary = cells[cellIndices[0]]; + const label = cellIndices.join(','); + return { + reading: primary, + value: primary?.temp, + boolValue: undefined, + cellNum: label, + cellCount: cellIndices.length + }; + }); + } + + get topRowCells(): DisplayCell[] { + if (this.view === HeatMapView.Temperature) { + return this.toThermDisplayCells(this.betaCells, BETA_THERM_CELL_MAP).reverse(); + } + return this.toDisplayCells(this.betaCells).reverse(); + } + + get bottomRowCells(): DisplayCell[] { + if (this.view === HeatMapView.Temperature) { + return this.toThermDisplayCells(this.alphaCells, ALPHA_THERM_CELL_MAP); + } + return this.toDisplayCells(this.alphaCells); + } + + private getCellValue(cell: CellReading): number | undefined { + if (this.view === HeatMapView.Temperature) return cell.temp; + if (this.view === HeatMapView.Voltage) return cell.voltage; + return undefined; + } + + private getCellBoolValue(cell: CellReading): boolean | undefined { + if (this.view !== HeatMapView.Balancing) return undefined; + return cell.balancing; + } + + getColor(cell: DisplayCell): string { + if (this.view === HeatMapView.Balancing) return this.getBalancingColor(cell.boolValue); + if (this.view === HeatMapView.Temperature) return this.getTempColor(cell.value); + return this.getVoltColor(cell.value); + } + + private getTempColor(value: number | undefined): string { + if (value === undefined) return 'grey'; + const hsl = Math.min(Math.max(55 - value, 0) * 6, 120); + return `hsl(${hsl}, 100%, 50%)`; + } + + private getVoltColor(value: number | undefined): string { + if (value === undefined) return 'grey'; + const hsl = Math.min(Math.max((value - 3.0) * 200, 0), 120); + return `hsl(${hsl}, 100%, 50%)`; + } + + private getBalancingColor(value: boolean | undefined): string { + if (value === undefined) return 'grey'; + return value ? '#4169e1' : 'yellow'; + } + + cellClicked(displayCell: DisplayCell): void { + const segment = this.segment(); + + // In temperature view, find therm group and add all member cells + if (this.view === HeatMapView.Temperature) { + const { cellNumber: cellNum, chip } = displayCell.reading; + const thermMap = chip === Chip.Alpha ? ALPHA_THERM_CELL_MAP : BETA_THERM_CELL_MAP; + const cells = chip === Chip.Alpha ? this.alphaCells : this.betaCells; + + // Find which therm group this cell belongs to + const group = thermMap.find((indices) => indices.includes(cellNum)) ?? [cellNum]; + + // Check if any cell in the group is already selected — if so, toggle all off + const anySelected = group.some((idx) => cells[idx] && this.heatMapService.isCellSelected(cells[idx])); + + for (const idx of group) { + const reading = cells[idx]; + if (!reading) continue; + const info: SelectedCellInfo = { reading, cellNum: idx.toString(), segment }; + if (anySelected) { + // Remove if present + const i = this.heatMapService.selectedCells.findIndex((s) => s.reading === reading); + if (i >= 0) this.heatMapService.selectedCells.splice(i, 1); + } else if (!this.heatMapService.isCellSelected(reading)) { + this.heatMapService.selectedCells.push(info); + } + } + } else { + const info: SelectedCellInfo = { + reading: displayCell.reading, + cellNum: displayCell.cellNum, + segment + }; + this.heatMapService.toggleCell(info); + } + + // Open dialog on first selection + if (this.heatMapService.selectedCells.length > 0 && !this.heatMapService.dialogRef) { + this.heatMapService.dialogRef = this.dialogService.open(CellViewComponent, { + data: { cells: this.heatMapService.selectedCells }, + header: 'Cell Comparison', + draggable: true, + closable: true, + closeAriaLabel: 'Close', + styleClass: 'cell-compare-dialog' + }); + this.heatMapService.dialogRef.onClose.subscribe(() => { + this.heatMapService.clearSelection(); + this.heatMapService.dialogRef = null; + }); + } else if (this.heatMapService.selectedCells.length === 0 && this.heatMapService.dialogRef) { + // Close dialog when all cells deselected + this.heatMapService.dialogRef.close(); + } + } + + isSelected(cell: CellReading): boolean { + return this.heatMapService.isCellSelected(cell); + } + + ngOnDestroy(): void { + this.subscriptions.forEach((s) => s.unsubscribe()); + } +} diff --git a/angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.css b/angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.css new file mode 100644 index 00000000..5e51eb97 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.css @@ -0,0 +1,56 @@ +:host { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; +} + +.overview-zone { + display: flex; + gap: 0; + align-items: center; + justify-content: center; + height: 100%; + overflow: hidden; + max-width: fit-content; + margin: 0 auto; +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: 0; + flex: 1 1 0; + padding: 0 clamp(4px, 1vw, 24px); + overflow: hidden; +} + +.stat + .stat { + border-left: 1px solid #4a4a4a; +} + +.stat-value { + font-family: 'Roboto', sans-serif; + font-weight: 700; + font-size: clamp(0.8rem, 2.2vw, 3rem); + white-space: nowrap; + color: #efefef; + line-height: 1.1; +} + +.stat-unit { + font-size: 0.45em; + font-weight: 400; + color: #aaa; +} + +.stat-label { + font-family: 'Roboto', sans-serif; + font-size: clamp(0.4rem, 0.75vw, 1rem); + white-space: nowrap; + color: #cacaca; + margin-top: 4px; +} diff --git a/angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.html b/angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.html new file mode 100644 index 00000000..0bdd8836 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.html @@ -0,0 +1,14 @@ +
+
+ {{ formatTemp() }}°C + Temperature +
+
+ {{ formatVoltage() }}V + Voltage +
+
+ {{ formatChipTemp() }}°C + Chip Temp +
+
diff --git a/angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.ts b/angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.ts new file mode 100644 index 00000000..53213f73 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.ts @@ -0,0 +1,45 @@ +import { Component, inject, input, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import Storage from 'src/services/storage.service'; +import { Segment, segmentInfoMap, SegmentInfo } from 'src/utils/bms.utils'; + +@Component({ + selector: 'segment-overview', + templateUrl: './segment-overview.component.html', + styleUrl: './segment-overview.component.css', + standalone: true +}) +export class SegmentOverviewComponent implements OnInit, OnDestroy { + private storage = inject(Storage); + private subscriptions: Subscription[] = []; + + segment = input.required(); + temperature!: number; + voltage!: number; + chipTemp!: number; + + ngOnInit(): void { + const info: SegmentInfo = segmentInfoMap[this.segment()]; + this.subscriptions.push( + this.storage.get(info.segmentTempKey).subscribe((v) => (this.temperature = parseFloat(v.values[0]))), + this.storage.get(info.voltageKey).subscribe((v) => (this.voltage = parseFloat(v.values[0]))), + this.storage.get(info.alphaChipTempKey).subscribe((v) => (this.chipTemp = parseFloat(v.values[0]))) + ); + } + + formatTemp(): string { + return this.temperature !== undefined ? this.temperature.toFixed(0) : '-'; + } + + formatVoltage(): string { + return this.voltage !== undefined ? this.voltage.toFixed(1) : '-'; + } + + formatChipTemp(): string { + return this.chipTemp !== undefined ? this.chipTemp.toFixed(0) : '-'; + } + + ngOnDestroy(): void { + this.subscriptions.forEach((s) => s.unsubscribe()); + } +} diff --git a/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.css b/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.css new file mode 100644 index 00000000..b964b576 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.css @@ -0,0 +1,141 @@ +:host { + display: block; +} + +/* ── Segment Row ── */ +.segment-row { + display: flex; + align-items: stretch; + border-radius: clamp(4px, 0.63vw, 14px); + gap: clamp(4px, 0.94vw, 20px); + width: 100%; + background-color: #1a1a1a; + padding: clamp(2px, 0.4vw, 7px); +} + +/* ── Left: Controls (inside dark wrapper) ── */ +.controls-zone { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + gap: clamp(3px, 0.63vw, 12px); + min-width: clamp(70px, 10vw, 200px); + flex-shrink: 0; + padding: clamp(3px, 0.63vw, 12px); +} + +/* ── Right: Separate light panels ── */ +.content-zone { + flex: 1; + display: flex; + align-items: stretch; + gap: 10px; + justify-content: flex-start; + padding: 0; + background-color: transparent; + border-radius: 0; + min-width: 0; +} + +.heatmap-panel { + flex: 3 1 auto; + display: flex; + align-items: center; + justify-content: center; + padding: clamp(1px, 0.3vw, 6px) clamp(4px, 0.94vw, 20px) clamp(1px, 0.3vw, 6px) clamp(20px, 4vw, 80px); + background-color: #2c2c2c; + border-radius: clamp(4px, 0.63vw, 14px); + clip-path: polygon(clamp(34px, 6.9vw, 136px) 0, 100% 0, 100% 100%, 0 100%, 0 clamp(34px, 6.9vw, 136px)); + min-height: 0; +} + +.overview-panel { + display: flex; + align-items: center; + padding: clamp(1px, 0.3vw, 6px) clamp(6px, 1.25vw, 26px); + background-color: #2c2c2c; + border-radius: clamp(4px, 0.63vw, 14px); + min-width: clamp(120px, 18vw, 380px); + min-height: 0; + flex: 1 1 auto; +} + +.segment-title { + display: flex; + align-items: center; + gap: 6px; +} + +.battery-icon { + width: clamp(8px, 1vw, 20px); + height: clamp(10px, 1.33vw, 26px); +} + +.title-text { + font-family: 'Roboto', sans-serif; + font-weight: 700; + font-size: clamp(10px, 1.56vw, 30px); + color: #efefef; + white-space: nowrap; +} + +/* ── Actions group: dropdown + button same width ── */ +.controls-actions { + display: flex; + flex-direction: column; + gap: clamp(2px, 0.47vw, 10px); + width: clamp(64px, 9.4vw, 190px); +} + +.see-more-btn { + background-color: #2aaeee; + color: #000; + border: none; + border-radius: clamp(8px, 1.25vw, 24px); + padding: clamp(2px, 0.47vw, 10px) clamp(6px, 1.4vw, 28px); + font-size: clamp(7px, 0.94vw, 18px); + font-weight: 600; + cursor: pointer; + white-space: nowrap; + width: 100%; + text-align: center; + height: clamp(18px, 2.3vw, 44px); + box-sizing: border-box; +} + +.see-more-btn:hover { + background-color: #1e9ad8; +} + +/* ── Dropdown overrides inside controls-zone ── */ +.controls-zone ::ng-deep .small-grey-select { + width: 100% !important; + min-width: 0 !important; + height: clamp(18px, 2.3vw, 44px) !important; + border-radius: clamp(8px, 1.25vw, 24px) !important; + background-color: #555 !important; + font-size: clamp(7px, 0.94vw, 18px) !important; + display: flex !important; + align-items: center !important; + box-sizing: border-box !important; +} + +.controls-zone ::ng-deep .p-select-label, +.controls-zone ::ng-deep .p-select-label.p-placeholder { + width: auto !important; + padding: 0 clamp(4px, 0.94vw, 20px) !important; + font-size: clamp(7px, 0.94vw, 18px) !important; + font-weight: 600 !important; + color: #c0c0c0 !important; + --p-select-placeholder-color: #c0c0c0 !important; + line-height: clamp(18px, 2.3vw, 44px) !important; + display: flex !important; + align-items: center !important; +} + +.controls-zone ::ng-deep .p-select-trigger { + padding-right: clamp(3px, 0.63vw, 14px) !important; + display: flex !important; + align-items: center !important; +} diff --git a/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.html b/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.html new file mode 100644 index 00000000..ce483cfa --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.html @@ -0,0 +1,29 @@ + +
+ +
+
+ battery + Segment {{ segment() + 1 }} +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+
diff --git a/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.ts b/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.ts new file mode 100644 index 00000000..8918aac7 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.ts @@ -0,0 +1,70 @@ +import { Component, inject, input, OnInit, OnDestroy } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { Segment } from 'src/utils/bms.utils'; +import { HeatMapService, HeatMapView } from 'src/services/heat-map.service'; +import { + DropdownOption, + SelectorConfig, + SelectDropdownComponent +} from 'src/components/select-dropdown/select-dropdown.component'; +import { appRoutes } from 'src/app/app-routing.module'; +import { SegmentHeatmapComponent } from '../segment-heatmap/segment-heatmap.component'; +import { SegmentOverviewComponent } from '../segment-overview/segment-overview.component'; + +@Component({ + selector: 'segment-row', + templateUrl: './segment-row.component.html', + styleUrl: './segment-row.component.css', + standalone: true, + imports: [SelectDropdownComponent, SegmentHeatmapComponent, SegmentOverviewComponent] +}) +export class SegmentRowComponent implements OnInit, OnDestroy { + private heatMapService = inject(HeatMapService); + private router = inject(Router); + private subscriptions: Subscription[] = []; + + segment = input.required(); + + // View selector config (Voltage and Balancing only per ticket) + viewSelectorConfig!: SelectorConfig; + private viewOptions: DropdownOption[] = [ + { + name: HeatMapView.Voltage.toString(), + function: () => this.heatMapService.setCurrentView(this.segment(), HeatMapView.Voltage) + }, + { + name: HeatMapView.Temperature.toString(), + function: () => this.heatMapService.setCurrentView(this.segment(), HeatMapView.Temperature) + }, + { + name: HeatMapView.Balancing.toString(), + function: () => this.heatMapService.setCurrentView(this.segment(), HeatMapView.Balancing) + } + ]; + + constructor() {} + + ngOnInit(): void { + this.viewSelectorConfig = { options: this.viewOptions, placeholder: 'Voltage' }; + const viewSub = this.heatMapService.getCurrentView(this.segment()); + if (viewSub) { + this.subscriptions.push( + viewSub.subscribe((view) => { + this.viewSelectorConfig = { + ...this.viewSelectorConfig, + defaultValue: view !== undefined ? view : 'Voltage' + }; + }) + ); + } + } + + openSegmentPage = (): void => { + this.router.navigate([appRoutes.bmsSegmentViewRoute(this.segment())]); + }; + + ngOnDestroy(): void { + this.subscriptions.forEach((s) => s.unsubscribe()); + } +} diff --git a/angular-client/src/services/cell.service.ts b/angular-client/src/services/cell.service.ts index 46d7e7f7..a0162fc3 100644 --- a/angular-client/src/services/cell.service.ts +++ b/angular-client/src/services/cell.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { ALPHA_THERM_CELL_MAP, BETA_THERM_CELL_MAP, BMS_CONFIG } from 'src/utils/bms.config'; import { Chip, numToSegmentType, Segment } from 'src/utils/bms.utils'; import Storage from './storage.service'; import { @@ -15,78 +16,39 @@ export type CellReading = { chip: Chip; segment: Segment; temp: number | undefined; - volt1: number | undefined; - volt2: number | undefined; - balancing1: boolean | undefined; - balancing2: boolean | undefined; - cellNumbers: [number, number] | undefined; + voltage: number | undefined; + balancing: boolean | undefined; + cellNumber: number; }; -/* 7 alpha cell reading (for 14 cells) (if we only record data for every other CellReading, or anything like that, adjacents will contain the same data for field) */ -export type AlphaCells = [CellReading, CellReading, CellReading, CellReading, CellReading, CellReading, CellReading]; -export type PerSegmentAlphaCells = [AlphaCells, AlphaCells, AlphaCells, AlphaCells, AlphaCells]; -const createSegmentAlphaCells = (segment: number): AlphaCells => { +const createSegmentCells = (segment: number, chip: Chip, count: number): CellReading[] => { return Array.from( - { length: 7 }, - (): CellReading => ({ - chip: Chip.Alpha, + { length: count }, + (_, i): CellReading => ({ + chip, segment, temp: undefined, - volt1: undefined, - volt2: undefined, - balancing1: undefined, - balancing2: undefined, - cellNumbers: undefined + voltage: undefined, + balancing: undefined, + cellNumber: i }) - ) as AlphaCells; // Type assertion here is safe due to length enforcement + ); }; -const startingPerSegmentAlphaCells: PerSegmentAlphaCells = [ - createSegmentAlphaCells(0), - createSegmentAlphaCells(1), - createSegmentAlphaCells(2), - createSegmentAlphaCells(3), - createSegmentAlphaCells(4) -]; - -/* 11 beta cells (if we only record data for every other CellReading, or anything like that, adjacents will contain the same data for field) */ -// Explicit tuple types -export type BetaCells = [CellReading, CellReading, CellReading, CellReading, CellReading, CellReading]; - -export type PerSegmentBetaCells = [BetaCells, BetaCells, BetaCells, BetaCells, BetaCells]; - -// Utility function to create a BetaCells array for a specific segment -const createSegmentBetaCells = (segment: number): BetaCells => { - return Array.from( - { length: 6 }, - (): CellReading => ({ - chip: Chip.Beta, - segment, - temp: undefined, - volt1: undefined, - volt2: undefined, - balancing1: undefined, - balancing2: undefined, - cellNumbers: undefined - }) - ) as BetaCells; + +const createPerSegmentCells = (chip: Chip, cellsPerSegment: number): CellReading[][] => { + return Array.from({ length: BMS_CONFIG.NUM_SEGMENTS }, (_, seg) => createSegmentCells(seg, chip, cellsPerSegment)); }; -// Create the main structure -const startingPerSegmentBetaCells: PerSegmentBetaCells = [ - createSegmentBetaCells(0), - createSegmentBetaCells(1), - createSegmentBetaCells(2), - createSegmentBetaCells(3), - createSegmentBetaCells(4) -]; +const startingPerSegmentAlphaCells: CellReading[][] = createPerSegmentCells(Chip.Alpha, BMS_CONFIG.ALPHA_VOLT_COUNT); +const startingPerSegmentBetaCells: CellReading[][] = createPerSegmentCells(Chip.Beta, BMS_CONFIG.BETA_VOLT_COUNT); @Injectable({ providedIn: 'root' }) export class CellService { private storageService: Storage; - private perSegmentAlphaCells: PerSegmentAlphaCells; - private perSegmentBetaCells: PerSegmentBetaCells; + private perSegmentAlphaCells: CellReading[][]; + private perSegmentBetaCells: CellReading[][]; constructor(storageService: Storage) { this.storageService = storageService; @@ -102,40 +64,31 @@ export class CellService { private subscribeToAlphaCellInfo = () => { this.perSegmentAlphaCells.map((segmentAlphaCells, index) => { const segmentNumber = numToSegmentType(index); - allAlphaThermValues.forEach((therm, index) => { + + // Therms: apply temperature to cells defined in ALPHA_THERM_CELL_MAP + allAlphaThermValues.forEach((therm, thermIndex) => { this.storageService.get(topics.alphaTemp(segmentNumber, therm)).subscribe((data) => { - const tempBtwnTwoCells = parseFloat(data.values[0]); - const cellIndex = index; - segmentAlphaCells[cellIndex].temp = tempBtwnTwoCells; - segmentAlphaCells[cellIndex].cellNumbers = [cellIndex * 2, cellIndex * 2 + 1]; + const temp = parseFloat(data.values[0]); + const cellIndices = ALPHA_THERM_CELL_MAP[thermIndex] ?? []; + for (const cellIdx of cellIndices) { + if (cellIdx < segmentAlphaCells.length) { + segmentAlphaCells[cellIdx].temp = temp; + } + } }); }); - allAlphaVoltValues.forEach((therm, index) => { - const constIndex = index; - const cellIndex = Math.floor(constIndex / 2); - this.storageService.get(topics.alphaVolt(segmentNumber, therm)).subscribe((data) => { - const voltage = parseFloat(data.values[0]); - if (constIndex % 2 === 0) { - segmentAlphaCells[cellIndex].cellNumbers = [cellIndex * 2, cellIndex * 2 + 1]; - segmentAlphaCells[cellIndex].volt1 = voltage; - } else { - segmentAlphaCells[cellIndex].volt2 = voltage; - } + // Volts: one per cell + allAlphaVoltValues.forEach((volt, voltIndex) => { + this.storageService.get(topics.alphaVolt(segmentNumber, volt)).subscribe((data) => { + segmentAlphaCells[voltIndex].voltage = parseFloat(data.values[0]); }); }); - allAlphaBurnValues.forEach((burn, index) => { - const constIndex = index; - const cellIndex = Math.floor(constIndex / 2); + // Burns: one per cell + allAlphaBurnValues.forEach((burn, burnIndex) => { this.storageService.get(topics.alphaBurning(segmentNumber, burn)).subscribe((data) => { - const balancing = parseInt(data.values[0]) === 1; - segmentAlphaCells[cellIndex].cellNumbers = [cellIndex * 2, cellIndex * 2 + 1]; - if (constIndex % 2 === 0) { - segmentAlphaCells[cellIndex].balancing1 = balancing; - } else { - segmentAlphaCells[cellIndex].balancing2 = balancing; - } + segmentAlphaCells[burnIndex].balancing = parseInt(data.values[0]) === 1; }); }); }); @@ -144,61 +97,49 @@ export class CellService { private subscribeToBetaCellInfo = () => { this.perSegmentBetaCells.map((segmentBetaCells, index) => { const segmentNumber = numToSegmentType(index); - allBetaThermValues.map((therm, index) => { - const constIndex = index; - this.storageService.get(topics.betaTemp(segmentNumber, therm)).subscribe((data) => { - const tempBtwnTwoCells = parseFloat(data.values[0]); - segmentBetaCells[constIndex].cellNumbers = [constIndex * 2, Math.min(constIndex * 2 + 1, 10)]; - segmentBetaCells[constIndex].temp = tempBtwnTwoCells; + // Therms: apply temperature to cells defined in BETA_THERM_CELL_MAP + allBetaThermValues.map((therm, thermIndex) => { + this.storageService.get(topics.betaTemp(segmentNumber, therm)).subscribe((data) => { + const temp = parseFloat(data.values[0]); + const cellIndices = BETA_THERM_CELL_MAP[thermIndex] ?? []; + for (const cellIdx of cellIndices) { + if (cellIdx < segmentBetaCells.length) { + segmentBetaCells[cellIdx].temp = temp; + } + } }); }); - allBetaVoltValues.map((volt, index) => { - const constIndex = index; - const cellIndex = Math.floor(constIndex / 2); + // Volts: one per cell + allBetaVoltValues.map((volt, voltIndex) => { this.storageService.get(topics.betaVolt(segmentNumber, volt)).subscribe((data) => { - const voltage = parseFloat(data.values[0]); - segmentBetaCells[cellIndex].cellNumbers = [cellIndex * 2, Math.min(cellIndex * 2 + 1, 10)]; - if (constIndex % 2 === 0) { - segmentBetaCells[cellIndex].volt1 = voltage; - } else { - segmentBetaCells[cellIndex].volt2 = voltage; - } + segmentBetaCells[voltIndex].voltage = parseFloat(data.values[0]); }); }); - allBetaBurnValues.map((burn, index) => { - const constIndex = index; - const cellIndex = Math.floor(constIndex / 2); + // Burns: one per cell + allBetaBurnValues.map((burn, burnIndex) => { this.storageService.get(topics.betaBurning(segmentNumber, burn)).subscribe((data) => { - const balancing = parseInt(data.values[0]) === 1; - segmentBetaCells[cellIndex].cellNumbers = [cellIndex * 2, Math.min(cellIndex * 2 + 1, 10)]; - if (constIndex % 2 === 0) { - segmentBetaCells[cellIndex].balancing1 = balancing; - } else { - segmentBetaCells[cellIndex].balancing2 = balancing; - } + segmentBetaCells[burnIndex].balancing = parseInt(data.values[0]) === 1; }); }); }); }; - getAllAlphaCells = (): Readonly => { + getAllAlphaCells = (): Readonly => { return this.perSegmentAlphaCells; }; - // 0 2 4 6 8 10 12 - getAlphaCellsBySegment = (segment: number): Readonly => { + getAlphaCellsBySegment = (segment: number): Readonly => { return this.perSegmentAlphaCells[segment]; }; - getAllBetaCells = (): Readonly => { + getAllBetaCells = (): Readonly => { return this.perSegmentBetaCells; }; - // 0 2 4 6 8 10 - getBetaCellsBySegment = (segment: number): Readonly => { + getBetaCellsBySegment = (segment: number): Readonly => { return this.perSegmentBetaCells[segment]; }; } diff --git a/angular-client/src/services/heat-map.service.ts b/angular-client/src/services/heat-map.service.ts index 843bba97..2b7da36b 100644 --- a/angular-client/src/services/heat-map.service.ts +++ b/angular-client/src/services/heat-map.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { CellReading } from './cell.service'; import { Segment } from 'src/utils/bms.utils'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; export enum HeatMapView { Voltage = 'Voltage', @@ -9,26 +10,41 @@ export enum HeatMapView { Balancing = 'Balancing' } +export interface SelectedCellInfo { + reading: CellReading; + cellNum: string; + segment: Segment; +} + @Injectable({ providedIn: 'root' }) export class HeatMapService { - private selectedCellMap: Map> = new Map(); private currentViewMap: Map> = new Map(); - setSelectedCell = (cell: CellReading) => { - if (!this.selectedCellMap.get(cell.segment)) { - this.selectedCellMap.set(cell.segment, new BehaviorSubject(cell)); - } - this.selectedCellMap.get(cell.segment)?.next(cell); - }; + /** Global default view — drives the "Set ALL Maps" selector and initial per-segment defaults */ + readonly globalView$ = new BehaviorSubject(HeatMapView.Voltage); - getSelectedCell = (segment: Segment) => { - if (!this.selectedCellMap.get(segment)) { - this.selectedCellMap.set(segment, new BehaviorSubject(undefined)); + /** Multi-cell selection state */ + selectedCells: SelectedCellInfo[] = []; + dialogRef: DynamicDialogRef | null = null; + + toggleCell(info: SelectedCellInfo): void { + const idx = this.selectedCells.findIndex((s) => s.reading === info.reading); + if (idx >= 0) { + this.selectedCells.splice(idx, 1); + } else { + this.selectedCells.push(info); } - return this.selectedCellMap.get(segment)!; - }; + } + + clearSelection(): void { + this.selectedCells.length = 0; + } + + isCellSelected(reading: CellReading): boolean { + return this.selectedCells.some((s) => s.reading === reading); + } setCurrentView = (segment: Segment, view: HeatMapView) => { if (!this.currentViewMap.get(segment)) { @@ -39,12 +55,13 @@ export class HeatMapService { getCurrentView = (segment: Segment) => { if (!this.currentViewMap.get(segment)) { - this.currentViewMap.set(segment, new BehaviorSubject(HeatMapView.Voltage)); + this.currentViewMap.set(segment, new BehaviorSubject(this.globalView$.value)); } return this.currentViewMap.get(segment); }; setAllSegViews = (view: HeatMapView) => { + this.globalView$.next(view); this.currentViewMap.forEach((subject) => { subject.next(view); }); diff --git a/angular-client/src/styles.css b/angular-client/src/styles.css index 4e34d7d0..d5b856cb 100644 --- a/angular-client/src/styles.css +++ b/angular-client/src/styles.css @@ -125,3 +125,29 @@ body { .p-dialog-mask:has(.nav-options-dialog) { background: transparent !important; } + +/* Cell comparison dialog — compact, no backdrop */ +.cell-compare-dialog.p-dialog { + width: auto !important; + min-width: 0 !important; + border-radius: 12px !important; +} + +.cell-compare-dialog .p-dialog-header { + padding: 8px 14px !important; + border-radius: 12px 12px 0 0 !important; +} + +.cell-compare-dialog .p-dialog-content { + padding: 0 14px 10px !important; + border-radius: 0 0 12px 12px !important; +} + +.p-dialog-mask:has(.cell-compare-dialog) { + background: transparent !important; + pointer-events: none !important; +} + +.cell-compare-dialog { + pointer-events: auto !important; +} diff --git a/angular-client/src/utils/bms.config.ts b/angular-client/src/utils/bms.config.ts new file mode 100644 index 00000000..3c5cc245 --- /dev/null +++ b/angular-client/src/utils/bms.config.ts @@ -0,0 +1,45 @@ +/** + * Central BMS configuration — change counts here to match the current accumulator. + * All segment/cell arrays and topic subscriptions derive from these values. + * + * This file is intentionally free of imports to avoid circular dependency issues + * between bms.utils.ts and topic.utils.ts. + */ +export const BMS_CONFIG = { + NUM_SEGMENTS: 5, + ALPHA_VOLT_COUNT: 13, + BETA_VOLT_COUNT: 13, + ALPHA_THERM_COUNT: 7, + BETA_THERM_COUNT: 7, + ALPHA_BURN_COUNT: 13, + BETA_BURN_COUNT: 13 +} as const; + +/** + * Thermistor-to-cell mapping masks. + * + * Each entry maps one thermistor reading to the cell indices it covers. + * Index in the array corresponds to the thermistor index (0-based), + * and the value is the array of cell indices that receive that temperature. + * + * Example: therm 0 covers cells [0, 1], therm 6 covers only cell [12]. + */ +export const ALPHA_THERM_CELL_MAP: number[][] = [ + [0, 1], // therm 0 + [2, 3], // therm 1 + [4, 5], // therm 2 + [6, 7], // therm 3 + [8, 9], // therm 4 + [10, 11], // therm 5 + [12] // therm 6 — no adjacent cell to share with +]; + +export const BETA_THERM_CELL_MAP: number[][] = [ + [0, 1], // therm 0 + [2, 3], // therm 1 + [4, 5], // therm 2 + [6, 7], // therm 3 + [8, 9], // therm 4 + [10, 11], // therm 5 + [12] // therm 6 — no adjacent cell to share with +]; diff --git a/angular-client/src/utils/bms.utils.ts b/angular-client/src/utils/bms.utils.ts index 44e08fb1..2dc67120 100644 --- a/angular-client/src/utils/bms.utils.ts +++ b/angular-client/src/utils/bms.utils.ts @@ -1,4 +1,6 @@ import { topics } from './topic.utils'; +export { BMS_CONFIG } from './bms.config'; +import { BMS_CONFIG } from './bms.config'; export enum Chip { Alpha = 0, @@ -14,18 +16,15 @@ export const chipToString = (chip: Chip, singleLetter = false): string => { throw new Error('Invalid chip type ' + chip); } }; -export enum Segment { - Segment0 = 0, - Segment1 = 1, - Segment2 = 2, - Segment3 = 3, - Segment4 = 4 -} -export const allSegments = [Segment.Segment0, Segment.Segment1, Segment.Segment2, Segment.Segment3, Segment.Segment4]; + +/** Segment is a plain numeric index (0-based). */ +export type Segment = number; + +export const allSegments: Segment[] = Array.from({ length: BMS_CONFIG.NUM_SEGMENTS }, (_, i) => i); + export const numToSegmentType = (segment: number): Segment => { - const segmentType: Segment | undefined = segment as Segment; - if (segmentType !== undefined) { - return segmentType; + if (segment >= 0 && segment < BMS_CONFIG.NUM_SEGMENTS) { + return segment; } throw new Error('Invalid segment number ' + segment); }; @@ -37,48 +36,18 @@ export type SegmentInfo = { voltageKey: string; }; -export const segment0: SegmentInfo = { - segmentTempKey: topics.segmentTemp(Segment.Segment0), - alphaChipTempKey: topics.dieTemp(Segment.Segment0, Chip.Alpha), - betaChipTempKey: topics.dieTemp(Segment.Segment0, Chip.Beta), - voltageKey: topics.segmentVoltage(Segment.Segment0) -}; - -export const segment1: SegmentInfo = { - segmentTempKey: topics.segmentTemp(Segment.Segment1), - alphaChipTempKey: topics.dieTemp(Segment.Segment1, Chip.Alpha), - betaChipTempKey: topics.dieTemp(Segment.Segment1, Chip.Beta), - voltageKey: topics.segmentVoltage(Segment.Segment1) -}; - -export const segment2: SegmentInfo = { - segmentTempKey: topics.segmentTemp(Segment.Segment2), - alphaChipTempKey: topics.dieTemp(Segment.Segment2, Chip.Alpha), - betaChipTempKey: topics.dieTemp(Segment.Segment2, Chip.Beta), - voltageKey: topics.segmentVoltage(Segment.Segment2) -}; - -export const segment3: SegmentInfo = { - segmentTempKey: topics.segmentTemp(Segment.Segment3), - alphaChipTempKey: topics.dieTemp(Segment.Segment3, Chip.Alpha), - betaChipTempKey: topics.dieTemp(Segment.Segment3, Chip.Beta), - voltageKey: topics.segmentVoltage(Segment.Segment3) -}; - -export const segment4: SegmentInfo = { - segmentTempKey: topics.segmentTemp(Segment.Segment4), - alphaChipTempKey: topics.dieTemp(Segment.Segment4, Chip.Alpha), - betaChipTempKey: topics.dieTemp(Segment.Segment4, Chip.Beta), - voltageKey: topics.segmentVoltage(Segment.Segment4) -}; - -export const segmentInfoMap = { - [Segment.Segment0]: segment0, - [Segment.Segment1]: segment1, - [Segment.Segment2]: segment2, - [Segment.Segment3]: segment3, - [Segment.Segment4]: segment4 -}; +/** Dynamically generated map of segment index → SegmentInfo topic keys. */ +export const segmentInfoMap: Record = Object.fromEntries( + allSegments.map((seg) => [ + seg, + { + segmentTempKey: topics.segmentTemp(seg), + alphaChipTempKey: topics.dieTemp(seg, Chip.Alpha), + betaChipTempKey: topics.dieTemp(seg, Chip.Beta), + voltageKey: topics.segmentVoltage(seg) + } + ]) +); export const getConnectionDotStatusColor = (voltage: number): string => { if (voltage <= 375) { diff --git a/angular-client/src/utils/topic.utils.ts b/angular-client/src/utils/topic.utils.ts index ad68e7f8..3cd6432b 100644 --- a/angular-client/src/utils/topic.utils.ts +++ b/angular-client/src/utils/topic.utils.ts @@ -1,11 +1,12 @@ +import { BMS_CONFIG } from './bms.config'; import { Chip, chipToString, Segment } from './bms.utils'; -export const alphaTemp = (segment: Segment, cell: AlphaThermReading) => `BMS/PerCell/Alpha/${segment}/Therms/${cell}`; -export const betaTemp = (segment: Segment, cell: BetaThermReading) => `BMS/PerCell/Beta/${segment}/Therms/${cell}`; -export const alphaVolt = (segment: Segment, cell: AlphaVoltReading) => `BMS/PerCell/Alpha/${segment}/Volts/${cell}`; -export const betaVolt = (segment: Segment, cell: BetaVoltReading) => `BMS/PerCell/Beta/${segment}/Volts/${cell}`; -export const alphaBurning = (segment: Segment, cell: AlphaBurnReading) => `BMS/PerCell/Alpha/${segment}/Burning/${cell}`; -export const betaBurning = (segment: Segment, cell: BetaBurnReading) => `BMS/PerCell/Beta/${segment}/Burning/${cell}`; +export const alphaTemp = (segment: Segment, cell: number) => `BMS/PerCell/Alpha/${segment}/Therms/${cell}`; +export const betaTemp = (segment: Segment, cell: number) => `BMS/PerCell/Beta/${segment}/Therms/${cell}`; +export const alphaVolt = (segment: Segment, cell: number) => `BMS/PerCell/Alpha/${segment}/Volts/${cell}`; +export const betaVolt = (segment: Segment, cell: number) => `BMS/PerCell/Beta/${segment}/Volts/${cell}`; +export const alphaBurning = (segment: Segment, cell: number) => `BMS/PerCell/Alpha/${segment}/Burning/${cell}`; +export const betaBurning = (segment: Segment, cell: number) => `BMS/PerCell/Beta/${segment}/Burning/${cell}`; export const segmentTemp = (segment: Segment) => `BMS/Segment_Temp/${segment}`; export const segmentVoltage = (segment: Segment) => `BMS/Segment_Volt/${segment}`; export const vref = (segment: Segment, chip: Chip) => `BMS/PerCell/${chipToString(chip)}/${segment}/Vref2`; @@ -71,159 +72,13 @@ export const topics = { msgsPerSecond }; -export const enum AlphaThermReading { - Therm0 = 0 * 2, - Therm1 = 1 * 2, - Therm2 = 2 * 2, - Therm3 = 3 * 2, - Therm4 = 4 * 2, - Therm5 = 5 * 2, - Therm6 = 6 * 2 -} -export const allAlphaThermValues = [ - AlphaThermReading.Therm0, - AlphaThermReading.Therm1, - AlphaThermReading.Therm2, - AlphaThermReading.Therm3, - AlphaThermReading.Therm4, - AlphaThermReading.Therm5, - AlphaThermReading.Therm6 -]; - -export const enum BetaThermReading { - Therm0 = 0 * 2, - Therm1 = 1 * 2, - Therm2 = 2 * 2, - Therm3 = 3 * 2, - Therm4 = 4 * 2, - Therm5 = 5 * 2 -} -export const allBetaThermValues = [ - BetaThermReading.Therm0, - BetaThermReading.Therm1, - BetaThermReading.Therm2, - BetaThermReading.Therm3, - BetaThermReading.Therm4, - BetaThermReading.Therm5 -]; - -export enum BetaVoltReading { - Cell0 = 0, - Cell1 = 1, - Cell2 = 2, - Cell3 = 3, - Cell4 = 4, - Cell5 = 5, - Cell6 = 6, - Cell7 = 7, - Cell8 = 8, - Cell9 = 9, - Cell10 = 10 -} -export const allBetaVoltValues = [ - BetaVoltReading.Cell0, - BetaVoltReading.Cell1, - BetaVoltReading.Cell2, - BetaVoltReading.Cell3, - BetaVoltReading.Cell4, - BetaVoltReading.Cell5, - BetaVoltReading.Cell6, - BetaVoltReading.Cell7, - BetaVoltReading.Cell8, - BetaVoltReading.Cell9, - BetaVoltReading.Cell10 -]; -export enum AlphaVoltReading { - Cell0 = 0, - Cell1 = 1, - Cell2 = 2, - Cell3 = 3, - Cell4 = 4, - Cell5 = 5, - Cell6 = 6, - Cell7 = 7, - Cell8 = 8, - Cell9 = 9, - Cell10 = 10, - Cell11 = 11, - Cell12 = 12, - Cell13 = 13 -} -export const allAlphaVoltValues = [ - AlphaVoltReading.Cell0, - AlphaVoltReading.Cell1, - AlphaVoltReading.Cell2, - AlphaVoltReading.Cell3, - AlphaVoltReading.Cell4, - AlphaVoltReading.Cell5, - AlphaVoltReading.Cell6, - AlphaVoltReading.Cell7, - AlphaVoltReading.Cell8, - AlphaVoltReading.Cell9, - AlphaVoltReading.Cell10, - AlphaVoltReading.Cell11, - AlphaVoltReading.Cell12, - AlphaVoltReading.Cell13 -]; -export enum AlphaBurnReading { - Cell0 = 0, - Cell1 = 1, - Cell2 = 2, - Cell3 = 3, - Cell4 = 4, - Cell5 = 5, - Cell6 = 6, - Cell7 = 7, - Cell8 = 8, - Cell9 = 9, - Cell10 = 10, - Cell11 = 11, - Cell12 = 12, - Cell13 = 13 -} -export const allAlphaBurnValues = [ - AlphaBurnReading.Cell0, - AlphaBurnReading.Cell1, - AlphaBurnReading.Cell2, - AlphaBurnReading.Cell3, - AlphaBurnReading.Cell4, - AlphaBurnReading.Cell5, - AlphaBurnReading.Cell6, - AlphaBurnReading.Cell7, - AlphaBurnReading.Cell8, - AlphaBurnReading.Cell9, - AlphaBurnReading.Cell10, - AlphaBurnReading.Cell11, - AlphaBurnReading.Cell12, - AlphaBurnReading.Cell13 -]; - -export enum BetaBurnReading { - Cell0 = 0, - Cell1 = 1, - Cell2 = 2, - Cell3 = 3, - Cell4 = 4, - Cell5 = 5, - Cell6 = 6, - Cell7 = 7, - Cell8 = 8, - Cell9 = 9, - Cell10 = 10 -} -export const allBetaBurnValues = [ - BetaBurnReading.Cell0, - BetaBurnReading.Cell1, - BetaBurnReading.Cell2, - BetaBurnReading.Cell3, - BetaBurnReading.Cell4, - BetaBurnReading.Cell5, - BetaBurnReading.Cell6, - BetaBurnReading.Cell7, - BetaBurnReading.Cell8, - BetaBurnReading.Cell9, - BetaBurnReading.Cell10 -]; +/* Dynamically generated cell-index arrays derived from BMS_CONFIG */ +export const allAlphaThermValues: number[] = Array.from({ length: BMS_CONFIG.ALPHA_THERM_COUNT }, (_, i) => i * 2); +export const allBetaThermValues: number[] = Array.from({ length: BMS_CONFIG.BETA_THERM_COUNT }, (_, i) => i * 2); +export const allAlphaVoltValues: number[] = Array.from({ length: BMS_CONFIG.ALPHA_VOLT_COUNT }, (_, i) => i); +export const allBetaVoltValues: number[] = Array.from({ length: BMS_CONFIG.BETA_VOLT_COUNT }, (_, i) => i); +export const allAlphaBurnValues: number[] = Array.from({ length: BMS_CONFIG.ALPHA_BURN_COUNT }, (_, i) => i); +export const allBetaBurnValues: number[] = Array.from({ length: BMS_CONFIG.BETA_BURN_COUNT }, (_, i) => i); export enum ChipFault { VA_OV = 'VA_OV',