diff --git a/Assets/Plugins/SimpleJSON.cs b/Assets/Plugins/SimpleJSON.cs new file mode 100644 index 0000000..f4647a9 --- /dev/null +++ b/Assets/Plugins/SimpleJSON.cs @@ -0,0 +1,1070 @@ +//#define USE_SharpZipLib +#if !UNITY_WEBPLAYER +#define USE_FileIO +#endif + +/* * * * * + * A simple JSON Parser / builder + * ------------------------------ + * + * It mainly has been written as a simple JSON parser. It can build a JSON string + * from the node-tree, or generate a node tree from any valid JSON string. + * + * If you want to use compression when saving to file / stream / B64 you have to include + * SharpZipLib ( http://www.icsharpcode.net/opensource/sharpziplib/ ) in your project and + * define "USE_SharpZipLib" at the top of the file + * + * Written by Bunny83 + * 2012-06-09 + * + * Features / attributes: + * - provides strongly typed node classes and lists / dictionaries + * - provides easy access to class members / array items / data values + * - the parser ignores data types. Each value is a string. + * - only double quotes (") are used for quoting strings. + * - values and names are not restricted to quoted strings. They simply add up and are trimmed. + * - There are only 3 types: arrays(JSONArray), objects(JSONClass) and values(JSONData) + * - provides "casting" properties to easily convert to / from those types: + * int / float / double / bool + * - provides a common interface for each node so no explicit casting is required. + * - the parser try to avoid errors, but if malformed JSON is parsed the result is undefined + * + * + * 2012-12-17 Update: + * - Added internal JSONLazyCreator class which simplifies the construction of a JSON tree + * Now you can simple reference any item that doesn't exist yet and it will return a JSONLazyCreator + * The class determines the required type by it's further use, creates the type and removes itself. + * - Added binary serialization / deserialization. + * - Added support for BZip2 zipped binary format. Requires the SharpZipLib ( http://www.icsharpcode.net/opensource/sharpziplib/ ) + * The usage of the SharpZipLib library can be disabled by removing or commenting out the USE_SharpZipLib define at the top + * - The serializer uses different types when it comes to store the values. Since my data values + * are all of type string, the serializer will "try" which format fits best. The order is: int, float, double, bool, string. + * It's not the most efficient way but for a moderate amount of data it should work on all platforms. + * + * * * * */ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + + +namespace SimpleJSON +{ + public enum JSONBinaryTag + { + Array = 1, + Class = 2, + Value = 3, + IntValue = 4, + DoubleValue = 5, + BoolValue = 6, + FloatValue = 7, + } + + public class JSONNode + { + #region common interface + public virtual void Add(string aKey, JSONNode aItem){ } + public virtual JSONNode this[int aIndex] { get { return null; } set { } } + public virtual JSONNode this[string aKey] { get { return null; } set { } } + public virtual string Value { get { return ""; } set { } } + public virtual int Count { get { return 0; } } + + public virtual void Add(JSONNode aItem) + { + Add("", aItem); + } + + public virtual JSONNode Remove(string aKey) { return null; } + public virtual JSONNode Remove(int aIndex) { return null; } + public virtual JSONNode Remove(JSONNode aNode) { return aNode; } + + public virtual IEnumerable Childs { get { yield break;} } + public IEnumerable DeepChilds + { + get + { + foreach (var C in Childs) + foreach (var D in C.DeepChilds) + yield return D; + } + } + + public override string ToString() + { + return "JSONNode"; + } + public virtual string ToString(string aPrefix) + { + return "JSONNode"; + } + + #endregion common interface + + #region typecasting properties + public virtual int AsInt + { + get + { + int v = 0; + if (int.TryParse(Value,out v)) + return v; + return 0; + } + set + { + Value = value.ToString(); + } + } + public virtual float AsFloat + { + get + { + float v = 0.0f; + if (float.TryParse(Value,out v)) + return v; + return 0.0f; + } + set + { + Value = value.ToString(); + } + } + public virtual double AsDouble + { + get + { + double v = 0.0; + if (double.TryParse(Value,out v)) + return v; + return 0.0; + } + set + { + Value = value.ToString(); + } + } + public virtual bool AsBool + { + get + { + bool v = false; + if (bool.TryParse(Value,out v)) + return v; + return !string.IsNullOrEmpty(Value); + } + set + { + Value = (value)?"true":"false"; + } + } + public virtual JSONArray AsArray + { + get + { + return this as JSONArray; + } + } + public virtual JSONClass AsObject + { + get + { + return this as JSONClass; + } + } + + + #endregion typecasting properties + + #region operators + public static implicit operator JSONNode(string s) + { + return new JSONData(s); + } + public static implicit operator string(JSONNode d) + { + return (d == null)?null:d.Value; + } + public static bool operator ==(JSONNode a, object b) + { + if (b == null && a is JSONLazyCreator) + return true; + return System.Object.ReferenceEquals(a,b); + } + + public static bool operator !=(JSONNode a, object b) + { + return !(a == b); + } + public override bool Equals (object obj) + { + return System.Object.ReferenceEquals(this, obj); + } + public override int GetHashCode () + { + return base.GetHashCode(); + } + + + #endregion operators + + internal static string Escape(string aText) + { + string result = ""; + foreach(char c in aText) + { + switch(c) + { + case '\\' : result += "\\\\"; break; + case '\"' : result += "\\\""; break; + case '\n' : result += "\\n" ; break; + case '\r' : result += "\\r" ; break; + case '\t' : result += "\\t" ; break; + case '\b' : result += "\\b" ; break; + case '\f' : result += "\\f" ; break; + default : result += c ; break; + } + } + return result; + } + + public static JSONNode Parse(string aJSON) + { + Stack stack = new Stack(); + JSONNode ctx = null; + int i = 0; + string Token = ""; + string TokenName = ""; + bool QuoteMode = false; + while (i < aJSON.Length) + { + switch (aJSON[i]) + { + case '{': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + stack.Push(new JSONClass()); + if (ctx != null) + { + TokenName = TokenName.Trim(); + if (ctx is JSONArray) + ctx.Add(stack.Peek()); + else if (TokenName != "") + ctx.Add(TokenName,stack.Peek()); + } + TokenName = ""; + Token = ""; + ctx = stack.Peek(); + break; + + case '[': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + + stack.Push(new JSONArray()); + if (ctx != null) + { + TokenName = TokenName.Trim(); + if (ctx is JSONArray) + ctx.Add(stack.Peek()); + else if (TokenName != "") + ctx.Add(TokenName,stack.Peek()); + } + TokenName = ""; + Token = ""; + ctx = stack.Peek(); + break; + + case '}': + case ']': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + if (stack.Count == 0) + throw new Exception("JSON Parse: Too many closing brackets"); + + stack.Pop(); + if (Token != "") + { + TokenName = TokenName.Trim(); + if (ctx is JSONArray) + ctx.Add(Token); + else if (TokenName != "") + ctx.Add(TokenName,Token); + } + TokenName = ""; + Token = ""; + if (stack.Count>0) + ctx = stack.Peek(); + break; + + case ':': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + TokenName = Token; + Token = ""; + break; + + case '"': + QuoteMode ^= true; + break; + + case ',': + if (QuoteMode) + { + Token += aJSON[i]; + break; + } + if (Token != "") + { + if (ctx is JSONArray) + ctx.Add(Token); + else if (TokenName != "") + ctx.Add(TokenName, Token); + } + TokenName = ""; + Token = ""; + break; + + case '\r': + case '\n': + break; + + case ' ': + case '\t': + if (QuoteMode) + Token += aJSON[i]; + break; + + case '\\': + ++i; + if (QuoteMode) + { + char C = aJSON[i]; + switch (C) + { + case 't' : Token += '\t'; break; + case 'r' : Token += '\r'; break; + case 'n' : Token += '\n'; break; + case 'b' : Token += '\b'; break; + case 'f' : Token += '\f'; break; + case 'u': + { + string s = aJSON.Substring(i+1,4); + Token += (char)int.Parse(s, System.Globalization.NumberStyles.AllowHexSpecifier); + i += 4; + break; + } + default : Token += C; break; + } + } + break; + + default: + Token += aJSON[i]; + break; + } + ++i; + } + if (QuoteMode) + { + throw new Exception("JSON Parse: Quotation marks seems to be messed up."); + } + return ctx; + } + + public virtual void Serialize(System.IO.BinaryWriter aWriter) {} + + public void SaveToStream(System.IO.Stream aData) + { + var W = new System.IO.BinaryWriter(aData); + Serialize(W); + } + + #if USE_SharpZipLib + public void SaveToCompressedStream(System.IO.Stream aData) + { + using (var gzipOut = new ICSharpCode.SharpZipLib.BZip2.BZip2OutputStream(aData)) + { + gzipOut.IsStreamOwner = false; + SaveToStream(gzipOut); + gzipOut.Close(); + } + } + + public void SaveToCompressedFile(string aFileName) + { + #if USE_FileIO + System.IO.Directory.CreateDirectory((new System.IO.FileInfo(aFileName)).Directory.FullName); + using(var F = System.IO.File.OpenWrite(aFileName)) + { + SaveToCompressedStream(F); + } + #else + throw new Exception("Can't use File IO stuff in webplayer"); + #endif + } + public string SaveToCompressedBase64() + { + using (var stream = new System.IO.MemoryStream()) + { + SaveToCompressedStream(stream); + stream.Position = 0; + return System.Convert.ToBase64String(stream.ToArray()); + } + } + + #else + public void SaveToCompressedStream(System.IO.Stream aData) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + public void SaveToCompressedFile(string aFileName) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + public string SaveToCompressedBase64() + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + #endif + + public void SaveToFile(string aFileName) + { + #if USE_FileIO + System.IO.Directory.CreateDirectory((new System.IO.FileInfo(aFileName)).Directory.FullName); + using(var F = System.IO.File.OpenWrite(aFileName)) + { + SaveToStream(F); + } + #else + throw new Exception("Can't use File IO stuff in webplayer"); + #endif + } + public string SaveToBase64() + { + using (var stream = new System.IO.MemoryStream()) + { + SaveToStream(stream); + stream.Position = 0; + return System.Convert.ToBase64String(stream.ToArray()); + } + } + public static JSONNode Deserialize(System.IO.BinaryReader aReader) + { + JSONBinaryTag type = (JSONBinaryTag)aReader.ReadByte(); + switch(type) + { + case JSONBinaryTag.Array: + { + int count = aReader.ReadInt32(); + JSONArray tmp = new JSONArray(); + for(int i = 0; i < count; i++) + tmp.Add(Deserialize(aReader)); + return tmp; + } + case JSONBinaryTag.Class: + { + int count = aReader.ReadInt32(); + JSONClass tmp = new JSONClass(); + for(int i = 0; i < count; i++) + { + string key = aReader.ReadString(); + var val = Deserialize(aReader); + tmp.Add(key, val); + } + return tmp; + } + case JSONBinaryTag.Value: + { + return new JSONData(aReader.ReadString()); + } + case JSONBinaryTag.IntValue: + { + return new JSONData(aReader.ReadInt32()); + } + case JSONBinaryTag.DoubleValue: + { + return new JSONData(aReader.ReadDouble()); + } + case JSONBinaryTag.BoolValue: + { + return new JSONData(aReader.ReadBoolean()); + } + case JSONBinaryTag.FloatValue: + { + return new JSONData(aReader.ReadSingle()); + } + + default: + { + throw new Exception("Error deserializing JSON. Unknown tag: " + type); + } + } + } + + #if USE_SharpZipLib + public static JSONNode LoadFromCompressedStream(System.IO.Stream aData) + { + var zin = new ICSharpCode.SharpZipLib.BZip2.BZip2InputStream(aData); + return LoadFromStream(zin); + } + public static JSONNode LoadFromCompressedFile(string aFileName) + { + #if USE_FileIO + using(var F = System.IO.File.OpenRead(aFileName)) + { + return LoadFromCompressedStream(F); + } + #else + throw new Exception("Can't use File IO stuff in webplayer"); + #endif + } + public static JSONNode LoadFromCompressedBase64(string aBase64) + { + var tmp = System.Convert.FromBase64String(aBase64); + var stream = new System.IO.MemoryStream(tmp); + stream.Position = 0; + return LoadFromCompressedStream(stream); + } + #else + public static JSONNode LoadFromCompressedFile(string aFileName) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + public static JSONNode LoadFromCompressedStream(System.IO.Stream aData) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + public static JSONNode LoadFromCompressedBase64(string aBase64) + { + throw new Exception("Can't use compressed functions. You need include the SharpZipLib and uncomment the define at the top of SimpleJSON"); + } + #endif + + public static JSONNode LoadFromStream(System.IO.Stream aData) + { + using(var R = new System.IO.BinaryReader(aData)) + { + return Deserialize(R); + } + } + public static JSONNode LoadFromFile(string aFileName) + { + #if USE_FileIO + using(var F = System.IO.File.OpenRead(aFileName)) + { + return LoadFromStream(F); + } + #else + throw new Exception("Can't use File IO stuff in webplayer"); + #endif + } + public static JSONNode LoadFromBase64(string aBase64) + { + var tmp = System.Convert.FromBase64String(aBase64); + var stream = new System.IO.MemoryStream(tmp); + stream.Position = 0; + return LoadFromStream(stream); + } + } // End of JSONNode + + public class JSONArray : JSONNode, IEnumerable + { + private List m_List = new List(); + public override JSONNode this[int aIndex] + { + get + { + if (aIndex<0 || aIndex >= m_List.Count) + return new JSONLazyCreator(this); + return m_List[aIndex]; + } + set + { + if (aIndex<0 || aIndex >= m_List.Count) + m_List.Add(value); + else + m_List[aIndex] = value; + } + } + public override JSONNode this[string aKey] + { + get{ return new JSONLazyCreator(this);} + set{ m_List.Add(value); } + } + public override int Count + { + get { return m_List.Count; } + } + public override void Add(string aKey, JSONNode aItem) + { + m_List.Add(aItem); + } + public override JSONNode Remove(int aIndex) + { + if (aIndex < 0 || aIndex >= m_List.Count) + return null; + JSONNode tmp = m_List[aIndex]; + m_List.RemoveAt(aIndex); + return tmp; + } + public override JSONNode Remove(JSONNode aNode) + { + m_List.Remove(aNode); + return aNode; + } + public override IEnumerable Childs + { + get + { + foreach(JSONNode N in m_List) + yield return N; + } + } + public IEnumerator GetEnumerator() + { + foreach(JSONNode N in m_List) + yield return N; + } + public override string ToString() + { + string result = "[ "; + foreach (JSONNode N in m_List) + { + if (result.Length > 2) + result += ", "; + result += N.ToString(); + } + result += " ]"; + return result; + } + public override string ToString(string aPrefix) + { + string result = "[ "; + foreach (JSONNode N in m_List) + { + if (result.Length > 3) + result += ", "; + result += "\n" + aPrefix + " "; + result += N.ToString(aPrefix+" "); + } + result += "\n" + aPrefix + "]"; + return result; + } + public override void Serialize (System.IO.BinaryWriter aWriter) + { + aWriter.Write((byte)JSONBinaryTag.Array); + aWriter.Write(m_List.Count); + for(int i = 0; i < m_List.Count; i++) + { + m_List[i].Serialize(aWriter); + } + } + } // End of JSONArray + + public class JSONClass : JSONNode, IEnumerable + { + private Dictionary m_Dict = new Dictionary(); + public override JSONNode this[string aKey] + { + get + { + if (m_Dict.ContainsKey(aKey)) + return m_Dict[aKey]; + else + return new JSONLazyCreator(this, aKey); + } + set + { + if (m_Dict.ContainsKey(aKey)) + m_Dict[aKey] = value; + else + m_Dict.Add(aKey,value); + } + } + public override JSONNode this[int aIndex] + { + get + { + if (aIndex < 0 || aIndex >= m_Dict.Count) + return null; + return m_Dict.ElementAt(aIndex).Value; + } + set + { + if (aIndex < 0 || aIndex >= m_Dict.Count) + return; + string key = m_Dict.ElementAt(aIndex).Key; + m_Dict[key] = value; + } + } + public override int Count + { + get { return m_Dict.Count; } + } + + + public override void Add(string aKey, JSONNode aItem) + { + if (!string.IsNullOrEmpty(aKey)) + { + if (m_Dict.ContainsKey(aKey)) + m_Dict[aKey] = aItem; + else + m_Dict.Add(aKey, aItem); + } + else + m_Dict.Add(Guid.NewGuid().ToString(), aItem); + } + + public override JSONNode Remove(string aKey) + { + if (!m_Dict.ContainsKey(aKey)) + return null; + JSONNode tmp = m_Dict[aKey]; + m_Dict.Remove(aKey); + return tmp; + } + public override JSONNode Remove(int aIndex) + { + if (aIndex < 0 || aIndex >= m_Dict.Count) + return null; + var item = m_Dict.ElementAt(aIndex); + m_Dict.Remove(item.Key); + return item.Value; + } + public override JSONNode Remove(JSONNode aNode) + { + try + { + var item = m_Dict.Where(k => k.Value == aNode).First(); + m_Dict.Remove(item.Key); + return aNode; + } + catch + { + return null; + } + } + + public override IEnumerable Childs + { + get + { + foreach(KeyValuePair N in m_Dict) + yield return N.Value; + } + } + + public IEnumerator GetEnumerator() + { + foreach(KeyValuePair N in m_Dict) + yield return N; + } + public override string ToString() + { + string result = "{"; + foreach (KeyValuePair N in m_Dict) + { + if (result.Length > 2) + result += ", "; + result += "\"" + Escape(N.Key) + "\":" + N.Value.ToString(); + } + result += "}"; + return result; + } + public override string ToString(string aPrefix) + { + string result = "{ "; + foreach (KeyValuePair N in m_Dict) + { + if (result.Length > 3) + result += ", "; + result += "\n" + aPrefix + " "; + result += "\"" + Escape(N.Key) + "\" : " + N.Value.ToString(aPrefix+" "); + } + result += "\n" + aPrefix + "}"; + return result; + } + public override void Serialize (System.IO.BinaryWriter aWriter) + { + aWriter.Write((byte)JSONBinaryTag.Class); + aWriter.Write(m_Dict.Count); + foreach(string K in m_Dict.Keys) + { + aWriter.Write(K); + m_Dict[K].Serialize(aWriter); + } + } + } // End of JSONClass + + public class JSONData : JSONNode + { + private string m_Data; + public override string Value + { + get { return m_Data; } + set { m_Data = value; } + } + public JSONData(string aData) + { + m_Data = aData; + } + public JSONData(float aData) + { + AsFloat = aData; + } + public JSONData(double aData) + { + AsDouble = aData; + } + public JSONData(bool aData) + { + AsBool = aData; + } + public JSONData(int aData) + { + AsInt = aData; + } + + public override string ToString() + { + return "\"" + Escape(m_Data) + "\""; + } + public override string ToString(string aPrefix) + { + return "\"" + Escape(m_Data) + "\""; + } + public override void Serialize (System.IO.BinaryWriter aWriter) + { + var tmp = new JSONData(""); + + tmp.AsInt = AsInt; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.IntValue); + aWriter.Write(AsInt); + return; + } + tmp.AsFloat = AsFloat; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.FloatValue); + aWriter.Write(AsFloat); + return; + } + tmp.AsDouble = AsDouble; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.DoubleValue); + aWriter.Write(AsDouble); + return; + } + + tmp.AsBool = AsBool; + if (tmp.m_Data == this.m_Data) + { + aWriter.Write((byte)JSONBinaryTag.BoolValue); + aWriter.Write(AsBool); + return; + } + aWriter.Write((byte)JSONBinaryTag.Value); + aWriter.Write(m_Data); + } + } // End of JSONData + + internal class JSONLazyCreator : JSONNode + { + private JSONNode m_Node = null; + private string m_Key = null; + + public JSONLazyCreator(JSONNode aNode) + { + m_Node = aNode; + m_Key = null; + } + public JSONLazyCreator(JSONNode aNode, string aKey) + { + m_Node = aNode; + m_Key = aKey; + } + + private void Set(JSONNode aVal) + { + if (m_Key == null) + { + m_Node.Add(aVal); + } + else + { + m_Node.Add(m_Key, aVal); + } + m_Node = null; // Be GC friendly. + } + + public override JSONNode this[int aIndex] + { + get + { + return new JSONLazyCreator(this); + } + set + { + var tmp = new JSONArray(); + tmp.Add(value); + Set(tmp); + } + } + + public override JSONNode this[string aKey] + { + get + { + return new JSONLazyCreator(this, aKey); + } + set + { + var tmp = new JSONClass(); + tmp.Add(aKey, value); + Set(tmp); + } + } + public override void Add (JSONNode aItem) + { + var tmp = new JSONArray(); + tmp.Add(aItem); + Set(tmp); + } + public override void Add (string aKey, JSONNode aItem) + { + var tmp = new JSONClass(); + tmp.Add(aKey, aItem); + Set(tmp); + } + public static bool operator ==(JSONLazyCreator a, object b) + { + if (b == null) + return true; + return System.Object.ReferenceEquals(a,b); + } + + public static bool operator !=(JSONLazyCreator a, object b) + { + return !(a == b); + } + public override bool Equals (object obj) + { + if (obj == null) + return true; + return System.Object.ReferenceEquals(this, obj); + } + public override int GetHashCode () + { + return base.GetHashCode(); + } + + public override string ToString() + { + return ""; + } + public override string ToString(string aPrefix) + { + return ""; + } + + public override int AsInt + { + get + { + JSONData tmp = new JSONData(0); + Set(tmp); + return 0; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } + public override float AsFloat + { + get + { + JSONData tmp = new JSONData(0.0f); + Set(tmp); + return 0.0f; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } + public override double AsDouble + { + get + { + JSONData tmp = new JSONData(0.0); + Set(tmp); + return 0.0; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } + public override bool AsBool + { + get + { + JSONData tmp = new JSONData(false); + Set(tmp); + return false; + } + set + { + JSONData tmp = new JSONData(value); + Set(tmp); + } + } + public override JSONArray AsArray + { + get + { + JSONArray tmp = new JSONArray(); + Set(tmp); + return tmp; + } + } + public override JSONClass AsObject + { + get + { + JSONClass tmp = new JSONClass(); + Set(tmp); + return tmp; + } + } + } // End of JSONLazyCreator + + public static class JSON + { + public static JSONNode Parse(string aJSON) + { + return JSONNode.Parse(aJSON); + } + } +} \ No newline at end of file diff --git a/Assets/Plugins/SimpleJSON.cs.meta b/Assets/Plugins/SimpleJSON.cs.meta new file mode 100644 index 0000000..8601ab7 --- /dev/null +++ b/Assets/Plugins/SimpleJSON.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: ef4cbfe52ee0546cc9d2d7bf44dda929 +timeCreated: 1480804441 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SerialComm/Scripts/PackageAssembler.cs b/Assets/SerialComm/Scripts/PackageAssembler.cs new file mode 100644 index 0000000..cd50eac --- /dev/null +++ b/Assets/SerialComm/Scripts/PackageAssembler.cs @@ -0,0 +1,68 @@ +using System.IO; + +// The package format used in this class is described in UnityThreads.cpp. +public class PackageAssembler +{ + public const int packageSize = 64; + + public void AddPackage(byte[] package) + { + if(package.Length != packageSize) + return; + + var input = new BinaryReader(new MemoryStream(package)); + + var magic1 = input.ReadUInt32(); + var messageId = input.ReadUInt32(); + var packageNumber = input.ReadUInt32(); + var nBytesOfData = input.ReadByte(); + var data = input.ReadBytes(47); + var magic2 = input.ReadUInt32(); + + if(magic1 != 0xaaaa5555 || magic2 != 0xaa55aa55 || + nBytesOfData > 47 || + (messageId == lastMessageId && lastMessageWasBad) || + (messageId == lastMessageId && packageNumber != lastPackageNumber - 1)) + { + lastMessageWasBad = true; + return; + } + + if(messageId != lastMessageId) + { + fullMessageReceived = false; + message = ""; + } + + for(int i = 0; i < nBytesOfData; i++) + message += (char)data[i]; + + if(packageNumber == 0) + fullMessageReceived = true; + + lastMessageId = messageId; + lastPackageNumber = packageNumber; + lastMessageWasBad = false; + } + + public bool FullMessageWasReceived() + { + return fullMessageReceived; + } + + public string GetMessage() + { + return message; + } + + public bool LastMessageWasBad() + { + return lastMessageWasBad; + } + + uint lastMessageId = uint.MaxValue; + bool lastMessageWasBad = true; + uint lastPackageNumber = 0; + string message; + bool fullMessageReceived = false; +} diff --git a/Assets/SerialComm/Scripts/PackageAssembler.cs.meta b/Assets/SerialComm/Scripts/PackageAssembler.cs.meta new file mode 100644 index 0000000..08daebf --- /dev/null +++ b/Assets/SerialComm/Scripts/PackageAssembler.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 3ce79d686201049cbb8889067d6f65d9 +timeCreated: 1480802177 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SerialComm/Scripts/RobotCommand.cs b/Assets/SerialComm/Scripts/RobotCommand.cs new file mode 100644 index 0000000..2e56965 --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotCommand.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +public class RobotCommand +{ + public RobotCommand(string commandDescription, string actualCommand) + { + this.actualCommand = actualCommand; + this.commandDescription = commandDescription; + + BuildActualParameterList(); + } + + public bool Is(string commandDescription) + { + return this.commandDescription == commandDescription; + } + + public int GetInteger(int i) + { + return Convert.ToInt32(actualParameters[i]); + } + + public string GetWord(int i) + { + return actualParameters[i]; + } + + public string GetString(int i) + { + return actualParameters[i]; + } + + void BuildActualParameterList() + { + Tokenizer commandDescriptionTokenizer = new Tokenizer(commandDescription, ' '); + Tokenizer actualCommandTokenizer = new Tokenizer(actualCommand, ' '); + + while(actualCommandTokenizer.HasMore()) + { + var formalParameter = commandDescriptionTokenizer.GetToken(); + + if(formalParameter == "#i" || formalParameter == "#w") + actualParameters.Add(actualCommandTokenizer.GetToken()); + + else if(formalParameter == "#s") + actualParameters.Add(actualCommandTokenizer.GetString()); + + else + actualCommandTokenizer.GetToken(); // The token is a keyword and we ignore it. + } + + } + + string actualCommand; + string commandDescription; + IList actualParameters = new List(); +} diff --git a/Assets/SerialComm/Scripts/RobotCommand.cs.meta b/Assets/SerialComm/Scripts/RobotCommand.cs.meta new file mode 100644 index 0000000..78b2f0a --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotCommand.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 1124f602d7eaf42138213effd05dc430 +timeCreated: 1480805087 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SerialComm/Scripts/RobotConnection.cs b/Assets/SerialComm/Scripts/RobotConnection.cs new file mode 100644 index 0000000..f74c72a --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotConnection.cs @@ -0,0 +1,249 @@ +using System; +using System.Text; +using System.Collections.Generic; +using System.Threading; +using System.Net.Sockets; +using System.IO; +using UnityEngine; + +public interface RobotMessageReceiver +{ + void NewMessage(string message); +} + +public interface RobotMessageSender +{ + void SendMessage(string message); +} + +// The package format used in this class is described in UnityThreads.cpp. +public class RobotConnection : MonoBehaviour, RobotMessageSender +{ + public RobotConnection(string server, int port, RobotMessageReceiver robotMessageReceiver) + { + this.server = server; + this.port = port; + this.robotMessageReceiver = robotMessageReceiver; + + inputThread = new Thread(new ThreadStart(InputThread)); + outputThread = new Thread(new ThreadStart(OutputThread)); + + inputThread.Start(); + outputThread.Start(); + } + + public void SetServer(string server, int port) + { + lock(serverPortLock) + { + this.server = server; + this.port = port; + } + } + + public void Stop() + { + stopped = true; + inputThread.Join(); + outputThread.Join(); + + CloseSocket(); + } + + public void SendMessage(string message) + { + lock(sendMessagesLock) + sendMessages.Enqueue(message); + } + + void InputThread() + { + InputOutput(InputFunction, ref inputLock, ref outputLock, ref inputHasConnected, ref outputHasConnected); + } + + void OutputThread() + { + InputOutput(OutputFunction, ref outputLock, ref inputLock, ref outputHasConnected, ref inputHasConnected); + } + + void InputOutput(RobotConnectionFunction function, ref object myLock, ref object otherLock, ref bool hasConnected, ref bool otherHasConnected) + { + while(!stopped) + { + lock(connectingLock) + { + otherHasConnected = false; + } + + try + { + lock(myLock) + { + if(socket == null) + throw new Exception(); + + function(); + } + } + catch(Exception) + { + ResetConnection(ref outputLock, ref hasConnected, ref otherHasConnected); + } + } + } + + void ResetConnection(ref object otherLock, ref bool hasConnected, ref bool otherHasConnected) + { + lock(connectingLock) + { + if(otherHasConnected) + return; + + lock(otherLock) + { + if(stopped) + return; + + socket = null; + + while(!stopped && socket == null) + { + try + { + socket = new TcpClient(); + + IAsyncResult asyncResult; + lock(serverPortLock) + { + asyncResult = socket.BeginConnect(server, port, null, null); + } + asyncResult.AsyncWaitHandle.WaitOne(1000); + + if(socket.Connected) + socket.EndConnect(asyncResult); + else + throw new Exception(); + + socket.ReceiveTimeout = 1000; + socket.SendTimeout = 1000; + socket.NoDelay = true; + + hasConnected = true; + } + catch(Exception) + { + CloseSocket(); + } + } + } + } + } + + void InputFunction() + { + Debug.Log ("We have input"); + byte[] package = new byte[PackageAssembler.packageSize]; + NetworkStream stream = socket.GetStream (); + //enter to an infinite cycle to be able to handle every change in stream + while (true) { + if(stopped) + return; + + if(!stream.DataAvailable) + { + Thread.Sleep(1); + continue; + } + + Byte[] bytes = new Byte[socket.Available]; + + stream.Read(bytes, 0, bytes.Length); + + //translate bytes of request to string + String data = Encoding.UTF8.GetString(bytes); + Debug.Log (data); + robotMessageReceiver.NewMessage (data); + } + } + + void OutputFunction() + { + string message; + + while(!stopped && sendMessages.Count != 0) + { + lock(sendMessagesLock) + message = sendMessages.Dequeue(); + + SendAsPackages(message); + } + + var secondsSinceLastHello = DateTime.Now.Subtract(lastHelloTime).TotalSeconds; + if(secondsSinceLastHello > 5) + { + SendMessage("hello"); + lastHelloTime = DateTime.Now; + } + + Thread.Sleep(1); + } + + void SendAsPackages(string message) + { + var nBytesPerPackage = 47; + var nPackages = (message.Length + nBytesPerPackage - 1) / nBytesPerPackage; + var packageNumber = nPackages - 1; + var nBytesLeftInMessage = message.Length; + byte[] dataZeros = new byte[nBytesPerPackage]; // Elements are initialized to 0. + + messageId++; + + while(nBytesLeftInMessage != 0) + { + uint magic1 = 0xaaaa5555; + uint magic2 = 0xaa55aa55; + byte nBytesData = (byte)(nBytesLeftInMessage >= nBytesPerPackage ? nBytesPerPackage : nBytesLeftInMessage); + var data = message.Substring(message.Length - nBytesLeftInMessage); + + socket.GetStream().Write(BitConverter.GetBytes(magic1), 0, 4); + socket.GetStream().Write(BitConverter.GetBytes(messageId), 0, 4); + socket.GetStream().Write(BitConverter.GetBytes(packageNumber), 0, 4); + socket.GetStream().Write(BitConverter.GetBytes(nBytesData), 0, 1); + socket.GetStream().Write(Encoding.UTF8.GetBytes(data), 0, nBytesData); + socket.GetStream().Write(dataZeros, 0, nBytesPerPackage - nBytesData); + socket.GetStream().Write(BitConverter.GetBytes(magic2), 0, 4); + + packageNumber--; + nBytesLeftInMessage -= nBytesData; + } + } + + void CloseSocket() + { + if(socket != null) + socket.Close(); + + socket = null; + } + + delegate void RobotConnectionFunction(); + + string server; + int port; + RobotMessageReceiver robotMessageReceiver; + volatile bool stopped = false; + TcpClient socket; + Thread inputThread; + Thread outputThread; + bool inputHasConnected = false; + bool outputHasConnected = false; + object connectingLock = new object(); + object inputLock = new object(); + object outputLock = new object(); + object serverPortLock = new object(); + PackageAssembler packageAssembler = new PackageAssembler(); + object sendMessagesLock = new object(); + Queue sendMessages = new Queue(); + int messageId = 0; + DateTime lastHelloTime = DateTime.Now; +} diff --git a/Assets/SerialComm/Scripts/RobotConnection.cs.meta b/Assets/SerialComm/Scripts/RobotConnection.cs.meta new file mode 100644 index 0000000..b9c8242 --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotConnection.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 82ae2e0e5268a47d7a34185f5858d114 +timeCreated: 1480802177 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SerialComm/Scripts/RobotMessages.cs b/Assets/SerialComm/Scripts/RobotMessages.cs new file mode 100644 index 0000000..39f4d12 --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotMessages.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +//using SimpleJSON; +using UnityEngine; + + +//Basic structure for elements containing a chat message +public struct RobotChatMessage { + + public string user; //user will eventually need user type + public string message; //message is the text input from the user, unless it's a command. + //TODO: messages should all be messages, except we assign a message type to them in the future. + public bool isCommand; //Is this message a command? + public bool isExecuting; //Is it executing? +} + +//Message parameters are defined and set here to determine how they are displayed on the GUI +public struct InternalRobotMessage { + + + //Set these values for a normal chat message. + public InternalRobotMessage(string user, string message) + { + this.user = user; + this.message = message; + commandDescription = ""; + commandId = 0; + isCommand = false; + isExecuting = false; + newMessage = false; + } + + //Set these values if the message is a command + public InternalRobotMessage(string user, string message, string commandDescription, int commandId) + { + this.user = user; + this.message = message; + this.commandDescription = commandDescription; + this.commandId = commandId; + isCommand = true; + isExecuting = false; + newMessage = false; + } + + //These values are set by InternalRobotMessage method above + public string user; + public string message; + public string commandDescription; + public int commandId; + public bool isCommand; + public bool isExecuting; + public bool newMessage; +}; + +//Gets messages from another source over the network via TCP Sockets +//Class uses RobotMessageReceiver & RobotMEssageSender interfaces from RobotConnection.cs +public class RobotMessages : MonoBehaviour, RobotMessageReceiver, RobotMessageSender +{ + + IList chatMessages = new List(); + IDictionary variables = new Dictionary(); + RobotConnection connection; + public RobotMessages(string server, int port) + { + connection = new RobotConnection(server, port, this); + } + + public void SetServer(string server, int port) + { + connection.SetServer(server, port); + } + + public void Stop() + { + connection.Stop(); + } + + //Message package is assembled from string + public void NewMessage(string message) + { + Debug.Log ("You hit here"); + Debug.Log(message); + AddMessage(new InternalRobotMessage ("", message)); + Debug.Log ("last"); + } + + int maxMessageNumber = 100; + public void SetMaximumNumberOfMessages(int maxMessageNumber) + { + this.maxMessageNumber = maxMessageNumber; + + TrimChatMessages(); + } + + //Package up the components of the message into a List + //TODO: This should not be directly referencing Constants, this class should be as encapsulated as possible. + + object chatMessagesLock = new object(); + public IList GetChatMessages() + { + var copyOfChatMessages = new List(); + lock(chatMessagesLock) + { + Debug.Log ("Made it to chat messages"); + Debug.Log (chatMessages.Count); + foreach(var message in chatMessages) + { + var chatMessage = new RobotChatMessage(); + chatMessage.user = message.user; + chatMessage.message = message.message; + chatMessage.isCommand = message.isCommand; + chatMessage.isExecuting = message.isExecuting; + + copyOfChatMessages.Add(chatMessage); + } + } + + return copyOfChatMessages; + } + + //Get specific variables from broadcast source related to the robot. + object variablesLock = new object(); + public IDictionary GetVariables() + { + lock(variablesLock) + { + return new Dictionary(variables); + } + } + + IList commands = new List(); + object commandsLock = new object(); + + public IList GetCommands() + { + lock(commandsLock) + { + IList returnCommands = commands; + commands = new List(); + return returnCommands; + } + } + + public void SendMessage(string message) + { + connection.SendMessage(message); + } + + void AddMessage(InternalRobotMessage message) + { + lock(chatMessagesLock) + { + chatMessages.Add(message); + } + + TrimChatMessages(); + } + + void AddCommand(InternalRobotMessage internalRobotMessage) + { + lock(commandsLock) + { + var command = new RobotCommand(internalRobotMessage.commandDescription, internalRobotMessage.message); + commands.Add(command); + } + } + + void SetCommandIsExecuting(int commandId, bool isExecuting) + { + lock(commandsLock) + { + for(int i = 0; i < chatMessages.Count; i++) + { + var message = chatMessages[i]; + + if(message.commandId == commandId) + message.isExecuting = isExecuting; + else + message.isExecuting = false; + + chatMessages[i] = message; + } + } + } + + void SetVariable(string variable, string value) + { + lock(variablesLock) + { + variables[variable] = value; + } + } + + void TrimChatMessages() + { + lock(chatMessagesLock) + { + while(chatMessages.Count > maxMessageNumber) + chatMessages.RemoveAt(0); + } + } + + + // 20160603 rtharp + // moved out to RobotStuff, so we can have multiple connecctions + // but only one set of chat & variable + // since we only have the one display of them + //IList chatMessages = new List(); + //IDictionary variables = new Dictionary(); +} diff --git a/Assets/SerialComm/Scripts/RobotMessages.cs.meta b/Assets/SerialComm/Scripts/RobotMessages.cs.meta new file mode 100644 index 0000000..6c24cf0 --- /dev/null +++ b/Assets/SerialComm/Scripts/RobotMessages.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 858a879b66ce842ba83d5010739168c9 +timeCreated: 1480804241 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SerialComm/Scripts/TellyNetConnect.cs b/Assets/SerialComm/Scripts/TellyNetConnect.cs index dabce10..40453a9 100644 --- a/Assets/SerialComm/Scripts/TellyNetConnect.cs +++ b/Assets/SerialComm/Scripts/TellyNetConnect.cs @@ -2,58 +2,58 @@ using System.Collections; using System; using System.Threading; +using System.Collections.Generic; -public class TellyNetConnect : MonoBehaviour { +public class TellyNetWrapper : MonoBehaviour { - int messageId = 0; + public RobotMessages robotMessages; - // Connection to Tellynet Server - string tellynetServer = "ec2-54-191-54-225.us-west-2.compute.amazonaws.com"; - string tellynetPort = ":3000"; - string tellynetSocketProtocol = "ws://"; - - // Serial connection to Robot - public string portName = "/dev/cu.usbmodem12341"; - public int baudRate = 9600; - public int delayBeforeReconnecting = 1000; - public int maxUnreadMessages = 5; + public TellyNetWrapper(string server, int port) { + robotMessages = new RobotMessages (server, port); + } +} +public class TellyNetConnect : MonoBehaviour { + TellyNetWrapper tw; + SerialThread serialThread; + Thread thread; + // Use this for initialization IEnumerator Start () { + // Connection to Tellynet Server + //string tellynetServer = "ec2-54-191-54-225.us-west-2.compute.amazonaws.com"; + string tellynetServer = "127.0.0.1"; + int tellynetPort = 3000; + // Serial connection to Robot + //string portName = "/dev/cu.usbmodem12341"; + string portName = "/dev/tty.ArcBotics-DevB"; + int baudRate = 9600; + int delayBeforeReconnecting = 1000; + int maxUnreadMessages = 5; + tw = new TellyNetWrapper ("127.0.0.1", 8000); // Connect to the robot and start the serial thread - var serialThread = new SerialThread(portName, baudRate, delayBeforeReconnecting, maxUnreadMessages); - var thread = new Thread(new ThreadStart(serialThread.RunForever)); + serialThread = new SerialThread(portName, baudRate, delayBeforeReconnecting, maxUnreadMessages); + thread = new Thread(new ThreadStart(serialThread.RunForever)); thread.Start(); - // Connect to the Tellynet web socket server - WebSocket w = new WebSocket(new Uri(tellynetSocketProtocol + tellynetServer + tellynetPort)); - yield return StartCoroutine(w.Connect()); - - // Start Message Loop - while (true) - { - string reply = w.RecvString(); - string botReply = serialThread.ReadSerialMessage (); - - if (reply != null) { - serialThread.SendSerialMessage (reply); - Debug.Log (reply); - } - if (w.error != null) { - Debug.LogError ("Error: "+w.error); - break; - } - - if (botReply != null) { - Debug.Log("Bot replied: " + botReply); - } - - yield return 0; + yield return 0; + } + + void Update() { + var messages = tw.robotMessages.GetChatMessages(); + string botReply = serialThread.ReadSerialMessage (); + if (messages.Count > 0) { + string message = messages [0].message; + Debug.Log (message); + serialThread.SendSerialMessage (message); + } + + if (botReply != null) { + Debug.Log("Bot replied: " + botReply); } - w.Close(); } } diff --git a/Assets/SerialComm/Scripts/Tokenizer.cs b/Assets/SerialComm/Scripts/Tokenizer.cs new file mode 100644 index 0000000..cc5f8b1 --- /dev/null +++ b/Assets/SerialComm/Scripts/Tokenizer.cs @@ -0,0 +1,64 @@ +using System; + +class Tokenizer +{ + public Tokenizer(string str, char delimiter) + { + this.str = str; + this.delimiter = delimiter; + } + + public string GetToken() + { + string token = ""; + + if(!HasMore()) + throw new Exception(); + + while(position < str.Length && str[position] != delimiter) + { + token += str[position]; + position++; + } + + return token; + } + + public string GetString() + { + if(!HasMore()) + throw new Exception(); + + var stringBegin = position; + position = str.Length; + + return str.Substring(stringBegin); + } + + public string GetString(int nChars) + { + if(!HasMore()) + throw new Exception(); + + string returnString = str.Substring(position, nChars); + position += nChars; + return returnString; + } + + public bool HasMore() + { + SkipDelimiters(); + + return position < str.Length; + } + + void SkipDelimiters() + { + while(position < str.Length && str[position] == delimiter) + position++; + } + + string str; + char delimiter; + int position; +} diff --git a/Assets/SerialComm/Scripts/Tokenizer.cs.meta b/Assets/SerialComm/Scripts/Tokenizer.cs.meta new file mode 100644 index 0000000..03b8666 --- /dev/null +++ b/Assets/SerialComm/Scripts/Tokenizer.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 9ac90bd4871624965a3b36eacb34c07c +timeCreated: 1480804241 +licenseType: Free +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 1d8dc69..c186628 100644 Binary files a/ProjectSettings/ProjectSettings.asset and b/ProjectSettings/ProjectSettings.asset differ diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index 8c353d8..96b25e7 100755 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 5.1.2f1 +m_EditorVersion: 5.4.1f1 m_StandardAssetsVersion: 0