From 5b9705c612971a8149be34142058f532ad489d65 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sat, 21 Feb 2026 22:57:56 -0500 Subject: [PATCH 01/24] #525 Generalize BMS cell data model for config-driven counts --- .../cell-by-cell-heat-map.component.ts | 6 +- angular-client/src/services/cell.service.ts | 75 ++------ angular-client/src/utils/bms.utils.ts | 87 ++++----- angular-client/src/utils/topic.utils.ts | 174 ++---------------- 4 files changed, 70 insertions(+), 272 deletions(-) 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..a3f8be08 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[] = [ diff --git a/angular-client/src/services/cell.service.ts b/angular-client/src/services/cell.service.ts index 46d7e7f7..f7796dbc 100644 --- a/angular-client/src/services/cell.service.ts +++ b/angular-client/src/services/cell.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Chip, numToSegmentType, Segment } from 'src/utils/bms.utils'; +import { BMS_CONFIG, Chip, numToSegmentType, Segment } from 'src/utils/bms.utils'; import Storage from './storage.service'; import { allAlphaBurnValues, @@ -22,14 +22,11 @@ export type CellReading = { cellNumbers: [number, number] | undefined; }; -/* 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 }, + { length: count }, (): CellReading => ({ - chip: Chip.Alpha, + chip, segment, temp: undefined, volt1: undefined, @@ -38,55 +35,23 @@ const createSegmentAlphaCells = (segment: number): AlphaCells => { balancing2: undefined, cellNumbers: undefined }) - ) 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_THERM_COUNT); +const startingPerSegmentBetaCells: CellReading[][] = createPerSegmentCells(Chip.Beta, BMS_CONFIG.BETA_THERM_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; @@ -148,7 +113,7 @@ export class CellService { 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].cellNumbers = [constIndex * 2, Math.min(constIndex * 2 + 1, BMS_CONFIG.BETA_VOLT_COUNT - 1)]; segmentBetaCells[constIndex].temp = tempBtwnTwoCells; }); @@ -159,7 +124,7 @@ export class CellService { const cellIndex = Math.floor(constIndex / 2); 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)]; + segmentBetaCells[cellIndex].cellNumbers = [cellIndex * 2, Math.min(cellIndex * 2 + 1, BMS_CONFIG.BETA_VOLT_COUNT - 1)]; if (constIndex % 2 === 0) { segmentBetaCells[cellIndex].volt1 = voltage; } else { @@ -173,7 +138,7 @@ export class CellService { const cellIndex = Math.floor(constIndex / 2); 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)]; + segmentBetaCells[cellIndex].cellNumbers = [cellIndex * 2, Math.min(cellIndex * 2 + 1, BMS_CONFIG.BETA_VOLT_COUNT - 1)]; if (constIndex % 2 === 0) { segmentBetaCells[cellIndex].balancing1 = balancing; } else { @@ -184,21 +149,19 @@ export class CellService { }); }; - 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/utils/bms.utils.ts b/angular-client/src/utils/bms.utils.ts index 44e08fb1..aa2a83ca 100644 --- a/angular-client/src/utils/bms.utils.ts +++ b/angular-client/src/utils/bms.utils.ts @@ -1,5 +1,19 @@ import { topics } from './topic.utils'; +/** + * Central BMS configuration — change counts here to match the current accumulator. + * All segment/cell arrays and topic subscriptions derive from these values. + */ +export const BMS_CONFIG = { + NUM_SEGMENTS: 5, + ALPHA_VOLT_COUNT: 14, + BETA_VOLT_COUNT: 11, + ALPHA_THERM_COUNT: 7, + BETA_THERM_COUNT: 6, + ALPHA_BURN_COUNT: 14, + BETA_BURN_COUNT: 11 +} as const; + export enum Chip { Alpha = 0, Beta = 1 @@ -14,18 +28,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 +48,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..8829c323 100644 --- a/angular-client/src/utils/topic.utils.ts +++ b/angular-client/src/utils/topic.utils.ts @@ -1,11 +1,11 @@ -import { Chip, chipToString, Segment } from './bms.utils'; +import { BMS_CONFIG, 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 +71,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', From c68574c81a56d7585538cc2e7a791a5f270dec6c Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 00:26:41 -0500 Subject: [PATCH 02/24] #525 Extract BMS_CONFIG to own file to fix circular import --- angular-client/src/services/cell.service.ts | 18 ++++++++++++++---- angular-client/src/utils/bms.config.ts | 16 ++++++++++++++++ angular-client/src/utils/bms.utils.ts | 16 ++-------------- angular-client/src/utils/topic.utils.ts | 3 ++- 4 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 angular-client/src/utils/bms.config.ts diff --git a/angular-client/src/services/cell.service.ts b/angular-client/src/services/cell.service.ts index f7796dbc..43360c04 100644 --- a/angular-client/src/services/cell.service.ts +++ b/angular-client/src/services/cell.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; -import { BMS_CONFIG, Chip, numToSegmentType, Segment } from 'src/utils/bms.utils'; +import { BMS_CONFIG } from 'src/utils/bms.config'; +import { Chip, numToSegmentType, Segment } from 'src/utils/bms.utils'; import Storage from './storage.service'; import { allAlphaBurnValues, @@ -113,7 +114,10 @@ export class CellService { 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, BMS_CONFIG.BETA_VOLT_COUNT - 1)]; + segmentBetaCells[constIndex].cellNumbers = [ + constIndex * 2, + Math.min(constIndex * 2 + 1, BMS_CONFIG.BETA_VOLT_COUNT - 1) + ]; segmentBetaCells[constIndex].temp = tempBtwnTwoCells; }); @@ -124,7 +128,10 @@ export class CellService { const cellIndex = Math.floor(constIndex / 2); 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, BMS_CONFIG.BETA_VOLT_COUNT - 1)]; + segmentBetaCells[cellIndex].cellNumbers = [ + cellIndex * 2, + Math.min(cellIndex * 2 + 1, BMS_CONFIG.BETA_VOLT_COUNT - 1) + ]; if (constIndex % 2 === 0) { segmentBetaCells[cellIndex].volt1 = voltage; } else { @@ -138,7 +145,10 @@ export class CellService { const cellIndex = Math.floor(constIndex / 2); 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, BMS_CONFIG.BETA_VOLT_COUNT - 1)]; + segmentBetaCells[cellIndex].cellNumbers = [ + cellIndex * 2, + Math.min(cellIndex * 2 + 1, BMS_CONFIG.BETA_VOLT_COUNT - 1) + ]; if (constIndex % 2 === 0) { segmentBetaCells[cellIndex].balancing1 = balancing; } else { diff --git a/angular-client/src/utils/bms.config.ts b/angular-client/src/utils/bms.config.ts new file mode 100644 index 00000000..2a37fcc2 --- /dev/null +++ b/angular-client/src/utils/bms.config.ts @@ -0,0 +1,16 @@ +/** + * 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: 14, + BETA_VOLT_COUNT: 11, + ALPHA_THERM_COUNT: 7, + BETA_THERM_COUNT: 6, + ALPHA_BURN_COUNT: 14, + BETA_BURN_COUNT: 11 +} as const; diff --git a/angular-client/src/utils/bms.utils.ts b/angular-client/src/utils/bms.utils.ts index aa2a83ca..2dc67120 100644 --- a/angular-client/src/utils/bms.utils.ts +++ b/angular-client/src/utils/bms.utils.ts @@ -1,18 +1,6 @@ import { topics } from './topic.utils'; - -/** - * Central BMS configuration — change counts here to match the current accumulator. - * All segment/cell arrays and topic subscriptions derive from these values. - */ -export const BMS_CONFIG = { - NUM_SEGMENTS: 5, - ALPHA_VOLT_COUNT: 14, - BETA_VOLT_COUNT: 11, - ALPHA_THERM_COUNT: 7, - BETA_THERM_COUNT: 6, - ALPHA_BURN_COUNT: 14, - BETA_BURN_COUNT: 11 -} as const; +export { BMS_CONFIG } from './bms.config'; +import { BMS_CONFIG } from './bms.config'; export enum Chip { Alpha = 0, diff --git a/angular-client/src/utils/topic.utils.ts b/angular-client/src/utils/topic.utils.ts index 8829c323..3cd6432b 100644 --- a/angular-client/src/utils/topic.utils.ts +++ b/angular-client/src/utils/topic.utils.ts @@ -1,4 +1,5 @@ -import { BMS_CONFIG, Chip, chipToString, Segment } from './bms.utils'; +import { BMS_CONFIG } from './bms.config'; +import { Chip, chipToString, Segment } from './bms.utils'; 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}`; From e959516e3927fe87b18cb5f3fe39eef9c3784dff Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 03:07:45 -0500 Subject: [PATCH 03/24] #527 Combine segment summary and heat map into unified rows --- .../bms-debug-page.component.css | 25 ++ .../bms-debug-page.component.html | 34 +-- .../bms-debug-page.component.ts | 45 +++- .../hex-tile/hex-tile.component.css | 58 +++++ .../hex-tile/hex-tile.component.html | 9 + .../components/hex-tile/hex-tile.component.ts | 33 +++ .../segment-row/segment-row.component.css | 136 +++++++++++ .../segment-row/segment-row.component.html | 64 +++++ .../segment-row/segment-row.component.ts | 224 ++++++++++++++++++ angular-client/src/utils/bms.config.ts | 12 +- 10 files changed, 606 insertions(+), 34 deletions(-) create mode 100644 angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.css create mode 100644 angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.html create mode 100644 angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.ts create mode 100644 angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.css create mode 100644 angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.html create mode 100644 angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.ts 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..55e68834 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: 16px 0 8px; + gap: 12px; +} + +.section-title { + font-family: 'Roboto', sans-serif; + font-weight: 700; + font-size: 18px; + color: #efefef; +} + +.section-title-right { + margin-left: auto; +} + +.segment-rows { + display: flex; + flex-direction: column; + gap: 12px; +} 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..eb88e131 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,17 @@ -import { Component, HostListener } from '@angular/core'; +import { Component, HostListener, inject } from '@angular/core'; 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 +23,13 @@ import { CellByCellHeatMapComponent } from './components/cell-by-cell-heat-map/c MatGridTile, BmsHeaderComponent, BmsAtAGlanceComponent, - AccHighVoltageComponent, - AccLowVoltageComponent, - AccHighTempComponent, - SegmentSummaryComponent, - CellByCellHeatMapComponent + SegmentRowComponent, + SelectDropdownComponent ] }) export class BmsDebugPageComponent { + private heatMapService = inject(HeatMapService); + time = new Date(); newRunIsLoading = false; mobileThreshold = 768; @@ -34,6 +37,26 @@ 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: Temp...' + }; + constructor() {} @HostListener('window:resize', ['$event']) 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..8429038a --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.css @@ -0,0 +1,58 @@ +: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); +} + +.hex-tile.selected { + filter: brightness(1.4) drop-shadow(0 0 4px rgba(255, 255, 255, 0.6)); + 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; +} 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..5c8f2e58 --- /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..85c7db24 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/hex-tile/hex-tile.component.ts @@ -0,0 +1,33 @@ +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 +}) +export class HexTileComponent { + value = input(); + booleanValue = input(); + color = input.required(); + currentView = input(); + boxShadowColor = input(false); + cellNumber = input(); + + 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-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..179f4470 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.css @@ -0,0 +1,136 @@ +/* ── Host: hex sizing custom properties ── */ +:host { + display: block; + /* Pointy-top hex: taller than wide (h ≈ w × 1.155) */ + --hex-w: clamp(28px, 3vw, 48px); + --hex-h: calc(var(--hex-w) * 1.155); + /* Small gap between hexes in a row (can be negative for tighter packing) */ + --hex-gap: 2px; + /* Bottom row shifts right by half a hex width for honeycomb offset */ + --row-offset: calc(var(--hex-w) / 2 + 1px); + /* Rows nestle: overlap by 25% of hex height (pointy-top geometry) */ + --row-gap: calc(var(--hex-h) * -0.25); +} + +/* ── Segment Row ── */ +.segment-row { + display: flex; + align-items: center; + background-color: #2c2c2c; + border-radius: 8px; + padding: 12px 16px; + gap: 16px; + width: 100%; +} + +/* ── Left: Controls ── */ +.controls-zone { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + min-width: 110px; + flex-shrink: 0; +} + +.segment-title { + display: flex; + align-items: center; + gap: 6px; +} + +.battery-icon { + font-size: 16px; +} + +.title-text { + font-family: 'Roboto', sans-serif; + font-weight: 700; + font-size: 16px; + color: #efefef; + white-space: nowrap; +} + +.see-more-btn { + background-color: #2aaeee; + color: #000; + border: none; + border-radius: 16px; + padding: 6px 14px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + white-space: nowrap; +} + +.see-more-btn:hover { + background-color: #1e9ad8; +} + +/* ── Center: Hex Grid ── */ +.hex-grid-zone { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + overflow: hidden; + min-width: 0; +} + +.hex-row { + display: flex; + flex-wrap: nowrap; +} + +/* Gap between hex tiles in a row */ +.hex-row hex-tile { + margin-right: var(--hex-gap); +} + +.hex-row hex-tile:last-child { + margin-right: 0; +} + +/* Bottom row: honeycomb offset right + nestle up */ +.hex-row.bottom-row { + margin-top: var(--row-gap); + margin-left: var(--row-offset); +} + +/* ── Right: Overview Stats ── */ +.overview-zone { + display: flex; + gap: 20px; + flex-shrink: 0; + align-items: center; + padding-left: 16px; + border-left: 1px solid #444; +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; + min-width: 55px; +} + +.stat-value { + font-family: 'Roboto', sans-serif; + font-weight: 700; + font-size: 1.6rem; + color: #efefef; + line-height: 1.1; +} + +.stat-unit { + font-size: 0.55em; + font-weight: 400; + color: #aaa; +} + +.stat-label { + font-family: 'Roboto', sans-serif; + font-size: 0.7rem; + color: #cacaca; + margin-top: 2px; +} 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..5ae3915b --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.html @@ -0,0 +1,64 @@ + +
+ +
+
+ 🔋 + Segment {{ segment() + 1 }} +
+ + +
+ + +
+ +
+ @for (cell of topRowCells; track $index) { + + } +
+ +
+ @for (cell of bottomRowCells; track $index) { + + } +
+
+ + +
+
+ {{ formatTemp() }}°C + Temperature +
+
+ {{ formatVoltage() }}V + Voltage +
+
+ {{ formatChipTemp() }}°C + Chip Temp +
+
+
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..d6347fd3 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.ts @@ -0,0 +1,224 @@ +import { Component, effect, inject, input, OnInit, OnDestroy } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs'; +import { Segment, segmentInfoMap, SegmentInfo } from 'src/utils/bms.utils'; +import { HeatMapService, HeatMapView } from 'src/services/heat-map.service'; +import { CellReading, CellService } from 'src/services/cell.service'; +import { DialogService } from 'primeng/dynamicdialog'; +import { CellViewComponent } from '../cell-view/cell-view.component'; +import { HexTileComponent } from '../hex-tile/hex-tile.component'; +import { + DropdownOption, + SelectorConfig, + SelectDropdownComponent +} from 'src/components/select-dropdown/select-dropdown.component'; +import Storage from 'src/services/storage.service'; +import { appRoutes } from 'src/app/app-routing.module'; + +/** A single hex cell to render in the grid. */ +export interface DisplayCell { + reading: CellReading; + value: number | undefined; + boolValue: boolean | undefined; + cellNum: string; +} + +@Component({ + selector: 'segment-row', + templateUrl: './segment-row.component.html', + styleUrl: './segment-row.component.css', + standalone: true, + imports: [HexTileComponent, SelectDropdownComponent] +}) +export class SegmentRowComponent implements OnInit, OnDestroy { + private cellService = inject(CellService); + private heatMapService = inject(HeatMapService); + private dialogService = inject(DialogService); + private storage = inject(Storage); + private router = inject(Router); + private subscriptions: Subscription[] = []; + + segment = input.required(); + + // Cell data + alphaCells!: Readonly; + betaCells!: Readonly; + + // Heat map view state + view = HeatMapView.Voltage; + selectedCell: CellReading | undefined = undefined; + + // Segment overview stats + temperature!: number; + voltage!: number; + chipTemp!: number; + + // 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.Balancing.toString(), + function: () => this.heatMapService.setCurrentView(this.segment(), HeatMapView.Balancing) + } + ]; + + 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()); + + // Subscribe to view changes + this.viewSelectorConfig = { options: this.viewOptions, placeholder: 'Voltage' }; + const viewSub = this.heatMapService.getCurrentView(this.segment()); + if (viewSub) { + this.subscriptions.push( + viewSub.subscribe((view) => { + this.view = view; + this.viewSelectorConfig = { + ...this.viewSelectorConfig, + defaultValue: view !== undefined ? view : 'Voltage' + }; + }) + ); + } + + // Subscribe to segment overview stats + 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]))) + ); + } + + // --- Display cell builders (1 hex per CellReading) --- + + /** Top row: beta cells reversed — one hex per CellReading. */ + get topRowCells(): DisplayCell[] { + const cells: DisplayCell[] = []; + const reversed = this.betaCells.slice().reverse(); + + for (let i = 0; i < reversed.length; i++) { + const cell = reversed[i]; + cells.push({ + reading: cell, + value: this.getCellValue(cell), + boolValue: this.getCellBoolValue(cell), + cellNum: (reversed.length - 1 - i).toString() + }); + } + return cells; + } + + /** Bottom row: alpha cells — one hex per CellReading. */ + get bottomRowCells(): DisplayCell[] { + const cells: DisplayCell[] = []; + + for (let i = 0; i < this.alphaCells.length; i++) { + const cell = this.alphaCells[i]; + cells.push({ + reading: cell, + value: this.getCellValue(cell), + boolValue: this.getCellBoolValue(cell), + cellNum: i.toString() + }); + } + return cells; + } + + /** Get the numeric value to display for a CellReading based on current view. */ + private getCellValue(cell: CellReading): number | undefined { + if (this.view === HeatMapView.Temperature) return cell.temp; + if (this.view === HeatMapView.Voltage) return this.averageVolt(cell); + return undefined; // Balancing uses boolValue + } + + /** Get the boolean value to display for a CellReading (balancing view). */ + private getCellBoolValue(cell: CellReading): boolean | undefined { + if (this.view !== HeatMapView.Balancing) return undefined; + // Show true if either voltage cell in the pair is balancing + if (cell.balancing1 === undefined && cell.balancing2 === undefined) return undefined; + return !!(cell.balancing1 || cell.balancing2); + } + + /** Average of volt1 and volt2 for a CellReading. */ + private averageVolt(cell: CellReading): number | undefined { + if (cell.volt1 === undefined) return cell.volt2; + if (cell.volt2 === undefined) return cell.volt1; + return (cell.volt1 + cell.volt2) / 2; + } + + // --- Color helpers --- + + 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'; + } + + // --- Interactions --- + + cellClicked(cell: CellReading): void { + this.selectedCell = cell; + this.heatMapService.setSelectedCell(cell); + const ref = this.dialogService.open(CellViewComponent, { + data: { forSegment: this.segment() }, + width: '40%', + draggable: true, + closable: true, + closeAriaLabel: 'Close' + }); + ref.onClose.subscribe(() => (this.selectedCell = undefined)); + } + + isSelected(cell: CellReading): boolean { + return this.selectedCell === cell; + } + + openSegmentPage = (): void => { + this.router.navigate([appRoutes.bmsSegmentViewRoute(this.segment())]); + }; + + 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/utils/bms.config.ts b/angular-client/src/utils/bms.config.ts index 2a37fcc2..ae1c1640 100644 --- a/angular-client/src/utils/bms.config.ts +++ b/angular-client/src/utils/bms.config.ts @@ -7,10 +7,10 @@ */ export const BMS_CONFIG = { NUM_SEGMENTS: 5, - ALPHA_VOLT_COUNT: 14, - BETA_VOLT_COUNT: 11, - ALPHA_THERM_COUNT: 7, - BETA_THERM_COUNT: 6, - ALPHA_BURN_COUNT: 14, - BETA_BURN_COUNT: 11 + ALPHA_VOLT_COUNT: 26, + BETA_VOLT_COUNT: 26, + ALPHA_THERM_COUNT: 13, + BETA_THERM_COUNT: 13, + ALPHA_BURN_COUNT: 26, + BETA_BURN_COUNT: 26 } as const; From 2ea95994463f1e0f7cf6a7dddbe5b28e5b529ddd Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 03:38:48 -0500 Subject: [PATCH 04/24] #527 Extract heatmap and overview components, style controls --- .../hex-tile/hex-tile.component.css | 84 +++++++++ .../hex-tile/hex-tile.component.html | 2 +- .../components/hex-tile/hex-tile.component.ts | 15 +- .../segment-heatmap.component.css | 37 ++++ .../segment-heatmap.component.html | 30 ++++ .../segment-heatmap.component.ts | 150 ++++++++++++++++ .../segment-overview.component.css | 46 +++++ .../segment-overview.component.html | 14 ++ .../segment-overview.component.ts | 45 +++++ .../segment-row/segment-row.component.css | 170 +++++++++--------- .../segment-row/segment-row.component.html | 67 ++----- .../segment-row/segment-row.component.ts | 170 +----------------- 12 files changed, 530 insertions(+), 300 deletions(-) create mode 100644 angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.css create mode 100644 angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.html create mode 100644 angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.ts create mode 100644 angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.css create mode 100644 angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.html create mode 100644 angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.ts 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 index 8429038a..316d961e 100644 --- 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 @@ -56,3 +56,87 @@ 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: 18% 0 0 20%; + 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; +} + +:host(.heatmap-cell) .cell-value-container { + gap: 1px; +} + +:host(.heatmap-cell) .cell-value { + font-size: calc(var(--hex-w, 48px) * 0.24); + font-weight: 700; + color: #1a1a1a; + line-height: 1.1; +} + +:host(.heatmap-cell) .cell-unit { + font-size: calc(var(--hex-w, 48px) * 0.16); + 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 index 5c8f2e58..78375ff0 100644 --- 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 @@ -1,4 +1,4 @@ -
+
{{ cellNumber() }} {{ displayValue() }} 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 index 85c7db24..a5e79e4a 100644 --- 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 @@ -5,9 +5,13 @@ import { HeatMapView } from 'src/services/heat-map.service'; selector: 'hex-tile', templateUrl: './hex-tile.component.html', styleUrl: './hex-tile.component.css', - standalone: true + standalone: true, + host: { + '[class]': 'variant()' + } }) export class HexTileComponent { + variant = input(''); value = input(); booleanValue = input(); color = input.required(); @@ -15,6 +19,15 @@ export class HexTileComponent { 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'; 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..e06ace5a --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.css @@ -0,0 +1,37 @@ +:host { + display: block; + --hex-w: clamp(42px, 3.6vw, 54px); + --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: hidden; + 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.bottom-row { + margin-top: var(--row-gap); + margin-left: var(--row-offset); +} 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..ac546cbe --- /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..14937996 --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.ts @@ -0,0 +1,150 @@ +import { Component, effect, inject, input, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { Segment } from 'src/utils/bms.utils'; +import { HeatMapService, HeatMapView } from 'src/services/heat-map.service'; +import { CellReading, CellService } from 'src/services/cell.service'; +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; +} + +@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; + selectedCell: CellReading | undefined = undefined; + + 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; + }) + ); + } + } + + get topRowCells(): DisplayCell[] { + const cells: DisplayCell[] = []; + const reversed = this.betaCells.slice().reverse(); + + for (let i = 0; i < reversed.length; i++) { + const cell = reversed[i]; + cells.push({ + reading: cell, + value: this.getCellValue(cell), + boolValue: this.getCellBoolValue(cell), + cellNum: (reversed.length - 1 - i).toString() + }); + } + return cells; + } + + get bottomRowCells(): DisplayCell[] { + const cells: DisplayCell[] = []; + + for (let i = 0; i < this.alphaCells.length; i++) { + const cell = this.alphaCells[i]; + cells.push({ + reading: cell, + value: this.getCellValue(cell), + boolValue: this.getCellBoolValue(cell), + cellNum: i.toString() + }); + } + return cells; + } + + private getCellValue(cell: CellReading): number | undefined { + if (this.view === HeatMapView.Temperature) return cell.temp; + if (this.view === HeatMapView.Voltage) return this.averageVolt(cell); + return undefined; + } + + private getCellBoolValue(cell: CellReading): boolean | undefined { + if (this.view !== HeatMapView.Balancing) return undefined; + if (cell.balancing1 === undefined && cell.balancing2 === undefined) return undefined; + return !!(cell.balancing1 || cell.balancing2); + } + + private averageVolt(cell: CellReading): number | undefined { + if (cell.volt1 === undefined) return cell.volt2; + if (cell.volt2 === undefined) return cell.volt1; + return (cell.volt1 + cell.volt2) / 2; + } + + 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(cell: CellReading): void { + this.selectedCell = cell; + this.heatMapService.setSelectedCell(cell); + const ref = this.dialogService.open(CellViewComponent, { + data: { forSegment: this.segment() }, + width: '40%', + draggable: true, + closable: true, + closeAriaLabel: 'Close' + }); + ref.onClose.subscribe(() => (this.selectedCell = undefined)); + } + + isSelected(cell: CellReading): boolean { + return this.selectedCell === 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..f7bc1b3d --- /dev/null +++ b/angular-client/src/pages/bms-debug-page/components/segment-overview/segment-overview.component.css @@ -0,0 +1,46 @@ +:host { + display: block; + height: 100%; +} + +.overview-zone { + display: flex; + gap: 0; + flex-shrink: 0; + align-items: center; + height: 100%; +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-width: 72px; + padding: 0 14px; +} + +.stat + .stat { + border-left: 1px solid #4a4a4a; +} + +.stat-value { + font-family: 'Roboto', sans-serif; + font-weight: 700; + font-size: 1.6rem; + color: #efefef; + line-height: 1.1; +} + +.stat-unit { + font-size: 0.55em; + font-weight: 400; + color: #aaa; +} + +.stat-label { + font-family: 'Roboto', sans-serif; + font-size: 0.7rem; + color: #cacaca; + margin-top: 2px; +} 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 index 179f4470..2e6a13b2 100644 --- 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 @@ -1,36 +1,64 @@ -/* ── Host: hex sizing custom properties ── */ :host { display: block; - /* Pointy-top hex: taller than wide (h ≈ w × 1.155) */ - --hex-w: clamp(28px, 3vw, 48px); - --hex-h: calc(var(--hex-w) * 1.155); - /* Small gap between hexes in a row (can be negative for tighter packing) */ - --hex-gap: 2px; - /* Bottom row shifts right by half a hex width for honeycomb offset */ - --row-offset: calc(var(--hex-w) / 2 + 1px); - /* Rows nestle: overlap by 25% of hex height (pointy-top geometry) */ - --row-gap: calc(var(--hex-h) * -0.25); } /* ── Segment Row ── */ .segment-row { display: flex; - align-items: center; - background-color: #2c2c2c; + align-items: stretch; border-radius: 8px; - padding: 12px 16px; - gap: 16px; + gap: 12px; width: 100%; + background-color: #1a1a1a; + padding: 10px; } -/* ── Left: Controls ── */ +/* ── Left: Controls (inside dark wrapper) ── */ .controls-zone { display: flex; flex-direction: column; align-items: flex-start; + justify-content: center; gap: 8px; - min-width: 110px; + min-width: 128px; flex-shrink: 0; + padding: 8px 8px; +} + +/* ── 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: 0 1 auto; + display: flex; + align-items: center; + padding: 10px 12px 10px 100px; + background-color: #2c2c2c; + border-radius: 8px; + clip-path: polygon(88px 0, 100% 0, 100% 100%, 0 100%, 0 88px); + width: fit-content; + min-width: fit-content; + min-height: 112px; +} + +.overview-panel { + display: flex; + align-items: center; + padding: 10px 16px; + background-color: #2c2c2c; + border-radius: 8px; + min-width: 264px; + min-height: 112px; } .segment-title { @@ -51,86 +79,62 @@ white-space: nowrap; } +/* ── Actions group: dropdown + button same width ── */ +.controls-actions { + display: flex; + flex-direction: column; + gap: 6px; + width: 120px; +} + .see-more-btn { background-color: #2aaeee; color: #000; border: none; border-radius: 16px; - padding: 6px 14px; + padding: 6px 18px; font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap; + width: 100%; + text-align: center; + height: 30px; + box-sizing: border-box; } .see-more-btn:hover { background-color: #1e9ad8; } -/* ── Center: Hex Grid ── */ -.hex-grid-zone { - flex: 1; - display: flex; - flex-direction: column; - align-items: flex-start; - overflow: hidden; - min-width: 0; -} - -.hex-row { - display: flex; - flex-wrap: nowrap; -} - -/* Gap between hex tiles in a row */ -.hex-row hex-tile { - margin-right: var(--hex-gap); -} - -.hex-row hex-tile:last-child { - margin-right: 0; -} - -/* Bottom row: honeycomb offset right + nestle up */ -.hex-row.bottom-row { - margin-top: var(--row-gap); - margin-left: var(--row-offset); -} - -/* ── Right: Overview Stats ── */ -.overview-zone { - display: flex; - gap: 20px; - flex-shrink: 0; - align-items: center; - padding-left: 16px; - border-left: 1px solid #444; -} - -.stat { - display: flex; - flex-direction: column; - align-items: center; - min-width: 55px; -} - -.stat-value { - font-family: 'Roboto', sans-serif; - font-weight: 700; - font-size: 1.6rem; - color: #efefef; - line-height: 1.1; -} - -.stat-unit { - font-size: 0.55em; - font-weight: 400; - color: #aaa; -} - -.stat-label { - font-family: 'Roboto', sans-serif; - font-size: 0.7rem; - color: #cacaca; - margin-top: 2px; +/* ── Dropdown overrides inside controls-zone ── */ +.controls-zone ::ng-deep .small-grey-select { + width: 100% !important; + min-width: 0 !important; + height: 30px !important; + border-radius: 16px !important; + background-color: #555 !important; + font-size: 12px !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 12px !important; + font-size: 12px !important; + font-weight: 600 !important; + color: #c0c0c0 !important; + --p-select-placeholder-color: #c0c0c0 !important; + line-height: 30px !important; + display: flex !important; + align-items: center !important; +} + +.controls-zone ::ng-deep .p-select-trigger { + padding-right: 8px !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 index 5ae3915b..fe0fb246 100644 --- 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 @@ -1,64 +1,29 @@ - +
- +
🔋 Segment {{ segment() + 1 }}
- - -
- - -
- -
- @for (cell of topRowCells; track $index) { - - } -
- -
- @for (cell of bottomRowCells; track $index) { - - } +
+ +
- -
-
- {{ formatTemp() }}°C - Temperature + +
+
+
-
- {{ formatVoltage() }}V - Voltage -
-
- {{ formatChipTemp() }}°C - Chip Temp + +
+
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 index d6347fd3..a5243afe 100644 --- 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 @@ -1,58 +1,31 @@ -import { Component, effect, inject, input, OnInit, OnDestroy } from '@angular/core'; +import { Component, inject, input, OnInit, OnDestroy } from '@angular/core'; import { Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { Segment, segmentInfoMap, SegmentInfo } from 'src/utils/bms.utils'; +import { Segment } from 'src/utils/bms.utils'; import { HeatMapService, HeatMapView } from 'src/services/heat-map.service'; -import { CellReading, CellService } from 'src/services/cell.service'; -import { DialogService } from 'primeng/dynamicdialog'; -import { CellViewComponent } from '../cell-view/cell-view.component'; -import { HexTileComponent } from '../hex-tile/hex-tile.component'; import { DropdownOption, SelectorConfig, SelectDropdownComponent } from 'src/components/select-dropdown/select-dropdown.component'; -import Storage from 'src/services/storage.service'; import { appRoutes } from 'src/app/app-routing.module'; - -/** A single hex cell to render in the grid. */ -export interface DisplayCell { - reading: CellReading; - value: number | undefined; - boolValue: boolean | undefined; - cellNum: string; -} +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: [HexTileComponent, SelectDropdownComponent] + imports: [SelectDropdownComponent, SegmentHeatmapComponent, SegmentOverviewComponent] }) export class SegmentRowComponent implements OnInit, OnDestroy { - private cellService = inject(CellService); private heatMapService = inject(HeatMapService); - private dialogService = inject(DialogService); - private storage = inject(Storage); private router = inject(Router); private subscriptions: Subscription[] = []; segment = input.required(); - // Cell data - alphaCells!: Readonly; - betaCells!: Readonly; - - // Heat map view state - view = HeatMapView.Voltage; - selectedCell: CellReading | undefined = undefined; - - // Segment overview stats - temperature!: number; - voltage!: number; - chipTemp!: number; - // View selector config (Voltage and Balancing only per ticket) viewSelectorConfig!: SelectorConfig; private viewOptions: DropdownOption[] = [ @@ -66,24 +39,14 @@ export class SegmentRowComponent implements OnInit, OnDestroy { } ]; - constructor() { - effect(() => { - this.alphaCells = this.cellService.getAlphaCellsBySegment(this.segment()); - this.betaCells = this.cellService.getBetaCellsBySegment(this.segment()); - }); - } + constructor() {} ngOnInit(): void { - this.alphaCells = this.cellService.getAlphaCellsBySegment(this.segment()); - this.betaCells = this.cellService.getBetaCellsBySegment(this.segment()); - - // Subscribe to view changes this.viewSelectorConfig = { options: this.viewOptions, placeholder: 'Voltage' }; const viewSub = this.heatMapService.getCurrentView(this.segment()); if (viewSub) { this.subscriptions.push( viewSub.subscribe((view) => { - this.view = view; this.viewSelectorConfig = { ...this.viewSelectorConfig, defaultValue: view !== undefined ? view : 'Voltage' @@ -91,133 +54,12 @@ export class SegmentRowComponent implements OnInit, OnDestroy { }) ); } - - // Subscribe to segment overview stats - 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]))) - ); - } - - // --- Display cell builders (1 hex per CellReading) --- - - /** Top row: beta cells reversed — one hex per CellReading. */ - get topRowCells(): DisplayCell[] { - const cells: DisplayCell[] = []; - const reversed = this.betaCells.slice().reverse(); - - for (let i = 0; i < reversed.length; i++) { - const cell = reversed[i]; - cells.push({ - reading: cell, - value: this.getCellValue(cell), - boolValue: this.getCellBoolValue(cell), - cellNum: (reversed.length - 1 - i).toString() - }); - } - return cells; - } - - /** Bottom row: alpha cells — one hex per CellReading. */ - get bottomRowCells(): DisplayCell[] { - const cells: DisplayCell[] = []; - - for (let i = 0; i < this.alphaCells.length; i++) { - const cell = this.alphaCells[i]; - cells.push({ - reading: cell, - value: this.getCellValue(cell), - boolValue: this.getCellBoolValue(cell), - cellNum: i.toString() - }); - } - return cells; - } - - /** Get the numeric value to display for a CellReading based on current view. */ - private getCellValue(cell: CellReading): number | undefined { - if (this.view === HeatMapView.Temperature) return cell.temp; - if (this.view === HeatMapView.Voltage) return this.averageVolt(cell); - return undefined; // Balancing uses boolValue - } - - /** Get the boolean value to display for a CellReading (balancing view). */ - private getCellBoolValue(cell: CellReading): boolean | undefined { - if (this.view !== HeatMapView.Balancing) return undefined; - // Show true if either voltage cell in the pair is balancing - if (cell.balancing1 === undefined && cell.balancing2 === undefined) return undefined; - return !!(cell.balancing1 || cell.balancing2); - } - - /** Average of volt1 and volt2 for a CellReading. */ - private averageVolt(cell: CellReading): number | undefined { - if (cell.volt1 === undefined) return cell.volt2; - if (cell.volt2 === undefined) return cell.volt1; - return (cell.volt1 + cell.volt2) / 2; - } - - // --- Color helpers --- - - 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'; - } - - // --- Interactions --- - - cellClicked(cell: CellReading): void { - this.selectedCell = cell; - this.heatMapService.setSelectedCell(cell); - const ref = this.dialogService.open(CellViewComponent, { - data: { forSegment: this.segment() }, - width: '40%', - draggable: true, - closable: true, - closeAriaLabel: 'Close' - }); - ref.onClose.subscribe(() => (this.selectedCell = undefined)); - } - - isSelected(cell: CellReading): boolean { - return this.selectedCell === cell; } openSegmentPage = (): void => { this.router.navigate([appRoutes.bmsSegmentViewRoute(this.segment())]); }; - 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()); } From 34fd2a32d87e54ba3089df123ac7384797d4161e Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 04:05:23 -0500 Subject: [PATCH 05/24] #527 Style heatmap cells and segment row controls --- angular-client/src/assets/icons/battery.svg | 3 +++ .../components/hex-tile/hex-tile.component.css | 9 +++++---- .../segment-heatmap/segment-heatmap.component.css | 2 +- .../segment-overview/segment-overview.component.css | 10 +++++----- .../components/segment-row/segment-row.component.css | 7 ++++--- .../components/segment-row/segment-row.component.html | 2 +- 6 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 angular-client/src/assets/icons/battery.svg 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/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 index 316d961e..09fc3a2f 100644 --- 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 @@ -111,7 +111,7 @@ height: var(--hex-h, 48px); align-items: flex-start; justify-content: center; - padding: 18% 0 0 20%; + padding: 6% 0 0 8%; flex-direction: column; gap: 0; } @@ -123,6 +123,7 @@ color: #1a1a1a; opacity: 0.6; line-height: 1; + margin-bottom: 1px; } :host(.heatmap-cell) .cell-value-container { @@ -130,13 +131,13 @@ } :host(.heatmap-cell) .cell-value { - font-size: calc(var(--hex-w, 48px) * 0.24); + font-size: calc(var(--hex-w, 48px) * 0.36); font-weight: 700; color: #1a1a1a; - line-height: 1.1; + line-height: 1; } :host(.heatmap-cell) .cell-unit { - font-size: calc(var(--hex-w, 48px) * 0.16); + font-size: calc(var(--hex-w, 48px) * 0.2); color: #1a1a1a; } 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 index e06ace5a..edbb3147 100644 --- 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 @@ -1,6 +1,6 @@ :host { display: block; - --hex-w: clamp(42px, 3.6vw, 54px); + --hex-w: clamp(44px, 4.1vw, 60px); --hex-h: calc(var(--hex-w) * 1.155); --hex-gap: 2px; --row-offset: calc(var(--hex-w) / 2 + 2px); 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 index f7bc1b3d..aa68ac87 100644 --- 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 @@ -17,7 +17,7 @@ align-items: center; justify-content: center; min-width: 72px; - padding: 0 14px; + padding: 0 clamp(12px, 1.4vw, 22px); } .stat + .stat { @@ -27,20 +27,20 @@ .stat-value { font-family: 'Roboto', sans-serif; font-weight: 700; - font-size: 1.6rem; + font-size: clamp(1.7rem, 2.6vw, 2.8rem); color: #efefef; line-height: 1.1; } .stat-unit { - font-size: 0.55em; + font-size: 0.45em; font-weight: 400; color: #aaa; } .stat-label { font-family: 'Roboto', sans-serif; - font-size: 0.7rem; + font-size: clamp(0.65rem, 0.8vw, 0.85rem); color: #cacaca; - margin-top: 2px; + margin-top: 4px; } 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 index 2e6a13b2..2f12463b 100644 --- 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 @@ -18,7 +18,7 @@ display: flex; flex-direction: column; align-items: flex-start; - justify-content: center; + justify-content: flex-start; gap: 8px; min-width: 128px; flex-shrink: 0; @@ -68,13 +68,14 @@ } .battery-icon { - font-size: 16px; + width: 13px; + height: 17px; } .title-text { font-family: 'Roboto', sans-serif; font-weight: 700; - font-size: 16px; + font-size: 20px; color: #efefef; white-space: nowrap; } 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 index fe0fb246..ce483cfa 100644 --- 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 @@ -3,7 +3,7 @@
- 🔋 + battery Segment {{ segment() + 1 }}
From afc49d9f689c8f3db7ff4c6dc46d781bc0f5c172 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 04:24:14 -0500 Subject: [PATCH 06/24] #527 Fix cell dialog numbering and live update --- .../cell-view/cell-view.component.html | 72 ++++++------------- .../cell-view/cell-view.component.ts | 38 ++++++++-- .../hex-tile/hex-tile.component.css | 2 +- .../segment-heatmap.component.html | 4 +- .../segment-heatmap.component.ts | 4 +- 5 files changed, 61 insertions(+), 59 deletions(-) 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..0cf92386 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 @@ - - - - - - - - - - - - - + + + + + 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..86cfd8e6 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,4 +1,4 @@ -import { Component, HostListener, inject, input } from '@angular/core'; +import { ChangeDetectorRef, Component, HostListener, inject, input, OnDestroy } from '@angular/core'; import { DynamicDialogConfig } from 'primeng/dynamicdialog'; import { CellReading } from 'src/services/cell.service'; import { HeatMapService } from 'src/services/heat-map.service'; @@ -6,23 +6,24 @@ 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'; @Component({ selector: 'cell-view', templateUrl: './cell-view.component.html', styleUrl: './cell-view.component.css', standalone: true, - imports: [InfoBackgroundComponent, InfoValueDisplayComponent, DividerComponent, HStackComponent, VStackComponent] + imports: [InfoBackgroundComponent, InfoValueDisplayComponent, HStackComponent] }) -export class CellViewComponent { +export class CellViewComponent implements OnDestroy { private heatMapService = inject(HeatMapService); + private cdr = inject(ChangeDetectorRef); + private refreshInterval: ReturnType | undefined; cellViewData: CellReading | undefined = undefined; screenWidth = window.innerWidth; forSegment = input.required(); segment: Segment; + displayCellIndex: number | undefined; public config = inject(DynamicDialogConfig); // Update view width @@ -33,9 +34,20 @@ export class CellViewComponent { constructor() { this.segment = this.config.data.forSegment; + this.displayCellIndex = + this.config.data.displayCellIndex !== undefined ? parseInt(this.config.data.displayCellIndex, 10) : undefined; this.heatMapService.getSelectedCell(this.segment)?.subscribe((data) => { this.cellViewData = data; }); + // CellReading properties are mutated in-place by CellService as MQTT data arrives, + // so we poll for changes to keep the dialog values up to date. + this.refreshInterval = setInterval(() => this.cdr.detectChanges(), 500); + } + + ngOnDestroy(): void { + if (this.refreshInterval) { + clearInterval(this.refreshInterval); + } } getTitle = (): string => { @@ -75,4 +87,20 @@ export class CellViewComponent { const balancingLabel = this.screenWidth <= 1100 ? `Bal.?` : `Balancing?`; return balancingLabel; }; + + getAverageVoltage(): number | undefined { + const v1 = this.cellViewData?.volt1; + const v2 = this.cellViewData?.volt2; + if (v1 === undefined && v2 === undefined) return undefined; + if (v1 === undefined) return v2; + if (v2 === undefined) return v1; + return (v1 + v2) / 2; + } + + getBalancing(): boolean | undefined { + const b1 = this.cellViewData?.balancing1; + const b2 = this.cellViewData?.balancing2; + if (b1 === undefined && b2 === undefined) return undefined; + return !!(b1 || b2); + } } 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 index 09fc3a2f..9fd9da57 100644 --- 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 @@ -111,7 +111,7 @@ height: var(--hex-h, 48px); align-items: flex-start; justify-content: center; - padding: 6% 0 0 8%; + padding: 0% 0 0 8%; flex-direction: column; gap: 0; } 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 index ac546cbe..1801c710 100644 --- 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 @@ -9,7 +9,7 @@ [currentView]="view" [boxShadowColor]="isSelected(cell.reading)" [cellNumber]="cell.cellNum" - (click)="cellClicked(cell.reading)" + (click)="cellClicked(cell.reading, cell.cellNum)" /> }
@@ -23,7 +23,7 @@ [currentView]="view" [boxShadowColor]="isSelected(cell.reading)" [cellNumber]="cell.cellNum" - (click)="cellClicked(cell.reading)" + (click)="cellClicked(cell.reading, cell.cellNum)" /> }
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 index 14937996..36ded04f 100644 --- 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 @@ -127,11 +127,11 @@ export class SegmentHeatmapComponent implements OnInit, OnDestroy { return value ? '#4169e1' : 'yellow'; } - cellClicked(cell: CellReading): void { + cellClicked(cell: CellReading, displayIndex: string): void { this.selectedCell = cell; this.heatMapService.setSelectedCell(cell); const ref = this.dialogService.open(CellViewComponent, { - data: { forSegment: this.segment() }, + data: { forSegment: this.segment(), displayCellIndex: displayIndex }, width: '40%', draggable: true, closable: true, From c508468010e1bc6cb2907f2bc124e0f9cb814673 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 04:31:00 -0500 Subject: [PATCH 07/24] #527 Make segment row sizing fully dynamic --- .../bms-debug-page.component.css | 2 +- .../segment-overview.component.css | 15 +++-- .../segment-row/segment-row.component.css | 60 +++++++++---------- 3 files changed, 41 insertions(+), 36 deletions(-) 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 55e68834..210dfdae 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 @@ -23,7 +23,7 @@ mat-grid-tile { .section-title { font-family: 'Roboto', sans-serif; font-weight: 700; - font-size: 18px; + font-size: 34px; color: #efefef; } 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 index aa68ac87..4cbf7654 100644 --- 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 @@ -6,9 +6,10 @@ .overview-zone { display: flex; gap: 0; - flex-shrink: 0; + flex-shrink: 1; align-items: center; height: 100%; + overflow: hidden; } .stat { @@ -16,8 +17,10 @@ flex-direction: column; align-items: center; justify-content: center; - min-width: 72px; - padding: 0 clamp(12px, 1.4vw, 22px); + min-width: 0; + flex: 1 1 0; + padding: 0 clamp(8px, 1vw, 18px); + overflow: hidden; } .stat + .stat { @@ -27,7 +30,8 @@ .stat-value { font-family: 'Roboto', sans-serif; font-weight: 700; - font-size: clamp(1.7rem, 2.6vw, 2.8rem); + font-size: clamp(1.4rem, 2.2vw, 2.4rem); + white-space: nowrap; color: #efefef; line-height: 1.1; } @@ -40,7 +44,8 @@ .stat-label { font-family: 'Roboto', sans-serif; - font-size: clamp(0.65rem, 0.8vw, 0.85rem); + font-size: clamp(0.6rem, 0.75vw, 0.8rem); + white-space: nowrap; color: #cacaca; margin-top: 4px; } 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 index 2f12463b..d1ab27fa 100644 --- 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 @@ -6,11 +6,11 @@ .segment-row { display: flex; align-items: stretch; - border-radius: 8px; - gap: 12px; + border-radius: clamp(6px, 0.63vw, 12px); + gap: clamp(8px, 0.94vw, 16px); width: 100%; background-color: #1a1a1a; - padding: 10px; + padding: clamp(6px, 0.78vw, 14px); } /* ── Left: Controls (inside dark wrapper) ── */ @@ -19,10 +19,10 @@ flex-direction: column; align-items: flex-start; justify-content: flex-start; - gap: 8px; - min-width: 128px; + gap: clamp(6px, 0.63vw, 10px); + min-width: clamp(100px, 10vw, 160px); flex-shrink: 0; - padding: 8px 8px; + padding: clamp(6px, 0.63vw, 10px); } /* ── Right: Separate light panels ── */ @@ -42,23 +42,23 @@ flex: 0 1 auto; display: flex; align-items: center; - padding: 10px 12px 10px 100px; + padding: clamp(6px, 0.78vw, 14px) clamp(8px, 0.94vw, 16px) clamp(6px, 0.78vw, 14px) clamp(70px, 7.8vw, 120px); background-color: #2c2c2c; - border-radius: 8px; - clip-path: polygon(88px 0, 100% 0, 100% 100%, 0 100%, 0 88px); + border-radius: clamp(6px, 0.63vw, 12px); + clip-path: polygon(clamp(60px, 6.9vw, 108px) 0, 100% 0, 100% 100%, 0 100%, 0 clamp(60px, 6.9vw, 108px)); width: fit-content; min-width: fit-content; - min-height: 112px; + min-height: clamp(90px, 8.75vw, 140px); } .overview-panel { display: flex; align-items: center; - padding: 10px 16px; + padding: clamp(6px, 0.78vw, 14px) clamp(10px, 1.25vw, 20px); background-color: #2c2c2c; - border-radius: 8px; - min-width: 264px; - min-height: 112px; + border-radius: clamp(6px, 0.63vw, 12px); + min-width: clamp(180px, 18vw, 300px); + min-height: clamp(90px, 8.75vw, 140px); } .segment-title { @@ -68,14 +68,14 @@ } .battery-icon { - width: 13px; - height: 17px; + width: clamp(10px, 1vw, 16px); + height: clamp(13px, 1.33vw, 21px); } .title-text { font-family: 'Roboto', sans-serif; font-weight: 700; - font-size: 20px; + font-size: clamp(16px, 1.56vw, 24px); color: #efefef; white-space: nowrap; } @@ -84,23 +84,23 @@ .controls-actions { display: flex; flex-direction: column; - gap: 6px; - width: 120px; + gap: clamp(4px, 0.47vw, 8px); + width: clamp(96px, 9.4vw, 150px); } .see-more-btn { background-color: #2aaeee; color: #000; border: none; - border-radius: 16px; - padding: 6px 18px; - font-size: 12px; + border-radius: clamp(12px, 1.25vw, 20px); + padding: clamp(4px, 0.47vw, 8px) clamp(12px, 1.4vw, 22px); + font-size: clamp(10px, 0.94vw, 14px); font-weight: 600; cursor: pointer; white-space: nowrap; width: 100%; text-align: center; - height: 30px; + height: clamp(26px, 2.3vw, 36px); box-sizing: border-box; } @@ -112,10 +112,10 @@ .controls-zone ::ng-deep .small-grey-select { width: 100% !important; min-width: 0 !important; - height: 30px !important; - border-radius: 16px !important; + height: clamp(26px, 2.3vw, 36px) !important; + border-radius: clamp(12px, 1.25vw, 20px) !important; background-color: #555 !important; - font-size: 12px !important; + font-size: clamp(10px, 0.94vw, 14px) !important; display: flex !important; align-items: center !important; box-sizing: border-box !important; @@ -124,18 +124,18 @@ .controls-zone ::ng-deep .p-select-label, .controls-zone ::ng-deep .p-select-label.p-placeholder { width: auto !important; - padding: 0 12px !important; - font-size: 12px !important; + padding: 0 clamp(8px, 0.94vw, 16px) !important; + font-size: clamp(10px, 0.94vw, 14px) !important; font-weight: 600 !important; color: #c0c0c0 !important; --p-select-placeholder-color: #c0c0c0 !important; - line-height: 30px !important; + line-height: clamp(26px, 2.3vw, 36px) !important; display: flex !important; align-items: center !important; } .controls-zone ::ng-deep .p-select-trigger { - padding-right: 8px !important; + padding-right: clamp(6px, 0.63vw, 10px) !important; display: flex !important; align-items: center !important; } From b904a10961764aa1a4b6bc751f29f7fc25874d28 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 11:24:18 -0500 Subject: [PATCH 08/24] #527 Port hex heatmap to segment detail page --- .../bms-segment-view.component.css | 8 ++ .../bms-segment-view.component.html | 11 ++- .../bms-segment-view.component.ts | 83 +++++++++++++++++-- 3 files changed, 96 insertions(+), 6 deletions(-) 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()); + } } From bb09fffa98ae3c0497cc5084b83edcb7161d7cc8 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 11:42:00 -0500 Subject: [PATCH 09/24] #527 Widen dynamic sizing for 70-150% zoom range --- .../bms-debug-page.component.css | 8 +-- .../segment-heatmap.component.css | 2 +- .../segment-overview.component.css | 8 ++- .../segment-row/segment-row.component.css | 66 +++++++++---------- 4 files changed, 43 insertions(+), 41 deletions(-) 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 210dfdae..a3e5b94a 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 @@ -16,14 +16,14 @@ mat-grid-tile { .section-header { display: flex; align-items: center; - padding: 16px 0 8px; - gap: 12px; + 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: 34px; + font-size: clamp(18px, 2.66vw, 44px); color: #efefef; } @@ -34,5 +34,5 @@ mat-grid-tile { .segment-rows { display: flex; flex-direction: column; - gap: 12px; + gap: clamp(6px, 0.94vw, 18px); } 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 index edbb3147..aece8576 100644 --- 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 @@ -1,6 +1,6 @@ :host { display: block; - --hex-w: clamp(44px, 4.1vw, 60px); + --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); 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 index 4cbf7654..b8003e89 100644 --- 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 @@ -8,6 +8,8 @@ gap: 0; flex-shrink: 1; align-items: center; + justify-content: center; + width: 100%; height: 100%; overflow: hidden; } @@ -19,7 +21,7 @@ justify-content: center; min-width: 0; flex: 1 1 0; - padding: 0 clamp(8px, 1vw, 18px); + padding: 0 clamp(4px, 1vw, 24px); overflow: hidden; } @@ -30,7 +32,7 @@ .stat-value { font-family: 'Roboto', sans-serif; font-weight: 700; - font-size: clamp(1.4rem, 2.2vw, 2.4rem); + font-size: clamp(0.8rem, 2.2vw, 3rem); white-space: nowrap; color: #efefef; line-height: 1.1; @@ -44,7 +46,7 @@ .stat-label { font-family: 'Roboto', sans-serif; - font-size: clamp(0.6rem, 0.75vw, 0.8rem); + 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-row/segment-row.component.css b/angular-client/src/pages/bms-debug-page/components/segment-row/segment-row.component.css index d1ab27fa..0218c90a 100644 --- 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 @@ -6,11 +6,11 @@ .segment-row { display: flex; align-items: stretch; - border-radius: clamp(6px, 0.63vw, 12px); - gap: clamp(8px, 0.94vw, 16px); + border-radius: clamp(4px, 0.63vw, 14px); + gap: clamp(4px, 0.94vw, 20px); width: 100%; background-color: #1a1a1a; - padding: clamp(6px, 0.78vw, 14px); + padding: clamp(4px, 0.78vw, 16px); } /* ── Left: Controls (inside dark wrapper) ── */ @@ -19,10 +19,10 @@ flex-direction: column; align-items: flex-start; justify-content: flex-start; - gap: clamp(6px, 0.63vw, 10px); - min-width: clamp(100px, 10vw, 160px); + gap: clamp(3px, 0.63vw, 12px); + min-width: clamp(70px, 10vw, 200px); flex-shrink: 0; - padding: clamp(6px, 0.63vw, 10px); + padding: clamp(3px, 0.63vw, 12px); } /* ── Right: Separate light panels ── */ @@ -39,26 +39,26 @@ } .heatmap-panel { - flex: 0 1 auto; + flex: 3 1 auto; display: flex; align-items: center; - padding: clamp(6px, 0.78vw, 14px) clamp(8px, 0.94vw, 16px) clamp(6px, 0.78vw, 14px) clamp(70px, 7.8vw, 120px); + justify-content: center; + padding: clamp(4px, 0.78vw, 18px) clamp(4px, 0.94vw, 20px) clamp(4px, 0.78vw, 18px) clamp(40px, 7.8vw, 150px); background-color: #2c2c2c; - border-radius: clamp(6px, 0.63vw, 12px); - clip-path: polygon(clamp(60px, 6.9vw, 108px) 0, 100% 0, 100% 100%, 0 100%, 0 clamp(60px, 6.9vw, 108px)); - width: fit-content; - min-width: fit-content; - min-height: clamp(90px, 8.75vw, 140px); + 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: clamp(60px, 8.75vw, 180px); } .overview-panel { display: flex; align-items: center; - padding: clamp(6px, 0.78vw, 14px) clamp(10px, 1.25vw, 20px); + padding: clamp(4px, 0.78vw, 18px) clamp(6px, 1.25vw, 26px); background-color: #2c2c2c; - border-radius: clamp(6px, 0.63vw, 12px); - min-width: clamp(180px, 18vw, 300px); - min-height: clamp(90px, 8.75vw, 140px); + border-radius: clamp(4px, 0.63vw, 14px); + min-width: clamp(120px, 18vw, 380px); + min-height: clamp(60px, 8.75vw, 180px); + flex: 1 1 auto; } .segment-title { @@ -68,14 +68,14 @@ } .battery-icon { - width: clamp(10px, 1vw, 16px); - height: clamp(13px, 1.33vw, 21px); + width: clamp(8px, 1vw, 20px); + height: clamp(10px, 1.33vw, 26px); } .title-text { font-family: 'Roboto', sans-serif; font-weight: 700; - font-size: clamp(16px, 1.56vw, 24px); + font-size: clamp(10px, 1.56vw, 30px); color: #efefef; white-space: nowrap; } @@ -84,23 +84,23 @@ .controls-actions { display: flex; flex-direction: column; - gap: clamp(4px, 0.47vw, 8px); - width: clamp(96px, 9.4vw, 150px); + 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(12px, 1.25vw, 20px); - padding: clamp(4px, 0.47vw, 8px) clamp(12px, 1.4vw, 22px); - font-size: clamp(10px, 0.94vw, 14px); + 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(26px, 2.3vw, 36px); + height: clamp(18px, 2.3vw, 44px); box-sizing: border-box; } @@ -112,10 +112,10 @@ .controls-zone ::ng-deep .small-grey-select { width: 100% !important; min-width: 0 !important; - height: clamp(26px, 2.3vw, 36px) !important; - border-radius: clamp(12px, 1.25vw, 20px) !important; + height: clamp(18px, 2.3vw, 44px) !important; + border-radius: clamp(8px, 1.25vw, 24px) !important; background-color: #555 !important; - font-size: clamp(10px, 0.94vw, 14px) !important; + font-size: clamp(7px, 0.94vw, 18px) !important; display: flex !important; align-items: center !important; box-sizing: border-box !important; @@ -124,18 +124,18 @@ .controls-zone ::ng-deep .p-select-label, .controls-zone ::ng-deep .p-select-label.p-placeholder { width: auto !important; - padding: 0 clamp(8px, 0.94vw, 16px) !important; - font-size: clamp(10px, 0.94vw, 14px) !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(26px, 2.3vw, 36px) !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(6px, 0.63vw, 10px) !important; + padding-right: clamp(3px, 0.63vw, 14px) !important; display: flex !important; align-items: center !important; } From 4239d1cefdb29fa085258e8d988a504165d4077d Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 11:45:30 -0500 Subject: [PATCH 10/24] #527 Center overview stats and reduce row height --- .../segment-overview/segment-overview.component.css | 9 ++++++--- .../components/segment-row/segment-row.component.css | 10 +++++----- 2 files changed, 11 insertions(+), 8 deletions(-) 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 index b8003e89..5e51eb97 100644 --- 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 @@ -1,17 +1,20 @@ :host { - display: block; + display: flex; + align-items: center; + justify-content: center; height: 100%; + width: 100%; } .overview-zone { display: flex; gap: 0; - flex-shrink: 1; align-items: center; justify-content: center; - width: 100%; height: 100%; overflow: hidden; + max-width: fit-content; + margin: 0 auto; } .stat { 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 index 0218c90a..1ed5b40d 100644 --- 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 @@ -10,7 +10,7 @@ gap: clamp(4px, 0.94vw, 20px); width: 100%; background-color: #1a1a1a; - padding: clamp(4px, 0.78vw, 16px); + padding: clamp(3px, 0.55vw, 10px); } /* ── Left: Controls (inside dark wrapper) ── */ @@ -43,21 +43,21 @@ display: flex; align-items: center; justify-content: center; - padding: clamp(4px, 0.78vw, 18px) clamp(4px, 0.94vw, 20px) clamp(4px, 0.78vw, 18px) clamp(40px, 7.8vw, 150px); + padding: clamp(2px, 0.5vw, 12px) clamp(4px, 0.94vw, 20px) clamp(2px, 0.5vw, 12px) clamp(40px, 7.8vw, 150px); 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: clamp(60px, 8.75vw, 180px); + min-height: clamp(50px, 7vw, 150px); } .overview-panel { display: flex; align-items: center; - padding: clamp(4px, 0.78vw, 18px) clamp(6px, 1.25vw, 26px); + padding: clamp(2px, 0.5vw, 12px) clamp(6px, 1.25vw, 26px); background-color: #2c2c2c; border-radius: clamp(4px, 0.63vw, 14px); min-width: clamp(120px, 18vw, 380px); - min-height: clamp(60px, 8.75vw, 180px); + min-height: clamp(50px, 7vw, 150px); flex: 1 1 auto; } From b75ff62b952398f764f5f3d84b36aec423ebd06d Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 12:18:02 -0500 Subject: [PATCH 11/24] #527 Refactor CellReading to single cell model --- .../cell-by-cell-heat-map.component.html | 74 +++-------- .../cell-by-cell-heat-map.component.ts | 12 +- .../cell-view/cell-view.component.ts | 18 ++- .../segment-heatmap.component.html | 4 +- .../segment-heatmap.component.ts | 79 ++++++------ angular-client/src/services/cell.service.ts | 116 +++++++----------- 6 files changed, 111 insertions(+), 192 deletions(-) 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 a3f8be08..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 @@ -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.ts b/angular-client/src/pages/bms-debug-page/components/cell-view/cell-view.component.ts index 86cfd8e6..1e093235 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,7 +1,6 @@ import { ChangeDetectorRef, Component, HostListener, inject, input, 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'; @@ -16,10 +15,10 @@ import HStackComponent from 'src/components/hstack/hstack.component'; imports: [InfoBackgroundComponent, InfoValueDisplayComponent, HStackComponent] }) export class CellViewComponent implements OnDestroy { - private heatMapService = inject(HeatMapService); private cdr = inject(ChangeDetectorRef); private refreshInterval: ReturnType | undefined; cellViewData: CellReading | undefined = undefined; + readingB: CellReading | undefined = undefined; screenWidth = window.innerWidth; forSegment = input.required(); segment: Segment; @@ -27,7 +26,7 @@ export class CellViewComponent implements OnDestroy { public config = inject(DynamicDialogConfig); // Update view width - @HostListener('window:resize', ['$event']) + @HostListener('window:resize') onResize() { this.screenWidth = window.innerWidth; } @@ -36,9 +35,8 @@ export class CellViewComponent implements OnDestroy { this.segment = this.config.data.forSegment; this.displayCellIndex = this.config.data.displayCellIndex !== undefined ? parseInt(this.config.data.displayCellIndex, 10) : undefined; - this.heatMapService.getSelectedCell(this.segment)?.subscribe((data) => { - this.cellViewData = data; - }); + this.cellViewData = this.config.data.readingA; + this.readingB = this.config.data.readingB; // CellReading properties are mutated in-place by CellService as MQTT data arrives, // so we poll for changes to keep the dialog values up to date. this.refreshInterval = setInterval(() => this.cdr.detectChanges(), 500); @@ -89,8 +87,8 @@ export class CellViewComponent implements OnDestroy { }; getAverageVoltage(): number | undefined { - const v1 = this.cellViewData?.volt1; - const v2 = this.cellViewData?.volt2; + const v1 = this.cellViewData?.voltage; + const v2 = this.readingB?.voltage; if (v1 === undefined && v2 === undefined) return undefined; if (v1 === undefined) return v2; if (v2 === undefined) return v1; @@ -98,8 +96,8 @@ export class CellViewComponent implements OnDestroy { } getBalancing(): boolean | undefined { - const b1 = this.cellViewData?.balancing1; - const b2 = this.cellViewData?.balancing2; + const b1 = this.cellViewData?.balancing; + const b2 = this.readingB?.balancing; if (b1 === undefined && b2 === undefined) return undefined; return !!(b1 || b2); } 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 index 1801c710..308835f0 100644 --- 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 @@ -9,7 +9,7 @@ [currentView]="view" [boxShadowColor]="isSelected(cell.reading)" [cellNumber]="cell.cellNum" - (click)="cellClicked(cell.reading, cell.cellNum)" + (click)="cellClicked(cell)" /> }
@@ -23,7 +23,7 @@ [currentView]="view" [boxShadowColor]="isSelected(cell.reading)" [cellNumber]="cell.cellNum" - (click)="cellClicked(cell.reading, cell.cellNum)" + (click)="cellClicked(cell)" /> }
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 index 36ded04f..816860da 100644 --- 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 @@ -9,6 +9,7 @@ import { HexTileComponent } from '../hex-tile/hex-tile.component'; export interface DisplayCell { reading: CellReading; + readingB?: CellReading; value: number | undefined; boolValue: boolean | undefined; cellNum: string; @@ -55,53 +56,46 @@ export class SegmentHeatmapComponent implements OnInit, OnDestroy { } } - get topRowCells(): DisplayCell[] { - const cells: DisplayCell[] = []; - const reversed = this.betaCells.slice().reverse(); - - for (let i = 0; i < reversed.length; i++) { - const cell = reversed[i]; - cells.push({ - reading: cell, - value: this.getCellValue(cell), - boolValue: this.getCellBoolValue(cell), - cellNum: (reversed.length - 1 - i).toString() + /** Pair adjacent cells (0+1, 2+3, …) into 13 display tiles */ + private pairCells(cells: Readonly): DisplayCell[] { + const paired: DisplayCell[] = []; + for (let i = 0; i < cells.length; i += 2) { + const cellA = cells[i]; + const cellB = i + 1 < cells.length ? cells[i + 1] : undefined; + paired.push({ + reading: cellA, + readingB: cellB, + value: this.getPairedValue(cellA, cellB), + boolValue: this.getPairedBoolValue(cellA, cellB), + cellNum: (i / 2).toString() }); } - return cells; + return paired; + } + + get topRowCells(): DisplayCell[] { + const reversed = this.betaCells.slice().reverse(); + return this.pairCells(reversed); } get bottomRowCells(): DisplayCell[] { - const cells: DisplayCell[] = []; - - for (let i = 0; i < this.alphaCells.length; i++) { - const cell = this.alphaCells[i]; - cells.push({ - reading: cell, - value: this.getCellValue(cell), - boolValue: this.getCellBoolValue(cell), - cellNum: i.toString() - }); - } - return cells; + return this.pairCells(this.alphaCells); } - private getCellValue(cell: CellReading): number | undefined { - if (this.view === HeatMapView.Temperature) return cell.temp; - if (this.view === HeatMapView.Voltage) return this.averageVolt(cell); + private getPairedValue(a: CellReading, b: CellReading | undefined): number | undefined { + if (this.view === HeatMapView.Temperature) return a.temp; + if (this.view === HeatMapView.Voltage) { + if (a.voltage === undefined) return b?.voltage; + if (b === undefined || b.voltage === undefined) return a.voltage; + return (a.voltage + b.voltage) / 2; + } return undefined; } - private getCellBoolValue(cell: CellReading): boolean | undefined { + private getPairedBoolValue(a: CellReading, b: CellReading | undefined): boolean | undefined { if (this.view !== HeatMapView.Balancing) return undefined; - if (cell.balancing1 === undefined && cell.balancing2 === undefined) return undefined; - return !!(cell.balancing1 || cell.balancing2); - } - - private averageVolt(cell: CellReading): number | undefined { - if (cell.volt1 === undefined) return cell.volt2; - if (cell.volt2 === undefined) return cell.volt1; - return (cell.volt1 + cell.volt2) / 2; + if (a.balancing === undefined && (b === undefined || b.balancing === undefined)) return undefined; + return !!(a.balancing || b?.balancing); } getColor(cell: DisplayCell): string { @@ -127,11 +121,16 @@ export class SegmentHeatmapComponent implements OnInit, OnDestroy { return value ? '#4169e1' : 'yellow'; } - cellClicked(cell: CellReading, displayIndex: string): void { - this.selectedCell = cell; - this.heatMapService.setSelectedCell(cell); + cellClicked(displayCell: DisplayCell): void { + this.selectedCell = displayCell.reading; + this.heatMapService.setSelectedCell(displayCell.reading); const ref = this.dialogService.open(CellViewComponent, { - data: { forSegment: this.segment(), displayCellIndex: displayIndex }, + data: { + forSegment: this.segment(), + displayCellIndex: displayCell.cellNum, + readingA: displayCell.reading, + readingB: displayCell.readingB + }, width: '40%', draggable: true, closable: true, diff --git a/angular-client/src/services/cell.service.ts b/angular-client/src/services/cell.service.ts index 43360c04..c5a12579 100644 --- a/angular-client/src/services/cell.service.ts +++ b/angular-client/src/services/cell.service.ts @@ -16,25 +16,21 @@ 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; }; const createSegmentCells = (segment: number, chip: Chip, count: number): CellReading[] => { return Array.from( { length: count }, - (): CellReading => ({ + (_, i): CellReading => ({ chip, segment, temp: undefined, - volt1: undefined, - volt2: undefined, - balancing1: undefined, - balancing2: undefined, - cellNumbers: undefined + voltage: undefined, + balancing: undefined, + cellNumber: i }) ); }; @@ -43,8 +39,8 @@ const createPerSegmentCells = (chip: Chip, cellsPerSegment: number): CellReading return Array.from({ length: BMS_CONFIG.NUM_SEGMENTS }, (_, seg) => createSegmentCells(seg, chip, cellsPerSegment)); }; -const startingPerSegmentAlphaCells: CellReading[][] = createPerSegmentCells(Chip.Alpha, BMS_CONFIG.ALPHA_THERM_COUNT); -const startingPerSegmentBetaCells: CellReading[][] = createPerSegmentCells(Chip.Beta, BMS_CONFIG.BETA_THERM_COUNT); +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' @@ -68,40 +64,31 @@ export class CellService { private subscribeToAlphaCellInfo = () => { this.perSegmentAlphaCells.map((segmentAlphaCells, index) => { const segmentNumber = numToSegmentType(index); - allAlphaThermValues.forEach((therm, index) => { + + // Therms: each therm covers two adjacent cells + 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 cellA = thermIndex * 2; + const cellB = thermIndex * 2 + 1; + segmentAlphaCells[cellA].temp = temp; + if (cellB < segmentAlphaCells.length) { + segmentAlphaCells[cellB].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; }); }); }); @@ -110,50 +97,31 @@ 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, BMS_CONFIG.BETA_VOLT_COUNT - 1) - ]; - segmentBetaCells[constIndex].temp = tempBtwnTwoCells; + // Therms: each therm covers two adjacent cells + allBetaThermValues.map((therm, thermIndex) => { + this.storageService.get(topics.betaTemp(segmentNumber, therm)).subscribe((data) => { + const temp = parseFloat(data.values[0]); + const cellA = thermIndex * 2; + const cellB = thermIndex * 2 + 1; + segmentBetaCells[cellA].temp = temp; + if (cellB < segmentBetaCells.length) { + segmentBetaCells[cellB].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, BMS_CONFIG.BETA_VOLT_COUNT - 1) - ]; - 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, BMS_CONFIG.BETA_VOLT_COUNT - 1) - ]; - if (constIndex % 2 === 0) { - segmentBetaCells[cellIndex].balancing1 = balancing; - } else { - segmentBetaCells[cellIndex].balancing2 = balancing; - } + segmentBetaCells[burnIndex].balancing = parseInt(data.values[0]) === 1; }); }); }); From 65bcd5c7350c5efc45dbd1a5923669ab189c4705 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 12:37:55 -0500 Subject: [PATCH 12/24] #527 Revert BMS config to 26-cell counts --- .../components/segment-heatmap/segment-heatmap.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 index 816860da..dc6e4102 100644 --- 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 @@ -74,8 +74,7 @@ export class SegmentHeatmapComponent implements OnInit, OnDestroy { } get topRowCells(): DisplayCell[] { - const reversed = this.betaCells.slice().reverse(); - return this.pairCells(reversed); + return this.pairCells(this.betaCells).reverse(); } get bottomRowCells(): DisplayCell[] { From 33a444e204e7ec54dda8452508a98ed5bb93d60a Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 13:25:58 -0500 Subject: [PATCH 13/24] #527 Remove cell pairing, one tile per CellReading --- .../cell-view/cell-view.component.ts | 14 +----- .../segment-heatmap.component.ts | 46 +++++++------------ angular-client/src/utils/bms.config.ts | 12 ++--- 3 files changed, 24 insertions(+), 48 deletions(-) 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 1e093235..e173fb6e 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 @@ -18,7 +18,6 @@ export class CellViewComponent implements OnDestroy { private cdr = inject(ChangeDetectorRef); private refreshInterval: ReturnType | undefined; cellViewData: CellReading | undefined = undefined; - readingB: CellReading | undefined = undefined; screenWidth = window.innerWidth; forSegment = input.required(); segment: Segment; @@ -36,7 +35,6 @@ export class CellViewComponent implements OnDestroy { this.displayCellIndex = this.config.data.displayCellIndex !== undefined ? parseInt(this.config.data.displayCellIndex, 10) : undefined; this.cellViewData = this.config.data.readingA; - this.readingB = this.config.data.readingB; // CellReading properties are mutated in-place by CellService as MQTT data arrives, // so we poll for changes to keep the dialog values up to date. this.refreshInterval = setInterval(() => this.cdr.detectChanges(), 500); @@ -87,18 +85,10 @@ export class CellViewComponent implements OnDestroy { }; getAverageVoltage(): number | undefined { - const v1 = this.cellViewData?.voltage; - const v2 = this.readingB?.voltage; - if (v1 === undefined && v2 === undefined) return undefined; - if (v1 === undefined) return v2; - if (v2 === undefined) return v1; - return (v1 + v2) / 2; + return this.cellViewData?.voltage; } getBalancing(): boolean | undefined { - const b1 = this.cellViewData?.balancing; - const b2 = this.readingB?.balancing; - if (b1 === undefined && b2 === undefined) return undefined; - return !!(b1 || b2); + return this.cellViewData?.balancing; } } 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 index dc6e4102..ee2d8ff2 100644 --- 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 @@ -9,7 +9,6 @@ import { HexTileComponent } from '../hex-tile/hex-tile.component'; export interface DisplayCell { reading: CellReading; - readingB?: CellReading; value: number | undefined; boolValue: boolean | undefined; cellNum: string; @@ -56,45 +55,33 @@ export class SegmentHeatmapComponent implements OnInit, OnDestroy { } } - /** Pair adjacent cells (0+1, 2+3, …) into 13 display tiles */ - private pairCells(cells: Readonly): DisplayCell[] { - const paired: DisplayCell[] = []; - for (let i = 0; i < cells.length; i += 2) { - const cellA = cells[i]; - const cellB = i + 1 < cells.length ? cells[i + 1] : undefined; - paired.push({ - reading: cellA, - readingB: cellB, - value: this.getPairedValue(cellA, cellB), - boolValue: this.getPairedBoolValue(cellA, cellB), - cellNum: (i / 2).toString() - }); - } - return paired; + /** 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() + })); } get topRowCells(): DisplayCell[] { - return this.pairCells(this.betaCells).reverse(); + return this.toDisplayCells(this.betaCells).reverse(); } get bottomRowCells(): DisplayCell[] { - return this.pairCells(this.alphaCells); + return this.toDisplayCells(this.alphaCells); } - private getPairedValue(a: CellReading, b: CellReading | undefined): number | undefined { - if (this.view === HeatMapView.Temperature) return a.temp; - if (this.view === HeatMapView.Voltage) { - if (a.voltage === undefined) return b?.voltage; - if (b === undefined || b.voltage === undefined) return a.voltage; - return (a.voltage + b.voltage) / 2; - } + 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 getPairedBoolValue(a: CellReading, b: CellReading | undefined): boolean | undefined { + private getCellBoolValue(cell: CellReading): boolean | undefined { if (this.view !== HeatMapView.Balancing) return undefined; - if (a.balancing === undefined && (b === undefined || b.balancing === undefined)) return undefined; - return !!(a.balancing || b?.balancing); + return cell.balancing; } getColor(cell: DisplayCell): string { @@ -127,8 +114,7 @@ export class SegmentHeatmapComponent implements OnInit, OnDestroy { data: { forSegment: this.segment(), displayCellIndex: displayCell.cellNum, - readingA: displayCell.reading, - readingB: displayCell.readingB + readingA: displayCell.reading }, width: '40%', draggable: true, diff --git a/angular-client/src/utils/bms.config.ts b/angular-client/src/utils/bms.config.ts index ae1c1640..19ede5c3 100644 --- a/angular-client/src/utils/bms.config.ts +++ b/angular-client/src/utils/bms.config.ts @@ -7,10 +7,10 @@ */ export const BMS_CONFIG = { NUM_SEGMENTS: 5, - ALPHA_VOLT_COUNT: 26, - BETA_VOLT_COUNT: 26, - ALPHA_THERM_COUNT: 13, - BETA_THERM_COUNT: 13, - ALPHA_BURN_COUNT: 26, - BETA_BURN_COUNT: 26 + 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; From 7d26125dde7198198a1fa03497562e1d9abdd6dd Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 13:31:45 -0500 Subject: [PATCH 14/24] #527 Shift hex offset to top row --- .../components/segment-heatmap/segment-heatmap.component.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index aece8576..7b9c901c 100644 --- 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 @@ -31,7 +31,10 @@ margin-right: 0; } +.hex-row.top-row { + margin-left: var(--row-offset); +} + .hex-row.bottom-row { margin-top: var(--row-gap); - margin-left: var(--row-offset); } From 4a2343c8fd4df9bd7fe5bd0ec295155c457b1916 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 13:37:50 -0500 Subject: [PATCH 15/24] #527 Reduce heatmap panel padding and row height --- .../components/segment-row/segment-row.component.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 1ed5b40d..d5c0d2bb 100644 --- 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 @@ -43,11 +43,11 @@ display: flex; align-items: center; justify-content: center; - padding: clamp(2px, 0.5vw, 12px) clamp(4px, 0.94vw, 20px) clamp(2px, 0.5vw, 12px) clamp(40px, 7.8vw, 150px); + padding: clamp(2px, 0.5vw, 12px) clamp(4px, 0.94vw, 20px) clamp(2px, 0.5vw, 12px) 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: clamp(50px, 7vw, 150px); + min-height: clamp(50px, 6vw, 130px); } .overview-panel { @@ -57,7 +57,7 @@ background-color: #2c2c2c; border-radius: clamp(4px, 0.63vw, 14px); min-width: clamp(120px, 18vw, 380px); - min-height: clamp(50px, 7vw, 150px); + min-height: clamp(50px, 6vw, 130px); flex: 1 1 auto; } From 77d3b860187b2c7dbc5528c84778881000fe7f99 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 13:41:52 -0500 Subject: [PATCH 16/24] #527 Tighten segment row spacing --- .../src/pages/bms-debug-page/bms-debug-page.component.css | 2 +- .../components/segment-row/segment-row.component.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 a3e5b94a..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 @@ -34,5 +34,5 @@ mat-grid-tile { .segment-rows { display: flex; flex-direction: column; - gap: clamp(6px, 0.94vw, 18px); + gap: clamp(2px, 0.35vw, 8px); } 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 index d5c0d2bb..a3a0ca2f 100644 --- 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 @@ -10,7 +10,7 @@ gap: clamp(4px, 0.94vw, 20px); width: 100%; background-color: #1a1a1a; - padding: clamp(3px, 0.55vw, 10px); + padding: clamp(2px, 0.4vw, 7px); } /* ── Left: Controls (inside dark wrapper) ── */ From 15c0048b96ff0a7ae9edc3222ebcee225928e23e Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 13:48:33 -0500 Subject: [PATCH 17/24] #527 Improve cell selection glow and reduce row height --- .../components/hex-tile/hex-tile.component.css | 7 ++++++- .../components/hex-tile/hex-tile.component.ts | 3 ++- .../segment-heatmap/segment-heatmap.component.css | 3 ++- .../components/segment-row/segment-row.component.css | 8 ++++---- 4 files changed, 14 insertions(+), 7 deletions(-) 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 index 9fd9da57..71d2be61 100644 --- 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 @@ -21,8 +21,13 @@ filter: brightness(1.15); } +/* Selected: drop-shadow on host follows the clip-path shape */ .hex-tile.selected { - filter: brightness(1.4) drop-shadow(0 0 4px rgba(255, 255, 255, 0.6)); + filter: brightness(1.3); +} + +:host(.selected-cell) { + filter: drop-shadow(0 0 3px white) drop-shadow(0 0 1px white); z-index: 1; } 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 index a5e79e4a..28f0b812 100644 --- 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 @@ -7,7 +7,8 @@ import { HeatMapView } from 'src/services/heat-map.service'; styleUrl: './hex-tile.component.css', standalone: true, host: { - '[class]': 'variant()' + '[class]': 'variant()', + '[class.selected-cell]': 'boxShadowColor()' } }) export class HexTileComponent { 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 index 7b9c901c..37192104 100644 --- 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 @@ -14,7 +14,8 @@ display: flex; flex-direction: column; align-items: flex-start; - overflow: hidden; + overflow: visible; + padding: 4px 0; min-width: 0; } 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 index a3a0ca2f..b964b576 100644 --- 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 @@ -43,21 +43,21 @@ display: flex; align-items: center; justify-content: center; - padding: clamp(2px, 0.5vw, 12px) clamp(4px, 0.94vw, 20px) clamp(2px, 0.5vw, 12px) clamp(20px, 4vw, 80px); + 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: clamp(50px, 6vw, 130px); + min-height: 0; } .overview-panel { display: flex; align-items: center; - padding: clamp(2px, 0.5vw, 12px) clamp(6px, 1.25vw, 26px); + 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: clamp(50px, 6vw, 130px); + min-height: 0; flex: 1 1 auto; } From ea29fd74f2c05c814ef50b7ec63ec25fff15b7da Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 14:11:15 -0500 Subject: [PATCH 18/24] #527 Add temperature view with merged double-hex tiles --- .../hex-tile/hex-tile.component.css | 54 +++++++++++++++++++ .../segment-heatmap.component.html | 4 +- .../segment-heatmap.component.ts | 26 ++++++++- angular-client/src/services/cell.service.ts | 26 ++++----- angular-client/src/utils/bms.config.ts | 29 ++++++++++ 5 files changed, 123 insertions(+), 16 deletions(-) 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 index 71d2be61..146cf037 100644 --- 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 @@ -146,3 +146,57 @@ 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/segment-heatmap/segment-heatmap.component.html b/angular-client/src/pages/bms-debug-page/components/segment-heatmap/segment-heatmap.component.html index 308835f0..272fb5a6 100644 --- 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 @@ -2,7 +2,7 @@
@for (cell of topRowCells; track $index) { @for (cell of bottomRowCells; track $index) { , 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); } diff --git a/angular-client/src/services/cell.service.ts b/angular-client/src/services/cell.service.ts index c5a12579..a0162fc3 100644 --- a/angular-client/src/services/cell.service.ts +++ b/angular-client/src/services/cell.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { BMS_CONFIG } from 'src/utils/bms.config'; +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 { @@ -65,15 +65,15 @@ export class CellService { this.perSegmentAlphaCells.map((segmentAlphaCells, index) => { const segmentNumber = numToSegmentType(index); - // Therms: each therm covers two adjacent cells + // 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 temp = parseFloat(data.values[0]); - const cellA = thermIndex * 2; - const cellB = thermIndex * 2 + 1; - segmentAlphaCells[cellA].temp = temp; - if (cellB < segmentAlphaCells.length) { - segmentAlphaCells[cellB].temp = temp; + const cellIndices = ALPHA_THERM_CELL_MAP[thermIndex] ?? []; + for (const cellIdx of cellIndices) { + if (cellIdx < segmentAlphaCells.length) { + segmentAlphaCells[cellIdx].temp = temp; + } } }); }); @@ -98,15 +98,15 @@ export class CellService { this.perSegmentBetaCells.map((segmentBetaCells, index) => { const segmentNumber = numToSegmentType(index); - // Therms: each therm covers two adjacent cells + // 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 cellA = thermIndex * 2; - const cellB = thermIndex * 2 + 1; - segmentBetaCells[cellA].temp = temp; - if (cellB < segmentBetaCells.length) { - segmentBetaCells[cellB].temp = temp; + const cellIndices = BETA_THERM_CELL_MAP[thermIndex] ?? []; + for (const cellIdx of cellIndices) { + if (cellIdx < segmentBetaCells.length) { + segmentBetaCells[cellIdx].temp = temp; + } } }); }); diff --git a/angular-client/src/utils/bms.config.ts b/angular-client/src/utils/bms.config.ts index 19ede5c3..3c5cc245 100644 --- a/angular-client/src/utils/bms.config.ts +++ b/angular-client/src/utils/bms.config.ts @@ -14,3 +14,32 @@ export const BMS_CONFIG = { 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 +]; From 09387197ec874c85615d777d46300f03028c8f30 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 14:46:50 -0500 Subject: [PATCH 19/24] #527 Add multi-cell selection with comparison dialog --- .../cell-view/cell-view.component.css | 96 +++++++++++++++++++ .../cell-view/cell-view.component.html | 45 ++++----- .../cell-view/cell-view.component.ts | 84 ++++------------ .../hex-tile/hex-tile.component.css | 13 +-- .../segment-heatmap.component.ts | 45 +++++---- .../src/services/heat-map.service.ts | 37 ++++--- angular-client/src/styles.css | 26 +++++ 7 files changed, 216 insertions(+), 130 deletions(-) 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 0cf92386..e4e298d1 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,25 +1,20 @@ - - - - - - - +
+ +
+
+ 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 e173fb6e..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,42 +1,27 @@ -import { ChangeDetectorRef, Component, HostListener, inject, input, OnDestroy } from '@angular/core'; +import { ChangeDetectorRef, Component, inject, OnDestroy } from '@angular/core'; import { DynamicDialogConfig } from 'primeng/dynamicdialog'; -import { CellReading } from 'src/services/cell.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 HStackComponent from 'src/components/hstack/hstack.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, HStackComponent] + imports: [] }) export class CellViewComponent implements OnDestroy { private cdr = inject(ChangeDetectorRef); private refreshInterval: ReturnType | undefined; - cellViewData: CellReading | undefined = undefined; - screenWidth = window.innerWidth; - forSegment = input.required(); - segment: Segment; - displayCellIndex: number | undefined; public config = inject(DynamicDialogConfig); - // Update view width - @HostListener('window:resize') - 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.displayCellIndex = - this.config.data.displayCellIndex !== undefined ? parseInt(this.config.data.displayCellIndex, 10) : undefined; - this.cellViewData = this.config.data.readingA; - // CellReading properties are mutated in-place by CellService as MQTT data arrives, - // so we poll for changes to keep the dialog values up to date. + this.cells = this.config.data.cells; + // Poll for MQTT value changes and selection array changes. this.refreshInterval = setInterval(() => this.cdr.detectChanges(), 500); } @@ -46,49 +31,20 @@ export class CellViewComponent implements OnDestroy { } } - 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}`; - - return title; - }; - - getCellNumTitle = (): string => { - const cellNumLabel = this.screenWidth <= 1100 ? `Cell` : `Cell Number`; - return cellNumLabel; - }; - - getCellVoltageTitle = (): string => { - const cellVoltageLabel = this.screenWidth <= 1100 ? `Volts` : `Voltage`; - return cellVoltageLabel; - }; + chipLabel(chip: Chip): string { + return chipToString(chip, true); + } - getBalancingTitle = (): string => { - const balancingLabel = this.screenWidth <= 1100 ? `Bal.?` : `Balancing?`; - return balancingLabel; - }; + formatVoltage(v: number | undefined): string { + return v !== undefined ? `${v.toFixed(3)} V` : '-'; + } - getAverageVoltage(): number | undefined { - return this.cellViewData?.voltage; + formatTemp(t: number | undefined): string { + return t !== undefined && t !== null ? `${t.toFixed(1)} °C` : '-'; } - getBalancing(): boolean | undefined { - return this.cellViewData?.balancing; + 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 index 146cf037..7e18856b 100644 --- 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 @@ -155,18 +155,7 @@ /* 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% - ); + 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%; 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 index 16d95098..f1bc5be5 100644 --- 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 @@ -1,7 +1,7 @@ import { Component, effect, inject, input, OnDestroy, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; import { Segment } from 'src/utils/bms.utils'; -import { HeatMapService, HeatMapView } from 'src/services/heat-map.service'; +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'; @@ -34,7 +34,6 @@ export class SegmentHeatmapComponent implements OnInit, OnDestroy { alphaCells!: Readonly; betaCells!: Readonly; view = HeatMapView.Voltage; - selectedCell: CellReading | undefined = undefined; constructor() { effect(() => { @@ -132,24 +131,36 @@ export class SegmentHeatmapComponent implements OnInit, OnDestroy { } cellClicked(displayCell: DisplayCell): void { - this.selectedCell = displayCell.reading; - this.heatMapService.setSelectedCell(displayCell.reading); - const ref = this.dialogService.open(CellViewComponent, { - data: { - forSegment: this.segment(), - displayCellIndex: displayCell.cellNum, - readingA: displayCell.reading - }, - width: '40%', - draggable: true, - closable: true, - closeAriaLabel: 'Close' - }); - ref.onClose.subscribe(() => (this.selectedCell = undefined)); + const info: SelectedCellInfo = { + reading: displayCell.reading, + cellNum: displayCell.cellNum, + segment: this.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.selectedCell === cell; + return this.heatMapService.isCellSelected(cell); } ngOnDestroy(): void { diff --git a/angular-client/src/services/heat-map.service.ts b/angular-client/src/services/heat-map.service.ts index 843bba97..0b0b01c0 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,38 @@ 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); - }; + /** Multi-cell selection state */ + selectedCells: SelectedCellInfo[] = []; + dialogRef: DynamicDialogRef | null = null; - getSelectedCell = (segment: Segment) => { - if (!this.selectedCellMap.get(segment)) { - this.selectedCellMap.set(segment, new BehaviorSubject(undefined)); + 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)) { 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; +} From bf9857cd14c91a5ed01f94e0f8e52dd4dffd276f Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 14:47:23 -0500 Subject: [PATCH 20/24] #527 Add multi-cell selection with comparison dialog --- .../components/cell-view/cell-view.component.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 e4e298d1..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 @@ -14,7 +14,12 @@
{{ formatVoltage(cell.reading.voltage) }} {{ formatTemp(cell.reading.temp) }} - {{ formatBool(cell.reading.balancing) }} + {{ formatBool(cell.reading.balancing) }}
}
From 91f34970cb73d2eb5866909b305803c5ee085585 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 15:28:47 -0500 Subject: [PATCH 21/24] #527 Add temperature option to segment view selector --- .../components/segment-row/segment-row.component.ts | 4 ++++ 1 file changed, 4 insertions(+) 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 index a5243afe..8918aac7 100644 --- 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 @@ -33,6 +33,10 @@ export class SegmentRowComponent implements OnInit, OnDestroy { 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) From adb7861663368af23f6fc0b43a1f0d91d9f1ca65 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 15:45:50 -0500 Subject: [PATCH 22/24] #527 Expand therm group to individual columns in dialog --- .../segment-heatmap.component.ts | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) 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 index f1bc5be5..f857d79a 100644 --- 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 @@ -1,6 +1,6 @@ import { Component, effect, inject, input, OnDestroy, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; -import { Segment } from 'src/utils/bms.utils'; +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'; @@ -131,13 +131,40 @@ export class SegmentHeatmapComponent implements OnInit, OnDestroy { } cellClicked(displayCell: DisplayCell): void { - const info: SelectedCellInfo = { - reading: displayCell.reading, - cellNum: displayCell.cellNum, - segment: this.segment() - }; + const segment = this.segment(); - this.heatMapService.toggleCell(info); + // 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) { From 7059adc1b4344b86510a2ef160b9983aa7c2f202 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 15:54:31 -0500 Subject: [PATCH 23/24] #527 Sync row selectors with Set ALL Maps via effect --- .../select-dropdown.component.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) 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) { From 43f5671a06fbbc502fb24d3ed4696ecf1844c517 Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 22 Feb 2026 15:57:26 -0500 Subject: [PATCH 24/24] #527 Unify default view from service globalView --- .../bms-debug-page.component.ts | 21 ++++++++++++++++--- .../src/services/heat-map.service.ts | 6 +++++- 2 files changed, 23 insertions(+), 4 deletions(-) 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 eb88e131..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,4 +1,5 @@ -import { Component, HostListener, inject } 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'; @@ -27,8 +28,9 @@ const formatAllSelectorName = (name: string) => 'Set ALL Maps: ' + name; SelectDropdownComponent ] }) -export class BmsDebugPageComponent { +export class BmsDebugPageComponent implements OnInit, OnDestroy { private heatMapService = inject(HeatMapService); + private subscription?: Subscription; time = new Date(); newRunIsLoading = false; @@ -54,11 +56,24 @@ export class BmsDebugPageComponent { ]; allSegSelectorConfig: SelectorConfig = { options: this.allViewOptions, - placeholder: 'Set ALL Maps: Temp...' + 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/services/heat-map.service.ts b/angular-client/src/services/heat-map.service.ts index 0b0b01c0..2b7da36b 100644 --- a/angular-client/src/services/heat-map.service.ts +++ b/angular-client/src/services/heat-map.service.ts @@ -22,6 +22,9 @@ export interface SelectedCellInfo { export class HeatMapService { private currentViewMap: Map> = new Map(); + /** Global default view — drives the "Set ALL Maps" selector and initial per-segment defaults */ + readonly globalView$ = new BehaviorSubject(HeatMapView.Voltage); + /** Multi-cell selection state */ selectedCells: SelectedCellInfo[] = []; dialogRef: DynamicDialogRef | null = null; @@ -52,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); });