blob: 54618993e1906c14a645d5b854233d9e69e2c474 [file] [log] [blame]
Philipp Schradere625ba22020-11-16 20:11:37 -08001import * as configuration from 'org_frc971/aos/configuration_generated';
2import * as web_proxy from 'org_frc971/aos/network/web_proxy_generated';
3import {Builder} from 'org_frc971/external/com_github_google_flatbuffers/ts/builder';
4import {ByteBuffer} from 'org_frc971/external/com_github_google_flatbuffers/ts/byte-buffer';
5
James Kuszmaul527038a2020-12-21 23:40:44 -08006import ChannelFb = configuration.aos.Channel;
Philipp Schradere625ba22020-11-16 20:11:37 -08007import Configuration = configuration.aos.Configuration;
James Kuszmaul5f5e1232020-12-22 20:58:00 -08008import Schema = configuration.reflection.Schema;
Philipp Schradere625ba22020-11-16 20:11:37 -08009import MessageHeader = web_proxy.aos.web_proxy.MessageHeader;
10import WebSocketIce = web_proxy.aos.web_proxy.WebSocketIce;
11import WebSocketMessage = web_proxy.aos.web_proxy.WebSocketMessage;
12import Payload = web_proxy.aos.web_proxy.Payload;
13import WebSocketSdp = web_proxy.aos.web_proxy.WebSocketSdp;
14import SdpType = web_proxy.aos.web_proxy.SdpType;
James Kuszmaul527038a2020-12-21 23:40:44 -080015import SubscriberRequest = web_proxy.aos.web_proxy.SubscriberRequest;
16import ChannelRequestFb = web_proxy.aos.web_proxy.ChannelRequest;
17import TransferMethod = web_proxy.aos.web_proxy.TransferMethod;
James Kuszmaula5822682021-12-23 18:39:28 -080018import ChannelState = web_proxy.aos.web_proxy.ChannelState;
Alex Perry5f474f22020-02-01 12:14:24 -080019
20// There is one handler for each DataChannel, it maintains the state of
21// multi-part messages and delegates to a callback when the message is fully
22// assembled.
23export class Handler {
24 private dataBuffer: Uint8Array|null = null;
25 private receivedMessageLength: number = 0;
26 constructor(
James Kuszmaul48413bf2020-09-01 19:19:05 -070027 private readonly handlerFunc:
28 (data: Uint8Array, sentTime: number) => void,
Philipp Schrader47445a02020-11-14 17:31:04 -080029 private readonly channel: RTCDataChannel) {
Alex Perry5f474f22020-02-01 12:14:24 -080030 channel.addEventListener('message', (e) => this.handleMessage(e));
31 }
32
33 handleMessage(e: MessageEvent): void {
Philipp Schradere625ba22020-11-16 20:11:37 -080034 const fbBuffer = new ByteBuffer(new Uint8Array(e.data));
35 const messageHeader = MessageHeader.getRootAsMessageHeader(
36 fbBuffer as unknown as flatbuffers.ByteBuffer);
James Kuszmaul48413bf2020-09-01 19:19:05 -070037 const time = messageHeader.monotonicSentTime().toFloat64() * 1e-9;
James Kuszmaula5822682021-12-23 18:39:28 -080038
39 const stateBuilder = new Builder(512) as unknown as flatbuffers.Builder;
40 ChannelState.startChannelState(stateBuilder);
41 ChannelState.addQueueIndex(stateBuilder, messageHeader.queueIndex());
42 ChannelState.addPacketIndex(stateBuilder, messageHeader.packetIndex());
43 const state = ChannelState.endChannelState(stateBuilder);
44 stateBuilder.finish(state);
45 const stateArray = stateBuilder.asUint8Array();
46 this.channel.send(stateArray);
47
Alex Perry5f474f22020-02-01 12:14:24 -080048 // Short circuit if only one packet
Alex Perry22824d72020-02-29 17:11:43 -080049 if (messageHeader.packetCount() === 1) {
James Kuszmaul48413bf2020-09-01 19:19:05 -070050 this.handlerFunc(messageHeader.dataArray(), time);
Alex Perry5f474f22020-02-01 12:14:24 -080051 return;
52 }
53
54 if (messageHeader.packetIndex() === 0) {
55 this.dataBuffer = new Uint8Array(messageHeader.length());
Alex Perry22824d72020-02-29 17:11:43 -080056 this.receivedMessageLength = 0;
57 }
58 if (!messageHeader.dataLength()) {
59 return;
Alex Perry5f474f22020-02-01 12:14:24 -080060 }
61 this.dataBuffer.set(
62 messageHeader.dataArray(),
63 this.receivedMessageLength);
64 this.receivedMessageLength += messageHeader.dataLength();
65
66 if (messageHeader.packetIndex() === messageHeader.packetCount() - 1) {
James Kuszmaul48413bf2020-09-01 19:19:05 -070067 this.handlerFunc(this.dataBuffer, time);
Alex Perry5f474f22020-02-01 12:14:24 -080068 }
69 }
70}
Alex Perryb3b50792020-01-18 16:13:45 -080071
James Kuszmaul527038a2020-12-21 23:40:44 -080072class Channel {
73 constructor(public readonly name: string, public readonly type: string) {}
74 key(): string {
75 return this.name + "/" + this.type;
76 }
77}
78
79class ChannelRequest {
80 constructor(
81 public readonly channel: Channel,
82 public readonly transferMethod: TransferMethod) {}
83}
84
Alex Perryb3b50792020-01-18 16:13:45 -080085// Analogous to the Connection class in //aos/network/web_proxy.h. Because most
86// of the apis are native in JS, it is much simpler.
87export class Connection {
88 private webSocketConnection: WebSocket|null = null;
89 private rtcPeerConnection: RTCPeerConnection|null = null;
Philipp Schrader47445a02020-11-14 17:31:04 -080090 private dataChannel: RTCDataChannel|null = null;
Alex Perryb3b50792020-01-18 16:13:45 -080091 private webSocketUrl: string;
Alex Perry6249aaf2020-02-29 14:51:49 -080092
Philipp Schradere625ba22020-11-16 20:11:37 -080093 private configInternal: Configuration|null = null;
Alex Perry6249aaf2020-02-29 14:51:49 -080094 // A set of functions that accept the config to handle.
Philipp Schradere625ba22020-11-16 20:11:37 -080095 private readonly configHandlers = new Set<(config: Configuration) => void>();
Alex Perry6249aaf2020-02-29 14:51:49 -080096
James Kuszmaul48413bf2020-09-01 19:19:05 -070097 private readonly handlerFuncs =
James Kuszmaul5f5e1232020-12-22 20:58:00 -080098 new Map<string, ((data: Uint8Array, sentTime: number) => void)[]>();
Alex Perry5f474f22020-02-01 12:14:24 -080099 private readonly handlers = new Set<Handler>();
Alex Perryb3b50792020-01-18 16:13:45 -0800100
James Kuszmaul527038a2020-12-21 23:40:44 -0800101 private subscribedChannels: ChannelRequest[] = [];
102
Alex Perryb3b50792020-01-18 16:13:45 -0800103 constructor() {
104 const server = location.host;
105 this.webSocketUrl = `ws://${server}/ws`;
106 }
107
Philipp Schradere625ba22020-11-16 20:11:37 -0800108 addConfigHandler(handler: (config: Configuration) => void): void {
Alex Perry6249aaf2020-02-29 14:51:49 -0800109 this.configHandlers.add(handler);
110 }
111
Alex Perryb49a3fb2020-02-29 15:26:54 -0800112 /**
James Kuszmaul527038a2020-12-21 23:40:44 -0800113 * Add a handler for a specific message type, with reliable delivery of all
114 * messages.
Alex Perryb49a3fb2020-02-29 15:26:54 -0800115 */
James Kuszmaul527038a2020-12-21 23:40:44 -0800116 addReliableHandler(
117 name: string, type: string,
118 handler: (data: Uint8Array, sentTime: number) => void): void {
119 this.addHandlerImpl(
120 name, type, TransferMethod.EVERYTHING_WITH_HISTORY, handler);
121 }
122
123 /**
124 * Add a handler for a specific message type.
125 */
126 addHandler(
127 name: string, type: string,
128 handler: (data: Uint8Array, sentTime: number) => void): void {
129 this.addHandlerImpl(name, type, TransferMethod.SUBSAMPLE, handler);
130 }
131
132 addHandlerImpl(
133 name: string, type: string, method: TransferMethod,
134 handler: (data: Uint8Array, sentTime: number) => void): void {
135 const channel = new Channel(name, type);
136 const request = new ChannelRequest(channel, method);
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800137 if (!this.handlerFuncs.has(channel.key())) {
138 this.handlerFuncs.set(channel.key(), []);
James Kuszmaulc4ae11c2020-12-26 16:26:58 -0800139 } else {
140 if (method == TransferMethod.EVERYTHING_WITH_HISTORY) {
141 console.warn(
142 'Behavior of multiple reliable handlers is currently poorly ' +
143 'defined and may not actually deliver all of the messages.');
144 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800145 }
146 this.handlerFuncs.get(channel.key()).push(handler);
James Kuszmaul527038a2020-12-21 23:40:44 -0800147 this.subscribeToChannel(request);
148 }
149
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800150 getSchema(typeName: string): Schema {
151 let schema = null;
152 const config = this.getConfig();
153 for (let ii = 0; ii < config.channelsLength(); ++ii) {
154 if (config.channels(ii).type() === typeName) {
155 schema = config.channels(ii).schema();
156 }
157 }
158 if (schema === null) {
159 throw new Error('Unable to find schema for ' + typeName);
160 }
161 return schema;
162 }
163
James Kuszmaul527038a2020-12-21 23:40:44 -0800164 subscribeToChannel(channel: ChannelRequest): void {
165 this.subscribedChannels.push(channel);
166 if (this.configInternal === null) {
167 throw new Error(
168 'Must call subscribeToChannel after we\'ve received the config.');
169 }
170 const builder = new Builder(512) as unknown as flatbuffers.Builder;
171 const channels: flatbuffers.Offset[] = [];
172 for (const channel of this.subscribedChannels) {
173 const nameFb = builder.createString(channel.channel.name);
174 const typeFb = builder.createString(channel.channel.type);
175 ChannelFb.startChannel(builder);
176 ChannelFb.addName(builder, nameFb);
177 ChannelFb.addType(builder, typeFb);
178 const channelFb = ChannelFb.endChannel(builder);
179 ChannelRequestFb.startChannelRequest(builder);
180 ChannelRequestFb.addChannel(builder, channelFb);
181 ChannelRequestFb.addMethod(builder, channel.transferMethod);
182 channels.push(ChannelRequestFb.endChannelRequest(builder));
183 }
184
185 const channelsFb =
186 SubscriberRequest.createChannelsToTransferVector(builder, channels);
187 SubscriberRequest.startSubscriberRequest(builder);
188 SubscriberRequest.addChannelsToTransfer(builder, channelsFb);
189 const connect = SubscriberRequest.endSubscriberRequest(builder);
190 builder.finish(connect);
191 this.sendConnectMessage(builder);
Alex Perry5f474f22020-02-01 12:14:24 -0800192 }
193
Alex Perryb3b50792020-01-18 16:13:45 -0800194 connect(): void {
195 this.webSocketConnection = new WebSocket(this.webSocketUrl);
196 this.webSocketConnection.binaryType = 'arraybuffer';
197 this.webSocketConnection.addEventListener(
198 'open', () => this.onWebSocketOpen());
199 this.webSocketConnection.addEventListener(
200 'message', (e) => this.onWebSocketMessage(e));
201 }
202
Alex Perry3dfcb812020-03-04 19:32:17 -0800203 getConfig() {
Philipp Schrader47445a02020-11-14 17:31:04 -0800204 return this.configInternal;
Alex Perry6249aaf2020-02-29 14:51:49 -0800205 }
206
Alex Perry5f474f22020-02-01 12:14:24 -0800207 // Handle messages on the DataChannel. Handles the Configuration message as
208 // all other messages are sent on specific DataChannels.
James Kuszmaul1ec74432020-07-30 20:26:45 -0700209 onConfigMessage(data: Uint8Array): void {
Philipp Schradere625ba22020-11-16 20:11:37 -0800210 const fbBuffer = new ByteBuffer(data);
211 this.configInternal = Configuration.getRootAsConfiguration(
212 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perry3dfcb812020-03-04 19:32:17 -0800213 for (const handler of Array.from(this.configHandlers)) {
Alex Perry6249aaf2020-02-29 14:51:49 -0800214 handler(this.configInternal);
Alex Perry5f474f22020-02-01 12:14:24 -0800215 }
216 }
217
218 onDataChannel(ev: RTCDataChannelEvent): void {
219 const channel = ev.channel;
220 const name = channel.label;
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800221 const handlers = this.handlerFuncs.get(name);
222 for (const handler of handlers) {
223 this.handlers.add(new Handler(handler, channel));
224 }
Alex Perryb3b50792020-01-18 16:13:45 -0800225 }
226
227 onIceCandidate(e: RTCPeerConnectionIceEvent): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800228 if (!e.candidate) {
229 return;
230 }
231 const candidate = e.candidate;
Philipp Schradere625ba22020-11-16 20:11:37 -0800232 const builder = new Builder(512);
Alex Perryb3b50792020-01-18 16:13:45 -0800233 const candidateString = builder.createString(candidate.candidate);
234 const sdpMidString = builder.createString(candidate.sdpMid);
235
Philipp Schradere625ba22020-11-16 20:11:37 -0800236 const iceFb = WebSocketIce.createWebSocketIce(
237 builder as unknown as flatbuffers.Builder, candidateString,
238 sdpMidString, candidate.sdpMLineIndex);
239 const messageFb = WebSocketMessage.createWebSocketMessage(
240 builder as unknown as flatbuffers.Builder, Payload.WebSocketIce, iceFb);
Alex Perryb3b50792020-01-18 16:13:45 -0800241 builder.finish(messageFb);
242 const array = builder.asUint8Array();
243 this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
244 }
245
Philipp Schrader87277f42022-01-01 07:45:12 -0800246 onIceCandidateError(e: Event): void {
James Kuszmaul54424d02020-12-26 18:09:20 -0800247 console.warn(e);
248 }
249
Alex Perryb3b50792020-01-18 16:13:45 -0800250 // Called for new SDPs. Make sure to set it locally and remotely.
Philipp Schrader47445a02020-11-14 17:31:04 -0800251 onOfferCreated(description: RTCSessionDescriptionInit): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800252 this.rtcPeerConnection.setLocalDescription(description);
Philipp Schradere625ba22020-11-16 20:11:37 -0800253 const builder = new Builder(512);
Alex Perryb3b50792020-01-18 16:13:45 -0800254 const offerString = builder.createString(description.sdp);
255
Philipp Schradere625ba22020-11-16 20:11:37 -0800256 const webSocketSdp = WebSocketSdp.createWebSocketSdp(
257 builder as unknown as flatbuffers.Builder, SdpType.OFFER, offerString);
258 const message = WebSocketMessage.createWebSocketMessage(
259 builder as unknown as flatbuffers.Builder, Payload.WebSocketSdp,
260 webSocketSdp);
Alex Perryb3b50792020-01-18 16:13:45 -0800261 builder.finish(message);
262 const array = builder.asUint8Array();
263 this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
264 }
265
266 // We now have a websocket, so start setting up the peer connection. We only
267 // want a DataChannel, so create it and then create an offer to send.
268 onWebSocketOpen(): void {
James Kuszmaul54424d02020-12-26 18:09:20 -0800269 this.rtcPeerConnection = new RTCPeerConnection(
270 {'iceServers': [{'urls': ['stun:stun.l.google.com:19302']}]});
Alex Perry5f474f22020-02-01 12:14:24 -0800271 this.rtcPeerConnection.addEventListener(
Alex Perry22824d72020-02-29 17:11:43 -0800272 'datachannel', (e) => this.onDataChannel(e));
Alex Perry5f474f22020-02-01 12:14:24 -0800273 this.dataChannel = this.rtcPeerConnection.createDataChannel('signalling');
James Kuszmaul1ec74432020-07-30 20:26:45 -0700274 this.handlers.add(
275 new Handler((data) => this.onConfigMessage(data), this.dataChannel));
Alex Perryb3b50792020-01-18 16:13:45 -0800276 this.rtcPeerConnection.addEventListener(
277 'icecandidate', (e) => this.onIceCandidate(e));
James Kuszmaul54424d02020-12-26 18:09:20 -0800278 this.rtcPeerConnection.addEventListener(
279 'icecandidateerror', (e) => this.onIceCandidateError(e));
Alex Perryb3b50792020-01-18 16:13:45 -0800280 this.rtcPeerConnection.createOffer().then(
281 (offer) => this.onOfferCreated(offer));
282 }
283
284 // When we receive a websocket message, we need to determine what type it is
285 // and handle appropriately. Either by setting the remote description or
286 // adding the remote ice candidate.
287 onWebSocketMessage(e: MessageEvent): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800288 const buffer = new Uint8Array(e.data)
Philipp Schradere625ba22020-11-16 20:11:37 -0800289 const fbBuffer = new ByteBuffer(buffer);
290 const message = WebSocketMessage.getRootAsWebSocketMessage(
291 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perryb3b50792020-01-18 16:13:45 -0800292 switch (message.payloadType()) {
Philipp Schradere625ba22020-11-16 20:11:37 -0800293 case Payload.WebSocketSdp:
294 const sdpFb = message.payload(new WebSocketSdp());
295 if (sdpFb.type() !== SdpType.ANSWER) {
Alex Perryb3b50792020-01-18 16:13:45 -0800296 console.log('got something other than an answer back');
297 break;
298 }
299 this.rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(
300 {'type': 'answer', 'sdp': sdpFb.payload()}));
301 break;
Philipp Schradere625ba22020-11-16 20:11:37 -0800302 case Payload.WebSocketIce:
303 const iceFb = message.payload(new WebSocketIce());
Alex Perryb3b50792020-01-18 16:13:45 -0800304 const candidate = {} as RTCIceCandidateInit;
305 candidate.candidate = iceFb.candidate();
306 candidate.sdpMid = iceFb.sdpMid();
307 candidate.sdpMLineIndex = iceFb.sdpMLineIndex();
308 this.rtcPeerConnection.addIceCandidate(candidate);
309 break;
310 default:
311 console.log('got an unknown message');
312 break;
313 }
314 }
Alex Perry6249aaf2020-02-29 14:51:49 -0800315
316 /**
Alex Perryb49a3fb2020-02-29 15:26:54 -0800317 * Subscribes to messages. Only the most recent connect message is in use. Any
318 * channels not specified in the message are implicitely unsubscribed.
Alex Perry6249aaf2020-02-29 14:51:49 -0800319 * @param a Finished flatbuffer.Builder containing a Connect message to send.
320 */
321 sendConnectMessage(builder: any) {
Alex Perry3dfcb812020-03-04 19:32:17 -0800322 const array = builder.asUint8Array();
Alex Perry6249aaf2020-02-29 14:51:49 -0800323 this.dataChannel.send(array.buffer.slice(array.byteOffset));
324 }
Alex Perryb3b50792020-01-18 16:13:45 -0800325}