diff --git a/BI-PSI/README.adoc b/BI-PSI/README.adoc new file mode 100644 index 0000000..e73c6ab --- /dev/null +++ b/BI-PSI/README.adoc @@ -0,0 +1,11 @@ += Computer Networks + +Got 10 bonus points from semester for activity. + +Found critical bug in homework submission server Bouda. + +Got 90 % from exam, resulting in grade A. + +== Semestral task + +In Computer Networks, in order to get assessment, we had to develop a simple program for simulating communication with Robots. + +I chose to develop it in Kotlin, and you can link:semestral/[see the semestral task here]. diff --git a/BI-PSI/grades.pdf b/BI-PSI/grades.pdf new file mode 100644 index 0000000..5cfa7db Binary files /dev/null and b/BI-PSI/grades.pdf differ diff --git a/BI-PSI/semestral/pom.xml b/BI-PSI/semestral/pom.xml new file mode 100644 index 0000000..24363a6 --- /dev/null +++ b/BI-PSI/semestral/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + sem + cz.fit.cvut.wrzecond + 1.0.0 + jar + + sem + + + UTF-8 + official + 1.8 + + + + + mavenCentral + https://repo1.maven.org/maven2/ + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + 1.4.31 + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + maven-assembly-plugin + + + package + + single + + + + + + + true + MainKt + + + + jar-with-dependencies + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + 1.4.31 + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + 1.4.3 + + + + \ No newline at end of file diff --git a/BI-PSI/semestral/src/main/kotlin/main.kt b/BI-PSI/semestral/src/main/kotlin/main.kt new file mode 100644 index 0000000..4c49104 --- /dev/null +++ b/BI-PSI/semestral/src/main/kotlin/main.kt @@ -0,0 +1,412 @@ +import java.io.* +import java.net.* +import kotlin.math.max +import kotlinx.coroutines.* + +// === CONSTANTS === +const val END = "${7.toChar()}${8.toChar()}" +const val SERVER_PORT = 19132 +const val MESSAGE_TIMEOUT = 1000 + +const val RECHARGING_MESSAGE = "RECHARGING" +const val FULL_POWER_MESSAGE = "FULL POWER" +const val RECHARGING_TIMEOUT = 5000 + +// Allowed server-client keypair +val KEYS = arrayOf ( + Pair(23019, 32037), + Pair(32037, 29295), + Pair(18789, 13603), + Pair(16443, 29533), + Pair(18189, 21952), +) + +// Obstacle move sequence +val OBSTACLE_MOVES_LEFT = arrayOf( + ServerResponse.TURN_LEFT, + ServerResponse.MOVE, + ServerResponse.TURN_RIGHT, + ServerResponse.MOVE, +) +val OBSTACLE_MOVES_RIGHT = arrayOf( + ServerResponse.TURN_RIGHT, + ServerResponse.MOVE, + ServerResponse.TURN_LEFT, + ServerResponse.MOVE, +) + +/** + * Extension function allowing to hash string + * @param key the server/client key used for hashing + */ +fun String.hash (key: Int) : Int { + val sum = ( this.toCharArray().sumBy { it.toInt() } * 1000 ) % 65536 + return ( sum + key ) % 65536 +} + +/** + * Helper enum class for robot phases + * each phase includes maximum allowed message length + */ +enum class RobotPhase (val maxLength: Int) { + // Login + WAITING_FOR_NAME (20), + WAITING_FOR_KEY_ID (5), + WAITING_FOR_CONFIRMATION (7), + // Moving + WAITING_FOR_MOVE_RESPONSE (12), + WAITING_FOR_SECRET (100), + // Recharging + RECHARGING (12) +} + +/** Helper enum class for server response */ +enum class ServerResponse (private val text: String) { + // Move + MOVE ("102 MOVE"), + TURN_LEFT ("103 TURN LEFT"), + TURN_RIGHT ("104 TURN RIGHT"), + // General + PICK_UP ("105 GET MESSAGE"), + LOGOUT ("106 LOGOUT"), + KEY_REQUEST ("107 KEY REQUEST"), + SERVER_OK ("200 OK"), + // Error + LOGIN_FAILED ("300 LOGIN FAILED"), + SYNTAX_ERROR ("301 SYNTAX ERROR"), + LOGIC_ERROR ("302 LOGIC ERROR"), + KEY_OUT_OF_RANGE_ERROR ("303 KEY OUT OF RANGE"); + + override fun toString() = text +} + +/** Helper enum class to hold direction information */ +enum class Direction { + NORTH, EAST, SOUTH, WEST; + fun turnLeft () = when(this) { + NORTH -> WEST + WEST -> SOUTH + SOUTH -> EAST + EAST -> NORTH + } +} + +/** Helper data class for robot position */ +data class Position (val x: Int, val y: Int, val dir: Direction? = null) { + fun obstacleMoves () : Array { + val rightMoves = (x > 0 && y > 0 && dir == Direction.SOUTH) || + (x > 0 && y < 0 && dir == Direction.WEST ) || + (x < 0 && y < 0 && dir == Direction.NORTH) || + (x < 0 && y > 0 && dir == Direction.EAST) + return if (rightMoves) OBSTACLE_MOVES_RIGHT else OBSTACLE_MOVES_LEFT + } + fun detectPosition (diff: Position) : Position? { + val direction = when (diff) { + Position(1, 0) -> Direction.EAST + Position(-1, 0) -> Direction.WEST + Position(0, 1) -> Direction.NORTH + Position(0, -1) -> Direction.SOUTH + else -> return null + } + return Position(x, y, direction) + } + fun bestMove () : ServerResponse { + val shouldMove = when (dir) { + Direction.NORTH -> y < 0 + Direction.SOUTH -> y > 0 + Direction.WEST -> x > 0 + Direction.EAST -> x < 0 + else -> false + } + return if (shouldMove) ServerResponse.MOVE else ServerResponse.TURN_LEFT + } + fun turnLeft () = Position(x, y, dir?.turnLeft()) + operator fun minus (other: Position) = Position(x - other.x, y - other.y, dir) +} + +/** Helper message reader class */ +class Reader (private val reader: BufferedReader) { + + /** + * Tries to read message from socket + * valid: recharging / expected length + correct ending + */ + private fun readBuffer (maxLength: Int) : String? { + val builder = StringBuilder() + while ( !builder.endsWith(END) && builder.length < max(maxLength, RECHARGING_MESSAGE.length + 2) ) + builder.append(reader.read().toChar()) + + val valid = ( builder.length <= maxLength || "$RECHARGING_MESSAGE$END" == builder.toString() ) + return if ( builder.endsWith(END) && valid ) builder.substring(0, builder.length - 2) else null + } + + /** + * Reads message for robot + * returns null and closes connection if read failed + */ + fun readMessage (robot: Robot) : String? { + val message : String? + try { message = readBuffer(robot.phase.maxLength) } + catch (timeout: SocketTimeoutException) { + println("[SERVER] Error: Timeout, closing connection...") + robot.socket.close() + return null + } + if (message == null) { + println("[SERVER] Syntax error, closing connection...") + robot.endConnection(ServerResponse.SYNTAX_ERROR) + } + return message + } + +} + +/** Robot move logic helper class */ +class MoveLogic { + + // Current and last positions + var position: Position? = null + private var previous: Position? = null + + // Obstacle solving data + private var initialObstacle = false + private var obstacleSolving: Int? = null + private var obstacleMoves: Array = OBSTACLE_MOVES_LEFT + + /** + * Detects direction based on first two requests + * schedules one more request if there is an obstacle + */ + private fun detectDirection () : ServerResponse? { + val position = position ?: return null + val previous = previous ?: return null + val diff = position - previous + + // obstacle at the beginning + if (diff.x == 0 && diff.y == 0) { + initialObstacle = !initialObstacle + this.previous = this.position + return if (initialObstacle) ServerResponse.TURN_LEFT else ServerResponse.MOVE + } + + // update position based on diff + this.position = position.detectPosition(diff) + return null + } + + /** + * Handles moving when there is/was an obstacle + * (continue in avoiding obstacle) + */ + private fun avoidObstacle () : ServerResponse? { + val position = position ?: return null + val osn: Int + + if (obstacleSolving == null) { + obstacleMoves = position.obstacleMoves() // start avoiding + osn = 0 + } + else osn = (obstacleSolving ?: 0) + 1 // continue avoiding + obstacleSolving = if (osn == obstacleMoves.size - 1) null else osn + return obstacleMoves[osn] + } + + /** + * Finds the best move and returns it + * 1. handles initial position and direction detecting + * 2. handles classic moving + * 3. detects obstacles and avoids it + */ + fun execute () : ServerResponse? { + // Position not set yet, move once again + if (previous == null) { + previous = position + return ServerResponse.MOVE + } + + // Direction is not known = detect it + if (position?.dir == null) { + val response = detectDirection() + if (response != null) return response + } + + // We know the direction and position, we can act + val position = position ?: return null + if ( obstacleSolving != null || previous == position ) + return avoidObstacle() + + // Find the best move and execute it + val move = position.bestMove() + this.position = if (move == ServerResponse.TURN_LEFT) position.turnLeft() else position + this.previous = position + return move + } + +} + +/** Robot controller class */ +class Robot (val socket: Socket) { + + // Socket utilities + private val reader: Reader + private val writer: BufferedWriter + + // Current phase information + var phase = RobotPhase.WAITING_FOR_NAME + private lateinit var backupPhase: RobotPhase + + // Current robot information + private lateinit var name: String + private lateinit var key: Pair + private val moveLogic = MoveLogic() + + init { + println("[SERVER] Client login") + reader = Reader(BufferedReader(InputStreamReader(socket.getInputStream()))) + writer = BufferedWriter(OutputStreamWriter(socket.getOutputStream())) + waitForResponse() + } + + // Output tools + private fun sendResponse (response: Any) { + println("[SERVER] Sending $response") + writer.write("$response$END") + writer.flush() + } + fun endConnection (response: ServerResponse) { + println("[SERVER] Ending connection (reason $response)") + sendResponse(response) + socket.close() + } + + /** Set recharging phase and backup current phase */ + private fun startRecharging () { + backupPhase = phase + phase = RobotPhase.RECHARGING + waitForResponse() + } + + /** Remove recharging phase and return backup phase */ + private fun stopRecharging (message: String) { + if (message != FULL_POWER_MESSAGE) { + endConnection(ServerResponse.LOGIC_ERROR) + return + } + + phase = backupPhase + waitForResponse() + } + + /** Initial login setup (get robot's name) */ + private fun waitingForName (message: String) { + name = message + phase = RobotPhase.WAITING_FOR_KEY_ID + sendResponse(ServerResponse.KEY_REQUEST) + waitForResponse() + } + + /** Initial key setup (get and validate robot key) */ + private fun waitingForKeyID (message: String) { + val keyId = message.toIntOrNull() + if (keyId == null || keyId !in KEYS.indices) { + endConnection( if (keyId == null) ServerResponse.SYNTAX_ERROR else ServerResponse.KEY_OUT_OF_RANGE_ERROR ) + return + } + + key = KEYS[keyId] + phase = RobotPhase.WAITING_FOR_CONFIRMATION + sendResponse(name.hash(key.first).toString()) + waitForResponse() + } + + /** Finish key setup (get and validate key confirmation) */ + private fun waitingConfirmation (message: String) { + val keyConfirmation = message.toIntOrNull() + if ( keyConfirmation != name.hash(key.second) ) { + endConnection( if (keyConfirmation == null) ServerResponse.SYNTAX_ERROR else ServerResponse.LOGIN_FAILED ) + return + } + + sendResponse(ServerResponse.SERVER_OK) + sendResponse(ServerResponse.MOVE) + phase = RobotPhase.WAITING_FOR_MOVE_RESPONSE + waitForResponse() + } + + /** Handle robot moving */ + private fun waitingMoveResponse (message: String) { + val position = if (message.startsWith("OK ")) parseMoveResponse(message) else null + if (position == null) { + endConnection(ServerResponse.SYNTAX_ERROR) + return + } + + // We're on center coordinates, pick it up + if (position == Position(0, 0)) { + sendResponse(ServerResponse.PICK_UP) + phase = RobotPhase.WAITING_FOR_SECRET + waitForResponse() + return + } + + // Update position coordinates + val movePos = moveLogic.position + moveLogic.position = if (movePos == null) position else Position(position.x, position.y, movePos.dir) + + // Get move command and execute it + val command = moveLogic.execute() ?: return + sendResponse(command) + waitForResponse() + } + private fun parseMoveResponse (message: String) : Position? { + val nums = message.substring(3).split(" ") + if (nums.size == 2) { + val x = nums[0].toIntOrNull() + val y = nums[1].toIntOrNull() + if (x != null && y != null) + return Position(x, y) + } + return null + } + + /** Main waiting function, reacts based on current phase */ + private fun waitForResponse () { + // Read message, if there was an error, skip + socket.soTimeout = if (phase == RobotPhase.RECHARGING) RECHARGING_TIMEOUT else MESSAGE_TIMEOUT + val message = reader.readMessage(this) ?: return + println("[SERVER] Read $message whilst in phase $phase") + + // Recharging + if (message == RECHARGING_MESSAGE) { + startRecharging() + return + } + + // Execute commands based on current phase + when (phase) { + RobotPhase.WAITING_FOR_NAME -> waitingForName(message) + RobotPhase.WAITING_FOR_KEY_ID -> waitingForKeyID(message) + RobotPhase.WAITING_FOR_CONFIRMATION -> waitingConfirmation(message) + RobotPhase.RECHARGING -> stopRecharging(message) + RobotPhase.WAITING_FOR_MOVE_RESPONSE -> waitingMoveResponse(message) + RobotPhase.WAITING_FOR_SECRET -> endConnection(ServerResponse.LOGOUT) + } + } + +} + +/** + * Server class listening for incoming connections + * for each connection, new coroutine created + */ +class Server (private val server: ServerSocket = ServerSocket(SERVER_PORT)) { + fun run () { + println("[SERVER] Listening on port $SERVER_PORT") + while (true) { + val socket = server.accept() + GlobalScope.launch { Robot(socket) } + } + } +} + +fun main () = Server().run() \ No newline at end of file diff --git a/README.adoc b/README.adoc index 993b2fb..0dabf6a 100644 --- a/README.adoc +++ b/README.adoc @@ -90,7 +90,7 @@ link:BI-ANG/[English Exam after A2L Course (BI-ANG)] - exam essay link:BI-OSY/[Operating Systems (BI-OSY)] - homeworks on ProgTest -BI-PSI - semestral task, grades export +link:BI-PSI/[Computer Networks (BI-PSI)] - semestral task and grades export BI-BEZ - lab wolfram notebooks, programming tasks, grades and MARAST export