├── .gitignore ├── .gitmodules ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── DCC.adoc ├── LICENSE ├── Loconet.adoc ├── README.adoc ├── include ├── README └── debug.h ├── lib ├── DCC │ ├── DCC.cpp │ ├── DCC.h │ ├── LocoAddress.h │ ├── LocoSpeed.cpp │ ├── LocoSpeed.h │ ├── README.adoc │ ├── extra-script.py │ └── library.json └── README ├── loconet.code-workspace ├── platformio.ini ├── src ├── CommandStation.cpp ├── CommandStation.h ├── LbServer.h ├── LocoNetSerial.h ├── LocoNetSlotManager.cpp ├── LocoNetSlotManager.h ├── Watchdog.h ├── WiThrottle.cpp ├── WiThrottle.h ├── log.h └── main.cpp └── test ├── README └── test_native └── LocoSpeedTest.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/LocoNet2"] 2 | path = lib/LocoNet2 3 | url = https://github.com/positron96/LocoNet2 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration (CI) is the practice, in software 2 | # engineering, of merging all developer working copies with a shared mainline 3 | # several times a day < https://docs.platformio.org/page/ci/index.html > 4 | # 5 | # Documentation: 6 | # 7 | # * Travis CI Embedded Builds with PlatformIO 8 | # < https://docs.travis-ci.com/user/integration/platformio/ > 9 | # 10 | # * PlatformIO integration with Travis CI 11 | # < https://docs.platformio.org/page/ci/travis.html > 12 | # 13 | # * User Guide for `platformio ci` command 14 | # < https://docs.platformio.org/page/userguide/cmd_ci.html > 15 | # 16 | # 17 | # Please choose one of the following templates (proposed below) and uncomment 18 | # it (remove "# " before each line) or use own configuration according to the 19 | # Travis CI documentation (see above). 20 | # 21 | 22 | 23 | # 24 | # Template #1: General project. Test it using existing `platformio.ini`. 25 | # 26 | 27 | # language: python 28 | # python: 29 | # - "2.7" 30 | # 31 | # sudo: false 32 | # cache: 33 | # directories: 34 | # - "~/.platformio" 35 | # 36 | # install: 37 | # - pip install -U platformio 38 | # - platformio update 39 | # 40 | # script: 41 | # - platformio run 42 | 43 | 44 | # 45 | # Template #2: The project is intended to be used as a library with examples. 46 | # 47 | 48 | # language: python 49 | # python: 50 | # - "2.7" 51 | # 52 | # sudo: false 53 | # cache: 54 | # directories: 55 | # - "~/.platformio" 56 | # 57 | # env: 58 | # - PLATFORMIO_CI_SRC=path/to/test/file.c 59 | # - PLATFORMIO_CI_SRC=examples/file.ino 60 | # - PLATFORMIO_CI_SRC=path/to/test/directory 61 | # 62 | # install: 63 | # - pip install -U platformio 64 | # - platformio update 65 | # 66 | # script: 67 | # - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N 68 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "queue": "c", 4 | "functional": "cpp", 5 | "array": "cpp", 6 | "*.tcc": "cpp", 7 | "deque": "cpp", 8 | "string": "cpp", 9 | "unordered_map": "cpp", 10 | "unordered_set": "cpp", 11 | "vector": "cpp", 12 | "memory": "cpp", 13 | "random": "cpp", 14 | "initializer_list": "cpp" 15 | }, 16 | 17 | } -------------------------------------------------------------------------------- /DCC.adoc: -------------------------------------------------------------------------------- 1 | # This is a short memo about DCC packets 2 | 3 | *Documentation:* 4 | 5 | On basic packets: https://www.nmra.org/sites/default/files/s-92-2004-07.pdf[] 6 | 7 | On extended packets: https://www.nmra.org/sites/default/files/s-9.2.1_2012_07.pdf 8 | 9 | Example of extended accessory packet addressing: https://github.com/JMRI/JMRI/blob/master/java/src/jmri/NmraPacket.java#L652 10 | 11 | Preamble is 12 "1" bits. DCC++ uses 22 bits. Preamble is followed by "0" bit 12 | 13 | ``` 14 | PPP - preamble 15 | EEEE-EEEE - Error Detection Data Byte = XOR of all previous bytes 16 | PPP 0 0AAA-AAAA 0 01DC-SSSS 0 EEEE-EEEE 1 Speed and Direction Packet For Locomotive Decoders (7-bit). S-9.2#36 17 | D dir (1=FWD) 18 | SSSS 14-steps speed 19 | C lowest bit of 28-steps speed or F0, see S-9.2#48 20 | 21 | PPP 0 11AA-AAAA 0 AAAA-AAAA 0 CCCD DDDD () 0 EEEE-EEEE 1 - Extended Locomotive packet (14-bit address), S-9.2.1#60 22 | () - may be 0, 1 or 2 bytes, depending on CCC 23 | 01DC SSSS - 14/28 speed and dir (as in Basic packet) 24 | 0011 1111 0 CDDD-DDDD 0 E..E - 128 steps speed 25 | C - dir (1=FWD) 26 | DDD-DDDD - 128 steps speed, normal bit order 27 | 100D DDDD - F0,F4,F3,F2,F1 28 | 1011 DDDD - F8,F7,F6,F5 29 | 1010 DDDD - F12,F11,F10,F9 30 | 1101 1110 0 DDDD-DDDD 0 E..E - F20..F13 31 | 1101 1111 0 DDDD-DDDD 0 E..E - F28..F21 32 | 33 | 34 | PPP 0 10AA-AAAA 0 1AAA-CDDD 0 EEEE-EEEE 1 - Basic Accessory Decoder Packet Format (9/11-bit address) S-9.2.1#420 35 | AAA - high bits of address, in 1's complement 36 | C - activate or deactivate 37 | DD - output pair or 2 lowest bits of 11-bit address 38 | D - thrown/closed 39 | 40 | PPP 0 10AA-AAAA 0 0AAA-0AA1 0 000X-XXXX 0 EEEE-EEEE 1 - Extended Accessory Decoder Control Packet S-9.2.1#436 41 | AA-AAAA AAA AA - 11-bit address 42 | AAA - 1's complement, highest 3 bits of address 43 | AA - lowest 2 bits of address 44 | X-XXXX - aspect to be shown. 0=stop aspect 45 | 46 | PPP 0 1011-1111 0 1000-CDDD 0 EEEE-EEEE 1 - broadcast for basic accessory decoders S-9.2.1#447 47 | 48 | PPP 0 1011-1111 0 0000-0111 0 000X-XXXX 0 EEEE-EEEE 1 - broadcast for extended accessory decoders S-9.2.1#456 49 | 50 | PPP 0 0000-0000 0 0000-0000 0 0000-0000 1 - Digital Decoder Reset Packet For All Decoders S-9.2#74 51 | 52 | PPP 0 1111-1111 0 0000-0000 0 1111-1111 1 - Digital Decoder Idle Packet For All Decoders S-9.2#89 53 | 54 | PPP 0 0000-0000 0 01DC-000S 0 EEEE-EEEE 1 - Digital Decoder Broadcast Stop Packets For All Decoders S-9.2#99 55 | S - 0=stop, 1=coast (stop delivering energy to motors) 56 | D - dir 57 | C - 2= D(dir) may be ignored 58 | 59 | 60 | 61 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Pavel Melnikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Loconet.adoc: -------------------------------------------------------------------------------- 1 | = Comprehensive LocoNet(R) Personal Use Edition 1.0 Specification With Comments 2 | Based on **LocoNet(R) Personal Use Edition 1.0 Specification** by Digitrax Inc., Panama City, FL 32404. 1.0, October 16, 1997, all rights reserved. 3 | 4 | :doctype: book 5 | :toc: 6 | 7 | ifdef::env-github[] 8 | :tip-caption: :bulb: 9 | :note-caption: :information_source: 10 | :important-caption: :heavy_exclamation_mark: 11 | :caution-caption: :fire: 12 | :warning-caption: :warning: 13 | endif::[] 14 | 15 | [preface] 16 | ## Preface 17 | The goal of this document is to create a comprehensive interpretation of LocoNet Personal specification. 18 | Its first version is roughy the same as original specification, but subsequent versions would add meaning to badly formulated paragraphs and clarify unclear points. 19 | The style should also be improved. 20 | 21 | ### Sources and references 22 | . LocoNet Personal Use Edition 1.0 SPECIFICATION: https://www.digitrax.com/static/apps/cms/media/documents/loconet/loconetpersonaledition.pdf 23 | . LocoNet Personal Use Edition 1.0 Extension: http://embeddedloconet.sourceforge.net/SV_Programming_Messages_v13_PE.pdf 24 | 25 | 26 | ## Introduction 27 | 28 | This is the definition of the protocol used by Digitrax products that communicate on the Long distance version the LocoNet(R) network. 29 | This LocoNet Personal Use Edition 1.0 information is provided solely for non-commercial private use by Digitrax customers. 30 | No rights are conveyed for the commercial use of this information, and Digitrax Inc. is not able to provide technical support for private use. 31 | Digitrax conveys no warranty for this information and incurs no obligations for its use or incorrect usage. 32 | Possession of this information signifies acceptance of these conditions of usage. 33 | LocoNet is a registered trademark of Digitrax Inc. 34 | 35 | 36 | ### Summary and design philosophy 37 | 38 | LocoNet(R) is a "PEER to PEER" distributed network system on which all devices can monitor the network data flow. 39 | The network is event driven by different devices in time, and is not polled by a centralized controller in normal operation. 40 | LocoNet is a powerful power decentralized and "scalable" distributed system. 41 | This is similar to the way the worldwide telephone system works -- i.e. there is no worldwide "master control program" and all telephone central offices follow a strict set of "rules" for worldwide access. 42 | The Internet operates in a similar "distributed” manner. 43 | 44 | The access technology is Carrier Sense Multiple access with Carrier Detect, CSMA/CD. 45 | This type of network technology is the overwhelming choice for connecting computer LAN's around the world. 46 | The first implemented 10M bps version of Ethernet has been upgraded to 100M bps and now has even 1,000M bps extensions. 47 | The CSMA/CD is the basic physical or media access layer that allows multiple devices to interoperate and exchange data efficiently. 48 | 49 | LocoNet(R) uses CSMA/CD techniques to arbitrate and control network access. 50 | On top of this physical layer, LocoNet(R) specifies a higher level of message protocol that gives efficient management of data structures for operations in the model railroad environment. 51 | A sensible bit rate was chosen as a good compromise between ease of wiring, with no need for terminations or complex distribution rules, and fast latency or rapid response time. 52 | We avoided a simple "specsmanship" of specifying faster bit rates to distinguish us from other system manufacturers' capabilities, and instead chose a fundamentally new system architecture we call LocoNet(R). 53 | 54 | Since LocoNet(R) is a distributed system where each device can determine the urgency of its access, it is not easy to compare in a one-to-one manner with older technology such as polled bus systems. 55 | We can compare the explosive growth of commercial computer LAN's, and even the Internet itself, to understand that all new "state-of-the-art" data processing systems are based on network type topology. 56 | The old idea of a central "mainframe" and "Main Control Program" was state of the art for the 60's and 70's and has yielded to new technologies and ideas. 57 | 58 | Comparing "raw" bit rates is not meaningful in this context since the strength of a true network system is in allowing multiple queued access without requiring polling or data flow controlling overheads. 59 | The real strengths of a LAN become apparent when a large number of devices need fast data access, or low latency, to transfer requests or state change information. 60 | With a LocoNet(R) implementation, a modest bit rate is sufficient on a large railroad layout to give realistic operation. 61 | Note that a typical LocoNet(R) implementation allows us to achieve about 98% of network traffic capacity with less than 1% collisions. 62 | 63 | Another decisive advantage of an optimally designed LAN is the ability to overlay or add new capabilities and thus "scale" the system in features as well as size. 64 | Not having to modify a "master control program" each time we wish to reconfigure or expand system features is a very real advantage. 65 | 66 | An example of this is the way the Digitrax "Big Boy" can be expanded with DT100's and get a network "fast clock", even though the DT200 master did not itself support this. 67 | The soon to be released Digitrax Tetherless throttles (both Radio and IR) can take advantage of the network environment to add a number of different types of features among many interfaces on the same LocoNet (R) network. 68 | LocoNet(R) is configured to allow all types of data traffic to flow on a single wiring scheme. 69 | This means you do not have to run separate Throttle, Booster, Feedback and PC wiring for the system. 70 | Since LocoNet (R) is based on LAN technology, a large LocoNet(R) layout can use Bridging, Routing and Fault isolation techniques as used on commercial networks to expand to a large physical extent. 71 | 72 | To allow the addition of multiple independently operating PC's around a large physical layout using simple access hardware, the LocoNet(R) was "slowed down" and simplified somewhat over a "raw hardware" driven system capability. 73 | This trade off was made to ensure ease of attachment of PC's, and we at Digitrax feel that once the power of "computer assisted modeling" was appreciated, a single PC would rapidly prove inadequate, as task complexity and demands increased. 74 | Also, we feel it is important that PC tasks can be independent, modular and spread out among many different PC's around the layout. 75 | In particular being able to use slower old "AT" 286's and 386's running DOS and Windows 3.1 etc., was important, since requiring a dedicated new "Wintel PC" costing over US$2,000 seems like a very expensive proposition for what is essentially a hobby! 76 | 77 | ## Technical Specification 78 | 79 | The normal LocoNet(R) state is IDLE, with no data traffic unless a device has information to send. 80 | With no traffic flow, the network is RFI quiet. 81 | 82 | ### Physical 83 | 84 | The full implementation of LocoNet(R) uses a 6 pin USOC RJ12 style TELCO connector. 85 | The network is designed to operate "daisy chained" on unterminated 26 AWG 3 pair cable or flat 6 conductor type 120 ohm impedance ribbon cable. 86 | It can be cabled in numerous variations. 87 | It is designed to be tolerant of cabling environment. 88 | It is permissible for the individual wires to loop back on themselves, noting that the 2 Rail_sync lines are of opposite phase and cannot be connected. 89 | The connections are balanced to minimize RFI. 90 | The connections may be branched in any combination to yield a "Star" or "Bus" or any combination thereof. 91 | Only a single LocoNet(R) current termination is needed and is typically supplied by the system **master**. 92 | 93 | 94 | Pinouts for the RJ11/6 connector are: 95 | 96 | 1. RAIL_SYNC- {nbsp}{nbsp}{nbsp}{nbsp} white 97 | 2. SIGNAL GROUND 98 | 3. LOCONET- 99 | 4. LOCONET+ 100 | 5. SIGNAL GROUND 101 | 6. RAIL_SYNC+ {nbsp}{nbsp}{nbsp}{nbsp} [blue]#blue# 102 | 103 | 104 | Using typical UL6010 6 conductor 26AWG Telephone flat ribbon cable, a network may typically have a total parallel cable length of up to 2000{nbsp}ft (609{nbsp}m), with no point-to-point length exceeding 1000{nbsp}ft (304{nbsp}m). 105 | This is using the "standard" Long Distance LocoNet(R) termination of a 15 milliamp positive current source on pins 3 and 4 of the connectors. 106 | The capacitance of 2000{nbsp}ft of this cable is approximately 68{nbsp}nF, and the loop resistance of a pair of these stranded 26AWG conductors is 80{nbsp}ohms per 1000{nbsp}ft. 107 | 108 | The maximum amount of parallel cable is limited by the requirement that the minimum RISE TIME in the region of +2 Volts to +7 Volts, is 0.35 Volts per microsecond. 109 | The minimum FALL TIME in the same region should be greater than 0.75 Volts per microsecond. 110 | 111 | 112 | ### Electrical 113 | 114 | LocoNet(R) is a "Wired-Or" multiple access linear network using CSMA/CD techniques. 115 | Repeaters, network buffers and isolators can be implemented. 116 | For the current **single ended** implementation, the 2 LocoNet signals, + and -, are paralleled and the RJ11 cable connections become polarity insensitive, wire resistance is lowered and connection reliability is enhanced. 117 | 118 | **Single ended** voltage levels and characteristics are: 119 | 120 | [loweralpha] 121 | . High = 1 = "MARK" : LOCONET+/- voltage above +4.0 Volts with respect to ground conductors. 122 | . Low = 0 = "SPACE" : LOCONET+/- voltage below +4.0 Volts with respect to grounds. 123 | . The data should be received with 1.0 Volt of HYSTERESIS centered on +4.0 Volts. 124 | . Maximum LOCONET+/- high voltage is +24V and nominal is +12V. 125 | . Minimum receiver input impedance is 47 Kilohms, measured from pins 3&4 to pins 2&5 (GND). 126 | . The transmitter is OPEN COLLECTOR to SIGNAL GROUND and should be able to sink 50 milliamps in the "ON" state at no more than 1.6V, and withstand 35 Volts in the OFF state. 127 | . One single device shall provide the "Wired-Or" pull-up for the LOCONET+/- signals. 128 | Typical termination is performed by the packet generating "MASTER" and is a 15 milliamp current source from +12V. 129 | . Loconet devices may draw up to 15 mA from the RAIL_SYNC +/- lines whenever the voltage is greater than 7V. 130 | The unloaded voltage is between 12V and 26V max. 131 | It is general practice to provide a LOCAL current limited copy of the closest track voltages, to pins 1&6 of Throttle jacks around the layout. 132 | In this case the master "backbone" copy of RAILSYNC +/- is not on the Throttle jack. 133 | . The RAIL_SYNC+/- are a low power copy of the DCC data to be transmitted to the rails. 134 | The signals may be received by a differential receiver and boosted to drive the rails. 135 | . A device with a separate power supply isolated from Loconet may connect to the LOCONET+/- pins 3&4 and SIGNAL GROUND pins 2&5 with a just 2 wires. 136 | . To use a 1/4" (6.3 mm) stereo 3-pin plug, the SIGNAL GROUND should be connected to the Sleeve, the LOCONET +/- connected to the Tip, and the Sleeve may be connected as a power source. 137 | The power supplied to the Sleeve MUST be a CURRENT SOURCE (from +12V to +26V) and be limited to 20 milliamps maximum, because the Plug shorts the Tip and Ring when initially 138 | inserted. 139 | 140 | NOTE: There must be a typo, Sleeve is mentioned twice, but Ring is not mentioned. Anyway, these jacks are not in use since early 2000s. 141 | 142 | #### Network timing 143 | 144 | LocoNet(R) data is sent in normal asynchronous format using 1 START bit, 8 DATA bits and 1 STOP bit. 145 | The 8 bit data is transmitted LSB first. 146 | The bit times are 60.0 uSecs or 16.66 KBaud +/-1.5%. 147 | A PC serial "COM" device can use the convenient rate of 16.457 KBaud. 148 | This corresponds to a divisor of 07 for the standard NS8250 UART chip or equivalent used by most compatibles. 149 | Bytes may be transmitted "back-to-back", with a Start bit immediately following the Stop bit of the previous character. 150 | Normal network "IDLE" is the "MARK" voltage state. 151 | Data is sent **half duplex** and transmitters process the transmit echo to monitor network collisions. 152 | 153 | CARRIER DETECT (CD) for fundamental network access timing may utilize simple RC time constant "one-shots". 154 | CD becomes active immediately on any detection of network in the SPACE state. 155 | It then times out for 20 bit times or 1.2 milliseconds as the CD BACKOFF time and goes inactive. 156 | CD jitter of up to 180uS is acceptable and helps ensure even statistical network access with minimal collisions. 157 | 158 | All transmitters are responsible for detecting TRANSMIT COLLISIONS on a 1 bit or whole 159 | echo-byte basis. If a TRANSMIT collision is detected the TRANSMITTER will force a line BREAK of 15 160 | BIT times with a Low or "SPACE" on LocoNet(R), and decrement the Transmit Attempt count. (The 161 | device can attempt the next acess at the same Priority, or change it by some small amount, depending on 162 | an internal Phase reference, if the delay from Network free to Siezure is greater than 2{nbsp}uS). 163 | 164 | All receivers will process the BREAK as bad data framing and reset Message parsers The network is then 165 | free to re-arbitrate access. Any message that has format or framing errors , data errors or is a fragment 166 | caused by noise glitches and does not completely follow the MESSAGE FORMAT will be ignored by ALL 167 | receivers, and a new OPCODE will be scanned for re-synchronization. 168 | 169 | 170 | #### Network access: 171 | 172 | To SEIZE access to the LocoNet(R) a device shall wait for the CD BACKOFF time to elapse from 173 | the last space level seen on LOCONET+/-. The "MASTER" device may at this time seize the network 174 | immediately upon seeing CD has "released". All other devices add additional time delays before being 175 | allowed to attempt NETWORK SEIZE. Throttles and other devices will always wait a minimum of 176 | another 6 bit times or 360uS MASTER delay before being allowed to attempt a network seize or access. 177 | 178 | On the first attempt to access the network to transmit new input information, a device will add a further 179 | PRIORITY delay of up to 20 bit times. If network access is not gained after the priority delay, due to 180 | seizure/usage by another device, the PRIORITY delay is decremented by 1 bit time for the next access 181 | attempt, which may occur after the current message or fragment ends. In this way all devices may be 182 | queued in priority, and none may seize the network in priority over the MASTER, which often returns 183 | acknowledgments and other information based on a previous request message. 184 | 185 | A device shall make at least 25 Transmit Attempts before deciding Message Transmit failure.The 186 | Transmit Attempts must include attempting Network access for at least 15 milliseconds per access 187 | attempt. 188 | 189 | A BUSY opcode is included to allow the master to keep the network active whilst it is performing a task 190 | that requires a response, and entails a significant processing delay, i.e. it can ensure no new requests are 191 | started until it has responded to the last message. In addition to the BUSY opcode, the master may simply 192 | add 15 bit BREAK sequences to the network to delay any new messages starting until it has completed 193 | and responded. 194 | 195 | Individual device types may have their access tailored by setting different maximum and minimum 196 | PRIORITY delays. In particular, SENSOR type devices may have initial Priority of 6 or less, so they can 197 | broadcast messages to the network in a timely manner. 198 | 199 | To provide the greatest protection against network bandwidth being wasted due to repeated collisions a 200 | device should _assert the SPACE of the start bit of the message OPCODE within 2 microseconds of determining that its access delays have elapsed [.underline]#and the network is still free#_. This has the effect of improving the COLLISION aperture uncertainty for a transmit collision. If the transmitting device detects a transmit collision either by bad TRANSMIT ECHO or a TRANSMITTED 1 bit being forced to 0 on LOCONET, it will initiate the 15 bit BREAK sequence to flag all devices that data is bad. 201 | 202 | #### PC access 203 | 204 | A simple "COM" port on a PC may access the _[.underline]#network#_ by a more direct method. 205 | The protocol has been encoded so that a PC may watch the LocoNet(R) message dialog and infer that the network is free because 206 | the last message decoded does not imply a follow-on response, so that the network is immediately free for a new message dialog. 207 | In this situation, the PC may immediately seize the network before the CD BACKOFF time has elapsed. 208 | This allows the PC to pre-empt all other devices and completely control the LocoNet(R) to the level desired. Note that the message `<81><7e>` is a "time burner" NOP code sent by a Master to restart the CD Backoff timers, and hence keep the network busy in a hardware sense. 209 | This `<81>` opcode should thus be simply stripped and ignored. 210 | 211 | Several PC's may share access to LocoNet(R) by subdividing the 20 bit CD BACKOFF delay into priority windows for access. 212 | They are responsible for detecting transmit COLLISIONS by checking their TRANSMIT ECHO data and watching a CARRIER DETECT to see if a PC transmit "window" is active already, before attempting to transmit. 213 | 214 | If the LOCONET+/- signal remains at a fixed SPACE (low) level for more than 100 milliseconds, a DEVICE will assume a DISCONNECT state is in effect. 215 | From this DISCONNECT state or initial start-up state a device will wait a 250 millisecond STARTUP backoff before attempting to access the network. 216 | A device will not need to reset its internal state upon DISCONNECT and re-connection, but if it is maintaining a SLOT in the refresh stack it will be required to check the SLOT status matches its internal state before re-using any SLOT. 217 | If a device diconnects from LocoNet(R) and so does not access or reference a slot within the system PURGE time, the master will force the unaccessed SLOT to "COMMON" status so other system devices can use the SLOT. 218 | The typical purge time of a DT200 operating as a Master is about 200 seconds. 219 | A good "ping" or Slot update activity is about every 100 seconds, i.e. if a user makes no change to a throttle/slot within 100 220 | seconds, the throttle/device should automatically send another speed update at the current speed to reset the Purge timeout for that Slot. 221 | 222 | ### Message format 223 | 224 | All LocoNet(R) communications are via multi-byte messages. The "MASTER" is defined as the 225 | device that is maintaining the refresh stack for DCC packet generation and is actively generating the DCC 226 | track data. Refresh of information is typically only performed for MOBILE decoders. Stationary type 227 | decoders are not refreshed and individual IMMEDIATE commands are sent out to the track as requested. 228 | 229 | The MASTER is only privileged in respect to performing the task of maintaining the locomotive 230 | REFRESH stack and generating DCC packets. In this way other network transactions may occur that the 231 | MASTER does not need to be involved with or understand , as long as they follow the MESSAGE 232 | PROTOCOL and timing requirements. i.e. Other devices may have a dialog on the network without 233 | disturbing or involving the "MASTER". 234 | 235 | Devices on LocoNet(R) monitor the MESSAGES, check for format and data integrity and parse good 236 | messages to decode if action is required in the context. Devices such as Throttles, Input Sensors , 237 | Computer interfaces and Control panels may generate LocoNet(R) messages without needing prompting or 238 | polling by a central controller. 239 | 240 | Devices frequently will be added and removed from an operating LocoNet (R). The devices and protocol are 241 | tolerant of electrical and data transients. The format chosen gives a good degree of data integrity, 242 | guaranteed quick network-state synchronization, high data throughput , good distribution of access to 243 | many competing devices and low event latency. Also , the devices may be operated without need for 244 | unique ID or other requirements that can make network administration awkward. 245 | 246 | The data bytes on LocoNet(R) are defined as 8 bit data with the most significant bit (transmitted last in the 247 | 8 bit octet) as an OPCODE flag bit. If the MS bit , D7, is 1 the 7 least significant bits are interpreted as a 248 | network OPCODE . The opcode byte may only occur once in a valid message and is the first byte of a 249 | message. All the remaining bytes in the message must have a most significant bit of 0 , including the last 250 | CHECKSUM byte. The CHECKSUM is the 1's complement of the byte wise Exclusive Or of all the 251 | bytes in the message, except the CHECKSUM itself. To validate data accuracy, all the bytes in a correctly 252 | formatted message are Exclusive Or'ed. If this resulting byte value is "FF" hexadecimal, the message data 253 | is accepted as good. 254 | 255 | The OPCODES may be examined to determine message length and if subsequent response message is required. Data bits D6 and D5 encode the message length. D3=1 implies Follow-on message/reply: 256 | 257 | D7 D6 D5 D4 -- D3 D2 D1 D0 258 | (Opcode Flag) 259 | 1 0 0 F D C B A Message is 2 bytes, including Checksum 260 | 1 0 1 F D C B A Message is 4 bytes, inc. checksum 261 | 1 1 0 F D C B A Message is 6 bytes, inc. checksum 262 | 1 1 1 F D C B A Message in N bytes, where next byte in message is a 7 bit byte count. 263 | 264 | The A,B,C,D,F are bits available to encode 32 opcodes per message length. 265 | 266 | 267 | ## Refresh slots 268 | 269 | The model of the MASTER refresh stack is an array of up to 120 read/write refresh SLOTS. The slot address is a principal component and is generally the second byte or 1st argument of a message to the master. The refresh SLOT contains up to 10 data bytes relating to a Locomotive and also controls a task in the Track DCC refresh stack. Most mobile decoder or Locomotive operations process the SLOT associated 270 | with the Locomotive to be controlled. The SLOT number is a similar shorthand ID# to a "file handle" 271 | used to mark and process files in a DOS PC environment. Slot addresses 120-127 ARE reserved for 272 | System and Master control. 273 | 274 | Slot #124 ($7C) is allocated for read/write access to the DCS100 programming track, and the format of 275 | the 10 data bytes is not the same as a "normal" slot. See <>. 276 | 277 | ### Standard address selection 278 | 279 | To request a MOBILE or LOCOMOTIVE decoder task in the refresh stack, a Throttle device requests a locomotive address for use (opcode <> `,,,` ). 280 | The Master (or PC in a Limited Master environment) responds with a <> for the SLOT, (opcode `...`)>, that contains this locomotive address and all of its state information. 281 | If the address is currently not in any SLOT, the master will load this NEW locomotive address into a new SLOT (speed=0, FWD, Light/Functions OFF and 128 step mode) and return this as a SLOT DATA READ. 282 | If no inactive slots are free to load the NEW locomotive address, the response will be the <> (opcode ``) with a "fail" code, 0. 283 | 284 | Note that regular "SHORT" 7 bit NMRA addresses are denoted by `=0`. 285 | The Analog, Zero stretched, loco is selected when both `==0`. `` is always a 7 bit value. 286 | If `` is non-zero then the Master will generate NMRA type 14 bit or "LONG" address packets using all 14 bits from `` and `` with `` being the most significant address bits. 287 | Note that a DT200 Master **does not** process 14 bit address requests and will consider the to always be zero. 288 | You can check the <`>> return bits to see if the Master is a DT200. 289 | 290 | *The throttle must then examine the SLOT READ DATA bytes to work out how to process the Master response.* 291 | [[status1]]If the STATUS1 byte shows the SLOT to be COMMON, IDLE or FREE ("NEW" in original spec) the throttle 292 | may change the SLOT to IN_USE by performing a NULL MOVE instruction, opcode (<> 293 | `,,,`) on this SLOT. *This activation mechanism is used to guarantee proper SLOT usage interlocking in a multi-user asynchronous environment.* 294 | 295 | If the SLOT return information shows the Locomotive requested is IN_USE or UP-CONSISTED (i.e. the SL_CONUP, bit 6 of STATUS1 =1 ) the user should NOT use the SLOT. Any UP_CONSISTED locos must be UNLINKED before usage! Always process the result from the LINK and UNLINK commands, since the Master reserves the right to change the reply slot number and can reject the linking tasks under 296 | several circumstances. Verify the reply slot # and the Link UP/DN bits in STAT1 are as you expected. The throttle will then be able to update Speed./Direction and Function information. Whenever SLOT 297 | information is changed in an active slot , the SLOT is flagged to be updated as the next DCC packet sent 298 | to the track. If the SLOT is part of linked CONSIST SLOTS the whole CONSIST chain is updated 299 | consecutively. 300 | 301 | If a throttle is disconnected from the LocoNet(R), upon reconnection (if the throttle retains the SLOT state 302 | from before disconnection) it will request the full status of the SLOT it was previously using. If the 303 | reported STATUS and Speed/Function data etc., from the master exactly matches the remembered SLOT 304 | state the throttle will continue using the SLOT. If the SLOT data does not match, the throttle will assume the SLOT was purged free by the system and will go through the setup "log on" procedure again. 305 | 306 | With this procedure the throttle does not need to have a unique "ID number". SLOT addresses DO NOT imply they contain any particular LOCOMOTIVE address. The system can be mapped such that the 307 | SLOT address matches the LOCOMOTIVE address within, if the user directly Reads and Writes to 308 | SLOTs without using the Master to allocate Locomotive addresses 309 | 310 | ### Dispatching 311 | 312 | Active Locomotives (including Consist TOP) SLOTS may be released for assignment to BT2 throttles in the "DISPATCH" mode. 313 | In this case a BT2 operating in its normal mode will request a DISPATCH SLOT that has been prepared by a supervisor type device. 314 | This is included for club-type operations where simpler throttles with limited capabilities are given to Engineers (Operators) by the Hostler or Dispatcher. 315 | 316 | To DISPATCH PUT a slot, perform a SLOT MOVE to Slot 0. In this case the Destination Slot 0 is not copied to, but the source SLOT number is marked by the system as the DISPATCH slot. 317 | This is only a "one-deep stack". 318 | 319 | To DISPATCH GET, perform a SLOT MOVE from Slot 0 (no destination needed). 320 | If there is a DISPATCH marked slot in the system, a SLOT DATA READ (`,..`) with the SLOT information will be the response. 321 | If there is NO DISPATCH slot, the response will be a LONG ACK (opc `,..`) with the Fail code, 00. 322 | 323 | ## Future expansion codes 324 | 325 | (still in definition stage) 326 | 327 | Immediate codes may be sent to the Master by a device. 328 | These are converted to DCC packets and sent as the next packet to the rails. 329 | They are not entered into any refresh stack. 330 | These are available in a system based on the DCS100/"Chief". 331 | 332 | Opcodes for access to an auxiliary Service mode Programming Track are included. 333 | These requests are not entered in the main DCC packet stream . 334 | 335 | Note that several confusing expansions and opcode sequences have been stripped from this LocoNet (R) version. 336 | An experimenter who implements this protocol correctly should have no problems running on a LocoNet(R) that has other expanded features. 337 | Again, we recommend resisting the temptation to "optimise" or take shortcuts with this protocol since it will lead to guaranteed future problems with your hardware and software. 338 | 339 | ## LocoNet(R) opcode summary 340 | 341 | All Copyrights and rights reserved, Digitrax 1997. 342 | 343 | NOTE: Any opcodes shown here in _itallics_ are not finalised and are informational only. 344 | Do not use. 345 | All other OPCODES and states are reserved for future expansion. 346 | 347 | LocoNet(R) Personal Use version definitions 1.0 348 | 349 | DRAFT DEFINITIONS October 16, 1997 SUBJECT TO REVISION 350 | 351 | [cols="2,1,4,1"] 352 | |=== 353 | | Opcode | Byte | Description | Follow-on message? Response opcode 354 | 355 | 356 | 4+a| ### 2 Byte MESSAGE opcodes 357 | 358 | FORMAT = `,` 359 | 360 | |[[OPC_IDLE]]OPC_IDLE | 0x85 | FORCE IDLE state, B'cast emerg. STOP | NO 361 | 362 | |[[OPC_GPON]]OPC_GPON | 0x83 | GLOBAL power ON request | NO 363 | 364 | |[[OPC_GPOFF]]OPC_GPOFF | 0x82 | GLOBAL power OFF request | NO 365 | 366 | |[[OPC_BUSY]]OPC_BUSY | 0x81 | MASTER busy code, NUL | NO 367 | 368 | 369 | 370 | 4+a| ### 4 byte MESSAGE OPCODES 371 | 372 | FORMAT = `,,,` 373 | 374 | |[[OPC_LOCO_ADR]]OPC_LOCO_ADR |0xBF |REQ loco ADR | <> 375 | | 3+| `<0xBF>,,,` 376 | 377 | DATA return ``, is SLOT#,DATA that ADR was found in. 378 | 379 | If address is not found, master puts address in free slot and sends <>`......` 380 | 381 | If there is no free slot, <> with argument 0 is returned (`,<3F>,<0>,`). 382 | 383 | 384 | |[[OPC_SW_ACK]]OPC_SW_ACK | 0xBD | REQ SWITCH WITH acknowledge function (not DT200) | <> 385 | | 3+a| `<0xBD>,,,` 386 | 387 | REQ SWITCH function 388 | 389 | = <0,A6,A5,A4 - A3,A2,A1,A0> - 7 LS bits of address. A1, A0 select 1 of 4 input pairs in a DS54 390 | 391 | = <0,0,DIR,ON - A10,A9,A8,A7> - control bits and 4 MS bits of address. 392 | 393 | DIR=1 for Closed,/GREEN, =0 for Thrown/RED 394 | 395 | ON=1 for Output ON, =0 FOR output OFF 396 | 397 | Response is: 398 | 399 | * <<3D><00> >> if DCS100 FIFO is full, command rejected 400 | * <<3D><7F> >> if DCS100 accepted 401 | 402 | [NOTE] 403 | -- 404 | JMRI sends this message to command turnout in case "Bypass Bushby Bit" flag is set in the Command station. 405 | 406 | With this flag set, normal turnout commands(<>) are not forwarded to DCC by command stations. 407 | 408 | See details 409 | https://www.jmri.org/help/en/html/scripthelp/LnBushbyForwarder/LnBushbyForwarder.shtml[here] and 410 | https://www.jmri.org/jython/LnBushbyForwarder.py[here (a python script to turn normal commands to bushby ones)]. 411 | 412 | Upon receiving, JMRI treats this OPCode equally to <>. 413 | -- 414 | 415 | 416 | |[[OPC_SW_STATE]]OPC_SW_STATE |0xBC | REQ state of SWITCH | <> 417 | | 3+a| `<0xBC>,,,` 418 | 419 | Request a switch to have specific state. 420 | 421 | and are same as in <> 422 | 423 | NOTE: This message seems to be ignored by JMRI 424 | 425 | 426 | |[[OPC_RQ_SL_DATA]]OPC_RQ_SL_DATA |0xBB |Request SLOT DATA/status block |<> 427 | | 3+a| `<0xBB>,,<0>,` 428 | 429 | Request slot data / status block 430 | 431 | 432 | |[[OPC_MOVE_SLOTS]]OPC_MOVE_SLOTS |0xBA |MOVE slot SRC to DEST |<> or <> 433 | | 3+a| 434 | `<0xBA> ` 435 | 436 | Move SRC to DEST 437 | 438 | If slot is not in IN_USE state, clear SRC. 439 | 440 | SPECIAL CASES: 441 | 442 | * If SRC=0 (DISPATCH Get), return back SLOT READ DATA of DISPATCH Slot. 443 | * IF SRC=DEST (NULL move) then SRC(=DEST) slot state is set to IN_USE (if move is legal). 444 | * If DEST=0 (DISPATCH Put), mark slot as DISPATCH, return <> `<0xE7>` of destination slot (if move is legal). 445 | 446 | NOTE: Probably slot needs to be returned if DEST=0. 447 | 448 | Return Fail <> code if illegal move `,<3A>,<0>,`. 449 | It's illegal to move to/from slots 120/127. 450 | 451 | 452 | |[[OPC_LINK_SLOTS]]OPC_LINK_SLOTS |0xB9 | LINK slot ARG1 to slot ARG2 | SLOT READ 453 | | 3+| `<0xB9> ` 454 | 455 | Make slot SL1 slave to slot SL2. 456 | 457 | Master LINKER sets the SL_CONUP/DN flags appropriately 458 | 459 | Reply is return of <> <0xE7>. 460 | 461 | Inspect to see result of Link invalid Link will return Long Ack Fail ` <39> <0> ` 462 | 463 | 464 | |[[OPC_UNLINK_SLOTS]]OPC_UNLINK_SLOTS |0xB8 |;UNLINK slot ARG1 from slot ARG2 |YES SLOT READ 465 | | 3+a| ;<0xB8>,,, UNLINK slot SL1 from SL2 466 | ;UNLINKER executes unlink STRATEGY and returns new SLOT# 467 | ; DATA/STATUS of unlinked LOCO . Inspect data to evaluate UNLINK 468 | 469 | 470 | | 3+|CODES 0xB8 to 0xBF have responses 471 | 472 | 473 | |OPC_CONSIST_FUNC |0xB6 |;SET FUNC bits in a CONSIST uplink element |NO 474 | | 3+| ;<0xB6>,,, UP consist FUNC bits 475 | ;NOTE this SLOT adr is considered in UPLINKED slot space 476 | 477 | 478 | |[[OPC_SLOT_STAT1]]OPC_SLOT_STAT1 |0xB5 |WRITE slot stat1 |NO 479 | | 3+| `<0xB5>,,,` 480 | 481 | 482 | |[[OPC_LACK]]OPC_LONG_ACK |0xB4 |Long acknowledge |NO 483 | | 3+a| 484 | `<0xB4>,,,` 485 | 486 | `` is copy of OPCode the LACK is responding to (with MSB set to 0). LOPC=0 (unused OPC) is also valid fail code. 487 | 488 | `` is an appropriate response code for the OPCode 489 | 490 | 491 | |OPC_INPUT_REP |0xB2 | General SENSOR Input codes |NO 492 | | 3+|; <0xB2>, , , 493 | =<0,A6,A5,A4- A3,A2,A1,A0>, 7 ls adr bits. A1,A0 select 1 of 4 inputs pairs in a DS54 494 | =<0,X,I,L- A10,A9,A8,A7> Report/status bits and 4 MS adr bits. 495 | "I"=0 for DS54 "aux" inputs and 1 for "switch" inputs mapped to 4K SENSOR space. 496 | (This is effectively a least significant adr bit when using DS54 input configuration) 497 | "L"=0 for input SENSOR now 0V (LO) , 1 for Input sensor >=+6V (HI) 498 | "X"=1, control bit , 0 is RESERVED for future! 499 | 500 | 501 | |[[OPC_SW_REP]]OPC_SW_REP |0xB1 |Turnout SENSOR state REPORT | NO 502 | | 3+a| 503 | `<0xB1>,,,` Report of sensor (turnout) state 504 | 505 | =<0,A6,A5,A4 - A3,A2,A1,A0>, 7 ls adr bits. A1,A0 select 1 of 4 input pairs in a DS54. 506 | 507 | =<0,**1**,I,L - A10,A9,A8,A7> Report/status bits and 4 MS adr bits. 508 | This opcode encodes input levels for turnout feedback. 509 | 510 | I=0 - "aux" inputs (normally not feedback), =1 - "switch" input used for turnout feedback for DS54 ouput/turnout address (encoded by A0-A10). 511 | 512 | L=0 - input level is 0V (LO), =1 - input level > +6V (HI). 513 | 514 | Alternately: 515 | 516 | =<0,**0**,C,T - A10,A9,A8,A7> Report/status bits and 4 MS adr bits. 517 | This opcode encodes current OUTPUT levels. 518 | 519 | C=0 - "Closed" output line is OFF, =1 - "Closed" output line is ON (sinks current). 520 | 521 | T=0 - "Thrown" output line is OFF, =1 - "Thrown" output line is ON (sinks current). 522 | 523 | [NOTE] 524 | -- 525 | When JMRI receives this message, it is used to decode turnout state from the data. 526 | -- 527 | 528 | |[[OPC_SW_REQ]]OPC_SW_REQ |0xB0 | REQ SWITCH function| NO 529 | 530 | | 3+a| `<0xB0>,,,` Request switch state 531 | 532 | =<0,A6,A5,A4 - A3,A2,A1,A0>, 7 LS address bits. A1,A0 select 1 of 4 input pairs in a DS54. 533 | 534 | =<0,0,DIR,ON - A10,A9,A8,A7> Control bits and 4 MS address bits. 535 | 536 | DIR=1 - Closed/GREEN, =0 - Thrown/RED. 537 | 538 | ON=1 - Output ON, =0 - Output OFF. 539 | 540 | If command fails, immediate response of <> `<0xB4><30><00>`, otherwise no response. 541 | 542 | There are special values for SW2 for <> and <>. 543 | 544 | [NOTE] 545 | -- 546 | JMRI sends this message to tell turnout to change its state if its "Bypass Bushby Bit" is not set 547 | 548 | (by default, it's not set). 549 | 550 | Otherwise, <> is used to control turnout. 551 | 552 | When received (including looped back), JMRI treats this message as equal to <>. 553 | -- 554 | 555 | 556 | | 3+a| **"A" class codes** 557 | 558 | NOTE: CODES 0xA8 to 0xAF have responses 559 | 560 | 561 | |OPC_LOCO_SND | 0xA2 |SET SLOT sound functions |NO 562 | 563 | 564 | |OPC_LOCO_DIRF | 0xA1 |SET SLOT dir,F0-4 state |NO 565 | 566 | 567 | |OPC_LOCO_SPD | 0xA0 |SET SLOT speed |NO 568 | | 3+|e.g. `` 569 | 570 | 571 | 572 | 4+a| ### 6 Byte MESSAGE OPCODES 573 | 574 | FORMAT = `,,,,,` 575 | 576 | 4+a| 577 | 578 | 579 | 580 | 4+a| ### VARIABLE Byte MESSAGE OPCODES 581 | 582 | FORMAT: `,,,,...,,` 583 | 584 | |[[OPC_WR_SL_DATA]]OPC_WR_SL_DATA |0xEF | WRITE SLOT DATA, 10 bytes | <> 585 | | 3+| `<0xEF>,<0E>,,,,,, 586 | ,,,,,` 587 | 588 | SLOT DATA WRITE, 10 bytes data /14 byte MSG 589 | 590 | 591 | |[[OPC_SL_RD_DATA]]OPC_SL_RD_DATA |0xE7 | SLOT DATA return, 10 bytes |NO 592 | | 3+a| `<0xE7> <0E> <++>> <++>> <++>> <++>> <++>> <++>> <++>> <++>> <++>> <++>> ` 593 | 594 | SLOT DATA READ, 10 bytes data / 14 byte MSG 595 | 596 | If STAT2.2=0, EX1/EX2 encodes an ID# 597 | 598 | if STAT2.2=1, the STAT2.3=0 means EX1/EX2 are ALIAS 599 | 600 | ID1/ID2 are two 7 bit values encoding a 14 bit unique DEVICE usage ID: 601 | 602 | [horizontal] 603 | 00/00:: means NO ID being used 604 | 01/00 to 7F/01:: slot is used by PC. Low nibble is typically a PC number (PC can use high values) 605 | 00/02 to 7F/03:: SYSTEM reserved 606 | 00/04 to 7F/7E:: normal range for throttles 607 | 608 | 609 | |[[OPC_PEER_XFER]]_OPC_PEER_XFER_ |0xE5 | Move 8 bytes peer to peer, SRC->DST | NO 610 | | 3+a| `<0xE5>,<10>,,,,,,,, 611 | ,,,,,` 612 | 613 | SRC/DST are 7 bit args. DSTL/H=0 is broadcast message. 614 | 615 | SRC=0 is MASTER 616 | 617 | SRC=0x70-0x7E are reserved. 618 | 619 | SRC=7F is throttle msg xfer, encode address (ID), <0><0> is throttle broadcast. 620 | 621 | ;=<0,XC2,XC1,XC0 - D4.7,D3.7,D2.7,D1.7>, 622 | ;XC0-XC2=ADR type CODE-0=7 bit Peer TO Peer addresses 623 | 624 | ; 1=>is SRC HI,is DST HI 625 | ;=<0,XC5,XC4,XC3 - D8.7,D7.7,D6.7,D5.7> 626 | ;XC3-XC5=data type CODE- 0=ANSI TEXT string,balance RESERVED 627 | 628 | [NOTE] 629 | -- 630 | This message is used to work with System Variables (SVs). 631 | 632 | This is described in LocoNet Personal Use Extension (see e.g. 633 | http://embeddedloconet.sourceforge.net/SV_Programming_Messages_v13_PE.pdf[embeddedloconet]) 634 | -- 635 | 636 | |[[OPC_IMM_PACKET]]_OPC_IMM_PACKET_ |0xED | Send n-byte packet (to DCC bus) immediately |yes LACK 637 | | 3+a| `<0xED>,<0B>,<7F>,,,,,,,,` 638 | 639 | D4,5,6=number of bytes in packet,D3=0(reserved); D2,1,0=repeat count. 640 | 641 | = <0,0,1,IM5.7 - IM4.7,IM3.7,IM2.7,IM1.7> - high bits of packet. 642 | 643 | NOTE: JMRI sends DHI byte with D5=0, i.e only high bits are set. 644 | 645 | 646 | ;Not limited MASTER then <>=,<7D>,<7F>, if CMD ok 647 | ;IF limited MASTER then Lim Masters respond with ,<7E>,, 648 | ;If internal buffer BUSY/full respond with ,<7D>,<0>, 649 | 650 | (NOT IMPLEMENTED IN DT200) 651 | |=== 652 | 653 | ## Slot data 654 | 655 | Values returned by <> or <> 656 | 657 | NOTE: for slot 0 read return master config information. 658 | 659 | [start=0] 660 | 0. [[arg_slot_number]]SLOT NUMBER: 0-7FH. 0 is special SLOT, 070H-07FH reserved by DIGITRAX. 661 | 662 | 1. [[arg_slot_stat]]SLOT STATUS1: 663 | + 664 | [horizontal] 665 | D7 SL_SPURGE:: 666 | 1=SLOT purge en, ALSO adrSEL (INTERNAL use only) (not seen on NET!) 667 | D6 SL_CONUP:: 668 | + 669 | -- 670 | CONDN/CONUP: bit encoding-Control double linked Consist List 671 | 672 | [horizontal] 673 | 11:::: LOGICAL MID CONSIST, Linked up AND down 674 | 10:::: LOGICAL CONSIST TOP, Only linked downwards 675 | 01:::: LOGICAL CONSIST SUB-MEMBER, Only linked upwards 676 | 00:::: FREE locomotive, no CONSIST indirection/linking 677 | 678 | ALLOWS "CONSISTS of CONSISTS". 679 | Uplinked means that Slot SPD number is now SLOT adr of SPD/DIR and STATUS of consist, i.e. is an Indirect pointer. 680 | This Slot has same BUSY/ACTIVE bits as TOP of Consist. 681 | TOP is loco with SPD/DIR for whole consist. (top of list). 682 | -- 683 | 684 | D5 SL_BUSY:: 685 | D4 SL_ACTIVE:: BUSY(slot is allocaed by some throttle)/ACTIVE(slot data is sent to track): bit encoding for SLOT activity 686 | + 687 | [horizontal] 688 | 11:::: IN_USE - **refreshed** 689 | 10:::: IDLE - not refreshed, allocated but can be given to new throttle, see <> 690 | 01:::: COMMON - **refreshed**, can be given to new throttle 691 | 00:::: FREE - not refreshed, can be given to new throttle 692 | 693 | D3 SL_CONDN:: Shows other SLOT Consist linked INTO this slot, see SL_CONUP 694 | 695 | D2 SL_SPDEX:: 696 | D1 SL_SPD14:: 697 | D0 SL_SPD28:: 3 BITS for Decoder TYPE encoding for this SLOT: 698 | + 699 | [horizontal] 700 | 011:::: send 128 speed mode packets 701 | 010:::: 14 step MODE 702 | 001:::: 28 step. + Generate Trinary packets for this Mobile ADR 703 | 000:::: 28 step/ 3 BYTE PKT regular mode 704 | 111:::: 128 Step decoder, Allow Advanced DCC consisting 705 | 100:::: 28 Step decoder, Allow Advanced DCC consisting 706 | 707 | 2. [[arg_slot_adr]]SLOT LOCO ADR: LOCO adr Low 7 bits (byte sent as ARG2 in ADR req opcode ) 708 | 709 | 3. [[arg_slot_spd]]SLOT SPEED (byte also sent as ARG2 in SPD opcode ) 710 | [horizontal] 711 | 0x00:: SPEED 0 STOP inertially 712 | 0x01:: SPEED 0 EMERGENCY stop 713 | 0x02->0x7F:: increasing SPEED, 0x7F=max speed 714 | 715 | 4. [[arg_slot_dirf]]SLOT DIRF byte: (byte also sent as ARG2 in DIRF opcode ) 716 | + 717 | [horizontal] 718 | D7-0:: always 0 719 | D6-SL_XCNT:: reserved, set 0 720 | D5-SL_DIR:: 1=loco direction FORWARD 721 | D4-SL_F0:: 1=Directional lighting ON 722 | D3-SL_F4:: 1=F4 ON 723 | D2-SL_F3:: 1=F3 ON 724 | D1-SL_F2:: 1=F2 ON 725 | D0-SL_F1:: 1=F1 ON 726 | 727 | 5. [[arg_slot_trk]]TRK byte: GLOBAL system/track status. 728 | + 729 | [horizontal] 730 | D7-D4:: Reserved 731 | D3 GTRK_PROG_BUSY:: 1=Programming track in this Master is BUSY. 732 | D2 GTRK_MLOK1:: 1=This Master implements LocoNet 1.1 capability, 0=Master is DT200 733 | D1 GTRK_IDLE:: 0=track is paused, B'cast EMERG STOP. 734 | D0 GTRK_POWER:: 1=DCC packets are on in master, global power up 735 | 736 | 6. [[arg_slot_ss2]]SLOT STATUS2: 737 | [horizontal] 738 | D3:: 1=expansion IN ID1/2, 0=ENCODED alias 739 | D2:: 1=expansion ID1/2 is NOT ID usage 740 | D0:: 1=this slot has SUPPRESSED ADV consist 741 | 742 | 7. [[arg_slot_loco_adr2]]SLOT LOCO ADR HIGH 743 | + 744 | Locomotive address high 7 bits. If this is 0 then Low address is normal 7 bit NMRA SHORT address. 745 | If this is not zero then the most significant 6 bits of this address are used in the first LONG address byte (matching CV17). 746 | The second DCC LONG address byte matches CV18 and includes the Adr Low 7 bit value with the LS bit of ADR high in the MS postion of this track adr byte. 747 | + 748 | NOTE: a DT200 MASTER will always interpret this as 0. 749 | 750 | 8. [[arg_slot_snd]]SLOT SOUND: Slot sound/ Accesory Function mode II packets. F5-F8. 751 | (byte also sent as ARG2 in SND opcode) 752 | + 753 | [horizontal] 754 | D7-D4:: reserved 755 | D3-SL_SND4:: F8 756 | D2-SL_SND3:: F7 757 | D1-SL_SND2:: F6 758 | D0-SL_SND1:: F5 1 = SLOT Sound 1 function 1 active (accessory 2) 759 | 760 | 9. [[arg_slot_id1]]EXPANSION RESERVED ID1: 7 bit ls ID code written by THROTTLE/PC when STAT2.4=1 761 | 762 | NOTE: This could be STAT2.3 instead of STAT2.4 (as STAT2.4 is not mentioned anywhere, while STAT2.3 seems to mean "treat ID fields as ID") 763 | 764 | 10. [[arg_slot_id2]]EXPANSION RESERVED ID2: 7 bit ms ID code written by THROTTLE/PC when STAT2.4=1 765 | 766 | ### [[stationary_broadcast]] Stationary Broadcast Command: 767 | 768 | Note that a 3 byte DCC track packet configured as `,<1011-1111>,<1000-D c b a > ` is a DCC Broadcast Address to Stationary decoders. 769 | 770 | Broadcast LocoNet Switch adr is then `=<0,0,a,D-1,1,1,1>`, `=<0,1,1,1-1,0,c,b>` 771 | 772 | ### [[stationary_interrogation]] Stationary Interrogate Command: 773 | 774 | The DCC packet `,<1011-1111>,<1100-D c b a> ` is an Interrogation for all DS54's. 775 | This causes a 2 LocoNet `` messages encoding both Output state and Input state, for each sensor adr a/b/c encodes. 776 | 777 | Interrogate LocoNet Switch adr is `=<0,0,a,1-0,1,1,1>`, `= <0,1,1,1-1,0,c,b>`. 778 | 779 | This is generated by DCS100 at power ON, and scans all 8 inputs of all DS54's. 780 | 781 | ## [[programmer_track]] Programmer track: 782 | 783 | The programmer track is accessed as Special slot #124 (0x7C). 784 | It is a fully asynchronous shared system resource. 785 | 786 | To start Programmer task, write to slot 124. 787 | There will be an immediate LACK acknowledge that indicates what programming will be allowed. 788 | If a valid programming task is started, then at the final (asynchronous) programming completion, a Slot read from slot 124 will be sent. 789 | This is the final task status reply. 790 | 791 | ### Programmer task start: 792 | 793 | `<0xEF>,<0E>,<7C>,,<0>,,,;,,,<0>,<0>,` 794 | 795 | This OPC leads to immediate LACK codes: 796 | 797 | * ,<7F>,<7F>, Function NOT implemented, no reply. 798 | * ,<7F>,<0>, Programmer BUSY, task aborted, no reply. 799 | * ,<7F>,<1>, Task accepted, reply at completion. 800 | * ,<7F>,<0x40>, Task accepted blind NO reply at completion. 801 | 802 | Note that the <7F> code will occur in Operations Mode Read requests if the System is not configured for 803 | and has no Advanced Acknowlegement detection installed.. Operations Mode requests can be made and 804 | executed whilst a current Service Mode programming task is keeping the Programming track BUSY. If a 805 | Programming request is rejected, delay and resend the complete request later. Some readback operations 806 | can keep the Programming track busy for up to a minute. Multiple devices, throttles/PC's etc, can share 807 | and sequentially use the Programming track as long as they correctly interpret the response messages . 808 | Any Slot RD from the master will also contain the Programmer Busy status in bit 3 of the byte. 809 | 810 | A value of <00> will abort current SERVICE mode programming task and will echo with an 811 | RD the command string that was aborted. 812 | 813 | Programmer command: defined 814 | 815 | * D7 = 0 816 | * D6 -- Write/Read, *1* -- Write, *0* -- Read 817 | * D5 -- Byte Mode, *1* -- Byte operation, *0* -- Bit operation (if possible) 818 | * D4 -- TY1 Programming Type select bit 819 | * D3 -- TY0 Prog type select bit 820 | * D2 -- Ops Mode, *1* -- Ops Mode on Mainlines, *0* -- Service Mode on Programming Track 821 | * D1 = 0 -- reserved 822 | * D0 = 0 -- reserved 823 | 824 | Type codes: 825 | 826 | |=== 827 | |Byte Mode |Ops Mode |TY1 |TY0 |Meaning 828 | 829 | |1 |0 |0 |0 |Paged mode byte Read/Write on Service Track 830 | 831 | |1 |0 |0 |0 |Paged mode byte Read/Write on Service Track 832 | 833 | |1 |0 |0 |1 |Direct mode byteRead/Write on Service Track 834 | 835 | |0 |0 |0 |1 |Direct mode bit Read/Write on Service Track 836 | 837 | |x |0 |1 |0 |Physical Register byte Read/Write on Service Track 838 | 839 | |x |0 |1 |1 |Service Track- reserved function 840 | 841 | |1 |1 |0 |0 |Ops mode Byte program, no feedback 842 | 843 | |1 |1 |0 |1 |Ops mode Byte program, feedback 844 | 845 | |0 |1 |0 |0 |Ops mode Bit program, no feedback 846 | 847 | |0 |1 |0 |1 |Ops mode Bit program, feedback 848 | 849 | |=== 850 | 851 | Operations Mode Programming -- 7 High address bits of Loco to program, 0 if Service Mode 852 | 853 | Operations Mode Programming -- 7 Low address bits of Loco to program, 0 if Service Mode 854 | 855 | Normal Global Track status for this Master, Bit 3 also is 1 WHEN Service Mode track is BUSY 856 | 857 | High 3 BITS of CV#, and ms bit of DATA.7 <0,0,CV9,CV8 - 0,0, D7,CV7> 858 | 859 | Low 7 bits of 10 bit CV address. <0,CV6,CV5,CV4-CV3,CV2,CV1,CV0> 860 | 861 | Low 7 BITS OF data to WR or RD COMPARE <0,D6,D5,D4 - D3,D2,D1,D0> ms bit is at CVH bit 1 position. 862 | 863 | ### Programmer task final reply: 864 | 865 | (if saw LACK ,<7F>,<1>, code reply at task start) 866 | 867 | `<0xE7>,<0E>,<7C>,,,,,;,,,<0>,<0>,` 868 | 869 | Programmer Status error flags. Reply codes resulting from completed task in PCMD 870 | 871 | [horizontal] 872 | D7-D4:: reserved 873 | D3:: *1* = User Aborted this command 874 | D2:: *1* = Failed to detect READ Compare acknowledge response from decoder 875 | D1:: *1* = No Write acknowledge response from decoder 876 | D0:: *1* = Service Mode programming track empty -- No decoder detected 877 | 878 | This response is issued whenever a Programming task is completed. It echos most of the request 879 | information and returns the PSTAT status code to indicate how the task completed. If a READ was 880 | requested and contain the returned data, if the PSTAT indicates a successful readback 881 | (typically =0). Note that if a Paged Read fails to detect a successful Page write acknowledge when first 882 | setting the Page register, the read will be aborted, showing no Write acknowledge flag D1=1. 883 | 884 | ## Fast clock: 885 | 886 | The system FAST clock and parameters are implemented in Slot 123 (0x7B). 887 | Use < WR_SL_DATA>> to write new clock information; Request to read slot 0x7B (<<7B>..>>), will return current System clock information, and other throttles will update to this SYNC. Note that all attached display devices 888 | keep a current clock calculation based on this SYNC read value, i.e. devices MUST not continuously poll the clock SLOT to generate time, but use this merely to restore SYNC and follow current RATE etc. This clock slot is typically "pinged" or read SYNC'd every 70 to 100 seconds , by a single user, so all attached devices can synchronise any phase drifts. Upon seeing a SYNC read, all devices should reset their local 889 | sub-minute phase counter and invalidate the SYNC update ping generator. 890 | 891 | ### Clock slot format: 892 | 893 | `<0xEF>,<0E>,<7B>,,,,<256-MINS_60>,<256-HRS_24>,,,,<1D2>,` 894 | 895 | [horizontal] 896 | :: 0=Freeze clock, 1=normal 1:1 rate, 10=10:1 etc, max VALUE is 128(0x7F) to 1 897 | :: FRAC mins hi/lo are a sub-minute counter, depending on the CLOCK generator 898 | :: Not for ext. usage. This counter is reset when valid <7B> SYNC msg seen 899 | <256-MINS_60>:: This is FAST clock MINUTES subtracted from 256. Modulo 0-59 900 | <256-HRS_24>:: This is FAST clock HOURS subtracted from 256. Modulo 0-23 901 | :: number of 24 Hr clock rolls, positive count 902 | :: Clock Control Byte 903 | D6 -- 1=This is valid Clock information, 0=ignore this `<7B>`, SYNC reply 904 | ,<1D2>:: This is device ID last setting the clock. `<00><00>` shows no set has happened; `<7F><7x>` are reserved for PC access. 905 | 906 | +++[END]+++ 907 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | # A LocoNet Command station for model railroad based on ESP32 2 | 3 | image:https://api.codacy.com/project/badge/Grade/6a41f43541d4424987b09b8cc7de2da1[link="https://app.codacy.com/gh/positron96/LocoNetControlStation?utm_source=github.com&utm_medium=referral&utm_content=positron96/LocoNetControlStation&utm_campaign=Badge_Grade"] 4 | 5 | Video demo: 6 | 7 | image:https://img.youtube.com/vi/KVks68XLQuE/mqdefault.jpg[link=https://youtu.be/KVks68XLQuE] 8 | 9 | The command station is based on ESP32 chip. 10 | Development is done with Arduino core for ESP32 on PlatformIO. 11 | 12 | Planned features are: 13 | 14 | * [x] Running locos on main track via DCC 15 | * [x] Ops mode programming via DCC (reading CVs works) 16 | * Loconet interface with routing from several sources: 17 | ** [x] Physical LocoNet bus 18 | ** [x] LbServer (LocoNet over TCP) over WiFi 19 | ** [x] USB-Serial interface (not tested) 20 | * [x] WiFi control via WiThrottle protocol (EngineDriver or WiThrottle) 21 | * [ ] Stored turnout roster (partially done) 22 | * [ ] +++DCC++ interface via USB-Serial or WiFi+++ (No need at the moment)+++ 23 | 24 | The intended primary interface of the command station is LocoNet. 25 | LocoNet messages are received and transmitted from physical LocoNet bus, WiFi, USB. 26 | However, if needed, LocoNet can be cut from project and DCC packet generation with corresponding API can be used to implement other interfaces, like DCC++. 27 | WiThrottle protocol is used to connect WiFi throttle to Command station and does not depend on LocoNet routing. 28 | 29 | ## Hardware 30 | 31 | The current iteration of project is designed to work on a custom-designed PCB with Lolin32 Lite ESP32 module 32 | (that's one of the cheapest ESP32 board). 33 | The board has L298N chip for DCC generation for both main and program tracks. 34 | 35 | LocoNet part uses LM393 comparator with resistors adapted for 3.3V VCC levels. 36 | This circuitry is common in DIY LocoNet projects{empty}footnote:[http://www.spcoast.com/pages/LocoShield.html]footnote:[https://images.beneluxspoor.net/bnls/LocoNet_Shield_Schema.png]. 37 | 38 | The PCB is open-source and is available here: https://oshwlab.com/positron96/loconet-command-station. 39 | 40 | The project will also work on a breadboard with a L298N board (a red one with current sensing pins) 41 | and DIP versions of components. 42 | Other available LocoNet breakout boards will also work. 43 | They must operate at 3.3V. 44 | 5V levels on ESP32 GPIOs are out of spec (though it'll probably work). 45 | 46 | ## Pin functions 47 | 48 | These pins are used on ESP32 for the following purposes: 49 | 50 | * LocoNet 51 | ** RX - 16 (IN) 52 | ** TX - 17 (OUT) 53 | * DCC Main 54 | ** Data - 25 (OUT) 55 | ** Enable - 32 (OUT) 56 | ** Sense - 36 (analog in only) 57 | * DCC Prog 58 | ** Data - 26 (OUT) 59 | ** Enable - 33 (OUT) 60 | ** Sense - 39 (analog in only) 61 | * Auxiliary: 62 | ** Mode LED - 22 (OUT) 63 | ** Mode button - 13 (IN) 64 | ** User button - 15 (IN) 65 | 66 | Pins are used as generic GPIO in/out/analog mode, so can be changed freely. 67 | 68 | Reference: https://randomnerdtutorials.com/esp32-pinout-reference-gpios/ 69 | 70 | 71 | ## Code architecture: 72 | 73 | * lib/DCC/DCC.h/.cpp: DCC packet generation link:lib/DCC[library]. 74 | ** `IDCCChannel` - an interface for working with one track (main/programming). 75 | ** `DCCESP32Channel`- implementation of IDCCChannel. 76 | Contains structures to store packets, switch between allocated slots. 77 | A lot of architecture is derived from DCC++. 78 | ** `DCCESP32SignalGenerator` - a class that runs timer for DCC waveform generation. 79 | Contains references for 2 instances of IDCCChannel, so for both tracks only one timer is used. 80 | 81 | * CommandStation.h/.cpp: an API for a command station. 82 | The class finds, allocates, releases locomotive slots, sends programming data on programming tracks, stores turnout list. 83 | Calls functions from DCC.h to generate DCC packets. 84 | 85 | * LocoNetSlotManager.h/.cpp: a class that parses and generates LocoNet messages concerning command station functions. 86 | Does slot managing and programming. 87 | Calls functions from CommandStation.h for actual access to locomotives and tracks. 88 | 89 | * WiThrottle.h/.cpp: class for WiFi-based throttles (EngineDriver, WiThrottle and such). 90 | Calls functions from CommandStation.h for actual access to locomotives and tracks. 91 | 92 | * LbServer.h: http://loconetovertcp.sourceforge.net/Protocol/LoconetOverTcp.html[LocoNet over TCP] protocol implementation (for PC to connect wirelessly over WiFi). 93 | Parses data from TCP, injects LocoNet packets into LocoNet bus, sends back result of sending packet over physical bus. 94 | Packets from the bus are sent to TCP. 95 | 96 | * LocoNetSerial: an implementation of LocoNet over UART (for connecting to PC with USB cable). 97 | Since the connection does not allow controlling of RTS/DTR lines, usefulness of this function is limited. 98 | Not used at the moment, as serial port is used for debugging output. 99 | 100 | ### LocoNet routing 101 | 102 | Since this project is an Ultimate Command Station, it must accepts Loconet messages from different sources: physical LocoNet bus, USB-Serial, LbServer (LocoNet over TCP). All messages must be transparently routed to between all connected buses. It means that: 103 | 104 | * There must be a routing mechanism for broadcasting messages from one source to others. 105 | * Existing frontend classes (e.g. Throttle) must be separated from hardware backend and conncted to other sources. 106 | When command station itself generates a message (e.g. a reply to throttle), it must be broadcasted to all handlers. 107 | * Result of sending a message to the bus is only taken into account for real LocoNet bus; it is assumed that USB and WiFi always send packets successfully. 108 | 109 | With this in mind, the LocoNet2 library was heavily redesigned and refactored. 110 | The LocoNet class is now separated into hardware backend (LocoNetPhy), a message parser (LocoNetDispatcher), and a routing bus (LocoNetBus) between them. 111 | Other sources of messages extend LocoNetConsumer class and are connected to the bus. and messages. 112 | Frontend classes (Throttle, FastClock, SystemVariable etc) are connected to the message parser instead of hardware class and so can handle messages from all sources. 113 | 114 | The implementation is based on these libraries: 115 | 116 | * https://github.com/positron96/LocoNet2[LocoNet2] - LocoNet bus support. The library is heavily modified to support several sources of loconet messages and to better use timers and RTOS tasks. 117 | 118 | * link:lib/DCC[DCC] library is based on https://github.com/positron96/DCCpp[DCCpp library] and https://github.com/DccPlusPlus/BaseStation[DCC++ project] (heavily modified). Used for DCC packet generation. 119 | 120 | * https://github.com/positron96/withrottle[WifiThrottle] WiThrottle protocol. Mostly rewritten from scratch with the help of https://www.jmri.org/[JMRI] sources. 121 | 122 | * https://www.etlcpp.com/[Embedded Template Library] for statically-sized maps, vectors, bitsets etc. 123 | 124 | 125 | ## Journal 126 | 127 | ### 2023-02 128 | 129 | **Beware that CV reading/writing may not work for your decoder.** 130 | 131 | It appears that ADC on my esp32 changed its parameters over the year. 132 | It now does not detect decoder responses when reading/writing CVs. 133 | I tested with one particular decoder, the voltage over the shunt resistor is around 30mV, the resistor is 0.1 Ohm, so the current is 0.3A, which is within NMRA specs for basic acknowledge. 134 | However, the ESP32 shows non-zero readings only starting from around 80 mV, and ADC readings are around 2-8. 135 | This is actually within specs of ESP32 that https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32/api-reference/peripherals/adc.html#adc-attenuation[state] that measurable readings start from 100 mV. 136 | Somehow it did work much better before, as I have the ADC threshold in the code at 500 before. 137 | I had to lower it to 2, so now the CV reading somewhat works, but not reliably. 138 | 139 | In the future version, a proper amplifier needs to be adde to the schematic. 140 | I am looking at this TI https://www.ti.com/product/INA180[INA180]. -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /include/debug.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #if defined(DEBUG) 4 | #undef DEBUG 5 | #endif 6 | 7 | #define DEBUG 8 | 9 | #if defined(DEBUG) && !defined(NODEBUG) 10 | #define DEBUGS(s) do{Serial.println(s);}while(0) 11 | 12 | #else 13 | #define DEBUGS(s) {} 14 | #endif -------------------------------------------------------------------------------- /lib/DCC/DCC.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * @see On basic packets: https://www.nmra.org/sites/default/files/s-92-2004-07.pdf 3 | * @see On extended packets: https://www.nmra.org/sites/default/files/s-9.2.1_2012_07.pdf 4 | */ 5 | 6 | #include "DCC.h" 7 | 8 | uint8_t idlePacket[3] = {0xFF, 0x00, 0}; 9 | uint8_t resetPacket[3] = {0x00, 0x00, 0}; 10 | 11 | #define ACK_BASE_COUNT 100 /**< Number of analogRead samples to take before each CV verify to establish a baseline current.*/ 12 | #define ACK_SAMPLE_MILLIS 50 ///< analogReads are taken for this number of milliseconds 13 | #define ACK_SAMPLE_SMOOTHING 0.3 /**< Exponential smoothing to use in processing the analogRead samples after a CV verify (bit or byte) has been sent.*/ 14 | #define ACK_SAMPLE_THRESHOLD 2 /**< The threshold that the exponentially-smoothed analogRead samples (after subtracting the baseline current) must cross to establish ACKNOWLEDGEMENT.*/ 15 | 16 | void Packet::setData(uint8_t *src, uint8_t nBytes, int repeatCount) { 17 | 18 | // copy first byte into what will become the checksum byte 19 | // XOR remaining bytes into checksum byte 20 | src[nBytes] = src[0]; 21 | for(int i=1; i>1; // start bit + src[2], bits 7-1 31 | buf[6] = src[2]<<7; // src[2], bit 0 32 | 33 | if(nBytes == 3) { 34 | nBits = 49; 35 | } else { 36 | buf[6] |= src[3]>>2; // src[3], bits 7-2 37 | buf[7] = src[3]<<6; // src[3], bit 1-0 38 | if(nBytes==4) { 39 | nBits = 58; 40 | } else { 41 | buf[7] |= src[4]>>3; // src[4], bits 7-3 42 | buf[8] = src[4]<<5; // src[4], bits 2-0 43 | if(nBytes==5) { 44 | nBits = 67; 45 | } else { 46 | buf[8] |= src[5]>>4; // src[5], bits 7-4 47 | buf[9] = src[5]<<4; // src[5], bits 3-0 48 | nBits = 76; 49 | } 50 | } 51 | } 52 | nRepeat = repeatCount; 53 | debugPrint(); 54 | } 55 | 56 | void IDCCChannel::sendThrottle(int iReg, LocoAddress addr, uint8_t tSpeed, SpeedMode sm, uint8_t tDirection) { 57 | uint8_t b[5]; // save space for checksum byte 58 | uint8_t nB = 0; 59 | 60 | uint16_t iAddr = addr.addr(); 61 | if ( addr.isLong() ) { 62 | b[nB++] = highByte(iAddr) | 0xC0; // convert train number into a two-byte address 63 | } 64 | 65 | b[nB++] = lowByte(iAddr); 66 | if(sm==SpeedMode::S128) { 67 | // Advanced Operations Instruction: https://www.nmra.org/sites/default/files/s-9.2.1_2012_07.pdf #200 68 | b[nB++] = 0b0011'1111; 69 | b[nB++] = (tSpeed & 0x7F) | ( (tDirection & 0x1) << 7); 70 | } else { 71 | // basic packet: https://www.nmra.org/sites/default/files/s-92-2004-07.pdf #35 72 | uint8_t t=nB; 73 | b[nB++] = 0b0100'0000; 74 | if(tDirection==1) b[t] |= 0b0010'0000; 75 | b[t] |= (tSpeed & 0b0001'1111); 76 | } 77 | 78 | DCC_LOGI("iReg %d, addr %d, speed=%d(mode %d) %c", iReg, addr, tSpeed, (int)sm, (tDirection==1)?'F':'B'); 79 | 80 | loadPacket(iReg, b, nB, 0); 81 | } 82 | 83 | void IDCCChannel::sendFunctionGroup(int iReg, LocoAddress addr, DCCFnGroup group, uint32_t fn) { 84 | DCC_LOGI("iReg %d, addr %d, group=%d fn=%08x", iReg, addr, (uint8_t)group, fn); 85 | switch(group) { 86 | case DCCFnGroup::F0_4: 87 | // move FL(F0) to 5th bit 88 | fn = (fn & 0x1)<<4 | (fn & 0b1'1110)>>1; 89 | sendFunction(iReg, addr, 0b1000'0000 | (fn & 0b0001'1111) ); 90 | break; 91 | case DCCFnGroup::F5_8: 92 | fn >>= 5; 93 | sendFunction(iReg, addr, 0b1011'0000 | (fn & 0b0000'1111) ); 94 | break; 95 | case DCCFnGroup::F9_12: 96 | fn >>= 9; 97 | sendFunction(iReg, addr, 0b1010'0000 | (fn & 0b0000'1111) ); 98 | break; 99 | case DCCFnGroup::F13_20: 100 | fn >>= 13; 101 | sendFunction(iReg, addr, 0b1101'1110, (uint8_t)fn ); 102 | break; 103 | case DCCFnGroup::F21_28: 104 | fn >>= 21; 105 | sendFunction(iReg, addr, 0b1101'1111, (uint8_t)fn ); 106 | break; 107 | default: 108 | break; 109 | } 110 | 111 | } 112 | void IDCCChannel::sendFunction(int iReg, LocoAddress addr, uint8_t fByte, uint8_t eByte) { 113 | // save space for checksum byte 114 | uint8_t b[5]; 115 | uint8_t nB = 0; 116 | uint16_t iAddr = addr.addr(); 117 | 118 | if (addr.isLong()) { 119 | b[nB++] = highByte(iAddr) | 0xC0; // convert train number into a two-byte address 120 | } 121 | 122 | b[nB++] = lowByte(iAddr); 123 | 124 | if ( (fByte & 0b1100'0000) == 0b1000'0000) {// this is a request for functions FL,F1-F12 125 | b[nB++] = (fByte | 0x80) & 0xBF; // for safety this guarantees that first nibble of function byte will always be of binary form 10XX which should always be the case for FL,F1-F12 126 | } else { // this is a request for functions F13-F28 127 | b[nB++] = (fByte | 0xDE) & 0xDF; // for safety this guarantees that first byte will either be 0xDE (for F13-F20) or 0xDF (for F21-F28) 128 | b[nB++] = eByte; 129 | } 130 | 131 | DCC_LOGI("iReg %d, addr %d, fByte=%02x eByte=%02x", iReg, addr, fByte, eByte); 132 | 133 | /* 134 | NMRA DCC norm ask for two DCC packets instead of only one: 135 | "Command Stations that generate these packets, and which are not periodically refreshing these functions, 136 | must send at least two repetitions of these commands when any function state is changed." 137 | https://www.nmra.org/sites/default/files/s-9.2.1_2012_07.pdf 138 | */ 139 | loadPacket(0, b, nB, 4); 140 | 141 | } 142 | 143 | void IDCCChannel::sendAccessory(uint16_t addr11, bool thrown) { 144 | if(addr11>0) { 145 | addr11--; 146 | } 147 | sendAccessory( (addr11>>2) + 1U, addr11 & 0x3, thrown); 148 | } 149 | 150 | void IDCCChannel::sendAccessory(uint16_t addr9, uint8_t ch, bool thrown) { 151 | DCC_LOGI("addr9=%d, ch=%d, %c", addr9, ch, thrown?'T':'C'); 152 | 153 | uint8_t b[3]; // save space for checksum byte 154 | 155 | /* 156 | first byte is of the form 10AAAAAA, where AAAAAA represent 157 | 6 least significant bits of accessory address (9-bit. Here we have 14-bit address, so take bits 2-7) */ 158 | b[0] = ( addr9 & 0x3F) | 0x80; 159 | /* 160 | "The most significant bits of the 9-bit address are bits 4-6 of the second data byte. 161 | By convention these bits (bits 4-6 of the second data byte) are in ones complement. " 162 | https://www.nmra.org/sites/default/files/s-9.2.1_2012_07.pdf 163 | */ 164 | // second byte is of the form 1AAACDDD, where C should be 1, and the least significant D represents throw/close 165 | b[1] = ( ((addr9>>6 & 0x7) << 4 ) ^ 0b0111'0000 ) 166 | | (ch & 0x3) << 1 167 | | (thrown?0x1:0) 168 | | 0b1000'0000 ; 169 | 170 | loadPacket(0, b, 2, 4); 171 | } 172 | 173 | uint IDCCChannel::getBaselineCurrent() const { 174 | uint baseline = 0; 175 | 176 | // collect baseline current 177 | for (int j = 0; j < ACK_BASE_COUNT; j++) { 178 | uint16_t v = getCurrent(); 179 | baseline += v; 180 | delayMicroseconds(500); 181 | } 182 | baseline /= ACK_BASE_COUNT; 183 | DCC_LOGD("Baseline %d", baseline); 184 | return baseline; 185 | } 186 | 187 | // https://www.nmra.org/sites/default/files/s-9.2.3_2012_07.pdf 188 | bool IDCCChannel::checkCurrentResponse(uint baseline) const { 189 | bool ret = false; 190 | int max = 0; 191 | delay(ACK_SAMPLE_MILLIS); 192 | max = getMaxCurrent(); 193 | ret = max - baseline > ACK_SAMPLE_THRESHOLD; 194 | DCC_LOGD("result is %d, max: %d, baseline: %d", ret?1:0, max, baseline); 195 | return ret; 196 | } 197 | 198 | int16_t IDCCChannel::readCVProg(int cv) { 199 | uint8_t packet[4]; 200 | int ret; 201 | 202 | cv--; // actual CV addresses are cv-1 (0-1023) 203 | 204 | packet[0] = 0x78 | (highByte(cv) & 0x03); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03 205 | packet[1] = lowByte(cv); 206 | 207 | ret = 0; 208 | 209 | int baseline = getBaselineCurrent(); 210 | 211 | for (uint8_t i = 0; i<8; i++) { 212 | packet[2] = 0xE8 | i; 213 | 214 | loadPacket(0, resetPacket, 2, 3); // NMRA recommends starting with 3 reset packets 215 | resetMaxCurrent(); 216 | loadPacket(0, packet, 3, 5); // NMRA recommends 5 verify packets 217 | loadPacket(0, resetPacket, 2, 1); // forces code to wait until all repeats of packet are completed (and decoder begins to respond) 218 | 219 | bool bitVal = checkCurrentResponse(baseline); 220 | if(bitVal) bitSet(ret, i); 221 | 222 | DCC_LOGD("Reading bit %d, value is %d", i, bitVal?1:0); 223 | } 224 | 225 | return verifyCVByteProg(cv+1, ret) ? ret : -1; 226 | 227 | } 228 | 229 | bool IDCCChannel::verifyCVByteProg(uint16_t cv, uint8_t bValue) { 230 | DCC_LOGI("Verifying cv%d==%d", cv, bValue); 231 | uint8_t packet[4]; 232 | 233 | cv--; 234 | 235 | packet[0] = 0x74 | (highByte(cv) & 0x03); 236 | packet[1] = lowByte(cv); 237 | packet[2] = bValue; 238 | 239 | loadPacket(0, resetPacket, 2, 1); // NMRA recommends starting with 3 reset packets 240 | loadPacket(0, resetPacket, 2, 3); 241 | uint baseline = getBaselineCurrent(); 242 | resetMaxCurrent(); 243 | loadPacket(0, packet, 3, 5); // NMRA recommends 5 verify packets 244 | loadPacket(0, resetPacket, 2, 1); // forces code to wait until all repeats of packet are completed (and decoder begins to respond) 245 | 246 | return checkCurrentResponse(baseline); 247 | 248 | } 249 | 250 | bool IDCCChannel::writeCVByteProg(int cv, uint8_t bValue) { 251 | uint8_t packet[4]; 252 | uint baseline; 253 | 254 | cv--; // actual CV addresses are cv-1 (0-1023) 255 | 256 | packet[0]=0x7C | (highByte(cv)&0x03); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03 257 | packet[1]=lowByte(cv); 258 | packet[2]=bValue; 259 | 260 | loadPacket(0,resetPacket,2,1); 261 | loadPacket(0,packet,3,4); 262 | loadPacket(0,resetPacket,2,1); 263 | loadPacket(0,idlePacket,2,10); 264 | 265 | baseline = getBaselineCurrent(); 266 | 267 | packet[0]=0x74 | (highByte(cv)&0x03); // set-up to re-verify entire byte 268 | 269 | loadPacket(0,resetPacket,2,3); // NMRA recommends starting with 3 reset packets 270 | resetMaxCurrent(); 271 | loadPacket(0,packet,3,5); // NMRA recommends 5 verfy packets 272 | loadPacket(0,resetPacket,2,1); // forces code to wait until all repeats of bRead are completed (and decoder begins to respond) 273 | 274 | return checkCurrentResponse(baseline); 275 | 276 | } 277 | 278 | bool IDCCChannel::writeCVBitProg(int cv, uint8_t bNum, uint8_t bValue){ 279 | uint8_t packet[4]; 280 | uint baseline; 281 | 282 | cv--; // actual CV addresses are cv-1 (0-1023) 283 | bValue &= 0x1; 284 | bNum &= 0x7; 285 | 286 | packet[0] = 0x78 | (highByte(cv)&0x03); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03 287 | packet[1] = lowByte(cv); 288 | packet[2] = 0xF0 | bValue<<3 | bNum; 289 | 290 | loadPacket(0,resetPacket,2,1); 291 | loadPacket(0,packet,3,4); 292 | loadPacket(0,resetPacket,2,1); 293 | loadPacket(0,idlePacket,2,10); 294 | 295 | baseline = getBaselineCurrent(); 296 | 297 | bitClear(packet[2],4); // change instruction code from Write Bit to Verify Bit 298 | 299 | loadPacket(0,resetPacket,2,3); // NMRA recommends starting with 3 reset packets 300 | resetMaxCurrent(); 301 | loadPacket(0,packet,3,5); // NMRA recommends 5 verfy packets 302 | loadPacket(0,resetPacket,2,1); // forces code to wait until all repeats of bRead are completed (and decoder begins to respond) 303 | 304 | return checkCurrentResponse(baseline); 305 | 306 | } 307 | 308 | void IDCCChannel::writeCVByteMain(LocoAddress addr, int cv, uint8_t bValue) { 309 | uint8_t packet[6]; // save space for checksum byte 310 | 311 | byte nB=0; 312 | 313 | cv--; 314 | 315 | uint16_t iAddr = addr.addr(); 316 | if( addr.isLong() ) 317 | packet[nB++]=highByte(iAddr) | 0xC0; // convert train number into a two-byte address 318 | 319 | packet[nB++] = lowByte(iAddr); 320 | packet[nB++] = 0xEC | (highByte(cv)&0x03); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03 321 | packet[nB++] = lowByte(cv); 322 | packet[nB++] = bValue; 323 | 324 | loadPacket(0,packet,nB,4); 325 | 326 | } 327 | 328 | void IDCCChannel::writeCVBitMain(LocoAddress addr, int cv, uint8_t bNum, uint8_t bValue) { 329 | uint8_t b[6]; // save space for checksum byte 330 | 331 | byte nB=0; 332 | 333 | cv--; 334 | 335 | bValue &= 0x1; 336 | bNum &= 0x3; 337 | 338 | uint16_t iAddr = addr.addr(); 339 | if( addr.isLong() ) 340 | b[nB++] = highByte(iAddr) | 0xC0; // convert train number into a two-byte address 341 | 342 | b[nB++]=lowByte(iAddr); 343 | b[nB++]=0xE8 | (highByte(cv)&0x03); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03 344 | b[nB++]=lowByte(cv); 345 | b[nB++]=0xF0 | bValue<<3 | bNum; 346 | 347 | loadPacket(0,b,nB,4); 348 | 349 | } 350 | 351 | 352 | 353 | 354 | 355 | //char _msg[1024]; 356 | //char _buf[100]; 357 | /* 358 | void DCCESP32Channel::PacketSlot::initPackets(){ 359 | activePacket = packet; 360 | updatePacket = packet+1; 361 | } */ 362 | 363 | 364 | static DCCESP32SignalGenerator * _inst = nullptr; 365 | 366 | 367 | void IRAM_ATTR timerCallback() { 368 | _inst->timerFunc(); 369 | } 370 | 371 | void adcTimerCallback(void* arg) { 372 | ((DCCESP32SignalGenerator*)arg)->adcTimerFunc(); 373 | } 374 | 375 | 376 | DCCESP32SignalGenerator::DCCESP32SignalGenerator(uint8_t timerNum) 377 | : _timerNum(timerNum) 378 | { 379 | _inst = this; 380 | } 381 | 382 | void DCCESP32SignalGenerator::begin() { 383 | if (main!=nullptr) main->begin(); 384 | if (prog!=nullptr) prog->begin(); 385 | 386 | _timer = timerBegin(_timerNum, 464, true); 387 | timerAttachInterrupt(_timer, timerCallback, true); 388 | timerAlarmWrite(_timer, 10, true); 389 | timerAlarmEnable(_timer); 390 | timerStart(_timer); 391 | 392 | esp_timer_create_args_t cfg{adcTimerCallback, this, ESP_TIMER_TASK, "adc"}; 393 | esp_timer_create(&cfg, &_adcTimer); 394 | esp_timer_start_periodic(_adcTimer, 1000); // 1ms 395 | } 396 | 397 | void DCCESP32SignalGenerator::end() { 398 | if(_timer!=nullptr) { 399 | if(timerStarted(_timer) ) { timerStop(_timer); } 400 | timerEnd(_timer); 401 | _timer = nullptr; 402 | } 403 | esp_timer_stop(_adcTimer); 404 | esp_timer_delete(_adcTimer); 405 | if (main!=nullptr) main->end(); 406 | if (prog!=nullptr) prog->end(); 407 | } 408 | 409 | void DCCESP32SignalGenerator::timerFunc() { 410 | 411 | if (main!=nullptr) main->timerFunc(); 412 | if (prog!=nullptr) prog->timerFunc(); 413 | 414 | } 415 | 416 | void DCCESP32SignalGenerator::adcTimerFunc() { 417 | if (main!=nullptr) main->updateCurrent(); 418 | if (prog!=nullptr) prog->updateCurrent(); 419 | } 420 | -------------------------------------------------------------------------------- /lib/DCC/DCC.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "LocoAddress.h" 4 | #include "LocoSpeed.h" 5 | 6 | #include 7 | //#include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include 16 | 17 | 18 | constexpr float ADC_RESISTANCE = 0.1; 19 | constexpr float ADC_MAX_MV = 1100; 20 | constexpr float ADC_TO_MV = ADC_MAX_MV/4096; 21 | constexpr float ADC_TO_MA = ADC_TO_MV / ADC_RESISTANCE; 22 | constexpr uint16_t MAX_CURRENT = 2000; 23 | 24 | #define DCC_DEBUG 25 | 26 | #ifdef DCC_DEBUG 27 | #define DCC_LOGD(format, ...) log_printf(ARDUHAL_LOG_FORMAT(D, format), ##__VA_ARGS__) 28 | #define DCC_LOGD_ISR(...) 29 | #define DCC_LOGI(format, ...) log_printf(ARDUHAL_LOG_FORMAT(I, format), ##__VA_ARGS__) 30 | #define DCC_LOGI_ISR(format, ...) ets_printf(ARDUHAL_LOG_FORMAT(I, format), ##__VA_ARGS__) 31 | #define DCC_LOGW(format, ...) log_printf(ARDUHAL_LOG_FORMAT(W, format), ##__VA_ARGS__) 32 | //extern char _msg[1024]; 33 | //extern char _buf[100]; 34 | //#define DCC_DEBUG_ISR(...) do{ snprintf(_buf, 100, __VA_ARGS__); snprintf(_msg, 1024, "%s%s\n", _msg, _buf ); } while(0) 35 | //#define DCC_DEBUG_ISR_DUMP() do{ Serial.print(_msg); _msg[0]=0; } while(0); 36 | #else 37 | #define DCC_LOGD(...) 38 | #define DCC_LOGD_ISR(...) 39 | #define DCC_LOGI(...) 40 | #define DCC_LOGI_ISR( ...) 41 | #define DCC_LOGW( ...) 42 | #endif 43 | 44 | 45 | extern uint8_t idlePacket[3]; 46 | extern uint8_t resetPacket[3]; 47 | 48 | enum class DCCFnGroup { 49 | F0_4, F5_8, F9_12, F13_20, F21_28 50 | }; 51 | 52 | struct Packet { 53 | uint8_t buf[10]; 54 | uint8_t nBits; 55 | int8_t nRepeat; 56 | void debugPrint() const { 57 | /* 58 | char ttt[50]; ttt[0]='\0'; 59 | char *pos=ttt; 60 | uint8_t nBytes = nBits/8; if (nBytes*8!=nBits) nBytes++; 61 | for(int i=0; iMAX_CURRENT) { 112 | setPower(false); 113 | return false; 114 | } 115 | else return true; 116 | } 117 | 118 | void resetMaxCurrent() { maxCurrent = 0; } 119 | uint16_t getMaxCurrent() const { return maxCurrent; } 120 | uint16_t getCurrent() const { return current; } 121 | virtual void updateCurrent() = 0; 122 | 123 | virtual ~IDCCChannel() = default; 124 | 125 | protected: 126 | std::atomic current; 127 | std::atomic maxCurrent; 128 | 129 | virtual void timerFunc()=0; 130 | virtual bool loadPacket(int, uint8_t*, uint8_t, int)=0; 131 | 132 | private: 133 | friend class DCCESP32SignalGenerator; 134 | 135 | uint getBaselineCurrent() const; 136 | bool checkCurrentResponse(uint baseline) const; 137 | 138 | }; 139 | 140 | template 141 | class DCCESP32Channel: public IDCCChannel { 142 | public: 143 | 144 | DCCESP32Channel(uint8_t outputPin, uint8_t enPin, uint8_t sensePin): 145 | _outputPin{outputPin}, _enPin{enPin}, _sensePin{sensePin} 146 | { 147 | } 148 | 149 | 150 | void begin() override { 151 | pinMode(_outputPin, OUTPUT); 152 | pinMode(_enPin, OUTPUT); 153 | digitalWrite(_enPin, LOW); 154 | 155 | //DCC_LOGI("DCCESP32Channel(enPin=%d)::begin", _enPin); 156 | 157 | //analogSetCycles(16); 158 | //analogSetWidth(11); 159 | analogSetPinAttenuation(_sensePin, ADC_0db); 160 | /*esp_adc_cal_value_t ar = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_0, ADC_WIDTH_BIT_12, 1100, &adc_chars); 161 | if (ar == ESP_ADC_CAL_VAL_EFUSE_VREF) { 162 | DCC_LOGI("eFuse Vref"); 163 | } else { 164 | DCC_LOGI("Default vref"); 165 | }*/ 166 | 167 | // during loadPacket there is time when new index is enabled for refresh, but urgentPacket is not yet loaded. 168 | for(size_t i=1; i<=SLOT_COUNT; i++) { 169 | R.packets[i].setData(idlePacket, 2, 0); 170 | } 171 | loadPacket(1, idlePacket, 2, 0); 172 | } 173 | 174 | void end() override { 175 | pinMode(_outputPin, INPUT); 176 | pinMode(_enPin, INPUT); 177 | } 178 | 179 | void setPower(bool v) override { 180 | DCC_LOGI("setPower(%d)", v); 181 | digitalWrite(_enPin, v ? HIGH : LOW); 182 | } 183 | 184 | bool getPower() override { 185 | return digitalRead(_enPin) == HIGH; 186 | } 187 | 188 | /** Define a series of registers that can be sequentially accessed over a loop to generate a repeating series of DCC Packets. */ 189 | struct RegisterList { 190 | Packet packets[SLOT_COUNT+1]; 191 | etl::bitset indicesTaken; 192 | etl::map regToIdxMap; 193 | Packet * volatile currentPacket; 194 | size_t maxIdx = 0; 195 | Packet * volatile urgentPacket = nullptr; 196 | /* how many 58us periods needed for half-cycle (1 for "1", 2 for "0") */ 197 | volatile uint8_t timerPeriodsHalf = 1; // first thing a timerfunc does is decrement this, so make it not underflow 198 | /* how many 58us periods are left (at start, 2 for "1", 4 for "0"). */ 199 | volatile uint8_t timerPeriodsLeft = 2; // some sane nonzero value. 200 | Packet newPacket; 201 | //int8_t newSlot; 202 | 203 | volatile uint8_t currentBit = 0; 204 | 205 | RegisterList(): currentPacket{&packets[0]} { 206 | 207 | } 208 | 209 | ~RegisterList() = default; 210 | 211 | inline bool newPacketVacant() const { return urgentPacket==nullptr;} 212 | 213 | inline bool currentBitValue() const { 214 | return (currentPacket->buf[currentBit/8] & 1<<(7-currentBit%8) )!= 0; 215 | } 216 | 217 | inline uint8_t currentIdx() { return currentPacket-&packets[0]; } 218 | 219 | inline void advanceCurrentPacket() { 220 | if (urgentPacket != nullptr) { 221 | currentPacket = urgentPacket; 222 | *currentPacket = newPacket; // this copies packet into packet array 223 | urgentPacket = nullptr; 224 | // flip active and update Packets 225 | //Packet * p = currentPacket->flip(); 226 | DCC_LOGD_ISR("advance to urgentPacket %d", currentIdx() ); 227 | //currentPacket->debugPrint(); 228 | } else { 229 | // ELSE simply move to next Register 230 | // BUT IF this is last Register loaded, first reset currentPacket to base Register, THEN 231 | // increment current Register (note this logic causes Register[0] to be skipped when simply cycling through all Registers) 232 | 233 | size_t i = currentIdx(); 234 | do { 235 | if (i == maxIdx) { i = 0; } 236 | i++; 237 | //DCC_DEBUGF_ISR("indicesTaken[%d]=%d (max=%d)", i, indicesTaken[i]?1:0, maxIdx); 238 | } while( !indicesTaken[i] ); 239 | 240 | currentPacket = &packets[i]; 241 | 242 | DCC_LOGD_ISR("advance to next slot=%d", i ); 243 | //currentPacket->debugPrint(); 244 | } 245 | } 246 | inline void setBitTimings() { 247 | if ( currentBitValue() ) { 248 | /* For "1" bit, we need 1 58us timer tick for each signal level */ 249 | DCC_LOGD_ISR("bit %d (0x%02x) = 1", currentBit, currentPacket->buf[currentBit/8] ); 250 | timerPeriodsHalf = 1; 251 | timerPeriodsLeft = 2; 252 | } else { /* ELSE it is a ZERO bit */ 253 | /* For "0" bit, we need 2 58us timer ticks for each signal level */ 254 | DCC_LOGD_ISR("bit %d (0x%02x) = 0", currentBit, currentPacket->buf[currentBit/8] ); 255 | timerPeriodsHalf = 2; 256 | timerPeriodsLeft = 4; 257 | } 258 | } 259 | 260 | size_t findEmptyIdx() { 261 | if(regToIdxMap.available()==0) return 0; 262 | 263 | for(int i=1; i<=SLOT_COUNT; i++) 264 | if (!indicesTaken[i]) return i; 265 | return 0; 266 | } 267 | 268 | size_t findOrAllocateIdx(size_t iReg) { 269 | size_t arrIdx; 270 | const auto it = regToIdxMap.find(iReg); 271 | if(it == regToIdxMap.end() ) { 272 | arrIdx = findEmptyIdx(); 273 | if(arrIdx==0) return 0; 274 | regToIdxMap[iReg] = arrIdx; 275 | indicesTaken[arrIdx] = true; 276 | maxIdx = max(maxIdx, arrIdx); 277 | DCC_LOGI("Allocated new index %d for reg %d, max idx %d", arrIdx, iReg, maxIdx); 278 | 279 | } else { 280 | arrIdx = it->second; 281 | } 282 | return arrIdx; 283 | } 284 | 285 | void prepareNewPacket(size_t idx, uint8_t *packetData, uint8_t nBytes, int nRepeat) { 286 | newPacket.setData(packetData, nBytes, nRepeat); 287 | // newPacket will be copied to idx when new packet is starting to be sent 288 | urgentPacket = &packets[idx]; 289 | } 290 | 291 | bool loadReg(int iReg, uint8_t *b, uint8_t nBytes, int nRepeat) { 292 | //DCC_DEBUGF("reg=%d len=%d, repeat=%d", iReg, nBytes, nRepeat); 293 | 294 | // force slot to be between 0 and maxNumRegs, inclusive 295 | //iReg = iReg % (SLOT_COUNT+1); 296 | 297 | // pause while there is a Register already waiting to be updated -- urgentPacket will be reset to NULL by timer when prior Register updated fully processed 298 | int t = 1000; 299 | while(!newPacketVacant() && t>0) { delay(1); t--;} 300 | if(t==0) { 301 | DCC_LOGW("timeout for slot %d", iReg ); 302 | return false; 303 | } 304 | //DCC_DEBUGF("Loading into slot %d, took %d ms", iReg, 1000-t ); 305 | 306 | size_t arrIdx; 307 | if(iReg==0) { 308 | arrIdx = 0; 309 | } else { 310 | arrIdx = findOrAllocateIdx(iReg); 311 | if(arrIdx==0) return false; 312 | } 313 | 314 | prepareNewPacket(arrIdx, b, nBytes, nRepeat); 315 | return true; 316 | } 317 | 318 | void unloadReg(size_t iReg) { 319 | const auto it = regToIdxMap.find(iReg); 320 | if(it==regToIdxMap.end()) { 321 | DCC_LOGW("Did not find slot for reg %d", iReg); 322 | return; 323 | } 324 | 325 | size_t idx = it->second; 326 | DCC_LOGI("unloading idx %d for reg %d", idx, iReg); 327 | if(regToIdxMap.size()==1) { 328 | // if it's last last idx, remove it and load Idle packet into idx 1 329 | loadReg(1, idlePacket, 2, 0); 330 | DCC_LOGI(" only 1 idx remaining, filled as idlePacket"); 331 | if(idx==1) return; // don't unload idx 1 if it's the only one left 332 | } 333 | 334 | regToIdxMap.erase(iReg); 335 | indicesTaken[idx] = false; 336 | for(int i=1; i<=SLOT_COUNT; i++) { 337 | if (indicesTaken[i]) { maxIdx = i; break; } 338 | } 339 | } 340 | }; 341 | 342 | void updateCurrent() override { 343 | uint16_t c = analogRead(_sensePin); 344 | current = c; 345 | if(c > maxCurrent) maxCurrent = c; 346 | } 347 | 348 | void IRAM_ATTR timerFunc() override { 349 | R.timerPeriodsLeft--; 350 | //DCC_DEBUGF_ISR("DCCESP32Channel::timerFunc, periods left: %d, total: %d\n", R.timerPeriodsLeft, R.timerPeriodsHalf*2); 351 | if(R.timerPeriodsLeft == R.timerPeriodsHalf) { 352 | digitalWrite(_outputPin, HIGH ); 353 | } 354 | if(R.timerPeriodsLeft == 0) { 355 | digitalWrite(_outputPin, LOW ); 356 | nextBit(); 357 | } 358 | 359 | //current = readCurrentAdc(); 360 | } 361 | 362 | RegisterList * getReg() { return &R; } 363 | 364 | protected: 365 | 366 | bool loadPacket(int iReg, uint8_t *b, uint8_t nBytes, int nRepeat) override { 367 | return R.loadReg(iReg, b, nBytes, nRepeat); 368 | } 369 | 370 | void unloadSlot(uint8_t iReg) override { 371 | R.unloadReg(iReg); 372 | } 373 | 374 | private: 375 | 376 | uint8_t _outputPin; 377 | uint8_t _enPin; 378 | uint8_t _sensePin; 379 | //esp_adc_cal_characteristics_t adc_chars; 380 | 381 | RegisterList R; 382 | 383 | void IRAM_ATTR nextBit() { 384 | auto p = R.currentPacket; 385 | //DCC_DEBUGF_ISR("nextBit: currentPacket=%d, activePacket=%d, cbit=%d, bits=%d", R.currentIdx(), R.currentPacket->activeIdx(), R.currentBit, p->nBits ); 386 | 387 | // IF no more bits in this DCC Packet, reset current bit pointer and determine which Register and Packet to process next 388 | if (R.currentBit == p->nBits) { 389 | R.currentBit = 0; 390 | // IF current Register is first Register AND should be repeated, decrement repeat count; result is this same Packet will be repeated 391 | if (p->nRepeat>0 && R.currentPacket == &R.packets[0]) { 392 | p->nRepeat--; 393 | DCC_LOGD_ISR("repeat packet = %d", p->nRepeat); 394 | } else { 395 | // IF another slot has been updated, update currentPacket to urgentPacket and reset urgentPacket to NULL 396 | R.advanceCurrentPacket(); 397 | } 398 | } // currentPacket, activePacket, and currentBit should now be properly set to point to next DCC bit 399 | 400 | R.setBitTimings(); 401 | 402 | R.currentBit++; 403 | } 404 | 405 | //void IRAM_ATTR timerFunc(); 406 | 407 | }; 408 | 409 | 410 | class DCCESP32SignalGenerator { 411 | 412 | public: 413 | explicit DCCESP32SignalGenerator(uint8_t timerNum = 1); 414 | 415 | void setProgChannel(IDCCChannel * ch) { prog = ch;} 416 | void setMainChannel(IDCCChannel * ch) { main = ch;} 417 | 418 | /** 419 | * Starts half-bit timer. 420 | * To get 58us tick we need divisor of 58us/0.0125us(80mhz) = 4640, 421 | * separate this into 464 prescaler and 10 timer alarm. 422 | */ 423 | void begin(); 424 | 425 | void end(); 426 | 427 | private: 428 | hw_timer_t * _timer = nullptr; 429 | uint8_t _timerNum; 430 | esp_timer_handle_t _adcTimer; 431 | IDCCChannel *main = nullptr; 432 | IDCCChannel *prog = nullptr; 433 | 434 | friend void timerCallback(); 435 | friend void adcTimerCallback(void*); 436 | 437 | void IRAM_ATTR timerFunc(); 438 | void IRAM_ATTR adcTimerFunc(); 439 | }; 440 | -------------------------------------------------------------------------------- /lib/DCC/LocoAddress.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | /** 8 | * Represents a DCC address. 9 | * Can be either short (or basic or promary) or long (or extended). 10 | * By DCC standard, short addresses are in range 1--127, long addresses are in range 1--10239. 11 | * Other systems can limit address range 12 | * (e.g. LocoNet has extended address range of 128--9983, i.e. a numeric value uniquely identifies address type). 13 | * Use static creation methods to create appropriate address. 14 | * Address 0 is treated as invalid. 15 | * 16 | * Internally, the address is stored as int16_t, the signum specifies short(positive)/long(negative). 17 | * This class is small and can be chaply passed by value. 18 | */ 19 | class LocoAddress { 20 | public: 21 | LocoAddress() : num(0) {} 22 | /** Creates short address. */ 23 | static LocoAddress shortAddr(uint8_t addr) { return LocoAddress(addr); } 24 | /** Creates long address. */ 25 | static LocoAddress longAddr(uint16_t addr) { return LocoAddress(-addr); } 26 | bool isShort() const { return num>=0; } 27 | bool isLong() const { return num<=0; } 28 | /** Returns numeric value of this address. */ 29 | uint16_t addr() const { return abs(num); } 30 | bool isValid() const { return num!=0; } 31 | bool operator < (const LocoAddress& a) const { return (num < a.num); } 32 | operator String() const { return String( (isShort() ? 'S' : 'L') )+addr(); } 33 | private: 34 | LocoAddress(int16_t num): num{num} { } 35 | int16_t num; 36 | }; -------------------------------------------------------------------------------- /lib/DCC/LocoSpeed.cpp: -------------------------------------------------------------------------------- 1 | #include "LocoSpeed.h" 2 | 3 | #include 4 | 5 | 6 | LocoSpeed LocoSpeed::fromFloat(float speed) { 7 | if(speed<0) return SPEED_EMGR; 8 | else { 9 | int s = ceil(speed*MAX_SPEED_VAL); 10 | if(s==0) return SPEED_IDLE; 11 | else return LocoSpeed::from128(s+1); 12 | } 13 | } 14 | 15 | float LocoSpeed::getFloat() const { 16 | if(speed128==DCC_SPEED_EMGR ) return -1.0; 17 | if(speed128==DCC_SPEED_IDLE ) return 0.0; 18 | return (float)(speed128-1)/MAX_SPEED_VAL; 19 | } 20 | 21 | uint8_t LocoSpeed::getDCCByte(SpeedMode speedMode) const { 22 | uint8_t ret = getDCC(speedMode); 23 | if(speedMode == SpeedMode::S28) 24 | ret = (ret & 1)<<4 | (ret & 0b11110)>>1; // bit swap 25 | return ret; 26 | } 27 | 28 | uint8_t LocoSpeed::getDCC(SpeedMode speedMode) const { 29 | if(speed128<=1) return speed128; 30 | switch(speedMode) { 31 | case SpeedMode::S14: return ((speed128-1)/9 ) + 1; 32 | case SpeedMode::S128: return speed128; 33 | case SpeedMode::S28: 34 | /// @see https://www.nmra.org/sites/default/files/s-92-2004-07.pdf table at line 57 35 | return ((speed128-1)*2 + 4)/9 + 3; // first +4 is an attempt to properly round 36 | default: return 0; 37 | } 38 | } 39 | 40 | uint8_t LocoSpeed::dccTo128(uint8_t s, SpeedMode speedMode) { 41 | if(s<=1) return s; // 0 and 1 are always IDLE and EMGR 42 | switch(speedMode) { 43 | case SpeedMode::S128: return s; 44 | case SpeedMode::S14: if(s>=15) return 127; return (s-1)*9 + 1; 45 | case SpeedMode::S28: 46 | if(s<=3) return s-2; // 2 and 3 are IDLE and EMGR 47 | if(s>=31) return 127; 48 | return (s-3)*9/2 + 1; 49 | default: return 0; 50 | } 51 | } 52 | 53 | 54 | constexpr uint8_t getMaxSpeedVal(SpeedMode s) { 55 | switch(s) { 56 | case SpeedMode::S14: return 14; 57 | case SpeedMode::S28: return 28; 58 | case SpeedMode::S128: return 126; 59 | default: return 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/DCC/LocoSpeed.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | /** DCC speed mode. */ 5 | enum class SpeedMode { S14, S28, S128 }; 6 | 7 | constexpr uint8_t DCC_SPEED_IDLE = 0; ///< DCC speed value for idle (stop) 8 | constexpr uint8_t DCC_SPEED_EMGR = 1; ///< DCC speed value for emergency stop 9 | 10 | /** 11 | * Stores locomotive speed. 12 | * 13 | * Has utility functions to convert to/from different DCC speed modes (14, 28, 128) and float (0..1) values. 14 | * Internally, stores speed as DCC 128 speed steps. 15 | * It's a lightweight object, so can be passed by value cheapliy. 16 | */ 17 | class LocoSpeed { 18 | public: 19 | constexpr LocoSpeed(): LocoSpeed{0} {} 20 | 21 | LocoSpeed(uint8_t speed, SpeedMode mode): LocoSpeed{dccTo128(speed, mode)} {} 22 | 23 | static constexpr LocoSpeed from128(uint8_t speed128) { return LocoSpeed{speed128}; } 24 | static LocoSpeed fromFloat(float speed) ; 25 | static LocoSpeed fromDCC(uint8_t speed, SpeedMode sm) { return LocoSpeed{speed, sm}; } 26 | 27 | constexpr uint8_t get128() const { return speed128; } 28 | uint8_t getDCC(SpeedMode mode) const; 29 | uint8_t getDCCByte(SpeedMode speedMode) const; ///< In addition to getDCC also swaps bits for S28. 30 | float getFloat() const ; 31 | 32 | bool operator==(const LocoSpeed& rhs) const { return speed128 == rhs.speed128; } 33 | bool operator< (const LocoSpeed& rhs) const { return speed128 < rhs.speed128; } 34 | 35 | bool isEmgr() const { return speed128==DCC_SPEED_EMGR; } 36 | 37 | private: 38 | constexpr static uint8_t MAX_SPEED_VAL = 126; 39 | uint8_t speed128; 40 | constexpr LocoSpeed(uint8_t speed128): speed128{speed128} {} 41 | 42 | static uint8_t dccTo128(uint8_t s, SpeedMode mode); 43 | }; 44 | 45 | constexpr uint8_t getMaxSpeedVal(SpeedMode s); 46 | 47 | constexpr LocoSpeed SPEED_IDLE = LocoSpeed::from128(DCC_SPEED_IDLE); 48 | constexpr LocoSpeed SPEED_EMGR = LocoSpeed::from128(DCC_SPEED_EMGR); 49 | 50 | -------------------------------------------------------------------------------- /lib/DCC/README.adoc: -------------------------------------------------------------------------------- 1 | # DCC waveform generation library 2 | 3 | The principle is this: 4 | 5 | The DCC standard requires that bit "1" be 58us HIGH, 58us LOW (55-61us), bit "0" must be at least 100us HIGH + at least 100us LOW. Maximum length of "0" bit pulses is 9900us (95-9900us to be precise). Varying "0" pulses allows to control analog locomotives (called zero-stretching) 6 | footnote:[https://www.nmra.org/sites/default/files/standards/sandrp/pdf/s-9.1_electrical_standards_for_digital_command_control_2021.pdf]. 7 | 8 | `DCCESP32SignalGenerator` is a class that does signal generation on ESP32. 9 | It starts a timer with 58us period. 10 | If bit "1" is generated, each timer tick the output pin is toggled, giving pulses of 58us. 11 | If bit "0" is generated, the pin is toggled each 2 ticks, giving 116us pulse length, providing standard-compliant and simple implementation. 12 | Only 1 timer is needed for several DCC tracks, here it is used for both main and programming tracks. 13 | The algorithm can be tweaked to do zero-stretching for analog loco control, but there are no plans for this. 14 | 15 | The `IDCCChannel` class is an interface for classes implementing packet generation and sending logic. 16 | Currently there is 1 implementation for ESP32, `DCCESP32Channel`. 17 | It is almost completely architecture-agnostic, so can be ported to other acritectures easily. 18 | 19 | The DCC channel maintains a "RegisterList", a low-level structure (linked list) that contains a list of packets to be sent to tracks. 20 | This list contains N+1 slots (0..N), N is a template parameter for `DCCESP32Channel` class. 21 | 0-th slot is for non-cycled packets. 22 | These packets are function toggle commands and CV-related packets. 23 | Slots 1..N are cycled packets that need to be sent to tracks regularly. 24 | These are commands for locomotive speed (repeating is needed in case locomotive looses contact with tracks, reboots and looses its stored speed data). 25 | 26 | When one slot finishes transmitting a packet and new packets have been written to other slot, the sending code moves to that slot. If there are no new slots written to, the code goes to next slot or rolls back to 1st slot (not 0th slot since 0th slot is for non-cycled packets). 27 | Each slot has a repeat count, so the packet in a slot is repeated a required number of times before moving to another slot (this should mostly be used for 0th slot). 28 | This logic is based on https://github.com/positron96/DCCpp[DCCpp library] and https://github.com/DccPlusPlus/BaseStation[DCC++ project]. 29 | The class interface is also based on those projects, though it's heavily modified. 30 | 31 | The class also contains code for current sensing used for overcurrent-detection and reading CVs on programming track. 32 | This is ESP32-specific part of the code (though it uses `analogRead` function, so could be ported easily). -------------------------------------------------------------------------------- /lib/DCC/extra-script.py: -------------------------------------------------------------------------------- 1 | Import('env') 2 | from SCons.Script import COMMAND_LINE_TARGETS # pylint: disable=import-error 3 | 4 | if '__test' in COMMAND_LINE_TARGETS: 5 | #print(env.get('SRC_FILTER')) 6 | env.Replace(SRC_FILTER=["+<*>", "-"]) 7 | 8 | # pass flags to a global build environment (for all libraries, etc) 9 | # global_env = DefaultEnvironment() 10 | # global_env.Append( 11 | # CPPDEFINES=[ 12 | # ("MQTT_MAX_PACKET_SIZE", 512), 13 | # "ARDUINOJSON_ENABLE_STD_STRING", 14 | # ("BUFFER_LENGTH", 32) 15 | # ] 16 | # ) -------------------------------------------------------------------------------- /lib/DCC/library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DCC", 3 | "version": "0.0.0", 4 | "build": { 5 | "srcDir": ".", 6 | "extraScript": "extra-script.py" 7 | } 8 | } -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /loconet.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "files.associations": { 9 | "queue": "c", 10 | "functional": "cpp", 11 | "*.tcc": "cpp", 12 | "string": "cpp", 13 | "unordered_map": "cpp", 14 | "unordered_set": "cpp", 15 | "array": "cpp", 16 | "deque": "cpp", 17 | "vector": "cpp", 18 | "initializer_list": "cpp", 19 | "ratio": "cpp", 20 | "system_error": "cpp", 21 | "tuple": "cpp", 22 | "type_traits": "cpp", 23 | "utility": "cpp", 24 | "atomic": "cpp", 25 | "cctype": "cpp", 26 | "chrono": "cpp", 27 | "clocale": "cpp", 28 | "cmath": "cpp", 29 | "cstdarg": "cpp", 30 | "cstddef": "cpp", 31 | "cstdint": "cpp", 32 | "cstdio": "cpp", 33 | "cstdlib": "cpp", 34 | "cstring": "cpp", 35 | "ctime": "cpp", 36 | "cwchar": "cpp", 37 | "cwctype": "cpp", 38 | "exception": "cpp", 39 | "algorithm": "cpp", 40 | "fstream": "cpp", 41 | "iomanip": "cpp", 42 | "iosfwd": "cpp", 43 | "iostream": "cpp", 44 | "istream": "cpp", 45 | "limits": "cpp", 46 | "memory": "cpp", 47 | "mutex": "cpp", 48 | "new": "cpp", 49 | "ostream": "cpp", 50 | "numeric": "cpp", 51 | "sstream": "cpp", 52 | "stdexcept": "cpp", 53 | "streambuf": "cpp", 54 | "cinttypes": "cpp", 55 | "typeinfo": "cpp", 56 | "random": "cpp" 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [platformio] 12 | default_envs = lolin32 13 | 14 | [env] 15 | build_unflags = -std=gnu++11 -Werror=reorder 16 | build_flags = -std=gnu++14 17 | build_src_flags = 18 | -Wall 19 | -Werror 20 | 21 | [env:lolin32] 22 | platform = espressif32 @ ^3 23 | board = lolin32 24 | framework = arduino 25 | 26 | lib_deps = 27 | EEPROM 28 | etlcpp/Embedded Template Library @ ^20.35.10 29 | https://github.com/tzapu/WiFiManager.git#e759a48 30 | https://github.com/positron96/AsyncTCP.git#features/keepalive 31 | 32 | ; PIO seems to select port automatically. 33 | ;upload_port = COM8 34 | ;monitor_port = COM8 35 | 36 | monitor_filters = esp32_exception_decoder 37 | monitor_speed = 115200 38 | 39 | [env:native] 40 | platform = native 41 | ;lib_ignore = 42 | ;build_flags = -Ilib/DCC 43 | 44 | ;test_src_filter = 45 | ;+<../lib/DCC/LocoAddress.cpp> 46 | -------------------------------------------------------------------------------- /src/CommandStation.cpp: -------------------------------------------------------------------------------- 1 | #include "CommandStation.h" 2 | 3 | CommandStation CS; 4 | 5 | uint8_t CommandStation::LocoData::dccSpeedByte() { 6 | uint8_t ret = speed.getDCCByte(speedMode); 7 | if(speedMode==SpeedMode::S14) ret |= this->fn[0] << 4; 8 | return ret; 9 | } -------------------------------------------------------------------------------- /src/CommandStation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | /** 3 | * Contains all the stuff related to command station. 4 | * I.e. high-level DCC generation, turnout list and their respective 5 | * settings. 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | #include "DCC.h" 12 | #include "LocoAddress.h" 13 | #include 14 | 15 | #include "Watchdog.h" 16 | 17 | 18 | #define CS_DEBUG 19 | 20 | #ifdef CS_DEBUG 21 | #define CS_DEBUGF(format, ...) do{ log_printf(ARDUHAL_LOG_FORMAT(I, format), ##__VA_ARGS__); } while(0) 22 | #else 23 | #define CS_DEBUGF 24 | #endif 25 | 26 | #define STATES CLOSED,THROWN 27 | enum class TurnoutState { 28 | STATES, UNKNOWN 29 | }; 30 | enum class TurnoutAction { 31 | STATES, TOGGLE 32 | }; 33 | 34 | /** Puts bit 0 of arg to 5th place, shifts bits 1-4 to right */ 35 | inline static uint8_t moveBit1to5(uint8_t normalByte) { 36 | return (normalByte & 0x1)<<4 | (normalByte & 0b1'1110)>>1; 37 | } 38 | 39 | /** Puts bit 5 of arg to 0th place, shifts bits 1-4 to left */ 40 | inline static uint8_t moveBit5to1(uint8_t dccByte) { 41 | return (dccByte & 0b0001'0000)>>4 | (dccByte & 0b0000'1111)<<1 ; 42 | } 43 | 44 | class CommandStation { 45 | public: 46 | 47 | static constexpr uint8_t N_FUNCTIONS = 29; 48 | 49 | static constexpr uint8_t MAX_SLOTS = 10; 50 | 51 | static constexpr millis_t PURGE_DELAY = 200*1000; //200s 52 | 53 | CommandStation(): dccMain(nullptr), dccProg(nullptr), locoNet(nullptr) { 54 | loadTurnouts(); 55 | } 56 | 57 | void setDccMain(IDCCChannel * ch) { dccMain = ch; } 58 | void setDccProg(IDCCChannel * ch) { dccProg = ch; } 59 | void setLocoNetBus(LocoNetBus *bus) { locoNet = bus; } 60 | 61 | void setPowerState(bool v) { 62 | if( dccMain!=nullptr ) dccMain->setPower(v); 63 | } 64 | 65 | bool getPowerState() const { 66 | return dccMain!=nullptr ? dccMain->getPower() 67 | //: dccProg!=nullptr ? dccProg->getPower() 68 | : false; 69 | } 70 | 71 | 72 | /* Define turnout object structures */ 73 | struct TurnoutData { 74 | uint16_t addr11; 75 | int userTag; 76 | TurnoutState tStatus; 77 | }; 78 | 79 | static constexpr int MAX_TURNOUTS = 15; 80 | using TurnoutMap = etl::map; 81 | TurnoutMap turnoutData; 82 | 83 | uint16_t getTurnoutCount() { return turnoutData.size(); } 84 | 85 | void loadTurnouts() { 86 | turnoutData[6] = { 6, 0, TurnoutState::CLOSED }; 87 | turnoutData[7] = { 7, 1, TurnoutState::CLOSED }; 88 | turnoutData[10] = { 10, 2, TurnoutState::UNKNOWN }; 89 | turnoutData[11] = { 11, 3, TurnoutState::THROWN }; 90 | 91 | /*sendDCCppCmd("T"); 92 | waitForDCCpp(); 93 | int t = 0; 94 | while(Serial.available()>0) { 95 | char data[maxCommandLength]; 96 | sprintf(data, "%s", readResponse().c_str() ); 97 | if (strlen(data)==0) break; 98 | int addr, sub, stat, id; 99 | int ret = sscanf(data, "%*c %d %d %d %d", &id, &addr, &sub, &stat ); 100 | turnoutData[t] = { ((addr-1)*4+1) + (sub&0x3) , id, stat==0 ? 2 : 4}; 101 | t++; 102 | }*/ 103 | } 104 | 105 | //const TurnoutData& getTurnout(uint16_t i) { return turnoutData[i]; } 106 | 107 | struct LocoData { 108 | using Fns = etl::bitset; 109 | LocoAddress addr; 110 | //uint8_t speed128; ///< <0=stop, 1=emgr, 2..127 = speed 0..max 111 | LocoSpeed speed; 112 | SpeedMode speedMode; 113 | int8_t dir; ///< 1 = FWD, 0 = REW 114 | Fns fn; 115 | bool refreshing; 116 | Watchdog wdt; 117 | bool allocated() const { return addr.isValid(); } 118 | void deallocate() { addr = LocoAddress(); } 119 | void kickWatchdog() { wdt.kick(); } 120 | uint8_t dccSpeedByte(); 121 | }; 122 | 123 | bool isSlotAllocated(uint8_t slot) const { 124 | if(slot<1 || slot>MAX_SLOTS) return true; 125 | return slots[slot-1].allocated(); 126 | } 127 | 128 | bool isLocoAllocated(LocoAddress addr) { 129 | return locoSlot.find(addr) != locoSlot.end(); 130 | } 131 | 132 | uint8_t findLocoSlot(LocoAddress addr) { 133 | auto it = locoSlot.find(addr); 134 | if(it != locoSlot.end() ) { 135 | return it->second; 136 | } 137 | return 0; 138 | } 139 | 140 | uint8_t locateFreeSlot() { 141 | if(locoSlot.size() < MAX_SLOTS) { 142 | for(int i=0; iunloadSlot(slot); 194 | } 195 | } 196 | 197 | void kickSlot(uint8_t slot) { 198 | LocoData &dd = getSlot(slot); 199 | if(!dd.allocated()) { CS_DEBUGF("slot not allocated"); return; } 200 | dd.kickWatchdog(); 201 | } 202 | 203 | LocoAddress getLocoAddr(uint8_t slot) { 204 | if(!isSlotAllocated(slot)) return LocoAddress{}; 205 | return getSlot(slot).addr; 206 | } 207 | 208 | const LocoData &getSlotData(uint8_t slot) { 209 | return getSlot(slot); 210 | } 211 | 212 | void setLocoSpeedMode(uint8_t slot, SpeedMode mode) { 213 | LocoData &dd = getSlot(slot); 214 | dd.kickWatchdog(); 215 | if(dd.speedMode == mode) return; 216 | dd.speedMode = mode; 217 | if(dd.refreshing) 218 | dccMain->sendThrottle(slot, dd.addr, dd.dccSpeedByte(), dd.speedMode, dd.dir); 219 | } 220 | 221 | SpeedMode getLocoSpeedMode(uint8_t slot) { 222 | return getSlot(slot).speedMode; 223 | } 224 | 225 | void setLocoFn(uint8_t slot, uint8_t fn, bool val) { 226 | LocoData &dd = getSlot(slot); 227 | dd.kickWatchdog(); 228 | if(dd.fn[fn] == val) return; 229 | 230 | dd.fn[fn] = val; 231 | DCCFnGroup fg; 232 | 233 | uint32_t ifn = dd.fn.value(); 234 | if (fn<5) fg = DCCFnGroup::F0_4; 235 | else if(fn<9) fg = DCCFnGroup::F5_8; 236 | else if(fn<13) fg = DCCFnGroup::F9_12; 237 | else if(fn<21) fg = DCCFnGroup::F13_20; 238 | else fg = DCCFnGroup::F21_28; 239 | dccMain->sendFunctionGroup(slot, dd.addr, fg, ifn); 240 | } 241 | 242 | void setLocoFns(uint8_t slot, uint32_t m, uint32_t f ) { 243 | LocoData &dd = getSlot(slot); 244 | dd.kickWatchdog(); 245 | uint32_t v = dd.fn.value(); 246 | // if required bits (m) intersect function group bits (GM) and these bits (f^v != 0) differ from current value, 247 | // update bits (v=) and send function group 248 | #define CHECK_SEND(GM, FG) if( ( (m&GM)!=0) && ( ( (v^f)&m&GM)!=0 ) ) \ 249 | { v = (v&(0xFFFF'FFFF&~GM)) | (f&m&GM); dccMain->sendFunctionGroup(slot, dd.addr, FG, v ); } 250 | 251 | CHECK_SEND( 0x1F, DCCFnGroup::F0_4); 252 | CHECK_SEND( 0x1E0, DCCFnGroup::F5_8); 253 | CHECK_SEND( 0x1E00, DCCFnGroup::F9_12); 254 | CHECK_SEND( 0x1F'E000, DCCFnGroup::F13_20); 255 | CHECK_SEND(0x1FE'0000, DCCFnGroup::F21_28); 256 | dd.fn = LocoData::Fns( v ); 257 | } 258 | 259 | bool getLocoFn(uint8_t slot, uint8_t fn) { 260 | return getSlot(slot).fn[fn] != 0; 261 | } 262 | 263 | /** 264 | * @param speed DCC speed (0=sop, 1=EMGR stop) 265 | * @param dir 1 - FWD, 0 - REW 266 | * */ 267 | void setLocoDir(uint8_t slot, uint8_t dir) { 268 | LocoData &dd = getSlot(slot); 269 | dd.kickWatchdog(); 270 | if(dd.dir==dir) return; 271 | dd.dir = dir; 272 | if(dd.refreshing) 273 | dccMain->sendThrottle(slot, dd.addr, dd.dccSpeedByte(), dd.speedMode, dd.dir); 274 | } 275 | 276 | uint8_t getLocoDir(uint8_t slot) { 277 | return getSlot(slot).dir; 278 | } 279 | 280 | /** 281 | * Updates slots that have not been used for a long time (PURGE_DELAY) 282 | */ 283 | void loop() { 284 | for(const auto &i: locoSlot) { 285 | uint8_t slot = i.second; 286 | LocoData &dd = getSlot(slot); 287 | if(dd.refreshing && dd.wdt.timedOut()) { 288 | CS_DEBUGF("slot %d timed out, current %lds, last update was at %lds", slot, 289 | millis()/1000, dd.wdt.getLastUpdate()/1000 ); 290 | setLocoSlotRefresh(slot, false); 291 | } 292 | } 293 | } 294 | 295 | /// Sets speed 296 | void setLocoSpeed(uint8_t slot, LocoSpeed spd) { 297 | LocoData &dd = getSlot(slot); 298 | dd.kickWatchdog(); 299 | if(dd.speed == spd) return; 300 | dd.speed = spd; 301 | if(dd.refreshing) 302 | dccMain->sendThrottle(slot, dd.addr, dd.dccSpeedByte(), dd.speedMode, dd.dir); 303 | } 304 | 305 | /// Returns speed 306 | LocoSpeed getLocoSpeed(uint8_t slot) { 307 | return getSlot(slot).speed; 308 | } 309 | 310 | void setLocoSpeedF(uint8_t slot, float spd) { 311 | setLocoSpeed(slot, LocoSpeed::fromFloat(spd) ); 312 | } 313 | 314 | float getLocoSpeedF(uint8_t slot) { 315 | return getLocoSpeed(slot).getFloat(); 316 | } 317 | 318 | int16_t readCVProg(uint16_t cv) { 319 | //IDCCChannel *dccProg = dccMain; 320 | if(dccProg==nullptr) return -2; 321 | return dccProg->readCVProg(cv); 322 | } 323 | bool verifyCVProg(uint16_t cv, uint8_t val) { 324 | //IDCCChannel *dccProg = dccMain; 325 | if(dccProg==nullptr) return false; 326 | return dccProg->verifyCVByteProg(cv, val); 327 | } 328 | bool writeCvProg(uint16_t cv, uint8_t val) { 329 | //IDCCChannel *dccProg = dccMain; 330 | if(dccProg ==nullptr) return false; 331 | return dccProg->writeCVByteProg(cv, val); 332 | } 333 | bool writeCvProgBit(uint16_t cv, uint8_t bit, bool val) { 334 | //IDCCChannel *dccProg = dccMain; 335 | if(dccProg ==nullptr) return false; 336 | return dccProg->writeCVBitProg(cv, bit, val); 337 | } 338 | void writeCvMain(LocoAddress addr, uint16_t cv, uint8_t val) { 339 | if(dccMain==nullptr) return; 340 | dccMain->writeCVByteMain(addr, cv, val); 341 | } 342 | void writeCvMainBit(LocoAddress addr, uint16_t cv, uint8_t bit, bool val) { 343 | if(dccMain==nullptr) return; 344 | dccMain->writeCVBitMain(addr, cv, bit, val?1:0); 345 | } 346 | 347 | const TurnoutMap& getTurnouts() { 348 | return turnoutData; 349 | } 350 | 351 | TurnoutState turnoutToggle(uint16_t aAddr, bool fromRoster) { 352 | return turnoutAction(aAddr, fromRoster, TurnoutAction::TOGGLE); 353 | } 354 | 355 | TurnoutState getTurnoutState(uint16_t aAddr) { 356 | auto t = turnoutData.find(aAddr); 357 | if(t != turnoutData.end() ) { 358 | return t->second.tStatus; 359 | } 360 | return TurnoutState::UNKNOWN; 361 | } 362 | 363 | TurnoutState turnoutAction(uint16_t aAddr, bool fromRoster, TurnoutAction action) { 364 | CS_DEBUGF("addr=%d named=%d action=%d", aAddr, fromRoster, (int)action ); 365 | 366 | TurnoutState newState = TurnoutState::THROWN; 367 | 368 | if(fromRoster) { 369 | auto t = turnoutData.find(aAddr); 370 | if(t != turnoutData.end() ) { 371 | if (action==TurnoutAction::TOGGLE) { 372 | newState = t->second.tStatus==TurnoutState::THROWN ? TurnoutState::CLOSED : TurnoutState::THROWN; 373 | } else { // throw or close 374 | newState = (TurnoutState)(int)action; 375 | } 376 | 377 | //sendDCCppCmd("T "+String(turnoutData[t].id)+" "+newStat); 378 | //dccMain.sendAccessory(turnoutData[t].addr, turnoutData[t].subAddr, newStat); 379 | t->second.tStatus = newState; 380 | aAddr = t->second.addr11; 381 | } else { 382 | CS_DEBUGF("Did not find turnout in roster"); 383 | return TurnoutState::UNKNOWN; 384 | } 385 | } else { 386 | if (action==TurnoutAction::TOGGLE) { 387 | CS_DEBUGF("Trying to toggle numeric turnout"); 388 | newState = TurnoutState::THROWN; 389 | } else { // throw or close 390 | newState = (TurnoutState)(int)action; 391 | } 392 | 393 | if(turnoutData.available()>0) { 394 | // add turnout to roster 395 | turnoutData[aAddr] = {aAddr, int(turnoutData.size()+1), newState}; 396 | CS_DEBUGF("Added new turnout to roster: ID=%d, addr=%d", aAddr, aAddr ); 397 | } 398 | } 399 | 400 | // send to DCC 401 | dccMain->sendAccessory(aAddr, newState==TurnoutState::THROWN); 402 | // send to LocoNet 403 | // FIXME: this is a dirty hack. 404 | // If LocoNet calls this function, it will be bounced back to bus. 405 | // Fortunately, right now, accessory commands from LocoNet do not get propagated to DCC 406 | // and this command is only called from WiThrottle code. 407 | if(locoNet!=nullptr) { 408 | LnMsg ttt = makeSwRec(aAddr, true, newState==TurnoutState::THROWN); 409 | locoNet->broadcast(ttt); 410 | } 411 | 412 | //sendDCCppCmd("a "+String(addr)+" "+sub+" "+int(newStat) ); 413 | 414 | return newState; 415 | } 416 | 417 | private: 418 | IDCCChannel * dccMain; 419 | IDCCChannel * dccProg; 420 | LocoNetBus* locoNet; 421 | 422 | etl::map locoSlot; 423 | 424 | LocoData slots[MAX_SLOTS]; ///< slot 1 has index 0 in this array. Slot 0 is invalid. 425 | inline LocoData & getSlot(uint8_t slot) { return slots[slot-1]; } 426 | 427 | }; 428 | 429 | extern CommandStation CS; -------------------------------------------------------------------------------- /src/LbServer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @see http://loconetovertcp.sourceforge.net/Protocol/LoconetOverTcp.html 3 | */ 4 | #pragma once 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | 16 | #define LB_DEBUG 17 | 18 | #ifdef LB_DEBUG 19 | #define LB_LOGI(format, ...) do{ log_printf(ARDUHAL_LOG_FORMAT(I, format), ##__VA_ARGS__); }while(0) 20 | #define LB_LOGD(format, ...) //do{ log_printf(ARDUHAL_LOG_FORMAT(D, format), ##__VA_ARGS__); }while(0) 21 | #else 22 | #define LB_LOGI(...) 23 | #define LB_LOGD(...) 24 | #endif 25 | 26 | 27 | #define FROM_HEX(c) ( ((c)>'9') ? ((c) & ~0x20)-'A'+0xA : ((c)-'0') ) 28 | 29 | class LbServer: public LocoNetConsumer { 30 | 31 | public: 32 | 33 | LbServer(const uint16_t port, LocoNetBus * const bus): bus(bus), port(port), server(port) { 34 | //server = Server(port); 35 | bus->addConsumer(this); 36 | 37 | server.onClient( [this](void*, AsyncClient* cli ) { 38 | if(clients.full()) { 39 | LB_LOGI("onConnect: Not accepting client: %s", cli->remoteIP().toString().c_str() ); 40 | cli->close(); 41 | return; 42 | } 43 | cli->setKeepAlive(10000, 2); 44 | clients.insert(cli); 45 | LB_LOGI("onConnect: New client(%X): %s", (intptr_t)cli, cli->remoteIP().toString().c_str() ); 46 | cli->write("VERSION ESP32 WiFi 0.1"); 47 | 48 | cli->onDisconnect([this](void*, AsyncClient* cli) { 49 | LB_LOGI("onDisconnect: Client(%X) disconnected", (intptr_t)cli ); 50 | clients.erase(cli); 51 | }); 52 | 53 | cli->onData( [this](void*, AsyncClient* cli, void *data, size_t len) { 54 | for(size_t i=0; ionError([this](void*, AsyncClient* cli, int8_t err) { 59 | LB_LOGI("onError(%X): %d", (intptr_t)cli, err); 60 | }); 61 | cli->onTimeout([this](void*, AsyncClient* cli, uint32_t time) { 62 | LB_LOGI("onTimeout(%X): %d", (intptr_t)cli, time); 63 | cli->close(); 64 | }); 65 | 66 | }, nullptr); 67 | } 68 | 69 | void begin() { 70 | MDNS.addService("lbserver","tcp", port); 71 | server.begin(); 72 | } 73 | 74 | void end() { 75 | server.end(); 76 | } 77 | 78 | 79 | void loop() { 80 | if (!clients.empty()) { 81 | while(!txQueue.empty()) { 82 | sendMessage(txQueue.front()); 83 | txQueue.pop(); 84 | } 85 | } 86 | } 87 | 88 | LN_STATUS onMessage(const lnMsg& msg) override { 89 | if( !txQueue.full() && !clients.empty() ) txQueue.push(msg); 90 | return LN_DONE; 91 | } 92 | 93 | private: 94 | 95 | LocoNetBus *bus; 96 | 97 | uint16_t port; 98 | 99 | AsyncServer server; 100 | etl::set clients; 101 | 102 | etl::queue txQueue; 103 | 104 | LocoNetMessageBuffer lbBuf; 105 | const static int LB_BUF_SIZE = 100; 106 | char lbStr[LB_BUF_SIZE]; 107 | int lbPos = 0; 108 | 109 | void processRx(char v, AsyncClient *cli) { 110 | lbStr[lbPos] = v; 111 | if(v=='\n' || v=='\r') { 112 | if(lbPos==0) return; // deal with CRLF ending 113 | lbStr[lbPos] = ' '; lbStr[lbPos+1]=0; 114 | LB_LOGD("Processing string '%s'", lbStr); 115 | if(strncmp("SEND ", lbStr, 5)==0) { 116 | for(uint8_t i=5; i<=lbPos; i++) { 117 | if(lbStr[i]==' ') { 118 | uint8_t val = FROM_HEX(lbStr[i-2])<<4 | FROM_HEX(lbStr[i-1]); 119 | LB_LOGD("LbServer::loop adding byte %02x from chars '%c' '%c' (pos %d)", val, lbStr[i-2], lbStr[i-1], i); 120 | LnMsg *msg = lbBuf.addByte(val); 121 | if(msg!=nullptr) { 122 | 123 | sendMessage(*msg); // echo 124 | LN_STATUS ret = bus->broadcast(*msg, this); 125 | 126 | if(ret==LN_DONE) cli->write("SENT OK\n"); else 127 | if(ret==LN_RETRY_ERROR) cli->write("SENT ERROR LN_RETRY_ERROR\n"); else 128 | cli->write("SENT ERROR generic\n"); 129 | break; 130 | } 131 | } 132 | } 133 | } else { 134 | LB_LOGI("Got line but it's not SEND: '%s'", lbStr); 135 | } 136 | lbPos=0; 137 | } else { 138 | lbPos++; 139 | } 140 | } 141 | 142 | void sendMessage(const LnMsg &msg) { 143 | 144 | char ttt[LB_BUF_SIZE] = "RECEIVE"; 145 | uint t = strlen(ttt); 146 | uint8_t ln = msg.length(); 147 | for(int j=0; jwrite(ttt); 154 | if(len != t) { 155 | LB_LOGI("cli(%x) tx length mismatch: expected %d, actual %d", (intptr_t)cli, t, len); 156 | } 157 | } 158 | } 159 | 160 | }; -------------------------------------------------------------------------------- /src/LocoNetSerial.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class LocoNetSerial: public LocoNetConsumer { 7 | 8 | public: 9 | LocoNetSerial(Stream * const str, LocoNetBus * const bus) : stream(str), bus(bus) { 10 | bus->addConsumer(this); 11 | } 12 | 13 | void begin() {} 14 | 15 | void end() {} 16 | 17 | void loop() { 18 | if(stream->available() ) { 19 | uint8_t inByte = stream->read(); 20 | lnMsg *msg = buf.addByte(inByte); 21 | if(msg != nullptr) { 22 | bus->broadcast(*msg, this); 23 | } 24 | } 25 | } 26 | 27 | virtual LN_STATUS onMessage(const lnMsg& msg) { 28 | uint8_t ln = msg.length(); 29 | for(int j=0; jwrite(msg.data[j]); 31 | } 32 | return LN_DONE; 33 | } 34 | 35 | private: 36 | Stream *stream; 37 | LocoNetBus * bus; 38 | 39 | LocoNetMessageBuffer buf; 40 | 41 | }; -------------------------------------------------------------------------------- /src/LocoNetSlotManager.cpp: -------------------------------------------------------------------------------- 1 | #include "LocoNetSlotManager.h" 2 | 3 | #define LOG_LEVEL LEVEL_INFO 4 | #include "log.h" 5 | 6 | /// LocoNet 1.0 tells 0x7F, but JMRI expects OPC_WR_SL_DATA 7 | constexpr uint8_t PROG_LACK = OPC_WR_SL_DATA;//0x7F; 8 | 9 | static LocoAddress lnAddr(uint16_t addr) { 10 | if(addr<=127) { return LocoAddress::shortAddr(addr); } 11 | return LocoAddress::longAddr(addr); 12 | } 13 | 14 | // reverse to ADDR(hi,lo) ( ((lo) | (((hi) & 0x0F ) << 7)) ) 15 | inline static uint8_t addrLo(const LocoAddress &addr) { 16 | return addr.addr() & 0b00011111; 17 | } 18 | 19 | inline static uint8_t addrHi(const LocoAddress &addr) { 20 | return (addr.addr() >> 7); 21 | } 22 | 23 | using LocoData = CommandStation::LocoData; 24 | 25 | inline static uint8_t speedMode2int(SpeedMode sm) { 26 | using SM = SpeedMode; 27 | switch(sm) { 28 | case SM::S128: return DEC_MODE_128; 29 | case SM::S28: return DEC_MODE_28; 30 | case SM::S14: return DEC_MODE_14; 31 | } 32 | LOGW("bad speed mode: %d", (int)sm); 33 | return DEC_MODE_128; 34 | } 35 | 36 | inline static SpeedMode int2SpeedMode(uint8_t sm) { 37 | using SM = SpeedMode; 38 | sm &= DEC_MODE_MASK; 39 | if(sm == DEC_MODE_128) return SM::S128; 40 | if(sm == DEC_MODE_14) return SM::S14; 41 | if(sm == DEC_MODE_28) return SM::S28; 42 | LOGW("bad speed mode bits: %x", (int)sm); 43 | return SM::S128; 44 | } 45 | 46 | LocoNetSlotManager::LocoNetSlotManager(LocoNetBus * const ln): _ln(ln) { 47 | ln->addConsumer(this); 48 | } 49 | 50 | void LocoNetSlotManager::fillSlotMsg(uint8_t slot, rwSlotDataMsg &sd) { 51 | sd.command = OPC_SL_RD_DATA; 52 | sd.mesg_size = 14; 53 | sd.slot = slot; 54 | 55 | if(!CS.isSlotAllocated(slot) ) { 56 | sd.stat = DEC_MODE_128 | LOCO_FREE; 57 | sd.adr = 0; 58 | sd.spd = 0; 59 | sd.spd = 0; 60 | sd.spd = 0; 61 | sd.dirf = DIRF_DIR; // FWD 62 | sd.adr2 = 0; 63 | sd.snd = 0; 64 | 65 | sd.ss2 = 0; 66 | sd.id1 = slot; 67 | sd.id2 = 0; 68 | } else { 69 | const CommandStation::LocoData &d = CS.getSlotData(slot); 70 | uint32_t fns = d.fn.value(); 71 | sd.stat = speedMode2int(d.speedMode) | STAT1_SL_BUSY; 72 | if(d.refreshing) sd.stat |= STAT1_SL_ACTIVE; 73 | sd.adr = addrLo(d.addr); 74 | sd.spd = d.speed.get128(); 75 | sd.dirf = d.dir==1 ? DIRF_DIR : 0; 76 | sd.dirf |= moveBit1to5(fns); 77 | sd.adr2 = addrHi(d.addr); 78 | sd.snd = (fns & 0b1'1110'0000)>>5; 79 | 80 | const LnSlotData & e = extra[slot]; 81 | sd.ss2 = e.ss2; 82 | sd.id1 = e.id1; 83 | sd.id2 = e.id2; 84 | } 85 | 86 | sd.trk = GTRK_IDLE | GTRK_MLOK1; // no emgr across layout, & Loconet 1.1 by default; 87 | if(CS.getPowerState()) sd.trk |= GTRK_POWER; 88 | } 89 | 90 | #define LOGI_SLOT(TAG, I, S) LOGI( TAG \ 91 | " slot %d: ADDR=%d STAT=%02X(%s) ID=%02X%02X", I, \ 92 | ADDR(S.adr2, S.adr), S.stat, LOCO_STAT(S.stat), S.id1, S.id2 ) 93 | 94 | void LocoNetSlotManager::processMessage(const lnMsg* msg) { 95 | 96 | switch(msg->data[0]) { 97 | case OPC_GPON: 98 | CS.setPowerState(true); 99 | break; 100 | case OPC_GPOFF: 101 | CS.setPowerState(false); 102 | break; 103 | case OPC_LOCO_ADR: { 104 | int slot = locateSlot( msg->la.adr_hi, msg->la.adr_lo ); 105 | if(slot<=0) { 106 | LOGI("OPC_LOCO_ADR for addr %d, no available slots", ADDR(msg->la.adr_hi, msg->la.adr_lo) ); 107 | sendLack(OPC_LOCO_ADR); 108 | break; 109 | } 110 | 111 | LOGI("OPC_LOCO_ADR for addr %d, found slot %d", ADDR(msg->la.adr_hi, msg->la.adr_lo), slot); 112 | sendSlotData(slot); 113 | break; 114 | } 115 | case OPC_MOVE_SLOTS: { 116 | uint8_t srcSlot = msg->sm.src; 117 | uint8_t dstSlot = msg->sm.dest; 118 | if( dstSlot==srcSlot && slotValid(srcSlot)) { 119 | LOGI("OPC_MOVE_SLOTS NULL MOVE for slot %d", srcSlot ); 120 | CS.setLocoSlotRefresh(srcSlot, true); // enable refresh 121 | sendSlotData(srcSlot); 122 | } else 123 | if(dstSlot==0 && slotValid(srcSlot) ) { 124 | LOGI("OPC_MOVE_SLOTS DISPATCH PUT for slot %d", srcSlot ); 125 | if(haveDispatchedSlot() ) { 126 | sendLack(OPC_MOVE_SLOTS, 0); 127 | } else { 128 | dispatchedSlot = srcSlot; 129 | sendSlotData(dispatchedSlot); 130 | } 131 | } else 132 | if(srcSlot == 0 ) { 133 | // DISPATCH GET 134 | LOGI("OPC_MOVE_SLOTS DISPATCH GET" ); 135 | if(haveDispatchedSlot() ) { 136 | sendSlotData(dispatchedSlot); 137 | removeDispatchedSlot(); 138 | } else { 139 | sendLack(OPC_MOVE_SLOTS, 0); 140 | } 141 | } else 142 | if(slotValid(srcSlot) && slotValid(dstSlot)) { 143 | // a valid move, but we don't support it atm 144 | sendLack(OPC_MOVE_SLOTS); 145 | } else { 146 | sendLack(OPC_MOVE_SLOTS); 147 | } 148 | break; 149 | } 150 | case OPC_SLOT_STAT1: { 151 | uint8_t slot = msg->ss.slot; 152 | if( !slotValid(slot) ) { sendLack(OPC_LOCO_SND); break; } 153 | processStat1(slot, msg->ss.stat); 154 | break; 155 | } 156 | case OPC_LOCO_SND: { 157 | uint8_t slot = msg->ls.slot; 158 | if( !slotValid(slot) ) { sendLack(OPC_LOCO_SND); break; } 159 | processSnd(slot, msg->ls.snd); 160 | break; 161 | } 162 | case OPC_LOCO_DIRF: { 163 | uint8_t slot = msg->ldf.slot; 164 | if( !slotValid(slot) ) { sendLack(OPC_LOCO_DIRF); break; } 165 | processDirf(slot, msg->ldf.dirf); 166 | break; 167 | } 168 | case OPC_LOCO_SPD : { 169 | uint8_t slot = msg->lsp.slot; 170 | if( !slotValid(slot) ) { sendLack(OPC_LOCO_SPD); break; } 171 | processSpd(slot, msg->lsp.spd); 172 | break; 173 | } 174 | case OPC_WR_SL_DATA: { 175 | const rwSlotDataMsg & m = msg->sd; 176 | uint8_t slot = m.slot; 177 | if(m.slot == PRG_SLOT) { 178 | processProgMsg(msg->pt); 179 | break; 180 | } 181 | if( !slotValid(slot) ) { sendLack(OPC_WR_SL_DATA); break; } 182 | rwSlotDataMsg _slot; 183 | fillSlotMsg(slot, _slot); 184 | 185 | if(_slot.stat != m.stat) processStat1(slot, m.stat); 186 | if( !CS.isSlotAllocated(slot) ) break; // stat1 can set slot to inactive, do not continue in this case 187 | if(_slot.spd != m.spd) processSpd(slot, m.spd); 188 | if(_slot.dirf != m.dirf) processDirf(slot, m.dirf); 189 | if(_slot.snd != m.snd) processSnd(slot, m.snd); 190 | 191 | if(extra.find(slot) != extra.end() ) { 192 | extra[slot] = LnSlotData{}; 193 | } 194 | LnSlotData &e = extra[slot]; 195 | e.ss2 = m.ss2; 196 | e.id1 = m.id1; 197 | e.id2 = m.id2; 198 | 199 | LOGI_SLOT("OPC_WR_SL_DATA", slot, m); 200 | 201 | break; 202 | } 203 | case OPC_RQ_SL_DATA: { 204 | uint8_t slot = msg->sr.slot; 205 | if( !slotValid(slot) ) { sendLack(OPC_RQ_SL_DATA); break;} 206 | LOGI("OPC_RQ_SL_DATA slot %d", slot); 207 | sendSlotData(slot); 208 | break; 209 | } 210 | default: break; 211 | } 212 | 213 | } 214 | 215 | 216 | 217 | int LocoNetSlotManager::locateSlot(uint8_t hi, uint8_t lo) { 218 | LocoAddress addr = (hi==0) ? LocoAddress::shortAddr(lo) : LocoAddress::longAddr(ADDR(hi,lo)); 219 | uint8_t slot = CS.findLocoSlot(addr); 220 | if(slot==0) { 221 | slot = CS.locateFreeSlot(); 222 | if(slot==0) { return 0; } 223 | CS.initLocoSlot(slot, addr); 224 | extra[slot] = LnSlotData{}; 225 | } 226 | return slot; 227 | } 228 | 229 | void LocoNetSlotManager::releaseSlot(uint8_t slot) { 230 | CS.releaseLocoSlot(slot); 231 | extra.erase(slot); 232 | } 233 | 234 | void LocoNetSlotManager::sendSlotData(uint8_t slot) { 235 | LnMsg ret; 236 | fillSlotMsg(slot, ret.sd); 237 | 238 | LOGI_SLOT("Sending", slot, (ret.sd)); 239 | 240 | writeChecksum(ret); 241 | _ln->broadcast(ret, this); 242 | } 243 | 244 | void LocoNetSlotManager::sendLack(uint8_t cmd, uint8_t arg) { 245 | LnMsg lack = makeLongAck(cmd, arg); 246 | _ln->broadcast(lack, this); 247 | } 248 | 249 | void LocoNetSlotManager::processDirf(uint8_t slot, uint v) { 250 | LOGI("OPC_LOCO_DIRF slot %d dirf %02x", slot, v); 251 | uint8_t dir = ((v & DIRF_DIR) == DIRF_DIR) ? 0 : 1; 252 | CS.setLocoDir(slot, dir); 253 | // fn order in received byte is 04321, needs swapping 254 | CS.setLocoFns(slot, 0b0001'1111, moveBit5to1(v) ); 255 | } 256 | 257 | void LocoNetSlotManager::processSnd(uint8_t slot, uint8_t snd) { 258 | LOGI("OPC_LOCO_SND slot %d snd %02x", slot, snd); 259 | CS.setLocoFns(slot, 0x1E0, snd << 5 ); 260 | } 261 | 262 | void LocoNetSlotManager::processStat1(uint8_t slot, uint8_t stat) { 263 | LOGI("OPC_SLOT_STAT1 slot %d stat1 %02x", slot, stat); 264 | 265 | auto newSpeedMode = int2SpeedMode(stat); 266 | bool newActive = (stat & STAT1_SL_ACTIVE) == STAT1_SL_ACTIVE; 267 | bool newBusy = (stat & STAT1_SL_BUSY) == STAT1_SL_BUSY; 268 | if( !newBusy ) { 269 | releaseSlot(slot); 270 | return; 271 | } 272 | 273 | const LocoData &dd = CS.getSlotData(slot); 274 | if(newSpeedMode != dd.speedMode) CS.setLocoSpeedMode(slot, newSpeedMode); 275 | if(newActive != dd.refreshing) CS.setLocoSlotRefresh(slot, newActive); 276 | } 277 | 278 | void LocoNetSlotManager::processSpd(uint8_t slot, uint8_t spd) { 279 | LOGI("OPC_LOCO_SPD slot %d spd %d", slot, spd); 280 | CS.setLocoSpeed(slot, LocoSpeed::from128(spd) ); 281 | } 282 | 283 | void LocoNetSlotManager::sendProgData(progTaskMsg ret, uint8_t pstat, uint8_t value ) { 284 | 285 | LOGI("pstat=%02xh, val=%d", pstat, value); 286 | 287 | ret.command = OPC_SL_RD_DATA; 288 | ret.mesg_size = 14; 289 | ret.slot = PRG_SLOT; 290 | ret.pstat = pstat; 291 | //value = (((progTaskMsg.cvh & CVH_D7) << 6) | (progTaskMsg.data7 & 0x7f)) 292 | bitWrite(ret.cvh, 1, (value>>7)); 293 | ret.data7 = value & 0x7F; 294 | 295 | LnMsg msg; msg.pt = ret; 296 | writeChecksum(msg); 297 | _ln->broadcast(msg, this); 298 | } 299 | 300 | void LocoNetSlotManager::processProgMsg(const progTaskMsg &msg) { 301 | uint16_t cv = PROG_CV_NUM(msg)+1; 302 | uint8_t mode = PCMD_MODE_MASK & msg.pcmd; 303 | uint8_t val = PROG_DATA(msg); 304 | uint16_t addr = (msg.hopsa&0x7F)<<7 | (msg.lopsa & 0x7F); 305 | bool read = (msg.pcmd & PCMD_RW)==0; 306 | if(read) { 307 | switch(mode) { 308 | case DIR_BYTE_ON_SRVC_TRK: { 309 | LOGI("Read byte on prog CV%d", cv); 310 | sendLack(PROG_LACK, 1); // ack ok 311 | int16_t ret = CS.readCVProg(cv); 312 | sendProgData(msg, (ret>=0) ? 0 : PSTAT_READ_FAIL, ret>=0?ret:0); 313 | break; 314 | } 315 | case SRVC_TRK_RESERVED: {// make it a verify command. 316 | LOGI("Verify byte on prog CV%d==%d", cv, val); 317 | sendLack(PROG_LACK, 1); // ack ok 318 | bool ret = CS.verifyCVProg(cv, val); 319 | sendProgData(msg, ret?0:PSTAT_READ_FAIL, val); 320 | break; 321 | } 322 | default: 323 | sendLack(PROG_LACK, 0x7F); // not implemented 324 | break; 325 | } 326 | } else { // write 327 | switch(mode) { 328 | case DIR_BYTE_ON_SRVC_TRK: { 329 | LOGI("Write byte on prog CV%d=%d", cv, val); 330 | sendLack(PROG_LACK, 1); // ack ok 331 | bool ret = CS.writeCvProg(cv, val); 332 | sendProgData(msg, ret?0:PSTAT_WRITE_FAIL, val); 333 | break; 334 | } 335 | /*case DIR_BIT_ON_SRVC_TRK: 336 | sendLack(0x7F, 1); // ack ok 337 | bool ret = CS.writeCvProgBit(cv, 0, val); 338 | break;*/ 339 | case OPS_BYTE_NO_FEEDBACK: 340 | LOGI("Read byte on prog CV%d", cv); 341 | sendLack(PROG_LACK, 0x40); // ack ok, no reply will follow 342 | CS.writeCvMain(lnAddr(addr), cv, val); 343 | break; 344 | /*case OPS_BIT_NO_FEEDBACK: 345 | sendLack(0x7F, 0x40); // ack ok, no reply will follow 346 | CS.writeCvMainBit(lnAddr(addr), cv, val); 347 | break;*/ 348 | default: 349 | sendLack(PROG_LACK, 0x7F); // not implemented 350 | break; 351 | 352 | } 353 | } 354 | 355 | //sendLack(PROG_LACK, 1); // ack ok 356 | //sendLack(PROG_LACK, 0x40); // ack ok, no reply will follow 357 | 358 | } -------------------------------------------------------------------------------- /src/LocoNetSlotManager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "CommandStation.h" 7 | 8 | class LocoNetSlotManager : public LocoNetConsumer { 9 | 10 | public: 11 | LocoNetSlotManager(LocoNetBus * const ln); 12 | 13 | //void initSlot(uint8_t i, uint8_t addrHi=0, uint8_t addrLo=0); 14 | 15 | LN_STATUS onMessage(const lnMsg& msg) override { 16 | processMessage(&msg); 17 | return LN_DONE; 18 | } 19 | 20 | void processMessage(const lnMsg* msg); 21 | 22 | 23 | private: 24 | 25 | LocoNetBus * const _ln; 26 | 27 | uint8_t dispatchedSlot; 28 | 29 | struct LnSlotData { 30 | uint8_t ss2; 31 | uint8_t id1; 32 | uint8_t id2; 33 | LnSlotData(): ss2(0),id1(0),id2(0) {} 34 | }; 35 | 36 | etl::map extra; 37 | 38 | bool slotValid(uint8_t slot) { 39 | return (slot>=1) && (slot < CommandStation::MAX_SLOTS); 40 | } 41 | 42 | bool haveDispatchedSlot() { return slotValid(dispatchedSlot); } 43 | void removeDispatchedSlot() { dispatchedSlot = 0;} 44 | 45 | int locateSlot(uint8_t hi, uint8_t lo); 46 | 47 | void releaseSlot(uint8_t slot); 48 | 49 | void sendSlotData(uint8_t slot); 50 | 51 | void sendLack(uint8_t cmd, uint8_t arg=0); 52 | 53 | void sendProgData(progTaskMsg, uint8_t pstat, uint8_t value ); 54 | 55 | void processDirf(uint8_t slot, uint v) ; 56 | 57 | void processSnd(uint8_t slot, uint8_t snd); 58 | 59 | void processStat1(uint8_t slot, uint8_t stat) ; 60 | 61 | void processSpd(uint8_t slot, uint8_t spd); 62 | 63 | void processProgMsg(const progTaskMsg &msg); 64 | 65 | void fillSlotMsg(uint8_t slot, rwSlotDataMsg &msg); 66 | 67 | }; 68 | -------------------------------------------------------------------------------- /src/Watchdog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | using millis_t = decltype(millis()); 7 | 8 | #define W_LOGI(format, ...) do{ log_printf(ARDUHAL_LOG_FORMAT(I, format), ##__VA_ARGS__); } while(0) 9 | 10 | /** 11 | * @param TIMEOUT how long to wait for update before timing out, ms 12 | * @param FUTURE_COMPENSATION - if somewhy kick happens "after" timedOut, then math inside 13 | * timedOut goes crazy and fires timedOut immediately. For this reason, kick stored timestamp 14 | * rolled back some time in the past (by this value, in ms) 15 | */ 16 | template 17 | class Watchdog { 18 | millis_t lastUpdate; 19 | 20 | public: 21 | Watchdog(): lastUpdate(0) {} 22 | 23 | void kick() { lastUpdate = millis()-FUTURE_COMPENSATION; } 24 | 25 | millis_t getLastUpdate() { return lastUpdate; } 26 | 27 | bool timedOut() { 28 | millis_t ms = millis(); 29 | //if(ms-lastUpdate >= TIMEOUT) W_LOGI("timeout at %ld, last update was at %ld", ms, getLastUpdate() ); 30 | return ms-lastUpdate >= TIMEOUT; 31 | } 32 | 33 | template 34 | std::enable_if_t timedOut2() { 35 | return millis()-lastUpdate >= TIMEOUT2; 36 | } 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /src/WiThrottle.cpp: -------------------------------------------------------------------------------- 1 | #include "WiThrottle.h" 2 | 3 | #include "DCC.h" 4 | #define FILE_LOG_LEVEL LEVEL_DEBUG 5 | #include "log.h" 6 | 7 | #include 8 | #include 9 | 10 | /* Network parameters */ 11 | #define TURNOUT_PREF "LT" 12 | #define TURNOUT_UNKNOWN '1' 13 | #define TURNOUT_CLOSED '2' 14 | #define TURNOUT_THROWN '4' 15 | 16 | #define DELIM "<;>" 17 | 18 | 19 | String addr2str(const LocoAddress & a) { 20 | return (a.isShort() ? 'S' : 'L') + String(a.addr()); 21 | } 22 | 23 | LocoAddress str2addr(etl::string_view addr) { 24 | uint16_t iLocoAddr = etl::to_arithmetic(addr.substr(1)).value(); 25 | if (addr[0] == 'S') return LocoAddress::shortAddr((uint8_t)iLocoAddr); 26 | if (addr[0] == 'L') return LocoAddress::longAddr(iLocoAddr); 27 | return LocoAddress(); 28 | } 29 | 30 | inline int invert(int value) { 31 | return (value == 0) ? 1 : 0; 32 | }; 33 | 34 | char turnoutState2Chr(TurnoutState s) { 35 | switch(s) { 36 | case TurnoutState::THROWN : return TURNOUT_THROWN; 37 | case TurnoutState::CLOSED: return TURNOUT_CLOSED; 38 | case TurnoutState::UNKNOWN : return TURNOUT_UNKNOWN; 39 | default: 40 | LOGW("Unknown turnout state: %d", (int)s); 41 | return TURNOUT_UNKNOWN; 42 | } 43 | } 44 | 45 | int speedMode2int(SpeedMode sm) { 46 | switch(sm) { 47 | case SpeedMode::S128: return 1; 48 | case SpeedMode::S14: return 8; 49 | case SpeedMode::S28: return 2; 50 | default: return 0; 51 | } 52 | } 53 | 54 | int speed2int(LocoSpeed s) { 55 | if(s==SPEED_EMGR) return -1; 56 | if(s==SPEED_IDLE) return 0; 57 | return s.get128()-1; // 2..127 -> 1..126 58 | } 59 | 60 | LocoSpeed int2speed(int s) { 61 | if(s<0) return SPEED_EMGR; 62 | if(s==0) return SPEED_IDLE; 63 | return LocoSpeed::from128((uint8_t)(s+1)); // 1..126 -> 2..127 64 | } 65 | 66 | 67 | void WiThrottleServer::onNewClient(AsyncClient* cli) { 68 | if(clients.full()) { 69 | LOGI("onConnect: Not accepting client: %s, no space left", cli->remoteIP().toString().c_str() ); 70 | cli->close(); 71 | return; 72 | } 73 | // here keepalives are built into protocol, no need for TCP keepalives 74 | //cli->setKeepAlive(10000, 2); 75 | bool initNewClient = true; 76 | for(auto it = clients.cbegin(); it!=clients.cend(); ) { 77 | const auto &c = it->first; 78 | if(c->getRemoteAddress() == cli->getRemoteAddress()) { 79 | LOGI("onConnect: There is a client(%X) with this addr already", (intptr_t)c); 80 | initNewClient = false; 81 | clients[cli] = it->second; // copy ClientData 82 | ClientData& cc = clients[cli]; 83 | cc.rxpos = 0; 84 | cc.cli = cli; 85 | cc.updateHeartbeat(); 86 | it = clients.erase(it); // onDisconnect(c) won't find the client and won't cleanup ClientData 87 | c->close(); 88 | break; 89 | } else it++; 90 | } 91 | if(initNewClient) clientStart(cli); 92 | 93 | cli->setAckTimeout(HEARTBEAT_INTL*1000*3); 94 | LOGI("onConnect: New client(%X): %s, now have %d clients", 95 | (intptr_t)cli, cli->remoteIP().toString().c_str(), clients.size() ); 96 | 97 | cli->onDisconnect([this](void*, AsyncClient* cli_) { 98 | LOGI("onDisconnect: Client(%X) disconnected", (intptr_t)cli_ ); 99 | auto it = clients.find(cli_); 100 | if(it==clients.end() ) { 101 | LOGW("Not clearing up client(%X), it is not registered", (intptr_t)cli_); 102 | return; 103 | } 104 | ClientData &cc = it->second; 105 | clientStop(cc); 106 | }); 107 | 108 | cli->onData([this](void*, AsyncClient* cli_, void *data, size_t len) { 109 | auto it = clients.find(cli_); 110 | if(it==clients.end() ) { 111 | // after sending Q(quit), the client might send other commands, ignore them 112 | LOGW("Got some data from client(%X), but it is not recognised", (intptr_t)cli_); 113 | return; 114 | } 115 | ClientData &cc = it->second; 116 | if(cc.heartbeatEnabled) { 117 | cc.updateHeartbeat(); 118 | } 119 | for(size_t i=0; ionError([this](void*, AsyncClient* cli_, int8_t err) { 137 | LOGW("onError(%X): %d", (intptr_t)cli_, err); 138 | }); 139 | cli->onTimeout([this](void*, AsyncClient* cli_, uint32_t time) { 140 | LOGI("onTimeout(%X): %d", (intptr_t)cli_, time); 141 | cli_->close(); 142 | }); 143 | } 144 | 145 | WiThrottleServer::WiThrottleServer(uint16_t port, const char* name) : 146 | port{port}, name{name}, server(port) 147 | { 148 | server.onClient([this](void*, AsyncClient* cli ){this->onNewClient(cli);}, nullptr); 149 | } 150 | 151 | void WiThrottleServer::begin() { 152 | 153 | LOGI("WiThrottleServer::begin"); 154 | 155 | server.begin(); 156 | 157 | MDNS.addService("withrottle","tcp", port); 158 | 159 | notifyPowerStatus(); 160 | 161 | } 162 | 163 | void WiThrottleServer::loop() { 164 | for (auto &p: clients) { 165 | ClientData &cc = p.second; 166 | if (cc.heartbeatEnabled) { 167 | cc.checkHeartbeat(); 168 | } 169 | } 170 | } 171 | 172 | void WiThrottleServer::notifyHearbeatStatus(ClientData &c) { 173 | wifiPrintln(c.cli, "*" + String(HEARTBEAT_INTL)); 174 | } 175 | 176 | void WiThrottleServer::processCmd(ClientData & cc) { 177 | etl::string_view dataStr{cc.rx}; 178 | LOGD("RX '%s'(len %d)", cc.rx, dataStr.length()); 179 | switch(dataStr[0]) { 180 | case '*': { 181 | if(dataStr.length()>1) { 182 | switch(dataStr[1] ) { 183 | case '+' : cc.heartbeatEnabled = true; cc.updateHeartbeat(); break; 184 | case '-' : cc.heartbeatEnabled = false; break; 185 | default: LOGW("Unknown * cmd: %s", dataStr.data()); 186 | } 187 | LOGI("Heartbeat is %s", cc.heartbeatEnabled?"ON":"OFF"); 188 | } 189 | break; 190 | } 191 | case 'P': { 192 | if (dataStr.starts_with("PPA") ) { 193 | turnPower(dataStr[3]); 194 | } else if (dataStr.starts_with("PTA")) { 195 | char action = dataStr[3]; 196 | unsigned aAddr; 197 | bool named; 198 | if(dataStr.substr(4,2)==TURNOUT_PREF) { 199 | // named turnout 200 | aAddr = etl::to_arithmetic(dataStr.substr(6)).value(); 201 | named = true; 202 | } else { 203 | aAddr = etl::to_arithmetic(dataStr.substr(4)).value(); 204 | named = false; 205 | } 206 | accessoryToggle(aAddr, action, named, cc); 207 | } 208 | break; 209 | } 210 | case 'N': { // device name 211 | auto remoteName = dataStr.substr(1); 212 | LOGI("Device name: '%s'", remoteName.data()); 213 | notifyHearbeatStatus(cc); 214 | break; 215 | } 216 | case 'H': { 217 | if(dataStr[1]=='U') { 218 | auto hwId = dataStr.substr(2); 219 | LOGI("Hardware ID: '%s'", hwId.data() ); 220 | cc.hwId = String(hwId.data()); 221 | // kill other clients with same ID (assume it's reconnect from same device) 222 | const auto cli = cc.cli; 223 | for(auto it = clients.cbegin(); it!=clients.cend(); ) { 224 | const auto &c = it->first; 225 | if(c == cli) {it++; continue;} 226 | if(it->second.hwId == hwId.data()) { 227 | LOGW(" There is a client(%X) with this ID already. Kicking it!", (intptr_t)c); 228 | cc = it->second; // copy ClientData 229 | cc.rxpos = 0; 230 | cc.cli = cli; 231 | cc.updateHeartbeat(); 232 | it = clients.erase(it); // onDisconnect(c) won't find the client and won't cleanup ClientData 233 | c->close(); 234 | break; 235 | } else it++; 236 | } 237 | } 238 | break; 239 | } 240 | case 'M': { 241 | char th = dataStr[1]; 242 | char action = dataStr[2]; 243 | auto actionData = dataStr.substr(3); 244 | size_t delimiter = actionData.find(DELIM); 245 | if(delimiter == -1) { 246 | LOGW("Malformed M command"); 247 | break; 248 | } 249 | auto actionKey = actionData.substr(0, delimiter); 250 | auto actionVal = actionData.substr(delimiter+3); 251 | if (action == '+') { 252 | cc.locoAdd(th, actionKey); 253 | } else if (action == '-') { 254 | cc.locosRelease(th, actionKey); 255 | } else if (action == 'A') { 256 | cc.locosAction(th, actionKey, actionVal); 257 | } 258 | break; 259 | } 260 | case 'Q': 261 | // quit 262 | //clientStop(cc); 263 | cc.cli->close(); 264 | break; 265 | default: 266 | break; 267 | } 268 | } 269 | 270 | void WiThrottleServer::clientStart(AsyncClient *cli) { 271 | ClientData cc; 272 | LOGI( "New client " ); 273 | 274 | wifiPrintln(cli, "VN2.0"); 275 | if(name!=nullptr) wifiPrintln(cli, String("Ht")+name ); 276 | wifiPrintln(cli, "RL0"); // roster list is size 0 277 | notifyPowerStatus(cli); 278 | wifiPrintln(cli, "PTT]\\[Turnouts}|{Turnout]\\[Closed}|{"+String(TURNOUT_CLOSED)+"]\\[Thrown}|{"+String(TURNOUT_THROWN) ); 279 | wifiPrint(cli, "PTL"); 280 | for(const auto &t: CS.getTurnouts() ) { 281 | //for (int t = 0 ; tconnected() ) cli->stop(); 313 | clients.erase(cli); 314 | } 315 | 316 | void WiThrottleServer::ClientData::locoAdd(char th, etl::string_view sLocoAddr) { 317 | LocoAddress addr = str2addr(sLocoAddr); 318 | AddrToSlotMap &slotmap = slots[th]; 319 | if(slotmap.available()==0 && slotmap.find(addr)==slotmap.end() ) { 320 | sendMessage("No space for new loco", true); 321 | LOGI("locoAdd(thr=%c, addr=%d) no space for new loco\n", th, addr.addr() ); 322 | return; 323 | } 324 | 325 | uint8_t slot = CS.findOrAllocateLocoSlot(addr); // TODO: check result 326 | slotmap[addr] = slot; 327 | 328 | sendThrottleMsg(th,'+',addr, ""); 329 | for (uint8_t fKey=0; fKey tmp; 344 | for(const auto& slot: slots[th]) { 345 | tmp.push_back(slot.first); 346 | } 347 | for(const auto& addr: tmp) { 348 | locoRelease(th, addr); 349 | } 350 | } else { 351 | LocoAddress iLocoAddr = str2addr(sLocoAddr); 352 | locoRelease(th, iLocoAddr); 353 | } 354 | } 355 | 356 | void WiThrottleServer::ClientData::locoRelease(char th, LocoAddress addr) { 357 | String sAddr = addr; 358 | LOGI("loco release thr=%c; addr=%s", th, sAddr.c_str() ); 359 | sendThrottleMsg(th,'-',addr, "" ); 360 | 361 | auto &throttle = slots[th]; 362 | auto it = throttle.find(addr); 363 | if(it == throttle.end()) { 364 | LOGW("No loco '%s' in this throttle", sAddr.c_str()); 365 | return; 366 | } 367 | uint8_t slot = it->second; 368 | CS.releaseLocoSlot(slot); 369 | slots[th].erase(addr); 370 | } 371 | 372 | 373 | void WiThrottleServer::ClientData::locosAction(char th, etl::string_view sLocoAddr, etl::string_view actionVal) { 374 | if(sLocoAddr=="*") { 375 | for(const auto& slot: slots[th]) 376 | locoAction(th, slot.first, actionVal); 377 | } else { 378 | LocoAddress iLocoAddr = str2addr(sLocoAddr); 379 | locoAction(th, iLocoAddr, actionVal); 380 | } 381 | } 382 | 383 | void WiThrottleServer::ClientData::locoAction(char th, LocoAddress iLocoAddr, etl::string_view actionVal) { 384 | auto &throttle = slots[th]; 385 | auto it = throttle.find(iLocoAddr); 386 | if(it == throttle.end()) { 387 | LOGW("No loco '%s' in this throttle", String(iLocoAddr).c_str()); 388 | return; 389 | } 390 | uint8_t slot = it->second; 391 | 392 | LOGI("loco action thr=%c; addr=%s; action=%s", th, String(iLocoAddr).c_str(), actionVal.data() ); 393 | switch(actionVal[0] ) { 394 | case 'F': { 395 | uint8_t fKey = etl::to_arithmetic(actionVal.substr(2)).value(); 396 | bool val = CS.getLocoFn(slot, fKey); 397 | if(actionVal[1]=='1'){ 398 | val = !val; 399 | CS.setLocoFn(slot, fKey, val); 400 | } 401 | sendThrottleMsg(th, 'A', iLocoAddr, (val?"F1":"F0")+String(fKey) ); 402 | break; 403 | } 404 | case 'q': 405 | switch(actionVal[1]) { 406 | case 'V': 407 | //DEBUGS("query speed for loco "+String(dccLocoAddr) ); 408 | sendThrottleMsg(th,'A', iLocoAddr, String("V")+speed2int(CS.getLocoSpeed(slot)) ); 409 | break; 410 | case 'R': 411 | //DEBUGS("query dir for loco "+String(dccLocoAddr) ); 412 | sendThrottleMsg(th,'A', iLocoAddr, String("R")+String(CS.getLocoDir(slot) )); 413 | break; 414 | } 415 | CS.kickSlot(slot); 416 | break; 417 | case 'V': 418 | { 419 | //DEBUGS("Sending velocity to addr "+String(dccLocoAddr) ); 420 | int s = etl::to_arithmetic(actionVal.substr(1)).value(); 421 | CS.setLocoSpeed(slot, int2speed(s) ); 422 | } 423 | break; 424 | case 'R': 425 | //DEBUGS("Sending dir to addr "+String(dccLocoAddr) ); 426 | CS.setLocoDir(slot, etl::to_arithmetic(actionVal.substr(1)).value() ); 427 | break; 428 | case 'X': // EMGR stop 429 | CS.setLocoSpeed(slot, SPEED_EMGR); 430 | break; 431 | case 'I': // idle 432 | CS.setLocoSpeed(slot, SPEED_IDLE); 433 | break; 434 | case 'Q': // quit 435 | CS.setLocoSpeed(slot, SPEED_IDLE); 436 | cli->close(); 437 | break; 438 | } 439 | 440 | } 441 | 442 | void WiThrottleServer::ClientData::checkHeartbeat() { 443 | if(! heartbeatEnabled) return; 444 | 445 | if (wdt.timedOut() && heartbeat==Heartbeat::Alive) { 446 | // stop loco 447 | LOGI("timeout exceeded: current %ds, last updated at %ds", millis()/1000, wdt.getLastUpdate()/1000 ); 448 | heartbeat = Heartbeat::SoftTimeout; 449 | for(const auto& throttle: slots) 450 | for(const auto& slot: throttle.second) { 451 | CS.setLocoSpeed(slot.second, SPEED_EMGR); 452 | sendThrottleMsg(throttle.first, 'A', slot.first, "V-1" ); 453 | } 454 | sendMessage("Timeout exceeded, locos stopped"); 455 | } 456 | if ((wdt.timedOut2() && heartbeat==Heartbeat::SoftTimeout)) { 457 | LOGI("timeout exceeded twice: closing connection" ); 458 | heartbeat = Heartbeat::HardTimeout; 459 | cli->close(); 460 | } 461 | 462 | } 463 | 464 | void WiThrottleServer::ClientData::sendMessage(String msg, bool alert) { 465 | wifiPrintln(cli, String("H")+(alert?'M':'m')+msg); 466 | } 467 | 468 | void WiThrottleServer::ClientData::sendThrottleMsg(char th, char cmd, LocoAddress iLocoAddr, String resp) { 469 | char tt[4] = "MTA"; 470 | tt[1] = th; 471 | tt[2] = cmd; 472 | wifiPrintln(cli, String(tt)+addr2str(iLocoAddr)+DELIM + resp); 473 | } 474 | 475 | void WiThrottleServer::accessoryToggle(unsigned aAddr, char action, bool isNamed, ClientData &cc) { 476 | 477 | LOGI("Turnout addr=%d(named: %c) action=%c", aAddr, isNamed?'Y':'N', action ); 478 | 479 | TurnoutAction a; 480 | switch(action) { 481 | case 'T': a = TurnoutAction::THROWN; break; 482 | case 'C': a = TurnoutAction::CLOSED; break; 483 | case '2': a = TurnoutAction::TOGGLE; break; 484 | default: 485 | cc.sendMessage("Unknown turnout command!", true); 486 | return; 487 | } 488 | TurnoutState newStat = CS.turnoutAction(aAddr, isNamed, a); 489 | 490 | if(newStat==TurnoutState::UNKNOWN) cc.sendMessage("Could not change turnout!", true); 491 | char cStat = turnoutState2Chr(newStat); 492 | 493 | for (auto p: clients) { 494 | wifiPrintln(p.first, String("PTA")+cStat+(isNamed?TURNOUT_PREF:"")+aAddr); 495 | } 496 | 497 | } -------------------------------------------------------------------------------- /src/WiThrottle.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on https://github.com/positron96/withrottle 3 | * 4 | * Also, see JMRI sources, start at https://github.com/JMRI/JMRI/blob/master/java/src/jmri/jmrit/withrottle/DeviceServer.java 5 | * 6 | * Official documentation: https://www.jmri.org/help/en/package/jmri/jmrit/withrottle/Protocol.shtml 7 | */ 8 | 9 | #pragma once 10 | 11 | #include "CommandStation.h" 12 | #include "Watchdog.h" 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | 25 | #define WT_DEBUG 26 | 27 | #ifdef WT_DEBUG 28 | #define WT_LOGI(format, ...) do{ ets_printf(ARDUHAL_LOG_FORMAT(I, format), ##__VA_ARGS__); } while(0) 29 | #else 30 | #define WT_LOGI(...) 31 | #endif 32 | 33 | 34 | class WiThrottleServer { 35 | public: 36 | 37 | constexpr static uint16_t DEF_PORT = 4444; 38 | 39 | WiThrottleServer(uint16_t port, const char* name=nullptr); 40 | 41 | void begin(); 42 | 43 | void end() { 44 | server.end(); 45 | } 46 | 47 | 48 | void notifyPowerStatus(AsyncClient *c=nullptr) { 49 | bool v = CS.getPowerState(); 50 | powerStatus = v ? '1' : '0'; 51 | if(c==nullptr) { 52 | for (auto p: clients) { 53 | wifiPrintln(p.first, String("PPA")+powerStatus); 54 | } 55 | } else wifiPrintln(c, String("PPA")+powerStatus); 56 | } 57 | 58 | void loop(); 59 | 60 | private: 61 | 62 | const uint16_t port; 63 | const char* name; 64 | 65 | constexpr static int MAX_CLIENTS = 3; 66 | constexpr static int MAX_THROTTLES_PER_CLIENT = 6; 67 | constexpr static int MAX_LOCOS_PER_THROTTLE = 2; 68 | 69 | constexpr static millis_t HEARTBEAT_INTL = 20; ///< in seconds 70 | 71 | AsyncServer server; 72 | 73 | enum class Heartbeat { 74 | Alive, 75 | SoftTimeout, ///< stop all locos after soft timeout 76 | HardTimeout, ///< disconnect client after hard timeout 77 | Dead ///< no need to check anything, it should be cleaned up soon 78 | }; 79 | 80 | struct ClientData { 81 | AsyncClient *cli; 82 | bool heartbeatEnabled; 83 | Heartbeat heartbeat = Heartbeat::Alive; 84 | Watchdog wdt; 85 | 86 | constexpr static size_t RX_SIZE = 100; 87 | char rx[RX_SIZE]; 88 | size_t rxpos = 0; 89 | 90 | String hwId; 91 | 92 | using AddrToSlotMap = etl::map; 93 | // each client can have up to 6 multi throttles, each MT can have multiple locos (and slots) 94 | etl::map< char, AddrToSlotMap, MAX_THROTTLES_PER_CLIENT> slots; 95 | uint8_t slot(char thr, LocoAddress addr) { 96 | auto it = slots.find(thr); 97 | if(it == slots.end()) return 0; 98 | auto iit = it->second.find(addr); 99 | if(iit == it->second.end() ) return 0; 100 | return iit->second; 101 | } 102 | 103 | void locoAdd(char th, etl::string_view sLocoAddr); 104 | 105 | void locosRelease(char th, etl::string_view sLocoAddr); 106 | void locoRelease(char th, LocoAddress addr); 107 | 108 | void locosAction(char th, etl::string_view sLocoAddr, etl::string_view actionVal); 109 | void locoAction(char th, LocoAddress addr, etl::string_view actionVal); 110 | 111 | void checkHeartbeat(); 112 | void updateHeartbeat() { 113 | wdt.kick(); 114 | heartbeat = Heartbeat::Alive; 115 | } 116 | 117 | void sendMessage(String msg, bool alert=false); 118 | 119 | void sendThrottleMsg(char th, char a, LocoAddress iLocoAddr, String resp); 120 | 121 | }; 122 | 123 | etl::map clients; 124 | 125 | void onNewClient(AsyncClient* cli); 126 | 127 | void processCmd(ClientData &cc); 128 | 129 | void notifyHearbeatStatus(ClientData &c); 130 | 131 | char powerStatus = '0'; 132 | 133 | void turnPower(char v) { 134 | CS.setPowerState(v=='1'); 135 | notifyPowerStatus(); 136 | } 137 | 138 | 139 | static void wifiPrintln(AsyncClient *c, String v) { 140 | c->add(v.c_str(), v.length() ); 141 | char lf = '\n'; 142 | c->add(&lf, 1); 143 | c->send(); 144 | //WT_LOGI("TX '%s'", v.c_str() ); 145 | } 146 | static void wifiPrint(AsyncClient *c, String v) { 147 | c->write(v.c_str(), v.length() ); 148 | //WT_LOGI("WFTX %s", v.c_str() ); 149 | } 150 | 151 | void clientStart(AsyncClient *cli) ; 152 | 153 | void clientStop(ClientData &c); 154 | 155 | void accessoryToggle(unsigned aAddr, char aStatus, bool namedTurnout, ClientData &cc); 156 | }; -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define LEVEL_NONE (0) 4 | #define LEVEL_ERROR (1) 5 | #define LEVEL_WARN (2) 6 | #define LEVEL_INFO (3) 7 | #define LEVEL_DEBUG (4) 8 | #define LEVEL_VERBOSE (5) 9 | 10 | #if defined(GLOBAL_LOG_LEVEL) && !defined(LOG_LEVEL) 11 | #define LOG_LEVEL GLOBAL_LOG_LEVEL 12 | #endif 13 | 14 | #if defined(FILE_LOG_LEVEL) 15 | #define LOG_LEVEL FILE_LOG_LEVEL 16 | #endif 17 | 18 | 19 | #if LOG_LEVEL >= LEVEL_VERBOSE 20 | #define LOGV(format, ...) log_printf(ARDUHAL_LOG_FORMAT(V, format), ##__VA_ARGS__) 21 | #define LOGV_ISR(format, ...) ets_printf(ARDUHAL_LOG_FORMAT(V, format), ##__VA_ARGS__) 22 | #else 23 | #define LOGV(format, ...) 24 | #define LOGV_ISR(format, ...) 25 | #endif 26 | 27 | #if LOG_LEVEL >= LEVEL_DEBUG 28 | #define LOGD(format, ...) log_printf(ARDUHAL_LOG_FORMAT(D, format), ##__VA_ARGS__) 29 | #define LOGD_ISR(format, ...) ets_printf(ARDUHAL_LOG_FORMAT(D, format), ##__VA_ARGS__) 30 | #else 31 | #define LOGD(format, ...) 32 | #define LOGD_ISR(format, ...) 33 | #endif 34 | 35 | #if LOG_LEVEL >= LEVEL_INFO 36 | #define LOGI(format, ...) log_printf(ARDUHAL_LOG_FORMAT(I, format), ##__VA_ARGS__) 37 | #define LOGI_ISR(format, ...) ets_printf(ARDUHAL_LOG_FORMAT(I, format), ##__VA_ARGS__) 38 | #else 39 | #define LOGI(format, ...) 40 | #define LOGI_ISR(format, ...) 41 | #endif 42 | 43 | #if LOG_LEVEL >= LEVEL_WARN 44 | #define LOGW(format, ...) log_printf(ARDUHAL_LOG_FORMAT(W, format), ##__VA_ARGS__) 45 | #define LOGW_ISR(format, ...) ets_printf(ARDUHAL_LOG_FORMAT(W, format), ##__VA_ARGS__) 46 | #else 47 | #define LOGW(format, ...) 48 | #define LOGW_ISR(format, ...) 49 | #endif 50 | 51 | #if LOG_LEVEL >= LEVEL_ERROR 52 | #define LOGE(format, ...) log_printf(ARDUHAL_LOG_FORMAT(E, format), ##__VA_ARGS__) 53 | #define LOGE_ISR(format, ...) ets_printf(ARDUHAL_LOG_FORMAT(E, format), ##__VA_ARGS__) 54 | #else 55 | #define LOGE(format, ...) 56 | #define LOGE_ISR(format, ...) 57 | #endif 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "CommandStation.h" 4 | 5 | #include "LocoNetSlotManager.h" 6 | 7 | #include "LocoNetSerial.h" 8 | #include "LbServer.h" 9 | 10 | #include "WiThrottle.h" 11 | 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | LocoNetBus bus; 25 | 26 | #define LOCONET_PIN_RX 16 27 | #define LOCONET_PIN_TX 17 28 | //#include 29 | //LocoNetESP32Uart locoNetPhy(&bus, LOCONET_PIN_RX, LOCONET_PIN_TX, 1, false, true, false ); 30 | //#include 31 | //LocoNetESP32Hybrid locoNetPhy(&bus, LOCONET_PIN_RX, LOCONET_PIN_TX, 1, false, true, 0 ); 32 | //#include 33 | LocoNetESP32 locoNetPhy(&bus, LOCONET_PIN_RX, LOCONET_PIN_TX, 0); 34 | LocoNetDispatcher parser(&bus); 35 | 36 | 37 | #define LBSERVER_TCP_PORT 1234 38 | LbServer lbServer(LBSERVER_TCP_PORT, &bus); 39 | 40 | //LocoNetSerial lSerial(&Serial, &bus); 41 | 42 | #define CS_NAME "ESP32CommandStation" 43 | 44 | #define PIN_LED 22 45 | 46 | #define DCC_MAIN_PIN 25 47 | #define DCC_MAIN_PIN_EN 32 48 | #define DCC_MAIN_PIN_SENSE 36 49 | #define DCC_PROG_PIN 26 50 | #define DCC_PROG_PIN_EN 33 51 | #define DCC_PROG_PIN_SENSE 39 52 | 53 | DCCESP32Channel<10> dccMain(DCC_MAIN_PIN, DCC_MAIN_PIN_EN, DCC_MAIN_PIN_SENSE); 54 | DCCESP32Channel<2> dccProg(DCC_PROG_PIN, DCC_PROG_PIN_EN, DCC_PROG_PIN_SENSE); 55 | DCCESP32SignalGenerator dccTimer(1); //timer1 56 | 57 | LocoNetSlotManager slotMan(&bus); 58 | 59 | WiThrottleServer withrottleServer(WiThrottleServer::DEF_PORT, CS_NAME); 60 | 61 | #define PIN_BT 13 62 | #define PIN_BT2 15 63 | 64 | constexpr int LED_INTL_NORMAL = 1000; 65 | constexpr int LED_INTL_CONFIG1 = 500; 66 | constexpr int LED_INTL_CONFIG2 = 250; 67 | 68 | uint8_t ledVal; 69 | 70 | void ledStartBlinking(uint32_t ms=0, uint8_t val=1); 71 | void ledStop(); 72 | void ledUpdate(); 73 | 74 | void checkCurrent(); 75 | 76 | using TimerType = etl::callback_timer_atomic<2, std::atomic_uint>; 77 | TimerType timerController; 78 | etl::timer::id::type ledTimer; 79 | etl::timer::id::type checkCurrentTimer; 80 | 81 | void setup() { 82 | 83 | Serial.begin(115200); 84 | Serial.println(CS_NAME); 85 | 86 | pinMode(PIN_BT, INPUT_PULLUP); 87 | pinMode(PIN_BT2, INPUT_PULLUP); 88 | pinMode(PIN_LED, OUTPUT); 89 | 90 | digitalWrite(PIN_LED, LOW); 91 | 92 | locoNetPhy.begin(); 93 | //lSerial.begin(); 94 | 95 | 96 | parser.onPacket(CALLBACK_FOR_ALL_OPCODES, [](const lnMsg *rxPacket) { 97 | char tmp[100]; 98 | formatMsg(*rxPacket, tmp, sizeof(tmp)); 99 | Serial.printf("onPacket: %s\n", tmp); 100 | }); 101 | 102 | 103 | parser.onSwitchRequest([](uint16_t address, bool output, bool direction) { 104 | Serial.print("Switch Request: "); 105 | Serial.print(address, DEC); 106 | Serial.print(':'); 107 | Serial.print(direction ? "Closed" : "Thrown"); 108 | Serial.print(" - "); 109 | Serial.println(output ? "On" : "Off"); 110 | }); 111 | parser.onSwitchReport([](uint16_t address, bool state, bool sensor) { 112 | Serial.print("Switch/Sensor Report: "); 113 | Serial.print(address, DEC); 114 | Serial.print(':'); 115 | Serial.print(sensor ? "Switch" : "Aux"); 116 | Serial.print(" - "); 117 | Serial.println(state ? "Active" : "Inactive"); 118 | }); 119 | parser.onSensorChange([](uint16_t address, bool state) { 120 | Serial.print("Sensor: "); 121 | Serial.print(address, DEC); 122 | Serial.print(" - "); 123 | Serial.println(state ? "Active" : "Inactive"); 124 | }); 125 | 126 | dccTimer.setMainChannel(&dccMain); 127 | dccTimer.setProgChannel(&dccProg); 128 | 129 | CS.setDccMain(&dccMain); 130 | CS.setDccProg(&dccProg); 131 | CS.setLocoNetBus(&bus); 132 | 133 | ledTimer = timerController.register_timer( 134 | TimerType::callback_type::create(), 135 | LED_INTL_NORMAL, true); 136 | checkCurrentTimer = timerController.register_timer( 137 | TimerType::callback_type::create(), 138 | 1, true); 139 | 140 | 141 | bool bt = digitalRead(PIN_BT)==0; 142 | if(bt) { 143 | // start AP 144 | WiFi.persistent(false); 145 | //WiFi.softAPConfig(IPAddress{192,168,1,0}, IPAddress{192,168,1,1}, IPAddress{255,255,255,0}); 146 | WiFi.softAP(CS_NAME " AP", ""); 147 | Serial.println(""); 148 | Serial.println("WiFi AP started."); 149 | Serial.println("IP address: "); 150 | Serial.println(WiFi.softAPIP()); 151 | ledStartBlinking(LED_INTL_NORMAL/2); 152 | } else { 153 | WiFiManager wifiManager; 154 | wifiManager.setConfigPortalTimeout(300); // 5 min 155 | if ( !wifiManager.autoConnect(CS_NAME " AP") ) { 156 | delay(1000); 157 | Serial.print("Failed connection"); 158 | ESP.restart(); 159 | } 160 | Serial.println(""); 161 | Serial.println("WiFi connected."); 162 | Serial.println("IP address: "); 163 | Serial.println(WiFi.localIP()); 164 | ledStartBlinking(); 165 | } 166 | 167 | MDNS.begin("ESP32Server"); 168 | //MDNS.addService("http","tcp", DCCppServer_Port); 169 | MDNS.setInstanceName(CS_NAME); 170 | 171 | dccTimer.begin(); 172 | 173 | dccMain.setPower(true); 174 | dccProg.setPower(true); 175 | 176 | lbServer.begin(); 177 | withrottleServer.begin(); 178 | 179 | timerController.enable(true); 180 | timerController.start(checkCurrentTimer); 181 | 182 | } 183 | 184 | 185 | void loop() { 186 | 187 | lbServer.loop(); 188 | withrottleServer.loop(); 189 | CS.loop(); 190 | //lSerial.loop(); 191 | 192 | /* 193 | static unsigned long nextDccMeter = 0; 194 | if(millis()>nextDccMeter) { 195 | uint16_t v = dccMain.readCurrentAdc() ; 196 | if(v > 15) dccMain.setPower(false); 197 | nextDccMeter = millis()+20; 198 | }*/ 199 | uint32_t ms = millis(); 200 | static uint32_t lastMs = 0; 201 | if(timerController.tick(ms - lastMs)) { 202 | lastMs = ms; 203 | } 204 | 205 | static unsigned long nextInRead = 0; 206 | static int inState = 0; 207 | static int inState2 = 0; 208 | if(millis()>nextInRead) { 209 | int v = 1-digitalRead(PIN_BT); 210 | if(v!=inState) { 211 | 212 | //bool r = dccMain.verifyCVByteProg(1, v==1 ? 13 : 14); 213 | //int r = dccMain.readCVProg(1); 214 | 215 | //Serial.printf("main(): readCVProg: %d\n", r); 216 | Serial.printf( "reporting sensor %d\n", v==HIGH) ; 217 | reportSensor(&bus, 1, v==HIGH); 218 | Serial.printf("errs: rx:%d, tx:%d\n", locoNetPhy.getRxStats()->rxErrors, locoNetPhy.getTxStats()->txErrors ); 219 | } 220 | inState = v; 221 | 222 | v = 1-digitalRead(PIN_BT2); 223 | if(v!=inState2) { 224 | if(dccMain.getPower()) { 225 | dccMain.setPower(false); 226 | dccProg.setPower(false); 227 | } else { 228 | dccMain.setPower(true); 229 | dccProg.setPower(true); 230 | } 231 | } 232 | inState2 = v; 233 | 234 | nextInRead = millis() + 10; 235 | } 236 | 237 | /* 238 | if(Serial.available()) { 239 | Serial.read(); 240 | DCCESP32Channel<10>::RegisterList *r = dccMain.getReg(); 241 | Packet *p = r->currentPacket; 242 | while(r->currentPacket == p) { 243 | dccMain.timerFunc(); 244 | delay(1); 245 | if(r->currentBit==1) break; 246 | } 247 | } 248 | */ 249 | 250 | } 251 | 252 | void checkCurrent() { 253 | bool oc = dccMain.checkOvercurrent(); 254 | if(!oc) { 255 | withrottleServer.notifyPowerStatus(); 256 | Serial.println("Overcurrent on main"); 257 | } 258 | 259 | oc = dccProg.checkOvercurrent(); 260 | if(!oc) { 261 | Serial.println("Overcurrent on prog"); 262 | } 263 | 264 | //uint32_t v = dccMain.readCurrentAdc(); 265 | //cur = cur*0.9 + v*0.1; 266 | //if(v!=0)Serial.printf("%d, %d\n", v, (int)cur ); 267 | //dccMain.timerFunc(); 268 | } 269 | 270 | 271 | void ledStartBlinking(uint32_t ms, uint8_t val) { 272 | if(ms==0) ms = LED_INTL_NORMAL; 273 | ledVal = val; 274 | digitalWrite(PIN_LED, ledVal); 275 | //ledNextUpdate = millis()+ms; 276 | 277 | timerController.set_period(ledTimer, ms); 278 | timerController.start(ledTimer); 279 | } 280 | 281 | void ledStop() { 282 | //ledNextUpdate = 0; // turn off blink 283 | timerController.stop(ledTimer); 284 | digitalWrite(PIN_LED, LOW); 285 | } 286 | void ledUpdate() { 287 | //if(ledNextUpdate!=0 && millis()>ledNextUpdate) { 288 | ledVal = 1-ledVal; 289 | digitalWrite(PIN_LED, ledVal); 290 | 291 | //if(!configMode) { 292 | //ledNextUpdate = LED_INTL_NORMAL; 293 | /*} else { 294 | ledNextUpdate = configVar==0 ? LED_INTL_CONFIG1 : LED_INTL_CONFIG2; 295 | }*/ 296 | //ledNextUpdate += millis(); 297 | //} 298 | } -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | -------------------------------------------------------------------------------- /test/test_native/LocoSpeedTest.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "LocoSpeed.h" 3 | 4 | #include 5 | #include 6 | 7 | void testLocoSpeed() { 8 | 9 | TEST_ASSERT_EQUAL_MESSAGE(0, SPEED_IDLE.get128(), "IDLE"); 10 | TEST_ASSERT_EQUAL_MESSAGE(1, SPEED_EMGR.get128(), "EMGR"); 11 | 12 | // test 14 speed steps 13 | for(uint8_t a = 0; a<16; a++) { 14 | LocoSpeed s = LocoSpeed{a, SpeedMode::S14}; 15 | uint8_t ret = s.getDCC(SpeedMode::S14); 16 | //printf("in %d (%d) out %d\n", a, s.get128(), ret); 17 | TEST_ASSERT_EQUAL_MESSAGE(a, ret, "S14" ); 18 | } 19 | 20 | // test 28 speed steps 21 | for(uint8_t a = 0; a<32; a++) { 22 | LocoSpeed s = LocoSpeed{a, SpeedMode::S28}; 23 | uint8_t ret = s.getDCC(SpeedMode::S28); 24 | //printf("in %d (%d) out %d\n", a, s.get128(), ret); 25 | if(a==2) { TEST_ASSERT_MESSAGE(2==ret || 0==ret, "IDLE S28" ); 26 | } else if(a==3) { TEST_ASSERT_MESSAGE(3==ret || 1==ret, "EMGR S28" ); 27 | } else TEST_ASSERT_EQUAL_MESSAGE(a, ret, "S28" ); 28 | } 29 | 30 | // test 128 sppeds, effectively reversibility 31 | for(size_t a = 0; a<128; a++) { 32 | LocoSpeed s = LocoSpeed::from128(a); 33 | TEST_ASSERT_EQUAL_MESSAGE(a, s.get128(), "S128" ); 34 | } 35 | 36 | // test float 37 | const size_t N=100; 38 | for(size_t a = 0; a<=N; a++) { 39 | float in = (float)a/N; 40 | LocoSpeed s = LocoSpeed::fromFloat(in); 41 | float out = s.getFloat(); 42 | //printf("in %f (%d) out %f\n", in, s.get128(), out); 43 | TEST_ASSERT_EQUAL_MESSAGE(a, int(out*N), "Float" ); 44 | } 45 | TEST_ASSERT_TRUE_MESSAGE(SPEED_EMGR == LocoSpeed::fromFloat(-1), "fromFloat(-1)"); 46 | //TEST_ASSERT_TRUE_MESSAGE(SPEED_IDLE == LocoSpeed::fromFloat(0), "fromFloat(0)"); 47 | //TEST_ASSERT_EQUAL_MESSAGE(127, LocoSpeed::fromFloat(1).get128(), "fromFloat(1)"); 48 | 49 | } 50 | 51 | 52 | int main(int argc, char **argv) { 53 | UNITY_BEGIN(); 54 | RUN_TEST(testLocoSpeed); 55 | return UNITY_END(); 56 | } 57 | --------------------------------------------------------------------------------