|
Thanks Luc I'll give it a shot
"We can't stop here - this is bat country" - Hunter S Thompson - RIP
|
|
|
|
|
Hi Luc, I tried it with ASCII and no different I'll show you the entire string this time ( I abridged it in the original post )
"ENAME\u0005LMSPiJSON\u00049000UUID$0b5a576d-a276-4920-a9e8-134c41133102VERS\u00057.9.2"
Breaking it down
ENAME = LMSPi -- my server hostname
JSON = 9000 -- my server port
UUID = self explanatory
VERS = software version
If I print the byte array one byte at a time and convert it to a char I get
for (int i = 0; i < BytesReceived.Length; ++i)
{
char x = (char)BytesReceived[i];
Console.Write(x);
}
ENAMELMSPiJSON9000UUID$0b5a576d-a276-4920-a9e8-134c41133102VERS7.9.2
which is more like what I expect
Thanks very much for your help
"We can't stop here - this is bat country" - Hunter S Thompson - RIP
modified 1-Jul-20 13:28pm.
|
|
|
|
|
So it is a series of key-value pairs, where each value (except the GUID that has a fixed length) would need either a length or a separator; in this case a length is used, i.e. the 6 characters "\u000X" stand for a the length of X for the value that follows; normally a small length could be stored in a single byte.
As you receive the lengths in this escape format no matter what encoding is used to receive them (so you told me), it suggests something might be wrong at the transmitter, not the receiver.
ALERT
Wait a minute, where are you seeing those unexpected strings exactly?
You are aware Visual Studio sometimes uses escapes, quotes, etc in its immediate window and maybe elsewhere too. Use Console.WriteLine to make sure, and/or apply string.Length and count the characters manually to compare!!!
modified 5-Jul-20 9:29am.
|
|
|
|
|
Hi Luc it was VS misleading me here is what I ended up doing to parse the values
Response from server - there are non printable characters in the response which I can't get to display here which contain the data lengths
ENAMELMSPiJSON9000UUID$0b5a576d-a276-4920-a9e8-134c41133102VERS7.9.2;
ResponseColumn ParseResponseString(string FieldName,string ResponseString)
{
ResponseColumn rc = new ResponseColumn();
int pos = ResponseString.IndexOf(FieldName);
string ColumnName = ResponseString.Substring(pos,5);
int DataLen = Convert.ToInt32(ColumnName[4]);
pos+= 5;
string ColumnValue = ResponseString.Substring(pos,DataLen);
rc.ColumnName = FieldName;
rc.ColumnValue = ColumnValue;
return rc;
}
public class ResponseColumn
{
public string ColumnName {get;set;}
public string ColumnValue{get;set;}
}
and use it thus
ResponseColumn rcPortNumber = ParseResponseString("JSON",ResponseString);
ResponseColumn rcServerName = ParseResponseString("NAME",ResponseString);
ResponseColumn rcServerVer = ParseResponseString("VERS",ResponseString);
Thanks for all your help
"We can't stop here - this is bat country" - Hunter S Thompson - RIP
modified 2-Jul-20 7:34am.
|
|
|
|
|
You're welcome.
Two comments though:
1. you completely replaced the content of this message; I got an e-mail when you first created it. And obviously not when you replaced all of it, so it took a while before I accidentally noticed the new content. You should not replace/delete messages on CodeProject, you can create new ones, or add to or slightly edit existing ones.
2. your code is fine with the current data. It has a few weaknesses in general: it assumes field names are always 4 chars (see constants 4 and 5), and it locates field names by a string search, which could go wrong when one field name happens to appear as (part of) another field's value.
The universal way to handle this would be to scan the entire line and build a Dictionary<string,string> holding all the key-value pairs in the line; that too would rely on some restrictions on field names and/or values (e.g. field names are uppercase letters only, and field value lengths must be less than 'A').
Cheers
modified 5-Jul-20 9:45am.
|
|
|
|
|
Hi Luc, just some answers to your observations
it assumes field names are always 4 chars
they have been 4 chars for the last 25 years
and it locates field names by a string search,
How else could it be done ?
which could go wrong when one field name happens to appear as (part of) another field's value.
The fields always end with a data length
Thanks again
"We can't stop here - this is bat country" - Hunter S Thompson - RIP
modified 6-Jul-20 4:27am.
|
|
|
|
|
Let's assume your server was renamed ZVERSA (or MYJSONMACHINE) and you start getting the string
ENAMEZVERSAJSON9000UUID$0b5a576d-a276-VERS-a9e8-134c41133102VERS7.9.2
^^^^
And now you try to get the version (or the json) field...
|
|
|
|
|
I agree with you totally Luc but I can't see a better way of parsing the string - if you have any suggestions please post them
"We can't stop here - this is bat country" - Hunter S Thompson - RIP
|
|
|
|
|
Sure, here is some C#-like pseudocode that scans the line from left to right:
private Dictionary<string,string> parseLine(line) {
dict=New Dictionary<string,string>;
while not at end of line
key=readString(4)
length=readInt(1)
value=readString(length)
dict[key]=value
repeat
return dict
}
Afterwards you can easily access the values like so: dict["VERS"] ; beware they are all stored as strings, when a number is required, you'd have to apply int.TryParse() or similar while pulling them from the dict.
|
|
|
|
|
Hi Luc, I was just going to post what I've done and I saw your reply - anyway here it is
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LMS
{
public class ParseLMSResponse
{
private const string NAME = "ENAME";
private const string PORT = "JSON";
private const string UUID = "UUID";
private const string VERS = "VERS";
private byte[] ResponseBytes { get; set;}
private Dictionary<string, string> KeyValuePairs { get; set; }
public ParseLMSResponse(byte[] ResponseBytes)
{
this.ResponseBytes = ResponseBytes;
this.GetKeyValuePairs();
}
private void GetKeyValuePairs()
{
string keyname = "";
string keyvalue = "";
int i = 0;
byte b;
int DataLen = 0;
KeyValuePairs = new Dictionary<string, string>();
while (i < ResponseBytes.Length)
{
keyname = "";
keyvalue = "";
b = ResponseBytes[i];
while (b >= 65 && b <= 90)
{
keyname += Convert.ToChar(b);
++i;
b = ResponseBytes[i];
}
DataLen = Convert.ToInt32(b);
++i;
keyvalue = Encoding.UTF8.GetString(ResponseBytes, i, DataLen);
KeyValuePairs.Add(keyname, keyvalue);
i += DataLen;
}
}
public string GetHostName
{
get
{
return GetValue(NAME);
}
}
public string GetUUID
{
get
{
return GetValue(UUID);
}
}
public string GetVersion
{
get
{
return GetValue(VERS);
}
}
public int GetPortNumber
{
get
{
return Convert.ToInt32(GetValue(PORT));
}
}
private string GetValue(string Name)
{
string RetVal = "";
KeyValuePairs.TryGetValue(Name, out RetVal);
return RetVal;
}
}
}
A much better way methinks - thanks for your help
"We can't stop here - this is bat country" - Hunter S Thompson - RIP
|
|
|
|
|
OK, you've got it!
|
|
|
|
|
Hi Pete,
while your last message keeps showing up and disappearing again, I changed your code a bit to be more concise and better match C# conventions:
using System;
using System.Collections.Generic;
using System.Text;
namespace LMS {
public class LMSResponseParser {
private byte[] responseBytes;
private Dictionary<string, string> dict;
public LMSResponseParser(byte[] responseBytes) {
this.responseBytes=responseBytes;
}
public string HostName {
get {
return getValue("ENAME");
}
}
public string UUID {
get {
return getValue("UUID");
}
}
public string Version {
get {
return getValue("VERS");
}
}
public int PortNumber {
get {
return Convert.ToInt32(getValue("PORT"));
}
}
private string getValue(string key) {
try {
if (dict==null) parse();
return dict[key];
} catch (Exception exc) {
throw new ApplicationException(
"Error parsing LMS response '"+responseBytes+"'", exc);
}
}
private void parse() {
dict=new Dictionary<string, string>();
for (int i = 0; i<responseBytes.Length;) {
string key = "";
byte b = responseBytes[i++];
while (b>='A'&&b<='Z') {
key+=(char)b;
b=responseBytes[i++];
}
int len = b;
string value = Encoding.ASCII.GetString(responseBytes, i, len);
i+=len;
dict[key]=value;
}
}
}
}
What I changed:
- usage slightly different: create an LMSResponseParser then get its properties; no reuse, parsing the next LMSresponse will require a new parser object!
- public variables, properties, method names start with uppercase, locals/privates don't.
- class names don't start with a verb, think "object" not "action".
- property names don't start with a verb (except sometimes "Is"); think "characteristic".
(OTOH Java does not have properties, there one uses getXxx() and setXxx() methods.)
- no internal properties (such as your ResponseBytes), there was no need to make them properties.
- no advance declaration of local variables, declare on first use instead (C# isn't C !)
- lazy parsing: only parse (once) when some result is actually asked for.
- error handling: what if an invalid set of bytes is given? or one of the keywords isn't present while trying to get its value? The above code catches it all and throws an exception.
PS: I did not test, there might be minor mistakes...
PS2: my way of placing { at the right rather than on the next line isn't what most people do, however I prefer it as it is more vertically compact.
|
|
|
|
|
Hi Luc, why did you remove the const strings ?
"We can't stop here - this is bat country" - Hunter S Thompson - RIP
|
|
|
|
|
I would surely use const statements at the start of a class if they were public, or were used more than once throughout the class, or the right values isn't sure yet and will probably need a change (that would deserve a comment of course!).
Your const names seem not to satisfy any of these conditions, and then I prefer to use their values right where they are needed, making it all more readable in my view.
And yes I made a little mistake in doing so, "PORT" should be "JSON" in the PortNumber property...
OTOH in your earlier code, where you had 4 and 5 for fieldname length and fieldname length + 1 I would have used either a const, or a variable set equal to fieldName.Length; that makes it more readable (less "magic"), as does my 'A' and 'Z' replacing your 65 and 90.
I prefer code to be both readable and compact (even when that may sound in conflict); both characteristics make it easier to understand and maintain code, leading to fewer errors over time.
|
|
|
|
|
I don't know why my message is disappearing but hey ho ( hamsters )
I think it just comes down to coding style and preference my old tutor used to tell me "get it working and then tidy it up" - I've been coding professionally for 40+ years so obviously I've developed my own style - I don't follow Microsoft's or anyone else's coding conventions unless I agree with them - but that's just me. Nice liaising with you.
"We can't stop here - this is bat country" - Hunter S Thompson - RIP
modified 6-Jul-20 13:42pm.
|
|
|
|
|
Of course we all have preferences and a personal style, and that is OK if the code is touched only by very few people; it may get quite messy very soon when many individuals work on the same project.
So I don't mind conventions and am willing to adhere to them as long as they don't hamper me (example the casing rules); when they do I need some convincing
IMO the verb/noun issue in class/method/property names is much more than a convention, it really helps you to better perform in OOP.
|
|
|
|
|
Fur inuff we shall agree to differ
"We can't stop here - this is bat country" - Hunter S Thompson - RIP
modified 6-Jul-20 16:32pm.
|
|
|
|
|
I am trying to understand how I would convert / combine two integers into a floating point value. For example the numbers [17352,9147] = 400.28. Could someone share an example of how this would be done? I am reading these two integer values from a Modbus device in holding registers.
modified 1-Jul-20 11:53am.
|
|
|
|
|
Hi,
one float takes 4 bytes of memory; two shorts would also take 4 bytes. One way to convert one into the other is by mapping them at the same location in memory.
I know of two ways to achieve this:
(1) the techniques available for interacting with unmanaged code and data offer the right tools. Here is how:
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Explicit)]
struct ShortsOverlayFloat {
[FieldOffset(0)]
public short I1;
[FieldOffset(2)]
public short I2;
[FieldOffset(0)]
public float F;
}
public float shortsToFloat(short i1, short i2) {
ShortsOverlayFloat S;
S.F=0;
S.I1=i1;
S.I2=i2;
return S.F;
}
and now
float f = shortsToFloat(9147, 17352);
will yield 400.2791
(2) the more orthodox way is by using some methods of the BitConverter class, such as BitConverter.ToSingle ; I haven't checked but I expect they use the above technique behind the scenes. That class uses somewhat different type names, don't let that confuse you!
PS: it is IEEE754, not 745.
modified 30-Jun-20 23:51pm.
|
|
|
|
|
Thank you. I will give this a shot today and see how I make out. Thank you for pointing out my typo, I wish I could figure out how to edit the topic title.
|
|
|
|
|
You're welcome.
BTW: You can click the "Edit" widget of your original post, and modify title and/or content.
|
|
|
|
|
Trying to work through this, I have attempted the following, but get a compile error.
int val1 = 17352;
int val2 = 9147;
byte[] buffer = { (byte)val1, (byte)val2 };
for (int n = 0; n < buffer.Length; n += 2)
{
short sample = (short)BitConverter.ToSingle(buffer, n);
MessageBox.Show(sample.ToString());
}
The exception that I currently get is: Could you help a bit more? I was able to fix the title......
Quote: System.ArgumentException
HResult=0x80070057
Message=Destination array is not long enough to copy all the items in the collection. Check array index and length.
Source=mscorlib
StackTrace:
at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
at System.BitConverter.ToSingle(Byte[] value, Int32 startIndex)
at WindowsFormsApp2.Form1.tmr_Modbus_Conn_Tick(Object sender, EventArgs e) in C:\Users\Justin Fehl\Documents\Visual Studio 2019\Projects\ModBus Development\ModbusTest2.cs:line 94
at System.Windows.Forms.Timer.OnTick(EventArgs e)
at System.Windows.Forms.Timer.TimerNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(IntPtr dwComponentID, Int32 reason, Int32 pvLoopData)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
at System.Windows.Forms.Application.Run(Form mainForm)
at WindowsFormsApp2.Program.Main() in C:\Users\Justin Fehl\Documents\Visual Studio 2019\Projects\ModBus Development\Modbus Development 6-26-2020\WindowsFormsApp2\Program.cs:line 19
|
|
|
|
|
The error message is pretty clear when it says
Destination array is not long enough
Apparently this line is wrong:
byte[] buffer = { (byte)val1, (byte)val2 };
How many bytes would that be?
and what is the purpose of that for loop?
It seems you didn't understand the conversion at all, why would you need ToSingle twice if you only are trying to get one Single? (Single and float are the same thing).
|
|
|
|
|
I will scratch my head awhile............
|
|
|
|
|
I was able to get this to work:
var byteval1 = BitConverter.GetBytes(val1);
var byteval2 = BitConverter.GetBytes(val2);
byte[] temp2 = new byte[4];
temp2[0] = byteval1[0];
temp2[1] = byteval1[1];
temp2[2] = byteval2[0];
temp2[3] = byteval2[1];
float myFloat = System.BitConverter.ToSingle(temp2, 0);
Console.WriteLine(myFloat);
Console.ReadLine();
|
|
|
|