blob: 0bda8b94aeea4fce5d58e75372b3c7f58da03d0e [file] [log] [blame]
James Kuszmauldac091f2022-03-22 09:35:06 -07001import {Builder, ByteBuffer, Offset} from 'flatbuffers';
2import {Channel as ChannelFb, Configuration} from 'org_frc971/aos/configuration_generated';
3import {ChannelRequest as ChannelRequestFb, ChannelState, MessageHeader, Payload, SdpType, SubscriberRequest, TransferMethod, WebSocketIce, WebSocketMessage, WebSocketSdp} from 'org_frc971/aos/network/web_proxy_generated';
James Kuszmaul136aa2b2022-04-02 14:50:56 -07004import {Schema} from 'flatbuffers_reflection/reflection_generated';
Alex Perry5f474f22020-02-01 12:14:24 -08005
6// There is one handler for each DataChannel, it maintains the state of
7// multi-part messages and delegates to a callback when the message is fully
8// assembled.
9export class Handler {
10 private dataBuffer: Uint8Array|null = null;
11 private receivedMessageLength: number = 0;
12 constructor(
James Kuszmaul48413bf2020-09-01 19:19:05 -070013 private readonly handlerFunc:
14 (data: Uint8Array, sentTime: number) => void,
Philipp Schrader47445a02020-11-14 17:31:04 -080015 private readonly channel: RTCDataChannel) {
Alex Perry5f474f22020-02-01 12:14:24 -080016 channel.addEventListener('message', (e) => this.handleMessage(e));
17 }
18
19 handleMessage(e: MessageEvent): void {
Philipp Schradere625ba22020-11-16 20:11:37 -080020 const fbBuffer = new ByteBuffer(new Uint8Array(e.data));
James Kuszmauldac091f2022-03-22 09:35:06 -070021 const messageHeader = MessageHeader.getRootAsMessageHeader(fbBuffer);
22 const time = Number(messageHeader.monotonicSentTime()) * 1e-9;
James Kuszmaula5822682021-12-23 18:39:28 -080023
James Kuszmauldac091f2022-03-22 09:35:06 -070024 const stateBuilder = new Builder(512);
James Kuszmaula5822682021-12-23 18:39:28 -080025 ChannelState.startChannelState(stateBuilder);
26 ChannelState.addQueueIndex(stateBuilder, messageHeader.queueIndex());
27 ChannelState.addPacketIndex(stateBuilder, messageHeader.packetIndex());
28 const state = ChannelState.endChannelState(stateBuilder);
29 stateBuilder.finish(state);
30 const stateArray = stateBuilder.asUint8Array();
31 this.channel.send(stateArray);
32
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
James Kuszmaul527038a2020-12-21 23:40:44 -080057class Channel {
58 constructor(public readonly name: string, public readonly type: string) {}
59 key(): string {
60 return this.name + "/" + this.type;
61 }
62}
63
64class ChannelRequest {
65 constructor(
66 public readonly channel: Channel,
67 public readonly transferMethod: TransferMethod) {}
68}
69
Alex Perryb3b50792020-01-18 16:13:45 -080070// Analogous to the Connection class in //aos/network/web_proxy.h. Because most
71// of the apis are native in JS, it is much simpler.
72export class Connection {
73 private webSocketConnection: WebSocket|null = null;
74 private rtcPeerConnection: RTCPeerConnection|null = null;
Philipp Schrader47445a02020-11-14 17:31:04 -080075 private dataChannel: RTCDataChannel|null = null;
Alex Perryb3b50792020-01-18 16:13:45 -080076 private webSocketUrl: string;
Alex Perry6249aaf2020-02-29 14:51:49 -080077
Philipp Schradere625ba22020-11-16 20:11:37 -080078 private configInternal: Configuration|null = null;
Alex Perry6249aaf2020-02-29 14:51:49 -080079 // A set of functions that accept the config to handle.
Philipp Schradere625ba22020-11-16 20:11:37 -080080 private readonly configHandlers = new Set<(config: Configuration) => void>();
Alex Perry6249aaf2020-02-29 14:51:49 -080081
James Kuszmaul48413bf2020-09-01 19:19:05 -070082 private readonly handlerFuncs =
James Kuszmaul5f5e1232020-12-22 20:58:00 -080083 new Map<string, ((data: Uint8Array, sentTime: number) => void)[]>();
Alex Perry5f474f22020-02-01 12:14:24 -080084 private readonly handlers = new Set<Handler>();
Alex Perryb3b50792020-01-18 16:13:45 -080085
James Kuszmaul527038a2020-12-21 23:40:44 -080086 private subscribedChannels: ChannelRequest[] = [];
87
Alex Perryb3b50792020-01-18 16:13:45 -080088 constructor() {
89 const server = location.host;
90 this.webSocketUrl = `ws://${server}/ws`;
91 }
92
Philipp Schradere625ba22020-11-16 20:11:37 -080093 addConfigHandler(handler: (config: Configuration) => void): void {
Alex Perry6249aaf2020-02-29 14:51:49 -080094 this.configHandlers.add(handler);
95 }
96
Alex Perryb49a3fb2020-02-29 15:26:54 -080097 /**
James Kuszmaul527038a2020-12-21 23:40:44 -080098 * Add a handler for a specific message type, with reliable delivery of all
99 * messages.
Alex Perryb49a3fb2020-02-29 15:26:54 -0800100 */
James Kuszmaul527038a2020-12-21 23:40:44 -0800101 addReliableHandler(
102 name: string, type: string,
103 handler: (data: Uint8Array, sentTime: number) => void): void {
104 this.addHandlerImpl(
James Kuszmaul1a29c082022-02-03 14:02:47 -0800105 name, type, TransferMethod.LOSSLESS, handler);
James Kuszmaul527038a2020-12-21 23:40:44 -0800106 }
107
108 /**
109 * Add a handler for a specific message type.
110 */
111 addHandler(
112 name: string, type: string,
113 handler: (data: Uint8Array, sentTime: number) => void): void {
114 this.addHandlerImpl(name, type, TransferMethod.SUBSAMPLE, handler);
115 }
116
117 addHandlerImpl(
118 name: string, type: string, method: TransferMethod,
119 handler: (data: Uint8Array, sentTime: number) => void): void {
120 const channel = new Channel(name, type);
121 const request = new ChannelRequest(channel, method);
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800122 if (!this.handlerFuncs.has(channel.key())) {
123 this.handlerFuncs.set(channel.key(), []);
James Kuszmaulc4ae11c2020-12-26 16:26:58 -0800124 } else {
James Kuszmaul1a29c082022-02-03 14:02:47 -0800125 if (method == TransferMethod.LOSSLESS) {
James Kuszmaulc4ae11c2020-12-26 16:26:58 -0800126 console.warn(
127 'Behavior of multiple reliable handlers is currently poorly ' +
128 'defined and may not actually deliver all of the messages.');
129 }
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800130 }
131 this.handlerFuncs.get(channel.key()).push(handler);
James Kuszmaul527038a2020-12-21 23:40:44 -0800132 this.subscribeToChannel(request);
133 }
134
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800135 getSchema(typeName: string): Schema {
136 let schema = null;
137 const config = this.getConfig();
138 for (let ii = 0; ii < config.channelsLength(); ++ii) {
139 if (config.channels(ii).type() === typeName) {
140 schema = config.channels(ii).schema();
141 }
142 }
143 if (schema === null) {
144 throw new Error('Unable to find schema for ' + typeName);
145 }
146 return schema;
147 }
148
James Kuszmaul527038a2020-12-21 23:40:44 -0800149 subscribeToChannel(channel: ChannelRequest): void {
150 this.subscribedChannels.push(channel);
151 if (this.configInternal === null) {
152 throw new Error(
153 'Must call subscribeToChannel after we\'ve received the config.');
154 }
James Kuszmauldac091f2022-03-22 09:35:06 -0700155 const builder = new Builder(512);
156 const channels: Offset[] = [];
James Kuszmaul527038a2020-12-21 23:40:44 -0800157 for (const channel of this.subscribedChannels) {
158 const nameFb = builder.createString(channel.channel.name);
159 const typeFb = builder.createString(channel.channel.type);
160 ChannelFb.startChannel(builder);
161 ChannelFb.addName(builder, nameFb);
162 ChannelFb.addType(builder, typeFb);
163 const channelFb = ChannelFb.endChannel(builder);
164 ChannelRequestFb.startChannelRequest(builder);
165 ChannelRequestFb.addChannel(builder, channelFb);
166 ChannelRequestFb.addMethod(builder, channel.transferMethod);
167 channels.push(ChannelRequestFb.endChannelRequest(builder));
168 }
169
170 const channelsFb =
171 SubscriberRequest.createChannelsToTransferVector(builder, channels);
172 SubscriberRequest.startSubscriberRequest(builder);
173 SubscriberRequest.addChannelsToTransfer(builder, channelsFb);
174 const connect = SubscriberRequest.endSubscriberRequest(builder);
175 builder.finish(connect);
176 this.sendConnectMessage(builder);
Alex Perry5f474f22020-02-01 12:14:24 -0800177 }
178
Alex Perryb3b50792020-01-18 16:13:45 -0800179 connect(): void {
180 this.webSocketConnection = new WebSocket(this.webSocketUrl);
181 this.webSocketConnection.binaryType = 'arraybuffer';
182 this.webSocketConnection.addEventListener(
183 'open', () => this.onWebSocketOpen());
184 this.webSocketConnection.addEventListener(
185 'message', (e) => this.onWebSocketMessage(e));
186 }
187
Alex Perry3dfcb812020-03-04 19:32:17 -0800188 getConfig() {
Philipp Schrader47445a02020-11-14 17:31:04 -0800189 return this.configInternal;
Alex Perry6249aaf2020-02-29 14:51:49 -0800190 }
191
Alex Perry5f474f22020-02-01 12:14:24 -0800192 // Handle messages on the DataChannel. Handles the Configuration message as
193 // all other messages are sent on specific DataChannels.
James Kuszmaul1ec74432020-07-30 20:26:45 -0700194 onConfigMessage(data: Uint8Array): void {
Philipp Schradere625ba22020-11-16 20:11:37 -0800195 const fbBuffer = new ByteBuffer(data);
James Kuszmauldac091f2022-03-22 09:35:06 -0700196 this.configInternal = Configuration.getRootAsConfiguration(fbBuffer);
Alex Perry3dfcb812020-03-04 19:32:17 -0800197 for (const handler of Array.from(this.configHandlers)) {
Alex Perry6249aaf2020-02-29 14:51:49 -0800198 handler(this.configInternal);
Alex Perry5f474f22020-02-01 12:14:24 -0800199 }
200 }
201
202 onDataChannel(ev: RTCDataChannelEvent): void {
203 const channel = ev.channel;
204 const name = channel.label;
James Kuszmaul5f5e1232020-12-22 20:58:00 -0800205 const handlers = this.handlerFuncs.get(name);
206 for (const handler of handlers) {
207 this.handlers.add(new Handler(handler, channel));
208 }
Alex Perryb3b50792020-01-18 16:13:45 -0800209 }
210
211 onIceCandidate(e: RTCPeerConnectionIceEvent): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800212 if (!e.candidate) {
213 return;
214 }
215 const candidate = e.candidate;
Philipp Schradere625ba22020-11-16 20:11:37 -0800216 const builder = new Builder(512);
Alex Perryb3b50792020-01-18 16:13:45 -0800217 const candidateString = builder.createString(candidate.candidate);
218 const sdpMidString = builder.createString(candidate.sdpMid);
219
Philipp Schradere625ba22020-11-16 20:11:37 -0800220 const iceFb = WebSocketIce.createWebSocketIce(
James Kuszmauldac091f2022-03-22 09:35:06 -0700221 builder, candidateString, sdpMidString, candidate.sdpMLineIndex);
Philipp Schradere625ba22020-11-16 20:11:37 -0800222 const messageFb = WebSocketMessage.createWebSocketMessage(
James Kuszmauldac091f2022-03-22 09:35:06 -0700223 builder, Payload.WebSocketIce, iceFb);
Alex Perryb3b50792020-01-18 16:13:45 -0800224 builder.finish(messageFb);
225 const array = builder.asUint8Array();
226 this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
227 }
228
Philipp Schrader87277f42022-01-01 07:45:12 -0800229 onIceCandidateError(e: Event): void {
James Kuszmaul54424d02020-12-26 18:09:20 -0800230 console.warn(e);
231 }
232
Alex Perryb3b50792020-01-18 16:13:45 -0800233 // Called for new SDPs. Make sure to set it locally and remotely.
Philipp Schrader47445a02020-11-14 17:31:04 -0800234 onOfferCreated(description: RTCSessionDescriptionInit): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800235 this.rtcPeerConnection.setLocalDescription(description);
Philipp Schradere625ba22020-11-16 20:11:37 -0800236 const builder = new Builder(512);
Alex Perryb3b50792020-01-18 16:13:45 -0800237 const offerString = builder.createString(description.sdp);
238
Philipp Schradere625ba22020-11-16 20:11:37 -0800239 const webSocketSdp = WebSocketSdp.createWebSocketSdp(
James Kuszmauldac091f2022-03-22 09:35:06 -0700240 builder, SdpType.OFFER, offerString);
Philipp Schradere625ba22020-11-16 20:11:37 -0800241 const message = WebSocketMessage.createWebSocketMessage(
James Kuszmauldac091f2022-03-22 09:35:06 -0700242 builder, Payload.WebSocketSdp,
Philipp Schradere625ba22020-11-16 20:11:37 -0800243 webSocketSdp);
Alex Perryb3b50792020-01-18 16:13:45 -0800244 builder.finish(message);
245 const array = builder.asUint8Array();
246 this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
247 }
248
249 // We now have a websocket, so start setting up the peer connection. We only
250 // want a DataChannel, so create it and then create an offer to send.
251 onWebSocketOpen(): void {
James Kuszmaul54424d02020-12-26 18:09:20 -0800252 this.rtcPeerConnection = new RTCPeerConnection(
253 {'iceServers': [{'urls': ['stun:stun.l.google.com:19302']}]});
Alex Perry5f474f22020-02-01 12:14:24 -0800254 this.rtcPeerConnection.addEventListener(
Alex Perry22824d72020-02-29 17:11:43 -0800255 'datachannel', (e) => this.onDataChannel(e));
Alex Perry5f474f22020-02-01 12:14:24 -0800256 this.dataChannel = this.rtcPeerConnection.createDataChannel('signalling');
James Kuszmaul1ec74432020-07-30 20:26:45 -0700257 this.handlers.add(
258 new Handler((data) => this.onConfigMessage(data), this.dataChannel));
Alex Perryb3b50792020-01-18 16:13:45 -0800259 this.rtcPeerConnection.addEventListener(
260 'icecandidate', (e) => this.onIceCandidate(e));
James Kuszmaul54424d02020-12-26 18:09:20 -0800261 this.rtcPeerConnection.addEventListener(
262 'icecandidateerror', (e) => this.onIceCandidateError(e));
Alex Perryb3b50792020-01-18 16:13:45 -0800263 this.rtcPeerConnection.createOffer().then(
264 (offer) => this.onOfferCreated(offer));
265 }
266
267 // When we receive a websocket message, we need to determine what type it is
268 // and handle appropriately. Either by setting the remote description or
269 // adding the remote ice candidate.
270 onWebSocketMessage(e: MessageEvent): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800271 const buffer = new Uint8Array(e.data)
Philipp Schradere625ba22020-11-16 20:11:37 -0800272 const fbBuffer = new ByteBuffer(buffer);
James Kuszmauldac091f2022-03-22 09:35:06 -0700273 const message = WebSocketMessage.getRootAsWebSocketMessage(fbBuffer);
Alex Perryb3b50792020-01-18 16:13:45 -0800274 switch (message.payloadType()) {
Philipp Schradere625ba22020-11-16 20:11:37 -0800275 case Payload.WebSocketSdp:
276 const sdpFb = message.payload(new WebSocketSdp());
277 if (sdpFb.type() !== SdpType.ANSWER) {
Alex Perryb3b50792020-01-18 16:13:45 -0800278 console.log('got something other than an answer back');
279 break;
280 }
281 this.rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(
282 {'type': 'answer', 'sdp': sdpFb.payload()}));
283 break;
Philipp Schradere625ba22020-11-16 20:11:37 -0800284 case Payload.WebSocketIce:
285 const iceFb = message.payload(new WebSocketIce());
Alex Perryb3b50792020-01-18 16:13:45 -0800286 const candidate = {} as RTCIceCandidateInit;
287 candidate.candidate = iceFb.candidate();
288 candidate.sdpMid = iceFb.sdpMid();
289 candidate.sdpMLineIndex = iceFb.sdpMLineIndex();
290 this.rtcPeerConnection.addIceCandidate(candidate);
291 break;
292 default:
293 console.log('got an unknown message');
294 break;
295 }
296 }
Alex Perry6249aaf2020-02-29 14:51:49 -0800297
298 /**
Alex Perryb49a3fb2020-02-29 15:26:54 -0800299 * Subscribes to messages. Only the most recent connect message is in use. Any
300 * channels not specified in the message are implicitely unsubscribed.
Alex Perry6249aaf2020-02-29 14:51:49 -0800301 * @param a Finished flatbuffer.Builder containing a Connect message to send.
302 */
303 sendConnectMessage(builder: any) {
Alex Perry3dfcb812020-03-04 19:32:17 -0800304 const array = builder.asUint8Array();
Alex Perry6249aaf2020-02-29 14:51:49 -0800305 this.dataChannel.send(array.buffer.slice(array.byteOffset));
306 }
Alex Perryb3b50792020-01-18 16:13:45 -0800307}