Compare commits

...

2 Commits

Author SHA1 Message Date
Jasper Blanckenburg a34a70ef2f Add overview box 2024-09-05 23:11:43 +02:00
Jasper Blanckenburg 1f0c9779f6 Unmarshal messages client side 2024-09-05 22:24:01 +02:00
9 changed files with 1640 additions and 200 deletions

1306
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,8 @@
"tailwindcss": "^3.4.3",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3"
"vite": "^5.0.3",
"vite-plugin-node-polyfills": "^0.22.0"
},
"type": "module",
"dependencies": {

View File

@ -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 {
type: 'error' | 'status' | 'slaveStatus' | 'slaveLog' | 'shuntCurrentLog' | 'shuntVoltage1Log' | 'shuntVoltage2Log';
type:
| 'error'
| 'status'
| 'slaveStatus'
| 'slaveLog'
| 'shuntCurrentLog'
| 'shuntVoltage1Log'
| 'shuntVoltage2Log';
}
export interface AMSError extends AMSMessage {
@ -73,4 +112,171 @@ 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;
}

View File

@ -1,5 +1,15 @@
<script lang="ts">
import type { AMSError, AMSMessage, AMSStatus, SlaveLog, SlaveStatus, ShuntLog } from '$lib/messages';
import {
type AMSError,
type AMSMessage,
type AMSStatus,
type SlaveLog,
type SlaveStatus,
type ShuntLog,
type MarshalledMessage,
unmarshalMessage,
decodeMessage
} from '$lib/messages';
import { source } from 'sveltekit-sse';
import MasterStatusDisplay from './master-status-display.svelte';
import SlaveStatusDisplay from './slave-status-display.svelte';
@ -7,7 +17,7 @@
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';
let error: AMSError | undefined;
let amsStatus: AMSStatus | undefined;
@ -31,7 +41,11 @@
if (!value) {
return;
}
const msg = JSON.parse(value) as AMSMessage;
const msg = decodeMessage(unmarshalMessage(JSON.parse(value) as MarshalledMessage));
if (!msg) {
return;
}
switch (msg.type) {
case 'error':
error = msg as AMSError;
@ -60,15 +74,22 @@
});
</script>
<div class="wrapper">
<div class="vertical-wrapper">
<div class="vertical-wrapper">
<Overview {amsStatus} current={shuntCurrentLog?.value} voltage={shuntVoltage1Log?.value} />
<div class="wrapper">
<MasterErrorDisplay {error} />
<MasterStatusDisplay status={amsStatus} />
<ShuntStatusDisplay currentLogData={shuntCurrentLog} voltage1LogData={shuntVoltage1Log} voltage2LogData={shuntVoltage2Log} />
<ShuntStatusDisplay
currentLogData={shuntCurrentLog}
voltage1LogData={shuntVoltage1Log}
voltage2LogData={shuntVoltage2Log}
/>
</div>
<div class="wrapper">
{#each Object.entries(slaveStatus) as [id, status]}
<SlaveStatusDisplay {id} {status} logData={slaveLog[id]} />
{/each}
</div>
{#each Object.entries(slaveStatus) as [id, status]}
<SlaveStatusDisplay {id} {status} logData={slaveLog[id]} />
{/each}
</div>
<style>
@ -87,4 +108,4 @@
margin: 0;
padding: 0;
}
</style>
</style>

View File

@ -1,155 +1,18 @@
import { produce } from 'sveltekit-sse';
import * as can from 'socketcan';
import type {
AMSError,
AMSMessage,
AMSStatus,
SlaveLog,
SlaveLogLastCell,
SlaveLogTemperature,
SlaveLogVoltage,
SlaveStatus,
ShuntLog
import {
type RawMessage,
marshalMessage,
CAN_ID_AMS_ERROR,
CAN_ID_AMS_SLAVE_STATUS_MASK,
CAN_ID_AMS_SLAVE_LOG_MASK,
CAN_ID_AMS_SLAVE_LOG_BASE,
CAN_ID_AMS_SLAVE_STATUS_BASE,
CAN_ID_AMS_STATUS,
CAN_ID_SHUNT_CURRENT,
CAN_ID_SHUNT_VOLTAGE_1,
CAN_ID_SHUNT_VOLTAGE_2
} 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;
const CAN_ID_SHUNT_CURRENT = 0x521;
const CAN_ID_SHUNT_VOLTAGE_1 = 0x522;
const CAN_ID_SHUNT_VOLTAGE_2 = 0x523;
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;
}
}
function decodeShunt(msg: RawMessage, type: String): ShuntLog | null {
if (msg.data.length != 6) {
console.warn('invalid shunt log frame', msg);
return null;
}
const data = msg.data;
return {
type: type,
value: data.readInt32BE(2) * 1e-3,
};
}
export function POST() {
return produce(async function start({ emit }) {
@ -165,39 +28,7 @@ export function POST() {
]);
network.addListener('onMessage', (msg: RawMessage) => {
if (msg.ext || msg.canfd) {
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;
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;
}
}
if (message) {
emit('message', JSON.stringify(message));
}
emit('message', JSON.stringify(marshalMessage(msg)));
});
network.start();

View File

@ -1,5 +1,5 @@
<script lang="ts">
import type { AMSStatus } from '$lib/messages';
import { AMSState, type AMSStatus } from '$lib/messages';
export let status: AMSStatus | undefined;
</script>
@ -8,9 +8,9 @@
{#if status}
<h2><b>Master Status</b></h2>
<div class="log-collumn">
<div>State: {status.state}</div>
<div>State: {AMSState[status.state]}</div>
<div>SDC Closed: {status.sdcClosed}</div>
<div>SoC: {status.soc}</div>
<div>SoC: {status.soc}%</div>
<div>Min. cell voltage: {Math.round(status.minCellVolt * 100) / 100}</div>
<div>Max. cell temperature: {Math.round(status.maxCellTemp * 100) / 100}</div>
</div>

View File

@ -0,0 +1,66 @@
<script lang="ts">
import { AMSState, type AMSStatus } from '$lib/messages';
export let amsStatus: AMSStatus | undefined;
export let current: number | undefined;
export let voltage: number | undefined;
</script>
<div class="overview grid grid-rows-3 gap-3">
{#if amsStatus != null && current != null && voltage != 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">{voltage.toFixed(1)} V</span>
<span class="text-center border-r">{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>
<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>

View 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 });
}

View File

@ -1,6 +1,7 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { nodePolyfills } from 'vite-plugin-node-polyfills';
export default defineConfig({
plugins: [sveltekit()]
plugins: [sveltekit(), nodePolyfills()]
});