Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5b9705c
#525 Generalize BMS cell data model for config-driven counts
bracyw Feb 22, 2026
c68574c
#525 Extract BMS_CONFIG to own file to fix circular import
bracyw Feb 22, 2026
e959516
#527 Combine segment summary and heat map into unified rows
bracyw Feb 22, 2026
2ea9599
#527 Extract heatmap and overview components, style controls
bracyw Feb 22, 2026
34fd2a3
#527 Style heatmap cells and segment row controls
bracyw Feb 22, 2026
afc49d9
#527 Fix cell dialog numbering and live update
bracyw Feb 22, 2026
c508468
#527 Make segment row sizing fully dynamic
bracyw Feb 22, 2026
b904a10
#527 Port hex heatmap to segment detail page
bracyw Feb 22, 2026
bb09fff
#527 Widen dynamic sizing for 70-150% zoom range
bracyw Feb 22, 2026
4239d1c
#527 Center overview stats and reduce row height
bracyw Feb 22, 2026
b75ff62
#527 Refactor CellReading to single cell model
bracyw Feb 22, 2026
65bcd5c
#527 Revert BMS config to 26-cell counts
bracyw Feb 22, 2026
33a444e
#527 Remove cell pairing, one tile per CellReading
bracyw Feb 22, 2026
7d26125
#527 Shift hex offset to top row
bracyw Feb 22, 2026
4a2343c
#527 Reduce heatmap panel padding and row height
bracyw Feb 22, 2026
77d3b86
#527 Tighten segment row spacing
bracyw Feb 22, 2026
15c0048
#527 Improve cell selection glow and reduce row height
bracyw Feb 22, 2026
ea29fd7
#527 Add temperature view with merged double-hex tiles
bracyw Feb 22, 2026
0938719
#527 Add multi-cell selection with comparison dialog
bracyw Feb 22, 2026
bf9857c
#527 Add multi-cell selection with comparison dialog
bracyw Feb 22, 2026
91f3497
#527 Add temperature option to segment view selector
bracyw Feb 22, 2026
adb7861
#527 Expand therm group to individual columns in dialog
bracyw Feb 22, 2026
7059adc
#527 Sync row selectors with Set ALL Maps via effect
bracyw Feb 22, 2026
43f5671
#527 Unify default view from service globalView
bracyw Feb 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions angular-client/src/assets/icons/battery.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -19,8 +19,7 @@ export interface DropdownOption {
standalone: true,
imports: [Select, ReactiveFormsModule, FormsModule]
})
export class SelectDropdownComponent implements OnInit {
constructor() {}
export class SelectDropdownComponent {
options = input<DropdownOption[]>([
{
name: 'default',
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@
@if (isMobile) {
<!-- TODO: Add mobile display component -->
} @else {
<!-- TODO: Add components for the segment summary-->
<mat-grid-list cols="6" gutterSize="15px" rowHeight="1.5rem" style="margin-top: -20px">
<mat-grid-tile [colspan]="6" rowspan="3">
<bms-header style="width: 100%" [pageTitle]="this.windowSize < 1200 ? 'ACCU' : 'Accumulator'" />
</mat-grid-tile>
<mat-grid-tile [colspan]="6" rowspan="5">
<bms-at-a-glance style="height: 100%; width: 100%" />
</mat-grid-tile>
<mat-grid-tile [colspan]="windowSize > 1300 ? 1 : 2" [rowspan]="16">
<div style="display: flex; flex-direction: column; width: 100%; height: 100%; gap: 3%">
<acc-high-voltage style="width: 100%; min-height: 31%" />
<acc-low-voltage style="width: 100%; min-height: 31%" />
<acc-high-temp style="width: 100%; min-height: 32%" />
</div>
</mat-grid-tile>
@for (segment of segments; track segment) {
<mat-grid-tile [colspan]="windowSize > 1300 ? 1 : 2" rowspan="16">
<segment-summary [segmentNumber]="segment" style="height: 100%; width: 100%" />
</mat-grid-tile>
}
</mat-grid-list>

<!-- Section header with "Set ALL Maps" dropdown -->
<div class="section-header">
<span class="section-title">Cell-by-Cell Heat Map</span>
<select-dropdown
[options]="allSegSelectorConfig.options"
[placeholder]="allSegSelectorConfig.placeholder"
[defaultValue]="allSegSelectorConfig.defaultValue"
/>
<span class="section-title section-title-right">Segment Overview</span>
</div>

<!-- Unified segment rows -->
<div class="segment-rows">
@for (segment of segments; track segment) {
<mat-grid-tile colspan="6" rowspan="8">
<cell-by-cell-heat-map style="width: 100%; height: 100%" [currentSegment]="segment"></cell-by-cell-heat-map>
</mat-grid-tile>
<segment-row [segment]="segment" />
}
</mat-grid-list>
</div>
}
</div>
62 changes: 50 additions & 12 deletions angular-client/src/pages/bms-debug-page/bms-debug-page.component.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -19,23 +24,56 @@ 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;
windowSize: number = window.innerWidth;
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,16 @@
<segment-at-a-glance style="width: 100%; height: 100%" [segmentNumber]="this.segmentId" />
</mat-grid-tile>
<mat-grid-tile colspan="6" rowspan="8">
<cell-by-cell-heat-map style="width: 100%; height: 100%" [currentSegment]="this.segmentId"></cell-by-cell-heat-map>
<info-background
style="width: 100%; height: 100%"
[title]="this.getHeatmapTitle()"
svgIcon="battery_charging_2"
[slicedLeftCorner]="true"
[selectorConfigs]="[this.currentSegmentSelectorConfig, this.allSegSelectorConfig]"
>
<div style="padding-top: 20px"></div>
<segment-heatmap class="segment-page-heatmap" [segment]="this.segmentId" />
</info-background>
</mat-grid-tile>
<mat-grid-tile colspan="3" rowspan="8">
<!-- Alpha -->
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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() {
Expand All @@ -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());
}
}
Loading