Compare commits

...

9 Commits

16 changed files with 8652 additions and 1769 deletions

6779
package-lock.json generated Normal file

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": {

80
src/lib/client.ts Normal file
View 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;
});
}

View File

@ -1,2 +1,2 @@
export const N_CELLS_PER_SLAVE = 15;
export const N_TEMP_SENSORS_PER_SLAVE = 32;
export const N_TEMP_SENSORS_PER_SLAVE = 44;

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';
type:
| 'error'
| 'status'
| 'slaveStatus'
| 'slaveLog'
| 'shuntCurrentLog'
| 'shuntVoltage1Log'
| 'shuntVoltage2Log';
}
export interface AMSError extends AMSMessage {
@ -68,3 +107,176 @@ export interface SlaveLogTemperature extends SlaveLog {
startIndex: 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;
}

View File

@ -0,0 +1,5 @@
<script lang="ts">
import '../app.css';
</script>
<slot />

View File

@ -1,59 +1,44 @@
<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 SlaveStatusDisplay from './slave-status-display.svelte';
import '../app.css';
import MasterErrorDisplay from './master-error-display.svelte';
import { SlaveLogData } from '$lib/slave-log';
// const value = source('/data').select('message');
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;
}
});
import ShuntStatusDisplay from './shunt-status-display.svelte';
import Overview from './overview.svelte';
import { amsStatus, error, shunt, slaveLog, slaveStatus } from '$lib/client';
</script>
<h1>FaSTTUBe Charger</h1>
<svelte:head>
<title>Charging</title>
</svelte:head>
<MasterErrorDisplay {error} />
<MasterStatusDisplay status={amsStatus} />
<div class="slaves">
{#each Object.entries(slaveStatus) as [id, status]}
<SlaveStatusDisplay {id} {status} logData={slaveLog[id]} />
{/each}
<div class="vertical-wrapper">
<Overview amsStatus={$amsStatus} shunt={$shunt} showPopout={true} />
<div class="wrapper">
<MasterErrorDisplay error={$error} />
<MasterStatusDisplay status={$amsStatus} />
<ShuntStatusDisplay data={$shunt} />
</div>
<div class="wrapper">
{#each Object.entries($slaveStatus) as [id, status]}
<SlaveStatusDisplay {id} {status} logData={$slaveLog[id]} />
{/each}
</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>

View File

@ -1,136 +1,18 @@
import { produce } from 'sveltekit-sse';
import * as can from 'socketcan';
import type {
AMSError,
AMSMessage,
AMSStatus,
SlaveLog,
SlaveLogLastCell,
SlaveLogTemperature,
SlaveLogVoltage,
SlaveStatus
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;
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() {
return produce(async function start({ emit }) {
@ -139,36 +21,14 @@ export function POST() {
{ id: CAN_ID_AMS_ERROR, 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_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) => {
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;
default:
console.warn('unknown frame', msg);
}
}
if (message) {
emit('message', JSON.stringify(message));
}
emit('message', JSON.stringify(marshalMessage(msg)));
});
network.start();

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,38 @@
<script lang="ts">
import type { AMSStatus } from '$lib/messages';
import { AMSState, type AMSStatus } from '$lib/messages';
export let status: AMSStatus | undefined;
</script>
{#if status}
<h2>Master Status</h2>
<div class="status-table">
<div>State:</div>
<div>{status.state}</div>
<div>SDC Closed:</div>
<div>{status.sdcClosed}</div>
<div>SoC:</div>
<div>{status.soc}</div>
<div>Min. cell voltage:</div>
<div>{status.minCellVolt}</div>
<div>Max. cell temperature:</div>
<div>{status.maxCellTemp}</div>
</div>
{/if}
<div class="master-content">
{#if status}
<h2><b>Master Status</b></h2>
<div class="log-collumn">
<div>State: {AMSState[status.state]}</div>
<div>SDC Closed: {status.sdcClosed}</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>
{/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>

View 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>

View 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} />

View 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>

View File

@ -1,40 +1,114 @@
<script lang="ts">
import { onMount } from 'svelte';
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 status: SlaveStatus;
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>
<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>Error</div>
<div>{status.error}</div>
<div>Min. cell voltage</div>
<div>{status.minCellVolt}</div>
<div>Max. cell voltage</div>
<div>{status.maxCellVolt}</div>
<div>Max. temperature</div>
<div>{status.maxTemp}</div>
<div>SoC</div>
<div>{status.soc}</div>
<div>Failed temperature sensors</div>
<div>{logData?.failedTempSensors ?? 0}</div>
</div>
<div class="log-collumn" style:--lightness={'' + Math.max(100 - ((timeDifference-200) / 150), 70) + '%'}>
<div class="log-entry">
<div>Error:</div>
<div><b>{status.error}</b></div>
</div>
<div class="log-entry">
<div>Min. cell voltage:</div>
<div><b>{Math.round(status.minCellVolt * 1000) / 1000}V</b></div>
</div>
<div class="log-entry">
<div>Max. cell voltage:</div>
<div><b>{Math.round(status.maxCellVolt * 1000) / 1000}V</b></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}
<details>
<div class="status-table">
{#if logData}
<div class="log-collumn">
{#each logData.voltages as volt, i}
<div>V_{i}</div>
<div>{volt}</div>
{#if i < 15}
<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 logData.temperatures as temp, i}
<div>T_{i}</div>
<div>{temp}</div>
{#if i > 33 && i < 44}
<div class="log-entry" style:--hue={210 - temp * 3.5}>
<div>T{i}:</div>
<div><b>{temp}°</b></div>
</div>
{/if}
{/each}
</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>

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()]
});