blob: 4fbba85ad9c43dd31c33105da1a822cbc3cadad3 [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;
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;
James Kuszmaul527038a2020-12-21 23:40:44 -080014import SubscriberRequest = web_proxy.aos.web_proxy.SubscriberRequest;
15import ChannelRequestFb = web_proxy.aos.web_proxy.ChannelRequest;
16import TransferMethod = web_proxy.aos.web_proxy.TransferMethod;
Alex Perry5f474f22020-02-01 12:14:24 -080017
18// There is one handler for each DataChannel, it maintains the state of
19// multi-part messages and delegates to a callback when the message is fully
20// assembled.
21export class Handler {
22 private dataBuffer: Uint8Array|null = null;
23 private receivedMessageLength: number = 0;
24 constructor(
James Kuszmaul48413bf2020-09-01 19:19:05 -070025 private readonly handlerFunc:
26 (data: Uint8Array, sentTime: number) => void,
Philipp Schrader47445a02020-11-14 17:31:04 -080027 private readonly channel: RTCDataChannel) {
Alex Perry5f474f22020-02-01 12:14:24 -080028 channel.addEventListener('message', (e) => this.handleMessage(e));
29 }
30
31 handleMessage(e: MessageEvent): void {
Philipp Schradere625ba22020-11-16 20:11:37 -080032 const fbBuffer = new ByteBuffer(new Uint8Array(e.data));
33 const messageHeader = MessageHeader.getRootAsMessageHeader(
34 fbBuffer as unknown as flatbuffers.ByteBuffer);
James Kuszmaul48413bf2020-09-01 19:19:05 -070035 const time = messageHeader.monotonicSentTime().toFloat64() * 1e-9;
Alex Perry5f474f22020-02-01 12:14:24 -080036 // Short circuit if only one packet
Alex Perry22824d72020-02-29 17:11:43 -080037 if (messageHeader.packetCount() === 1) {
James Kuszmaul48413bf2020-09-01 19:19:05 -070038 this.handlerFunc(messageHeader.dataArray(), time);
Alex Perry5f474f22020-02-01 12:14:24 -080039 return;
40 }
41
42 if (messageHeader.packetIndex() === 0) {
43 this.dataBuffer = new Uint8Array(messageHeader.length());
Alex Perry22824d72020-02-29 17:11:43 -080044 this.receivedMessageLength = 0;
45 }
46 if (!messageHeader.dataLength()) {
47 return;
Alex Perry5f474f22020-02-01 12:14:24 -080048 }
49 this.dataBuffer.set(
50 messageHeader.dataArray(),
51 this.receivedMessageLength);
52 this.receivedMessageLength += messageHeader.dataLength();
53
54 if (messageHeader.packetIndex() === messageHeader.packetCount() - 1) {
James Kuszmaul48413bf2020-09-01 19:19:05 -070055 this.handlerFunc(this.dataBuffer, time);
Alex Perry5f474f22020-02-01 12:14:24 -080056 }
57 }
58}
Alex Perryb3b50792020-01-18 16:13:45 -080059
James Kuszmaul527038a2020-12-21 23:40:44 -080060class Channel {
61 constructor(public readonly name: string, public readonly type: string) {}
62 key(): string {
63 return this.name + "/" + this.type;
64 }
65}
66
67class ChannelRequest {
68 constructor(
69 public readonly channel: Channel,
70 public readonly transferMethod: TransferMethod) {}
71}
72
Alex Perryb3b50792020-01-18 16:13:45 -080073// Analogous to the Connection class in //aos/network/web_proxy.h. Because most
74// of the apis are native in JS, it is much simpler.
75export class Connection {
76 private webSocketConnection: WebSocket|null = null;
77 private rtcPeerConnection: RTCPeerConnection|null = null;
Philipp Schrader47445a02020-11-14 17:31:04 -080078 private dataChannel: RTCDataChannel|null = null;
Alex Perryb3b50792020-01-18 16:13:45 -080079 private webSocketUrl: string;
Alex Perry6249aaf2020-02-29 14:51:49 -080080
Philipp Schradere625ba22020-11-16 20:11:37 -080081 private configInternal: Configuration|null = null;
Alex Perry6249aaf2020-02-29 14:51:49 -080082 // A set of functions that accept the config to handle.
Philipp Schradere625ba22020-11-16 20:11:37 -080083 private readonly configHandlers = new Set<(config: Configuration) => void>();
Alex Perry6249aaf2020-02-29 14:51:49 -080084
James Kuszmaul48413bf2020-09-01 19:19:05 -070085 private readonly handlerFuncs =
86 new Map<string, (data: Uint8Array, sentTime: number) => void>();
Alex Perry5f474f22020-02-01 12:14:24 -080087 private readonly handlers = new Set<Handler>();
Alex Perryb3b50792020-01-18 16:13:45 -080088
James Kuszmaul527038a2020-12-21 23:40:44 -080089 private subscribedChannels: ChannelRequest[] = [];
90
Alex Perryb3b50792020-01-18 16:13:45 -080091 constructor() {
92 const server = location.host;
93 this.webSocketUrl = `ws://${server}/ws`;
94 }
95
Philipp Schradere625ba22020-11-16 20:11:37 -080096 addConfigHandler(handler: (config: Configuration) => void): void {
Alex Perry6249aaf2020-02-29 14:51:49 -080097 this.configHandlers.add(handler);
98 }
99
Alex Perryb49a3fb2020-02-29 15:26:54 -0800100 /**
James Kuszmaul527038a2020-12-21 23:40:44 -0800101 * Add a handler for a specific message type, with reliable delivery of all
102 * messages.
Alex Perryb49a3fb2020-02-29 15:26:54 -0800103 */
James Kuszmaul527038a2020-12-21 23:40:44 -0800104 addReliableHandler(
105 name: string, type: string,
106 handler: (data: Uint8Array, sentTime: number) => void): void {
107 this.addHandlerImpl(
108 name, type, TransferMethod.EVERYTHING_WITH_HISTORY, handler);
109 }
110
111 /**
112 * Add a handler for a specific message type.
113 */
114 addHandler(
115 name: string, type: string,
116 handler: (data: Uint8Array, sentTime: number) => void): void {
117 this.addHandlerImpl(name, type, TransferMethod.SUBSAMPLE, handler);
118 }
119
120 addHandlerImpl(
121 name: string, type: string, method: TransferMethod,
122 handler: (data: Uint8Array, sentTime: number) => void): void {
123 const channel = new Channel(name, type);
124 const request = new ChannelRequest(channel, method);
125 this.handlerFuncs.set(channel.key(), handler);
126 this.subscribeToChannel(request);
127 }
128
129 subscribeToChannel(channel: ChannelRequest): void {
130 this.subscribedChannels.push(channel);
131 if (this.configInternal === null) {
132 throw new Error(
133 'Must call subscribeToChannel after we\'ve received the config.');
134 }
135 const builder = new Builder(512) as unknown as flatbuffers.Builder;
136 const channels: flatbuffers.Offset[] = [];
137 for (const channel of this.subscribedChannels) {
138 const nameFb = builder.createString(channel.channel.name);
139 const typeFb = builder.createString(channel.channel.type);
140 ChannelFb.startChannel(builder);
141 ChannelFb.addName(builder, nameFb);
142 ChannelFb.addType(builder, typeFb);
143 const channelFb = ChannelFb.endChannel(builder);
144 ChannelRequestFb.startChannelRequest(builder);
145 ChannelRequestFb.addChannel(builder, channelFb);
146 ChannelRequestFb.addMethod(builder, channel.transferMethod);
147 channels.push(ChannelRequestFb.endChannelRequest(builder));
148 }
149
150 const channelsFb =
151 SubscriberRequest.createChannelsToTransferVector(builder, channels);
152 SubscriberRequest.startSubscriberRequest(builder);
153 SubscriberRequest.addChannelsToTransfer(builder, channelsFb);
154 const connect = SubscriberRequest.endSubscriberRequest(builder);
155 builder.finish(connect);
156 this.sendConnectMessage(builder);
Alex Perry5f474f22020-02-01 12:14:24 -0800157 }
158
Alex Perryb3b50792020-01-18 16:13:45 -0800159 connect(): void {
160 this.webSocketConnection = new WebSocket(this.webSocketUrl);
161 this.webSocketConnection.binaryType = 'arraybuffer';
162 this.webSocketConnection.addEventListener(
163 'open', () => this.onWebSocketOpen());
164 this.webSocketConnection.addEventListener(
165 'message', (e) => this.onWebSocketMessage(e));
166 }
167
Alex Perry3dfcb812020-03-04 19:32:17 -0800168 getConfig() {
Philipp Schrader47445a02020-11-14 17:31:04 -0800169 return this.configInternal;
Alex Perry6249aaf2020-02-29 14:51:49 -0800170 }
171
Alex Perry5f474f22020-02-01 12:14:24 -0800172 // Handle messages on the DataChannel. Handles the Configuration message as
173 // all other messages are sent on specific DataChannels.
James Kuszmaul1ec74432020-07-30 20:26:45 -0700174 onConfigMessage(data: Uint8Array): void {
Philipp Schradere625ba22020-11-16 20:11:37 -0800175 const fbBuffer = new ByteBuffer(data);
176 this.configInternal = Configuration.getRootAsConfiguration(
177 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perry3dfcb812020-03-04 19:32:17 -0800178 for (const handler of Array.from(this.configHandlers)) {
Alex Perry6249aaf2020-02-29 14:51:49 -0800179 handler(this.configInternal);
Alex Perry5f474f22020-02-01 12:14:24 -0800180 }
181 }
182
183 onDataChannel(ev: RTCDataChannelEvent): void {
184 const channel = ev.channel;
185 const name = channel.label;
James Kuszmaul527038a2020-12-21 23:40:44 -0800186 const handlerFunc = this.handlerFuncs.get(name);
Alex Perry5f474f22020-02-01 12:14:24 -0800187 this.handlers.add(new Handler(handlerFunc, channel));
Alex Perryb3b50792020-01-18 16:13:45 -0800188 }
189
190 onIceCandidate(e: RTCPeerConnectionIceEvent): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800191 if (!e.candidate) {
192 return;
193 }
194 const candidate = e.candidate;
Philipp Schradere625ba22020-11-16 20:11:37 -0800195 const builder = new Builder(512);
Alex Perryb3b50792020-01-18 16:13:45 -0800196 const candidateString = builder.createString(candidate.candidate);
197 const sdpMidString = builder.createString(candidate.sdpMid);
198
Philipp Schradere625ba22020-11-16 20:11:37 -0800199 const iceFb = WebSocketIce.createWebSocketIce(
200 builder as unknown as flatbuffers.Builder, candidateString,
201 sdpMidString, candidate.sdpMLineIndex);
202 const messageFb = WebSocketMessage.createWebSocketMessage(
203 builder as unknown as flatbuffers.Builder, Payload.WebSocketIce, iceFb);
Alex Perryb3b50792020-01-18 16:13:45 -0800204 builder.finish(messageFb);
205 const array = builder.asUint8Array();
206 this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
207 }
208
209 // Called for new SDPs. Make sure to set it locally and remotely.
Philipp Schrader47445a02020-11-14 17:31:04 -0800210 onOfferCreated(description: RTCSessionDescriptionInit): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800211 this.rtcPeerConnection.setLocalDescription(description);
Philipp Schradere625ba22020-11-16 20:11:37 -0800212 const builder = new Builder(512);
Alex Perryb3b50792020-01-18 16:13:45 -0800213 const offerString = builder.createString(description.sdp);
214
Philipp Schradere625ba22020-11-16 20:11:37 -0800215 const webSocketSdp = WebSocketSdp.createWebSocketSdp(
216 builder as unknown as flatbuffers.Builder, SdpType.OFFER, offerString);
217 const message = WebSocketMessage.createWebSocketMessage(
218 builder as unknown as flatbuffers.Builder, Payload.WebSocketSdp,
219 webSocketSdp);
Alex Perryb3b50792020-01-18 16:13:45 -0800220 builder.finish(message);
221 const array = builder.asUint8Array();
222 this.webSocketConnection.send(array.buffer.slice(array.byteOffset));
223 }
224
225 // We now have a websocket, so start setting up the peer connection. We only
226 // want a DataChannel, so create it and then create an offer to send.
227 onWebSocketOpen(): void {
228 this.rtcPeerConnection = new RTCPeerConnection({});
Alex Perry5f474f22020-02-01 12:14:24 -0800229 this.rtcPeerConnection.addEventListener(
Alex Perry22824d72020-02-29 17:11:43 -0800230 'datachannel', (e) => this.onDataChannel(e));
Alex Perry5f474f22020-02-01 12:14:24 -0800231 this.dataChannel = this.rtcPeerConnection.createDataChannel('signalling');
James Kuszmaul1ec74432020-07-30 20:26:45 -0700232 this.handlers.add(
233 new Handler((data) => this.onConfigMessage(data), this.dataChannel));
Philipp Schrader47445a02020-11-14 17:31:04 -0800234 // TODO(james): Is this used? Can we delete it?
235 // window.dc = this.dataChannel;
Alex Perryb3b50792020-01-18 16:13:45 -0800236 this.rtcPeerConnection.addEventListener(
237 'icecandidate', (e) => this.onIceCandidate(e));
238 this.rtcPeerConnection.createOffer().then(
239 (offer) => this.onOfferCreated(offer));
240 }
241
242 // When we receive a websocket message, we need to determine what type it is
243 // and handle appropriately. Either by setting the remote description or
244 // adding the remote ice candidate.
245 onWebSocketMessage(e: MessageEvent): void {
Alex Perryb3b50792020-01-18 16:13:45 -0800246 const buffer = new Uint8Array(e.data)
Philipp Schradere625ba22020-11-16 20:11:37 -0800247 const fbBuffer = new ByteBuffer(buffer);
248 const message = WebSocketMessage.getRootAsWebSocketMessage(
249 fbBuffer as unknown as flatbuffers.ByteBuffer);
Alex Perryb3b50792020-01-18 16:13:45 -0800250 switch (message.payloadType()) {
Philipp Schradere625ba22020-11-16 20:11:37 -0800251 case Payload.WebSocketSdp:
252 const sdpFb = message.payload(new WebSocketSdp());
253 if (sdpFb.type() !== SdpType.ANSWER) {
Alex Perryb3b50792020-01-18 16:13:45 -0800254 console.log('got something other than an answer back');
255 break;
256 }
257 this.rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(
258 {'type': 'answer', 'sdp': sdpFb.payload()}));
259 break;
Philipp Schradere625ba22020-11-16 20:11:37 -0800260 case Payload.WebSocketIce:
261 const iceFb = message.payload(new WebSocketIce());
Alex Perryb3b50792020-01-18 16:13:45 -0800262 const candidate = {} as RTCIceCandidateInit;
263 candidate.candidate = iceFb.candidate();
264 candidate.sdpMid = iceFb.sdpMid();
265 candidate.sdpMLineIndex = iceFb.sdpMLineIndex();
266 this.rtcPeerConnection.addIceCandidate(candidate);
267 break;
268 default:
269 console.log('got an unknown message');
270 break;
271 }
272 }
Alex Perry6249aaf2020-02-29 14:51:49 -0800273
274 /**
Alex Perryb49a3fb2020-02-29 15:26:54 -0800275 * Subscribes to messages. Only the most recent connect message is in use. Any
276 * channels not specified in the message are implicitely unsubscribed.
Alex Perry6249aaf2020-02-29 14:51:49 -0800277 * @param a Finished flatbuffer.Builder containing a Connect message to send.
278 */
279 sendConnectMessage(builder: any) {
Alex Perry3dfcb812020-03-04 19:32:17 -0800280 const array = builder.asUint8Array();
Alex Perry6249aaf2020-02-29 14:51:49 -0800281 this.dataChannel.send(array.buffer.slice(array.byteOffset));
282 }
Alex Perryb3b50792020-01-18 16:13:45 -0800283}