diff --git a/hw_sim/.gitignore b/hw_sim/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/hw_sim/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/hw_sim/README.md b/hw_sim/README.md new file mode 100644 index 0000000..bba103a --- /dev/null +++ b/hw_sim/README.md @@ -0,0 +1,34 @@ +# Hardware simulation script + +## Requirements +Any versions of the below should work, but when in doubt, take the latest one + +- `npm` +- `node` + +## Setup + +`npm install` + +## Run + +- First make sure an instance of telraam is running (see below how to specify the telraam address and port) + +- Then run: `node main.js` + +## Arguments + +- `-h`: Show all the help messages. +- `-p`: What port to connect to (default: `4564`) +- `-a`: What address to connect to (default: `127.0.0.1`). +- `-r`: The amount of runners to spawn. +- `-b`: The amount of beacons to spawn. +- `-m`: The average time per round +- `-d`: Standard deviation for the average runner speed. +- `-D`: Standard deviation of round speed, global for all runners. +- `--miss-rate`: Missrate of runner detection. + +### Todo: added bonusses + +- Malformed message rate. We need closing and starting tags to properly detect this. So server code and code here. Look at the class `telraam.beacon.BeaconMessage`. +- Messages per beacon or something, because irl the beacons are probably gonna send more than 1 message for a runner passing by. diff --git a/hw_sim/main.js b/hw_sim/main.js new file mode 100644 index 0000000..203c6ce --- /dev/null +++ b/hw_sim/main.js @@ -0,0 +1,116 @@ +'use strict'; + +const ArgumentParser = require('argparse').ArgumentParser; +const net = require('net'); + +var args; + +// Standard Normal variate using Box-Muller transform. +function randn_bm(mean, dev) { + var u = 0, + v = 0; + while (u === 0) u = Math.random(); //Converting [0,1) to (0,1) + while (v === 0) v = Math.random(); + const out = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); + return out * dev + mean; +} + +class Beacon { + constructor(id) { + this.socket = net.Socket(); + this.socket.connect(args.port, args.address); + this.id = id; + } + + send(id) { + console.log("Beacon", id, "Runner", this.id); + + // This is so ugly + const start_tag = [60, 60, 60, 60]; + const end_tag = [62, 62, 62, 62]; + const actual_message_size = 10; + + const buffer = Buffer.alloc(actual_message_size + start_tag.length + end_tag.length); + + let offset = 0; + for (let tag of start_tag) { + buffer.writeInt8(tag, offset); + offset += 1; // '<' + } + + buffer.writeInt8(this.id, offset); + offset += 1; + buffer.writeInt8(id, offset); + offset += 1; + + buffer.writeBigInt64LE(BigInt(Date.now()), offset); + offset += 8; + + for (let tag of end_tag) { + buffer.writeInt8(tag, offset); + offset += 1; // '<' + } + this.socket.write(buffer); + } +} + +class Runner { + constructor(id, mean, beacons) { + this.mean = mean; + this.dev = args.round_deviation; + this.beacons = beacons; + this.at = 0; + this.id = id; + + this.run = this.send.bind(this); + + this.set_next(); + } + + send() { + if (Math.random() >= args.miss_rate) { + this.beacons[this.at].send(this.id); + } + + this.at++; + + if (this.at >= this.beacons.length) { + this.at = 0; + } + + this.set_next(); + } + + set_next() { + const time_till_next = randn_bm(this.mean, this.dev); + setTimeout(this.run, time_till_next); + } +} + +function main() { + const parser = new ArgumentParser({ + addHelp: true, + description: 'Hardware simulation script for Telraam' + }); + + parser.addArgument(['-p', "--port"], { defaultValue: 4564, type: "int", help: "Port to use" }); + parser.addArgument(['-a', "--address"], { defaultValue: "127.0.0.1", help: "Ip address to test" }); + parser.addArgument(['-b', "--beacons"], { defaultValue: 2, type: "int", help: "Amount of beacons" }); + parser.addArgument(['-r', "--runners"], { defaultValue: 5, type: "int", help: "Amount of runners" }); + parser.addArgument(['-m', "--mean"], { defaultValue: 500, type: "int", help: "Mean of runner speed (ms per round)" }); + parser.addArgument(['-d', "--runner-deviation"], { defaultValue: 10, type: "int", help: "Standard deviation of runner speed (per runner)" }); + parser.addArgument(['-D', "--round-deviation"], { defaultValue: 0, type: "int", help: "Standard deviation of runner speed (per round)" }); + parser.addArgument(['--miss-rate'], { defaultValue: 0, type: "float", help: "Missrate of runner detection." }); + args = parser.parseArgs(); + + const beacons = []; + for (let i = 0; i < args.beacons; i++) { + beacons.push(new Beacon(i + 1)); + } + + const runners = []; + for (let i = 0; i < args.runners; i++) { + runners.push(new Runner(i + 1, randn_bm(args.mean, args.runner_deviation), beacons)); + } +} +main(); \ No newline at end of file diff --git a/hw_sim/package-lock.json b/hw_sim/package-lock.json new file mode 100644 index 0000000..472547d --- /dev/null +++ b/hw_sim/package-lock.json @@ -0,0 +1,23 @@ +{ + "name": "hw_sim", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + } + } +} diff --git a/hw_sim/package.json b/hw_sim/package.json new file mode 100644 index 0000000..5f5221a --- /dev/null +++ b/hw_sim/package.json @@ -0,0 +1,15 @@ +{ + "name": "hw_sim", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": {}, + "devDependencies": { + "argparse": "^1.0.10" + } +} diff --git a/src/main/java/telraam/App.java b/src/main/java/telraam/App.java index 86f03d2..821c243 100644 --- a/src/main/java/telraam/App.java +++ b/src/main/java/telraam/App.java @@ -5,25 +5,33 @@ import io.dropwizard.jdbi3.bundles.JdbiExceptionsBundle; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; import org.jdbi.v3.core.Jdbi; import telraam.api.BatonResource; import telraam.api.HelloworldResource; +import telraam.beacon.BeaconAggregator; import telraam.database.daos.BatonDAO; import telraam.database.models.Baton; import telraam.database.models.Id; import telraam.healthchecks.TemplateHealthCheck; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; + public class App extends Application { private static Logger logger = Logger.getLogger(App.class.getName()); public static void main(String[] args) throws Exception { - new App().run(args); + BeaconAggregator ba = new BeaconAggregator(4564); + ba.onError((e) -> {logger.warning(e.getMessage()); return null;}); + ba.onData((e) -> {logger.info(e.toString()); return null;}); + ba.onConnect((_e) -> {logger.info("Connect"); return null;}); + ba.onDisconnect((_e) -> {logger.info("Disconnected"); return null;}); + // new App().run(args); + ba.run(); } @Override diff --git a/src/main/java/telraam/beacon/Beacon.java b/src/main/java/telraam/beacon/Beacon.java index faac2bc..0b0efcd 100644 --- a/src/main/java/telraam/beacon/Beacon.java +++ b/src/main/java/telraam/beacon/Beacon.java @@ -2,21 +2,27 @@ import java.io.IOException; import java.io.InputStream; +import java.io.BufferedInputStream; import java.io.EOFException; import java.net.Socket; +import java.nio.Buffer; +import java.util.List; +import java.util.ArrayList; /** -* Beacon is socket wrapper that listens to the sockets -* and emits BeaconMessages when enough bytes are read. -* -* Beacons are closed at the first Exception encountered. -* This could be changed if need be. -* -* @author Arthur Vercruysse -*/ + * Beacon is socket wrapper that listens to the sockets and emits BeaconMessages + * when enough bytes are read. + * + * Beacons are closed at the first Exception encountered. This could be changed + * if need be. + * + * @author Arthur Vercruysse + */ public class Beacon extends EventGenerator implements Runnable { private Socket s; private int messageSize = BeaconMessage.MESSAGESIZE; + private byte[] startTag = BeaconMessage.STARTTAG; + private byte[] endTag = BeaconMessage.ENDTAG; public Beacon(Socket socket, Callback> h) { super(h); @@ -29,12 +35,17 @@ public Beacon(Socket socket, Callback> h) { public void run() { this.connect(); - byte[] buf = new byte[messageSize]; - int at = 0; - InputStream is; + boolean readingMsg = false; + + List msgBuf = new ArrayList<>(messageSize); + int sTagIndex = 0; + int eTagIndex = 0; + byte[] buf = new byte[1024]; + + BufferedInputStream is; try { - is = s.getInputStream(); + is = new BufferedInputStream(s.getInputStream()); } catch (IOException e) { error(e); return; @@ -42,12 +53,64 @@ public void run() { try { while (true) { - int c = is.read(buf, at, messageSize - at); - if (c < 0) throw new EOFException(); - at += c; - if (at == messageSize) { - this.data(new BeaconMessage(buf)); - at = 0; + int c = is.read(buf); + if (c < 0) + throw new EOFException(); + + for (int i = 0; i < c; i++) { + byte b = buf[i]; + msgBuf.add(b); + + if (b == startTag[sTagIndex]) { + sTagIndex++; + + // A complete start tag is found + // Delete current msgBuf content and start over + if (sTagIndex == startTag.length) { + sTagIndex = 0; + + if (readingMsg) { + // TODO: Maybe we want to reset msgBuf idk + this.error(new BeaconException.MsgStartWithNoEnd()); + } else { + msgBuf.clear(); + readingMsg = true; + } + } + } else { + sTagIndex = 0; + } + + if (b == endTag[eTagIndex]) { + eTagIndex++; + + // A complete end tag is found + // Flush the msgBuffer + if (eTagIndex == endTag.length) { + eTagIndex = 0; + + if (readingMsg) { + + // Remove end tag from message + for (int k = 0; k < endTag.length; k++) { + msgBuf.remove(msgBuf.size() - 1); + } + + // Catch errors thrown at message decoding and propagate + try { + this.data(new BeaconMessage(msgBuf)); + } catch (Exception e) { + this.error(e); + } + + readingMsg = false; + } else { + this.error(new BeaconException.MsgEndWithNoStart()); + } + } + } else { + eTagIndex = 0; + } } } } catch (IOException e) { diff --git a/src/main/java/telraam/beacon/BeaconException.java b/src/main/java/telraam/beacon/BeaconException.java new file mode 100644 index 0000000..fc43769 --- /dev/null +++ b/src/main/java/telraam/beacon/BeaconException.java @@ -0,0 +1,19 @@ +package telraam.beacon; + +public class BeaconException extends Exception { + protected BeaconException(String reason) { + super(reason); + } + + public static class MsgEndWithNoStart extends BeaconException { + public MsgEndWithNoStart() { + super("Message end tag detected without a start tag"); + } + } + + public static class MsgStartWithNoEnd extends BeaconException { + public MsgStartWithNoEnd() { + super("2 message start tags detected."); + } + } +} diff --git a/src/main/java/telraam/beacon/BeaconMessage.java b/src/main/java/telraam/beacon/BeaconMessage.java index 558edc2..af2389f 100644 --- a/src/main/java/telraam/beacon/BeaconMessage.java +++ b/src/main/java/telraam/beacon/BeaconMessage.java @@ -1,18 +1,34 @@ package telraam.beacon; +import java.util.List; + /** -* BeaconMessage is the representation of what is received from a beacon. -* This should parse the incoming byte[]. -* -* @author Arthur Vercruysse -*/ + * BeaconMessage is the representation of what is received from a beacon. This + * should parse the incoming byte.get(). + * + * @author Arthur Vercruysse + */ public class BeaconMessage { - public static final int MESSAGESIZE = 10; + public static final int MESSAGESIZE = Byte.BYTES + Byte.BYTES + Long.BYTES; + + public static final byte[] STARTTAG = { '<', '<', '<', '<' }; + public static final byte[] ENDTAG = { '>', '>', '>', '>' }; - public byte[] data; + public byte beaconTag; + public byte batonTag; + public long timestamp; // DO NOT STORE THIS DATA, IT WILL BE OVERWRITTEN - public BeaconMessage(byte[] data) { - this.data = data; + public BeaconMessage(List data) { + beaconTag = data.get(0); + batonTag = data.get(1); + timestamp = ((long) data.get(9) << 56) | ((long) data.get(8) & 0xff) << 48 | ((long) data.get(7) & 0xff) << 40 + | ((long) data.get(6) & 0xff) << 32 | ((long) data.get(5) & 0xff) << 24 + | ((long) data.get(4) & 0xff) << 16 | ((long) data.get(3) & 0xff) << 8 | ((long) data.get(2) & 0xff); + } + + @Override + public String toString() { + return String.format("Beacon %o: runner: %o at %d", this.beaconTag, this.batonTag, this.timestamp); } } diff --git a/src/test/java/telraam/beacon/BeaconTest.java b/src/test/java/telraam/beacon/BeaconTest.java index 18dfcc7..6a9ab2a 100644 --- a/src/test/java/telraam/beacon/BeaconTest.java +++ b/src/test/java/telraam/beacon/BeaconTest.java @@ -20,12 +20,12 @@ import java.util.logging.Level; /** -* Beacon integration test. -* Spoofing ServerSocket and Socket so you can write to it at will. -* TODO: Test socket exception, but I don't really know what could fail. -* -* @author Arthur Vercruysse -*/ + * Beacon integration test. Spoofing ServerSocket and Socket so you can write to + * it at will. TODO: Test socket exception, but I don't really know what could + * fail. + * + * @author Arthur Vercruysse + */ public class BeaconTest { private static Logger logger = Logger.getLogger(BeaconTest.class.getName()); @@ -133,6 +133,7 @@ public void testEverythingBeacon() throws Exception { ba.onError((e) -> { logger.log(Level.SEVERE, "error", e); errors.incrementAndGet(); + barrier.release(); return null; }); @@ -157,8 +158,8 @@ public void testEverythingBeacon() throws Exception { // Check if no beacon messages are sent with incomplete data // Aka do they buffer correctly? - for (OurSocket s: connectedSockets) { - s.write("hadeksfd".getBytes(), false); + for (OurSocket s : connectedSockets) { + s.write("<<<>>>".getBytes(), true); } barrier.acquire(8); @@ -178,8 +179,31 @@ public void testEverythingBeacon() throws Exception { assertEquals(data.get(), connectedSockets.size()); assertEquals(errors.get(), 0); + // Test invalid msg send + + // Invalid message + connectedSockets.get(0).write("<<<>>>".getBytes(), true); + + barrier.acquire(8); + barrier.release(8); + assertEquals(errors.get(), 1); + + // No opening tag + connectedSockets.get(0).write("<<>>>".getBytes(), true); + + barrier.acquire(8); + barrier.release(8); + assertEquals(errors.get(), 2); + + // 2 Opening tags + connectedSockets.get(0).write("<<<