blob: 4952a117a2a6e026ccbc4a17f7c972c106380f20 [file] [log] [blame]
Alex Perry5f474f22020-02-01 12:14:24 -08001import {ConfigHandler} from './config_handler';
Philipp Schradere625ba22020-11-16 20:11:37 -08002import * as configuration from 'org_frc971/aos/configuration_generated';
3import * as web_proxy from 'org_frc971/aos/network/web_proxy_generated';
4import {Builder} from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
5import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
6
7import Configuration = configuration.aos.Configuration;
8import MessageHeader = web_proxy.aos.web_proxy.MessageHeader;
9import WebSocketIce = web_proxy.aos.web_proxy.WebSocketIce;
10import WebSocketMessage = web_proxy.aos.web_proxy.WebSocketMessage;
11import Payload = web_proxy.aos.web_proxy.Payload;
12import WebSocketSdp = web_proxy.aos.web_proxy.WebSocketSdp;
13import SdpType = web_proxy.aos.web_proxy.SdpType;
Alex Perry5f474f22020-02-01 12:14:24 -080014
15// There is one handler for each DataChannel, it maintains the state of
16// multi-part messages and delegates to a callback when the message is fully
17// assembled.
18export class Handler {
19 private dataBuffer: Uint8Array|null = null;
20 private receivedMessageLength: number = 0;
21 constructor(
James Kuszmaul48413bf2020-09-01 19:19:05 -070022 private readonly handlerFunc:
23 (data: Uint8Array, sentTime: number) => void,
Philipp Schrader47445a02020-11-14 17:31:04 -080024 private readonly channel: RTCDataChannel) {
Alex Perry5f474f22020-02-01 12:14:24 -080025 channel.addEventListener('message', (e) => this.handleMessage(e));
26 }
27
28 handleMessage(e: MessageEvent): void {
Philipp Schradere625ba22020-11-16 20:11:37 -080029 const fbBuffer = new ByteBuffer(new Uint8Array(e.data));
30 const messageHeader = MessageHeader.getRootAsMessageHeader(
31 fbBuffer as unknown as flatbuffers.ByteBuffer);
James Kuszmaul48413bf2020-09-01 19:19:05 -070032 const time = messageHeader.monotonicSentTime().toFloat64() * 1e-9;
Alex Perry5f474f22020-02-01 12:14:24 -080033 // Short circuit if only one packet
Alex Perry22824d72020-02-29 17:11:43 -080034 if (messageHeader.packetCount() === 1) {
James Kuszmaul48413bf2020-09-01 19:19:05 -070035 this.handlerFunc(messageHeader.dataArray(), time);
Alex Perry5f474f22020-02-01 12:14:24 -080036 return;
37 }
38
39 if (messageHeader.packetIndex() === 0) {
40 this.dataBuffer = new Uint8Array(messageHeader.length());
Alex Perry22824d72020-02-29 17:11:43 -080041 this.receivedMessageLength = 0;
42 }
43 if (!messageHeader.dataLength()) {
44 return;
Alex Perry5f474f22020-02-01 12:14:24 -080045 }
46 this.dataBuffer.set(
47 messageHeader.dataArray(),
48 this.receivedMessageLength);
49 this.receivedMessageLength += messageHeader.dataLength();
50
51 if (messageHeader.packetIndex() === messageHeader.packetCount() - 1) {
James Kuszmaul48413bf2020-09-01 19:19:05 -070052 this.handlerFunc(this.dataBuffer, time);
Alex Perry5f474f22020-02-01 12:14:24 -080053 }
54 }
55}
Alex Perryb3b50792020-01-18 16:13:45 -080056
57// Analogous to the Connection class in //aos/network/web_proxy.h. Because most
58// of the apis are native in JS, it is much simpler.
59export class Connection {
60 private webSocketConnection: WebSocket|null = null;
61 private rtcPeerConnection: RTCPeerConnection|null = null;
Philipp Schrader47445a02020-11-14 17:31:04 -080062 private dataChannel: RTCDataChannel|null = null;
Alex Perryb3b50792020-01-18 16:13:45 -080063 private webSocketUrl: string;
Alex Perry6249aaf2020-02-29 14:51:49 -080064
Philipp Schradere625ba22020-11-16 20:11:37 -080065 private configInternal: Configuration|null = null;
Alex Perry6249aaf2020-02-29 14:51:49 -080066 // A set of functions that accept the config to handle.
Philipp Schradere625ba22020-11-16 20:11:37 -080067 private readonly configHandlers = new Set<(config: Configuration) => void>();
Alex Perry6249aaf2020-02-29 14:51:49 -080068
James Kuszmaul48413bf2020-09-01 19:19:05 -070069 private readonly handlerFuncs =
70 new Map<string, (data: Uint8Array, sentTime: number) => void>();
Alex Perry5f474f22020-02-01 12:14:24 -080071 private readonly handlers = new Set<Handler>();
Alex Perryb3b50792020-01-18 16:13:45 -080072
73 constructor() {
74 const server = location.host;
75 this.webSocketUrl = `ws://${server}/ws`;
76 }
77
Philipp Schradere625ba22020-11-16 20:11:37 -080078 addConfigHandler(handler: (config: Configuration) => void): void {
Alex Perry6249aaf2020-02-29 14:51:49 -080079 this.configHandlers.add(handler);
80 }
81
Alex Perryb49a3fb2020-02-29 15:26:54 -080082 /**
83 * Add a handler for a specific message type. Until we need to handle
84 * different channel names with the same type differently, this is good
85 * enough.
86 */
Philipp Schradere625ba22020-11-16 20:11:37 -080087 addHandler(id: string, handler: (data: Uint8Array, sentTime: number) => void): void {
Alex Perry5f474f22020-02-01 12:14:24 -080088 this.handlerFuncs.set(id, handler);
89 }
90
Alex Perryb3b50792020-01-18 16:13:45 -080091 connect(): void {
92 this.webSocketConnection = new WebSocket(this.webSocketUrl);
93 this.webSocketConnection.binaryType = 'arraybuffer';
94 this.webSocketConnection.addEventListener(
95 'open', () => this.onWebSocketOpen());
96 this.webSocketConnection.addEventListener(
97 'message', (e) => this.onWebSocketMessage(e));
98 }
99
Alex Perry3dfcb812020-03-04 19:32:17 -0800100 getConfig() {
Philipp Schrader47445a02020-11-14 17:31:04 -0800101 return this.configInternal;
Alex Perry6249aaf2020-02-29 14:51:49 -0800102 }
103
Alex Perry5f474f22020-02-01 12:14:24 -0800104 // Handle messages on the DataChannel. Handles the Configuration message as
105 // all other messages are sent on specific DataChannels.
James Kuszmaul1ec74432020-07-30 20:26:45 -0700106 onConfigMessage(data: Uint8Array): void {
Philipp Schradere625ba22020-11-16 20:11:37 -0800107 const fbBuffer = new ByteBuffer(data);
108 this.configInternal = Configuration.getRootAsConfiguration(
109 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perry3dfcb812020-03-04 19:32:17 -0800110 for (const handler of Array.from(this.configHandlers)) {
Alex Perry6249aaf2020-02-29 14:51:49 -0800111 handler(this.configInternal);
Alex Perry5f474f22020-02-01 12:14:24 -0800112 }
113 }
114
115 onDataChannel(ev: RTCDataChannelEvent): void {
116 const channel = ev.channel;
117 const name = channel.label;
118 const channelType = name.split('/').pop();
119 const handlerFunc = this.handlerFuncs.get(channelType);
120 this.handlers.add(new Handler(handlerFunc, channel));
Alex Perryb3b50792020-01-18 16:13:45 -0800121 }
122
123 onIceCandidate(e: RTCPeerConnectionIceEvent): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800124 if (!e.candidate) {
125 return;
126 }
127 const candidate = e.candidate;
Philipp Schradere625ba22020-11-16 20:11:37 -0800128 const builder = new Builder(512);
Alex Perryb3b50792020-01-18 16:13:45 -0800129 const candidateString = builder.createString(candidate.candidate);
130 const sdpMidString = builder.createString(candidate.sdpMid);
131
Philipp Schradere625ba22020-11-16 20:11:37 -0800132 const iceFb = WebSocketIce.createWebSocketIce(
133 builder as unknown as flatbuffers.Builder, candidateString,
134 sdpMidString, candidate.sdpMLineIndex);
135 const messageFb = WebSocketMessage.createWebSocketMessage(
136 builder as unknown as flatbuffers.Builder, Payload.WebSocketIce, iceFb);
Alex Perryb3b50792020-01-18 16:13:45 -0800137 builder.finish(messageFb);
138 const array = builder.asUint8Array();
139 this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
140 }
141
142 // Called for new SDPs. Make sure to set it locally and remotely.
Philipp Schrader47445a02020-11-14 17:31:04 -0800143 onOfferCreated(description: RTCSessionDescriptionInit): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800144 this.rtcPeerConnection.setLocalDescription(description);
Philipp Schradere625ba22020-11-16 20:11:37 -0800145 const builder = new Builder(512);
Alex Perryb3b50792020-01-18 16:13:45 -0800146 const offerString = builder.createString(description.sdp);
147
Philipp Schradere625ba22020-11-16 20:11:37 -0800148 const webSocketSdp = WebSocketSdp.createWebSocketSdp(
149 builder as unknown as flatbuffers.Builder, SdpType.OFFER, offerString);
150 const message = WebSocketMessage.createWebSocketMessage(
151 builder as unknown as flatbuffers.Builder, Payload.WebSocketSdp,
152 webSocketSdp);
Alex Perryb3b50792020-01-18 16:13:45 -0800153 builder.finish(message);
154 const array = builder.asUint8Array();
155 this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
156 }
157
158 // We now have a websocket, so start setting up the peer connection. We only
159 // want a DataChannel, so create it and then create an offer to send.
160 onWebSocketOpen(): void {
161 this.rtcPeerConnection = new RTCPeerConnection({});
Alex Perry5f474f22020-02-01 12:14:24 -0800162 this.rtcPeerConnection.addEventListener(
Alex Perry22824d72020-02-29 17:11:43 -0800163 'datachannel', (e) => this.onDataChannel(e));
Alex Perry5f474f22020-02-01 12:14:24 -0800164 this.dataChannel = this.rtcPeerConnection.createDataChannel('signalling');
James Kuszmaul1ec74432020-07-30 20:26:45 -0700165 this.handlers.add(
166 new Handler((data) => this.onConfigMessage(data), this.dataChannel));
Philipp Schrader47445a02020-11-14 17:31:04 -0800167 // TODO(james): Is this used? Can we delete it?
168 // window.dc = this.dataChannel;
Alex Perryb3b50792020-01-18 16:13:45 -0800169 this.rtcPeerConnection.addEventListener(
170 'icecandidate', (e) => this.onIceCandidate(e));
171 this.rtcPeerConnection.createOffer().then(
172 (offer) => this.onOfferCreated(offer));
173 }
174
175 // When we receive a websocket message, we need to determine what type it is
176 // and handle appropriately. Either by setting the remote description or
177 // adding the remote ice candidate.
178 onWebSocketMessage(e: MessageEvent): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800179 const buffer = new Uint8Array(e.data)
Philipp Schradere625ba22020-11-16 20:11:37 -0800180 const fbBuffer = new ByteBuffer(buffer);
181 const message = WebSocketMessage.getRootAsWebSocketMessage(
182 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perryb3b50792020-01-18 16:13:45 -0800183 switch (message.payloadType()) {
Philipp Schradere625ba22020-11-16 20:11:37 -0800184 case Payload.WebSocketSdp:
185 const sdpFb = message.payload(new WebSocketSdp());
186 if (sdpFb.type() !== SdpType.ANSWER) {
Alex Perryb3b50792020-01-18 16:13:45 -0800187 console.log('got something other than an answer back');
188 break;
189 }
190 this.rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(
191 {'type': 'answer', 'sdp': sdpFb.payload()}));
192 break;
Philipp Schradere625ba22020-11-16 20:11:37 -0800193 case Payload.WebSocketIce:
194 const iceFb = message.payload(new WebSocketIce());
Alex Perryb3b50792020-01-18 16:13:45 -0800195 const candidate = {} as RTCIceCandidateInit;
196 candidate.candidate = iceFb.candidate();
197 candidate.sdpMid = iceFb.sdpMid();
198 candidate.sdpMLineIndex = iceFb.sdpMLineIndex();
199 this.rtcPeerConnection.addIceCandidate(candidate);
200 break;
201 default:
202 console.log('got an unknown message');
203 break;
204 }
205 }
Alex Perry6249aaf2020-02-29 14:51:49 -0800206
207 /**
Alex Perryb49a3fb2020-02-29 15:26:54 -0800208 * Subscribes to messages. Only the most recent connect message is in use. Any
209 * channels not specified in the message are implicitely unsubscribed.
Alex Perry6249aaf2020-02-29 14:51:49 -0800210 * @param a Finished flatbuffer.Builder containing a Connect message to send.
211 */
212 sendConnectMessage(builder: any) {
Alex Perry3dfcb812020-03-04 19:32:17 -0800213 const array = builder.asUint8Array();
Alex Perry6249aaf2020-02-29 14:51:49 -0800214 this.dataChannel.send(array.buffer.slice(array.byteOffset));
215 }
Alex Perryb3b50792020-01-18 16:13:45 -0800216}