Skip to content

Commit

Permalink
Refactor Dom: extract forms and some elements
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianscheit committed Feb 6, 2025
1 parent a8407cf commit 7a1f419
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 86 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# WEB Modbus
https://adrianscheit.github.io/web-modbus/

Currently it is is only a "beta" version.

Currently it is without a license, so legally the default policy is applicable (where all author rights are reserved).

## Goals:
Expand Down
7 changes: 3 additions & 4 deletions pages/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,20 @@
Source code is avaliable here: https://github.com/adrianscheit/web-modbus
</p>
<h2 class="error"></h2>
<form>
<form name="serial">
<fieldset>
<legend>Serial connection</legend>
<label>
Modbus mode
<select name="modbusmode" required>
<select name="mode" required>
<option>RTU</option>
<option>ASCII</option>
<option disabled>TCP</option>
</select>
</label>
<label>
baudRate
<input type="number" name="baudRate" list="baudRate" min="300" max="10000000" step="1" value="9600"
required />
<input type="number" name="baudRate" list="baudRate" min="300" max="10000000" step="1" required />
<datalist id="baudRate">
<option>300</option>
<option>600</option>
Expand Down
112 changes: 73 additions & 39 deletions src/dom.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,6 @@
import { Converters } from "./converters";
import { Frame } from "./frame";

export const extractFormData = <T>(form: any): T => {
return Object.fromEntries(
[...form].filter((it) => it.name).map((it: any): [string, string] => [it.name, it.value])
) as T;
};
export const setFormData = (form: any, data: any): void => {
[...form].forEach((field) => {
const value = data[field.name];
if (value != undefined) {
field.value = value;
}
});
};

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;
};
export const clearError = (): void => {
domError.nodeValue = ``;
};

const serialFieldset: HTMLFieldSetElement = document.querySelector('fieldset')!;
const sendFieldset: HTMLFieldSetElement = document.querySelector('form[name=send] fieldset')!;
export const setSerialFieldsetDisable = (disabled: boolean): void => {
serialFieldset.disabled = disabled;
};
export const setSendFieldsetDisable = (disabled: boolean): void => {
sendFieldset.disabled = disabled;
};

export class TableDataColumn {
readonly td: HTMLElement = document.createElement('td');
readonly csv: string;
Expand Down Expand Up @@ -66,7 +34,7 @@ const insertSniffedRow = (columns: TableDataColumn[]): void => {
if (snifferTable.childElementCount > 1000) {
snifferTable.removeChild(snifferTable.lastChild!);
}
allSniffedEntries.unshift(columns.map((it) => it.csv).join(','))
allSniffedEntries.unshift(columns.map((it) => it.csv).join(','));
};
export const insertFrameRow = (frame: Frame): void => insertSniffedRow(frame.getRow());

Expand All @@ -87,10 +55,76 @@ export const downloadAllSniffedEntries = (): void => {
a.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvString));
a.setAttribute('download', `sniffed data ${Frame.getDateTime()}.csv`);
a.click();
};

class DomForm<T> {
private readonly fieldset: HTMLFieldSetElement;
submit?: (data: T) => void;

constructor(private readonly element: Element) {
this.fieldset = element.querySelector('fieldset')!;
element.addEventListener('submit', (event) => {
event.preventDefault();
this.submit?.(this.getFormData());
});
}

getFormData(): T {
return Object.fromEntries(
[...this.element as any].filter((it) => it.name).map((it: any): [string, string] => [it.name, it.value])
) as T;
}

setFormData<T>(data: T): void {
[...this.element as any].forEach((field) => {
const value = (data as any)[field.name];
if (value !== undefined) {
field.value = value;
}
});
}

setFieldsetDisabled(disabled: boolean) {
this.fieldset.disabled = disabled;
}
}

export interface ConnectionFormData {
mode: 'ASCII' | 'RTU';
baudRate: number;
parity: ParityType;
stopBits?: 1 | 2;
dataBits?: 7 | 8;
}

export interface SendFormData {
slaveAddress: number;
functionCode: number;
hexData: string;
}
export const addLabel = (text: string, input: HTMLElement): HTMLLabelElement => {
const label = document.createElement('label');
label.appendChild(document.createTextNode(text));
label.appendChild(input);
return label;

export class Dom {
private static readonly errorText: Text = document.querySelector('h2.error')!.appendChild(document.createTextNode(''));

static reportError(error?: any): void {
console.error(error);
const errorMessage = `Error: ${error}`;
this.errorText.nodeValue = errorMessage;
}
static clearError(): void {
this.errorText.nodeValue = ``;
}

static readonly serialForm = new DomForm<ConnectionFormData>(document.querySelector('form[name=serial]')!);
static readonly sendForm = new DomForm<SendFormData>(document.querySelector('form[name=send]')!);
private static readonly functionCodeList = document.getElementById('functionCodeList')!;
static readonly downloadSnifferButton = document.getElementById('downloadSnifferButton')!;
static readonly clearSnifferButton = document.getElementById('clearSnifferButton')!;

static addFunctionCodeListOption(code: string, description: string): void {
const option = document.createElement('option');
option.value = code;
option.appendChild(document.createTextNode(`${Converters.byteToHex(+code)} ${description}`));
Dom.functionCodeList.appendChild(option);
}
}
8 changes: 2 additions & 6 deletions src/frame.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Converters } from "./converters";
import { dataFieldStrategies } from "./data-field";
import { setFormData, TableColumnButton, TableDataColumn } from "./dom";
import { Dom, TableColumnButton, TableDataColumn } from "./dom";
import { getFunctionCodeDescription } from "./function-codes";

export class Frame {
Expand Down Expand Up @@ -49,14 +49,10 @@ export class Frame {
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),
this.type === 'error' ? new TableDataColumn('', this.type) : new TableColumnButton('To send form', () => this.toSendForm()),
this.type === 'error' ? new TableDataColumn('', this.type) : new TableColumnButton('To send form', () => Dom.sendForm.setFormData(this)),
];
}

private toSendForm(): void {
setFormData(document.querySelector('form[name=send]'), this);
}

private getDataAsText(): string {
if (this.type === 'error') {
return `Invalid frame: 0x${this.hexData}`;
Expand Down
54 changes: 17 additions & 37 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,39 @@
/// <reference types="w3c-web-serial" />

import { AsciiModeStrategy, ModeStrategy, RtuModeStrategy } from "./mode";
import { clearError, clearSniffingTable, downloadAllSniffedEntries, extractFormData, insertFrameRow, reportError, setSerialFieldsetDisable } from "./dom";
import { clearSniffingTable, Dom, downloadAllSniffedEntries, insertFrameRow } from "./dom";
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) {
reportError('No serial support in this browser!');
setSerialFieldsetDisable(true);
reportError('No serial support in this browser. Use current version of Edge, Chrome or Opera.');
Dom.serialForm.setFieldsetDisabled(true);
}

document.getElementById('clearSnifferButton')!.addEventListener('click', () => {
Dom.clearSnifferButton.addEventListener('click', () => {
clearSniffingTable();
});
document.getElementById('downloadSnifferButton')!.addEventListener('click', () => {
Dom.downloadSnifferButton.addEventListener('click', () => {
downloadAllSniffedEntries();
});

interface ConnectionForm {
modbusmode: 'ASCII' | 'RTU';
baudRate: number;
parity: ParityType;
stopBits?: 1 | 2;
dataBits?: 7 | 8;
}

document.querySelector('form')!.addEventListener('submit', (event) => {
event.preventDefault();
const formData: ConnectionForm = extractFormData(event.target);
Dom.serialForm.submit = (formData) => {
formData.baudRate = +formData.baudRate;
formData.stopBits = formData.parity !== 'none' ? 1 : 2;
formData.dataBits = formData.modbusmode === 'ASCII' ? 7 : 8;
console.log(formData);
start(formData, formData.modbusmode === 'ASCII' ? new AsciiModeStrategy() : new RtuModeStrategy());
});
formData.dataBits = formData.mode === 'ASCII' ? 7 : 8;
start(formData, formData.mode === 'ASCII' ? new AsciiModeStrategy() : new RtuModeStrategy());
};

let send: ((bytest: number[]) => void) | undefined = undefined;
let send: ((bytest: number[]) => Promise<void>) | undefined = undefined;

const start = (serialOptions: SerialOptions, mode: ModeStrategy) => {
clearError();
Dom.clearError();
serial.requestPort().then((serialPort: SerialPort) => {
console.log('serialPort', serialPort);
serialPort.open(serialOptions).then(async () => {
setSerialFieldsetDisable(true);
Dom.serialForm.setFieldsetDisabled(true);
const writer = serialPort.writable?.getWriter();
if (writer) {
send = async (bytes: number[]) => {
Expand Down Expand Up @@ -75,22 +63,14 @@ const start = (serialOptions: SerialOptions, mode: ModeStrategy) => {
writer?.releaseLock();
send = undefined;
await serialPort.close();
setSerialFieldsetDisable(false);
Dom.sendForm.setFieldsetDisabled(false);
}, reportError);
}, console.warn);
};

const functionCodeList = document.getElementById('functionCodeList')!;
const addFunctionCodeListOption = (code: string, description: string): void => {
const option = document.createElement('option');
option.value = code;
option.appendChild(document.createTextNode(`${Converters.byteToHex(+code)} ${description}`));
functionCodeList.appendChild(option);
};
[...Object.entries(functionCodes), ...Object.entries(errorCodes)].forEach(([code, description]) => addFunctionCodeListOption(code, description));
document.querySelector('form[name=send]')!.addEventListener('submit', event => {
event.preventDefault();
const formData: { slaveAddress: number, functionCode: number, hexData: string } = extractFormData(event.target);
[...Object.entries(functionCodes), ...Object.entries(errorCodes)].forEach(([code, description]) => Dom.addFunctionCodeListOption(code, description));

Dom.sendForm.submit = (formData) => {
formData.slaveAddress = +formData.slaveAddress;
formData.functionCode = +formData.functionCode;
formData.hexData = formData.hexData.toString().replaceAll(/[\n\r\s]+/gm, '');
Expand All @@ -104,6 +84,6 @@ document.querySelector('form[name=send]')!.addEventListener('submit', event => {
if (send) {
send(frameBytes);
}
});
};

// intTest();

0 comments on commit 7a1f419

Please sign in to comment.