001package jmri.implementation; 002 003import jmri.ProgListener; 004import jmri.Programmer; 005import jmri.jmrix.AbstractProgrammerFacade; 006import org.slf4j.Logger; 007import org.slf4j.LoggerFactory; 008 009/** 010 * Programmer facade for accessing CVs that require one or more "index CVs" 011 * to have specific values before doing the final read or write operation. 012 * <p> 013 * Currently supports direct access to CVs (the usual style), operations where 014 * one index CV (called PI, for primary index) must have a specific value first, 015 * and operations where two index CVs (called PI and SI, for secondary index) 016 * must have a specific value first. 017 * <p> 018 * Accepts two different address formats so that the CV addresses can be 019 * written in the same style as the decoder manufacturer's documentation: 020 * <ul> 021 * <li>If cvFirst is true: 022 * <ul> 023 * <li> 123 Do read or write directly to CV 123; this allows unindexed CVs to go through 024 * <li> 123.11 Writes 11 to PI, the index CV, then does the final read or write to CV 123 025 * <li> 123.11.12 Writes 11 to the first index CV, then 12 to the second index CV, 026 * then does the final read or write to CV 123 027 * </ul> 028 * <li>If cvFirst is false: 029 * <ul> 030 * <li> 123 Do read or write directly to CV 123; this allows unindexed CVs to go through 031 * <li> 11.123 Writes 11 to the first index CV, then does the final read or write to CV 123 032 * <li> 11.12.123 Writes 11 to the first index CV, then 12 to the second index CV, 033 * then does the final read or write to CV 123 034 * </ul> 035 * </ul> 036 * QSI decoders generally use the 1st format, and ESU LokSound decoders the second. 037 * <p> 038 * The specific CV numbers for PI and SI are provided when constructing the object. 039 * They can be read from a decoder definition file by e.g. {@link jmri.implementation.ProgrammerFacadeSelector}. 040 * <p> 041 * Alternately the PI and/or SI CV numbers can be set by using a "nn=nn" syntax when specifying 042 * PI and/or SI. For example, using a cvFirst false syntax, "101=12.80" sets CV101 to 12 before 043 * accessing CV 80, regardless of the PI value configured into the facade. 044 * <p> 045 * If skipDupIndexWrite is true, sequential operations with the same PI and SI values 046 * (and only immediately sequential operations with both PI and SI unchanged) will 047 * skip writing of the PI and SI CVs. This might not work for some decoders, hence is 048 * configurable. See the logic in {@link jmri.implementation.ProgrammerFacadeSelector} 049 * for how the decoder file contents and default (preferences) interact. 050 * <p> 051 * State Diagram for read and write operations (click to magnify): 052 * <a href="doc-files/MultiIndexProgrammerFacade-State-Diagram.png"><img src="doc-files/MultiIndexProgrammerFacade-State-Diagram.png" alt="UML State diagram" height="50%" width="50%"></a> 053 * 054 * @see jmri.implementation.ProgrammerFacadeSelector 055 * 056 * @author Bob Jacobsen Copyright (C) 2013 057 * @author Andrew Crosland Copyright (C) 2021 058 */ 059 060/* 061 * @startuml jmri/implementation/doc-files/MultiIndexProgrammerFacade-State-Diagram.png 062 * [*] --> NOTPROGRAMMING 063 * NOTPROGRAMMING --> PROGRAMMING: readCV() & & PI==-1\n(read CV) 064 * NOTPROGRAMMING --> FINISHREAD: readCV() & PI!=-1\n(write PI) 065 * NOTPROGRAMMING --> PROGRAMMING: writeCV() & single CV\n(write CV) 066 * NOTPROGRAMMING --> FINISHWRITE: writeCV() & PI write needed\n(write PI) 067 * FINISHREAD --> FINISHREAD: OK reply & SI!=-1\n(write SI) 068 * FINISHREAD --> PROGRAMMING: OK reply & SI==-1\n(read CV) 069 * FINISHWRITE --> FINISHWRITE: OK reply & SI!=-1\n(write SI) 070 * FINISHWRITE --> PROGRAMMING: OK reply & SI==-1\n(write CV) 071 * PROGRAMMING --> NOTPROGRAMMING: OK reply received\n(return status and value) 072 * FINISHREAD --> NOTPROGRAMMING : Error reply received 073 * FINISHWRITE --> NOTPROGRAMMING : Error reply received 074 * PROGRAMMING --> NOTPROGRAMMING : Error reply received 075 * @enduml 076*/ 077 078public class MultiIndexProgrammerFacade extends AbstractProgrammerFacade implements ProgListener { 079 080 /** 081 * @param prog the programmer to which this facade is attached 082 * @param indexPI CV to which the first value is to be written for 083 * NN.NN and NN.NN.NN forms 084 * @param indexSI CV to which the second value is to be written 085 * for NN.NN.NN forms 086 * @param cvFirst true if first value in parsed CV is to be 087 * written; false if second value is to be written 088 * @param skipDupIndexWrite true if heuristics can be used to skip PI and SI 089 * writes; false requires them to be written each 090 * time. 091 */ 092 public MultiIndexProgrammerFacade(Programmer prog, String indexPI, String indexSI, boolean cvFirst, boolean skipDupIndexWrite) { 093 super(prog); 094 this.defaultIndexPI = indexPI; 095 this.defaultIndexSI = indexSI; 096 this.cvFirst = cvFirst; 097 this.skipDupIndexWrite = skipDupIndexWrite; 098 } 099 100 String defaultIndexPI; 101 String defaultIndexSI; 102 103 String indexPI; 104 String indexSI; 105 boolean cvFirst; 106 boolean skipDupIndexWrite; 107 108 long maxDelay = 1000; // max mSec since last successful end-of-operation for skipDupIndexWrite; longer delay writes anyway 109 110 // members for handling the programmer interface 111 int _val; // remember the value being read/written for confirmative reply 112 String _cv; // remember the cv number being read/written 113 int valuePI; // value to write to PI in current operation or -1 114 int valueSI; // value to write to SI in current operation or -1 115 int _startVal; // Current CV value hint 116 117 // remember last operation for skipDupIndexWrite 118 int lastValuePI = -1; // value written in last operation 119 int lastValueSI = -1; // value written in last operation 120 long lastOpTime = -1; // time of last complete 121 122 // take the CV string and configure the actions to take 123 void parseCV(String cv) { 124 valuePI = -1; 125 valueSI = -1; 126 if (cv.contains(".")) { 127 if (cvFirst) { 128 String[] splits = cv.split("\\."); 129 switch (splits.length) { 130 case 2: 131 if (hasAlternateAddress(splits[1])) { 132 valuePI = getAlternateValue(splits[1]); 133 indexPI = getAlternateAddress(splits[1]); 134 } else { 135 valuePI = Integer.parseInt(splits[1]); 136 indexPI = defaultIndexPI; 137 } 138 _cv = splits[0]; 139 break; 140 case 3: 141 if (hasAlternateAddress(splits[1])) { 142 valuePI = getAlternateValue(splits[1]); 143 indexPI = getAlternateAddress(splits[1]); 144 } else { 145 valuePI = Integer.parseInt(splits[1]); 146 indexPI = defaultIndexPI; 147 } 148 if (hasAlternateAddress(splits[2])) { 149 valueSI = getAlternateValue(splits[2]); 150 indexSI = getAlternateAddress(splits[2]); 151 } else { 152 valueSI = Integer.parseInt(splits[2]); 153 indexSI = defaultIndexSI; 154 } 155 _cv = splits[0]; 156 break; 157 default: 158 log.error("Too many parts in CV name; taking 1st two {}", cv); 159 valuePI = Integer.parseInt(splits[1]); 160 valueSI = Integer.parseInt(splits[2]); 161 _cv = splits[0]; 162 break; 163 } 164 } else { 165 String[] splits = cv.split("\\."); 166 switch (splits.length) { 167 case 2: 168 if (hasAlternateAddress(splits[0])) { 169 valuePI = getAlternateValue(splits[0]); 170 indexPI = getAlternateAddress(splits[0]); 171 } else { 172 valuePI = Integer.parseInt(splits[0]); 173 indexPI = defaultIndexPI; 174 } 175 _cv = splits[1]; 176 break; 177 case 3: 178 if (hasAlternateAddress(splits[0])) { 179 valuePI = getAlternateValue(splits[0]); 180 indexPI = getAlternateAddress(splits[0]); 181 } else { 182 valuePI = Integer.parseInt(splits[0]); 183 indexPI = defaultIndexPI; 184 } 185 if (hasAlternateAddress(splits[1])) { 186 valueSI = getAlternateValue(splits[1]); 187 indexSI = getAlternateAddress(splits[1]); 188 } else { 189 valueSI = Integer.parseInt(splits[1]); 190 indexSI = defaultIndexSI; 191 } 192 _cv = splits[2]; 193 break; 194 default: 195 log.error("Too many parts in CV name; taking 1st two {}", cv); 196 valuePI = Integer.parseInt(splits[0]); 197 valueSI = Integer.parseInt(splits[1]); 198 _cv = splits[2]; 199 break; 200 } 201 } 202 } else { 203 _cv = cv; 204 } 205 } 206 207 boolean hasAlternateAddress(String cv) { 208 return cv.contains("="); 209 } 210 211 String getAlternateAddress(String cv) { 212 return cv.split("=")[0]; 213 } 214 215 int getAlternateValue(String cv) { 216 return Integer.parseInt(cv.split("=")[1]); 217 } 218 219 /** 220 * Check if the last-written PI and SI values can still be counted on. 221 * 222 * @return true if last-written values are reliable; false otherwise 223 */ 224 boolean useCachePiSi() { 225 return skipDupIndexWrite 226 && (lastValuePI == valuePI) 227 && (lastValueSI == valueSI) 228 && ((System.currentTimeMillis() - lastOpTime) < maxDelay); 229 } 230 231 // programming interface 232 @Override 233 synchronized public void writeCV(String CV, int val, jmri.ProgListener p) throws jmri.ProgrammerException { 234 _val = val; 235 useProgrammer(p); 236 parseCV(CV); 237 if (valuePI == -1) { 238 lastValuePI = -1; // next indexed operation needs to write PI, SI 239 lastValueSI = -1; 240 241 // non-indexed operation 242 state = ProgState.PROGRAMMING; 243 prog.writeCV(_cv, val, this); 244 } else if (useCachePiSi()) { 245 // indexed operation with set values is same as non-indexed operation 246 state = ProgState.PROGRAMMING; 247 prog.writeCV(_cv, val, this); 248 } else { 249 lastValuePI = valuePI; // after check in 'if' statement 250 lastValueSI = valueSI; 251 252 // write index first 253 state = ProgState.FINISHWRITE; 254 prog.writeCV(indexPI, valuePI, this); 255 } 256 } 257 258 @Override 259 synchronized public void readCV(String CV, jmri.ProgListener p) throws jmri.ProgrammerException { 260 readCV(CV, p, 0); 261 } 262 263 @Override 264 synchronized public void readCV(String CV, jmri.ProgListener p, int startVal) throws jmri.ProgrammerException { 265 useProgrammer(p); 266 parseCV(CV); 267 _startVal = startVal; 268 if (valuePI == -1) { 269 lastValuePI = -1; // next indexed operation needs to write PI, SI 270 lastValueSI = -1; 271 272 state = ProgState.PROGRAMMING; 273 prog.readCV(_cv, this, _startVal); 274 } else if (useCachePiSi()) { 275 // indexed operation with set values is same as non-indexed operation 276 state = ProgState.PROGRAMMING; 277 prog.readCV(_cv, this, _startVal); 278 } else { 279 lastValuePI = valuePI; // after check in 'if' statement 280 lastValueSI = valueSI; 281 282 // write index first 283 state = ProgState.FINISHREAD; 284 prog.writeCV(indexPI, valuePI, this); 285 } 286 } 287 288 @Override 289 synchronized public void confirmCV(String CV, int val, jmri.ProgListener p) throws jmri.ProgrammerException { 290 _val = val; 291 useProgrammer(p); 292 parseCV(CV); 293 if (valuePI == -1) { 294 lastValuePI = -1; // next indexed operation needs to write PI, SI 295 lastValueSI = -1; 296 297 // non-indexed operation 298 state = ProgState.PROGRAMMING; 299 prog.confirmCV(_cv, val, this); 300 } else if (useCachePiSi()) { 301 // indexed operation with set values is same as non-indexed operation 302 state = ProgState.PROGRAMMING; 303 prog.confirmCV(_cv, val, this); 304 } else { 305 lastValuePI = valuePI; // after check in 'if' statement 306 lastValueSI = valueSI; 307 308 // write index first 309 state = ProgState.FINISHCONFIRM; 310 prog.writeCV(indexPI, valuePI, this); 311 } 312 } 313 314 private jmri.ProgListener _usingProgrammer = null; 315 316 // internal method to remember who's using the programmer 317 protected void useProgrammer(jmri.ProgListener p) throws jmri.ProgrammerException { 318 // test for only one! 319 if (_usingProgrammer != null && _usingProgrammer != p) { 320 log.info("programmer already in use by {}", _usingProgrammer); 321 throw new jmri.ProgrammerException("programmer in use"); 322 } else { 323 _usingProgrammer = p; 324 } 325 } 326 327 /** 328 * State machine for MultiIndexProgrammerFacade (click to magnify): 329 * <a href="doc-files/MultiIndexProgrammerFacade-State-Diagram.png"><img src="doc-files/MultiIndexProgrammerFacade-State-Diagram.png" alt="UML State diagram" height="50%" width="50%"></a> 330 */ 331 enum ProgState { 332 /** Waiting for response to (final) read or write operation, final reply next */ 333 PROGRAMMING, 334 /** Waiting for response to first or second index write before a final read operation */ 335 FINISHREAD, 336 /** Waiting for response to first or second index write before a final write operation */ 337 FINISHWRITE, 338 /** Waiting for response to first or second index write before a final confirm operation */ 339 FINISHCONFIRM, 340 /** No current operation */ 341 NOTPROGRAMMING 342 } 343 ProgState state = ProgState.NOTPROGRAMMING; 344 345 // get notified of the final result 346 // Note this assumes that there's only one phase to the operation 347 @Override 348 public void programmingOpReply(int value, int status) { 349 log.debug("notifyProgListenerEnd value {} status {} ", value, status); 350 351 if (status != OK) { 352 // clear memory of last PI, SI written 353 lastValuePI = -1; 354 lastValueSI = -1; 355 lastOpTime = -1; 356 357 // pass abort up 358 log.debug("Reset and pass abort up"); 359 jmri.ProgListener temp = _usingProgrammer; 360 _usingProgrammer = null; // done 361 state = ProgState.NOTPROGRAMMING; 362 temp.programmingOpReply(value, status); 363 return; 364 } 365 366 if (_usingProgrammer == null) { 367 log.error("No listener to notify, reset and ignore"); 368 state = ProgState.NOTPROGRAMMING; 369 return; 370 } 371 372 switch (state) { 373 case PROGRAMMING: 374 // the programmingOpReply handler might send an immediate reply, so 375 // clear the current listener _first_ 376 jmri.ProgListener temp = _usingProgrammer; 377 _usingProgrammer = null; // done 378 state = ProgState.NOTPROGRAMMING; 379 lastOpTime = System.currentTimeMillis(); 380 temp.programmingOpReply(value, status); 381 break; 382 case FINISHREAD: 383 if (valueSI == -1) { 384 try { 385 state = ProgState.PROGRAMMING; 386 prog.readCV(_cv, this, _startVal); 387 } catch (jmri.ProgrammerException e) { 388 log.error("Exception doing final read", e); 389 } 390 } else { 391 try { 392 int tempSI = valueSI; 393 valueSI = -1; 394 state = ProgState.FINISHREAD; 395 prog.writeCV(indexSI, tempSI, this); 396 } catch (jmri.ProgrammerException e) { 397 log.error("Exception doing write SI for read", e); 398 } 399 } 400 break; 401 case FINISHWRITE: 402 if (valueSI == -1) { 403 try { 404 state = ProgState.PROGRAMMING; 405 prog.writeCV(_cv, _val, this); 406 } catch (jmri.ProgrammerException e) { 407 log.error("Exception doing final write", e); 408 } 409 } else { 410 try { 411 int tempSI = valueSI; 412 valueSI = -1; 413 state = ProgState.FINISHWRITE; 414 prog.writeCV(indexSI, tempSI, this); 415 } catch (jmri.ProgrammerException e) { 416 log.error("Exception doing write SI for write", e); 417 } 418 } 419 break; 420 case FINISHCONFIRM: 421 if (valueSI == -1) { 422 try { 423 state = ProgState.PROGRAMMING; 424 prog.confirmCV(_cv, _val, this); 425 } catch (jmri.ProgrammerException e) { 426 log.error("Exception doing final confirm", e); 427 } 428 } else { 429 try { 430 int tempSI = valueSI; 431 valueSI = -1; 432 state = ProgState.FINISHCONFIRM; 433 prog.writeCV(indexSI, tempSI, this); 434 } catch (jmri.ProgrammerException e) { 435 log.error("Exception doing write SI for write", e); 436 } 437 } 438 break; 439 default: 440 log.error("Unexpected state on reply: {}", state); 441 // clean up as much as possible 442 _usingProgrammer = null; 443 state = ProgState.NOTPROGRAMMING; 444 lastValuePI = -1; 445 lastValueSI = -1; 446 lastOpTime = -1; 447 448 } 449 } 450 451 private final static Logger log = LoggerFactory.getLogger(MultiIndexProgrammerFacade.class); 452 453}