Skip to content
This repository has been archived by the owner on Jul 3, 2023. It is now read-only.

Commit

Permalink
Many changes: fixed port change bug, updated message dispatch pipelin…
Browse files Browse the repository at this point in the history
…e, updated docs, and more.
  • Loading branch information
mattyoung101 committed Sep 6, 2020
1 parent c6c9f4f commit cbb6277
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 98 deletions.
17 changes: 1 addition & 16 deletions ATTRIBUTION.txt
Original file line number Diff line number Diff line change
@@ -1,18 +1,3 @@
This file contains attribution if required for open source projects used.

Kryo: https://github.com/EsotericSoftware/kryo
Copyright (c) 2008-2018, Nathan Sweet
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Vectorz: https://github.com/mikera/vectorz
This library is LGPL licensed. As Omicron2D is under the BSD 3-Clause license and uses Kotlin on the JVM (i.e. dynamic linking),
nothing extra is required to comply with the LGPL. Simply download the code and change the dependency in Gradle to use a
different version if you wish. To retrieve the source for Vectorz, simply clone the repo linked at the URL above.
TODO: fill this out with latest projects
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
# Team Omicron2D
# Omicron2D

Omicron2D aims to be a team competing in RoboCup Soccer 2D Simulation.
This repo contains the Kotlin code, running on the JVM, powering our rcssserver agent.
Omicron2D aims to be a team competing in RoboCup Soccer 2D Simulation. It is currently in very early stages of development.
This repo contains the Kotlin code, running on the JVM, that powers our soccer agents.

Omicron2D is related to Team Omicron, an official team competing in RoboCup Jr Open Soccer.
Omicron2D, however, is currently just an independent side-project consisting of only one member: me, Matt Young (software
engineer on the RoboCup Jr team). I'm always looking for more members, so if you're interested don't hesitate to get
in touch!
engineer on the RoboCup Jr team). If you feel like getting involved, don't hesitate to get in touch :)

Contact Matt Young (matt.young.1@outlook.com) for any questions, queries, qualms or concerns.
If you have any questions, please contact Matt Young (matt.young.1@outlook.com).

## Project status
The project is just getting started. I have almost finished writing the parsers for incoming server messages (see, hear, etc),
and soon I can move on to writing localisation and basic movement.

## About the agents
If you want to know more about our agents, please check out some of the Markdown files in the docs folder (such as
PLANNING.md) and read the well-commented code. In the future when we're closer to the tournament, a team description
If you want to know more about our agents, please check out some Markdown files in the docs folder (such as
PLANNING.md) and/or read the code. In the future when we're closer to the tournament, a team description
paper (TDP) will be prepared.

## Building/running
Expand Down
7 changes: 6 additions & 1 deletion src/main/kotlin/io/github/omicron2d/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import io.github.omicron2d.communication.messages.SeeMessage
import io.github.omicron2d.utils.GeneralConfig
import io.github.omicron2d.utils.SERVER_PROTOCOL_VERSION
import io.github.omicron2d.utils.OMICRON2D_VERSION
import io.github.omicron2d.utils.currentConfig
import org.tinylog.kotlin.Logger
import java.io.FileReader
import java.net.InetAddress
Expand All @@ -42,7 +43,9 @@ object Main {
// load config from YAML files
val yamlReader = YamlReader(FileReader("config_general.yml"))
val generalConfig = yamlReader.read(GeneralConfig::class.java)
currentConfig = generalConfig
Logger.info("General config parsed successfully")
Logger.trace(generalConfig)

Logger.info("Connecting to ${generalConfig.serverHost}:${generalConfig.playerPort}")
val initMessage = OutgoingInitMessage(generalConfig.teamName,
Expand All @@ -51,6 +54,8 @@ object Main {
val agent = PlayerAgent(InetAddress.getByName(generalConfig.serverHost), generalConfig.playerPort)
agent.connect(initMessage)
agent.run()
agent.disconnect()
// hopefully the agent will have already disconnected itself by here (for example, in a timeout)
Logger.info("Omicron2D main finishing")
println("Goodbye!")
}
}
17 changes: 17 additions & 0 deletions src/main/kotlin/io/github/omicron2d/ai/world/ICPLocalisation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* This file is part of the Omicron2D RoboCup 2D Soccer Simulation team.
* Copyright (c) 2020 Matt Young. All rights reserved.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

package io.github.omicron2d.ai.world

/**
* Performs localisation by solving a 2D iterative closest point (ICP) problem, which is sort of similar to least squares
* type thing.
*/
class ICPLocalisation {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

package io.github.omicron2d.ai.world

import io.github.omicron2d.communication.messages.SeeObject
import io.github.omicron2d.utils.PlayMode
import io.github.omicron2d.utils.Side

Expand All @@ -18,5 +19,10 @@ import io.github.omicron2d.utils.Side
*/
data class LowLevelWorldModel(
var unum: Int = -1,
var side: Side = Side.UNKNOWN)
: WorldModel
var side: Side = Side.UNKNOWN,
/** list of flags, received directly from server */
var flags: List<SeeObject> = listOf(),
var players: List<SeeObject> = listOf(),
/** relative position of ball if visible, received directly from server */
var ball: SeeObject? = null
) : WorldModel
44 changes: 23 additions & 21 deletions src/main/kotlin/io/github/omicron2d/communication/PlayerAgent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package io.github.omicron2d.communication
import io.github.omicron2d.ai.world.HighLevelWorldModel
import io.github.omicron2d.ai.world.LowLevelWorldModel
import io.github.omicron2d.communication.messages.*
import io.github.omicron2d.utils.ObjectType
import io.github.omicron2d.utils.PlayMode
import org.tinylog.kotlin.Logger
import java.net.InetAddress
Expand All @@ -22,8 +23,7 @@ import java.util.concurrent.TimeUnit
*
* This is the class in which the main information processing pipeline takes place.
*/
class PlayerAgent(host: InetAddress = InetAddress.getLocalHost(), port: Int = 6000) : SoccerAgent(host, port) {
// TODO consider dropping the low level world model
class PlayerAgent(host: InetAddress = InetAddress.getLocalHost(), port: Int = 6000) : SoccerAgent(host, port), PlayerMessageHandler {
private val lowModel = LowLevelWorldModel()
private val highModel = HighLevelWorldModel()

Expand All @@ -32,27 +32,26 @@ class PlayerAgent(host: InetAddress = InetAddress.getLocalHost(), port: Int = 60

while (true){
// 1. Receive message from server and parse
val msgStr = messages.poll(Long.MAX_VALUE, TimeUnit.DAYS) ?: continue
val msg = MessageFactory.parseIncomingMessage(msgStr)
val msgStr = messages.poll(30, TimeUnit.SECONDS)
if (msgStr == null){
Logger.warn("Unexpected null message from message queue, server dead? Terminating!")
break
} else if (msgStr == "INTERNAL_TIMED_OUT"){
Logger.warn("Received server timeout message, terminating PlayerAgent!")
break
}

// 2. Dispatch message to handlers
if (msg != null){
// do you think there's a better solution than a giant loop? maybe we can inline it into MessageFactory somehow?
when (msg){
is IncomingInitMessage -> handleInitMessage(msg)
is SeeMessage -> handleSeeMessage(msg)
is HearMessage -> handleHearMessage(msg)
is ErrorMessage -> handleErrorMessage(msg)
}
} else {
Logger.warn("No parser for message: $msgStr")
}
dispatchMessage(msgStr)

// 3. Process world model
// 3. We have now created the low level world model. Perform localisation on high level world model.

// 4. Update positions of seen objects in high level world model
//transmitString("(move 1 1)")
}
}

private fun handleInitMessage(init: IncomingInitMessage){
override fun handleInitMessage(init: IncomingInitMessage){
if (init.playMode != PlayMode.BEFORE_KICK_OFF){
Logger.warn("Unexpected play mode during init: ${init.playMode}")
}
Expand All @@ -61,15 +60,18 @@ class PlayerAgent(host: InetAddress = InetAddress.getLocalHost(), port: Int = 60
Logger.debug("Parsed init message $init, world model is now $lowModel")
}

private fun handleSeeMessage(see: SeeMessage){

override fun handleSeeMessage(see: SeeMessage){
// filter out only flags in the see message
lowModel.flags = see.objects.filter { it.type == ObjectType.FLAG }
lowModel.players = see.objects.filter { it.type == ObjectType.PLAYER }
lowModel.ball = see.objects.firstOrNull { it.type == ObjectType.BALL }
}

private fun handleHearMessage(hear: HearMessage){
override fun handleHearMessage(hear: HearMessage){

}

private fun handleErrorMessage(error: ErrorMessage){
override fun handleErrorMessage(error: ErrorMessage){
Logger.warn("Received server error: ${error.message}")
}
}
82 changes: 64 additions & 18 deletions src/main/kotlin/io/github/omicron2d/communication/SoccerAgent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,41 @@
package io.github.omicron2d.communication

import io.github.omicron2d.communication.messages.OutgoingServerMessage
import org.greenrobot.eventbus.EventBus
import io.github.omicron2d.utils.currentConfig
import org.tinylog.kotlin.Logger
import java.net.*
import java.nio.charset.Charset
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread

/**
* Abstract class for all intelligent agents on the soccer simulation server, for example, players and coaches.
* The reference for this is page 71 of the manual (section 6)
* Also based on AbstractUDPClient from the atan Java rcssserver library
* @param host IP address of rcssserver
* @param defaultPort default port of server, will be switched to server assigned one later
*/
abstract class SoccerAgent(private var host: InetAddress, private var port: Int) {
abstract class SoccerAgent(private var host: InetAddress, private var defaultPort: Int) {
private var isConnected = false
private val socket = DatagramSocket().apply {
// 3 minute timer to ensure the socket stays connected (inherited from atan)
soTimeout = 300000
// 10 second timer to ensure the socket stays connected (inherited from atan)
soTimeout = currentConfig.timeout
}

/**
* Port to respond to, instead of the default port (init port), once we've received a response from rcssserver.
*
* The docs are extremely unclear, but it appears that in versions 8+ you're not allowed to send anything but init
* on the default port, and you have to switch over to the port that rcssserver replies to you with otherwise it
* gives you (error only_init_allowed_on_init_port). For reference, do a search in rcssserver project for that error:
* ~/workspace/rcssserver-16.0.0$ rg -i only_init_allowed_on_init_port
*/
private var respondTo: Int? = null

/**
* Contains the queue of messages received from the server, that are awaiting processing
*/
val messages = LinkedBlockingQueue<String>()

private val sockThread = thread(start = false){
Expand All @@ -44,17 +61,37 @@ abstract class SoccerAgent(private var host: InetAddress, private var port: Int)
try {
socket.receive(packet)
} catch (e: SocketException){
Logger.error("Failed to receive from server socket:")
// if the read was interrupted and we should be terminating, just quit
if (Thread.interrupted()){
println("Terminating socket thread from socket exception")
return@thread
}

// otherwise, log the error
Logger.error("Failed to receive():")
Logger.error(e)
} catch (e: SocketTimeoutException){
Logger.warn("Timeout during receive! Server may have gone offline.")
Logger.warn("Timeout during receive(), server probably offline:")
Logger.warn(e)
// TODO do we want to disconnect here?

// simple teardown routine since calling disconnect() doesn't work
isConnected = false
respondTo = null
socket.close()
messages.add("INTERNAL_TIMED_OUT")
println("Socket thread finished due to timeout")
return@thread
}

// switch to new port to avoid error (see respondTo description)
if (respondTo == null){
Logger.info("Switching to new port ${packet.port}")
respondTo = packet.port
}

val messageBytes = packet.data.takeWhile { it != 0.toByte() }.toByteArray()
val messageString = messageBytes.toString(charset = Charset.forName("US-ASCII"))
Logger.trace("Inbound message: $messageString")
Logger.trace("Inbound message (from ${packet.address}:${packet.port}): $messageString")

messages.add(messageString)
//println("Queue size: ${messages.size}")
Expand All @@ -67,22 +104,24 @@ abstract class SoccerAgent(private var host: InetAddress, private var port: Int)
abstract fun run()

/**
* Internal method to transmit a string to the remote server over UDP with ASCII encoding
* Internal method to transmit a string to the remote server over UDP with ASCII encoding. Generally you want to use
* transmit()
*/
private fun transmitString(str: String){
Logger.trace("Outbound message: $str")
protected fun transmitString(str: String){
if (!isConnected){
throw IllegalStateException("Tried to send message on unconnected socket")
}

Logger.trace("Outbound message (to ${host}:${respondTo ?: defaultPort}): $str")
val bytes = str.toByteArray(Charset.forName("US-ASCII"))
val packet = DatagramPacket(bytes, bytes.size, host, port)
val packet = DatagramPacket(bytes, bytes.size, host, respondTo ?: defaultPort)
socket.send(packet)
}

/**
* Serialises then transmits a message to the server
*/
fun transmit(message: OutgoingServerMessage){
if (!isConnected){
throw IllegalStateException("Tried to send message on unconnected socket")
}
transmitString(message.serialise())
}

Expand Down Expand Up @@ -111,12 +150,19 @@ abstract class SoccerAgent(private var host: InetAddress, private var port: Int)
return
}

// otherwise, hit the disconnect
Logger.debug("Disconnecting agent")
// tell the server we're disconnecting
println("Disconnecting agent")
transmitString("(bye)")
Thread.sleep(100)
// TODO find a better way to ensure transmission has completed? or do we not need to?

// close down the socket
sockThread.interrupt()
sockThread.join(500)
socket.close()
sockThread.join(500)
println("Socket thread joined!")

isConnected = false
respondTo = null
}
}

This file was deleted.

Loading

0 comments on commit cbb6277

Please sign in to comment.