diff --git a/src/org/openlcb/MessageTypeIdentifier.java b/src/org/openlcb/MessageTypeIdentifier.java index d9bb402c..f3efdbec 100644 --- a/src/org/openlcb/MessageTypeIdentifier.java +++ b/src/org/openlcb/MessageTypeIdentifier.java @@ -59,10 +59,10 @@ public enum MessageTypeIdentifier { IdentifyEventsGlobal ( false, false, true, 0, 2, 11, 0, "IdentifyEventsGlobal"), LearnEvent ( false, true, true, 0, 1, 12, 0, "LearnEvent"), - ProducerConsumerEventReport ( false, true, true, 0, 1, 13, 0, "ProducerConsumerEventReport"), // This is also the CAN PCER-only - PCERfirst ( false, true, true, 0, 1, 13, 3, "PCERfirst"), // This is CAN only - PCERmiddle ( false, true, true, 0, 1, 13, 2, "PCERmiddle"), // This is CAN only - PCERlast ( false, true, true, 0, 1, 13, 1, "PCERlast"), // This is CAN only + ProducerConsumerEventReport ( false, true, true, 0, 1, 13, 0, "ProducerConsumerEventReport"), // This is the CAN PCER-no-Payload + PCERfirst ( false, true, true, 0, 3, 24, 2, "PCERfirst"), // This is CAN only + PCERmiddle ( false, true, true, 0, 3, 24, 1, "PCERmiddle"), // This is CAN only + PCERlast ( false, true, true, 0, 3, 24, 0, "PCERlast"), // This is CAN only TractionControlRequest ( true, false, false, 0, 1, 15, 3, "TractionControlRequest" ), TractionControlReply ( true, false, false, 0, 0, 15, 1, "TractionControlReply" ), diff --git a/src/org/openlcb/implementations/LocationServiceUtils.java b/src/org/openlcb/implementations/LocationServiceUtils.java new file mode 100644 index 00000000..81340887 --- /dev/null +++ b/src/org/openlcb/implementations/LocationServiceUtils.java @@ -0,0 +1,261 @@ +package org.openlcb.implementations; + +import org.openlcb.*; +import org.openlcb.implementations.throttle.Float16; + +import java.util.*; +import net.jcip.annotations.*; + +/** + * A set of utility functions and classes for + * working with Location Services messages + * + * @author Bob Jacobsen Copyright (C) 2025 + */ + +public class LocationServiceUtils { + + static public Content parse(Message inputMessage) { + + // first, check the message type + if (! (inputMessage instanceof ProducerConsumerEventReportMessage)) return null; + + ProducerConsumerEventReportMessage msg = (ProducerConsumerEventReportMessage) inputMessage; + byte[] payload = msg.getPayloadArray(); + + // check for minimal message length + if (payload.length < 8) return null; + + // check for right event ID + byte[] eid = msg.getEventID().getContents(); + if (eid[0] != 0x01 || eid[1] != 0x02) return null; + + // This is Location Services EWP, process it + + // process first section + int overallFlags = payload[0]<<8+payload[1]; + NodeID scannerReporting = new NodeID(new byte[]{ + eid[2], eid[3], eid[4], eid[5], eid[6], eid[7] + }); + NodeID scannedDevice = new NodeID(new byte[]{ + payload[2], payload[3], payload[4], payload[5], payload[6], payload[7] + }); + + List blocks = parseBlock(payload, 8, new ArrayList()); + + Content retval = new Content(scannerReporting, scannedDevice, overallFlags, blocks); + return retval; + } + + static private List parseBlock(byte[] payload, int offset, ArrayList list) { + + if (offset >= payload.length) return list; + + int length = payload[offset]; + + if (length == 0) { + list.add(new Block(Block.Type.RESERVED, new byte[0])); + } else { + // here we parse the block into something useful + Block.Type type = Block.Type.get((int) payload[offset+1]); + byte[] content = Arrays.copyOfRange(payload, offset+1, offset+1+length); + switch (type) { + case ANALOG : + list.add(new AnalogBlock(content)); + break; + + default: + list.add(new Block(type, content)); + } + } + + return parseBlock(payload, offset+length+1, list); + } + + /** + * Accessors for the parse contents + */ + @Immutable + static public class Content { + // The nodeID of the scanner making the report + NodeID scannerReporting; + public NodeID getScannerReporting() { return scannerReporting; } + + // The nodeID of the scanned device + NodeID scannedDevice; + public NodeID getScannedDevice() { return scannedDevice; } + + // The overall flags + int overallFlags; + public int getOverallFlags() {return overallFlags; } + + // The blocks of content + List blocks; + public List getBlocks() { return blocks; } + + public Content(NodeID scannerReporting, NodeID scannedDevice, int overallFlags, List blocks) { + this.scannerReporting = scannerReporting; + this.scannedDevice = scannedDevice; + this.overallFlags = overallFlags; + this.blocks = blocks; + } + + } + + /** + * Generic accessor for the contents of a Block + */ + @Immutable + static public class Block { + public enum Type { + RESERVED(0, "Reserved"), + READABLE(1, "Readable"), + RFID(2, "RFID"), + QR(3, "QR"), + RAILCOM(4, "RailCom"), + TRANSPONDING(5, "Transponding"), + POSITION(6, "Position"), + DCCADDRESS(7, "DccAddress"), + SETSPEED(8, "Set Speed"), + COMMANDEDSPEED(9, "Commanded Speed"), + ACTUALSPEED(10, "Actual Speed"), + ANALOG(11, "Analog"); + + Type(int code, String name) { + this.code = code; + this.name = name; + + getMap().put(code, this); + } + + int code; + String name; + + public String toString() { + return name; + } + + public static Type get(Integer type) { + return mapping.get(type); + } + + private static Map mapping; + private static Map getMap() { + if (mapping == null) + mapping = new java.util.HashMap(); + return mapping; + } + } + + Block(Type type, byte[] content) { + this.length = content.length; + this.type = type; + this.content = content; + } + + int length; + public int getLength() { return length; } + Type type; + public Type getType() { return type; } + byte[] content; + public byte[] getContent() { return content; } + } + + + /** + * Accessor for the contents of a Block with Readable (String) contents + */ + @Immutable + static public class ReadableBlock extends Block { + + ReadableBlock(byte[] content) { + super(Block.Type.READABLE, content); + + // [1] through end are the user string (UTF8) + try { + text = new String(Arrays.copyOfRange(content, 1, content.length),"UTF-8"); + } catch (java.io.UnsupportedEncodingException ex) { + text = ""; + } + } + + String text; + public String getText() { return text; } + } + + + /** + * Accessor for the contents of a Block with Analog contents + */ + @Immutable + static public class AnalogBlock extends Block { + public enum Unit { + UNKNOWN(0, "Unknown"), + VOLTS(1, "Volts"), + AMPERES(2, "Amperes"), + WATTS(3, "Watts"), + OHMS(4, "OHMS"), + DEGREESC(5, "Degrees C"), + SECONDS(6, "Seconds"), + METERS(7, "Meters"), + METERS2(8, "Meters^2"), + METERS3(9, "Meters^3"), + METERSPERSECOND(10, "Meters/Second"), + METERSPERSECOND2(11, "Meters/Second^2"), + KILOGRAMS(12, "Kilograms"), + NEWTONS(13, "Newtons"); + + Unit(int code, String name) { + this.code = code; + this.name = name; + + getMap().put(code, this); + } + + int code; + String name; + + public String toString() { + return name; + } + + public static Unit get(Integer unit) { + return mapping.get(unit); + } + + private static Map mapping; + private static Map getMap() { + if (mapping == null) + mapping = new java.util.HashMap(); + return mapping; + } + + } + + AnalogBlock(byte[] content) { + super(Block.Type.ANALOG, content); + + // decode contents of this block + + // [1], [2] are the Float16 value + value = new Float16(content[1], content[2]).getFloat(); + // [3] is the unit + unit = Unit.get((int)content[3]); + + // [4] through end are the user string (UTF8) + try { + text = new String(Arrays.copyOfRange(content, 4, content.length),"UTF-8"); + } catch (java.io.UnsupportedEncodingException ex) { + text = ""; + } + } + + Unit unit; + public Unit getUnit() { return unit; } + double value; + public double getValue() { return value; } + String text; + public String getText() { return text; } + } + +} \ No newline at end of file diff --git a/test/org/openlcb/can/MessageBuilderTest.java b/test/org/openlcb/can/MessageBuilderTest.java index eeda8fae..f42eedfc 100644 --- a/test/org/openlcb/can/MessageBuilderTest.java +++ b/test/org/openlcb/can/MessageBuilderTest.java @@ -251,11 +251,11 @@ public void testProducerConsumerEventReportMessageShortPayload() { Assert.assertEquals("count", 2, list.size()); CanFrame f0 = list.get(0); - Assert.assertEquals("header", toHexString(0x195B7123), toHexString(f0.getHeader())); + Assert.assertEquals("header", toHexString(0x19F16123), toHexString(f0.getHeader())); compareContent(event.getContents(), f0); CanFrame f1 = list.get(1); - Assert.assertEquals("header", toHexString(0x195B5123), toHexString(f1.getHeader())); + Assert.assertEquals("header", toHexString(0x19F14123), toHexString(f1.getHeader())); compareContent(data, f1); // check that the frames code back to the original Message @@ -275,15 +275,15 @@ public void testProducerConsumerEventReportMessageLongPayload() { Assert.assertEquals("count", 3, list.size()); CanFrame f0 = list.get(0); - Assert.assertEquals("header", toHexString(0x195B7123), toHexString(f0.getHeader())); + Assert.assertEquals("header", toHexString(0x19F16123), toHexString(f0.getHeader())); compareContent(event.getContents(), f0); CanFrame f1 = list.get(1); - Assert.assertEquals("header", toHexString(0x195B6123), toHexString(f1.getHeader())); + Assert.assertEquals("header", toHexString(0x19F15123), toHexString(f1.getHeader())); compareContent(new byte[]{1,2,3,4,5,6,7,8}, f1); CanFrame f2 = list.get(2); - Assert.assertEquals("header", toHexString(0x195B5123), toHexString(f2.getHeader())); + Assert.assertEquals("header", toHexString(0x19F14123), toHexString(f2.getHeader())); compareContent(new byte[]{9}, f2); // check that the frames code back to the original Message @@ -815,7 +815,7 @@ public void testBogusMti() { } @Test - public void testAccumulateSniipReply() { + public void testAccumulateSnipReply() { // start frame OpenLcbCanFrame frame = new OpenLcbCanFrame(0x123); frame.setHeader(0x19A08071); @@ -860,7 +860,7 @@ public void testAccumulateSniipReply() { } @Test - public void testAccumulateLongSniipReply() { + public void testAccumulateLongSnipReply() { // note short frame at end of MFG info // as seen from real Signal32 diff --git a/test/org/openlcb/implementations/LocationServiceUtilsTest.java b/test/org/openlcb/implementations/LocationServiceUtilsTest.java new file mode 100644 index 00000000..00bf8e1f --- /dev/null +++ b/test/org/openlcb/implementations/LocationServiceUtilsTest.java @@ -0,0 +1,171 @@ +package org.openlcb.implementations; + +import org.junit.*; + +import java.util.*; +import org.openlcb.*; + +/** + * Tests for the LocationServiceUtils class + @ + @ @author Bob Jacobsen (C) 2025 + */ +public class LocationServiceUtilsTest { + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + @Test + public void testParseNotEventID() { + + NodeID source = new NodeID("02.03.04.05.06.07"); + EventID eID = new EventID("01.01.02.03.04.05.06.07"); // not proper prefix + ArrayList list = bytes(Arrays.asList( + 0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x08, 0x09 // flags, being scanned + // no content blocks + )); + Message msg = new ProducerConsumerEventReportMessage(source, eID, list); + + LocationServiceUtils.Content result = LocationServiceUtils.parse(msg); + + Assert.assertNull(result); + } + + @Test + public void testParseWrongMessageType() { + + NodeID source = new NodeID("02.03.04.05.06.07"); + Message msg = new InitializationCompleteMessage(source); + + LocationServiceUtils.Content result = LocationServiceUtils.parse(msg); + + Assert.assertNull(result); + } + + // helper to make it easier to create ArrayList from a List + ArrayList bytes(List input ) { + ArrayList retval = new ArrayList(); + for (int index = 0; index < input.size(); index++) { + retval.add( (byte) (input.get(index)&0xFF) ); + } + return retval; + } + + @Test + public void testParseAbortShortLS() { + + NodeID source = new NodeID("02.03.04.05.06.07"); + EventID eID = new EventID("01.02.02.03.04.05.06.07"); // prefix, scanner + ArrayList list = new ArrayList(); + Message msg = new ProducerConsumerEventReportMessage(source, eID, list); + + LocationServiceUtils.Content result = LocationServiceUtils.parse(msg); + + Assert.assertNull(result); + } + + @Test + public void testParseEmptyLS() { + + NodeID source = new NodeID("02.03.04.05.06.07"); + EventID eID = new EventID("01.02.02.03.04.05.06.07"); // prefix, scanner + ArrayList list = bytes(Arrays.asList( + 0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x08, 0x09 // flags, being scanned + // no content blocks + )); + Message msg = new ProducerConsumerEventReportMessage(source, eID, list); + + LocationServiceUtils.Content result = LocationServiceUtils.parse(msg); + + Assert.assertNotNull(result); + + Assert.assertEquals(result.getOverallFlags(), 0x0100); + Assert.assertEquals(result.getScannerReporting(), new NodeID("02.03.04.05.06.07")); + Assert.assertEquals(result.getScannedDevice(), new NodeID("02.03.04.05.08.09")); + Assert.assertEquals(result.getBlocks().size(), 0); + } + + @Test + public void testParseEmptyLSwTrailingZeros() { + + NodeID source = new NodeID("02.03.04.05.06.07"); + EventID eID = new EventID("01.02.02.03.04.05.06.07"); // prefix, scanner + ArrayList list = bytes(Arrays.asList( + 0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x08, 0x09, // flags, being scanned + 0x00, 0x00 // zero-length blocks + )); + Message msg = new ProducerConsumerEventReportMessage(source, eID, list); + + LocationServiceUtils.Content result = LocationServiceUtils.parse(msg); + + Assert.assertNotNull(result); + + Assert.assertEquals(result.getOverallFlags(), 0x0100); + Assert.assertEquals(result.getScannerReporting(), new NodeID("02.03.04.05.06.07")); + Assert.assertEquals(result.getScannedDevice(), new NodeID("02.03.04.05.08.09")); + Assert.assertEquals(result.getBlocks().size(), 2); + + // check the 1st block's contents + LocationServiceUtils.Block block = result.getBlocks().get(0); + Assert.assertEquals(block.getLength(), 0); + Assert.assertEquals(block.getType(), LocationServiceUtils.Block.Type.RESERVED); + + // check the 2nd block's contents + block = result.getBlocks().get(1); + Assert.assertEquals(block.getLength(), 0); + Assert.assertEquals(block.getType(), LocationServiceUtils.Block.Type.RESERVED); + + } + + @Test + public void testParseTypicalBoosterContent() { + + NodeID source = new NodeID("02.03.04.05.06.07"); + EventID eID = new EventID("01.02.02.03.04.05.06.07"); // prefix, scanner + ArrayList list = bytes(Arrays.asList( + 0x10, 0x00, 0x02, 0x03, 0x04, 0x05, 0x08, 0x09, // flags, being scanned + + 0x11, 0x0B, 0x4B, 0xB1, 0x01, 0x54, 0x72, 0x61, + 0x63, 0x6B, 0x20, 0x56, 0x6F, 0x6C, 0x74, 0x61, + 0x67, 0x65, + 0x11, 0x0B, 0x40, 0x42, 0x02, 0x54, + 0x72, 0x61, 0x63, 0x6B, 0x20, 0x43, 0x75, 0x72, + 0x72, 0x65, 0x6E, 0x74 + + )); + Message msg = new ProducerConsumerEventReportMessage(source, eID, list); + + LocationServiceUtils.Content result = LocationServiceUtils.parse(msg); + + Assert.assertNotNull(result); + + Assert.assertEquals(result.getOverallFlags(), 0x1000); + Assert.assertEquals(result.getScannerReporting(), new NodeID("02.03.04.05.06.07")); + Assert.assertEquals(result.getScannedDevice(), new NodeID("02.03.04.05.08.09")); + Assert.assertEquals(result.getBlocks().size(), 2); + + // check the 1st block's contents + LocationServiceUtils.Block block = result.getBlocks().get(0); + Assert.assertEquals(block.getLength(), 0x11); + Assert.assertEquals(block.getType(), LocationServiceUtils.Block.Type.ANALOG); + Assert.assertEquals(((LocationServiceUtils.AnalogBlock) block).getUnit(), LocationServiceUtils.AnalogBlock.Unit.VOLTS); + Assert.assertEquals(((LocationServiceUtils.AnalogBlock) block).getText(), "Track Voltage"); + Assert.assertEquals(((LocationServiceUtils.AnalogBlock) block).getValue(), 15.382812, 0.0001); + + + // check the 2nd block's contents + block = result.getBlocks().get(1); + Assert.assertEquals(block.getLength(), 0x11); + Assert.assertEquals(block.getType(), LocationServiceUtils.Block.Type.ANALOG); + Assert.assertEquals(((LocationServiceUtils.AnalogBlock) block).getUnit(), LocationServiceUtils.AnalogBlock.Unit.AMPERES); + Assert.assertEquals(((LocationServiceUtils.AnalogBlock) block).getText(), "Track Current"); + Assert.assertEquals(((LocationServiceUtils.AnalogBlock) block).getValue(), 2.12890625, 0.0001); + + } + +}