001package jmri.jmrix.dcc4pc; 002 003import java.util.ArrayList; 004import java.util.Arrays; 005import java.util.LinkedHashMap; 006import java.util.List; 007import java.util.Map; 008 009/** 010 * Works out which locomotives are in which block from Raw encoding packets. 011 * <p> 012 * Channel 1 is a broadcast: every decoder in a block sends its address in the 013 * same time slot, so with two locos the ADR_LOW halves collide into invalid 014 * codes. Channel 2 is only used by the decoder the DCC packet was addressed 015 * to, so a valid channel 2 reply on an input right after a packet to address 016 * X means X is in that block. A clean ADR_HIGH + ADR_LOW pair on channel 1 is 017 * also accepted, which covers a lone loco the command station isn't 018 * addressing. 019 * <p> 020 * A loco stays in a block until the block reads unoccupied, or until it has 021 * been seen in another block and not in this one for {@link #MOVE_MILLIS}. 022 * There is no plain timeout: a stopped decoder can go many seconds without a 023 * channel 2 reply. 024 * <p> 025 * A loco that is still present and heard again is reported as a 026 * {@link Change#refresh refresh}, at most every {@link #REFRESH_MILLIS}, so 027 * its "last seen" time keeps up without flooding the GUI. 028 * 029 * @author swalton00 Copyright (C) 2026 030 */ 031public class Dcc4PcBlockLocos { 032 033 /** How long a loco seen elsewhere keeps its old block, to allow for bridging a gap. */ 034 public static final long MOVE_MILLIS = 2000; 035 036 /** Shortest time between refreshes for a loco that stays in a block. */ 037 public static final long REFRESH_MILLIS = 1000; 038 039 /** 040 * RailCom from a loco on an input proves the input is occupied, whatever 041 * the debounced state says. The debounced state lags the raw one (a block 042 * becomes occupied after 2 packets, and unoccupied only after 300 ms), so 043 * a loco entering a block can be heard before its block reads occupied. 044 * An unoccupied reading therefore only removes locos not heard on that 045 * input for this long, which matches the unoccupied hold in the sensor 046 * manager: a block that has really been empty that long has had no RailCom. 047 */ 048 public static final long HEARD_PROVES_OCCUPIED_MILLIS = 300; 049 050 /** Added to an address to mark it as a long (14-bit) address. */ 051 public static final int LONG = 0x10000; 052 053 /** A loco added to or removed from a block, or heard again in it. */ 054 public static class Change { 055 056 /** Input number, from 1. */ 057 public final int input; 058 /** Address, with {@link #LONG} added for a long address. */ 059 public final int address; 060 public final boolean added; 061 /** True if the loco was already in the block and has been heard again; {@link #added} is false. */ 062 public final boolean refresh; 063 /** Why, for the log. */ 064 public final String how; 065 066 Change(int input, int address, boolean added, String how) { 067 this(input, address, added, false, how); 068 } 069 070 private Change(int input, int address, boolean added, boolean refresh, String how) { 071 this.input = input; 072 this.address = address; 073 this.added = added; 074 this.refresh = refresh; 075 this.how = how; 076 } 077 078 static Change heard(int input, int address, String how) { 079 return new Change(input, address, false, true, how); 080 } 081 082 @Override 083 public String toString() { 084 return (refresh ? "=" : added ? "+" : "-") + name(address) + " on input " + input + " (" + how + ")"; 085 } 086 } 087 088 private final int[] inputs; 089 /** Per enabled input: address -> last time evidence was seen. */ 090 private final List<Map<Integer, Long>> seen = new ArrayList<>(); 091 /** Per enabled input: address -> when it was last added or refreshed. */ 092 private final List<Map<Integer, Long>> reported = new ArrayList<>(); 093 private final Integer[] adrHigh; 094 095 /** 096 * @param inputs the enabled inputs, numbered from 1, in the decoder's order 097 */ 098 public Dcc4PcBlockLocos(int[] inputs) { 099 this.inputs = inputs.clone(); 100 adrHigh = new Integer[inputs.length]; 101 for (int i = 0; i < inputs.length; i++) { 102 seen.add(new LinkedHashMap<>()); 103 reported.add(new LinkedHashMap<>()); 104 } 105 } 106 107 /** 108 * @param input input number, from 1 109 * @return the locos in that block, in the order they were first seen 110 */ 111 public List<Integer> locos(int input) { 112 int i = indexOf(input); 113 return i < 0 ? new ArrayList<>() : new ArrayList<>(seen.get(i).keySet()); 114 } 115 116 /** Forget everything, as after a resync: the next packets rebuild it. */ 117 public void clear() { 118 Arrays.fill(adrHigh, null); 119 for (Map<Integer, Long> m : seen) { 120 m.clear(); 121 } 122 for (Map<Integer, Long> m : reported) { 123 m.clear(); 124 } 125 } 126 127 /** 128 * Apply one decoded packet. 129 * 130 * @param p the packet 131 * @param states the input states to use, debounced, in the decoder's order 132 * @param now the time, in milliseconds 133 * @return what changed 134 */ 135 public List<Change> apply(Dcc4PcRawDecoder.Packet p, int[] states, long now) { 136 List<Change> changes = new ArrayList<>(); 137 if (p.kind == Dcc4PcRawDecoder.Kind.OVERFLOW) { 138 // spec 9.3: don't pair an ADR_HIGH from before an overflow with an ADR_LOW after it 139 Arrays.fill(adrHigh, null); 140 return changes; 141 } 142 if (p.kind != Dcc4PcRawDecoder.Kind.DCC) { 143 return changes; 144 } 145 for (int i = 0; i < inputs.length; i++) { 146 if (states[i] == Dcc4PcRawDecoder.STATE_UNOCCUPIED) { 147 adrHigh[i] = null; 148 for (Map.Entry<Integer, Long> e : new ArrayList<>(seen.get(i).entrySet())) { 149 if (now - e.getValue() < HEARD_PROVES_OCCUPIED_MILLIS) { 150 continue; // heard here just now, so the block is occupied 151 } 152 seen.get(i).remove(e.getKey()); 153 reported.get(i).remove(e.getKey()); 154 changes.add(new Change(inputs[i], e.getKey(), false, "block unoccupied")); 155 } 156 } 157 } 158 int dest = mobileAddress(p.dcc); 159 for (Dcc4PcRawDecoder.InputData d : p.railCom) { 160 int i = indexOf(d.input); 161 if (i < 0 || d.data == null) { 162 continue; 163 } 164 int[] ch1 = Arrays.copyOfRange(d.data, 0, Math.min(2, d.data.length)); 165 int[] ch2 = Arrays.copyOfRange(d.data, ch1.length, d.data.length); 166 // Only fresh data counts as evidence. A duplicate just says "the same bytes as 167 // the Nth previous packet", and those bytes can be another loco's reply: on the 168 // layout, packets to short 9 and long 9999 (idle throttle slots, no such locos) 169 // were "answered" this way, even when the earlier packet went to the same 170 // address. Fresh replies come often enough to find every loco. 171 if (dest >= 0 && d.duplicateOf == 0 && hasValidReply(ch2)) { 172 see(i, dest, now, "channel 2 reply " + hex(d.data) + " after DCC " + hex(p.dcc), changes); 173 } 174 if (ch1.length == 2) { 175 int a = decode(ch1[0]); 176 int b = decode(ch1[1]); 177 if (a < 0 || b < 0) { 178 // a collision or noise: an ADR_HIGH held can't pair with what follows 179 adrHigh[i] = null; 180 continue; 181 } 182 int id = a >> 2; 183 int payload = ((a & 0x03) << 6) | b; 184 if (id == 1) { // ADR_HIGH 185 adrHigh[i] = payload; 186 } else if (id == 2 && adrHigh[i] != null) { // ADR_LOW 187 int address = address(adrHigh[i], payload); 188 if (address > 0) { 189 see(i, address, now, "channel 1 address " + hex(ch1) + " (ADR_HIGH " + Integer.toHexString(adrHigh[i]) + ")", changes); 190 } 191 } 192 } 193 } 194 return changes; 195 } 196 197 private void see(int i, int address, long now, String how, List<Change> changes) { 198 if (seen.get(i).put(address, now) == null) { 199 reported.get(i).put(address, now); 200 changes.add(new Change(inputs[i], address, true, how)); 201 } else if (now - reported.get(i).getOrDefault(address, 0L) >= REFRESH_MILLIS) { 202 reported.get(i).put(address, now); 203 changes.add(Change.heard(inputs[i], address, how)); 204 } 205 for (int j = 0; j < inputs.length; j++) { 206 Long last = j == i ? null : seen.get(j).get(address); 207 if (last != null && now - last > MOVE_MILLIS) { 208 seen.get(j).remove(address); 209 reported.get(j).remove(address); 210 changes.add(new Change(inputs[j], address, false, "seen on input " + inputs[i])); 211 } 212 } 213 } 214 215 private int indexOf(int input) { 216 for (int i = 0; i < inputs.length; i++) { 217 if (inputs[i] == input) { 218 return i; 219 } 220 } 221 return -1; 222 } 223 224 /** 225 * @param address an address as used here 226 * @return it as text, such as "9328(L)" or "3(S)" 227 */ 228 public static String name(int address) { 229 return (address & ~LONG) + ((address & LONG) != 0 ? "(L)" : "(S)"); 230 } 231 232 /** 233 * The locomotive address a DCC packet is sent to. 234 * 235 * @param dcc the packet bytes, without the error byte or with it 236 * @return the address, with {@link #LONG} added for a long address, or -1 237 * for a broadcast, idle, accessory or malformed packet 238 */ 239 public static int mobileAddress(int[] dcc) { 240 if (dcc == null || dcc.length == 0) { 241 return -1; 242 } 243 int b0 = dcc[0] & 0xFF; 244 if (b0 >= 1 && b0 <= 127) { 245 return b0; 246 } 247 if (b0 >= 192 && b0 <= 231 && dcc.length >= 2) { 248 return LONG | ((b0 & 0x3F) << 8) | (dcc[1] & 0xFF); 249 } 250 return -1; 251 } 252 253 /** 254 * Combine channel 1 ADR_HIGH and ADR_LOW payloads (RCN-217 5.2.1). 255 * 256 * @return the address, with {@link #LONG} added for a long address, or -1 257 * if ADR_HIGH isn't a valid address pattern 258 */ 259 static int address(int high, int low) { 260 if (high == 0) { 261 return low & 0x7F; 262 } 263 if ((high & 0xC0) == 0x80) { 264 return LONG | ((high & 0x3F) << 8) | (low & 0xFF); 265 } 266 if ((high & 0xF0) == 0x60) { 267 return low & 0x7F; // consist address 268 } 269 return -1; 270 } 271 272 /** 273 * True if channel 2 holds at least one well-formed datagram or an 274 * ACK/NACK/BUSY. 275 */ 276 static boolean hasValidReply(int[] ch2) { 277 int pos = 0; 278 while (pos < ch2.length) { 279 int first = decode(ch2[pos]); 280 if (first == SPECIAL) { 281 return true; 282 } 283 if (first < 0) { 284 pos++; 285 continue; 286 } 287 int id = first >> 2; 288 int symbols = (id == 3 || id == 7) ? 3 : (id >= 8 && id <= 11) ? 6 : 2; 289 if (pos + symbols > ch2.length) { 290 return false; 291 } 292 boolean valid = true; 293 for (int k = 1; k < symbols; k++) { 294 if (decode(ch2[pos + k]) < 0) { 295 valid = false; 296 break; 297 } 298 } 299 if (valid) { 300 return true; 301 } 302 pos += symbols; 303 } 304 return false; 305 } 306 307 static final int INVALID = -1; 308 /** ACK, NACK or BUSY. */ 309 static final int SPECIAL = -2; 310 311 /** ENCODE[value] is the RailCom 4-of-8 line code for a six-bit value. */ 312 private static final int[] ENCODE = { 313 0xAC, 0xAA, 0xA9, 0xA5, 0xA3, 0xA6, 0x9C, 0x9A, 314 0x99, 0x95, 0x96, 0x93, 0x8E, 0x8D, 0x8B, 0xB1, 315 0xB2, 0xB4, 0xB8, 0x74, 0x72, 0x6C, 0x6A, 0x69, 316 0x66, 0x65, 0x63, 0x5C, 0x5A, 0x59, 0x55, 0x56, 317 0x53, 0x4E, 0x4D, 0x4B, 0x47, 0x71, 0xE8, 0xE4, 318 0xE2, 0xD1, 0xC9, 0xC5, 0xD8, 0xD4, 0xD2, 0xCA, 319 0xC6, 0xCC, 0x78, 0x17, 0x1B, 0x1D, 0x1E, 0x2E, 320 0x36, 0x3A, 0x27, 0x2B, 0x2D, 0x35, 0x39, 0x33}; 321 322 private static final int[] DECODE = new int[256]; 323 324 static { 325 Arrays.fill(DECODE, INVALID); 326 for (int v = 0; v < ENCODE.length; v++) { 327 DECODE[ENCODE[v]] = v; 328 } 329 DECODE[0x0F] = SPECIAL; // ACK 330 DECODE[0xF0] = SPECIAL; // ACK 331 DECODE[0x3C] = SPECIAL; // NACK 332 DECODE[0xE1] = SPECIAL; // BUSY 333 } 334 335 /** @return the bytes as hex, for the log */ 336 static String hex(int[] bytes) { 337 if (bytes == null) { 338 return "?"; 339 } 340 StringBuilder sb = new StringBuilder(); 341 for (int b : bytes) { 342 if (sb.length() > 0) { 343 sb.append(' '); 344 } 345 sb.append(String.format("%02X", b & 0xFF)); 346 } 347 return sb.toString(); 348 } 349 350 /** 351 * @return the six-bit value of a 4-of-8 code, {@link #SPECIAL} or 352 * {@link #INVALID} 353 */ 354 static int decode(int b) { 355 return DECODE[b & 0xFF]; 356 } 357}