Skip to content

Commit e39e7bc

Browse files
authoredJul 10, 2023
Adding the ability to configure serial port output to the debug console (eclipse-cdt-cloud#271)
This commit introduces a set of parameters that allow us to configure a serial line in the adapter to print UART output from on-board/emulation debug to the debug console. These parameters include the TCP port to hook onto (for emulation programs outputting UART output), or the serial port on the host-machine to capture UART output from. For the serial line, we also configure the baud rate, stop bits, handshaking method, etc. The source code looks to take this parameters to then configure serial ports and sockets to read UART data and output it to the debug console. It will also set up necessary event handlers, and changes the log level in the non-verbose case to allow for "log" messages to be printed in a manner such that UART output isn't designated as a "warning".
1 parent 250881c commit e39e7bc

File tree

9 files changed

+392
-4
lines changed

9 files changed

+392
-4
lines changed
 

‎.github/workflows/build-pr.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
node-version: '14'
1515
- name: Install GCC & GDB & other build essentials
1616
run: |
17-
sudo apt-get -y install build-essential gcc g++ gdb gdbserver
17+
sudo apt-get -y install build-essential gcc g++ gdb gdbserver socat
1818
gdb --version
1919
gcc --version
2020
gdbserver --version

‎.github/workflows/build-push.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
node-version: '14'
1616
- name: Install GCC & GDB & other build essentials
1717
run: |
18-
sudo apt-get -y install build-essential gcc g++ gdb gdbserver
18+
sudo apt-get -y install build-essential gcc g++ gdb gdbserver socat
1919
gdb --version
2020
gcc --version
2121
gdbserver --version

‎package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,16 @@
6060
"dependencies": {
6161
"@vscode/debugadapter": "^1.59.0",
6262
"@vscode/debugprotocol": "^1.59.0",
63-
"node-addon-api": "^4.3.0"
63+
"node-addon-api": "^4.3.0",
64+
"serialport": "11.0.0"
6465
},
6566
"devDependencies": {
6667
"@types/chai": "^4.3.4",
6768
"@types/chai-string": "^1.4.2",
6869
"@types/mocha": "^9.1.0",
6970
"@types/node": "^14.18.17",
7071
"@types/tmp": "^0.2.3",
72+
"@types/serialport": "8.0.2",
7173
"@typescript-eslint/eslint-plugin": "^5.54.0",
7274
"@typescript-eslint/parser": "^5.54.0",
7375
"@vscode/debugadapter-testsupport": "^1.59.0",

‎src/GDBTargetDebugSession.ts

+129
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@ import {
1818
import * as mi from './mi';
1919
import { DebugProtocol } from '@vscode/debugprotocol';
2020
import { spawn, ChildProcess } from 'child_process';
21+
import { SerialPort, ReadlineParser } from 'serialport';
22+
import { Socket } from 'net';
23+
24+
interface UARTArguments {
25+
// Path to the serial port connected to the UART on the board.
26+
serialPort?: string;
27+
// Target TCP port on the host machine to attach socket to print UART output (defaults to 3456)
28+
socketPort?: string;
29+
// Baud Rate (in bits/s) of the serial port to be opened (defaults to 115200).
30+
baudRate?: number;
31+
// The number of bits in each character of data sent across the serial line (defaults to 8).
32+
characterSize?: 5 | 6 | 7 | 8;
33+
// The type of parity check enabled with the transmitted data (defaults to "none" - no parity bit sent)
34+
parity?: 'none' | 'even' | 'odd' | 'mark' | 'space';
35+
// The number of stop bits sent to allow the receiver to detect the end of characters and resynchronize with the character stream (defaults to 1).
36+
stopBits?: 1 | 1.5 | 2;
37+
// The handshaking method used for flow control across the serial line (defaults to "none" - no handshaking)
38+
handshakingMethod?: 'none' | 'XON/XOFF' | 'RTS/CTS';
39+
// The EOL character used to parse the UART output line-by-line.
40+
eolCharacter?: 'LF' | 'CRLF';
41+
}
2142

2243
export interface TargetAttachArguments {
2344
// Target type default is "remote"
@@ -31,6 +52,8 @@ export interface TargetAttachArguments {
3152
port?: string;
3253
// Target connect commands - if specified used in preference of type, parameters, host, target
3354
connectCommands?: string[];
55+
// Settings related to displaying UART output in the debug console
56+
uart?: UARTArguments;
3457
}
3558

3659
export interface TargetLaunchArguments extends TargetAttachArguments {
@@ -80,6 +103,11 @@ export class GDBTargetDebugSession extends GDBDebugSession {
80103
protected gdbserver?: ChildProcess;
81104
protected killGdbServer = true;
82105

106+
// Serial Port to capture UART output across the serial line
107+
protected serialPort?: SerialPort;
108+
// Socket to listen on a TCP port to capture UART output
109+
protected socket?: Socket;
110+
83111
/**
84112
* Define the target type here such that we can run the "disconnect"
85113
* command when servicing the disconnect request if the target type
@@ -271,6 +299,99 @@ export class GDBTargetDebugSession extends GDBDebugSession {
271299
});
272300
}
273301

302+
protected initializeUARTConnection(
303+
uart: UARTArguments,
304+
host: string | undefined
305+
): void {
306+
if (uart.serialPort !== undefined) {
307+
// Set the path to the serial port
308+
this.serialPort = new SerialPort({
309+
path: uart.serialPort,
310+
// If the serial port path is defined, then so will the baud rate.
311+
baudRate: uart.baudRate ?? 115200,
312+
// If the serial port path is deifned, then so will the number of data bits.
313+
dataBits: uart.characterSize ?? 8,
314+
// If the serial port path is defined, then so will the number of stop bits.
315+
stopBits: uart.stopBits ?? 1,
316+
// If the serial port path is defined, then so will the parity check type.
317+
parity: uart.parity ?? 'none',
318+
// If the serial port path is defined, then so will the type of handshaking method.
319+
rtscts: uart.handshakingMethod === 'RTS/CTS' ? true : false,
320+
xon: uart.handshakingMethod === 'XON/XOFF' ? true : false,
321+
xoff: uart.handshakingMethod === 'XON/XOFF' ? true : false,
322+
autoOpen: false,
323+
});
324+
325+
this.serialPort.on('open', () => {
326+
this.sendEvent(
327+
new OutputEvent(
328+
`listening on serial port ${this.serialPort?.path}`,
329+
'Serial Port'
330+
)
331+
);
332+
});
333+
334+
const SerialUartParser = new ReadlineParser({
335+
delimiter: uart.eolCharacter === 'LF' ? '\n' : '\r\n',
336+
encoding: 'utf8',
337+
});
338+
339+
this.serialPort
340+
.pipe(SerialUartParser)
341+
.on('data', (line: string) => {
342+
this.sendEvent(new OutputEvent(line, 'Serial Port'));
343+
});
344+
345+
this.serialPort.on('close', () => {
346+
this.sendEvent(
347+
new OutputEvent(
348+
'closing serial port connection',
349+
'Serial Port'
350+
)
351+
);
352+
});
353+
354+
this.serialPort.open();
355+
} else if (uart.socketPort !== undefined) {
356+
this.socket = new Socket();
357+
this.socket.setEncoding('utf-8');
358+
359+
const eolChar: string = uart.eolCharacter === 'LF' ? '\n' : '\r\n';
360+
361+
let tcpUartData = '';
362+
this.socket.on('data', (data: string) => {
363+
for (const char of data) {
364+
if (char === eolChar) {
365+
this.sendEvent(new OutputEvent(tcpUartData, 'Socket'));
366+
tcpUartData = '';
367+
} else {
368+
tcpUartData += char;
369+
}
370+
}
371+
});
372+
this.socket.on('close', () => {
373+
this.sendEvent(new OutputEvent(tcpUartData, 'Socket'));
374+
this.sendEvent(
375+
new OutputEvent('closing socket connection', 'Socket')
376+
);
377+
});
378+
this.socket.connect(
379+
// Putting a + (unary plus operator) infront of the string converts it to a number.
380+
+uart.socketPort,
381+
// Default to localhost if target.host is undefined.
382+
host ?? 'localhost',
383+
() => {
384+
this.sendEvent(
385+
new OutputEvent(
386+
`listening on tcp port ${uart?.socketPort}`,
387+
'Socket'
388+
)
389+
);
390+
}
391+
);
392+
}
393+
}
394+
274395
protected async startGDBAndAttachToTarget(
275396
response: DebugProtocol.AttachResponse | DebugProtocol.LaunchResponse,
276397
args: TargetAttachRequestArguments
@@ -338,6 +459,10 @@ export class GDBTargetDebugSession extends GDBDebugSession {
338459

339460
await this.gdb.sendCommands(args.initCommands);
340461

462+
if (target.uart !== undefined) {
463+
this.initializeUARTConnection(target.uart, target.host);
464+
}
465+
341466
if (args.imageAndSymbols) {
342467
if (args.imageAndSymbols.imageFileName) {
343468
await this.gdb.sendLoad(
@@ -380,9 +505,13 @@ export class GDBTargetDebugSession extends GDBDebugSession {
380505
_args: DebugProtocol.DisconnectArguments
381506
): Promise<void> {
382507
try {
508+
if (this.serialPort !== undefined && this.serialPort.isOpen)
509+
this.serialPort.close();
510+
383511
if (this.targetType === 'remote') {
384512
await this.gdb.sendCommand('disconnect');
385513
}
514+
386515
await this.gdb.sendGDBExit();
387516
if (this.killGdbServer) {
388517
await this.stopGDBServer();

‎src/integration-tests/debugClient.ts

+17
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,23 @@ export class CdtDebugClient extends DebugClient {
371371
});
372372
return evalResponse.body.result;
373373
}
374+
375+
/**
376+
* Obtain the output coming from the debug console..
377+
* @param category catgory of output event coming on debug console.
378+
* @param output expected output coming on debug console
379+
*/
380+
public async getDebugConsoleOutput(
381+
launchArgs: any,
382+
category: string,
383+
output: string
384+
): Promise<any> {
385+
return Promise.all([
386+
this.waitForEvent('initialized'),
387+
this.launch(launchArgs),
388+
this.waitForOutputEvent(category, output),
389+
]);
390+
}
374391
}
375392

376393
/**

‎src/integration-tests/launchRemote.spec.ts

+76
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@
88
* SPDX-License-Identifier: EPL-2.0
99
*********************************************************************/
1010

11+
import * as cp from 'child_process';
1112
import * as path from 'path';
1213
import {
1314
TargetLaunchRequestArguments,
1415
TargetLaunchArguments,
1516
} from '../GDBTargetDebugSession';
1617
import { CdtDebugClient } from './debugClient';
1718
import { fillDefaults, standardBeforeEach, testProgramsDir } from './utils';
19+
import { expect } from 'chai';
20+
import * as os from 'os';
1821

1922
describe('launch remote', function () {
2023
let dc: CdtDebugClient;
@@ -43,4 +46,77 @@ describe('launch remote', function () {
4346
}
4447
);
4548
});
49+
50+
it('can print a message to the debug console sent from a socket server', async function () {
51+
const socketServer = cp.spawn(
52+
'node',
53+
[`${path.join(testProgramsDir, 'socketServer.js')}`],
54+
{
55+
cwd: testProgramsDir,
56+
}
57+
);
58+
// Ensure that the socket port is defined prior to the test.
59+
let socketPort = '';
60+
socketServer.stdout.on('data', (data) => {
61+
socketPort = data.toString();
62+
socketPort = socketPort.substring(0, socketPort.indexOf('\n'));
63+
});
64+
65+
// Sleep for 1 second before running test to ensure socketPort is defined.
66+
await new Promise((f) => setTimeout(f, 1000));
67+
expect(socketPort).not.eq('');
68+
69+
await dc.getDebugConsoleOutput(
70+
fillDefaults(this.test, {
71+
program: emptyProgram,
72+
openGdbConsole: false,
73+
initCommands: ['break _fini'],
74+
target: {
75+
uart: {
76+
socketPort: socketPort,
77+
eolCharacter: 'LF',
78+
},
79+
} as TargetLaunchArguments,
80+
} as TargetLaunchRequestArguments),
81+
'Socket',
82+
'Hello World!'
83+
);
84+
85+
// Kill the spawned process.
86+
socketServer.kill();
87+
});
88+
89+
it('can print a message to the debug console sent from across a serial line', async function () {
90+
// Skip this test on Windows - socat utility only available on Linux.
91+
if (os.platform() === 'win32') this.skip();
92+
93+
// Start a virtual serial line. Use /tmp/ttyV0 and /tmp/ttyV1 to refer to the two ends.
94+
const virtualSerialLine = cp.spawn('socat', [
95+
'-d',
96+
'-d',
97+
'pty,rawer,echo=0,link=/tmp/ttyV0',
98+
'pty,rawer,echo=0,link=/tmp/ttyV1',
99+
]);
100+
101+
await dc.getDebugConsoleOutput(
102+
fillDefaults(this.test, {
103+
program: emptyProgram,
104+
openGdbConsole: false,
105+
initCommands: ['break _fini'],
106+
preRunCommands: [`shell echo "Hello World!" > /tmp/ttyV1`],
107+
target: {
108+
uart: {
109+
serialPort: '/tmp/ttyV0',
110+
eolCharacter: 'LF',
111+
baudRate: 38400,
112+
},
113+
} as TargetLaunchArguments,
114+
} as TargetLaunchRequestArguments),
115+
'Serial Port',
116+
'Hello World!'
117+
);
118+
119+
// Kill the spawned process.
120+
virtualSerialLine.kill();
121+
});
46122
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// eslint-disable-next-line @typescript-eslint/no-var-requires
2+
const net = require('net');
3+
4+
// Create socket echo server.
5+
const socketServer = new net.Server();
6+
7+
socketServer.on('connection', (connection) => {
8+
console.log('adapter connected');
9+
10+
connection.on('end', () => {
11+
console.log('adapter disconected');
12+
});
13+
14+
// Echo "Hello World!"
15+
connection.write('Hello World!\n');
16+
});
17+
18+
socketServer.on('close', () => {
19+
console.log('shutting down');
20+
});
21+
22+
socketServer.on('error', (error) => {
23+
throw error;
24+
});
25+
26+
function serverListen() {
27+
socketServer.listen(0, 'localhost', 1, () => {
28+
const port = socketServer.address().port;
29+
console.log(`${port}`);
30+
});
31+
}
32+
33+
serverListen();

‎tsconfig.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@
1919
}
2020
]
2121
},
22-
"include": ["src/**/*.ts"]
22+
"include": [
23+
"src/**/*.ts",
24+
"src/integration-tests/test-programs/socketServer.js"
25+
]
2326
}

0 commit comments

Comments
 (0)