475 lines
12 KiB
TypeScript
475 lines
12 KiB
TypeScript
import { BufReader } from "https://deno.land/std@0.157.0/io/buffer.ts";
|
|
import { ByteSet, LengthType } from "https://deno.land/x/bytes@1.0.3/mod.ts";
|
|
|
|
const PORT = 5588;
|
|
|
|
type Car = Plate;
|
|
|
|
type U16 = number;
|
|
type U32 = number;
|
|
|
|
type Deciseconds = U32;
|
|
type Location = U16;
|
|
type Plate = string;
|
|
|
|
type Road = U16;
|
|
type Mile = U16;
|
|
|
|
type SpeedLimit = U16;
|
|
type SpeedMPH100 = U16;
|
|
type Timestamp = U32;
|
|
|
|
interface Camera {
|
|
road: Road;
|
|
location: Location;
|
|
speedLimit: SpeedLimit;
|
|
}
|
|
|
|
interface Report {
|
|
camera: Camera;
|
|
car: Car;
|
|
timestamp: Timestamp;
|
|
}
|
|
|
|
const tEnc = new TextEncoder();
|
|
const tDec = new TextDecoder();
|
|
|
|
console.log(`Starting TCP listener on on 0.0.0.0:${PORT}`);
|
|
|
|
function session(conn: Deno.Conn) {
|
|
serve(conn).catch((e) => console.error("Exception during session:", e))
|
|
.finally(() => disconnect(conn));
|
|
}
|
|
|
|
enum MessageType {
|
|
ERROR = 0x10, // Server -> Client
|
|
PLATE = 0x20, // Client -> Server
|
|
TICKET = 0x21, // Server -> Client
|
|
WANT_HEARTBEAT = 0x40, // Client -> Server
|
|
HEARTBEAT = 0x41, // Server -> Client
|
|
I_AM_CAMERA = 0x80, // Client -> Server
|
|
I_AM_DISPATCHER = 0x81, // Client -> Server
|
|
}
|
|
|
|
console.log("Message Types:", MessageType);
|
|
|
|
type ErrorMessage = [
|
|
string, // msg
|
|
];
|
|
|
|
type PlateMessage = [
|
|
Plate,
|
|
Timestamp,
|
|
];
|
|
|
|
type TicketMessage = [
|
|
Plate, // plate
|
|
Road, // road
|
|
Mile, // mile1
|
|
Timestamp, // timestamp1
|
|
Mile, // mile2
|
|
Timestamp, // timestamp2
|
|
SpeedMPH100, // speed
|
|
];
|
|
|
|
type WantHeartbeatMessage = [
|
|
Deciseconds,
|
|
];
|
|
|
|
type HeartbeatMessage = [];
|
|
|
|
type IAmCameraMessage = [
|
|
Road,
|
|
Mile,
|
|
SpeedLimit,
|
|
];
|
|
|
|
type IAmDispatcherMessage = [
|
|
Road[],
|
|
];
|
|
|
|
type Message =
|
|
| ErrorMessage
|
|
| PlateMessage
|
|
| TicketMessage
|
|
| WantHeartbeatMessage
|
|
| HeartbeatMessage
|
|
| IAmCameraMessage
|
|
| IAmDispatcherMessage;
|
|
|
|
async function readU16(reader: BufReader): Promise<U16> {
|
|
const buf = await reader.readFull(new Uint8Array(2));
|
|
if (!buf) throw `unable to fill buffer of length 2 with U32 bytes`;
|
|
return ByteSet.from(buf, "big").read.uint16();
|
|
}
|
|
|
|
async function readU32(reader: BufReader): Promise<U32> {
|
|
const buf = await reader.readFull(new Uint8Array(4));
|
|
if (!buf) throw `unable to fill buffer of length 4 with U32 bytes`;
|
|
return ByteSet.from(buf, "big").read.uint32();
|
|
}
|
|
|
|
async function readString(reader: BufReader): Promise<string> {
|
|
const len = await reader.readByte();
|
|
if (!len) throw "unable to read string length byte";
|
|
const buf = await reader.readFull(new Uint8Array(len));
|
|
if (!buf) throw `unable to fill buffer of length ${len} with string bytes`;
|
|
return tDec.decode(buf);
|
|
}
|
|
|
|
async function readArray<T>(
|
|
reader: BufReader,
|
|
readCallback: (r: BufReader) => Promise<T>,
|
|
): Promise<T[]> {
|
|
const len = await reader.readByte();
|
|
if (!len) throw "unable to read array length byte";
|
|
const result = [];
|
|
while (result.length < len) {
|
|
result.push(await readCallback(reader));
|
|
}
|
|
if (result.length != len) {
|
|
throw new Error(
|
|
`unable to read array of length ${len} (got ${result.length})`,
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function seqRead<T extends Message>(
|
|
reader: BufReader,
|
|
seq: ((r: BufReader) => Promise<T[keyof T]>)[],
|
|
): Promise<T> {
|
|
const result = [];
|
|
for (const f of seq) {
|
|
result.push(await f(reader));
|
|
}
|
|
return result as T; // TODO: cheating
|
|
}
|
|
|
|
async function readError(reader: BufReader): Promise<ErrorMessage> {
|
|
return await seqRead(reader, [readString]);
|
|
}
|
|
|
|
async function readPlate(reader: BufReader): Promise<PlateMessage> {
|
|
return await seqRead(reader, [readString, readU32]);
|
|
}
|
|
|
|
async function readTicket(reader: BufReader): Promise<TicketMessage> {
|
|
return await seqRead(reader, [
|
|
readString,
|
|
readU16,
|
|
readU16,
|
|
readU32,
|
|
readU16,
|
|
readU32,
|
|
readU16,
|
|
]);
|
|
}
|
|
|
|
async function readWantHeartbeat(
|
|
reader: BufReader,
|
|
): Promise<WantHeartbeatMessage> {
|
|
return await seqRead(reader, [readU32]);
|
|
}
|
|
|
|
async function readIAmCamera(reader: BufReader): Promise<IAmCameraMessage> {
|
|
return await seqRead(reader, [readU16, readU16, readU16]);
|
|
}
|
|
|
|
async function readMessage(
|
|
reader: BufReader,
|
|
): Promise<{ type: MessageType; message: Message }> {
|
|
const type = await reader.readByte();
|
|
if (!type) throw new Error("Failed to read message type byte");
|
|
let message: Message | null = null;
|
|
switch (type) {
|
|
case MessageType.ERROR:
|
|
message = await readError(reader);
|
|
break;
|
|
case MessageType.PLATE:
|
|
message = await readPlate(reader);
|
|
break;
|
|
case MessageType.TICKET:
|
|
message = await readTicket(reader);
|
|
break;
|
|
case MessageType.WANT_HEARTBEAT:
|
|
message = await readWantHeartbeat(reader);
|
|
break;
|
|
case MessageType.HEARTBEAT:
|
|
message = [];
|
|
break;
|
|
case MessageType.I_AM_CAMERA:
|
|
message = await readIAmCamera(reader);
|
|
break;
|
|
case MessageType.I_AM_DISPATCHER:
|
|
message = [await readArray(reader, readU16)];
|
|
break;
|
|
default:
|
|
throw new Error(`failed to read message of type 0x${type.toString(16)}`);
|
|
}
|
|
return { type, message };
|
|
}
|
|
|
|
function dayOfTime(ts: Timestamp): number {
|
|
return Math.floor(ts / 86400);
|
|
}
|
|
|
|
const intervals = new Map<Deno.Conn, number>();
|
|
const cameras = new Map<Deno.Conn, Camera>();
|
|
const dispatchers = new Map<Deno.Conn, Road[]>();
|
|
const observations = new Map<Plate, Map<Road, Map<Location, Timestamp>>>();
|
|
const unsentTickets: TicketMessage[] = [];
|
|
const dispatchedTicketDays = new Map<Plate, Set<number>>();
|
|
|
|
function makeObservation(
|
|
plate: Plate,
|
|
road: Road,
|
|
location: Location,
|
|
when: Timestamp,
|
|
speedLimit: SpeedLimit,
|
|
) {
|
|
let forPlate = observations.get(plate);
|
|
if (!forPlate) {
|
|
const newForPlate = new Map<Road, Map<Location, Timestamp>>();
|
|
observations.set(plate, newForPlate);
|
|
forPlate = newForPlate;
|
|
}
|
|
let forRoad = forPlate.get(road);
|
|
if (!forRoad) {
|
|
const newForRoad = new Map<Location, Timestamp>();
|
|
forPlate.set(road, newForRoad);
|
|
forRoad = newForRoad;
|
|
}
|
|
|
|
for (const [loc, ts] of forRoad.entries()) {
|
|
const distMiles = Math.abs(location - loc);
|
|
const deltaSecs = Math.abs(ts - when);
|
|
const speed = distMiles / (deltaSecs / 60 / 60);
|
|
console.log("Diff:", {
|
|
plate,
|
|
road,
|
|
loc,
|
|
ts,
|
|
location,
|
|
when,
|
|
distMiles,
|
|
deltaSecs,
|
|
speed,
|
|
speedLimit,
|
|
day: dayOfTime(when),
|
|
});
|
|
if (speed > speedLimit + 0.5) {
|
|
let [m1, t1] = [loc, ts];
|
|
let [m2, t2] = [location, when];
|
|
if (when < ts) {
|
|
[m2, t2] = [loc, ts];
|
|
[m1, t1] = [location, when];
|
|
}
|
|
|
|
// for now, we will send one ticket per observation - should work, given the constraints
|
|
dispatchTicket([
|
|
plate,
|
|
road,
|
|
m1,
|
|
t1,
|
|
m2,
|
|
t2,
|
|
Math.floor(speed * 100),
|
|
]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
console.log("Observation:", { plate, road, location, when });
|
|
|
|
forRoad.set(location, when);
|
|
}
|
|
|
|
function handleMessage(
|
|
conn: Deno.Conn,
|
|
type: MessageType,
|
|
message: Message,
|
|
) {
|
|
console.debug("Message:", {
|
|
from: conn.remoteAddr,
|
|
type: MessageType[type.toString()],
|
|
message,
|
|
});
|
|
switch (type) {
|
|
case MessageType.WANT_HEARTBEAT: {
|
|
if (intervals.has(conn)) {
|
|
return sendErrorAndDisconnect(conn, "heartbeat already specified");
|
|
}
|
|
const [interval] = (message as WantHeartbeatMessage);
|
|
setIntervalFor(conn, interval);
|
|
break;
|
|
}
|
|
case MessageType.PLATE: {
|
|
const cam = cameras.get(conn);
|
|
if (!cam) return sendErrorAndDisconnect(conn, "not a camera");
|
|
const [plate, when] = (message as PlateMessage);
|
|
makeObservation(plate, cam.road, cam.location, when, cam.speedLimit);
|
|
break;
|
|
}
|
|
case MessageType.I_AM_CAMERA: {
|
|
if (cameras.has(conn) || dispatchers.has(conn)) {
|
|
sendErrorAndDisconnect(conn, "already identified");
|
|
}
|
|
const [road, location, speedLimit] = (message as IAmCameraMessage);
|
|
const camera: Camera = {
|
|
road,
|
|
location,
|
|
speedLimit,
|
|
};
|
|
cameras.set(conn, camera);
|
|
break;
|
|
}
|
|
case MessageType.I_AM_DISPATCHER: {
|
|
if (cameras.has(conn) || dispatchers.has(conn)) {
|
|
sendErrorAndDisconnect(conn, "already identified");
|
|
}
|
|
const [roads] = (message as IAmDispatcherMessage);
|
|
dispatchers.set(conn, roads);
|
|
for (let i = unsentTickets.length - 1; i >= 0; i--) {
|
|
const t = unsentTickets[i];
|
|
const [_, road] = t;
|
|
if (roads.includes(road)) {
|
|
unsentTickets.splice(i, 1);
|
|
sendTicket(conn, t);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
console.warn("Unable to handle unknown message:", {
|
|
from: conn.remoteAddr,
|
|
type: MessageType[type.toString()],
|
|
message,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
function markPlateDay(plate: Plate, day: number) {
|
|
let forTicketDay = dispatchedTicketDays.get(plate);
|
|
if (!forTicketDay) {
|
|
const newForTicketDay = new Set<number>();
|
|
dispatchedTicketDays.set(plate, newForTicketDay);
|
|
forTicketDay = newForTicketDay;
|
|
}
|
|
forTicketDay.add(day);
|
|
}
|
|
|
|
function hasPlateDay(plate: Plate, day: number): boolean {
|
|
return !!(dispatchedTicketDays.get(plate)?.has(day));
|
|
}
|
|
|
|
function dispatchTicket(t: TicketMessage) {
|
|
let [plate, road, mile1, ts1, mile2, ts2, speed] = t;
|
|
let day1 = dayOfTime(ts1);
|
|
const day2 = dayOfTime(ts2);
|
|
|
|
while (day1 < day2) {
|
|
const nts1 = Math.max(ts1, day1 * 86400);
|
|
const nts2 = Math.min(((day1 + 1) * 86400) - 1, ts2);
|
|
console.log("Multi-day ticket:", day1, day2, nts1, nts2);
|
|
day1++;
|
|
ts1 = nts2 + 1;
|
|
dispatchTicket([plate, road, mile1, nts1, mile2, nts2, speed]);
|
|
}
|
|
|
|
if (hasPlateDay(plate, day1)) {
|
|
return;
|
|
}
|
|
markPlateDay(plate, day1);
|
|
|
|
let sent = false;
|
|
for (const [conn, roads] of dispatchers.entries()) {
|
|
if (roads.includes(road)) {
|
|
sent = true;
|
|
sendTicket(conn, t);
|
|
break;
|
|
}
|
|
}
|
|
if (!sent) unsentTickets.push(t);
|
|
}
|
|
|
|
async function sendTicket(conn: Deno.Conn, t: TicketMessage) {
|
|
const [plate, road, m1, t1, m2, t2, speed] = t;
|
|
console.log("Sending ticket:", t);
|
|
const plateBytes = tEnc.encode(plate);
|
|
|
|
const p = new ByteSet(
|
|
1 + 1 + plateBytes.length + 2 + 2 + 4 + 2 + 4 + 2,
|
|
"big",
|
|
);
|
|
p.write.uint8(MessageType.TICKET);
|
|
// p.write.uint8(plateBytes.length);
|
|
p.write.uint8Array(plateBytes, LengthType.Uint8);
|
|
p.write.uint16(road);
|
|
p.write.uint16(m1);
|
|
p.write.uint32(t1);
|
|
p.write.uint16(m2);
|
|
p.write.uint32(t2);
|
|
p.write.uint16(speed);
|
|
await conn.write(p.buffer);
|
|
}
|
|
|
|
async function serve(conn: Deno.Conn) {
|
|
const reader = new BufReader(conn);
|
|
try {
|
|
while (true) {
|
|
const { type: type, message } = await readMessage(reader);
|
|
handleMessage(conn, type, message);
|
|
}
|
|
} finally {
|
|
clearIntervalFor(conn);
|
|
}
|
|
}
|
|
|
|
function disconnect(conn: Deno.Conn) {
|
|
console.log("Disconnecting:", conn.remoteAddr);
|
|
try {
|
|
conn.close();
|
|
} catch (_) {
|
|
console.debug("Failed to close connection (probably already closed)");
|
|
}
|
|
clearIntervalFor(conn);
|
|
cameras.delete(conn);
|
|
dispatchers.delete(conn);
|
|
}
|
|
|
|
async function sendErrorAndDisconnect(conn: Deno.Conn, msg: string) {
|
|
console.warn("Sending error:", msg);
|
|
await conn.write(new Uint8Array([MessageType.ERROR]));
|
|
await conn.write(tEnc.encode(msg));
|
|
disconnect(conn);
|
|
}
|
|
|
|
function setIntervalFor(conn: Deno.Conn, intervalDeciseconds: number) {
|
|
if (intervalDeciseconds <= 0) return;
|
|
|
|
intervals.set(
|
|
conn,
|
|
setInterval(() => {
|
|
try {
|
|
conn.write(new Uint8Array([MessageType.HEARTBEAT]));
|
|
} catch {
|
|
clearIntervalFor(conn);
|
|
}
|
|
}, intervalDeciseconds * 100 /* deciseconds to ms */),
|
|
);
|
|
}
|
|
|
|
function clearIntervalFor(conn: Deno.Conn) {
|
|
if (intervals.has(conn)) {
|
|
clearInterval(intervals.get(conn));
|
|
intervals.delete(conn);
|
|
}
|
|
}
|
|
|
|
for await (const conn of Deno.listen({ port: PORT })) {
|
|
console.log("Connection established:", conn.remoteAddr);
|
|
session(conn);
|
|
}
|