Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7bd5827

Browse files
authoredSep 12, 2022
Merge pull request #16 from nmzaheer/main
Support for Part separator symbols added
2 parents eac6d39 + 42382c4 commit 7bd5827

File tree

7 files changed

+314
-30
lines changed

7 files changed

+314
-30
lines changed
 
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Test TypeScript
2+
3+
env:
4+
# See: https://github.com/actions/setup-node/#readme
5+
NODE_VERSION: 10.x
6+
7+
on:
8+
push:
9+
paths:
10+
- ".github/workflows/test-typescript-npm.ya?ml"
11+
- ".github/.?codecov.ya?ml"
12+
- "dev/.?codecov.ya?ml"
13+
- ".?codecov.ya?ml"
14+
- "jest.config.js"
15+
- "package.json"
16+
- "package-lock.json"
17+
- "tsconfig.json"
18+
- "**.js"
19+
- "**.jsx"
20+
- "**.ts"
21+
- "**.tsx"
22+
pull_request:
23+
paths:
24+
- ".github/workflows/test-typescript-npm.ya?ml"
25+
- "jest.config.js"
26+
- "package.json"
27+
- "package-lock.json"
28+
- "tsconfig.json"
29+
- "**.js"
30+
- "**.jsx"
31+
- "**.ts"
32+
- "**.tsx"
33+
schedule:
34+
# Run periodically to catch breakage caused by external changes.
35+
- cron: "0 13 * * WED"
36+
workflow_dispatch:
37+
repository_dispatch:
38+
39+
jobs:
40+
test:
41+
runs-on: ${{ matrix.operating-system }}
42+
43+
strategy:
44+
fail-fast: false
45+
46+
matrix:
47+
operating-system:
48+
- macos-latest
49+
- ubuntu-latest
50+
# The version of node-gyp used by this project (7.1.2) requires an older version of Visual Studio that is not
51+
# available in the latest Windows GitHub Actions runner.
52+
- windows-2019
53+
54+
steps:
55+
- name: Checkout repository
56+
uses: actions/checkout@v3
57+
58+
- name: Setup Node.js
59+
uses: actions/setup-node@v3
60+
with:
61+
node-version: ${{ env.NODE_VERSION }}
62+
63+
- name: Install dependencies
64+
run: npm install
65+
66+
- name: Run tests
67+
run: npm run-script test
68+
69+
- name: Send unit test coverage to Codecov
70+
if: runner.os == 'Linux'
71+
uses: codecov/codecov-action@v3
72+
with:
73+
fail_ci_if_error: ${{ github.repository == 'arduino/arduino-serial-plotter-webapp' }}

‎README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Serial Plotter WebApp
22

3+
[![Test TypeScript status](https://github.com/arduino/arduino-serial-plotter-webapp/actions/workflows/test-typescript-npm.yml/badge.svg)](https://github.com/arduino/arduino-serial-plotter-webapp/actions/workflows/test-typescript-npm.yml)
4+
35
This is a SPA that receives data points over WebSocket and prints graphs. The purpose is to provide a visual and live representation of data printed to the Serial Port.
46

57
The application is designed to be as agnostic as possible regarding how and where it runs. For this reason, it accepts different settings when it's launched in order to configure the look&feel and the connection parameters.
@@ -162,6 +164,7 @@ These are sent to the middleware to be stored and propagated to other clients.
162164
## Development
163165
164166
- `npm i` to install dependencies
167+
- `npm test` to run automated tests
165168
- `npm start` to run the application in development mode @ [http://localhost:3000](http://localhost:3000)
166169
167170
## Deployment

‎jest.config.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* For a detailed explanation regarding each configuration property, visit:
3+
* https://jestjs.io/docs/en/configuration.html
4+
*/
5+
6+
module.exports = {
7+
collectCoverage: true,
8+
coverageDirectory: "coverage",
9+
coverageReporters: ["lcov"],
10+
preset: "ts-jest",
11+
testEnvironment: "node",
12+
};

‎package-lock.json

Lines changed: 62 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scripts": {
77
"start": "react-scripts start",
88
"build": "react-scripts build",
9-
"test": "react-scripts test",
9+
"test": "jest",
1010
"eject": "react-scripts eject"
1111
},
1212
"files": [
@@ -49,6 +49,7 @@
4949
"chartjs-plugin-streaming": "^2.0.0",
5050
"eslint-config-prettier": "^8.3.0",
5151
"eslint-plugin-prettier": "^4.0.0",
52+
"jest": "^26.6.0",
5253
"luxon": "^2.1.0",
5354
"node-sass": "^6.0.1",
5455
"prettier": "^2.4.1",
@@ -60,6 +61,7 @@
6061
"react-scripts": "4.0.3",
6162
"react-select": "^5.1.0",
6263
"react-switch": "^6.0.0",
64+
"ts-jest": "^26.5.6",
6365
"typescript": "^4.4.3",
6466
"web-vitals": "^1.1.2",
6567
"worker-loader": "^3.0.8"

‎src/msgAggregatorWorker.test.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
Copyright (C) 2022 Arduino SA
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU Affero General Public License as published
6+
by the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU Affero General Public License for more details.
13+
14+
You should have received a copy of the GNU Affero General Public License
15+
along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
class WorkerStub {
19+
listener = (_: { data: { command: string } }) => {};
20+
addEventListener(_: string, listenerCallback: (event: object) => void) {
21+
this.listener = listenerCallback;
22+
}
23+
}
24+
let worker = new WorkerStub();
25+
(global as any).self = worker;
26+
27+
const messageAggregator = require("./msgAggregatorWorker");
28+
29+
beforeEach(() => {
30+
worker.listener({ data: { command: "cleanup" } });
31+
});
32+
33+
describe("Parsing data", () => {
34+
describe.each([
35+
["space", " "],
36+
["tab", "\t"],
37+
["comma", ","],
38+
])("%s field delimiter", (_, fieldDelimiter) => {
39+
describe.each([
40+
["trailing", fieldDelimiter],
41+
["no trailing", ""],
42+
])("%s", (_, trailingFieldDelimiter) => {
43+
describe.each([
44+
["LF", "\n"],
45+
["CRLF", "\r\n"],
46+
])("%s record delimiter", (_, recordDelimiter) => {
47+
test("single field", () => {
48+
const messages = [
49+
`0${trailingFieldDelimiter}${recordDelimiter}`,
50+
`1${trailingFieldDelimiter}${recordDelimiter}`,
51+
`2${trailingFieldDelimiter}${recordDelimiter}`,
52+
];
53+
54+
const assertion = {
55+
datasetNames: ["value 1"],
56+
parsedLines: [{ "value 1": 1 }, { "value 1": 2 }],
57+
};
58+
59+
expect(messageAggregator.parseSerialMessages(messages)).toEqual(
60+
assertion
61+
);
62+
});
63+
64+
test("multi-field", () => {
65+
const messages = [
66+
`0${trailingFieldDelimiter}${recordDelimiter}`,
67+
`1${fieldDelimiter}2${trailingFieldDelimiter}${recordDelimiter}`,
68+
`3${fieldDelimiter}4${trailingFieldDelimiter}${recordDelimiter}`,
69+
];
70+
71+
const assertion = {
72+
datasetNames: ["value 1", "value 2"],
73+
parsedLines: [
74+
{ "value 1": 1, "value 2": 2 },
75+
{ "value 1": 3, "value 2": 4 },
76+
],
77+
};
78+
79+
expect(messageAggregator.parseSerialMessages(messages)).toEqual(
80+
assertion
81+
);
82+
});
83+
84+
test("labeled", () => {
85+
const messages = [
86+
`0${trailingFieldDelimiter}${recordDelimiter}`,
87+
`label_1:1${fieldDelimiter}label_2:2${trailingFieldDelimiter}${recordDelimiter}`,
88+
`label_1:3${fieldDelimiter}label_2:4${trailingFieldDelimiter}${recordDelimiter}`,
89+
];
90+
91+
const assertion = {
92+
datasetNames: ["label_1", "label_2"],
93+
parsedLines: [
94+
{ label_1: 1, label_2: 2 },
95+
{ label_1: 3, label_2: 4 },
96+
],
97+
};
98+
99+
expect(messageAggregator.parseSerialMessages(messages)).toEqual(
100+
assertion
101+
);
102+
});
103+
104+
test("buffering", () => {
105+
// Incomplete record
106+
let messages = [
107+
`0${trailingFieldDelimiter}${recordDelimiter}`,
108+
`1${fieldDelimiter}`,
109+
];
110+
111+
// Incomplete message is buffered
112+
let assertion: {
113+
datasetNames: string[];
114+
parsedLines: { [key: string]: number }[];
115+
} = {
116+
datasetNames: [],
117+
parsedLines: [],
118+
};
119+
120+
expect(messageAggregator.parseSerialMessages(messages)).toEqual(
121+
assertion
122+
);
123+
124+
// Second part of the record
125+
messages = [`2${trailingFieldDelimiter}${recordDelimiter}`];
126+
127+
assertion = {
128+
datasetNames: ["value 1", "value 2"],
129+
parsedLines: [{ "value 1": 1, "value 2": 2 }],
130+
};
131+
132+
expect(messageAggregator.parseSerialMessages(messages)).toEqual(
133+
assertion
134+
);
135+
});
136+
});
137+
});
138+
});
139+
});
140+
141+
export {};

‎src/msgAggregatorWorker.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ ctx.addEventListener("message", (event) => {
1818

1919
let buffer = "";
2020
let discardFirstLine = true;
21-
const separator = "\n";
22-
var re = new RegExp(`(${separator})`, "g");
21+
const separator = "\r?\n";
22+
const delimiter = "[, \t]+"; // Serial Plotter protocol supports Comma, Space & Tab characters as delimiters
23+
var separatorRegex = new RegExp(`(${separator})`, "g");
24+
var delimiterRegex = new RegExp(delimiter, "g");
2325

2426
export const parseSerialMessages = (
2527
messages: string[]
@@ -31,10 +33,11 @@ export const parseSerialMessages = (
3133
// so we need to discard it and start aggregating from the first encountered separator
3234
let joinMessages = messages.join("");
3335
if (discardFirstLine) {
34-
const firstSeparatorIndex = joinMessages.indexOf(separator);
35-
if (firstSeparatorIndex > -1) {
36+
separatorRegex.lastIndex = 0; // Reset lastIndex to ensure match happens from beginning of string
37+
const separatorMatch = separatorRegex.exec(joinMessages);
38+
if (separatorMatch && separatorMatch.index > -1) {
3639
joinMessages = joinMessages.substring(
37-
firstSeparatorIndex + separator.length
40+
separatorMatch.index + separatorMatch[0].length
3841
);
3942
discardFirstLine = false;
4043
} else {
@@ -47,13 +50,14 @@ export const parseSerialMessages = (
4750

4851
//add any leftover from the buffer to the first line
4952
const messagesAndBuffer = ((buffer || "") + joinMessages)
50-
.split(re)
53+
.split(separatorRegex)
5154
.filter((message) => message.length > 0);
5255

5356
// remove the previous buffer
5457
buffer = "";
58+
separatorRegex.lastIndex = 0;
5559
// check if the last message contains the delimiter, if not, it's an incomplete string that needs to be added to the buffer
56-
if (messagesAndBuffer[messagesAndBuffer.length - 1] !== separator) {
60+
if (!separatorRegex.test(messagesAndBuffer[messagesAndBuffer.length - 1])) {
5761
buffer = messagesAndBuffer[messagesAndBuffer.length - 1];
5862
messagesAndBuffer.splice(-1);
5963
}
@@ -62,19 +66,21 @@ export const parseSerialMessages = (
6266
const parsedLines: { [key: string]: number }[] = [];
6367

6468
// for each line, explode variables
69+
separatorRegex.lastIndex = 0;
6570
messagesAndBuffer
66-
.filter((message) => message !== separator)
71+
.filter((message) => !separatorRegex.test(message))
6772
.forEach((message) => {
6873
const parsedLine: { [key: string]: number } = {};
6974

70-
//there are two supported formats:
71-
// format1: <value1> <value2> <value3>
72-
// format2: name1:<value1>,name2:<value2>,name3:<value3>
75+
// Part Separator symbols i.e. Space, Tab & Comma are fully supported
76+
// SerialPlotter protocol specifies 3 message formats. The following 2 formats are supported
77+
// Value only format: <value1> <value2> <value3>
78+
// Label-Value format: name1:<value1>,name2:<value2>,name3:<value3>
7379

7480
// if we find a colon, we assume the latter is being used
7581
let tokens: string[] = [];
7682
if (message.indexOf(":") > 0) {
77-
message.split(",").forEach((keyValue: string) => {
83+
message.split(delimiterRegex).forEach((keyValue: string) => {
7884
let [key, value] = keyValue.split(":");
7985
key = key && key.trim();
8086
value = value && value.trim();
@@ -83,8 +89,8 @@ export const parseSerialMessages = (
8389
}
8490
});
8591
} else {
86-
// otherwise they are spaces
87-
const values = message.split(/\s/);
92+
// otherwise they are unlabelled
93+
const values = message.split(delimiterRegex);
8894
values.forEach((value, i) => {
8995
if (value.length) {
9096
tokens.push(...[`value ${i + 1}`, value]);

0 commit comments

Comments
 (0)
Please sign in to comment.