Skip to content

Commit

Permalink
Refactoring: moving functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianscheit committed Feb 6, 2025
1 parent 2770302 commit 0ae1f3c
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 93 deletions.
5 changes: 2 additions & 3 deletions pages/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ <h2 class="error"></h2>
<select name="modbusmode" required>
<option>RTU</option>
<option>ASCII</option>
<option disabled>TCP</option>
</select>
</label>
<label>
Expand Down Expand Up @@ -141,9 +142,8 @@ <h3>Send frame (not implemented yet)</h3>
<h3>Sniffer</h3>
<fieldset>
<legend>options</legend>
<button type="button" id="downloadSnifferButton">Download all sniffed data</button>
<button type="button" id="clearSnifferButton">Clear all sniffed data</button>
<label><input type="checkbox" checked name="onlyValid" />Accept only a valid data field (applies only to
RTU)</label>
<label><input type="checkbox" name="Uint8" />Uint8</label>
<label><input type="checkbox" name="Int8" />Int8</label>
<label><input type="checkbox" name="Uint16" />Uint16</label>
Expand All @@ -152,7 +152,6 @@ <h3>Sniffer</h3>
<label><input type="checkbox" name="Int32" />Int32</label>
<label><input type="checkbox" checked name="Float32" />Float32</label>
<label><input type="checkbox" name="Float64" />Float64</label>
<button type="button" id="downloadSnifferButton">Download all sniffed data</button>
</fieldset>
<p>First frams are for CRC/LRC/formats testing purpose only</p>
<table>
Expand Down
52 changes: 4 additions & 48 deletions src/dom.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { Converters } from "./converters";
import { Frame } from "./frame";
import { getFunctionCodeDescription } from "./function-codes";

const getDateTime = (): string => {
const now = new Date();
return `${now.toLocaleString()}+${now.getMilliseconds()}ms`;
};

const domError: Text = document.querySelector('h2.error')!.appendChild(document.createTextNode(''));
export const reportError = (error?: any): void => {
console.error(error);
const errorMessage = `Error: ${error}`;
domError.nodeValue = errorMessage;
insertErrorRow(errorMessage, undefined);
};
export const clearError = (): void => {
domError.nodeValue = ``;
Expand All @@ -27,7 +19,6 @@ export const setSendFieldsetDisable = (disabled: boolean): void => {
sendFieldset.disabled = disabled;
};


export class TableDataColumn {
readonly td: HTMLElement = document.createElement('td');
readonly csv: string;
Expand All @@ -37,13 +28,13 @@ export class TableDataColumn {
if (className) {
this.td.classList.add(className);
}
this.csv = `${`${text}`.replace(/[\n\r]/gm, "").replace(/,/gm, ';')}`;
this.csv = `"${`${text}`.replaceAll(/[\n\r,"]/gm, "")}"`;
}
}

const allSniffedEntries: string[] = [];
const snifferTable: HTMLElement = document.querySelector('tbody')!;
export const insertSniffedRow = (columns: TableDataColumn[]): void => {
const insertSniffedRow = (columns: TableDataColumn[]): void => {
const tr = document.createElement('tr');
columns.forEach((it) => tr.appendChild(it.td));
snifferTable.insertBefore(tr, snifferTable.firstChild);
Expand All @@ -52,39 +43,7 @@ export const insertSniffedRow = (columns: TableDataColumn[]): void => {
}
allSniffedEntries.unshift(columns.map((it) => it.csv).join(','))
};
export const insertFrameRow = (frame: Frame, className: '' | 'send' = ''): void => {
const columns = [
getDateTime(),
`${frame.slaveAddress}`,
`${frame.functionCode} ${getFunctionCodeDescription(frame.functionCode)}`,
`${frame.data.length}`,
].map((it) => new TableDataColumn(it, className));

if (frame.isUnknownFrame()) {
columns.push(new TableDataColumn(`Unknown frame: 0x${Converters.bytesAsHex(frame.data)}`, 'error'));
} else if (frame.isNoValidDataFormat()) {
columns.push(new TableDataColumn(`This frame format does not fit to the function:
fromMasterToSlaveError=${frame.fromMasterToSlaveError}
fromSlaveToMasterError=${frame.fromSlaveToMasterError}
, for: 0x${Converters.bytesAsHex(frame.data)}`, 'error'));
} else {
columns.push(new TableDataColumn(
JSON.stringify(frame.fromMasterToSlave) +
JSON.stringify(frame.fromSlaveToMaster)
));
}

insertSniffedRow(columns);
};
export const insertErrorRow = (errorMessage: string, dataLength: number | undefined): void => {
insertSniffedRow([
getDateTime(),
``,
``,
`${dataLength === undefined ? '' : dataLength}`,
errorMessage,
].map((it) => new TableDataColumn(it, 'error')));
};
export const insertFrameRow = (frame: Frame): void => insertSniffedRow(frame.getRow());

export const getInputChecked = (name: string): boolean => {
const input: HTMLInputElement = document.querySelector(`input[type=checkbox][name=${name}]`)!;
Expand All @@ -98,13 +57,10 @@ export const clearSniffingTable = (): void => {
}
};
export const downloadAllSniffedEntries = (): void => {
if (allSniffedEntries.length === 0) {
return;
}
const csvString = allSniffedEntries.join('\r\n');
const a = window.document.createElement('a');
a.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvString));
a.setAttribute('download', `sniffed data ${getDateTime()}.csv`);
a.setAttribute('download', `sniffed data ${Frame.getDateTime()}.csv`);
a.click();
}
export const addLabel = (text: string, input: HTMLElement): HTMLLabelElement => {
Expand Down
74 changes: 57 additions & 17 deletions src/frame.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,80 @@
import { Converters } from "./converters";
import { dataFieldStrategies } from "./data-field";
import { TableDataColumn } from "./dom";
import { getFunctionCodeDescription } from "./function-codes";

export class Frame {
readonly slaveAddress: number;
readonly functionCode: number;
readonly functionCode?: number;
readonly fromMasterToSlave?: any;
readonly fromSlaveToMaster?: any;
readonly fromMasterToSlaveError?: string;
readonly fromSlaveToMasterError?: string;

constructor(readonly data: number[]) {
constructor(readonly data: number[], readonly type: 'error' | 'send' | '') {
this.slaveAddress = data.shift()!;
this.functionCode = data.shift()!;
const specificFormat = dataFieldStrategies[this.functionCode];
if (specificFormat) {
try {
this.fromMasterToSlave = new specificFormat.fromMasterToSlave(data);
} catch (e: any) {
this.fromMasterToSlaveError = this.getError(e);
}
try {
this.fromSlaveToMaster = new specificFormat.fromSlaveToMaster(data);
} catch (e: any) {
this.fromSlaveToMasterError = this.getError(e);
this.functionCode = data.shift();
if (type !== 'error' || this.functionCode === undefined) {
const specificFormat = dataFieldStrategies[this.functionCode!];
if (specificFormat) {
try {
this.fromMasterToSlave = new specificFormat.fromMasterToSlave(data);
} catch (e: any) {
this.fromMasterToSlaveError = this.getError(e);
}
try {
this.fromSlaveToMaster = new specificFormat.fromSlaveToMaster(data);
} catch (e: any) {
this.fromSlaveToMasterError = this.getError(e);
}
}
}
}

isNoValidDataFormat(): boolean {
static getDateTime(): string {
const now = new Date();
return `${now.toLocaleString()}+${now.getMilliseconds()}ms`;
}

getDataLength(): number {
return this.data.length;
}

getDataAsHex(): string {
return Converters.bytesAsHex(this.data);
}

getRow(): TableDataColumn[] {
return [
new TableDataColumn(Frame.getDateTime(), this.type),
new TableDataColumn(`${this.slaveAddress}=0x${Converters.byteToHex(this.slaveAddress!)}`, this.type),
new TableDataColumn(`${this.functionCode}=${this.functionCode === undefined ? '' : getFunctionCodeDescription(this.functionCode)}`, this.type),
new TableDataColumn(this.getDataLength().toString(), this.type),
new TableDataColumn(this.getDataAsText(), this.isNoValidDataFormat() ? 'error' : this.type),
];
}

private getDataAsText(): string {
if (this.type === 'error') {
return `Invalid frame: 0x${this.getDataAsHex()}`;
} else if (this.isUnknownFrame()) {
return `Function code is invalid: 0x${this.getDataAsHex()}`;
} else if (this.isNoValidDataFormat()) {
return `This frame format does not fit to the function code: fromMasterToSlaveError=${this.fromMasterToSlaveError}; fromSlaveToMasterError=${this.fromSlaveToMasterError}; for: 0x${this.getDataAsHex()}`;
} else {
return `Valid frame: fromMasterToSlave=${JSON.stringify(this.fromMasterToSlave)}; fromSlaveToMasterError=${JSON.stringify(this.fromSlaveToMaster)}`;
}
}

private isNoValidDataFormat(): boolean {
return !!this.fromMasterToSlaveError && !!this.fromSlaveToMasterError;
}

isUnknownFrame(): boolean {
private isUnknownFrame(): boolean {
return !this.isNoValidDataFormat() && !this.fromMasterToSlave && !this.fromSlaveToMaster;
}

protected getError(e: any): string {
return `${e.message}`;
return e.message;
}
}
6 changes: 3 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { clearError, clearSniffingTable, downloadAllSniffedEntries, insertFrameR
import { Frame } from "./frame";
import { errorCodes, functionCodes } from "./function-codes";
import { intTest } from "./int.spec";
import { Converters } from "./converters";

const serial: Serial = navigator.serial;
if (!serial) {
Expand Down Expand Up @@ -76,7 +77,7 @@ const functionCodeList = document.getElementById('functionCodeList')!;
const addFunctionCodeListOption = (code: string, description: string): void => {
const option = document.createElement('option');
option.value = code;
option.appendChild(document.createTextNode(description));
option.appendChild(document.createTextNode(`${Converters.byteToHex(+code)} ${description}`));
functionCodeList.appendChild(option);
};
[...Object.entries(functionCodes), ...Object.entries(errorCodes)].forEach(([code, description]) => addFunctionCodeListOption(code, description));
Expand All @@ -89,11 +90,10 @@ document.querySelector('form[name=send]')!.addEventListener('submit', event => {
let data = (formData.data.length & 1 ? '0' : '') + formData.data;
const bytes: number[] = [];
for (let i = 0; i < data.length; i += 2) {
console.log(data.substring(i, 2));
bytes.push(parseInt(data.substring(i, i + 2), 16));
}
const frameBytes: number[] = [formData.slaveAddress, formData.functionCode, ...bytes];
insertFrameRow(new Frame([...frameBytes]), 'send');
insertFrameRow(new Frame([...frameBytes], 'send'));
new AsciiModeStrategy().send(frameBytes);
new RtuModeStrategy().send(frameBytes);
});
Expand Down
33 changes: 11 additions & 22 deletions src/mode.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { Converters } from "./converters";
import { getInputChecked, insertErrorRow, insertFrameRow } from "./dom";
import { insertFrameRow } from "./dom";
import { Frame } from "./frame";

export const reportValidFrame = (frame: Frame): void => {
export const reportFrame = (frame: Frame): void => {
insertFrameRow(frame);
};

export const reportInvalidData = (bytes: number[]): void => {
insertErrorRow(Converters.bytesAsHex(bytes), bytes.length);
};

export abstract class ModeStrategy {
abstract receive(data: Uint8Array): void;
abstract send(bytes: number[]): Uint8Array;
Expand Down Expand Up @@ -51,31 +47,24 @@ export class RtuModeStrategy extends ModeStrategy {
for (let i = 0; i < this.history.length; ++i) {
if (this.history[i].updateCrc(byte)) {
const bytes = this.history.map((it) => it.byte);
const validFrame = new Frame(bytes.slice(i, -2));
if (getInputChecked('onlyValid')) {
if (this.history.length < 4 || validFrame.isNoValidDataFormat()) {
console.warn(`Found end of frame (i=${i}) but the data field seems invalid!`, validFrame);
continue;
}
}
if (i) {
reportInvalidData(bytes.slice(0, i));
reportFrame(new Frame(bytes.slice(0, i), 'error'));
}
reportValidFrame(validFrame);
const validFrame = new Frame(bytes.slice(i, -2), '');
reportFrame(validFrame);
this.history = [];
clearTimeout(this.timeoutHandler!);
break;
}
}
if (this.history.length > 300) {
console.warn('Rejecteding because history bigger than 300', this.history.shift());
if (this.history.length > 2300) {
reportFrame(new Frame(this.history.splice(0, 200).map((it) => it.byte), 'error'))
}
});
}

private resetFrame(): void {
console.warn('timeout');
reportInvalidData(this.history.map((it) => it.byte));
reportFrame(new Frame(this.history.map((it) => it.byte), 'error'));
this.history = [];
}

Expand Down Expand Up @@ -108,9 +97,9 @@ export class AsciiModeStrategy extends ModeStrategy {
} else if (char === 0x0D && char2 === 0x0A) {
this.frameBytes.pop();
if (!isNaN(this.currentLrc) && (this.currentLrc & 0xff) === 0) {
reportValidFrame(new Frame(this.frameBytes))
reportFrame(new Frame(this.frameBytes, ''));
} else {
reportInvalidData(this.frameBytes);
reportFrame(new Frame(this.frameBytes, 'error'));
}
this.frameBytes = [];
this.currentLrc = 0x00;
Expand All @@ -125,7 +114,7 @@ export class AsciiModeStrategy extends ModeStrategy {

private resetFrame(): void {
if (this.frameBytes.length) {
reportInvalidData(this.frameBytes);
reportFrame(new Frame(this.frameBytes, 'error'));
this.frameBytes = [];
this.currentLrc = 0x00;
}
Expand Down

0 comments on commit 0ae1f3c

Please sign in to comment.