Skip to content

Commit

Permalink
feat: add functionality to set DTR/RTS line status on serial port open
Browse files Browse the repository at this point in the history
  • Loading branch information
cheton committed Jul 11, 2023
1 parent 50347c9 commit 6636362
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 27 deletions.
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -135,7 +135,7 @@
"@fortawesome/react-fontawesome": "~0.2.0",
"@serialport/parser-readline": "^10.0.0",
"@tanstack/react-table": "~8.7.2",
"@tonic-ui/react": "^1.21.0",
"@tonic-ui/react": "^1.22.0",
"@xstate/react": "~3.0.1",
"awilix": "~4.3.1",
"awilix-express": "~3.0.0",
Expand Down Expand Up @@ -332,7 +332,7 @@
"glob": "~7.2.0",
"html-webpack-plugin": "~5.5.0",
"husky": "^8.0.1",
"i18next-scanner": "~4.1.0",
"i18next-scanner": "~4.3.0",
"jest": "^29.3.1",
"json-loader": "~0.5.7",
"jsonlint": "~1.6.3",
Expand Down
1 change: 0 additions & 1 deletion src/app/components/Widget/index.styl
Expand Up @@ -2,7 +2,6 @@
@import './theme';

.widget {
background-color: #FFF;
margin-bottom: 10px;

&.widget-borderless {
Expand Down
12 changes: 10 additions & 2 deletions src/app/store/config/defaultState.js
Expand Up @@ -69,8 +69,16 @@ const defaultState = Object.freeze({ // Freezes the default state
serial: {
path: '',
baudRate: 115200,
// Hardware flow control (RTS/CTS)
rtscts: false

// RTS/CTS flow control
rtscts: false,

pin: {
// Set DTR line status (default to null)
dtr: null,
// Set RTS line status (default to null)
rts: null,
},
},
socket: {
host: '',
Expand Down
162 changes: 152 additions & 10 deletions src/app/widgets/Connection/Connection.jsx
Expand Up @@ -64,9 +64,11 @@ import { composeValidators, required } from 'app/widgets/shared/validations';
// @param {string} options.path
// @param {number} options.baudRate
// @param {boolean} options.rtscts
// @param {boolean} options.pin.dtr
// @param {boolean} options.pin.rts
const validateSerialConnectionOptions = (options) => {
const { path, baudRate, rtscts } = { ...options };
return (!!path) && (baudRate > 0) && (rtscts !== undefined);
const { path, baudRate, rtscts, pin } = { ...options };
return (!!path) && (baudRate > 0) && (rtscts !== undefined) && (pin?.dtr !== undefined) && (pin?.rts !== undefined);
};

// @param {string} options.host
Expand Down Expand Up @@ -124,6 +126,10 @@ const getMemoizedInitialValues = memoize((options) => {
path: config.get('connection.serial.path'),
baudRate: config.get('connection.serial.baudRate'),
rtscts: config.get('connection.serial.rtscts'),
pin: {
dtr: config.get('connection.serial.pin.dtr'),
rts: config.get('connection.serial.pin.rts'),
},
},
socket: {
host: config.get('connection.socket.host'),
Expand Down Expand Up @@ -345,16 +351,16 @@ function Connection({
return;
}

const { path, baudRate, rtscts } = config.get('connection.serial');
if (!validateSerialConnectionOptions({ path, baudRate, rtscts })) {
const { path, baudRate, rtscts, pin } = config.get('connection.serial');
if (!validateSerialConnectionOptions({ path, baudRate, rtscts, pin })) {
return;
}

const controllerType = config.get('controller.type');
const options = {};
_set(options, 'controller.type', controllerType);
_set(options, 'connection.type', connectionType);
_set(options, 'connection.options', { path, baudRate, rtscts });
_set(options, 'connection.options', { path, baudRate, rtscts, pin });

openConnection(options);

Expand Down Expand Up @@ -621,7 +627,6 @@ function Connection({
fixedWidth
spin={isFetchingSerialPorts}
style={{
color: '#222',
opacity: hovered ? 1 : 0.5,
}}
/>
Expand Down Expand Up @@ -681,7 +686,6 @@ function Connection({
fixedWidth
spin={isFetchingSerialBaudRates}
style={{
color: '#222',
opacity: hovered ? 1 : 0.5,
}}
/>
Expand All @@ -690,6 +694,142 @@ function Connection({
</Col>
</Row>
</FormGroup>
<FormGroup>
<Field name="connection.serial.pin.dtr">
{({ input, meta }) => {
const canChange = isDisconnected;
const isChecked = (typeof input.value === 'boolean');
const isDisabled = !canChange;

return (
<Checkbox
checked={isChecked}
disabled={isDisabled}
onChange={(event) => {
const checked = !!event.target.checked;
input.onChange(checked);

// Set DTR pin to `true` when checked and `null` when unchecked
config.set('connection.serial.pin.dtr', checked ? true : null);
}}
>
<Space width={8} />
{i18n._('Set DTR line status upon opening')}
</Checkbox>
);
}}
</Field>
<Field name="connection.serial.pin.dtr" subscription={{ value: true }}>
{({ input, meta }) => {
const canChange = isDisconnected;
const isChecked = (typeof input.value === 'boolean');
const isDisabled = !canChange;
const isSETSelected = (input.value === true);
const isCLRSelected = (input.value === false);

if (!isChecked) {
return null;
}

return (
<ButtonGroup variant="default">
<Button
disabled={isDisabled}
selected={isSETSelected}
onClick={(event) => {
// Set DTR pin to `true`
const value = true;
input.onChange(value);
config.set('connection.serial.pin.dtr', value);
}}
>
{i18n._('SET')}
</Button>
<Button
disabled={isDisabled}
selected={isCLRSelected}
onClick={(event) => {
// Set DTR pin to `false`
const value = false;
input.onChange(value);
config.set('connection.serial.pin.dtr', value);
}}
>
{i18n._('CLR')}
</Button>
</ButtonGroup>
);
}}
</Field>
</FormGroup>
<FormGroup>
<Field name="connection.serial.pin.rts">
{({ input, meta }) => {
const canChange = isDisconnected;
const isChecked = (typeof input.value === 'boolean');
const isDisabled = !canChange;

return (
<Checkbox
checked={isChecked}
disabled={isDisabled}
onChange={(event) => {
const checked = !!event.target.checked;
input.onChange(checked);

// Set RTS pin to `true` when checked and `null` when unchecked
config.set('connection.serial.pin.rts', checked ? true : null);
}}
>
<Space width={8} />
{i18n._('Set RTS line status upon opening')}
</Checkbox>
);
}}
</Field>
<Field name="connection.serial.pin.rts" subscription={{ value: true }}>
{({ input, meta }) => {
const canChange = isDisconnected;
const isChecked = (typeof input.value === 'boolean');
const isDisabled = !canChange;
const isSETSelected = (input.value === true);
const isCLRSelected = (input.value === false);

if (!isChecked) {
return null;
}

return (
<ButtonGroup variant="default">
<Button
disabled={isDisabled}
selected={isSETSelected}
onClick={(event) => {
// Set RTS pin to `true`
const value = true;
input.onChange(value);
config.set('connection.serial.pin.rts', value);
}}
>
{i18n._('SET')}
</Button>
<Button
disabled={isDisabled}
selected={isCLRSelected}
onClick={(event) => {
// Set RTS pin to `false`
const value = false;
input.onChange(value);
config.set('connection.serial.pin.rts', value);
}}
>
{i18n._('CLR')}
</Button>
</ButtonGroup>
);
}}
</Field>
</FormGroup>
<FormGroup>
<Field name="connection.serial.rtscts">
{({ input, meta }) => {
Expand All @@ -708,7 +848,7 @@ function Connection({
}}
>
<Space width={8} />
{i18n._('Enable hardware flow control')}
{i18n._('Use RTS/CTS flow control')}
</Checkbox>
);
}}
Expand Down Expand Up @@ -842,8 +982,9 @@ function Connection({
const path = _get(values, 'connection.serial.path');
const baudRate = _get(values, 'connection.serial.baudRate');
const rtscts = _get(values, 'connection.serial.rtscts');
const pin = _get(values, 'connection.serial.pin');

return validateSerialConnectionOptions({ path, baudRate, rtscts });
return validateSerialConnectionOptions({ path, baudRate, rtscts, pin });
}

if (connectionType === CONNECTION_TYPE_SOCKET) {
Expand All @@ -868,6 +1009,7 @@ function Connection({
path: _get(values, 'connection.serial.path'),
baudRate: _get(values, 'connection.serial.baudRate'),
rtscts: _get(values, 'connection.serial.rtscts'),
pin: _get(values, 'connection.serial.pin'),
},
[CONNECTION_TYPE_SOCKET]: {
host: _get(values, 'connection.socket.host'),
Expand Down Expand Up @@ -1028,7 +1170,7 @@ function SerialPortOption({
</Container>
{manufacturer && (
<Box ml="6x">
<Text color="#888">
<Text>
{i18n._('Manufacturer: {{manufacturer}}', { manufacturer })}
</Text>
</Box>
Expand Down
47 changes: 45 additions & 2 deletions src/server/lib/SerialConnection.js
@@ -1,6 +1,9 @@
import { EventEmitter } from 'events';
import { SerialPort } from 'serialport';
import { ReadlineParser } from '@serialport/parser-readline';
import delay from './delay';
import log from './logger';
import x from './json-stringify';

// Validation
const DATABITS = Object.freeze([5, 6, 7, 8]);
Expand Down Expand Up @@ -32,6 +35,8 @@ class SerialConnection extends EventEmitter {

parser = null;

pin = null; // { dtr: boolean, rts: boolean }

writeFilter = (data) => data;

eventListener = {
Expand All @@ -52,7 +57,11 @@ class SerialConnection extends EventEmitter {
constructor(props) {
super();

const { writeFilter, ...rest } = { ...props };
const { pin, writeFilter, ...rest } = { ...props };

if (pin) {
this.pin = pin;
}

if (writeFilter) {
if (typeof writeFilter !== 'function') {
Expand Down Expand Up @@ -138,7 +147,41 @@ class SerialConnection extends EventEmitter {
this.parser = this.port.pipe(new ReadlineParser({ delimiter: '\n' }));
this.parser.on('data', this.eventListener.data);

this.port.open(callback);
this.port.open(async (...args) => {
// This is an error-first callback
const isError = !!args[0];

if (!isError && this.pin) {
let controlFlag = null;

try {
// Set DTR and RTS control flags if they exist
if (typeof this.pin?.dtr === 'boolean') {
controlFlag = {
...controlFlag,
dtr: this.pin?.dtr,
};
}
if (typeof this.pin?.rts === 'boolean') {
controlFlag = {
...controlFlag,
rts: this.pin?.rts,
};
}

if (controlFlag) {
await delay(100);
await this.port.set(controlFlag);
await delay(100);
}
} catch (err) {
log.error(`An unexpected error occurred when setting the control flags: options=${x(this.options)}`);
log.error(err.message);
}
}

callback(...args);
});
}

// @param {function} callback The error-first callback.
Expand Down

0 comments on commit 6636362

Please sign in to comment.