green-screen-proxy 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +32 -0
- package/dist/hp6530/connection.d.ts +51 -0
- package/dist/hp6530/connection.js +258 -0
- package/dist/hp6530/constants.d.ts +64 -0
- package/dist/hp6530/constants.js +135 -0
- package/dist/hp6530/encoder.d.ts +37 -0
- package/dist/hp6530/encoder.js +89 -0
- package/dist/hp6530/parser.d.ts +45 -0
- package/dist/hp6530/parser.js +255 -0
- package/dist/hp6530/screen.d.ts +104 -0
- package/dist/hp6530/screen.js +252 -0
- package/dist/mock/mock-routes.d.ts +2 -0
- package/dist/mock/mock-routes.js +231 -0
- package/dist/protocols/hp6530-handler.d.ts +29 -0
- package/dist/protocols/hp6530-handler.js +64 -0
- package/dist/protocols/index.d.ts +11 -0
- package/dist/protocols/index.js +27 -0
- package/dist/protocols/tn3270-handler.d.ts +26 -0
- package/dist/protocols/tn3270-handler.js +61 -0
- package/dist/protocols/tn5250-handler.d.ts +26 -0
- package/dist/protocols/tn5250-handler.js +62 -0
- package/dist/protocols/types.d.ts +59 -0
- package/dist/protocols/types.js +7 -0
- package/dist/protocols/vt-handler.d.ts +30 -0
- package/dist/protocols/vt-handler.js +67 -0
- package/dist/routes.d.ts +2 -0
- package/dist/routes.js +141 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +34 -0
- package/dist/session.d.ts +32 -0
- package/dist/session.js +88 -0
- package/dist/tn3270/connection.d.ts +31 -0
- package/dist/tn3270/connection.js +266 -0
- package/dist/tn3270/constants.d.ts +262 -0
- package/dist/tn3270/constants.js +261 -0
- package/dist/tn3270/encoder.d.ts +24 -0
- package/dist/tn3270/encoder.js +97 -0
- package/dist/tn3270/parser.d.ts +22 -0
- package/dist/tn3270/parser.js +284 -0
- package/dist/tn3270/screen.d.ts +89 -0
- package/dist/tn3270/screen.js +207 -0
- package/dist/tn5250/connection.d.ts +41 -0
- package/dist/tn5250/connection.js +254 -0
- package/dist/tn5250/constants.d.ts +128 -0
- package/dist/tn5250/constants.js +156 -0
- package/dist/tn5250/ebcdic.d.ts +10 -0
- package/dist/tn5250/ebcdic.js +89 -0
- package/dist/tn5250/encoder.d.ts +30 -0
- package/dist/tn5250/encoder.js +121 -0
- package/dist/tn5250/parser.d.ts +33 -0
- package/dist/tn5250/parser.js +412 -0
- package/dist/tn5250/screen.d.ts +80 -0
- package/dist/tn5250/screen.js +155 -0
- package/dist/vt/connection.d.ts +45 -0
- package/dist/vt/connection.js +229 -0
- package/dist/vt/constants.d.ts +97 -0
- package/dist/vt/constants.js +163 -0
- package/dist/vt/encoder.d.ts +30 -0
- package/dist/vt/encoder.js +55 -0
- package/dist/vt/parser.d.ts +36 -0
- package/dist/vt/parser.js +534 -0
- package/dist/vt/screen.d.ts +101 -0
- package/dist/vt/screen.js +424 -0
- package/dist/websocket.d.ts +6 -0
- package/dist/websocket.js +50 -0
- package/package.json +57 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
const { values } = parseArgs({
|
|
4
|
+
options: {
|
|
5
|
+
mock: { type: 'boolean', default: false },
|
|
6
|
+
port: { type: 'string', default: '' },
|
|
7
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
if (values.help) {
|
|
11
|
+
console.log(`green-screen-proxy — WebSocket/REST proxy for legacy terminal connections
|
|
12
|
+
|
|
13
|
+
Usage: green-screen-proxy [options]
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--mock Run with mock data (no real host connection needed)
|
|
17
|
+
--port NUM Port to listen on (default: 3001, or PORT env var)
|
|
18
|
+
-h, --help Show this help message
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
npx green-screen-proxy # Start proxy on port 3001
|
|
22
|
+
npx green-screen-proxy --mock # Start with mock screens
|
|
23
|
+
npx green-screen-proxy --port 8080 # Start on port 8080`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
if (values.port) {
|
|
27
|
+
process.env.PORT = values.port;
|
|
28
|
+
}
|
|
29
|
+
if (values.mock) {
|
|
30
|
+
process.argv.push('--mock');
|
|
31
|
+
}
|
|
32
|
+
await import('./server.js');
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
export interface ConnectionEvents {
|
|
3
|
+
connected: () => void;
|
|
4
|
+
disconnected: () => void;
|
|
5
|
+
data: (data: Buffer) => void;
|
|
6
|
+
error: (err: Error) => void;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Manages a TCP/Telnet connection to an HP NonStop (Tandem) system.
|
|
10
|
+
*
|
|
11
|
+
* HP 6530 terminals use standard Telnet negotiation but differ from
|
|
12
|
+
* TN5250/TN3270 in that data is stream-based — there are no IAC EOR
|
|
13
|
+
* delimited records. Instead, the host sends escape sequences inline
|
|
14
|
+
* and block-mode data transfer uses DC1 (XON) / DC3 (XOFF) for flow
|
|
15
|
+
* control.
|
|
16
|
+
*/
|
|
17
|
+
export declare class HP6530Connection extends EventEmitter {
|
|
18
|
+
private socket;
|
|
19
|
+
private host;
|
|
20
|
+
private port;
|
|
21
|
+
private _connected;
|
|
22
|
+
private recvBuffer;
|
|
23
|
+
/** Whether we are in XOFF state (host asked us to pause sending) */
|
|
24
|
+
private xoff;
|
|
25
|
+
/** Queue of outbound data waiting for XON */
|
|
26
|
+
private sendQueue;
|
|
27
|
+
get isConnected(): boolean;
|
|
28
|
+
get remoteHost(): string;
|
|
29
|
+
get remotePort(): number;
|
|
30
|
+
connect(host: string, port: number): Promise<void>;
|
|
31
|
+
disconnect(): void;
|
|
32
|
+
/** Send raw bytes over the socket, respecting XON/XOFF flow control */
|
|
33
|
+
sendRaw(data: Buffer): void;
|
|
34
|
+
private cleanup;
|
|
35
|
+
private onData;
|
|
36
|
+
/**
|
|
37
|
+
* Process the receive buffer. We need to:
|
|
38
|
+
* 1. Handle Telnet IAC sequences (negotiation)
|
|
39
|
+
* 2. Handle DC1/DC3 flow control characters
|
|
40
|
+
* 3. Pass remaining data (escape sequences + printable chars) to the parser
|
|
41
|
+
*/
|
|
42
|
+
private processBuffer;
|
|
43
|
+
private emitData;
|
|
44
|
+
private flushSendQueue;
|
|
45
|
+
/** Find IAC SE sequence for subnegotiation end */
|
|
46
|
+
private findSubnegEnd;
|
|
47
|
+
private handleNegotiation;
|
|
48
|
+
private handleSubnegotiation;
|
|
49
|
+
private sendTerminalType;
|
|
50
|
+
private sendTelnet;
|
|
51
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import * as net from 'net';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { TELNET } from '../tn5250/constants.js';
|
|
4
|
+
import { TERMINAL_TYPE, CTRL } from './constants.js';
|
|
5
|
+
/**
|
|
6
|
+
* Manages a TCP/Telnet connection to an HP NonStop (Tandem) system.
|
|
7
|
+
*
|
|
8
|
+
* HP 6530 terminals use standard Telnet negotiation but differ from
|
|
9
|
+
* TN5250/TN3270 in that data is stream-based — there are no IAC EOR
|
|
10
|
+
* delimited records. Instead, the host sends escape sequences inline
|
|
11
|
+
* and block-mode data transfer uses DC1 (XON) / DC3 (XOFF) for flow
|
|
12
|
+
* control.
|
|
13
|
+
*/
|
|
14
|
+
export class HP6530Connection extends EventEmitter {
|
|
15
|
+
socket = null;
|
|
16
|
+
host = '';
|
|
17
|
+
port = 23;
|
|
18
|
+
_connected = false;
|
|
19
|
+
recvBuffer = Buffer.alloc(0);
|
|
20
|
+
/** Whether we are in XOFF state (host asked us to pause sending) */
|
|
21
|
+
xoff = false;
|
|
22
|
+
/** Queue of outbound data waiting for XON */
|
|
23
|
+
sendQueue = [];
|
|
24
|
+
get isConnected() {
|
|
25
|
+
return this._connected;
|
|
26
|
+
}
|
|
27
|
+
get remoteHost() {
|
|
28
|
+
return this.host;
|
|
29
|
+
}
|
|
30
|
+
get remotePort() {
|
|
31
|
+
return this.port;
|
|
32
|
+
}
|
|
33
|
+
connect(host, port) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
if (this.socket) {
|
|
36
|
+
this.disconnect();
|
|
37
|
+
}
|
|
38
|
+
this.host = host;
|
|
39
|
+
this.port = port;
|
|
40
|
+
this.recvBuffer = Buffer.alloc(0);
|
|
41
|
+
this.xoff = false;
|
|
42
|
+
this.sendQueue = [];
|
|
43
|
+
this.socket = new net.Socket();
|
|
44
|
+
this.socket.setTimeout(30000);
|
|
45
|
+
const onError = (err) => {
|
|
46
|
+
this.cleanup();
|
|
47
|
+
reject(err);
|
|
48
|
+
};
|
|
49
|
+
this.socket.once('error', onError);
|
|
50
|
+
this.socket.connect(port, host, () => {
|
|
51
|
+
this._connected = true;
|
|
52
|
+
this.socket.removeListener('error', onError);
|
|
53
|
+
this.socket.on('error', (err) => {
|
|
54
|
+
this.emit('error', err);
|
|
55
|
+
this.cleanup();
|
|
56
|
+
});
|
|
57
|
+
this.socket.on('close', () => {
|
|
58
|
+
this.cleanup();
|
|
59
|
+
this.emit('disconnected');
|
|
60
|
+
});
|
|
61
|
+
this.socket.on('timeout', () => {
|
|
62
|
+
this.emit('error', new Error('Connection timeout'));
|
|
63
|
+
});
|
|
64
|
+
this.socket.on('data', (data) => this.onData(data));
|
|
65
|
+
this.emit('connected');
|
|
66
|
+
resolve();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
disconnect() {
|
|
71
|
+
if (this.socket) {
|
|
72
|
+
this.socket.destroy();
|
|
73
|
+
this.cleanup();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Send raw bytes over the socket, respecting XON/XOFF flow control */
|
|
77
|
+
sendRaw(data) {
|
|
78
|
+
if (!this.socket || !this._connected)
|
|
79
|
+
return;
|
|
80
|
+
if (this.xoff) {
|
|
81
|
+
// Queue data until we receive XON
|
|
82
|
+
this.sendQueue.push(data);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.socket.write(data);
|
|
86
|
+
}
|
|
87
|
+
cleanup() {
|
|
88
|
+
this._connected = false;
|
|
89
|
+
this.xoff = false;
|
|
90
|
+
this.sendQueue = [];
|
|
91
|
+
if (this.socket) {
|
|
92
|
+
this.socket.removeAllListeners();
|
|
93
|
+
this.socket.destroy();
|
|
94
|
+
this.socket = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
onData(data) {
|
|
98
|
+
this.recvBuffer = Buffer.concat([this.recvBuffer, data]);
|
|
99
|
+
this.processBuffer();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Process the receive buffer. We need to:
|
|
103
|
+
* 1. Handle Telnet IAC sequences (negotiation)
|
|
104
|
+
* 2. Handle DC1/DC3 flow control characters
|
|
105
|
+
* 3. Pass remaining data (escape sequences + printable chars) to the parser
|
|
106
|
+
*/
|
|
107
|
+
processBuffer() {
|
|
108
|
+
while (this.recvBuffer.length > 0) {
|
|
109
|
+
const byte = this.recvBuffer[0];
|
|
110
|
+
// --- Telnet IAC handling ---
|
|
111
|
+
if (byte === TELNET.IAC) {
|
|
112
|
+
if (this.recvBuffer.length < 2)
|
|
113
|
+
return; // need more data
|
|
114
|
+
const cmd = this.recvBuffer[1];
|
|
115
|
+
// IAC IAC = escaped 0xFF literal
|
|
116
|
+
if (cmd === TELNET.IAC) {
|
|
117
|
+
// Emit as data
|
|
118
|
+
this.emitData(Buffer.from([0xFF]));
|
|
119
|
+
this.recvBuffer = this.recvBuffer.subarray(2);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Subnegotiation: IAC SB ... IAC SE
|
|
123
|
+
if (cmd === TELNET.SB) {
|
|
124
|
+
const seIdx = this.findSubnegEnd();
|
|
125
|
+
if (seIdx === -1)
|
|
126
|
+
return; // wait for more data
|
|
127
|
+
const subData = this.recvBuffer.subarray(2, seIdx);
|
|
128
|
+
this.recvBuffer = this.recvBuffer.subarray(seIdx + 2);
|
|
129
|
+
this.handleSubnegotiation(subData);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// DO / DONT / WILL / WONT: 3 bytes
|
|
133
|
+
if (cmd === TELNET.DO || cmd === TELNET.DONT ||
|
|
134
|
+
cmd === TELNET.WILL || cmd === TELNET.WONT) {
|
|
135
|
+
if (this.recvBuffer.length < 3)
|
|
136
|
+
return;
|
|
137
|
+
const option = this.recvBuffer[2];
|
|
138
|
+
this.recvBuffer = this.recvBuffer.subarray(3);
|
|
139
|
+
this.handleNegotiation(cmd, option);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// Other IAC command (2 bytes), skip
|
|
143
|
+
this.recvBuffer = this.recvBuffer.subarray(2);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
// --- XON/XOFF flow control ---
|
|
147
|
+
if (byte === CTRL.DC3) {
|
|
148
|
+
// XOFF: host asks us to stop sending
|
|
149
|
+
this.xoff = true;
|
|
150
|
+
this.recvBuffer = this.recvBuffer.subarray(1);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (byte === CTRL.DC1) {
|
|
154
|
+
// XON: host allows us to resume sending
|
|
155
|
+
this.xoff = false;
|
|
156
|
+
this.recvBuffer = this.recvBuffer.subarray(1);
|
|
157
|
+
this.flushSendQueue();
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// --- Regular data (escape sequences + printable characters) ---
|
|
161
|
+
// Find the next IAC or flow control character to know how much data to emit
|
|
162
|
+
let end = 1;
|
|
163
|
+
while (end < this.recvBuffer.length) {
|
|
164
|
+
const b = this.recvBuffer[end];
|
|
165
|
+
if (b === TELNET.IAC || b === CTRL.DC1 || b === CTRL.DC3)
|
|
166
|
+
break;
|
|
167
|
+
end++;
|
|
168
|
+
}
|
|
169
|
+
const chunk = this.recvBuffer.subarray(0, end);
|
|
170
|
+
this.recvBuffer = this.recvBuffer.subarray(end);
|
|
171
|
+
this.emitData(Buffer.from(chunk));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
emitData(data) {
|
|
175
|
+
if (data.length > 0) {
|
|
176
|
+
this.emit('data', data);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
flushSendQueue() {
|
|
180
|
+
if (!this.socket || !this._connected)
|
|
181
|
+
return;
|
|
182
|
+
while (this.sendQueue.length > 0 && !this.xoff) {
|
|
183
|
+
const queued = this.sendQueue.shift();
|
|
184
|
+
this.socket.write(queued);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Find IAC SE sequence for subnegotiation end */
|
|
188
|
+
findSubnegEnd() {
|
|
189
|
+
for (let i = 2; i < this.recvBuffer.length - 1; i++) {
|
|
190
|
+
if (this.recvBuffer[i] === TELNET.IAC && this.recvBuffer[i + 1] === TELNET.SE) {
|
|
191
|
+
return i;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return -1;
|
|
195
|
+
}
|
|
196
|
+
handleNegotiation(cmd, option) {
|
|
197
|
+
switch (cmd) {
|
|
198
|
+
case TELNET.DO:
|
|
199
|
+
// Server asks us to enable an option
|
|
200
|
+
if (option === TELNET.OPT_TTYPE ||
|
|
201
|
+
option === TELNET.OPT_BINARY ||
|
|
202
|
+
option === 0x03 /* SGA */ ||
|
|
203
|
+
option === 0x01 /* ECHO */) {
|
|
204
|
+
this.sendTelnet(TELNET.WILL, option);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
this.sendTelnet(TELNET.WONT, option);
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
case TELNET.WILL:
|
|
211
|
+
// Server offers an option
|
|
212
|
+
if (option === TELNET.OPT_BINARY ||
|
|
213
|
+
option === 0x03 /* SGA */ ||
|
|
214
|
+
option === 0x01 /* ECHO */) {
|
|
215
|
+
this.sendTelnet(TELNET.DO, option);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
this.sendTelnet(TELNET.DONT, option);
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
case TELNET.DONT:
|
|
222
|
+
this.sendTelnet(TELNET.WONT, option);
|
|
223
|
+
break;
|
|
224
|
+
case TELNET.WONT:
|
|
225
|
+
this.sendTelnet(TELNET.DONT, option);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
handleSubnegotiation(data) {
|
|
230
|
+
if (data.length === 0)
|
|
231
|
+
return;
|
|
232
|
+
const option = data[0];
|
|
233
|
+
if (option === TELNET.OPT_TTYPE && data.length >= 2 && data[1] === TELNET.TTYPE_SEND) {
|
|
234
|
+
this.sendTerminalType();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
sendTerminalType() {
|
|
238
|
+
const typeStr = TERMINAL_TYPE;
|
|
239
|
+
const buf = Buffer.alloc(4 + typeStr.length + 2);
|
|
240
|
+
let i = 0;
|
|
241
|
+
buf[i++] = TELNET.IAC;
|
|
242
|
+
buf[i++] = TELNET.SB;
|
|
243
|
+
buf[i++] = TELNET.OPT_TTYPE;
|
|
244
|
+
buf[i++] = TELNET.TTYPE_IS;
|
|
245
|
+
for (let j = 0; j < typeStr.length; j++) {
|
|
246
|
+
buf[i++] = typeStr.charCodeAt(j);
|
|
247
|
+
}
|
|
248
|
+
buf[i++] = TELNET.IAC;
|
|
249
|
+
buf[i++] = TELNET.SE;
|
|
250
|
+
this.sendRaw(buf);
|
|
251
|
+
}
|
|
252
|
+
sendTelnet(cmd, option) {
|
|
253
|
+
if (this.socket && this._connected) {
|
|
254
|
+
// Bypass flow control for Telnet negotiation
|
|
255
|
+
this.socket.write(Buffer.from([TELNET.IAC, cmd, option]));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export { TELNET } from '../tn5250/constants.js';
|
|
2
|
+
/** Terminal type string for Telnet TTYPE negotiation */
|
|
3
|
+
export declare const TERMINAL_TYPE = "T6530";
|
|
4
|
+
/** Alternative terminal type */
|
|
5
|
+
export declare const TERMINAL_TYPE_ALT = "HP700/96";
|
|
6
|
+
/** Screen dimensions */
|
|
7
|
+
export declare const SCREEN: {
|
|
8
|
+
readonly ROWS: 24;
|
|
9
|
+
readonly COLS: 80;
|
|
10
|
+
};
|
|
11
|
+
export declare const CTRL: {
|
|
12
|
+
readonly NUL: 0;
|
|
13
|
+
readonly SOH: 1;
|
|
14
|
+
readonly STX: 2;
|
|
15
|
+
readonly ETX: 3;
|
|
16
|
+
readonly EOT: 4;
|
|
17
|
+
readonly ENQ: 5;
|
|
18
|
+
readonly ACK: 6;
|
|
19
|
+
readonly BEL: 7;
|
|
20
|
+
readonly BS: 8;
|
|
21
|
+
readonly HT: 9;
|
|
22
|
+
readonly LF: 10;
|
|
23
|
+
readonly VT: 11;
|
|
24
|
+
readonly FF: 12;
|
|
25
|
+
readonly CR: 13;
|
|
26
|
+
readonly SO: 14;
|
|
27
|
+
readonly SI: 15;
|
|
28
|
+
readonly DC1: 17;
|
|
29
|
+
readonly DC3: 19;
|
|
30
|
+
readonly ESC: 27;
|
|
31
|
+
readonly DEL: 127;
|
|
32
|
+
};
|
|
33
|
+
/** Direct cursor address: ESC [ row ; col H (VT-style CUP) */
|
|
34
|
+
export declare const ESC_CUP_PREFIX: Buffer<ArrayBuffer>;
|
|
35
|
+
export declare const ESC_CUP_SEP = 59;
|
|
36
|
+
export declare const ESC_CUP_SUFFIX = 72;
|
|
37
|
+
/** Clear to end of display */
|
|
38
|
+
export declare const ESC_CLEAR_EOS: Buffer<ArrayBuffer>;
|
|
39
|
+
/** Clear to end of line */
|
|
40
|
+
export declare const ESC_CLEAR_EOL: Buffer<ArrayBuffer>;
|
|
41
|
+
/** Start protected field */
|
|
42
|
+
export declare const ESC_PROTECT_START: Buffer<ArrayBuffer>;
|
|
43
|
+
/** End protected field (start unprotected) */
|
|
44
|
+
export declare const ESC_PROTECT_END: Buffer<ArrayBuffer>;
|
|
45
|
+
export declare const ATTR: {
|
|
46
|
+
readonly NORMAL: 64;
|
|
47
|
+
readonly HALF_BRIGHT: 66;
|
|
48
|
+
readonly UNDERLINE: 68;
|
|
49
|
+
readonly BLINK: 72;
|
|
50
|
+
readonly INVERSE: 74;
|
|
51
|
+
readonly UNDERLINE_INVERSE: 76;
|
|
52
|
+
};
|
|
53
|
+
/** Attribute escape sequence prefix: ESC & d */
|
|
54
|
+
export declare const ESC_ATTR_PREFIX: Buffer<ArrayBuffer>;
|
|
55
|
+
/** Map of attribute code to human-readable name */
|
|
56
|
+
export declare const ATTR_NAMES: Record<number, string>;
|
|
57
|
+
/** F1–F8: ESC p through ESC w (0x70–0x77) */
|
|
58
|
+
/** F9–F16: ESC ` through ESC g (0x60–0x67) — varies by model */
|
|
59
|
+
/** SF1–SF16: shifted versions — ESC P through ESC W (0x50–0x57) for SF1-SF8,
|
|
60
|
+
* ESC H through ESC O (0x48–0x4F) for SF9-SF16 — varies by model */
|
|
61
|
+
export declare const FKEY_SEQUENCES: Record<string, Buffer>;
|
|
62
|
+
export declare const ARROW_SEQUENCES: Record<string, Buffer>;
|
|
63
|
+
export declare const KEY_TO_SEQUENCE: Record<string, Buffer>;
|
|
64
|
+
export declare const FKEY_BYTE_TO_NAME: Record<number, string>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// === Telnet Constants (reuse from TN5250) ===
|
|
2
|
+
export { TELNET } from '../tn5250/constants.js';
|
|
3
|
+
// === HP 6530 Terminal Constants ===
|
|
4
|
+
/** Terminal type string for Telnet TTYPE negotiation */
|
|
5
|
+
export const TERMINAL_TYPE = 'T6530';
|
|
6
|
+
/** Alternative terminal type */
|
|
7
|
+
export const TERMINAL_TYPE_ALT = 'HP700/96';
|
|
8
|
+
/** Screen dimensions */
|
|
9
|
+
export const SCREEN = {
|
|
10
|
+
ROWS: 24,
|
|
11
|
+
COLS: 80,
|
|
12
|
+
};
|
|
13
|
+
// === Control Characters ===
|
|
14
|
+
export const CTRL = {
|
|
15
|
+
NUL: 0x00,
|
|
16
|
+
SOH: 0x01, // Start of header
|
|
17
|
+
STX: 0x02, // Start of text
|
|
18
|
+
ETX: 0x03, // End of text
|
|
19
|
+
EOT: 0x04, // End of transmission
|
|
20
|
+
ENQ: 0x05, // Enquiry
|
|
21
|
+
ACK: 0x06, // Acknowledge
|
|
22
|
+
BEL: 0x07, // Bell
|
|
23
|
+
BS: 0x08, // Backspace
|
|
24
|
+
HT: 0x09, // Horizontal tab
|
|
25
|
+
LF: 0x0A, // Line feed
|
|
26
|
+
VT: 0x0B, // Vertical tab
|
|
27
|
+
FF: 0x0C, // Form feed
|
|
28
|
+
CR: 0x0D, // Carriage return
|
|
29
|
+
SO: 0x0E, // Shift out
|
|
30
|
+
SI: 0x0F, // Shift in
|
|
31
|
+
DC1: 0x11, // XON — Device control 1 (resume transmission)
|
|
32
|
+
DC3: 0x13, // XOFF — Device control 3 (pause transmission)
|
|
33
|
+
ESC: 0x1B, // Escape
|
|
34
|
+
DEL: 0x7F, // Delete
|
|
35
|
+
};
|
|
36
|
+
// === 6530 Escape Sequences ===
|
|
37
|
+
// These are the byte sequences sent from the host to control the terminal.
|
|
38
|
+
/** Direct cursor address: ESC [ row ; col H (VT-style CUP) */
|
|
39
|
+
export const ESC_CUP_PREFIX = Buffer.from([0x1B, 0x5B]); // ESC [
|
|
40
|
+
export const ESC_CUP_SEP = 0x3B; // ';'
|
|
41
|
+
export const ESC_CUP_SUFFIX = 0x48; // 'H'
|
|
42
|
+
/** Clear to end of display */
|
|
43
|
+
export const ESC_CLEAR_EOS = Buffer.from([0x1B, 0x4A]); // ESC J
|
|
44
|
+
/** Clear to end of line */
|
|
45
|
+
export const ESC_CLEAR_EOL = Buffer.from([0x1B, 0x4B]); // ESC K
|
|
46
|
+
/** Start protected field */
|
|
47
|
+
export const ESC_PROTECT_START = Buffer.from([0x1B, 0x29]); // ESC )
|
|
48
|
+
/** End protected field (start unprotected) */
|
|
49
|
+
export const ESC_PROTECT_END = Buffer.from([0x1B, 0x28]); // ESC (
|
|
50
|
+
// === Display Attribute Escape Sequences ===
|
|
51
|
+
// Format: ESC & d <code>
|
|
52
|
+
export const ATTR = {
|
|
53
|
+
NORMAL: 0x40, // '@' — Normal display / reset attributes
|
|
54
|
+
HALF_BRIGHT: 0x42, // 'B' — Dim
|
|
55
|
+
UNDERLINE: 0x44, // 'D' — Underline
|
|
56
|
+
BLINK: 0x48, // 'H' — Blink
|
|
57
|
+
INVERSE: 0x4A, // 'J' — Inverse video
|
|
58
|
+
UNDERLINE_INVERSE: 0x4C, // 'L' — Underline + inverse
|
|
59
|
+
};
|
|
60
|
+
/** Attribute escape sequence prefix: ESC & d */
|
|
61
|
+
export const ESC_ATTR_PREFIX = Buffer.from([0x1B, 0x26, 0x64]); // ESC & d
|
|
62
|
+
/** Map of attribute code to human-readable name */
|
|
63
|
+
export const ATTR_NAMES = {
|
|
64
|
+
[ATTR.NORMAL]: 'normal',
|
|
65
|
+
[ATTR.HALF_BRIGHT]: 'half_bright',
|
|
66
|
+
[ATTR.UNDERLINE]: 'underline',
|
|
67
|
+
[ATTR.BLINK]: 'blink',
|
|
68
|
+
[ATTR.INVERSE]: 'inverse',
|
|
69
|
+
[ATTR.UNDERLINE_INVERSE]: 'underline_inverse',
|
|
70
|
+
};
|
|
71
|
+
// === Function Key Escape Sequences ===
|
|
72
|
+
// Sequences sent from the terminal to the host when function keys are pressed.
|
|
73
|
+
/** F1–F8: ESC p through ESC w (0x70–0x77) */
|
|
74
|
+
/** F9–F16: ESC ` through ESC g (0x60–0x67) — varies by model */
|
|
75
|
+
/** SF1–SF16: shifted versions — ESC P through ESC W (0x50–0x57) for SF1-SF8,
|
|
76
|
+
* ESC H through ESC O (0x48–0x4F) for SF9-SF16 — varies by model */
|
|
77
|
+
export const FKEY_SEQUENCES = {
|
|
78
|
+
// F1–F8: ESC p through ESC w
|
|
79
|
+
F1: Buffer.from([0x1B, 0x70]), // ESC p
|
|
80
|
+
F2: Buffer.from([0x1B, 0x71]), // ESC q
|
|
81
|
+
F3: Buffer.from([0x1B, 0x72]), // ESC r
|
|
82
|
+
F4: Buffer.from([0x1B, 0x73]), // ESC s
|
|
83
|
+
F5: Buffer.from([0x1B, 0x74]), // ESC t
|
|
84
|
+
F6: Buffer.from([0x1B, 0x75]), // ESC u
|
|
85
|
+
F7: Buffer.from([0x1B, 0x76]), // ESC v
|
|
86
|
+
F8: Buffer.from([0x1B, 0x77]), // ESC w
|
|
87
|
+
// F9–F16: ESC ` through ESC g
|
|
88
|
+
F9: Buffer.from([0x1B, 0x60]), // ESC `
|
|
89
|
+
F10: Buffer.from([0x1B, 0x61]), // ESC a
|
|
90
|
+
F11: Buffer.from([0x1B, 0x62]), // ESC b
|
|
91
|
+
F12: Buffer.from([0x1B, 0x63]), // ESC c
|
|
92
|
+
F13: Buffer.from([0x1B, 0x64]), // ESC d
|
|
93
|
+
F14: Buffer.from([0x1B, 0x65]), // ESC e
|
|
94
|
+
F15: Buffer.from([0x1B, 0x66]), // ESC f
|
|
95
|
+
F16: Buffer.from([0x1B, 0x67]), // ESC g
|
|
96
|
+
// SF1–SF8 (shifted): ESC P through ESC W
|
|
97
|
+
SF1: Buffer.from([0x1B, 0x50]), // ESC P
|
|
98
|
+
SF2: Buffer.from([0x1B, 0x51]), // ESC Q
|
|
99
|
+
SF3: Buffer.from([0x1B, 0x52]), // ESC R
|
|
100
|
+
SF4: Buffer.from([0x1B, 0x53]), // ESC S
|
|
101
|
+
SF5: Buffer.from([0x1B, 0x54]), // ESC T
|
|
102
|
+
SF6: Buffer.from([0x1B, 0x55]), // ESC U
|
|
103
|
+
SF7: Buffer.from([0x1B, 0x56]), // ESC V
|
|
104
|
+
SF8: Buffer.from([0x1B, 0x57]), // ESC W
|
|
105
|
+
// SF9–SF16 (shifted): ESC H through ESC O
|
|
106
|
+
SF9: Buffer.from([0x1B, 0x48]), // ESC H
|
|
107
|
+
SF10: Buffer.from([0x1B, 0x49]), // ESC I
|
|
108
|
+
SF11: Buffer.from([0x1B, 0x4A]), // ESC J (note: conflicts with clear EOS in host→terminal direction)
|
|
109
|
+
SF12: Buffer.from([0x1B, 0x4B]), // ESC K
|
|
110
|
+
SF13: Buffer.from([0x1B, 0x4C]), // ESC L
|
|
111
|
+
SF14: Buffer.from([0x1B, 0x4D]), // ESC M
|
|
112
|
+
SF15: Buffer.from([0x1B, 0x4E]), // ESC N
|
|
113
|
+
SF16: Buffer.from([0x1B, 0x4F]), // ESC O
|
|
114
|
+
};
|
|
115
|
+
// === Arrow Key Sequences (terminal → host) ===
|
|
116
|
+
export const ARROW_SEQUENCES = {
|
|
117
|
+
UP: Buffer.from([0x1B, 0x41]), // ESC A
|
|
118
|
+
DOWN: Buffer.from([0x1B, 0x42]), // ESC B
|
|
119
|
+
RIGHT: Buffer.from([0x1B, 0x43]), // ESC C
|
|
120
|
+
LEFT: Buffer.from([0x1B, 0x44]), // ESC D
|
|
121
|
+
};
|
|
122
|
+
// === Combined key-to-sequence map ===
|
|
123
|
+
export const KEY_TO_SEQUENCE = {
|
|
124
|
+
...FKEY_SEQUENCES,
|
|
125
|
+
...ARROW_SEQUENCES,
|
|
126
|
+
ENTER: Buffer.from([CTRL.CR]),
|
|
127
|
+
TAB: Buffer.from([CTRL.HT]),
|
|
128
|
+
BACKSPACE: Buffer.from([CTRL.BS]),
|
|
129
|
+
DELETE: Buffer.from([CTRL.DEL]),
|
|
130
|
+
};
|
|
131
|
+
// === Reverse lookup: sequence second byte → function key name (for F-keys only) ===
|
|
132
|
+
export const FKEY_BYTE_TO_NAME = {};
|
|
133
|
+
for (const [name, seq] of Object.entries(FKEY_SEQUENCES)) {
|
|
134
|
+
FKEY_BYTE_TO_NAME[seq[1]] = name;
|
|
135
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { HP6530Screen } from './screen.js';
|
|
2
|
+
/**
|
|
3
|
+
* Encodes client input into HP 6530 wire-format data for sending
|
|
4
|
+
* to an HP NonStop (Tandem) host.
|
|
5
|
+
*
|
|
6
|
+
* In block mode, the terminal collects all modified unprotected field
|
|
7
|
+
* data and sends it as a single transmission when an action key
|
|
8
|
+
* (ENTER, function key) is pressed.
|
|
9
|
+
*/
|
|
10
|
+
export declare class HP6530Encoder {
|
|
11
|
+
private screen;
|
|
12
|
+
constructor(screen: HP6530Screen);
|
|
13
|
+
/**
|
|
14
|
+
* Build a block-mode response for a key press.
|
|
15
|
+
*
|
|
16
|
+
* For action keys (ENTER, function keys), we:
|
|
17
|
+
* 1. Send the function key escape sequence
|
|
18
|
+
* 2. Preceded by all modified unprotected field data
|
|
19
|
+
*
|
|
20
|
+
* The field data format for block-mode transmission:
|
|
21
|
+
* DC1 (XON) signals start of block
|
|
22
|
+
* For each modified field: position + data
|
|
23
|
+
* Followed by the key sequence
|
|
24
|
+
* CR signals end of input
|
|
25
|
+
*
|
|
26
|
+
* Returns a Buffer ready to send, or null if the key is unknown.
|
|
27
|
+
*/
|
|
28
|
+
buildKeyResponse(keyName: string): Buffer | null;
|
|
29
|
+
/**
|
|
30
|
+
* Insert text at the current cursor position in the current
|
|
31
|
+
* unprotected field. Updates the screen buffer and marks the
|
|
32
|
+
* field as modified.
|
|
33
|
+
*
|
|
34
|
+
* Returns true if text was successfully inserted.
|
|
35
|
+
*/
|
|
36
|
+
insertText(text: string): boolean;
|
|
37
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { KEY_TO_SEQUENCE, CTRL } from './constants.js';
|
|
2
|
+
/**
|
|
3
|
+
* Encodes client input into HP 6530 wire-format data for sending
|
|
4
|
+
* to an HP NonStop (Tandem) host.
|
|
5
|
+
*
|
|
6
|
+
* In block mode, the terminal collects all modified unprotected field
|
|
7
|
+
* data and sends it as a single transmission when an action key
|
|
8
|
+
* (ENTER, function key) is pressed.
|
|
9
|
+
*/
|
|
10
|
+
export class HP6530Encoder {
|
|
11
|
+
screen;
|
|
12
|
+
constructor(screen) {
|
|
13
|
+
this.screen = screen;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build a block-mode response for a key press.
|
|
17
|
+
*
|
|
18
|
+
* For action keys (ENTER, function keys), we:
|
|
19
|
+
* 1. Send the function key escape sequence
|
|
20
|
+
* 2. Preceded by all modified unprotected field data
|
|
21
|
+
*
|
|
22
|
+
* The field data format for block-mode transmission:
|
|
23
|
+
* DC1 (XON) signals start of block
|
|
24
|
+
* For each modified field: position + data
|
|
25
|
+
* Followed by the key sequence
|
|
26
|
+
* CR signals end of input
|
|
27
|
+
*
|
|
28
|
+
* Returns a Buffer ready to send, or null if the key is unknown.
|
|
29
|
+
*/
|
|
30
|
+
buildKeyResponse(keyName) {
|
|
31
|
+
const keySeq = KEY_TO_SEQUENCE[keyName];
|
|
32
|
+
if (!keySeq)
|
|
33
|
+
return null;
|
|
34
|
+
const parts = [];
|
|
35
|
+
// For block-mode action keys (ENTER, function keys), collect field data
|
|
36
|
+
const isActionKey = keyName === 'ENTER' ||
|
|
37
|
+
keyName.startsWith('F') ||
|
|
38
|
+
keyName.startsWith('SF');
|
|
39
|
+
if (isActionKey && this.screen.fields.length > 0) {
|
|
40
|
+
// Collect all modified unprotected field data
|
|
41
|
+
for (const field of this.screen.fields) {
|
|
42
|
+
if (field.isProtected || !field.modified)
|
|
43
|
+
continue;
|
|
44
|
+
const value = this.screen.getFieldValue(field);
|
|
45
|
+
// Trim trailing spaces
|
|
46
|
+
const trimmed = value.replace(/\s+$/, '');
|
|
47
|
+
if (trimmed.length > 0) {
|
|
48
|
+
parts.push(Buffer.from(trimmed, 'ascii'));
|
|
49
|
+
}
|
|
50
|
+
// Field separator: HT between fields
|
|
51
|
+
parts.push(Buffer.from([CTRL.HT]));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Append the key sequence itself
|
|
55
|
+
parts.push(keySeq);
|
|
56
|
+
// For ENTER in block mode, append CR if not already the key sequence
|
|
57
|
+
if (keyName !== 'ENTER' && isActionKey) {
|
|
58
|
+
parts.push(Buffer.from([CTRL.CR]));
|
|
59
|
+
}
|
|
60
|
+
return Buffer.concat(parts);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Insert text at the current cursor position in the current
|
|
64
|
+
* unprotected field. Updates the screen buffer and marks the
|
|
65
|
+
* field as modified.
|
|
66
|
+
*
|
|
67
|
+
* Returns true if text was successfully inserted.
|
|
68
|
+
*/
|
|
69
|
+
insertText(text) {
|
|
70
|
+
const field = this.screen.getFieldAtCursor();
|
|
71
|
+
if (!field || field.isProtected)
|
|
72
|
+
return false;
|
|
73
|
+
const fieldStart = this.screen.offset(field.row, field.col);
|
|
74
|
+
let cursorOffset = this.screen.offset(this.screen.cursorRow, this.screen.cursorCol);
|
|
75
|
+
const fieldEnd = fieldStart + field.length;
|
|
76
|
+
for (const ch of text) {
|
|
77
|
+
if (cursorOffset >= fieldEnd)
|
|
78
|
+
break;
|
|
79
|
+
this.screen.buffer[cursorOffset] = ch;
|
|
80
|
+
cursorOffset++;
|
|
81
|
+
}
|
|
82
|
+
// Update cursor position
|
|
83
|
+
const newPos = this.screen.toRowCol(Math.min(cursorOffset, fieldEnd - 1));
|
|
84
|
+
this.screen.cursorRow = newPos.row;
|
|
85
|
+
this.screen.cursorCol = newPos.col;
|
|
86
|
+
field.modified = true;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|