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 * In some circumstances, the PI and SI values should be set to specific values 039 * <u>after</u> the operation is otherwise complete. The syntax for this 040 * uses a semicolon: 041 * <ul> 042 * <li>11.12.123;0.1 After the read or write operation has accessed the main CV, 043 * write 0 to the PI and 1 to the SI. 044 * </ul> 045 * <p> 046 * The specific CV numbers for PI and SI are provided when constructing the object. 047 * They can be read from a decoder definition file 048 * by e.g. {@link jmri.implementation.ProgrammerFacadeSelector}. 049 * <p> 050 * Alternately the PI and/or SI CV numbers can be set by using a "nn=nn" syntax when specifying 051 * PI and/or SI. For example, using a cvFirst false syntax, "101=12.80" sets CV101 to 12 before 052 * accessing CV 80, regardless of the PI value configured into the facade. 053 * <p> 054 * If skipDupIndexWrite is true, sequential operations with the same PI and SI values 055 * (and only immediately sequential operations with both PI and SI unchanged) will 056 * skip writing of the PI and SI CVs. This might not work for some decoders, hence is 057 * configurable. See the logic in {@link jmri.implementation.ProgrammerFacadeSelector} 058 * for how the decoder file contents and default (preferences) interact. 059 * <p> 060 * State Diagram for read and write operations (click to magnify): 061 * <br> 062 * <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> 063 * 064 * @see jmri.implementation.ProgrammerFacadeSelector 065 * 066 * @author Bob Jacobsen Copyright (C) 2013 067 * @author Andrew Crosland Copyright (C) 2021 068 */ 069 070/* 071 * @startuml jmri/implementation/doc-files/MultiIndexProgrammerFacade-State-Diagram.png 072 * [*] --> NOTPROGRAMMING 073 * NOTPROGRAMMING --> PROGRAMMING: readCV() & & PI==-1\n(read CV) 074 * NOTPROGRAMMING --> FINISHREAD: readCV() & PI!=-1\n(write PI) 075 * NOTPROGRAMMING --> PROGRAMMING: writeCV() & single CV\n(write CV) 076 * NOTPROGRAMMING --> FINISHWRITE: writeCV() & PI write needed\n(write PI) 077 * 078 * FINISHREAD --> FINISHREAD: OK reply & SI!=-1\n(write SI) 079 * FINISHREAD --> PROGRAMMING: OK reply & SI==-1\n(read CV) 080 * FINISHWRITE --> FINISHWRITE: OK reply & SI!=-1\n(write SI) 081 * FINISHWRITE --> PROGRAMMING: OK reply & SI==-1\n(write CV) 082 * PROGRAMMING --> NOTPROGRAMMING: OK reply received and no after-index\n(return status and value) 083 * 084 * FINISHREAD --> NOTPROGRAMMING : Error reply received 085 * FINISHWRITE --> NOTPROGRAMMING : Error reply received 086 * PROGRAMMING --> NOTPROGRAMMING : Error reply received 087 * 088 * PROGRAMMING --> AFTERWRITEFIRST : if PI, SI are to be reset,\n write PI value 089 * 090 * AFTERWRITEFIRST --> NOTPROGRAMMING : Error reply received 091 * AFTERWRITEFIRST --> AFTERWRITESECOND : write SI value 092 * 093 * AFTERWRITESECOND --> NOTPROGRAMMING : Error reply received 094 * AFTERWRITESECOND --> NOTPROGRAMMING : OK reply received\n(return status and value) 095 * @enduml 096*/ 097 098public class MultiIndexProgrammerFacade extends AbstractProgrammerFacade implements ProgListener { 099 100 /** 101 * @param prog the programmer to which this facade is attached 102 * @param indexPI CV to which the first value is to be written for 103 * NN.NN and NN.NN.NN forms 104 * @param indexSI CV to which the second value is to be written 105 * for NN.NN.NN forms 106 * @param cvFirst true if first value is parsed CV to be 107 * written; false if second value is to be written 108 * @param skipDupIndexWrite true if heuristics can be used to skip PI and SI 109 * writes; false requires them to be written each 110 * time. 111 */ 112 public MultiIndexProgrammerFacade(Programmer prog, String indexPI, String indexSI, boolean cvFirst, boolean skipDupIndexWrite) { 113 super(prog); 114 this.defaultIndexPI = indexPI; 115 this.defaultIndexSI = indexSI; 116 this.cvFirst = cvFirst; 117 this.skipDupIndexWrite = skipDupIndexWrite; 118 } 119 120 String defaultIndexPI; 121 String defaultIndexSI; 122 123 String indexPI; 124 String indexSI; 125 boolean cvFirst; 126 boolean skipDupIndexWrite; 127 128 long maxDelay = 1000; // max mSec since last successful end-of-operation for skipDupIndexWrite; longer delay writes anyway 129 130 // members for handling the programmer interface 131 int _val; // remember the value being read/written for confirmative reply 132 String _cv; // remember the cv number being read/written 133 int valuePI; // value to write to PI before current operation or -1 134 int valueSI; // value to write to SI before current operation or -1 135 int valuePIafter; // value to write to PI after current operation or -1 136 int valueSIafter; // value to write to SI after current operation or -1 137 int _startVal; // Current CV value hint 138 139 // remember last operation for skipDupIndexWrite 140 int lastValuePI = -1; // value written in last operation 141 int lastValueSI = -1; // value written in last operation 142 long lastOpTime = -1; // time of last complete 143 144 // take the CV string and configure the actions to take 145 void parseCV(String cv) { 146 valuePI = -1; 147 valueSI = -1; 148 valuePIafter = -1; 149 valueSIafter = -1; 150 if (cv.contains(".")) { 151 String[] parts = cv.split(";"); 152 String[] splits = parts[0].split("\\."); 153 if (cvFirst) { 154 switch (splits.length) { 155 case 2: 156 if (hasAlternateAddress(splits[1])) { 157 valuePI = getAlternateValue(splits[1]); 158 indexPI = getAlternateAddress(splits[1]); 159 } else { 160 valuePI = Integer.parseInt(splits[1]); 161 indexPI = defaultIndexPI; 162 } 163 _cv = splits[0]; 164 break; 165 case 3: 166 if (hasAlternateAddress(splits[1])) { 167 valuePI = getAlternateValue(splits[1]); 168 indexPI = getAlternateAddress(splits[1]); 169 } else { 170 valuePI = Integer.parseInt(splits[1]); 171 indexPI = defaultIndexPI; 172 } 173 if (hasAlternateAddress(splits[2])) { 174 valueSI = getAlternateValue(splits[2]); 175 indexSI = getAlternateAddress(splits[2]); 176 } else { 177 valueSI = Integer.parseInt(splits[2]); 178 indexSI = defaultIndexSI; 179 } 180 _cv = splits[0]; 181 break; 182 default: 183 log.error("Too many parts in CV name {}; taking 1st two", cv); 184 valuePI = Integer.parseInt(splits[1]); 185 valueSI = Integer.parseInt(splits[2]); 186 _cv = splits[0]; 187 break; 188 } 189 } else { 190 // not cvFirst case 191 switch (splits.length) { 192 case 2: 193 if (hasAlternateAddress(splits[0])) { 194 valuePI = getAlternateValue(splits[0]); 195 indexPI = getAlternateAddress(splits[0]); 196 } else { 197 valuePI = Integer.parseInt(splits[0]); 198 indexPI = defaultIndexPI; 199 } 200 _cv = splits[1]; 201 break; 202 case 3: 203 if (hasAlternateAddress(splits[0])) { 204 valuePI = getAlternateValue(splits[0]); 205 indexPI = getAlternateAddress(splits[0]); 206 } else { 207 valuePI = Integer.parseInt(splits[0]); 208 indexPI = defaultIndexPI; 209 } 210 if (hasAlternateAddress(splits[1])) { 211 valueSI = getAlternateValue(splits[1]); 212 indexSI = getAlternateAddress(splits[1]); 213 } else { 214 valueSI = Integer.parseInt(splits[1]); 215 indexSI = defaultIndexSI; 216 } 217 _cv = splits[2]; 218 break; 219 default: 220 log.error("Too many parts in CV name {}; taking 1st two", cv); 221 valuePI = Integer.parseInt(splits[0]); 222 valueSI = Integer.parseInt(splits[1]); 223 _cv = splits[2]; 224 break; 225 } 226 // now handle anything after a semicolon 227 } 228 if (parts.length == 2) { 229 String[] afters = parts[1].split("\\."); 230 valuePIafter = Integer.parseInt(afters[0]); 231 valueSIafter = Integer.parseInt(afters[1]); 232 } 233 } else { 234 // no "." in CV number argument; accept as number, don't parts for semicolon 235 _cv = cv; 236 } 237 238 239 } 240 241 boolean hasAlternateAddress(String cv) { 242 return cv.contains("="); 243 } 244 245 String getAlternateAddress(String cv) { 246 return cv.split("=")[0]; 247 } 248 249 int getAlternateValue(String cv) { 250 return Integer.parseInt(cv.split("=")[1]); 251 } 252 253 /** 254 * Check if the last-written PI and SI values can still be counted on. 255 * 256 * @return true if last-written values are reliable; false otherwise 257 */ 258 boolean useCachePiSi() { 259 return skipDupIndexWrite 260 && (lastValuePI == valuePI) 261 && (lastValueSI == valueSI) 262 && ((System.currentTimeMillis() - lastOpTime) < maxDelay); 263 } 264 265 // programming interface 266 @Override 267 synchronized public void writeCV(String CV, int val, jmri.ProgListener p) throws jmri.ProgrammerException { 268 _val = val; 269 useProgrammer(p); 270 parseCV(CV); 271 if (valuePI == -1) { 272 lastValuePI = -1; // next indexed operation needs to write PI, SI 273 lastValueSI = -1; 274 275 // non-indexed operation 276 state = ProgState.PROGRAMMING; 277 prog.writeCV(_cv, val, this); 278 } else if (useCachePiSi()) { 279 // indexed operation with set values is same as non-indexed operation 280 state = ProgState.PROGRAMMING; 281 prog.writeCV(_cv, val, this); 282 } else { 283 lastValuePI = valuePI; // after check in 'if' statement 284 lastValueSI = valueSI; 285 286 // write index first 287 state = ProgState.FINISHWRITE; 288 prog.writeCV(indexPI, valuePI, this); 289 } 290 } 291 292 @Override 293 synchronized public void readCV(String CV, jmri.ProgListener p) throws jmri.ProgrammerException { 294 readCV(CV, p, 0); 295 } 296 297 @Override 298 synchronized public void readCV(String CV, jmri.ProgListener p, int startVal) throws jmri.ProgrammerException { 299 useProgrammer(p); 300 parseCV(CV); 301 _startVal = startVal; 302 if (valuePI == -1) { 303 lastValuePI = -1; // next indexed operation needs to write PI, SI 304 lastValueSI = -1; 305 306 state = ProgState.PROGRAMMING; 307 prog.readCV(_cv, this, _startVal); 308 } else if (useCachePiSi()) { 309 // indexed operation with set values is same as non-indexed operation 310 state = ProgState.PROGRAMMING; 311 prog.readCV(_cv, this, _startVal); 312 } else { 313 lastValuePI = valuePI; // after check in 'if' statement 314 lastValueSI = valueSI; 315 316 // write index first 317 state = ProgState.FINISHREAD; 318 prog.writeCV(indexPI, valuePI, this); 319 } 320 } 321 322 @Override 323 synchronized public void confirmCV(String CV, int val, jmri.ProgListener p) throws jmri.ProgrammerException { 324 _val = val; 325 useProgrammer(p); 326 parseCV(CV); 327 if (valuePI == -1) { 328 lastValuePI = -1; // next indexed operation needs to write PI, SI 329 lastValueSI = -1; 330 331 // non-indexed operation 332 state = ProgState.PROGRAMMING; 333 prog.confirmCV(_cv, val, this); 334 } else if (useCachePiSi()) { 335 // indexed operation with set values is same as non-indexed operation 336 state = ProgState.PROGRAMMING; 337 prog.confirmCV(_cv, val, this); 338 } else { 339 lastValuePI = valuePI; // after check in 'if' statement 340 lastValueSI = valueSI; 341 342 // write index first 343 state = ProgState.FINISHCONFIRM; 344 prog.writeCV(indexPI, valuePI, this); 345 } 346 } 347 348 private jmri.ProgListener _usingProgrammer = null; 349 350 // internal method to remember who's using the programmer 351 protected void useProgrammer(jmri.ProgListener p) throws jmri.ProgrammerException { 352 // test for only one! 353 if (_usingProgrammer != null && _usingProgrammer != p) { 354 log.info("programmer already in use by {}", _usingProgrammer); 355 throw new jmri.ProgrammerException("programmer in use"); 356 } else { 357 _usingProgrammer = p; 358 } 359 } 360 361 /** 362 * State machine for MultiIndexProgrammerFacade (click to magnify): 363 * <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> 364 */ 365 enum ProgState { 366 /** Waiting for response to (final) read or write operation, final reply next */ 367 PROGRAMMING, 368 /** Waiting for response to first or second index write before a final read operation */ 369 FINISHREAD, 370 /** Waiting for response to first or second index write before a final write operation */ 371 FINISHWRITE, 372 /** Waiting for response to first or second index write before a final confirm operation */ 373 FINISHCONFIRM, 374 /** Just wrote PI after the read or write has been done */ 375 AFTERWRITEFIRST, 376 /** Just wrote SI after the read or write has been done */ 377 AFTERWRITESECOND, 378 /** No current operation */ 379 NOTPROGRAMMING 380 } 381 ProgState state = ProgState.NOTPROGRAMMING; 382 383 int storedReturnValue = -1; 384 385 // get notified of the final result 386 // Note this assumes that there's only one phase to the operation 387 @Override 388 public void programmingOpReply(int value, int status) { 389 log.debug("notifyProgListenerEnd value {} status {} ", value, status); 390 391 if (status != OK) { 392 // clear memory of last PI, SI written 393 lastValuePI = -1; 394 lastValueSI = -1; 395 lastOpTime = -1; 396 397 // pass abort up 398 log.debug("Reset and pass abort up"); 399 jmri.ProgListener temp = _usingProgrammer; 400 _usingProgrammer = null; // done 401 state = ProgState.NOTPROGRAMMING; 402 temp.programmingOpReply(value, status); 403 return; 404 } 405 406 if (_usingProgrammer == null) { 407 log.error("No listener to notify, reset and ignore"); 408 state = ProgState.NOTPROGRAMMING; 409 return; 410 } 411 412 jmri.ProgListener tempProgListener; 413 414 switch (state) { 415 case PROGRAMMING: 416 // check for whether after operation id needed 417 if (valuePIafter != -1) { 418 // save the programming value 419 storedReturnValue = value; 420 // write primary index 421 try { 422 lastValuePI = valuePIafter; 423 lastValueSI = valueSIafter; 424 425 valuePIafter = -1; 426 state = ProgState.AFTERWRITEFIRST; 427 prog.writeCV(indexPI, lastValuePI, this); 428 } catch (jmri.ProgrammerException e) { 429 log.error("Exception doing write PI after operation", e); 430 } 431 break; 432 } 433 // No write after, this is the end of the operation 434 // the programmingOpReply handler might send an immediate reply, so 435 // clear the current listener _first_ 436 tempProgListener = _usingProgrammer; 437 _usingProgrammer = null; // done 438 state = ProgState.NOTPROGRAMMING; 439 lastOpTime = System.currentTimeMillis(); 440 tempProgListener.programmingOpReply(value, status); 441 break; 442 case AFTERWRITEFIRST: 443 if (valueSIafter == -1) { 444 log.error("Does not yet handle writing only one after index value"); 445 } else { 446 // need to write 2nd after-operation CV 447 try { 448 int tempSI = valueSIafter; 449 valueSIafter = -1; 450 state = ProgState.AFTERWRITESECOND; 451 prog.writeCV(indexSI, tempSI, this); 452 } catch (jmri.ProgrammerException e) { 453 log.error("Exception doing write SI after operation", e); 454 } 455 } 456 457 break; 458 459 case AFTERWRITESECOND: 460 // 2nd after write done, this is the end of the operation 461 // the programmingOpReply handler might send an immediate reply, so 462 // clear the current listener _first_ 463 tempProgListener = _usingProgrammer; 464 _usingProgrammer = null; // done 465 state = ProgState.NOTPROGRAMMING; 466 lastOpTime = System.currentTimeMillis(); 467 tempProgListener.programmingOpReply(storedReturnValue, status); 468 break; 469 470 471 case FINISHREAD: 472 if (valueSI == -1) { 473 try { 474 state = ProgState.PROGRAMMING; 475 prog.readCV(_cv, this, _startVal); 476 } catch (jmri.ProgrammerException e) { 477 log.error("Exception doing final read", e); 478 } 479 } else { 480 try { 481 int tempSI = valueSI; 482 valueSI = -1; 483 state = ProgState.FINISHREAD; 484 prog.writeCV(indexSI, tempSI, this); 485 } catch (jmri.ProgrammerException e) { 486 log.error("Exception doing write SI for read", e); 487 } 488 } 489 break; 490 case FINISHWRITE: 491 if (valueSI == -1) { 492 try { 493 state = ProgState.PROGRAMMING; 494 prog.writeCV(_cv, _val, this); 495 } catch (jmri.ProgrammerException e) { 496 log.error("Exception doing final write", e); 497 } 498 } else { 499 try { 500 int tempSI = valueSI; 501 valueSI = -1; 502 state = ProgState.FINISHWRITE; 503 prog.writeCV(indexSI, tempSI, this); 504 } catch (jmri.ProgrammerException e) { 505 log.error("Exception doing write SI for write", e); 506 } 507 } 508 break; 509 case FINISHCONFIRM: 510 if (valueSI == -1) { 511 try { 512 state = ProgState.PROGRAMMING; 513 prog.confirmCV(_cv, _val, this); 514 } catch (jmri.ProgrammerException e) { 515 log.error("Exception doing final confirm", e); 516 } 517 } else { 518 try { 519 int tempSI = valueSI; 520 valueSI = -1; 521 state = ProgState.FINISHCONFIRM; 522 prog.writeCV(indexSI, tempSI, this); 523 } catch (jmri.ProgrammerException e) { 524 log.error("Exception doing write SI for write", e); 525 } 526 } 527 break; 528 default: 529 log.error("Unexpected state on reply: {}", state); 530 // clean up as much as possible 531 _usingProgrammer = null; 532 state = ProgState.NOTPROGRAMMING; 533 lastValuePI = -1; 534 lastValueSI = -1; 535 lastOpTime = -1; 536 537 } 538 } 539 540 private final static Logger log = LoggerFactory.getLogger(MultiIndexProgrammerFacade.class); 541 542}