Compare commits
	
		
			9 Commits
		
	
	
		
			main
			...
			rule-compl
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 917464347a | |||
| 1da81476f7 | |||
| a34a70ef2f | |||
| 1f0c9779f6 | |||
| 39fec8bdfa | |||
| f1ca9ed5ff | |||
| 826318652d | |||
| 333939df87 | |||
| 971d6380e0 | 
							
								
								
									
										6779
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6779
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -33,7 +33,8 @@
 | 
				
			|||||||
		"tailwindcss": "^3.4.3",
 | 
							"tailwindcss": "^3.4.3",
 | 
				
			||||||
		"tslib": "^2.4.1",
 | 
							"tslib": "^2.4.1",
 | 
				
			||||||
		"typescript": "^5.0.0",
 | 
							"typescript": "^5.0.0",
 | 
				
			||||||
		"vite": "^5.0.3"
 | 
							"vite": "^5.0.3",
 | 
				
			||||||
 | 
							"vite-plugin-node-polyfills": "^0.22.0"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	"type": "module",
 | 
						"type": "module",
 | 
				
			||||||
	"dependencies": {
 | 
						"dependencies": {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										80
									
								
								src/lib/client.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/lib/client.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					import { source } from 'sveltekit-sse';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						decodeMessage,
 | 
				
			||||||
 | 
						unmarshalMessage,
 | 
				
			||||||
 | 
						type AMSError,
 | 
				
			||||||
 | 
						type AMSStatus,
 | 
				
			||||||
 | 
						type MarshalledMessage,
 | 
				
			||||||
 | 
						type ShuntLog,
 | 
				
			||||||
 | 
						type SlaveLog,
 | 
				
			||||||
 | 
						type SlaveStatus
 | 
				
			||||||
 | 
					} from './messages';
 | 
				
			||||||
 | 
					import { writable, type Writable } from 'svelte/store';
 | 
				
			||||||
 | 
					import { SlaveLogData } from './slave-log';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type ShuntData = {
 | 
				
			||||||
 | 
						current: number;
 | 
				
			||||||
 | 
						voltage1: number;
 | 
				
			||||||
 | 
						voltage2: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const error: Writable<AMSError | undefined> = writable(undefined);
 | 
				
			||||||
 | 
					export const amsStatus: Writable<AMSStatus | undefined> = writable(undefined);
 | 
				
			||||||
 | 
					export const slaveStatus: Writable<Record<number, SlaveStatus>> = writable({});
 | 
				
			||||||
 | 
					export const slaveLog: Writable<Record<number, SlaveLogData>> = writable({});
 | 
				
			||||||
 | 
					export const shunt: Writable<ShuntData> = writable({ current: 0, voltage1: 0, voltage2: 0 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					source('/data')
 | 
				
			||||||
 | 
						.select('message')
 | 
				
			||||||
 | 
						.subscribe((value) => {
 | 
				
			||||||
 | 
							if (!value) {
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const msg = decodeMessage(unmarshalMessage(JSON.parse(value) as MarshalledMessage));
 | 
				
			||||||
 | 
							if (!msg) {
 | 
				
			||||||
 | 
								return;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							switch (msg.type) {
 | 
				
			||||||
 | 
								case 'error':
 | 
				
			||||||
 | 
									error.set(msg as AMSError);
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								case 'status':
 | 
				
			||||||
 | 
									amsStatus.set(msg as AMSStatus);
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								case 'slaveStatus': {
 | 
				
			||||||
 | 
									const status = msg as SlaveStatus;
 | 
				
			||||||
 | 
									slaveStatus.update((r) => {
 | 
				
			||||||
 | 
										r[status.slaveId] = status;
 | 
				
			||||||
 | 
										return r;
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								case 'slaveLog':
 | 
				
			||||||
 | 
									handleSlaveLog(msg as SlaveLog);
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								case 'shuntCurrentLog':
 | 
				
			||||||
 | 
									shunt.update((s) => {
 | 
				
			||||||
 | 
										s.current = (msg as ShuntLog).value;
 | 
				
			||||||
 | 
										return s;
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								case 'shuntVoltage1Log':
 | 
				
			||||||
 | 
									shunt.update((s) => Object.assign(s, { voltage1: (msg as ShuntLog).value }));
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								case 'shuntVoltage2Log':
 | 
				
			||||||
 | 
									shunt.update((s) => Object.assign(s, { voltage2: (msg as ShuntLog).value }));
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function handleSlaveLog(msg: SlaveLog) {
 | 
				
			||||||
 | 
						slaveLog.update((r) => {
 | 
				
			||||||
 | 
							if (!(msg.slaveId in r)) {
 | 
				
			||||||
 | 
								r[msg.slaveId] = new SlaveLogData();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const slave = r[msg.slaveId];
 | 
				
			||||||
 | 
							slave.handleMsg(msg);
 | 
				
			||||||
 | 
							return r;
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,2 +1,2 @@
 | 
				
			|||||||
export const N_CELLS_PER_SLAVE = 15;
 | 
					export const N_CELLS_PER_SLAVE = 15;
 | 
				
			||||||
export const N_TEMP_SENSORS_PER_SLAVE = 32;
 | 
					export const N_TEMP_SENSORS_PER_SLAVE = 44;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,44 @@
 | 
				
			|||||||
 | 
					export interface MarshalledMessage {
 | 
				
			||||||
 | 
						id: number;
 | 
				
			||||||
 | 
						ext: boolean;
 | 
				
			||||||
 | 
						canfd: boolean;
 | 
				
			||||||
 | 
						data: number[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type RawMessage = {
 | 
				
			||||||
 | 
						ext?: boolean;
 | 
				
			||||||
 | 
						canfd?: boolean;
 | 
				
			||||||
 | 
						id: number;
 | 
				
			||||||
 | 
						data: Buffer;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function marshalMessage(msg: RawMessage): MarshalledMessage {
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							id: msg.id,
 | 
				
			||||||
 | 
							ext: msg.ext ?? false,
 | 
				
			||||||
 | 
							canfd: msg.canfd ?? false,
 | 
				
			||||||
 | 
							data: Array.from(msg.data)
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function unmarshalMessage(msg: MarshalledMessage): RawMessage {
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							id: msg.id,
 | 
				
			||||||
 | 
							ext: msg.ext,
 | 
				
			||||||
 | 
							canfd: msg.canfd,
 | 
				
			||||||
 | 
							data: Buffer.from(msg.data)
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface AMSMessage {
 | 
					export interface AMSMessage {
 | 
				
			||||||
	type: 'error' | 'status' | 'slaveStatus' | 'slaveLog';
 | 
						type:
 | 
				
			||||||
 | 
							| 'error'
 | 
				
			||||||
 | 
							| 'status'
 | 
				
			||||||
 | 
							| 'slaveStatus'
 | 
				
			||||||
 | 
							| 'slaveLog'
 | 
				
			||||||
 | 
							| 'shuntCurrentLog'
 | 
				
			||||||
 | 
							| 'shuntVoltage1Log'
 | 
				
			||||||
 | 
							| 'shuntVoltage2Log';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface AMSError extends AMSMessage {
 | 
					export interface AMSError extends AMSMessage {
 | 
				
			||||||
@ -68,3 +107,176 @@ export interface SlaveLogTemperature extends SlaveLog {
 | 
				
			|||||||
	startIndex: number;
 | 
						startIndex: number;
 | 
				
			||||||
	temperatures: [number, number, number, number, number, number, number, number];
 | 
						temperatures: [number, number, number, number, number, number, number, number];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ShuntLog extends AMSMessage {
 | 
				
			||||||
 | 
						type: 'shuntCurrentLog' | 'shuntVoltage1Log' | 'shuntVoltage2Log';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						value: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { N_CELLS_PER_SLAVE, N_TEMP_SENSORS_PER_SLAVE } from '$lib/defs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const CAN_ID_AMS_ERROR = 0x00c;
 | 
				
			||||||
 | 
					export const CAN_ID_AMS_STATUS = 0x00a;
 | 
				
			||||||
 | 
					export const CAN_ID_AMS_SLAVE_STATUS_BASE = 0x080;
 | 
				
			||||||
 | 
					export const CAN_ID_AMS_SLAVE_STATUS_MASK = 0xff0;
 | 
				
			||||||
 | 
					export const CAN_ID_AMS_SLAVE_LOG_BASE = 0x600;
 | 
				
			||||||
 | 
					export const CAN_ID_AMS_SLAVE_LOG_MASK = 0xf00;
 | 
				
			||||||
 | 
					export const CAN_ID_SHUNT_CURRENT = 0x521;
 | 
				
			||||||
 | 
					export const CAN_ID_SHUNT_VOLTAGE_1 = 0x522;
 | 
				
			||||||
 | 
					export const CAN_ID_SHUNT_VOLTAGE_2 = 0x523;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function decodeError(msg: RawMessage): AMSError | null {
 | 
				
			||||||
 | 
						if (msg.data.length != 2) {
 | 
				
			||||||
 | 
							console.warn('invalid error frame', msg);
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						console.log('error', msg);
 | 
				
			||||||
 | 
						const data = msg.data;
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							type: 'error',
 | 
				
			||||||
 | 
							kind: data[0],
 | 
				
			||||||
 | 
							arg: data[1]
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function decodeStatus(msg: RawMessage): AMSStatus | null {
 | 
				
			||||||
 | 
						if (msg.data.length != 8) {
 | 
				
			||||||
 | 
							console.warn('invalid status frame', msg);
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const data = msg.data;
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							type: 'status',
 | 
				
			||||||
 | 
							state: data[0] & 0x7f,
 | 
				
			||||||
 | 
							sdcClosed: !!(data[0] & 0x80),
 | 
				
			||||||
 | 
							soc: data[1],
 | 
				
			||||||
 | 
							minCellVolt: data.readUInt16BE(2) * 1e-3,
 | 
				
			||||||
 | 
							maxCellTemp: data.readUInt16BE(4) * 0.0625,
 | 
				
			||||||
 | 
							imdOK: !!(data[6] & 0x80)
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function decodeSlaveStatus(msg: RawMessage): SlaveStatus | null {
 | 
				
			||||||
 | 
						if (msg.data.length != 8) {
 | 
				
			||||||
 | 
							console.warn('invalid slave status frame', msg);
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const data = msg.data;
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							type: 'slaveStatus',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							slaveId: data[0] & 0x7f,
 | 
				
			||||||
 | 
							error: !!(data[0] & 0x80),
 | 
				
			||||||
 | 
							soc: data[1],
 | 
				
			||||||
 | 
							minCellVolt: data.readUInt16BE(2) * 1e-3,
 | 
				
			||||||
 | 
							maxCellVolt: data.readUInt16BE(4) * 1e-3,
 | 
				
			||||||
 | 
							maxTemp: data.readUInt16BE(6) * 0.0625
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function decodeSlaveLog(msg: RawMessage): SlaveLog | null {
 | 
				
			||||||
 | 
						if (msg.data.length != 8) {
 | 
				
			||||||
 | 
							console.warn('invalid slave log frame', msg);
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const slaveId = (msg.id & 0xf0) >> 4;
 | 
				
			||||||
 | 
						const logIndex = msg.id & 0x00f;
 | 
				
			||||||
 | 
						const data = msg.data;
 | 
				
			||||||
 | 
						if (logIndex < N_CELLS_PER_SLAVE / 4) {
 | 
				
			||||||
 | 
							const startIndex = logIndex * 4;
 | 
				
			||||||
 | 
							const voltages = [];
 | 
				
			||||||
 | 
							for (let i = 0; i < 4; i++) {
 | 
				
			||||||
 | 
								voltages.push(data.readUInt16BE(2 * i) * 1e-3);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const msg: SlaveLogVoltage = {
 | 
				
			||||||
 | 
								type: 'slaveLog',
 | 
				
			||||||
 | 
								slaveId,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								logType: 'voltage',
 | 
				
			||||||
 | 
								startIndex,
 | 
				
			||||||
 | 
								voltages: voltages as [number, number, number, number]
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
							return msg;
 | 
				
			||||||
 | 
						} else if (logIndex == N_CELLS_PER_SLAVE / 4) {
 | 
				
			||||||
 | 
							const msg: SlaveLogLastCell = {
 | 
				
			||||||
 | 
								type: 'slaveLog',
 | 
				
			||||||
 | 
								slaveId,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								logType: 'lastCell',
 | 
				
			||||||
 | 
								voltage: data.readUInt16BE(0) * 1e-3,
 | 
				
			||||||
 | 
								failed_temp_sensors: data.readInt32BE(2)
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
							return msg;
 | 
				
			||||||
 | 
						} else if (logIndex < N_CELLS_PER_SLAVE / 4 + 1 + N_TEMP_SENSORS_PER_SLAVE / 8) {
 | 
				
			||||||
 | 
							const temperatures = [];
 | 
				
			||||||
 | 
							for (let i = 0; i < 8; i++) {
 | 
				
			||||||
 | 
								temperatures.push(data.readInt8(i) * 1);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const msg: SlaveLogTemperature = {
 | 
				
			||||||
 | 
								type: 'slaveLog',
 | 
				
			||||||
 | 
								slaveId,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								logType: 'temperature',
 | 
				
			||||||
 | 
								startIndex: (logIndex - N_CELLS_PER_SLAVE / 4 - 1) * 8,
 | 
				
			||||||
 | 
								temperatures: temperatures as [number, number, number, number, number, number, number, number]
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
							return msg;
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							console.warn('Unknown slave log index', msg.id);
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function decodeShunt(msg: RawMessage, type: ShuntLog['type']): ShuntLog | null {
 | 
				
			||||||
 | 
						if (msg.data.length != 6) {
 | 
				
			||||||
 | 
							console.warn('invalid shunt log frame', msg);
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							type: type,
 | 
				
			||||||
 | 
							value: msg.data.readInt32BE(2) * 1e-3
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function decodeMessage(msg: RawMessage): AMSMessage | null {
 | 
				
			||||||
 | 
						if (msg.ext || msg.canfd) {
 | 
				
			||||||
 | 
							console.warn('invalid message', msg);
 | 
				
			||||||
 | 
							return null;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let message: AMSMessage | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ((msg.id & CAN_ID_AMS_SLAVE_STATUS_MASK) == CAN_ID_AMS_SLAVE_STATUS_BASE) {
 | 
				
			||||||
 | 
							message = decodeSlaveStatus(msg);
 | 
				
			||||||
 | 
						} else if ((msg.id & CAN_ID_AMS_SLAVE_LOG_MASK) == CAN_ID_AMS_SLAVE_LOG_BASE) {
 | 
				
			||||||
 | 
							message = decodeSlaveLog(msg);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							switch (msg.id) {
 | 
				
			||||||
 | 
								case CAN_ID_AMS_ERROR:
 | 
				
			||||||
 | 
									message = decodeError(msg);
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								case CAN_ID_AMS_STATUS:
 | 
				
			||||||
 | 
									message = decodeStatus(msg);
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								case CAN_ID_SHUNT_CURRENT:
 | 
				
			||||||
 | 
									message = decodeShunt(msg, 'shuntCurrentLog');
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								case CAN_ID_SHUNT_VOLTAGE_1:
 | 
				
			||||||
 | 
									message = decodeShunt(msg, 'shuntVoltage1Log');
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								case CAN_ID_SHUNT_VOLTAGE_2:
 | 
				
			||||||
 | 
									message = decodeShunt(msg, 'shuntVoltage2Log');
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								default:
 | 
				
			||||||
 | 
									console.warn(`unknown message id ${msg.id}`, msg);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return message;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								src/routes/+layout.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/routes/+layout.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import '../app.css';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<slot />
 | 
				
			||||||
@ -1,59 +1,44 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import type { AMSError, AMSMessage, AMSStatus, SlaveLog, SlaveStatus } from '$lib/messages';
 | 
					 | 
				
			||||||
	import { source } from 'sveltekit-sse';
 | 
					 | 
				
			||||||
	import MasterStatusDisplay from './master-status-display.svelte';
 | 
						import MasterStatusDisplay from './master-status-display.svelte';
 | 
				
			||||||
	import SlaveStatusDisplay from './slave-status-display.svelte';
 | 
						import SlaveStatusDisplay from './slave-status-display.svelte';
 | 
				
			||||||
	import '../app.css';
 | 
					 | 
				
			||||||
	import MasterErrorDisplay from './master-error-display.svelte';
 | 
						import MasterErrorDisplay from './master-error-display.svelte';
 | 
				
			||||||
	import { SlaveLogData } from '$lib/slave-log';
 | 
						import ShuntStatusDisplay from './shunt-status-display.svelte';
 | 
				
			||||||
	// const value = source('/data').select('message');
 | 
						import Overview from './overview.svelte';
 | 
				
			||||||
 | 
						import { amsStatus, error, shunt, slaveLog, slaveStatus } from '$lib/client';
 | 
				
			||||||
	let error: AMSError | undefined;
 | 
					 | 
				
			||||||
	let amsStatus: AMSStatus | undefined;
 | 
					 | 
				
			||||||
	let slaveStatus: Record<number, SlaveStatus> = {};
 | 
					 | 
				
			||||||
	let slaveLog: Record<number, SlaveLogData> = {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	function handleSlaveLog(msg: SlaveLog) {
 | 
					 | 
				
			||||||
		if (!(msg.slaveId in slaveLog)) {
 | 
					 | 
				
			||||||
			slaveLog[msg.slaveId] = new SlaveLogData();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		const slave = slaveLog[msg.slaveId];
 | 
					 | 
				
			||||||
		slave.handleMsg(msg);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	source('/data')
 | 
					 | 
				
			||||||
		.select('message')
 | 
					 | 
				
			||||||
		.subscribe((value) => {
 | 
					 | 
				
			||||||
			if (!value) {
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			const msg = JSON.parse(value) as AMSMessage;
 | 
					 | 
				
			||||||
			switch (msg.type) {
 | 
					 | 
				
			||||||
				case 'error':
 | 
					 | 
				
			||||||
					error = msg as AMSError;
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				case 'status':
 | 
					 | 
				
			||||||
					amsStatus = msg as AMSStatus;
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				case 'slaveStatus': {
 | 
					 | 
				
			||||||
					const status = msg as SlaveStatus;
 | 
					 | 
				
			||||||
					slaveStatus[status.slaveId] = status;
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
				case 'slaveLog':
 | 
					 | 
				
			||||||
					handleSlaveLog(msg as SlaveLog);
 | 
					 | 
				
			||||||
					break;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h1>FaSTTUBe Charger</h1>
 | 
					<svelte:head>
 | 
				
			||||||
 | 
						<title>Charging</title>
 | 
				
			||||||
 | 
					</svelte:head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<MasterErrorDisplay {error} />
 | 
					<div class="vertical-wrapper">
 | 
				
			||||||
<MasterStatusDisplay status={amsStatus} />
 | 
						<Overview amsStatus={$amsStatus} shunt={$shunt} showPopout={true} />
 | 
				
			||||||
 | 
						<div class="wrapper">
 | 
				
			||||||
<div class="slaves">
 | 
							<MasterErrorDisplay error={$error} />
 | 
				
			||||||
	{#each Object.entries(slaveStatus) as [id, status]}
 | 
							<MasterStatusDisplay status={$amsStatus} />
 | 
				
			||||||
		<SlaveStatusDisplay {id} {status} logData={slaveLog[id]} />
 | 
							<ShuntStatusDisplay data={$shunt} />
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="wrapper">
 | 
				
			||||||
 | 
							{#each Object.entries($slaveStatus) as [id, status]}
 | 
				
			||||||
 | 
								<SlaveStatusDisplay {id} {status} logData={$slaveLog[id]} />
 | 
				
			||||||
		{/each}
 | 
							{/each}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.wrapper {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: row;
 | 
				
			||||||
 | 
							justify-content: center;
 | 
				
			||||||
 | 
							align-items: center;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.vertical-wrapper {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							justify-content: center;
 | 
				
			||||||
 | 
							align-items: center;
 | 
				
			||||||
 | 
							margin: 0;
 | 
				
			||||||
 | 
							padding: 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,136 +1,18 @@
 | 
				
			|||||||
import { produce } from 'sveltekit-sse';
 | 
					import { produce } from 'sveltekit-sse';
 | 
				
			||||||
import * as can from 'socketcan';
 | 
					import * as can from 'socketcan';
 | 
				
			||||||
import type {
 | 
					import {
 | 
				
			||||||
	AMSError,
 | 
						type RawMessage,
 | 
				
			||||||
	AMSMessage,
 | 
						marshalMessage,
 | 
				
			||||||
	AMSStatus,
 | 
						CAN_ID_AMS_ERROR,
 | 
				
			||||||
	SlaveLog,
 | 
						CAN_ID_AMS_SLAVE_STATUS_MASK,
 | 
				
			||||||
	SlaveLogLastCell,
 | 
						CAN_ID_AMS_SLAVE_LOG_MASK,
 | 
				
			||||||
	SlaveLogTemperature,
 | 
						CAN_ID_AMS_SLAVE_LOG_BASE,
 | 
				
			||||||
	SlaveLogVoltage,
 | 
						CAN_ID_AMS_SLAVE_STATUS_BASE,
 | 
				
			||||||
	SlaveStatus
 | 
						CAN_ID_AMS_STATUS,
 | 
				
			||||||
 | 
						CAN_ID_SHUNT_CURRENT,
 | 
				
			||||||
 | 
						CAN_ID_SHUNT_VOLTAGE_1,
 | 
				
			||||||
 | 
						CAN_ID_SHUNT_VOLTAGE_2
 | 
				
			||||||
} from '$lib/messages';
 | 
					} from '$lib/messages';
 | 
				
			||||||
import { N_CELLS_PER_SLAVE, N_TEMP_SENSORS_PER_SLAVE } from '$lib/defs';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const CAN_ID_AMS_ERROR = 0x00c;
 | 
					 | 
				
			||||||
const CAN_ID_AMS_STATUS = 0x00a;
 | 
					 | 
				
			||||||
const CAN_ID_AMS_SLAVE_STATUS_BASE = 0x080;
 | 
					 | 
				
			||||||
const CAN_ID_AMS_SLAVE_STATUS_MASK = 0xff0;
 | 
					 | 
				
			||||||
const CAN_ID_AMS_SLAVE_LOG_BASE = 0x600;
 | 
					 | 
				
			||||||
const CAN_ID_AMS_SLAVE_LOG_MASK = 0xf00;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type RawMessage = {
 | 
					 | 
				
			||||||
	ext?: boolean;
 | 
					 | 
				
			||||||
	canfd?: boolean;
 | 
					 | 
				
			||||||
	id: number;
 | 
					 | 
				
			||||||
	data: Buffer;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function decodeError(msg: RawMessage): AMSError | null {
 | 
					 | 
				
			||||||
	if (msg.data.length != 2) {
 | 
					 | 
				
			||||||
		console.warn('invalid error frame', msg);
 | 
					 | 
				
			||||||
		return null;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	console.log('error', msg);
 | 
					 | 
				
			||||||
	const data = msg.data;
 | 
					 | 
				
			||||||
	return {
 | 
					 | 
				
			||||||
		type: 'error',
 | 
					 | 
				
			||||||
		kind: data[0],
 | 
					 | 
				
			||||||
		arg: data[1]
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function decodeStatus(msg: RawMessage): AMSStatus | null {
 | 
					 | 
				
			||||||
	if (msg.data.length != 8) {
 | 
					 | 
				
			||||||
		console.warn('invalid status frame', msg);
 | 
					 | 
				
			||||||
		return null;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const data = msg.data;
 | 
					 | 
				
			||||||
	return {
 | 
					 | 
				
			||||||
		type: 'status',
 | 
					 | 
				
			||||||
		state: data[0] & 0x7f,
 | 
					 | 
				
			||||||
		sdcClosed: !!(data[0] & 0x80),
 | 
					 | 
				
			||||||
		soc: data[1],
 | 
					 | 
				
			||||||
		minCellVolt: data.readUInt16BE(2) * 1e-3,
 | 
					 | 
				
			||||||
		maxCellTemp: data.readUInt16BE(4) * 0.0625,
 | 
					 | 
				
			||||||
		imdOK: !!(data[6] & 0x80)
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function decodeSlaveStatus(msg: RawMessage): SlaveStatus | null {
 | 
					 | 
				
			||||||
	if (msg.data.length != 8) {
 | 
					 | 
				
			||||||
		console.warn('invalid slave status frame', msg);
 | 
					 | 
				
			||||||
		return null;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const data = msg.data;
 | 
					 | 
				
			||||||
	return {
 | 
					 | 
				
			||||||
		type: 'slaveStatus',
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		slaveId: data[0] & 0x7f,
 | 
					 | 
				
			||||||
		error: !!(data[0] & 0x80),
 | 
					 | 
				
			||||||
		soc: data[1],
 | 
					 | 
				
			||||||
		minCellVolt: data.readUInt16BE(2) * 1e-3,
 | 
					 | 
				
			||||||
		maxCellVolt: data.readUInt16BE(4) * 1e-3,
 | 
					 | 
				
			||||||
		maxTemp: data.readUInt16BE(6) * 0.0625
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function decodeSlaveLog(msg: RawMessage): SlaveLog | null {
 | 
					 | 
				
			||||||
	if (msg.data.length != 8) {
 | 
					 | 
				
			||||||
		console.warn('invalid slave log frame', msg);
 | 
					 | 
				
			||||||
		return null;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const slaveId = (msg.id & 0xf0) >> 4;
 | 
					 | 
				
			||||||
	const logIndex = msg.id & 0x00f;
 | 
					 | 
				
			||||||
	const data = msg.data;
 | 
					 | 
				
			||||||
	if (logIndex < N_CELLS_PER_SLAVE / 4) {
 | 
					 | 
				
			||||||
		const startIndex = logIndex * 4;
 | 
					 | 
				
			||||||
		const voltages = [];
 | 
					 | 
				
			||||||
		for (let i = 0; i < 4; i++) {
 | 
					 | 
				
			||||||
			voltages.push(data.readUInt16BE(2 * i) * 1e-3);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		const msg: SlaveLogVoltage = {
 | 
					 | 
				
			||||||
			type: 'slaveLog',
 | 
					 | 
				
			||||||
			slaveId,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			logType: 'voltage',
 | 
					 | 
				
			||||||
			startIndex,
 | 
					 | 
				
			||||||
			voltages: voltages as [number, number, number, number]
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
		return msg;
 | 
					 | 
				
			||||||
	} else if (logIndex == N_CELLS_PER_SLAVE / 4) {
 | 
					 | 
				
			||||||
		const msg: SlaveLogLastCell = {
 | 
					 | 
				
			||||||
			type: 'slaveLog',
 | 
					 | 
				
			||||||
			slaveId,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			logType: 'lastCell',
 | 
					 | 
				
			||||||
			voltage: data.readUInt16BE(0) * 1e-3,
 | 
					 | 
				
			||||||
			failed_temp_sensors: data.readInt32BE(2)
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
		return msg;
 | 
					 | 
				
			||||||
	} else if (logIndex < N_CELLS_PER_SLAVE / 4 + 1 + N_TEMP_SENSORS_PER_SLAVE / 8) {
 | 
					 | 
				
			||||||
		const temperatures = [];
 | 
					 | 
				
			||||||
		for (let i = 0; i < 8; i++) {
 | 
					 | 
				
			||||||
			temperatures.push(data.readInt8(i) * 1);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		const msg: SlaveLogTemperature = {
 | 
					 | 
				
			||||||
			type: 'slaveLog',
 | 
					 | 
				
			||||||
			slaveId,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			logType: 'temperature',
 | 
					 | 
				
			||||||
			startIndex: (logIndex - N_CELLS_PER_SLAVE / 4 - 1) * 8,
 | 
					 | 
				
			||||||
			temperatures: temperatures as [number, number, number, number, number, number, number, number]
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
		return msg;
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		console.warn('Unknown slave log index', msg.id);
 | 
					 | 
				
			||||||
		return null;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function POST() {
 | 
					export function POST() {
 | 
				
			||||||
	return produce(async function start({ emit }) {
 | 
						return produce(async function start({ emit }) {
 | 
				
			||||||
@ -139,36 +21,14 @@ export function POST() {
 | 
				
			|||||||
			{ id: CAN_ID_AMS_ERROR, mask: 0xfff },
 | 
								{ id: CAN_ID_AMS_ERROR, mask: 0xfff },
 | 
				
			||||||
			{ id: CAN_ID_AMS_STATUS, mask: 0xfff },
 | 
								{ id: CAN_ID_AMS_STATUS, mask: 0xfff },
 | 
				
			||||||
			{ id: CAN_ID_AMS_SLAVE_STATUS_BASE, mask: CAN_ID_AMS_SLAVE_STATUS_MASK },
 | 
								{ id: CAN_ID_AMS_SLAVE_STATUS_BASE, mask: CAN_ID_AMS_SLAVE_STATUS_MASK },
 | 
				
			||||||
			{ id: CAN_ID_AMS_SLAVE_LOG_BASE, mask: CAN_ID_AMS_SLAVE_LOG_MASK }
 | 
								{ id: CAN_ID_AMS_SLAVE_LOG_BASE, mask: CAN_ID_AMS_SLAVE_LOG_MASK },
 | 
				
			||||||
 | 
								{ id: CAN_ID_SHUNT_CURRENT, mask: 0xfff },
 | 
				
			||||||
 | 
								{ id: CAN_ID_SHUNT_VOLTAGE_1, mask: 0xfff },
 | 
				
			||||||
 | 
								{ id: CAN_ID_SHUNT_VOLTAGE_2, mask: 0xfff }
 | 
				
			||||||
		]);
 | 
							]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		network.addListener('onMessage', (msg: RawMessage) => {
 | 
							network.addListener('onMessage', (msg: RawMessage) => {
 | 
				
			||||||
			if (msg.ext || msg.canfd) {
 | 
								emit('message', JSON.stringify(marshalMessage(msg)));
 | 
				
			||||||
				console.warn('invalid frame', msg);
 | 
					 | 
				
			||||||
				return;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			let message: AMSMessage | null = null;
 | 
					 | 
				
			||||||
			if ((msg.id & CAN_ID_AMS_SLAVE_STATUS_MASK) == CAN_ID_AMS_SLAVE_STATUS_BASE) {
 | 
					 | 
				
			||||||
				message = decodeSlaveStatus(msg);
 | 
					 | 
				
			||||||
			} else if ((msg.id & CAN_ID_AMS_SLAVE_LOG_MASK) == CAN_ID_AMS_SLAVE_LOG_BASE) {
 | 
					 | 
				
			||||||
				message = decodeSlaveLog(msg);
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				switch (msg.id) {
 | 
					 | 
				
			||||||
					case CAN_ID_AMS_ERROR:
 | 
					 | 
				
			||||||
						message = decodeError(msg);
 | 
					 | 
				
			||||||
						break;
 | 
					 | 
				
			||||||
					case CAN_ID_AMS_STATUS:
 | 
					 | 
				
			||||||
						message = decodeStatus(msg);
 | 
					 | 
				
			||||||
						break;
 | 
					 | 
				
			||||||
					default:
 | 
					 | 
				
			||||||
						console.warn('unknown frame', msg);
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (message) {
 | 
					 | 
				
			||||||
				emit('message', JSON.stringify(message));
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		network.start();
 | 
							network.start();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,21 +1,38 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
	import type { AMSStatus } from '$lib/messages';
 | 
						import { AMSState, type AMSStatus } from '$lib/messages';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let status: AMSStatus | undefined;
 | 
						export let status: AMSStatus | undefined;
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if status}
 | 
					<div class="master-content">
 | 
				
			||||||
	<h2>Master Status</h2>
 | 
						{#if status}
 | 
				
			||||||
	<div class="status-table">
 | 
							<h2><b>Master Status</b></h2>
 | 
				
			||||||
		<div>State:</div>
 | 
							<div class="log-collumn">
 | 
				
			||||||
		<div>{status.state}</div>
 | 
								<div>State: {AMSState[status.state]}</div>
 | 
				
			||||||
		<div>SDC Closed:</div>
 | 
								<div>SDC Closed: {status.sdcClosed}</div>
 | 
				
			||||||
		<div>{status.sdcClosed}</div>
 | 
								<div>SoC: {status.soc}%</div>
 | 
				
			||||||
		<div>SoC:</div>
 | 
								<div>Min. cell voltage: {Math.round(status.minCellVolt * 100) / 100}</div>
 | 
				
			||||||
		<div>{status.soc}</div>
 | 
								<div>Max. cell temperature: {Math.round(status.maxCellTemp * 100) / 100}</div>
 | 
				
			||||||
		<div>Min. cell voltage:</div>
 | 
					 | 
				
			||||||
		<div>{status.minCellVolt}</div>
 | 
					 | 
				
			||||||
		<div>Max. cell temperature:</div>
 | 
					 | 
				
			||||||
		<div>{status.maxCellTemp}</div>
 | 
					 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
{/if}
 | 
						{/if}
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.master-content {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							width: 12vw;
 | 
				
			||||||
 | 
							border: 2px solid black;
 | 
				
			||||||
 | 
							padding: 2px;
 | 
				
			||||||
 | 
							margin: 2px;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.log-collumn {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h2 {
 | 
				
			||||||
 | 
							text-align: center;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										82
									
								
								src/routes/overview.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/routes/overview.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import type { ShuntData } from '$lib/client';
 | 
				
			||||||
 | 
						import { AMSState, type AMSStatus } from '$lib/messages';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let amsStatus: AMSStatus | undefined;
 | 
				
			||||||
 | 
						export let shunt: ShuntData;
 | 
				
			||||||
 | 
						export let showPopout: boolean = false;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="flex flex-col w-[30rem] border-2 border-black p-1 m-1">
 | 
				
			||||||
 | 
						{#if showPopout}
 | 
				
			||||||
 | 
							<div class="flex flex-row justify-end mb-2">
 | 
				
			||||||
 | 
								<a
 | 
				
			||||||
 | 
									class="text-sm underline hover:no-underline"
 | 
				
			||||||
 | 
									href="/overview"
 | 
				
			||||||
 | 
									target="_blank"
 | 
				
			||||||
 | 
									on:click|preventDefault={() => {
 | 
				
			||||||
 | 
										window.open('/overview', 'newwindow', 'width=500,height=200,resizable=yes');
 | 
				
			||||||
 | 
										return false;
 | 
				
			||||||
 | 
									}}>Pop out</a
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
						<div class="grid grid-rows-3 gap-3">
 | 
				
			||||||
 | 
							{#if amsStatus != null}
 | 
				
			||||||
 | 
								<div class="progress" data-label="{amsStatus.soc}%">
 | 
				
			||||||
 | 
									<span class="value" style="width:{amsStatus.soc}%;"></span>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="grid grid-cols-4 gap-2">
 | 
				
			||||||
 | 
									<span class="text-center border-r">{shunt.voltage1.toFixed(1)} V</span>
 | 
				
			||||||
 | 
									<span class="text-center border-r">{shunt.current.toFixed(1)} A</span>
 | 
				
			||||||
 | 
									<span class="text-center border-r">{amsStatus.maxCellTemp.toFixed(1)}°C</span>
 | 
				
			||||||
 | 
									<span class="text-center">SDC {amsStatus.sdcClosed ? '✅' : '❌'}</span>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
								<div class="text-center">
 | 
				
			||||||
 | 
									<button
 | 
				
			||||||
 | 
										class="w-1/2 bg-green-600 text-white font-bold h-8 rounded-md hover:bg-green-800 disabled:bg-gray-600"
 | 
				
			||||||
 | 
										disabled={amsStatus.state != AMSState.TS_INACTIVE}
 | 
				
			||||||
 | 
										on:click={() => {
 | 
				
			||||||
 | 
											fetch('/start-charging', { method: 'POST' });
 | 
				
			||||||
 | 
										}}>Start Charging</button
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.overview {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							width: 24vw;
 | 
				
			||||||
 | 
							border: 2px solid black;
 | 
				
			||||||
 | 
							padding: 2px;
 | 
				
			||||||
 | 
							margin: 2px;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.progress {
 | 
				
			||||||
 | 
							height: 3em;
 | 
				
			||||||
 | 
							width: 100%;
 | 
				
			||||||
 | 
							background-color: white;
 | 
				
			||||||
 | 
							position: relative;
 | 
				
			||||||
 | 
							border: 1px solid black;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						.progress:before {
 | 
				
			||||||
 | 
							content: attr(data-label);
 | 
				
			||||||
 | 
							font-weight: bold;
 | 
				
			||||||
 | 
							color: darkgray;
 | 
				
			||||||
 | 
							font-size: 1em;
 | 
				
			||||||
 | 
							position: absolute;
 | 
				
			||||||
 | 
							text-align: center;
 | 
				
			||||||
 | 
							top: 50%;
 | 
				
			||||||
 | 
							transform: translateY(-50%);
 | 
				
			||||||
 | 
							left: 0;
 | 
				
			||||||
 | 
							right: 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						.progress .value {
 | 
				
			||||||
 | 
							background-color: green;
 | 
				
			||||||
 | 
							display: inline-block;
 | 
				
			||||||
 | 
							height: 100%;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
							
								
								
									
										10
									
								
								src/routes/overview/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/routes/overview/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import Overview from '../overview.svelte';
 | 
				
			||||||
 | 
						import { amsStatus, shunt } from '$lib/client';
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<svelte:head>
 | 
				
			||||||
 | 
						<title>Charging Overview</title>
 | 
				
			||||||
 | 
					</svelte:head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<Overview amsStatus={$amsStatus} shunt={$shunt} />
 | 
				
			||||||
							
								
								
									
										60
									
								
								src/routes/shunt-status-display.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/routes/shunt-status-display.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import type { ShuntData } from '$lib/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let data: ShuntData;
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="shunt-content">
 | 
				
			||||||
 | 
						<h2><b>Shunt Status</b></h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<div class="log-collumn">
 | 
				
			||||||
 | 
							<div class="log-entry">
 | 
				
			||||||
 | 
								<div>Current:</div>
 | 
				
			||||||
 | 
								<div><b>{data.current.toFixed(1)} A</b></div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="log-collumn">
 | 
				
			||||||
 | 
							<div class="log-entry">
 | 
				
			||||||
 | 
								<div>Voltage Accu:</div>
 | 
				
			||||||
 | 
								<div><b>{data.voltage1.toFixed(2)} V</b></div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div class="log-collumn">
 | 
				
			||||||
 | 
							<div class="log-entry">
 | 
				
			||||||
 | 
								<div>Voltage Vehicle:</div>
 | 
				
			||||||
 | 
								<div><b>{data.voltage2.toFixed(2)} V</b></div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.shunt-content {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							width: 12vw;
 | 
				
			||||||
 | 
							padding: 2px;
 | 
				
			||||||
 | 
							margin: 2px;
 | 
				
			||||||
 | 
							border: 2px solid black;
 | 
				
			||||||
 | 
							font-size: 14px;
 | 
				
			||||||
 | 
							margin-top: 10px;
 | 
				
			||||||
 | 
							background-color: hsl(0, 60%, var(--lightness));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.log-entry {
 | 
				
			||||||
 | 
							display: grid;
 | 
				
			||||||
 | 
							grid-template-columns: 5fr 3fr;
 | 
				
			||||||
 | 
							margin: 2px;
 | 
				
			||||||
 | 
							padding: 2px;
 | 
				
			||||||
 | 
							background-color: hsl(var(--hue), 80%, 80%);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.log-collumn {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							background-color: hsl(0, 60%, var(--lightness));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h2 {
 | 
				
			||||||
 | 
							text-align: center;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@ -1,40 +1,114 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { onMount } from 'svelte';
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	import type { SlaveStatus } from '$lib/messages';
 | 
						import type { SlaveStatus } from '$lib/messages';
 | 
				
			||||||
	import type { SlaveLogData } from '$lib/slave-log';
 | 
						import { SlaveLogData } from '$lib/slave-log';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let id: number;
 | 
						export let id: number;
 | 
				
			||||||
	export let status: SlaveStatus;
 | 
						export let status: SlaveStatus;
 | 
				
			||||||
	export let logData: SlaveLogData | undefined;
 | 
						export let logData: SlaveLogData | undefined;
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						let interval;
 | 
				
			||||||
 | 
						let lastMsgTime: number = 0;
 | 
				
			||||||
 | 
						let timeDifference: number = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						onMount(() => {
 | 
				
			||||||
 | 
							interval = setInterval(() => {
 | 
				
			||||||
 | 
								timeDifference = Date.now()-lastMsgTime;
 | 
				
			||||||
 | 
							}, 50);
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						$: totalVoltage = logData?.voltages.slice(0, 15).reduce((acc, x) => acc + x, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						$: {
 | 
				
			||||||
 | 
							slavePing(logData);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						function slavePing(logData: SlaveLogData | undefined): void {
 | 
				
			||||||
 | 
							lastMsgTime = Date.now();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<h2>Slave #{id}</h2>
 | 
					<div class="slave-content" style:--lightness={status.error ? '70%' : '100%'}>
 | 
				
			||||||
 | 
						<h2><b>Slave #{id} Δrx_t: {Math.round(timeDifference / 1000)}s</b></h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="status-table">
 | 
						<div class="log-collumn" style:--lightness={'' + Math.max(100 - ((timeDifference-200) / 150), 70) + '%'}>
 | 
				
			||||||
	<div>Error</div>
 | 
							<div class="log-entry">
 | 
				
			||||||
	<div>{status.error}</div>
 | 
								<div>Error:</div>
 | 
				
			||||||
	<div>Min. cell voltage</div>
 | 
								<div><b>{status.error}</b></div>
 | 
				
			||||||
	<div>{status.minCellVolt}</div>
 | 
							</div>
 | 
				
			||||||
	<div>Max. cell voltage</div>
 | 
							<div class="log-entry">
 | 
				
			||||||
	<div>{status.maxCellVolt}</div>
 | 
								<div>Min. cell voltage:</div>
 | 
				
			||||||
	<div>Max. temperature</div>
 | 
								<div><b>{Math.round(status.minCellVolt * 1000) / 1000}V</b></div>
 | 
				
			||||||
	<div>{status.maxTemp}</div>
 | 
							</div>
 | 
				
			||||||
	<div>SoC</div>
 | 
							<div class="log-entry">
 | 
				
			||||||
	<div>{status.soc}</div>
 | 
								<div>Max. cell voltage:</div>
 | 
				
			||||||
	<div>Failed temperature sensors</div>
 | 
								<div><b>{Math.round(status.maxCellVolt * 1000) / 1000}V</b></div>
 | 
				
			||||||
	<div>{logData?.failedTempSensors ?? 0}</div>
 | 
							</div>
 | 
				
			||||||
</div>
 | 
							<div class="log-entry">
 | 
				
			||||||
 | 
								<div>Max. temperature:</div>
 | 
				
			||||||
 | 
								<div><b>{Math.round(status.maxTemp * 100) / 100}°</b></div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<div class="log-entry">
 | 
				
			||||||
 | 
								<div>Total voltage:</div>
 | 
				
			||||||
 | 
								<div><b>{Math.round(totalVoltage * 100) / 100}V</b></div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
							<!-- <div>SoC</div>
 | 
				
			||||||
 | 
							<div>{status.soc}</div> -->
 | 
				
			||||||
 | 
							<!-- <div>Failed temperature sensors</div>
 | 
				
			||||||
 | 
							<div>{logData?.failedTempSensors ?? 0}</div> -->
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if logData}
 | 
						{#if logData}
 | 
				
			||||||
	<details>
 | 
							<div class="log-collumn">
 | 
				
			||||||
		<div class="status-table">
 | 
					 | 
				
			||||||
			{#each logData.voltages as volt, i}
 | 
								{#each logData.voltages as volt, i}
 | 
				
			||||||
				<div>V_{i}</div>
 | 
									{#if i < 15}
 | 
				
			||||||
				<div>{volt}</div>
 | 
										<div class="log-entry" style:--hue={Math.max((volt - 2.5) * 70.5, 0)}>
 | 
				
			||||||
 | 
											<div>V{i}:</div>
 | 
				
			||||||
 | 
											<div><b>{Math.round(volt * 1000) / 1000}V</b></div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{/if}
 | 
				
			||||||
			{/each}
 | 
								{/each}
 | 
				
			||||||
			{#each logData.temperatures as temp, i}
 | 
								{#each logData.temperatures as temp, i}
 | 
				
			||||||
				<div>T_{i}</div>
 | 
									{#if i > 33 && i < 44}
 | 
				
			||||||
				<div>{temp}</div>
 | 
										<div class="log-entry" style:--hue={210 - temp * 3.5}>
 | 
				
			||||||
 | 
											<div>T{i}:</div>
 | 
				
			||||||
 | 
											<div><b>{temp}°</b></div>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									{/if}
 | 
				
			||||||
			{/each}
 | 
								{/each}
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</details>
 | 
						{/if}
 | 
				
			||||||
{/if}
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
						.slave-content {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							width: 12vw;
 | 
				
			||||||
 | 
							padding: 2px;
 | 
				
			||||||
 | 
							margin: 2px;
 | 
				
			||||||
 | 
							border: 2px solid black;
 | 
				
			||||||
 | 
							font-size: 14px;
 | 
				
			||||||
 | 
							margin-top: 10px;
 | 
				
			||||||
 | 
							background-color: hsl(0, 60%, var(--lightness));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.log-entry {
 | 
				
			||||||
 | 
							display: grid;
 | 
				
			||||||
 | 
							grid-template-columns: 5fr 3fr;
 | 
				
			||||||
 | 
							margin: 2px;
 | 
				
			||||||
 | 
							padding: 2px;
 | 
				
			||||||
 | 
							background-color: hsl(var(--hue), 80%, 80%);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						.log-collumn {
 | 
				
			||||||
 | 
							display: flex;
 | 
				
			||||||
 | 
							flex-direction: column;
 | 
				
			||||||
 | 
							background-color: hsl(0, 60%, var(--lightness));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h2 {
 | 
				
			||||||
 | 
							text-align: center;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								src/routes/start-charging/+server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/routes/start-charging/+server.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					import { json } from '@sveltejs/kit';
 | 
				
			||||||
 | 
					import * as can from 'socketcan';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function POST() {
 | 
				
			||||||
 | 
						const network = can.createRawChannel('can0');
 | 
				
			||||||
 | 
						network.start();
 | 
				
			||||||
 | 
						network.send({ id: 0x00b, data: Buffer.from([0x01]), ext: false, rtr: false });
 | 
				
			||||||
 | 
						network.stop();
 | 
				
			||||||
 | 
						return json({ success: true }, { status: 201 });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import { sveltekit } from '@sveltejs/kit/vite';
 | 
					import { sveltekit } from '@sveltejs/kit/vite';
 | 
				
			||||||
import { defineConfig } from 'vite';
 | 
					import { defineConfig } from 'vite';
 | 
				
			||||||
 | 
					import { nodePolyfills } from 'vite-plugin-node-polyfills';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
	plugins: [sveltekit()]
 | 
						plugins: [sveltekit(), nodePolyfills()]
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user