1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2025-07-29 02:13:45 +02:00

Compare commits

...

234 Commits

Author SHA1 Message Date
Neil McKechnie
b2323f0e74 Create IO_S88.h
Experimental S88 driver code, not tested or validated.
2022-01-17 13:03:28 +00:00
Neil McKechnie
9ba43c28bf Create IO_LedChain.h
Shift register output device.
2021-12-15 14:15:00 +00:00
Neil McKechnie
58d2b69db5 Merge branch 'master' into neil-network 2021-12-01 15:41:08 +00:00
Harald Barth
58afea135c Committing a SHA 2021-11-30 18:57:58 +00:00
Harald Barth
9018ec9757 DISABLE_EEPROM explanation 2021-11-30 19:56:09 +01:00
Harald Barth
aa734b25e4 Merge branch 'disable-eeprom' into master 2021-11-30 19:45:33 +01:00
Harald Barth
67e48d34f4 do not include config.h direct 2021-11-30 19:40:31 +01:00
Florian Becker
419822ef06 Committing a SHA 2021-11-30 10:12:49 +00:00
Florian Becker
d4ee215ae6 fix typo (#194)
replace "manu" with "many"
2021-11-30 10:12:29 +00:00
Neil McKechnie
011a5c517b Merge branch 'master' into neil-network 2021-11-24 13:08:11 +00:00
Neil McKechnie
f05b3d1730 Committing a SHA 2021-11-24 13:02:35 +00:00
Neil McKechnie
a2f8a8ec91 Merge branch 'master' of https://github.com/DCC-EX/CommandStation-EX 2021-11-24 13:00:31 +00:00
Neil McKechnie
746350b846 Update version to 3.2.0 rc5 2021-11-24 12:54:02 +00:00
Neil McKechnie
97f3450621 Simplify OLED driver initialisation.
Simplify the initialisation in the SSD1306Ascii driver, by removing some of the complex structures that were inherited from the library on which it is based.  This should also allow it to compile on the ESP32 platform.
2021-11-24 12:53:03 +00:00
Asbelos
2be3e276f9 Committing a SHA 2021-11-24 12:02:40 +00:00
Asbelos
88fa5ad37c VPIN in RMFT2::doSignal 2021-11-24 12:02:16 +00:00
Harald Barth
a4f746c00c Warn for broken configs 2021-11-22 00:41:47 +01:00
Neil McKechnie
106fb612dc Committing a SHA 2021-11-21 17:56:29 +00:00
Neil McKechnie
53113e981d Update IO_PCF8574.h
Correct handling of input in immediate mode,
2021-11-21 17:56:06 +00:00
Neil McKechnie
d7fd9e1538 Committing a SHA 2021-11-15 16:16:55 +00:00
Neil McKechnie
197228c3b0 Update version to 3.2.0 rc4 2021-11-15 16:13:54 +00:00
Neil McKechnie
620dcbf925 Update myHal.cpp_example.txt
Update examples
2021-11-15 14:58:12 +00:00
Neil McKechnie
82f121c8ef Some comment changes 2021-11-15 14:45:03 +00:00
Neil McKechnie
6c98f90151 Reduce I2C interrupt time
Reduce the time spent with interrupts disabled in I2CManager response code by enabling interrupts after the state machine has finished.
Also, some comment changes.
2021-11-15 14:30:27 +00:00
Neil McKechnie
c90ea0c6df Improve validation of parameters to non-HAL digital calls.
When testing CS in minimal HAL mode but with mySetup.h and myAutomation.h files present, I experienced freezing of the arduino because the standard pinMode, digitalWrite etc don't validate the pin number passed to them.  So I've added checks on the pin number to the configure, write and read functions in the minimal HAL.
2021-11-15 13:25:11 +00:00
Neil McKechnie
d08f14be3b Rename user module mySetup.cpp to myHal.cpp, and function mySetup() to halSetup() within it. 2021-11-15 12:50:02 +00:00
Neil McKechnie
b8ee7f034b Merge branch 'master' into neil-network 2021-11-12 00:18:09 +00:00
Neil McKechnie
fb97ba11de Committing a SHA 2021-11-12 00:09:59 +00:00
Neil McKechnie
ee5db61349 Update version.h to 3.2.0 rc3. 2021-11-12 00:06:29 +00:00
Neil McKechnie
b384d6c14d Move call to mySetup into IODevice::begin().
Ensure that HAL devices are created before use by moving the call to mySetup into IODevice::begin().  The need for this became evident when it was noted that RMFT (EX-RAIL) interacts with HAL devices during its initialisation, by enabling pull-ups on digital inputs.
Any
2021-11-12 00:05:16 +00:00
Neil McKechnie
58fe81bf06 Update EthernetInterface.h
Remove spurious character.
2021-11-11 23:59:50 +00:00
Neil McKechnie
13e889a82c Avoid sending ethernet responses one character at a time.
Send commands
2021-11-11 13:33:27 +00:00
Neil McKechnie
4f688938a4 Update EthernetInterface.h
Remove spurious character
2021-11-11 13:32:26 +00:00
Neil McKechnie
6a1ffdb3fa Network support 2021-11-11 13:31:59 +00:00
Neil McKechnie
8e6c232d05 Delay Turnout <H> response following movement. 2021-11-11 13:30:37 +00:00
Harald Barth
1807189183 make it possible to disable EEPROM code to save flash space 2021-11-08 02:07:21 +01:00
Harald Barth
0e78cf6e55 Committing a SHA 2021-11-07 23:20:28 +00:00
Harald Barth
6c75563779 handle negative pins 2021-11-08 00:19:23 +01:00
Harald Barth
89dcafb2d7 Committing a SHA 2021-11-07 16:04:52 +00:00
Harald Barth
37904b5fa6 make rc1 2021-11-07 17:03:28 +01:00
Harald Barth
fbca15d2a7 Merge branch 'master-ex-rail' 2021-11-07 17:01:16 +01:00
Harald Barth
177c8c0367 Merge branch 'EX-RAIL-sensormod' 2021-11-07 16:17:22 +01:00
Harald Barth
7ea3faf177 Merge branch 'EX-RAIL' 2021-11-07 16:14:38 +01:00
Harald Barth
d3381c6b2d Committing a SHA 2021-11-07 15:05:58 +00:00
Harald Barth
8853b23f88 uopdate version.h 2021-11-07 16:04:49 +01:00
Harald Barth
a16f6c8749 configure pins correct even when HAL not used 2021-11-06 22:12:32 +01:00
Harald Barth
e3d771a24d set default pullup in EXRAIL begin code 2021-11-06 21:57:06 +01:00
Harald Barth
055bc7bfe2 unknown locos should have speed forward 2021-10-31 22:20:59 +01:00
Harald Barth
79ce71c2f9 Committing a SHA 2021-10-31 21:18:17 +00:00
Harald Barth
e3cbaf5f24 unknown locos should have speed forward 2021-10-31 22:17:51 +01:00
Harald Barth
250c372f5c Committing a SHA 2021-10-29 20:30:40 +00:00
Harald Barth
a9c31eb1ae YFROBOT: One more motor board with L298P 2021-10-29 22:30:01 +02:00
Asbelos
b9fed47d24 Merge branch 'EX-RAIL' of https://github.com/DCC-EX/CommandStation-EX into EX-RAIL 2021-10-21 22:44:47 +01:00
Asbelos
151f7d7f86 Fixup EXRAIL Read Loco issues 2021-10-21 22:44:25 +01:00
Neil McKechnie
b7bcd13347 Fix Arduino pin pullup initial state.
If an Arduino pin was used as an input (e.g. by EXRAIL) without previously configuring it, the default pullup wouldn't be set up.  Now, on first call to the _read() method the pullup will be enabled.
2021-10-21 16:43:42 +01:00
Neil McKechnie
4f16a4ca06 Fix GPIO Expander initial output state.
Previously, pullups were enabled on GPIO Expander digital pins by default, even if the pin was only ever used as an output.  This could lead to a spurious HIGH state being seen by external equipment before the output is initialised to LOW.  To avoid this, the pin pullup is now not enabled until a configure or read operation is issued for the pin.
2021-10-15 18:44:51 +01:00
Neil McKechnie
9097a62f42 Add new <D ANIN vpin> and <D ANOUT vpin value> commands.
Alias for existing <D SERVO ...> command added as <D ANOUT ...> (since not all analogue outputs are servos).  Also, <D ANIN vpin> added to display the value of an analogue input pin.
2021-10-08 13:30:23 +01:00
Neil McKechnie
80472a76dc I2CManager - support slower I2C speeds.
Previously the driver allowed speeds down to 32kHz but lower speeds were not implemented correctly.
2021-10-08 13:28:43 +01:00
Neil McKechnie
6dde811279 Optimise HAL drivers for TOF sensor and Analogue Inputs
Increased use of async I2C in HAL drivers to reduce overall loop time overhead.
2021-10-05 12:48:45 +01:00
Neil McKechnie
7aed7de6cd Change default LCD address.
LCD Backpack Address in example config.h changed to 0x27 (to match the most commonly available PCF8574 device).
2021-10-03 12:24:14 +01:00
Asbelos
bfc2b75eb5 SERIAL2 Typo 2021-10-01 11:01:32 +01:00
Neil McKechnie
9fc805831d HAL: Minor optimisations
Remove virtual method hasCallback().
Optimise findDevice() method (used by read, write etc.).
Simplify Sensor handling with regard to IO Devices that support callbacks.
2021-09-23 10:54:27 +01:00
Neil McKechnie
ffc5d91561 Update version.h 2021-09-23 08:59:43 +01:00
Neil McKechnie
e11fd18849 Update IO_DCCAccessory.cpp
Ensure the full range of addresses including 0 are handled.
2021-09-22 14:12:23 +01:00
Neil McKechnie
32eb8fe8c7 Update version.h (3.1.7draft)
Added partial list of changes from 3.1.6 to 3.1.7draft.
2021-09-22 14:00:05 +01:00
Neil McKechnie
e287af83ff DCC Turnouts: Store address/subaddress separately. Enable address 0.
The range of accessory decoder addresses for the <a> command is 0-511 in line with the DCC packet contents.  The turnout command previously rejected address 0; this has been changed to the same range of addresses can be used by both commands, i.e. address 0-511 and subaddress 0-3.  The linear address mapping remains so that linear address 1 is addr/subaddr 1/0; i.e. the first decoder address is not accessible by linear address.
2021-09-22 10:38:11 +01:00
Neil McKechnie
e59e07b971 Improved HAL diagnostics
Looptime diagnostic enhanced, and duplicated diagnostic messages removed from DFPlayer class.
2021-09-21 13:43:52 +01:00
Neil McKechnie
302b16547e HAL driver enhancements
Performance enhancements in IODevice::loop() function.
Improved error handling, device is placed off line if not responding.
Improved error reporting, device shown as offline if not operational (faulty or not present).
2021-09-21 11:02:23 +01:00
Asbelos
08835e25c6 Merge branch 'EX-RAIL' of https://github.com/DCC-EX/CommandStation-EX into EX-RAIL 2021-09-18 13:10:18 +01:00
Asbelos
bda3c05265 Auto power on tell JMR 2021-09-18 13:10:13 +01:00
Neil McKechnie
f947c5bae5 Merge pull request #190 from DCC-EX/EX-RAIL-neil-HALDRIVERS
Additional and enhanced HAL drivers
2021-09-17 22:48:51 +01:00
Neil McKechnie
afe2ecdc14 Update IODevice.cpp
Remove potentially irritating diag messages
2021-09-17 12:44:27 +01:00
Neil McKechnie
fa650673eb DFPlayer: allow volume to be set in play command. 2021-09-17 12:31:28 +01:00
Neil McKechnie
ad7cd5f401 Remove virtual _isBusy() function in favor of _read().
When writing to analogue outputs pins, the digital _read() function now returns the 'busy' status of the analogue pin.  Consequently, the _isBusy() function becomes superfluous and has been removed.  The static IODevice::isBusy() function now calls the object's _read() function instead.
Also, limit in DFPlayer of 3 pins has been removed.
2021-09-17 11:36:08 +01:00
Asbelos
d077e3a2ff Auto power on and POWEROFF macro 2021-09-16 16:47:47 +01:00
Neil McKechnie
07cc45d861 Update IO_DFPlayer.h
Fix volume control command.
2021-09-16 12:39:51 +01:00
Neil McKechnie
f3658aaee7 Update IO_HCSR04.h
Change transmitPin to trigPin and receivePin to echoPin to match the markings on the device module.
2021-09-16 00:17:26 +01:00
Neil McKechnie
3dc0b1619c Update IO_DFPlayer.h 2021-09-15 21:37:38 +01:00
Neil McKechnie
592f87303e Update IO_AnalogueInputs.h
Increase frequency of ADC conversions to 4ms, since 10ms driver cycle isn't enough time for a 7.8ms conversion to complete reliably.
2021-09-15 10:44:43 +01:00
Neil McKechnie
02a715d54d New DFPlayer MP3 device, and tidy comments in other drivers. 2021-09-15 00:23:24 +01:00
Neil McKechnie
f7d34b92ee Update mySetup.cpp_example.txt 2021-09-14 17:14:29 +01:00
Neil McKechnie
d316b72069 VL53L0X Time-Of-Flight sensor driver
HAL Driver for VL53L0X Time-Of-Flight sensor.  Basic implementation, which doesn't include most of the calibration etc. so is very lean on memory and CPU but not as accurate as it could be.
2021-09-14 12:34:31 +01:00
Ash-4
fc9aa71d9f Merge pull request #189 from DCC-EX/EX-RAIL-Ash
Ex rail ash
2021-09-11 13:35:11 -05:00
Ash-4
72528658be Merge branch 'EX-RAIL' into EX-RAIL-Ash 2021-09-11 13:32:17 -05:00
Neil McKechnie
4121a5f4da Merge pull request #188 from DCC-EX/EX-RAIL-neil-AnalogueInputs
Ex rail neil analogue inputs
2021-09-11 14:22:48 +01:00
Neil McKechnie
2ed578821f Add analogue inputs to HAL.
Add ability to read analogue inputs on arduino and on external ADS1115 I2C modules.
2021-09-11 13:35:11 +01:00
Asbelos
70b59d491c Ash's OVERLOAD check
Makes prog track accesses wait if track in overload
2021-09-09 10:23:27 +01:00
Asbelos
254d83b6fc Remove SERIAL warning 2021-09-09 10:12:27 +01:00
Ash-4
ebabbbe59e pause program steps if OVERLOAD
line added to pause program steps during OVERLOAD.  case BASELINE
if (DCCWaveform::progTrack.getPowerMode()==POWERMODE::OVERLOAD) return;
 -- also added a line in DCCWaveform.cpp
2021-09-08 14:06:39 -05:00
Ash-4
f8311b8c56 line added sentResetsSincePacket
in DCCWaveform::setPowerMode(POWERMODE mode)
 -- to pause while power is off due to PROG TRACK POWER OVERLOAD
and line added after case BASELINE in DCC.cpp
2021-09-08 14:00:42 -05:00
Asbelos
f38bf512ab Include SERIAL 2021-09-08 16:29:58 +01:00
Asbelos
9b3c6fe896 LCN and SERIAL/1/2/3 2021-09-08 16:21:04 +01:00
Asbelos
81dc512c86 Turnout print state and tell withrottle 2021-09-06 12:30:25 +01:00
Asbelos
222eca6524 XFON/XFOFF macros 2021-09-06 12:27:21 +01:00
Ash-4
4dff8a2b50 Restore ackManagerByte before retry Verify
Identify where initial value was not verified, but initial value returned with subsequent Read.
RCOUNT removed.  BIV and ITCBV added.
2021-09-05 16:43:24 -05:00
Ash-4
8d471d9f3f Restore ackManagerByte before retry Verify 2021-09-05 16:19:03 -05:00
Asbelos
9ba13a62c9 Negative sensor ids 2021-09-04 10:38:38 +01:00
Asbelos
99222bd37f Turnout recursion test 2021-09-03 22:39:13 +01:00
Asbelos
9d5781a87c Signal pin corrections 2021-09-03 21:33:53 +01:00
Ash-4
b4fb76b6c8 Display running total ackRetrySum
RCOUNT added to Verify program to report if Read step occurs.
Report ackRetrySum on LCD when <D ACK RETRY nn> is sent
2021-08-30 17:02:05 -05:00
Ash-4
4b87c879a9 RCOUNT step added to Verify byte program 2021-08-30 16:52:50 -05:00
Ash-4
08810dafd7 Update retry counter
<D ACK RETRY nn>  will also display running total prior to its reset.
RCOUNT step included in Verify program will count when Verify fails
2021-08-30 16:37:06 -05:00
Neil McKechnie
afe9141671 RMFT SIGNAL macro to allow for RGB LEDs.
The SIGNAL macro has been changed to allow for use of RGB LEDs.  Connect R and G pins, and assign as SIGNAL(redpin,0,greenpin).  Then if amber is requested, the macro will set red and green on at the same time.
2021-08-29 12:14:23 +01:00
Neil McKechnie
1bb7b5cc77 Make defaults for PWM (servo) positions 0 (PWM off) if not configured.
When writing to a PWM device (servo or LED for example), it is possible to request the target position in the call, or to ask for a SET or RESET position.  In the latter case, the positions corresponding to SET and RESET must be known, i.e. preconfigured.  Defaults were assigned for this, but because the correct values will depend on the hardware device being driven, the defaults have been removed.
In addition, the <T> command, when defining a servo turnout, now configures the PWM positions (not required by <T> commands, but desirable for consistency with other commands).
2021-08-29 12:04:13 +01:00
Neil McKechnie
09eae0ea91 Fix FADE(pin,0,0) operation in RMFT 2021-08-29 01:10:47 +01:00
Neil McKechnie
0f55835b8b Add RMFT WAITFOR() and SERVO2() commands.
WAITFOR(pin) waits until the corresponding pin is not busy (e.g. has finished moving the servo).  SERVO2(pin, value, ms) moves to the nominated position in a time given in milliseconds by ms.
2021-08-28 17:39:48 +01:00
Neil McKechnie
40c6bb7f2e Output Turnout state change diagnostic if DIAG_IO #defined. 2021-08-27 21:47:48 +01:00
Neil McKechnie
7dea284ba8 DCCAccessoryDecoder class tidy. RCN-213 option.
Rationalise address calculation into three macros.  Ensure device is added to device chain.
Allow inversion of the DCC packet to match definition of packet D bit in RCN-213, D=0 for 'throw' (rather than the DCC++ usage of D=1 for 'throw').
2021-08-27 21:47:13 +01:00
Neil McKechnie
fb6ab85c4a Add flag to invert DCC Accessory command <a> behaviour
<a addr subaddr 1> command puts a D=1 into the DCC packet for a DCC Accessory Decoder.  This was previously though to correspond to a 'throw' request and a D=0 to a 'close' request.  RCN-213 standard identifies that D=1 is 'close' and D=0 is 'throw', so this change allows CS to be configured to invert the states to conform to the RCN-213 definition.
2021-08-27 21:43:24 +01:00
Neil McKechnie
23ed4e61af Remove compiler warning
If no route or automation definitions were present, the compiler warned that parameter stream is not used in function RMFT2::emitWithrottleDescriptions.
2021-08-27 17:09:40 +01:00
Neil McKechnie
b2ddb34273 RMFT: Add new FADE command for LED
LED FADE command allows an LED to be attached to a PCA9685 PWM module and controlled to any arbitrary brightness (0-4095), changing over a specified period of time in milliseconds.
FADE(vpin,value,ms)
2021-08-27 17:01:18 +01:00
Neil McKechnie
f8858b952e Servo positioning - correct handling of profile 0.
Ensure that profile 0 uses the duration parameter to calculate the number of steps.
2021-08-27 16:59:04 +01:00
Neil McKechnie
6ebf908802 Ensure Turnout changes are notified on LCN activity.
Also, some comment updates.
2021-08-27 15:45:22 +01:00
Neil McKechnie
93dfdcce53 Add <D HAL SHOW> command to list HAL device configuration.
Also, only display HAL device configurations at startup if DIAG_IO is #defined.
2021-08-27 15:44:26 +01:00
Neil McKechnie
7e601c38c4 HAL writeAnalogue function change.
IODevice::writeAnalogue() has an additional optional parameter "duration", specifying the time taken for the animation in units of 100ms (max 3276 seconds, or about 54 minutes).
2021-08-27 15:42:47 +01:00
Neil McKechnie
1dd574dc03 On <E> commmand, output EEPROM size and amount used.
Also, formatting and indentation fixed.
2021-08-27 12:56:27 +01:00
Neil McKechnie
0aea9169b1 Rename IODevice::isActive(vpin) to isBusy(vpin). 2021-08-27 11:18:15 +01:00
Neil McKechnie
0c218e1e13 Add HAL function configureInput(vpin,...) and configureServo(vpin,...). 2021-08-27 10:58:00 +01:00
Neil McKechnie
0a9fcf6ebc Neil bugfixes. (#186)
* Re-enable native I2C driver.

* Minor non-functional changes to native I2C Manager.

* Minor changes to make variable types explicit in comparisons.

* Fix IODevice::loop() to avoid null pointer dereference.

Strange problems with LCD driver tracked down to being caused by a call to p->_loop() when p is NULL.

* Correct sense of comparison in LCN support function Turnout::setClosedStateOnly()

* Remove code (now unused) from LCD driver.

* Add I2C textual error messages.

* Add I2C textual error messages.

* Fix compile error in 4809 I2C driver.

* Remove init function call from SSD1306 driver.
2021-08-26 23:04:13 +01:00
Asbelos
5e30740c5b fix EXRAIL CALL/RETURN 2021-08-26 21:49:44 +01:00
Neil McKechnie
2469629cbb Temporarily use Wire for I2C. 2021-08-25 10:26:45 +01:00
Neil McKechnie
bad9e866f8 Merge pull request #185 from DCC-EX/EX-RAIL-neil-RCN213
Ex rail neil rcn213
2021-08-25 00:42:16 +01:00
Neil McKechnie
77d4d7c400 Merge branch 'EX-RAIL' into EX-RAIL-neil-RCN213 2021-08-25 00:38:38 +01:00
Neil McKechnie
fa04fa5084 I2C Manager, adjust loop code.
loop() contains startTransaction which is called after handleInterrupt().  However, startTransaction is called within handleInterrupt so remove the extra call.  This appears to solve strange problems encountered with the LCD display.
2021-08-25 00:34:19 +01:00
Neil McKechnie
80fc9e8a68 Make LCD Display I2C calls synchronous. 2021-08-25 00:29:57 +01:00
Neil McKechnie
d0fed2dd38 Make LCD output to I2C synchronous.
Temporary work-around to problems with LCD driver, until I can look at it in depth.
2021-08-24 23:02:24 +01:00
Neil McKechnie
08cfe41cf3 Revert to original DCC++ Classic Turnout command polarity.
Revert to <T id 1> command being 'throw' and <T id 0> being 'close', for turnouts.
2021-08-24 22:18:51 +01:00
Neil McKechnie
777d189cc5 Enable pullups for Arduino input pins as a default 2021-08-24 22:15:50 +01:00
Neil McKechnie
c45337d5d4 Enable pullups for Arduino input pins as a default (to match GPIO Extender modules). 2021-08-24 22:13:52 +01:00
Asbelos
8b498b8b49 </ROUTES> cmd for JMRI/Withrottle 2021-08-24 09:45:11 +01:00
Neil McKechnie
425de3fcc7 Create mySetup.cpp_example.txt
Provide an example showing directives for HAL device configuration.
2021-08-23 20:41:30 +01:00
Neil McKechnie
0d235b65d3 Turnouts - make code clearer.
Overlay of flags bits added in struct TurnoutData,, called flags.  This simplifies the the EEPROM update code.
2021-08-23 17:36:50 +01:00
Neil McKechnie
69c4733f2b Initialise turnouts to Closed by default
Ensure that the servo, VPIN and LCN turnouts are all initialised to closed if no initial state is provided in the create call or in EEPROM.  This applies irrespective of the RCN-213 configuration settings.
2021-08-23 15:26:23 +01:00
Neil McKechnie
f0cd96fed3 Changes associated with RCN-213 DCC Accessory Packet format 2021-08-23 12:43:14 +01:00
Asbelos
161b35ae84 indentation and LCD macro use
No actual code change.
2021-08-23 12:35:42 +01:00
Asbelos
214e6c643f Squashed commit of the following:
commit b34205b142
Merge: 8703248 2829716
Author: Neil McKechnie <75813993+Neil-McK@users.noreply.github.com>
Date:   Mon Aug 23 10:05:54 2021 +0100

    Merge branch 'EX-RAIL' into ackRetry

commit 8703248c49
Author: Ash-4 <81280775+Ash-4@users.noreply.github.com>
Date:   Sun Aug 22 16:47:38 2021 -0500

    ACK RETRY max 255 with fallback to 3 if greater

    And includes LCD lines for power and ACK diags.

commit f5d4522ed7
Author: Ash-4 <81280775+Ash-4@users.noreply.github.com>
Date:   Sun Aug 22 16:40:13 2021 -0500

    ACK RETRY updated datatypes

commit 1dbf236697
Author: Ash-4 <81280775+Ash-4@users.noreply.github.com>
Date:   Sun Aug 22 16:35:14 2021 -0500

    ACK RETRY updated datatypes

commit d93584e9a4
Author: Ash-4 <81280775+Ash-4@users.noreply.github.com>
Date:   Sun Aug 22 13:16:24 2021 -0500

    ACK RETRY updated default is 2 retries.

commit f58ebac670
Author: Ash-4 <81280775+Ash-4@users.noreply.github.com>
Date:   Sat Aug 21 16:43:21 2021 -0500

    ACK RETRY is 3 or less (default is 1)

commit 08350b215a
Author: Ash-4 <81280775+Ash-4@users.noreply.github.com>
Date:   Sat Aug 21 11:55:17 2021 -0500

    ACK RETRY

    LCD display update.
    lcd(0, F("RETRY %d %d %d %d"), ackManagerCv, ackManagerRetry, ackRetry, ackRetrySum);

commit 11cd216017
Author: Ash-4 <81280775+Ash-4@users.noreply.github.com>
Date:   Sat Aug 21 00:54:28 2021 -0500

    ACK RETRY

    ACK retry code added to ackManagerSetup and callback.
    The default is <D ACK RETRY 1>.  For ACK tuning, set retry to zero.
    Retry count is captured on the LCD display, and lines in the serial monitor.

commit b67027a1ed
Author: Ash-4 <81280775+Ash-4@users.noreply.github.com>
Date:   Sat Aug 21 00:33:01 2021 -0500

    ACK RETRY variables added

commit 34d2ab3543
Author: Ash-4 <81280775+Ash-4@users.noreply.github.com>
Date:   Sat Aug 21 00:23:34 2021 -0500

    Update DCCEXParser.cpp

    LCD lines added to display power commands and ACK settings, when updated.
    Also new command <D ACK RETRY 1>.

commit 8ca4011cb0
Author: Ash-4 <81280775+Ash-4@users.noreply.github.com>
Date:   Fri Aug 20 23:58:13 2021 -0500

    Update CommandStation-EX.ino

    Update LCD row number for Ready and Free RAM.

commit 6571138389
Author: Harald Barth <haba@kth.se>
Date:   Sun Aug 1 22:08:34 2021 +0200

    optimize command parser for size

commit c4f659243e
Author: Harald Barth <haba@kth.se>
Date:   Sun Aug 1 15:07:06 2021 +0200

    optimize for loops for size (and speed)

commit 55b7091d5a
Author: Harald Barth <haba@kth.se>
Date:   Sun Aug 1 12:45:29 2021 +0200

    take less progmem for messages

commit 6d7c1925b0
Author: Harald Barth <haba@kth.se>
Date:   Sun Aug 1 11:56:12 2021 +0200

    only pragma -O3 critical functions
2021-08-23 11:58:48 +01:00
Asbelos
50a9e08d1f defines/configig include tidy
now just 2 places where config is included...
1) in defines.h
2) At the start of the .ino so it can be made obvious to the user what is happening.
2021-08-23 11:55:42 +01:00
Neil McKechnie
ca55834051 Update defines.h
Add #include config.h (on which defines.h is reliant).
2021-08-23 10:46:12 +01:00
Neil McKechnie
2829716ea6 Merge branch 'EX-RAIL-neil2' into EX-RAIL 2021-08-22 22:40:14 +01:00
Neil McKechnie
00138be90d Increase default display line length to 20 (from 16). 2021-08-22 22:39:00 +01:00
Neil McKechnie
fdaa7b51b9 Move Turnout code from .h to .cpp.
Move implementation of Turnout::printState from Turnouts.h to Turnouts.cpp.  No functional changes.
2021-08-22 22:30:09 +01:00
Neil McKechnie
7b47b86143 Turnouts: adjust the split of code between .h and .cpp file. No functional changes. 2021-08-22 22:25:23 +01:00
Neil McKechnie
3e50a6bdad Add include guard to defines.h
Ensure that defines.h is only process once, even if included multiple times.
2021-08-22 22:23:08 +01:00
Asbelos
240b18a0df Merge branch 'EX-RAIL-neil2' into EX-RAIL 2021-08-22 19:36:08 +01:00
Asbelos
b35ce88fdd Deeay long values 2021-08-22 17:01:55 +01:00
Neil McKechnie
0875d27b0a Remove 'activate' functions from turnout classes.
Remove the static 'activate' function and rename the virtual 'activate' function to 'setClosedInternal'.
2021-08-22 14:07:16 +01:00
Neil McKechnie
39a69e340e Turnout EEPROM improvements.
Ensure state is saved and restored from EEPROM as expected.
Make constructors for turnouts private.  Otherwise, a statically created turnout may be initialising itself before the underlying HAL device has been created.  By requiring the create() call be used, there is more control over the timing of the turnout object's creation.
2021-08-21 23:16:52 +01:00
Neil McKechnie
dbabfdca80 Improvements to PCA9685 operation
Rationalise duplicated code;  improve initialisation;
2021-08-21 23:13:34 +01:00
Asbelos
60718f5eac int->int16_t to keep pedantic compilers happy 2021-08-21 13:17:14 +01:00
Neil McKechnie
071389a04b Remove compiler warnings in Turnout.h 2021-08-21 00:34:28 +01:00
Neil McKechnie
d8366f33c8 Make <s> output turnout state rather than full turnout definition.
<s> command currently prints the current states for outputs and for sensors, but prints the full configuration of turnouts.  This change makes the turnout output consistent, i.e. just <H id state> is output for each turnout.  The <T> command still outputs the full turnout definition.
2021-08-21 00:25:00 +01:00
Asbelos
ddcd40860f UNTESTED Allow ALIAS of ROUTE/AUTOMATION id
Runs ALIAS on first pass and creates a routine to emit the route stuff to withrottle because previous technique wouldnt compile for aliased ids.
2021-08-20 19:18:30 +01:00
Neil McKechnie
133c65bc42 Report Turnout configuration in old and new formats.
JMRI currently isn't aware of the newer types of turnout in DCC++EX, so when it receives the definitions of turnouts it barfs on them.  It still knows a turnout exists, but isn't able to display its full configuration.  For DCC Accessory turnouts, the configuration message has changed so that it includes the DCC string (to distinguish them from other types of turnout).  To enable current and older versions of JMRI to continue working with DCC turnouts, CS now reports the old and new formats, i.e. <T id addr subaddr state> and <T id DCC addr subadd state>.  It currently accepts the first one and ignores the second one, but in the fullness of time it might accept the second one too.
2021-08-20 15:43:03 +01:00
Neil McKechnie
482f4b1c79 Tidy up recent changes to Turnout class. 2021-08-20 14:36:18 +01:00
Neil McKechnie
b4a3b503bc Turnout notification handling enhanced.
Ensure that the <H> message is sent on the serial USB (to JMRI) whenever the turnout is closed or thrown, even if the request didn't originate on the serial USB.
2021-08-20 00:07:50 +01:00
Neil McKechnie
7f6173825f Various corrections to Turnout code. 2021-08-19 21:43:55 +01:00
Neil McKechnie
fd36ca2b92 Restructure Turnout class.
Turnout class split into a base class for common code and specific subclasses for Servo, DCC, VPIN and LCN turnouts.
Interface further narrowed to reduce direct access to member variables.
Turnout creation command handling has been moved into the DCCEXParser class.
Turnout function and parameter names changed to make the Throw and Close functionality explicit.
Turnout commands <T id C> (close) and <T id T> (throw) added.
2021-08-19 21:22:59 +01:00
Neil McKechnie
776a098a72 Bump EESTORE_ID version.
TurnoutData struct size has been reduced by one byte during rewrite of Turnout class.  Consequently, this renders any previous turnout definitions in EEPROM incompatible with the new format.  For safety, the version is increased so that incompatible EEPROM contents are discarded.
2021-08-19 20:17:48 +01:00
Asbelos
36f6e2f9ce Narrowing Turnout publics 2021-08-18 18:55:22 +01:00
Neil McKechnie
9dacd24d27 Various HAL enhancements. (#182)
* Add <D SERVO vpin position> command

Allow a PWM servo to be driven to any arbitrary position.

* Enhancements for HAL drivers

Add state change notification for external GPIO module drivers;
Allow drivers to be installed statically by declaration (as an alternative to the 'create' call).

* Create IO_HCSR04.h

HAL driver for HC-SR04 ultrasonic distance sensor (sonar).

* Enable servo commands in NO-HAL mode, but return error.

Avoid compile errors in RMFT.cpp when compiled with basic HAL by including the Turnout::createServo function as a stub that returns NULL.

* Update IO_HCSR04.h

Minor changes

* Change <D SERVO>

Give the <D SERVO> command an optional parameter of the profile.  For example, <D SERVO 100 200 3> will slowly move the servo on pin 100 to PWM position corresponding to 200.  If omitted, the servo will move immediately (no animation).

* IODevice (HAL) changes

1) Put new devices on the end of the chain instead of the beginning.  This will give better performance for devices created first (ArduinoPins and extender GPIO devices, typically).
2) Remove unused functions.

* Update IO_HCSR04.h

Allow thresholds for ON and OFF to be separately configured at creation.

* Update IODevice.cpp

Fix compile error on IO_NO_HAL minimal HAL version.

* Update IO_PCA9685.cpp

Remove unnecessary duplicated call to min() function.
2021-08-17 23:41:34 +01:00
Asbelos
552e1bf3d8 Merge remote-tracking branch 'origin/RCN-213' into EX-RAIL 2021-08-17 19:48:06 +01:00
Asbelos
edefd638f1 Handoff immediately after acquire 2021-08-17 18:32:11 +01:00
Asbelos
71486beb16 Stop loco on DONE/KILL 2021-08-16 22:27:50 +01:00
Asbelos
d8f23c58da SENDLOCO/START bug 2021-08-16 16:30:30 +01:00
Asbelos
7227a0696e task Id and KILL
Also fixes a long standing flags bug  no idea where that line went!
2021-08-15 23:15:02 +01:00
Asbelos
3a6e41ac49 Ptevent accidental broadcast throttles 2021-08-15 20:38:55 +01:00
Asbelos
1e61c2cd61 SENDLOCO/START mixup 2021-08-15 17:17:41 +01:00
Asbelos
3ee7ba0b53 Servo profiles 2021-08-15 16:39:21 +01:00
Asbelos
264908dc24 Merge branch 'EX-RAIL' of https://github.com/DCC-EX/CommandStation-EX into EX-RAIL 2021-08-14 16:43:05 +01:00
Asbelos
f1e84330ca PIN and SERVO turnout ids 2021-08-14 16:42:56 +01:00
Neil McKechnie
3b1759a88e Update IODevice.h to include other device includes files.. 2021-08-12 21:18:46 +01:00
Neil McKechnie
5f5efa7d23 Correct number of usable pins for ArduinoPins class. 2021-08-12 21:18:06 +01:00
Neil McKechnie
f86a14ceab Servo profile - avoid overrun of array bounds. 2021-08-12 21:17:40 +01:00
Asbelos
329df3a3ee correct example sensors 2021-08-12 20:35:56 +01:00
Neil McKechnie
5932b4d101 Remove unnecessary servo output demands.
If start and end position of a servo movement are identical, skip all but the last step.
2021-08-12 12:02:18 +01:00
Neil McKechnie
ec503e7d3e Make IODevice::read function return type consistent with underlying _read calls.
IODevice::read() now returns int, instead of bool.  This is consistent with the IODevice::_read() return and also allows for future devices that return a non-boolean value.
2021-08-12 12:01:10 +01:00
Neil McKechnie
2a79f67308 Fix EEPROM handling for outputs.
Output definitions in EEPROM were being lost once the output was activated or deactivated.  The handling has been corrected and tested.
2021-08-12 11:59:32 +01:00
Asbelos
23291b499f EXRAIL LCD macro 2021-08-12 08:53:52 +01:00
Asbelos
22b5d5e4c4 default off DIAG_LOOPTIMES 2021-08-12 08:32:48 +01:00
Asbelos
a0791b041c Fix status display 2021-08-12 08:25:51 +01:00
Asbelos
a93f88d3b7 PRINT command 2021-08-10 16:32:23 +01:00
Asbelos
b06db69b53 correct example 2021-08-10 10:41:35 +01:00
Asbelos
67be436898 Correct throw/close re haba 2021-08-08 18:48:51 +01:00
Asbelos
270b9df523 Remove obsolete docs 2021-08-04 08:54:29 +01:00
FrightRisk
5eff4c5ee5 Squash all commits on RMFT branch to create EX-RAIL branch 2021-08-03 17:12:25 -04:00
Harald Barth
f8fb08e331 Committing a SHA 2021-07-31 13:41:32 +00:00
Harald Barth
50fcbc088a fix version.h of merge 2021-07-31 15:40:32 +02:00
Harald Barth
e2263b1e75 Committing a SHA 2021-07-31 13:35:39 +00:00
Harald Barth
5eabe934b8 Merge branch 'master' into copyrightmessages 2021-07-31 15:32:02 +02:00
Harald Barth
8f1ed21aa3 Allow some pins that might be useful 2021-07-27 19:53:21 +02:00
Harald Barth
2443e5903c Fix type warnings 2021-07-27 18:39:54 +02:00
Harald Barth
a88454dded Disallow pins <= 7 2021-07-27 18:35:22 +02:00
Harald Barth
c292f210a4 datatypes used in eeprom should be a data type that has a given size 2021-07-25 23:12:12 +02:00
Harald Barth
cc4de0ad14 fix size of struct at right place 2021-07-25 23:07:20 +02:00
Harald Barth
1fab0f586b Merge branch 'output-wordsize' of github.com:DCC-EX/CommandStation-EX into output-wordsize 2021-07-25 22:54:18 +02:00
Harald Barth
c15d5048b5 EEPROM format heuristics 2021-07-25 22:53:20 +02:00
Harald Barth
ec2295219d 3rd arg of Z is bitfield 2021-07-24 23:44:24 +02:00
Harald Barth
d3ef5f53ae Committing a SHA 2021-07-24 19:22:25 +00:00
Harald Barth
1941402c52 reserve version number 2021-07-24 21:21:57 +02:00
Harald Barth
7206e46273 Merge branch 'master' into output-wordsize 2021-07-24 21:15:33 +02:00
Harald Barth
f24bcd6819 step version 2021-07-24 21:14:19 +02:00
Harald Barth
cb64725b42 make output ID two bytes 2021-07-24 21:11:18 +02:00
Neil McKechnie
129d1c2039 Committing a SHA 2021-07-22 20:49:33 +00:00
Neil McKechnie
52e8a93945 Fix LCD corruption on power-up.
LCD was not working correctly on power up, but was subsequently OK on reset.  Problem was caused by an uninitialised variable.
2021-07-22 21:49:09 +01:00
Harald Barth
4f0c80a503 Turnout states according to RCN-123 where Thrown is 0 and Closed is 1. Additional protection against invalid chars in protocol 2021-07-16 10:24:11 +02:00
Neil McKechnie
88949358b4 Committing a SHA 2021-07-13 20:15:56 +00:00
Neil McKechnie
38be1d6152 Refactor OLED and LCD drivers (#178)
* Refactor SSD and LCD drivers

Rework display drivers to use inheritance and to remove unused functionality.  The changes here were previously in neil-hal branch but have been separated out because of the amount of changes in neil-hal.

* Update version.h
2021-07-13 21:15:38 +01:00
Fred
29a4ad0072 Committing a SHA 2021-06-10 23:03:54 +00:00
Fred
a473f6ae5c Update version.h 2021-06-10 19:03:38 -04:00
Fred
915af8d981 Committing a SHA 2021-06-10 23:02:35 +00:00
Fred
6eeaacdf90 Add loop to retry Ethernet cable connection (#173)
Implement PaulS's change to delay in a loop to give more time for sensing an Ethernet cable connection
2021-06-10 19:02:20 -04:00
Fred
766f88166c Committing a SHA 2021-06-10 18:24:13 +00:00
Fred
afd08dafc1 Update version.h 2021-06-10 14:23:57 -04:00
Ash-4
7851e7099c Committing a SHA 2021-06-10 18:23:17 +00:00
Ash-4
04403bbf5e Update DCC.cpp (#165)
lines 906-910 added. avoid wait after write. use 1 PROG
2021-06-10 14:23:00 -04:00
Fred
55cce0ef99 Committing a SHA 2021-06-10 18:07:16 +00:00
Fred
dc4fa57dc6 Update version.h 2021-06-10 14:06:59 -04:00
Neil McKechnie
75eefc7677 Committing a SHA 2021-06-10 15:25:03 +00:00
Neil McKechnie
79763a3bb8 SH1106 OLED Display Offset Fix (#169)
Correct display offset for SH1106.  The SH1106 supports 132 columns but most displays use 128 column OLED, connected to the middle columns 2-129.
2021-06-10 11:24:44 -04:00
Harald Barth
3fa805c0e7 Merge branch 'master' into copyrightmessages 2021-05-16 08:12:09 +02:00
Harald Barth
ba8f625fb0 move DCC::begin() after communications 2021-05-16 08:04:43 +02:00
Fred
ac37228942 Committing a SHA 2021-05-16 02:13:48 +00:00
Fred
90487d2d83 Update release_notes_v3.1.0.md 2021-05-15 22:13:33 -04:00
Fred
1807fe5c5f Committing a SHA 2021-05-16 02:12:24 +00:00
Fred
6abb65c1f4 Update release_notes.md
Sync release_notes.md with release notes on release page
2021-05-15 22:12:09 -04:00
Fred
971732fce8 Committing a SHA 2021-05-16 01:51:24 +00:00
Fred
8a03b889a3 Update release_notes.md 2021-05-15 21:51:10 -04:00
Fred
e12c3fc295 Committing a SHA 2021-05-16 01:45:54 +00:00
Fred
de8f9396f7 Update release_notes.md 2021-05-15 21:45:39 -04:00
Fred
f75a6b47f9 Committing a SHA 2021-05-16 01:37:47 +00:00
Fred
bc1398d3c4 Update README.md 2021-05-15 21:37:33 -04:00
Fred
0988340ff8 Committing a SHA 2021-05-16 01:36:41 +00:00
Fred
c7af43c70b Update README.md
Add 3.1 features
2021-05-15 21:36:21 -04:00
Harald Barth
1baff001f4 copyright messages 2021-04-14 22:46:29 +02:00
89 changed files with 12937 additions and 1398 deletions

7
.gitignore vendored
View File

@@ -9,3 +9,10 @@ Release/*
config.h
.vscode/extensions.json
mySetup.h
mySetup.cpp
myHal.cpp
myAutomation.h
myFilter.cpp
myAutomation.h
myFilter.cpp
myLayout.h

View File

@@ -27,7 +27,8 @@
/*
* © 2020,2021 Chris Harlow, Harald Barth, David Cutting,
* Fred Decker, Gregor Baues, Anthony W - Dayton All rights reserved.
*
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -43,7 +44,6 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "DCCEX.h"
// Create a serial command parser for the USB connection,
@@ -58,15 +58,18 @@ void setup()
// Responsibility 1: Start the usb connection for diagnostics
// This is normally Serial but uses SerialUSB on a SAMD processor
Serial.begin(115200);
DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com"));
CONDITIONAL_LCD_START {
// This block is still executed for DIAGS if LCD not in use
LCD(0,F("DCC++ EX v%S"),F(VERSION));
LCD(1,F("Starting"));
LCD(1,F("Lic GPLv3"));
}
// Start the WiFi interface on a MEGA, Uno cannot currently handle WiFi
// Responsibility 2: Start all the communications before the DCC engine
// Start the WiFi interface on a MEGA, Uno cannot currently handle WiFi
// Start Ethernet if it exists
#if WIFI_ON
WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL);
#endif // WIFI_ON
@@ -79,16 +82,15 @@ void setup()
// Note: this provides DCC with two motor drivers, main and prog, which handle the motor shield(s)
// Standard supported devices have pre-configured macros but custome hardware installations require
// detailed pin mappings and may also require modified subclasses of the MotorDriver to implement specialist logic.
// STANDARD_MOTOR_SHIELD, POLOLU_MOTOR_SHIELD, FIREBOX_MK1, FIREBOX_MK1S are pre defined in MotorShields.h
DCC::begin(MOTOR_SHIELD_TYPE);
DCC::begin(MOTOR_SHIELD_TYPE);
#if defined(RMFT_ACTIVE)
RMFT::begin();
#endif
// Start RMFT (ignored if no automnation)
RMFT::begin();
// Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
// This can be used to create turnouts, outputs, sensors etc. through the normal text commands.
#if __has_include ( "mySetup.h")
#define SETUP(cmd) serialParser.parse(F(cmd))
#include "mySetup.h"
@@ -100,7 +102,7 @@ void setup()
LCN::init(LCN_SERIAL);
#endif
LCD(1,F("Ready"));
LCD(3,F("Ready"));
}
void loop()
@@ -122,15 +124,16 @@ void loop()
EthernetInterface::loop();
#endif
#if defined(RMFT_ACTIVE)
RMFT::loop();
#endif
RMFT::loop(); // ignored if no automation
#if defined(LCN_SERIAL)
LCN::loop();
#endif
LCDDisplay::loop(); // ignored if LCD not in use
// Handle/update IO devices.
IODevice::loop();
// Report any decrease in memory (will automatically trigger on first call)
static int ramLowWatermark = __INT_MAX__; // replaced on first loop
@@ -139,6 +142,6 @@ void loop()
if (freeNow < ramLowWatermark)
{
ramLowWatermark = freeNow;
LCD(2,F("Free RAM=%5db"), ramLowWatermark);
LCD(3,F("Free RAM=%5db"), ramLowWatermark);
}
}

60
DCC.cpp
View File

@@ -20,10 +20,13 @@
#include "DIAG.h"
#include "DCC.h"
#include "DCCWaveform.h"
#ifndef DISABLE_EEPROM
#include "EEStore.h"
#endif
#include "GITHUB_SHA.h"
#include "version.h"
#include "FSH.h"
#include "IODevice.h"
// This module is responsible for converting API calls into
// messages to be sent to the waveform generator.
@@ -52,9 +55,14 @@ void DCC::begin(const FSH * motorShieldName, MotorDriver * mainDriver, MotorDriv
shieldName=(FSH *)motorShieldName;
StringFormatter::send(Serial,F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE), shieldName, F(GITHUB_SHA));
// Initialise HAL layer before reading EEprom.
IODevice::begin();
#ifndef DISABLE_EEPROM
// Load stuff from EEprom
(void)EEPROM; // tell compiler not to warn this is unused
EEStore::init();
#endif
DCCWaveform::begin(mainDriver,progDriver);
}
@@ -137,7 +145,7 @@ uint8_t DCC::getThrottleSpeed(int cab) {
bool DCC::getThrottleDirection(int cab) {
int reg=lookupSpeedTable(cab);
if (reg<0) return false ;
if (reg<0) return true;
return (speedTable[reg].speedCode & 0x80) !=0;
}
@@ -235,6 +243,9 @@ void DCC::updateGroupflags(byte & flags, int16_t functionNumber) {
}
void DCC::setAccessory(int address, byte number, bool activate) {
#ifdef DIAG_IO
DIAG(F("DCC::setAccessory(%d,%d,%d)"), address, number, activate);
#endif
// use masks to detect wrong values and do nothing
if(address != (address & 511))
return;
@@ -350,8 +361,9 @@ const ackOp FLASH WRITE_BYTE_PROG[] = {
const ackOp FLASH VERIFY_BYTE_PROG[] = {
BASELINE,
BIV, // ackManagerByte initial value
VB,WACK, // validate byte
ITCB, // if ok callback value
ITCB, // if ok callback value
STARTMERGE, //clear bit and byte values ready for merge pass
// each bit is validated against 0 and the result inverted in MERGE
// this is because there tend to be more zeros in cv values than ones.
@@ -369,7 +381,7 @@ const ackOp FLASH VERIFY_BYTE_PROG[] = {
V0, WACK, MERGE,
V0, WACK, MERGE,
V0, WACK, MERGE,
VB, WACK, ITCB, // verify merged byte and return it if acked ok
VB, WACK, ITCBV, // verify merged byte and return it if acked ok - with retry report
FAIL };
@@ -679,9 +691,15 @@ int DCC::nextLoco = 0;
//ACK MANAGER
ackOp const * DCC::ackManagerProg;
ackOp const * DCC::ackManagerProgStart;
byte DCC::ackManagerByte;
byte DCC::ackManagerByteVerify;
byte DCC::ackManagerStash;
int DCC::ackManagerWord;
byte DCC::ackManagerRetry;
byte DCC::ackRetry = 2;
int16_t DCC::ackRetrySum;
int16_t DCC::ackRetryPSum;
int DCC::ackManagerCv;
byte DCC::ackManagerBitNum;
bool DCC::ackReceived;
@@ -714,7 +732,10 @@ void DCC::ackManagerSetup(int cv, byte byteValueOrBitnum, ackOp const program[]
ackManagerCv = cv;
ackManagerProg = program;
ackManagerProgStart = program;
ackManagerRetry = ackRetry;
ackManagerByte = byteValueOrBitnum;
ackManagerByteVerify = byteValueOrBitnum;
ackManagerBitNum=byteValueOrBitnum;
ackManagerCallback = callback;
}
@@ -740,6 +761,7 @@ void DCC::ackManagerLoop() {
// (typically waiting for a reset counter or ACK waiting, or when all finished.)
switch (opcode) {
case BASELINE:
if (DCCWaveform::progTrack.getPowerMode()==POWERMODE::OVERLOAD) return;
if (checkResets(DCCWaveform::progTrack.autoPowerOff || ackManagerRejoin ? 20 : 3)) return;
DCCWaveform::progTrack.setAckBaseline();
callbackState=READY;
@@ -813,7 +835,18 @@ void DCC::ackManagerLoop() {
return;
}
break;
case ITCBV: // If True callback(byte) - Verify
if (ackReceived) {
if (ackManagerByte == ackManagerByteVerify) {
ackRetrySum ++;
LCD(1, F("v %d %d Sum=%d"), ackManagerCv, ackManagerByte, ackRetrySum);
}
callback(ackManagerByte);
return;
}
break;
case ITCB7: // If True callback(byte & 0x7F)
if (ackReceived) {
callback(ackManagerByte & 0x7F);
@@ -831,7 +864,11 @@ void DCC::ackManagerLoop() {
case FAIL: // callback(-1)
callback(-1);
return;
case BIV: // ackManagerByte initial value
ackManagerByte = ackManagerByteVerify;
break;
case STARTMERGE:
ackManagerBitNum=7;
ackManagerByte=0;
@@ -897,6 +934,15 @@ void DCC::ackManagerLoop() {
}
void DCC::callback(int value) {
// check for automatic retry
if (value == -1 && ackManagerRetry > 0) {
ackRetrySum ++;
LCD(0, F("Retry %d %d Sum=%d"), ackManagerCv, ackManagerRetry, ackRetrySum);
ackManagerRetry --;
ackManagerProg = ackManagerProgStart;
return;
}
static unsigned long callbackStart;
// We are about to leave programming mode
// Rule 1: If we have written to a decoder we must maintain power for 100mS
@@ -904,6 +950,10 @@ void DCC::callback(int value) {
switch (callbackState) {
case AFTER_WRITE: // first attempt to callback after a write operation
if (!ackManagerRejoin && !DCCWaveform::progTrack.autoPowerOff) {
callbackState=READY;
break;
} // lines 906-910 added. avoid wait after write. use 1 PROG
callbackStart=millis();
callbackState=WAITING_100;
if (Diag::ACK) DIAG(F("Stable 100mS"));

18
DCC.h
View File

@@ -38,9 +38,11 @@ enum ackOp : byte
ITC1, // If True Callback(1) (if prevous WACK got an ACK)
ITC0, // If True callback(0);
ITCB, // If True callback(byte)
ITCBV, // If True callback(byte) - end of Verify Byte
ITCB7, // If True callback(byte &0x7F)
NAKFAIL, // if false callback(-1)
FAIL, // callback(-1)
BIV, // Set ackManagerByte to initial value for Verify retry
STARTMERGE, // Clear bit and byte settings ready for merge pass
MERGE, // Merge previous wack response with byte value and decrement bit number (use for readimng CV bytes)
SETBIT, // sets bit number to next prog byte
@@ -64,8 +66,10 @@ enum CALLBACK_STATE : byte {
// Allocations with memory implications..!
// Base system takes approx 900 bytes + 8 per loco. Turnouts, Sensors etc are dynamically created
#ifdef ARDUINO_AVR_UNO
#if defined(ARDUINO_AVR_UNO)
const byte MAX_LOCOS = 20;
#elif defined(ARDUINO_AVR_NANO)
const byte MAX_LOCOS = 30;
#else
const byte MAX_LOCOS = 50;
#endif
@@ -113,6 +117,12 @@ public:
static inline void setGlobalSpeedsteps(byte s) {
globalSpeedsteps = s;
};
static inline int16_t setAckRetry(byte retry) {
ackRetry = retry;
ackRetryPSum = ackRetrySum;
ackRetrySum = 0; // reset running total
return ackRetryPSum;
};
private:
struct LOCO
@@ -141,9 +151,15 @@ private:
// ACK MANAGER
static ackOp const *ackManagerProg;
static ackOp const *ackManagerProgStart;
static byte ackManagerByte;
static byte ackManagerByteVerify;
static byte ackManagerBitNum;
static int ackManagerCv;
static byte ackManagerRetry;
static byte ackRetry;
static int16_t ackRetrySum;
static int16_t ackRetryPSum;
static int ackManagerWord;
static byte ackManagerStash;
static bool ackReceived;

30
DCCEX.h
View File

@@ -1,3 +1,24 @@
/*
* (c) 2020 Chris Harlow. All rights reserved.
* (c) 2021 Fred Decker. All rights reserved.
* (c) 2020 Harald Barth. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// This include is intended to visually simplify the .ino for the end users.
// If there were any #ifdefs required they are much better handled in here.
@@ -16,10 +37,11 @@
#include "LCD_Implementation.h"
#include "LCN.h"
#include "freeMemory.h"
#include "IODevice.h"
#include "Turnouts.h"
#include "Sensors.h"
#include "Outputs.h"
#include "RMFT.h"
#if __has_include ( "myAutomation.h")
#include "RMFT.h"
#define RMFT_ACTIVE
#endif
#endif

View File

@@ -27,11 +27,22 @@
#include "freeMemory.h"
#include "GITHUB_SHA.h"
#include "version.h"
#include "defines.h"
#include "EEStore.h"
#include "DIAG.h"
#include <avr/wdt.h>
////////////////////////////////////////////////////////////////////////////////
//
// Figure out if we have enough memory for advanced features
//
#if defined(ARDUINO_AVR_UNO) || defined(ARDUINO_AVR_NANO)
// nope
#else
#define HAS_ENOUGH_MEMORY
#endif
// These keywords are used in the <1> command. The number is what you get if you use the keyword as a parameter.
// To discover new keyword numbers , use the <$ YOURKEYWORD> command
const int16_t HASH_KEYWORD_PROG = -29718;
@@ -40,22 +51,35 @@ const int16_t HASH_KEYWORD_JOIN = -30750;
const int16_t HASH_KEYWORD_CABS = -11981;
const int16_t HASH_KEYWORD_RAM = 25982;
const int16_t HASH_KEYWORD_CMD = 9962;
const int16_t HASH_KEYWORD_WIT = 31594;
const int16_t HASH_KEYWORD_WIFI = -5583;
const int16_t HASH_KEYWORD_ACK = 3113;
const int16_t HASH_KEYWORD_ON = 2657;
const int16_t HASH_KEYWORD_DCC = 6436;
const int16_t HASH_KEYWORD_SLOW = -17209;
const int16_t HASH_KEYWORD_PROGBOOST = -6353;
#ifndef DISABLE_EEPROM
const int16_t HASH_KEYWORD_EEPROM = -7168;
#endif
const int16_t HASH_KEYWORD_LIMIT = 27413;
const int16_t HASH_KEYWORD_ETHERNET = -30767;
const int16_t HASH_KEYWORD_MAX = 16244;
const int16_t HASH_KEYWORD_MIN = 15978;
const int16_t HASH_KEYWORD_LCN = 15137;
const int16_t HASH_KEYWORD_RESET = 26133;
const int16_t HASH_KEYWORD_RETRY = 25704;
const int16_t HASH_KEYWORD_SPEED28 = -17064;
const int16_t HASH_KEYWORD_SPEED128 = 25816;
const int16_t HASH_KEYWORD_SERVO=27709;
const int16_t HASH_KEYWORD_VPIN=-415;
const int16_t HASH_KEYWORD_C=67;
const int16_t HASH_KEYWORD_T=84;
const int16_t HASH_KEYWORD_LCN = 15137;
const int16_t HASH_KEYWORD_HAL = 10853;
const int16_t HASH_KEYWORD_SHOW = -21309;
const int16_t HASH_KEYWORD_ANIN = -10424;
const int16_t HASH_KEYWORD_ANOUT = -26399;
#ifdef HAS_ENOUGH_MEMORY
const int16_t HASH_KEYWORD_WIFI = -5583;
const int16_t HASH_KEYWORD_ETHERNET = -30767;
const int16_t HASH_KEYWORD_WIT = 31594;
#endif
int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS];
bool DCCEXParser::stashBusy;
@@ -107,6 +131,7 @@ void DCCEXParser::loop(Stream &stream)
}
}
Sensor::checkAll(&stream); // Update and print changes
Turnout::loop(); // Check for outstanding turnout responses
}
int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte *cmd)
@@ -253,9 +278,12 @@ void DCCEXParser::parse(const FSH * cmd) {
}
// See documentation on DCC class for info on this section
void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
{
#ifndef DISABLE_EEPROM
(void)EEPROM; // tell compiler not to warn this is unused
#endif
if (Diag::CMD)
DIAG(F("PARSING:%s"), com);
int16_t p[MAX_COMMAND_PARAMS];
@@ -344,8 +372,12 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
|| ((subaddress & 0x03) != subaddress) // invalid subaddress (limit 2 bits )
|| ((p[activep] & 0x01) != p[activep]) // invalid activate 0|1
) break;
// Honour the configuration option (config.h) which allows the <a> command to be reversed
#ifdef DCC_ACCESSORY_RCN_213
DCC::setAccessory(address, subaddress,p[activep]==0);
#else
DCC::setAccessory(address, subaddress,p[activep]==1);
#endif
}
return;
@@ -451,6 +483,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
if (mode == POWERMODE::OFF)
DCC::setProgTrackBoost(false); // Prog track boost mode will not outlive prog track off
StringFormatter::send(stream, F("<p%c>\n"), opcode);
LCD(2, F("p%c"), opcode);
return;
}
switch (p[0])
@@ -458,6 +491,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
case HASH_KEYWORD_MAIN:
DCCWaveform::mainTrack.setPowerMode(mode);
StringFormatter::send(stream, F("<p%c MAIN>\n"), opcode);
LCD(2, F("p%c MAIN"), opcode);
return;
case HASH_KEYWORD_PROG:
@@ -465,6 +499,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
if (mode == POWERMODE::OFF)
DCC::setProgTrackBoost(false); // Prog track boost mode will not outlive prog track off
StringFormatter::send(stream, F("<p%c PROG>\n"), opcode);
LCD(2, F("p%c PROG"), opcode);
return;
case HASH_KEYWORD_JOIN:
DCCWaveform::mainTrack.setPowerMode(mode);
@@ -473,9 +508,13 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
{
DCC::setProgTrackSyncMain(true);
StringFormatter::send(stream, F("<p1 JOIN>\n"), opcode);
LCD(2, F("p1 JOIN"));
}
else
{
StringFormatter::send(stream, F("<p0>\n"));
LCD(2, F("p0"));
}
return;
}
break;
@@ -506,6 +545,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
// TODO Send stats of speed reminders table
return;
#ifndef DISABLE_EEPROM
case 'E': // STORE EPROM <E>
EEStore::store();
StringFormatter::send(stream, F("<e %d %d %d>\n"), EEStore::eeStore->data.nTurnouts, EEStore::eeStore->data.nSensors, EEStore::eeStore->data.nOutputs);
@@ -515,7 +555,7 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
EEStore::clear();
StringFormatter::send(stream, F("<O>\n"));
return;
#endif
case ' ': // < >
StringFormatter::send(stream, F("\n"));
return;
@@ -578,7 +618,9 @@ bool DCCEXParser::parseZ(Print *stream, int16_t params, int16_t p[])
}
return true;
case 3: // <Z ID PIN INVERT>
case 3: // <Z ID PIN IFLAG>
if (p[0] < 0 || p[2] < 0 || p[2] > 7 )
return false;
if (!Output::create(p[0], p[1], p[2], 1))
return false;
StringFormatter::send(stream, F("<O>\n"));
@@ -596,7 +638,7 @@ bool DCCEXParser::parseZ(Print *stream, int16_t params, int16_t p[])
for (Output *tt = Output::firstOutput; tt != NULL; tt = tt->nextOutput)
{
gotone = true;
StringFormatter::send(stream, F("<Y %d %d %d %d>\n"), tt->data.id, tt->data.pin, tt->data.iFlag, tt->data.oStatus);
StringFormatter::send(stream, F("<Y %d %d %d %d>\n"), tt->data.id, tt->data.pin, tt->data.flags, tt->data.active);
}
return gotone;
}
@@ -655,11 +697,10 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
case 0: // <T> list turnout definitions
{
bool gotOne = false;
for (Turnout *tt = Turnout::firstTurnout; tt != NULL; tt = tt->nextTurnout)
for (Turnout *tt = Turnout::first(); tt != NULL; tt = tt->next())
{
gotOne = true;
StringFormatter::send(stream, F("<H %d %d %d %d>\n"), tt->data.id, tt->data.address,
tt->data.subAddress, (tt->data.tStatus & STATUS_ACTIVE)!=0);
tt->print(stream);
}
return gotOne; // will <X> if none found
}
@@ -670,24 +711,62 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
StringFormatter::send(stream, F("<O>\n"));
return true;
case 2: // <T id 0|1> activate turnout
{
Turnout *tt = Turnout::get(p[0]);
if (!tt)
return false;
tt->activate(p[1]);
StringFormatter::send(stream, F("<H %d %d>\n"), tt->data.id, (tt->data.tStatus & STATUS_ACTIVE)!=0);
}
return true;
case 2: // <T id 0|1|T|C>
{
bool state = false;
switch (p[1]) {
// Turnout messages use 1=throw, 0=close.
case 0:
case HASH_KEYWORD_C:
state = true;
break;
case 1:
case HASH_KEYWORD_T:
state= false;
break;
default:
return false; // Invalid parameter
}
if (!Turnout::setClosed(p[0], state)) return false;
case 3: // <T id addr subaddr> define turnout
if (!Turnout::create(p[0], p[1], p[2]))
return false;
StringFormatter::send(stream, F("<O>\n"));
return true;
// Send acknowledgement to caller if the command was not received over Serial
// (acknowledgement messages on Serial are sent by the Turnout class).
if (stream != &Serial) Turnout::printState(p[0], stream);
return true;
}
default:
return false; // will <x>
default: // Anything else is some kind of turnout create function.
if (params == 6 && p[1] == HASH_KEYWORD_SERVO) { // <T id SERVO n n n n>
if (!ServoTurnout::create(p[0], (VPIN)p[2], (uint16_t)p[3], (uint16_t)p[4], (uint8_t)p[5]))
return false;
} else
if (params == 3 && p[1] == HASH_KEYWORD_VPIN) { // <T id VPIN n>
if (!VpinTurnout::create(p[0], p[2])) return false;
} else
if (params >= 3 && p[1] == HASH_KEYWORD_DCC) {
// <T id DCC addr subadd> 0<=addr<=511, 0<=subadd<=3 (like <a> command).<T>
if (params==4 && p[2]>=0 && p[2]<512 && p[3]>=0 && p[3]<4) { // <T id DCC n m>
if (!DCCTurnout::create(p[0], p[2], p[3])) return false;
} else if (params==3 && p[2]>0 && p[2]<=512*4) { // <T id DCC nn>, 1<=nn<=2048
// Linearaddress 1 maps onto decoder address 1/0 (not 0/0!).
if (!DCCTurnout::create(p[0], (p[2]-1)/4+1, (p[2]-1)%4)) return false;
} else
return false;
} else
if (params==3) { // legacy <T id addr subadd> for DCC accessory
if (p[1]>=0 && p[1]<512 && p[2]>=0 && p[2]<4) {
if (!DCCTurnout::create(p[0], p[1], p[2])) return false;
} else
return false;
}
else
if (params==4) { // legacy <T id n n n> for Servo
if (!ServoTurnout::create(p[0], (VPIN)p[1], (uint16_t)p[2], (uint16_t)p[3], 1)) return false;
} else
return false;
StringFormatter::send(stream, F("<O>\n"));
return true;
}
}
@@ -709,13 +788,13 @@ bool DCCEXParser::parseS(Print *stream, int16_t params, int16_t p[])
return true;
case 0: // <S> list sensor definitions
if (Sensor::firstSensor == NULL)
return false;
for (Sensor *tt = Sensor::firstSensor; tt != NULL; tt = tt->nextSensor)
{
StringFormatter::send(stream, F("<Q %d %d %d>\n"), tt->data.snum, tt->data.pin, tt->data.pullUp);
}
return true;
if (Sensor::firstSensor == NULL)
return false;
for (Sensor *tt = Sensor::firstSensor; tt != NULL; tt = tt->nextSensor)
{
StringFormatter::send(stream, F("<Q %d %d %d>\n"), tt->data.snum, tt->data.pin, tt->data.pullUp);
}
return true;
default: // invalid number of arguments
break;
@@ -738,17 +817,20 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
StringFormatter::send(stream, F("Free memory=%d\n"), minimumFreeMemory());
break;
case HASH_KEYWORD_ACK: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX] Value>
case HASH_KEYWORD_ACK: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value>
if (params >= 3) {
if (p[1] == HASH_KEYWORD_LIMIT) {
DCCWaveform::progTrack.setAckLimit(p[2]);
StringFormatter::send(stream, F("Ack limit=%dmA\n"), p[2]);
LCD(1, F("Ack Limit=%dmA"), p[2]); // <D ACK LIMIT 42>
} else if (p[1] == HASH_KEYWORD_MIN) {
DCCWaveform::progTrack.setMinAckPulseDuration(p[2]);
StringFormatter::send(stream, F("Ack min=%dus\n"), p[2]);
LCD(0, F("Ack Min=%dus"), p[2]); // <D ACK MIN 1500>
} else if (p[1] == HASH_KEYWORD_MAX) {
DCCWaveform::progTrack.setMaxAckPulseDuration(p[2]);
StringFormatter::send(stream, F("Ack max=%dus\n"), p[2]);
LCD(0, F("Ack Max=%dus"), p[2]); // <D ACK MAX 9000>
} else if (p[1] == HASH_KEYWORD_RETRY) {
if (p[2] >255) p[2]=3;
LCD(0, F("Ack Retry=%d Sum=%d"), p[2], DCC::setAckRetry(p[2])); // <D ACK RETRY 2>
}
} else {
StringFormatter::send(stream, F("Ack diag %S\n"), onOff ? F("on") : F("off"));
@@ -760,21 +842,23 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
Diag::CMD = onOff;
return true;
#ifdef HAS_ENOUGH_MEMORY
case HASH_KEYWORD_WIFI: // <D WIFI ON/OFF>
Diag::WIFI = onOff;
return true;
case HASH_KEYWORD_ETHERNET: // <D ETHERNET ON/OFF>
case HASH_KEYWORD_ETHERNET: // <D ETHERNET ON/OFF>
Diag::ETHERNET = onOff;
return true;
case HASH_KEYWORD_WIT: // <D WIT ON/OFF>
Diag::WITHROTTLE = onOff;
return true;
case HASH_KEYWORD_LCN: // <D LCN ON/OFF>
Diag::LCN = onOff;
return true;
#endif
case HASH_KEYWORD_PROGBOOST:
DCC::setProgTrackBoost(true);
@@ -786,11 +870,13 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
delay(50); // wait for the prescaller time to expire
break; // and <X> if we didnt restart
}
#ifndef DISABLE_EEPROM
case HASH_KEYWORD_EEPROM: // <D EEPROM NumEntries>
if (params >= 2)
EEStore::dump(p[1]);
return true;
#endif
case HASH_KEYWORD_SPEED28:
DCC::setGlobalSpeedsteps(28);
@@ -802,6 +888,22 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
StringFormatter::send(stream, F("128 Speedsteps"));
return true;
case HASH_KEYWORD_SERVO: // <D SERVO vpin position [profile]>
case HASH_KEYWORD_ANOUT: // <D ANOUT vpin position [profile]>
IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
break;
case HASH_KEYWORD_ANIN: // <D ANIN vpin> Display analogue input value
DIAG(F("VPIN=%d value=%d"), p[1], IODevice::readAnalogue(p[1]));
break;
#if !defined(IO_MINIMAL_HAL)
case HASH_KEYWORD_HAL:
if (p[1] == HASH_KEYWORD_SHOW)
IODevice::DumpAll();
break;
#endif
default: // invalid/unknown
break;
}

View File

@@ -1,3 +1,23 @@
/*
* (c) 2021 Mike S. All rights reserved.
* (c) 2021 Fred Decker. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef DCCTimer_h
#define DCCTimer_h
#include "Arduino.h"

View File

@@ -17,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma GCC optimize ("-O3")
#include <Arduino.h>
#include "DCCWaveform.h"
@@ -46,10 +46,8 @@ void DCCWaveform::begin(MotorDriver * mainDriver, MotorDriver * progDriver) {
&& (mainDriver->getFaultPin() != UNUSED_PIN));
// Only use PWM if both pins are PWM capable. Otherwise JOIN does not work
MotorDriver::usePWM= mainDriver->isPWMCapable() && progDriver->isPWMCapable();
if (MotorDriver::usePWM)
DIAG(F("Signal pin config: high accuracy waveform"));
else
DIAG(F("Signal pin config: normal accuracy waveform"));
DIAG(F("Signal pin config: %S accuracy waveform"),
MotorDriver::usePWM ? F("high") : F("normal") );
DCCTimer::begin(DCCWaveform::interruptHandler);
}
@@ -58,6 +56,8 @@ void DCCWaveform::loop(bool ackManagerActive) {
progTrack.checkPowerOverload(ackManagerActive);
}
#pragma GCC push_options
#pragma GCC optimize ("-O3")
void DCCWaveform::interruptHandler() {
// call the timer edge sensitive actions for progtrack and maintrack
// member functions would be cleaner but have more overhead
@@ -79,7 +79,7 @@ void DCCWaveform::interruptHandler() {
else if (progTrack.ackPending) progTrack.checkAck();
}
#pragma GCC push_options
// An instance of this class handles the DCC transmissions for one track. (main or prog)
// Interrupts are marshalled via the statics.
@@ -114,6 +114,7 @@ void DCCWaveform::setPowerMode(POWERMODE mode) {
powerMode = mode;
bool ison = (mode == POWERMODE::ON);
motorDriver->setPower( ison);
sentResetsSincePacket=0;
}
@@ -124,6 +125,8 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) {
if (!isMainTrack && !ackManagerActive && !progTrackSyncMain && !progTrackBoosted)
tripValue=progTripValue;
// Trackname for diag messages later
const FSH*trackname = isMainTrack ? F("MAIN") : F("PROG");
switch (powerMode) {
case POWERMODE::OFF:
sampleDelay = POWER_SAMPLE_OFF_WAIT;
@@ -141,9 +144,9 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) {
}
// Write this after the fact as we want to turn on as fast as possible
// because we don't know which output actually triggered the fault pin
DIAG(F("*** COMMON FAULT PIN ACTIVE - TOGGLED POWER on %S ***"), isMainTrack ? F("MAIN") : F("PROG"));
DIAG(F("COMMON FAULT PIN ACTIVE - TOGGLED POWER on %S"), trackname);
} else {
DIAG(F("*** %S FAULT PIN ACTIVE - OVERLOAD ***"), isMainTrack ? F("MAIN") : F("PROG"));
DIAG(F("%S FAULT PIN ACTIVE - OVERLOAD"), trackname);
if (lastCurrent < tripValue) {
lastCurrent = tripValue; // exaggerate
}
@@ -161,7 +164,7 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) {
unsigned int maxmA=motorDriver->raw2mA(tripValue);
power_good_counter=0;
sampleDelay = power_sample_overload_wait;
DIAG(F("*** %S TRACK POWER OVERLOAD current=%d max=%d offtime=%d ***"), isMainTrack ? F("MAIN") : F("PROG"), mA, maxmA, sampleDelay);
DIAG(F("%S TRACK POWER OVERLOAD current=%d max=%d offtime=%d"), trackname, mA, maxmA, sampleDelay);
if (power_sample_overload_wait >= 10000)
power_sample_overload_wait = 10000;
else
@@ -173,7 +176,7 @@ void DCCWaveform::checkPowerOverload(bool ackManagerActive) {
setPowerMode(POWERMODE::ON);
sampleDelay = POWER_SAMPLE_ON_WAIT;
// Debug code....
DIAG(F("*** %S TRACK POWER RESET delay=%d ***"), isMainTrack ? F("MAIN") : F("PROG"), sampleDelay);
DIAG(F("%S TRACK POWER RESET delay=%d"), trackname, sampleDelay);
break;
default:
sampleDelay = 999; // cant get here..meaningless statement to avoid compiler warning.
@@ -197,6 +200,8 @@ const bool DCCWaveform::signalTransform[]={
/* WAVE_LOW_0 -> */ LOW,
/* WAVE_PENDING (should not happen) -> */ LOW};
#pragma GCC push_options
#pragma GCC optimize ("-O3")
void DCCWaveform::interrupt2() {
// calculate the next bit to be sent:
// set state WAVE_MID_1 for a 1=bit
@@ -252,7 +257,7 @@ void DCCWaveform::interrupt2() {
}
}
}
#pragma GCC pop_options
// Wait until there is no packet pending, then make this pending
@@ -306,6 +311,8 @@ byte DCCWaveform::getAck() {
return(0); // pending set off but not detected means no ACK.
}
#pragma GCC push_options
#pragma GCC optimize ("-O3")
void DCCWaveform::checkAck() {
// This function operates in interrupt() time so must be fast and can't DIAG
if (sentResetsSincePacket > 6) { //ACK timeout
@@ -355,3 +362,4 @@ void DCCWaveform::checkAck() {
}
ackPulseStart=0; // We have detected a too-short or too-long pulse so ignore and wait for next leading edge
}
#pragma GCC pop_options

View File

@@ -1,6 +1,6 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
@@ -17,11 +17,6 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// dummy LCD shim to keep linker happy
LCDDisplay::LCDDisplay() {}
void LCDDisplay::interfake(int p1, int p2, int p3) {(void)p1; (void)p2; (void)p3;}
void LCDDisplay::setRowNative(byte row) { (void)row;}
void LCDDisplay::clearNative() {}
void LCDDisplay::writeNative(char b){ (void)b;} //
void LCDDisplay::displayNative(){}
#include "DisplayInterface.h"
DisplayInterface *DisplayInterface::lcdDisplay = 0;

View File

@@ -1,6 +1,6 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
@@ -16,18 +16,20 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "LiquidCrystal_I2C.h"
LiquidCrystal_I2C LCDDriver(LCD_DRIVER); // set the LCD address, cols, rows
// DEVICE SPECIFIC LCDDisplay Implementation for LCD_DRIVER
LCDDisplay::LCDDisplay() {
lcdDisplay=this;
LCDDriver.init();
LCDDriver.backlight();
interfake(LCD_DRIVER);
clear();
}
void LCDDisplay::interfake(int p1, int p2, int p3) {(void)p1; (void)p2; lcdRows=p3; }
void LCDDisplay::clearNative() {LCDDriver.clear();}
void LCDDisplay::setRowNative(byte row) { LCDDriver.setCursor(0, row); }
void LCDDisplay::writeNative(char b){ LCDDriver.write(b); }
void LCDDisplay::displayNative() { LCDDriver.display(); }
#ifndef DisplayInterface_h
#define DisplayInterface_h
#include <Arduino.h>
// Definition of base class for displays. The base class does nothing.
class DisplayInterface : public Print {
public:
virtual DisplayInterface* loop2(bool force) { (void)force; return NULL; };
virtual void setRow(byte line) { (void)line; };
virtual void clear() {};
virtual size_t write(uint8_t c) { (void)c; return 0; };
static DisplayInterface *lcdDisplay;
};
#endif

View File

@@ -18,89 +18,92 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "defines.h"
#ifndef DISABLE_EEPROM
#include "EEStore.h"
#include "Turnouts.h"
#include "Sensors.h"
#include "Outputs.h"
#include "DIAG.h"
#include "Outputs.h"
#include "Sensors.h"
#include "Turnouts.h"
#if defined(ARDUINO_ARCH_SAMD)
ExternalEEPROM EEPROM;
#endif
void EEStore::init(){
void EEStore::init() {
#if defined(ARDUINO_ARCH_SAMD)
EEPROM.begin(0x50); // Address for Microchip 24-series EEPROM with all three A pins grounded (0b1010000 = 0x50)
EEPROM.begin(0x50); // Address for Microchip 24-series EEPROM with all three
// A pins grounded (0b1010000 = 0x50)
#endif
eeStore=(EEStore *)calloc(1,sizeof(EEStore));
EEPROM.get(0,eeStore->data); // get eeStore data
eeStore = (EEStore *)calloc(1, sizeof(EEStore));
if(strncmp(eeStore->data.id,EESTORE_ID,sizeof(EESTORE_ID))!=0){ // check to see that eeStore contains valid DCC++ ID
sprintf(eeStore->data.id,EESTORE_ID); // if not, create blank eeStore structure (no turnouts, no sensors) and save it back to EEPROM
eeStore->data.nTurnouts=0;
eeStore->data.nSensors=0;
eeStore->data.nOutputs=0;
EEPROM.put(0,eeStore->data);
}
EEPROM.get(0, eeStore->data); // get eeStore data
reset(); // set memory pointer to first free EEPROM space
Turnout::load(); // load turnout definitions
Sensor::load(); // load sensor definitions
Output::load(); // load output definitions
// check to see that eeStore contains valid DCC++ ID
if (strncmp(eeStore->data.id, EESTORE_ID, sizeof(EESTORE_ID)) != 0) {
// if not, create blank eeStore structure (no
// turnouts, no sensors) and save it back to EEPROM
strncpy(eeStore->data.id, EESTORE_ID, sizeof(EESTORE_ID));
eeStore->data.nTurnouts = 0;
eeStore->data.nSensors = 0;
eeStore->data.nOutputs = 0;
EEPROM.put(0, eeStore->data);
}
reset(); // set memory pointer to first free EEPROM space
Turnout::load(); // load turnout definitions
Sensor::load(); // load sensor definitions
Output::load(); // load output definitions
}
///////////////////////////////////////////////////////////////////////////////
void EEStore::clear(){
sprintf(eeStore->data.id,EESTORE_ID); // create blank eeStore structure (no turnouts, no sensors) and save it back to EEPROM
eeStore->data.nTurnouts=0;
eeStore->data.nSensors=0;
eeStore->data.nOutputs=0;
EEPROM.put(0,eeStore->data);
void EEStore::clear() {
sprintf(eeStore->data.id,
EESTORE_ID); // create blank eeStore structure (no turnouts, no
// sensors) and save it back to EEPROM
eeStore->data.nTurnouts = 0;
eeStore->data.nSensors = 0;
eeStore->data.nOutputs = 0;
EEPROM.put(0, eeStore->data);
}
///////////////////////////////////////////////////////////////////////////////
void EEStore::store(){
reset();
Turnout::store();
Sensor::store();
Output::store();
EEPROM.put(0,eeStore->data);
void EEStore::store() {
reset();
Turnout::store();
Sensor::store();
Output::store();
EEPROM.put(0, eeStore->data);
DIAG(F("EEPROM used: %d/%d bytes"), EEStore::pointer(), EEPROM.length());
}
///////////////////////////////////////////////////////////////////////////////
void EEStore::advance(int n){
eeAddress+=n;
}
void EEStore::advance(int n) { eeAddress += n; }
///////////////////////////////////////////////////////////////////////////////
void EEStore::reset(){
eeAddress=sizeof(EEStore);
}
void EEStore::reset() { eeAddress = sizeof(EEStore); }
///////////////////////////////////////////////////////////////////////////////
int EEStore::pointer(){
return(eeAddress);
}
int EEStore::pointer() { return (eeAddress); }
///////////////////////////////////////////////////////////////////////////////
void EEStore::dump(int num) {
byte b;
DIAG(F("Addr 0x char"));
for (int n=0 ; n<num; n++) {
EEPROM.get(n, b);
DIAG(F("%d %x %c"),n,b,isprint(b) ? b : ' ');
}
byte b;
DIAG(F("Addr 0x char"));
for (int n = 0; n < num; n++) {
EEPROM.get(n, b);
DIAG(F("%d %x %c"), n, b, isprint(b) ? b : ' ');
}
}
///////////////////////////////////////////////////////////////////////////////
EEStore *EEStore::eeStore=NULL;
int EEStore::eeAddress=0;
EEStore *EEStore::eeStore = NULL;
int EEStore::eeAddress = 0;
#endif

View File

@@ -1,3 +1,23 @@
/*
* (c) 2020 Chris Harlow. All rights reserved.
* (c) 2020 Harald Barth. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef DISABLE_EEPROM
#ifndef EEStore_h
#define EEStore_h
@@ -10,13 +30,13 @@ extern ExternalEEPROM EEPROM;
#include <EEPROM.h>
#endif
#define EESTORE_ID "DCC++"
#define EESTORE_ID "DCC++1"
struct EEStoreData{
char id[sizeof(EESTORE_ID)];
int nTurnouts;
int nSensors;
int nOutputs;
uint16_t nTurnouts;
uint16_t nSensors;
uint16_t nOutputs;
};
struct EEStore{
@@ -33,3 +53,4 @@ struct EEStore{
};
#endif
#endif // DISABLE_EEPROM

52
EtherCard.cpp Normal file
View File

@@ -0,0 +1,52 @@
// This code slightly follows the conventions of, but is not derived from:
// EHTERSHIELD_H library for Arduino etherShield
// Copyright (c) 2008 Xing Yu. All right reserved. (this is LGPL v2.1)
// It is however derived from the enc28j60 and ip code (which is GPL v2)
// Author: Pascal Stang
// Modified by: Guido Socher
// DHCP code: Andrew Lindsay
// Hence: GPL V2
//
// 2010-05-19 <jc@wippler.nl>
#include "EtherCard.h"
#include <stdarg.h>
#include <avr/eeprom.h>
EtherCard ether;
uint8_t EtherCard::mymac[ETH_LEN]; // my MAC address
uint8_t EtherCard::myip[IP_LEN]; // my ip address
uint8_t EtherCard::netmask[IP_LEN]; // subnet mask
uint8_t EtherCard::broadcastip[IP_LEN]; // broadcast address
uint8_t EtherCard::gwip[IP_LEN]; // gateway
uint8_t EtherCard::dhcpip[IP_LEN]; // dhcp server
uint8_t EtherCard::dnsip[IP_LEN]; // dns server
uint8_t EtherCard::hisip[IP_LEN]; // ip address of remote host
uint16_t EtherCard::delaycnt = 0; //request gateway ARP lookup
uint8_t EtherCard::begin (const uint16_t size,
const uint8_t* macaddr,
uint8_t csPin) {
copyMac(mymac, macaddr);
return initialize(size, mymac, csPin);
}
bool EtherCard::staticSetup (const uint8_t* my_ip,
const uint8_t* gw_ip,
const uint8_t* dns_ip,
const uint8_t* mask) {
if (my_ip != 0)
copyIp(myip, my_ip);
if (gw_ip != 0)
setGwIp(gw_ip);
if (dns_ip != 0)
copyIp(dnsip, dns_ip);
if(mask != 0)
copyIp(netmask, mask);
updateBroadcastAddress();
delaycnt = 0; //request gateway ARP lookup
return true;
}

238
EtherCard.h Normal file
View File

@@ -0,0 +1,238 @@
// This code slightly follows the conventions of, but is not derived from:
// EHTERSHIELD_H library for Arduino etherShield
// Copyright (c) 2008 Xing Yu. All right reserved. (this is LGPL v2.1)
// It is however derived from the enc28j60 and ip code (which is GPL v2)
// Author: Pascal Stang
// Modified by: Guido Socher
// DHCP code: Andrew Lindsay
// Hence: GPL V2
//
// 2010-05-19 <jc@wippler.nl>
//
//
// PIN Connections (Using Arduino UNO):
// VCC - 3.3V
// GND - GND
// SCK - Pin 13
// SO - Pin 12
// SI - Pin 11
// CS - Pin 8
//
/** @file */
#ifndef EtherCard_h
#define EtherCard_h
// #ifndef __PROG_TYPES_COMPAT__
// #define __PROG_TYPES_COMPAT__
// #endif
// #if ARDUINO >= 100
#include <Arduino.h> // Arduino 1.0
// #define WRITE_RESULT size_t
// #define WRITE_RETURN return 1;
// #else
// #include <WProgram.h> // Arduino 0022
// #define WRITE_RESULT void
// #define WRITE_RETURN
// #endif
// #include <avr/pgmspace.h>
#include "enc28j60.h"
// Based on the net.h file from the AVRlib library by Pascal Stang.
// Author: Guido Socher
// Copyright: GPL V2
//
// For AVRlib See http://www.procyonengineering.com/
// Used with explicit permission of Pascal Stang.
//
// 2010-05-20 <jc@wippler.nl>
// notation: _P = position of a field
// _V = value of a field
// ******* ETH *******
#define ETH_HEADER_LEN 14
#define ETH_LEN 6
// values of certain bytes:
#define ETHTYPE_ARP_H_V 0x08
#define ETHTYPE_ARP_L_V 0x06
#define ETHTYPE_IP_H_V 0x08
#define ETHTYPE_IP_L_V 0x00
// byte positions in the ethernet frame:
//
// Ethernet type field (2bytes):
#define ETH_TYPE_H_P 12
#define ETH_TYPE_L_P 13
//
#define ETH_DST_MAC 0
#define ETH_SRC_MAC 6
// ******* ARP *******
#define ETH_ARP_OPCODE_REPLY_H_V 0x0
#define ETH_ARP_OPCODE_REPLY_L_V 0x02
#define ETH_ARP_OPCODE_REQ_H_V 0x0
#define ETH_ARP_OPCODE_REQ_L_V 0x01
// start of arp header:
#define ETH_ARP_P 0xe
//
#define ETHTYPE_ARP_L_V 0x06
// arp.dst.ip
#define ETH_ARP_DST_IP_P 0x26
// arp.opcode
#define ETH_ARP_OPCODE_H_P 0x14
#define ETH_ARP_OPCODE_L_P 0x15
// arp.src.mac
#define ETH_ARP_SRC_MAC_P 0x16
#define ETH_ARP_SRC_IP_P 0x1c
#define ETH_ARP_DST_MAC_P 0x20
#define ETH_ARP_DST_IP_P 0x26
// ******* IP *******
#define IP_HEADER_LEN 20
#define IP_LEN 4
// ip.src
#define IP_SRC_P 0x1a
#define IP_DST_P 0x1e
#define IP_HEADER_LEN_VER_P 0xe
#define IP_CHECKSUM_P 0x18
#define IP_TTL_P 0x16
#define IP_FLAGS_P 0x14
#define IP_P 0xe
#define IP_TOTLEN_H_P 0x10
#define IP_TOTLEN_L_P 0x11
#define IP_PROTO_P 0x17
#define IP_PROTO_ICMP_V 1
#define IP_PROTO_TCP_V 6
// 17=0x11
#define IP_PROTO_UDP_V 17
// ******* ICMP *******
#define ICMP_TYPE_ECHOREPLY_V 0
#define ICMP_TYPE_ECHOREQUEST_V 8
//
#define ICMP_TYPE_P 0x22
#define ICMP_CHECKSUM_P 0x24
#define ICMP_CHECKSUM_H_P 0x24
#define ICMP_CHECKSUM_L_P 0x25
#define ICMP_IDENT_H_P 0x26
#define ICMP_IDENT_L_P 0x27
#define ICMP_DATA_P 0x2a
// ******* UDP *******
#define UDP_HEADER_LEN 8
//
#define UDP_SRC_PORT_H_P 0x22
#define UDP_SRC_PORT_L_P 0x23
#define UDP_DST_PORT_H_P 0x24
#define UDP_DST_PORT_L_P 0x25
//
#define UDP_LEN_H_P 0x26
#define UDP_LEN_L_P 0x27
#define UDP_CHECKSUM_H_P 0x28
#define UDP_CHECKSUM_L_P 0x29
#define UDP_DATA_P 0x2a
/** Enable DHCP.
* Setting this to zero disables the use of DHCP; if a program uses DHCP it will
* still compile but the program will not work. Saves about 60 bytes SRAM and
* 1550 bytes flash.
*/
#define ETHERCARD_DHCP 0
/** Enable client connections.
* Setting this to zero means that the program cannot issue TCP client requests
* anymore. Compilation will still work but the request will never be
* issued. Saves 4 bytes SRAM and 550 byte flash.
*/
#define ETHERCARD_TCPCLIENT 0
/** Enable TCP server functionality.
* Setting this to zero means that the program will not accept TCP client
* requests. Saves 2 bytes SRAM and 250 bytes flash.
*/
#define ETHERCARD_TCPSERVER 0
/** Enable UDP server functionality.
* If zero UDP server is disabled. It is
* still possible to register callbacks but these will never be called. Saves
* about 40 bytes SRAM and 200 bytes flash. If building with -flto this does not
* seem to save anything; maybe the linker is then smart enough to optimize the
* call away.
*/
#define ETHERCARD_UDPSERVER 0
/** Enable automatic reply to pings.
* Setting to zero means that the program will not automatically answer to
* PINGs anymore. Also the callback that can be registered to answer incoming
* pings will not be called. Saves 2 bytes SRAM and 230 bytes flash.
*/
#define ETHERCARD_ICMP 1
/** Enable use of stash.
* Setting this to zero means that the stash mechanism cannot be used. Again
* compilation will still work but the program may behave very unexpectedly.
* Saves 30 bytes SRAM and 80 bytes flash.
*/
#define ETHERCARD_STASH 0
/** This type definition defines the structure of a UDP server event handler callback function */
typedef void (*UdpServerCallback)(
uint16_t dest_port, ///< Port the packet was sent to
uint8_t src_ip[IP_LEN], ///< IP address of the sender
uint16_t src_port, ///< Port the packet was sent from
const char *data, ///< UDP payload data
uint16_t len); ///< Length of the payload data
/** This type definition defines the structure of a DHCP Option callback function */
typedef void (*DhcpOptionCallback)(
uint8_t option, ///< The option number
const byte* data, ///< DHCP option data
uint8_t len); ///< Length of the DHCP option data
/** This class provides the main interface to a ENC28J60 based network interface card and is the class most users will use.
* @note All TCP/IP client (outgoing) connections are made from source port in range 2816-3071. Do not use these source ports for other purposes.
*/
class EtherCard : public ENC28J60 {
public:
static uint8_t mymac[ETH_LEN]; ///< MAC address
static uint8_t myip[IP_LEN]; ///< IP address
static uint8_t netmask[IP_LEN]; ///< Netmask
static uint8_t broadcastip[IP_LEN]; ///< Subnet broadcast address
static uint8_t gwip[IP_LEN]; ///< Gateway
static uint8_t dhcpip[IP_LEN]; ///< DHCP server IP address
static uint8_t dnsip[IP_LEN]; ///< DNS server IP address
static uint8_t hisip[IP_LEN]; ///< DNS lookup result
static uint16_t hisport; ///< TCP port to connect to (default 80)
static bool using_dhcp; ///< True if using DHCP
static bool persist_tcp_connection; ///< False to break connections on first packet received
static uint16_t delaycnt; ///< Counts number of cycles of packetLoop when no packet received - used to trigger periodic gateway ARP request
static uint8_t begin (const uint16_t size, const uint8_t* macaddr,
uint8_t csPin = SS);
static bool staticSetup (const uint8_t* my_ip,
const uint8_t* gw_ip = 0,
const uint8_t* dns_ip = 0,
const uint8_t* mask = 0);
static void makeUdpReply (const char *data, uint8_t len, uint16_t port);
static uint16_t packetLoop (uint16_t plen);
static void setGwIp (const uint8_t *gwipaddr);
static void updateBroadcastAddress();
static uint8_t clientWaitingGw ();
static void udpPrepare (uint16_t sport, const uint8_t *dip, uint16_t dport);
static void udpTransmit (uint16_t len);
static void sendUdp (const char *data, uint8_t len, uint16_t sport,
const uint8_t *dip, uint16_t dport);
static void copyIp (uint8_t *dst, const uint8_t *src);
static void copyMac (uint8_t *dst, const uint8_t *src);
};
extern EtherCard ether; //!< Global presentation of EtherCard class
#endif

View File

@@ -17,12 +17,6 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*
*/
#if __has_include ( "config.h")
#include "config.h"
#else
#warning config.h not found. Using defaults from config.example.h
#include "config.example.h"
#endif
#include "defines.h"
#if ETHERNET_ON == true
#include "EthernetInterface.h"
@@ -68,6 +62,16 @@ EthernetInterface::EthernetInterface()
DIAG(F("Ethernet shield not found"));
return;
}
unsigned long startmilli = millis();
while ((millis() - startmilli) < 5500) // Loop to give time to check for cable connection
{
if (Ethernet.linkStatus() == LinkON)
break;
DIAG(F("Ethernet waiting for link (1sec) "));
delay(1000);
}
if (Ethernet.linkStatus() == LinkOFF) {
DIAG(F("Ethernet cable not connected"));
return;
@@ -174,8 +178,14 @@ void EthernetInterface::loop()
int socketOut=outboundRing->read();
if (socketOut>=0) {
int count=outboundRing->count();
uint16_t index = 0;
if (Diag::ETHERNET) DIAG(F("Ethernet reply socket=%d, count=:%d"), socketOut,count);
for(;count>0;count--) clients[socketOut].write(outboundRing->read());
while (count > 0) {
index = 0;
for(;count>0 && index < MAX_ETH_BUFFER;count--) buffer[index++] = outboundRing->read();
clients[socketOut].write(buffer, index);
}
//for(;count>0;count--) clients[socketOut].write(outboundRing->read());
clients[socketOut].flush(); //maybe
}
}

View File

@@ -22,12 +22,8 @@
#ifndef EthernetInterface_h
#define EthernetInterface_h
#if __has_include ( "config.h")
#include "config.h"
#else
#warning config.h not found. Using defaults from config.example.h
#include "config.example.h"
#endif
#include "defines.h"
#include "DCCEXParser.h"
#include <Arduino.h>
#include <avr/pgmspace.h>

23
FSH.h
View File

@@ -1,3 +1,21 @@
/*
* (c) 2021 Fred Decker. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef FSH_h
#define FSH_h
@@ -11,6 +29,7 @@
* __FlashStringHelper Use FSH instead.
* PROGMEM use FLASH instead
* pgm_read_byte_near use GETFLASH instead.
* pgm_read_word_near use GETFLASHW instead.
*
*/
#include <Arduino.h>
@@ -21,10 +40,14 @@
#define F(str) (str)
typedef char FSH;
#define GETFLASH(addr) (*(const unsigned char *)(addr))
#define GETFLASHW(addr) (*(const unsigned short *)(addr))
#define FLASH
#define strlen_P strlen
#define strcpy_P strcpy
#else
typedef __FlashStringHelper FSH;
#define GETFLASH(addr) pgm_read_byte_near(addr)
#define GETFLASHW(addr) pgm_read_word_near(addr)
#define FLASH PROGMEM
#endif
#endif

View File

@@ -1 +1 @@
#define GITHUB_SHA "6766d95"
#define GITHUB_SHA "9018ec9"

View File

@@ -18,14 +18,40 @@
*/
#include <stdarg.h>
#include <Wire.h>
#include "I2CManager.h"
#include "DIAG.h"
// If not already initialised, initialise I2C (wire).
// Include target-specific portions of I2CManager class
#if defined(I2C_USE_WIRE)
#include "I2CManager_Wire.h"
#elif defined(ARDUINO_ARCH_AVR)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_AVR.h" // Uno/Nano/Mega2560
#elif defined(ARDUINO_ARCH_MEGAAVR)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_Mega4809.h" // NanoEvery/UnoWifi
#else
#define I2C_USE_WIRE
#include "I2CManager_Wire.h" // Other platforms
#endif
// If not already initialised, initialise I2C
void I2CManagerClass::begin(void) {
//setTimeout(25000); // 25 millisecond timeout
if (!_beginCompleted) {
Wire.begin();
_beginCompleted = true;
_initialise();
// Probe and list devices.
bool found = false;
for (byte addr=1; addr<127; addr++) {
if (exists(addr)) {
found = true;
DIAG(F("I2C Device found at x%x"), addr);
}
}
if (!found) DIAG(F("No I2C Devices found"));
}
}
@@ -34,8 +60,8 @@ void I2CManagerClass::begin(void) {
void I2CManagerClass::setClock(uint32_t speed) {
if (speed < _clockSpeed && !_clockSpeedFixed) {
_clockSpeed = speed;
Wire.setClock(_clockSpeed);
}
_setClock(_clockSpeed);
}
// Force clock speed to that specified. It can then only
@@ -44,39 +70,21 @@ void I2CManagerClass::forceClock(uint32_t speed) {
if (!_clockSpeedFixed) {
_clockSpeed = speed;
_clockSpeedFixed = true;
Wire.setClock(_clockSpeed);
_setClock(_clockSpeed);
}
}
// Check if specified I2C address is responding.
// Returns 0 if OK, or error code.
// Check if specified I2C address is responding (blocking operation)
// Returns I2C_STATUS_OK (0) if OK, or error code.
uint8_t I2CManagerClass::checkAddress(uint8_t address) {
begin();
Wire.beginTransmission(address);
return Wire.endTransmission();
return write(address, NULL, 0);
}
bool I2CManagerClass::exists(uint8_t address) {
return checkAddress(address)==0;
}
// Write a complete transmission to I2C using a supplied buffer of data
uint8_t I2CManagerClass::write(uint8_t address, const uint8_t buffer[], uint8_t size) {
Wire.beginTransmission(address);
Wire.write(buffer, size);
return Wire.endTransmission();
}
// Write a complete transmission to I2C using a supplied buffer of data in Flash
uint8_t I2CManagerClass::write_P(uint8_t address, const uint8_t buffer[], uint8_t size) {
uint8_t ramBuffer[size];
memcpy_P(ramBuffer, buffer, size);
return write(address, ramBuffer, size);
}
// Write a complete transmission to I2C using a list of data
uint8_t I2CManagerClass::write(uint8_t address, int nBytes, ...) {
/***************************************************************************
* Write a transmission to I2C using a list of data (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(uint8_t address, uint8_t nBytes, ...) {
uint8_t buffer[nBytes];
va_list args;
va_start(args, nBytes);
@@ -86,30 +94,38 @@ uint8_t I2CManagerClass::write(uint8_t address, int nBytes, ...) {
return write(address, buffer, nBytes);
}
// Write a command and read response, returns number of bytes received.
// Different modules use different ways of accessing registers:
// PCF8574 I/O expander justs needs the address (no data);
// PCA9685 needs a two byte command to select the register(s) to be read;
// MCP23016 needs a one-byte command to select the register.
// Some devices use 8-bit registers exclusively and some have 16-bit registers.
// Therefore the following function is general purpose, to apply to any
// type of I2C device.
//
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeBuffer[], uint8_t writeSize) {
if (writeSize > 0) {
Wire.beginTransmission(address);
Wire.write(writeBuffer, writeSize);
Wire.endTransmission(false); // Don't free bus yet
}
Wire.requestFrom(address, readSize);
uint8_t nBytes = 0;
while (Wire.available() && nBytes < readSize)
readBuffer[nBytes++] = Wire.read();
return nBytes;
/***************************************************************************
* Initiate a write to an I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(uint8_t i2cAddress, const uint8_t writeBuffer[], uint8_t writeLen) {
I2CRB req;
uint8_t status = write(i2cAddress, writeBuffer, writeLen, &req);
return finishRB(&req, status);
}
// Overload of read() to allow command to be specified as a series of bytes.
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write_P(uint8_t i2cAddress, const uint8_t * data, uint8_t dataLen) {
I2CRB req;
uint8_t status = write_P(i2cAddress, data, dataLen, &req);
return finishRB(&req, status);
}
/***************************************************************************
* Initiate a write (optional) followed by a read from the I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen)
{
I2CRB req;
uint8_t status = read(i2cAddress, readBuffer, readLen, writeBuffer, writeLen, &req);
return finishRB(&req, status);
}
/***************************************************************************
* Overload of read() to allow command to be specified as a series of bytes (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeSize, ...) {
va_list args;
@@ -122,8 +138,104 @@ uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t rea
return read(address, readBuffer, readSize, writeBuffer, writeSize);
}
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize) {
return read(address, readBuffer, readSize, NULL, 0);
/***************************************************************************
* Finish off request block by posting status, etc. (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::finishRB(I2CRB *rb, uint8_t status) {
if ((status == I2C_STATUS_OK) && rb)
status = rb->wait();
return status;
}
/***************************************************************************
* Get a message corresponding to the error status
***************************************************************************/
const FSH *I2CManagerClass::getErrorMessage(uint8_t status) {
switch (status) {
case I2C_STATUS_OK: return F("OK");
case I2C_STATUS_TRUNCATED: return F("Transmission truncated");
case I2C_STATUS_NEGATIVE_ACKNOWLEDGE: return F("No response from device (address NAK)");
case I2C_STATUS_TRANSMIT_ERROR: return F("Transmit error (data NAK)");
case I2C_STATUS_OTHER_TWI_ERROR: return F("Other Wire/TWI error");
case I2C_STATUS_TIMEOUT: return F("Timeout");
case I2C_STATUS_ARBITRATION_LOST: return F("Arbitration lost");
case I2C_STATUS_BUS_ERROR: return F("I2C bus error");
case I2C_STATUS_UNEXPECTED_ERROR: return F("Unexpected error");
case I2C_STATUS_PENDING: return F("Request pending");
default: return F("Error code not recognised");
}
}
/***************************************************************************
* Declare singleton class instance.
***************************************************************************/
I2CManagerClass I2CManager = I2CManagerClass();
/////////////////////////////////////////////////////////////////////////////
// Helper functions associated with I2C Request Block
/////////////////////////////////////////////////////////////////////////////
/***************************************************************************
* Block waiting for request block to complete, and return completion status.
* Since such a loop could potentially last for ever if the RB status doesn't
* change, we set a high limit (1sec, 1000ms) on the wait time and, if it
* hasn't changed by that time we assume it's not going to, and just return
* a timeout status. This means that CS will not lock up.
***************************************************************************/
uint8_t I2CRB::wait() {
unsigned long waitStart = millis();
do {
I2CManager.loop();
// Rather than looping indefinitely, let's set a very high timeout (1s).
if ((millis() - waitStart) > 1000UL) {
DIAG(F("I2C TIMEOUT I2C:x%x I2CRB:x%x"), i2cAddress, this);
status = I2C_STATUS_TIMEOUT;
// Note that, although the timeout is posted, the request may yet complete.
// TODO: Ideally we would like to cancel the request.
return status;
}
} while (status==I2C_STATUS_PENDING);
return status;
}
/***************************************************************************
* Check whether request is still in progress.
***************************************************************************/
bool I2CRB::isBusy() {
I2CManager.loop();
return (status==I2C_STATUS_PENDING);
}
/***************************************************************************
* Helper functions to fill the I2CRequest structure with parameters.
***************************************************************************/
void I2CRB::setReadParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen) {
this->i2cAddress = i2cAddress;
this->writeLen = 0;
this->readBuffer = readBuffer;
this->readLen = readLen;
this->operation = OPERATION_READ;
this->status = I2C_STATUS_OK;
}
void I2CRB::setRequestParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen) {
this->i2cAddress = i2cAddress;
this->writeBuffer = writeBuffer;
this->writeLen = writeLen;
this->readBuffer = readBuffer;
this->readLen = readLen;
this->operation = OPERATION_REQUEST;
this->status = I2C_STATUS_OK;
}
void I2CRB::setWriteParams(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen) {
this->i2cAddress = i2cAddress;
this->writeBuffer = writeBuffer;
this->writeLen = writeLen;
this->readLen = 0;
this->operation = OPERATION_SEND;
this->status = I2C_STATUS_OK;
}
I2CManagerClass I2CManager = I2CManagerClass();

View File

@@ -17,13 +17,16 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CManager_h
#define I2CManager_h
#ifndef I2CMANAGER_H
#define I2CMANAGER_H
#include <inttypes.h>
#include "FSH.h"
/*
* Helper class to manage access to the I2C 'Wire' subsystem.
* Manager for I2C communications. For portability, it allows use
* of the Wire class, but also has a native implementation for AVR
* which supports non-blocking queued I/O requests.
*
* Helps to avoid calling Wire.begin() multiple times (which is not)
* entirely benign as it reinitialises).
@@ -33,13 +36,157 @@
*
* Thirdly, it provides a convenient way to check whether there is a
* device on a particular I2C address.
*
* Non-blocking requests are issued by creating an I2C Request Block
* (I2CRB) which is then added to the I2C manager's queue. The
* application refers to this block to check for completion of the
* operation, and for reading completion status.
*
* Examples:
* I2CRB rb;
* uint8_t status = I2CManager.write(address, buffer, sizeof(buffer), &rb);
* ...
* if (!rb.isBusy()) {
* status = rb.status;
* // Repeat write
* I2CManager.queueRequest(&rb);
* ...
* status = rb.wait(); // Wait for completion and read status
* }
* ...
* I2CRB rb2;
* outbuffer[0] = 12; // Register number in I2C device to be read
* rb2.setRequestParams(address, inBuffer, 1, outBuffer, 1);
* status = I2CManager.queueRequest(&rb2);
* if (status == I2C_STATUS_OK) {
* status = rb2.wait();
* if (status == I2C_STATUS_OK) {
* registerValue = inBuffer[0];
* }
* }
* ...
*
* Synchronous (blocking) calls are also possible, e.g.
* status = I2CManager.write(address, buffer, sizeof(buffer));
*
* When using non-blocking requests, neither the I2CRB nor the input or output
* buffers should be modified until the I2CRB is complete (not busy).
*
* Timeout monitoring is possible, but requires that the following call is made
* reasonably frequently in the program's loop() function:
* I2CManager.loop();
*
*/
class I2CManagerClass {
/*
* Future enhancement possibility:
*
* I2C Multiplexer (e.g. TCA9547, TCA9548)
*
* A multiplexer offers a way of extending the address range of I2C devices. For example, GPIO extenders use address range 0x20-0x27
* to are limited to 8 on a bus. By adding a multiplexer, the limit becomes 8 for each of the multiplexer's 8 sub-buses, i.e. 64.
* And a single I2C bus can have up to 8 multiplexers, giving up to 64 sub-buses and, in theory, up to 512 I/O extenders; that's
* as many as 8192 input/output pins!
* Secondly, the capacitance of the bus is an electrical limiting factor of the length of the bus, speed and number of devices.
* The multiplexer isolates each sub-bus from the others, and so reduces the capacitance of the bus. For example, with one
* multiplexer and 64 GPIO extenders, only 9 devices are connected to the bus at any time (multiplexer plus 8 extenders).
* Thirdly, the multiplexer offers the ability to use mixed-speed devices more effectively, by allowing high-speed devices to be
* put on a different bus to low-speed devices, enabling the software to switch the I2C speed on-the-fly between I2C transactions.
*
* Changes required: Increase the size of the I2CAddress field in the IODevice class from uint8_t to uint16_t.
* The most significant byte would contain a '1' bit flag, the multiplexer number (0-7) and bus number (0-7). Then, when performing
* an I2C operation, the I2CManager would check this byte and, if zero, do what it currently does. If the byte is non-zero, then
* that means the device is connected via a multiplexer so the I2C transaction should be preceded by a select command issued to the
* relevant multiplexer.
*
* Non-interrupting I2C:
*
* I2C may be operated without interrupts (undefine I2C_USE_INTERRUPTS). Instead, the I2C state
* machine handler, currently invoked from the interrupt service routine, is invoked from the loop() function.
* The speed at which I2C operations can be performed then becomes highly dependent on the frequency that
* the loop() function is called, and may be adequate under some circumstances.
* The advantage of NOT using interrupts is that the impact of I2C upon the DCC waveform (when accurate timing mode isn't in use)
* becomes almost zero.
*
*/
// Uncomment following line to enable Wire library instead of native I2C drivers
//#define I2C_USE_WIRE
// Uncomment following line to disable the use of interrupts by the native I2C drivers.
//#define I2C_NO_INTERRUPTS
// Default to use interrupts within the native I2C drivers.
#ifndef I2C_NO_INTERRUPTS
#define I2C_USE_INTERRUPTS
#endif
// Status codes for I2CRB structures.
enum : uint8_t {
// Codes used by Wire and by native drivers
I2C_STATUS_OK=0,
I2C_STATUS_TRUNCATED=1,
I2C_STATUS_NEGATIVE_ACKNOWLEDGE=2,
I2C_STATUS_TRANSMIT_ERROR=3,
I2C_STATUS_TIMEOUT=5,
// Code used by Wire only
I2C_STATUS_OTHER_TWI_ERROR=4, // catch-all error
// Codes used by native drivers only
I2C_STATUS_ARBITRATION_LOST=6,
I2C_STATUS_BUS_ERROR=7,
I2C_STATUS_UNEXPECTED_ERROR=8,
I2C_STATUS_PENDING=253,
};
// Status codes for the state machine (not returned to caller).
enum : uint8_t {
I2C_STATE_ACTIVE=253,
I2C_STATE_FREE=254,
I2C_STATE_CLOSING=255,
};
typedef enum : uint8_t
{
OPERATION_READ = 1,
OPERATION_REQUEST = 2,
OPERATION_SEND = 3,
OPERATION_SEND_P = 4,
} OperationEnum;
// Default I2C frequency
#ifndef I2C_FREQ
#define I2C_FREQ 400000L
#endif
// Class defining a request context for an I2C operation.
class I2CRB {
public:
volatile uint8_t status; // Completion status, or pending flag (updated from IRC)
volatile uint8_t nBytes; // Number of bytes read (updated from IRC)
I2CManagerClass() {}
inline I2CRB() { status = I2C_STATUS_OK; };
uint8_t wait();
bool isBusy();
void setReadParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen);
void setRequestParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen, const uint8_t *writeBuffer, uint8_t writeLen);
void setWriteParams(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen);
uint8_t writeLen;
uint8_t readLen;
uint8_t operation;
uint8_t i2cAddress;
uint8_t *readBuffer;
const uint8_t *writeBuffer;
#if !defined(I2C_USE_WIRE)
I2CRB *nextRequest;
#endif
};
// I2C Manager
class I2CManagerClass {
public:
// If not already initialised, initialise I2C (wire).
void begin(void);
@@ -49,28 +196,93 @@ public:
void forceClock(uint32_t speed);
// Check if specified I2C address is responding.
uint8_t checkAddress(uint8_t address);
bool exists(uint8_t address);
inline bool exists(uint8_t address) {
return checkAddress(address)==I2C_STATUS_OK;
}
// Write a complete transmission to I2C from an array in RAM
uint8_t write(uint8_t address, const uint8_t buffer[], uint8_t size);
uint8_t write(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb);
// Write a complete transmission to I2C from an array in Flash
uint8_t write_P(uint8_t address, const uint8_t buffer[], uint8_t size);
uint8_t write_P(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb);
// Write a transmission to I2C from a list of bytes.
uint8_t write(uint8_t address, int nBytes, ...);
uint8_t write(uint8_t address, uint8_t nBytes, ...);
// Write a command from an array in RAM and read response
uint8_t read(uint8_t address, uint8_t writeBuffer[], uint8_t writeSize,
uint8_t readBuffer[], uint8_t readSize);
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[]=NULL, uint8_t writeSize=0);
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb);
// Write a command from an arbitrary list of bytes and read response
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeSize, ...);
// Write a null command and read the response.
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize);
void queueRequest(I2CRB *req);
// Function to abort long-running operations.
void checkForTimeout();
// Loop method
void loop();
// Expand error codes into text. Note that they are in flash so
// need to be printed using FSH.
static const FSH *getErrorMessage(uint8_t status);
private:
bool _beginCompleted = false;
bool _clockSpeedFixed = false;
uint32_t _clockSpeed = 400000L; // 400kHz max on Arduino.
// Finish off request block by waiting for completion and posting status.
uint8_t finishRB(I2CRB *rb, uint8_t status);
void _initialise();
void _setClock(unsigned long);
#if !defined(I2C_USE_WIRE)
// I2CRB structs are queued on the following two links.
// If there are no requests, both are NULL.
// If there is only one request, then queueHead and queueTail both point to it.
// Otherwise, queueHead is the pointer to the first request in the queue and
// queueTail is the pointer to the last request in the queue.
// Within the queue, each request's nextRequest field points to the
// next request, or NULL.
// Mark volatile as they are updated by IRC and read/written elsewhere.
static I2CRB * volatile queueHead;
static I2CRB * volatile queueTail;
static volatile uint8_t state;
static I2CRB * volatile currentRequest;
static volatile uint8_t txCount;
static volatile uint8_t rxCount;
static volatile uint8_t bytesToSend;
static volatile uint8_t bytesToReceive;
static volatile uint8_t operation;
static volatile unsigned long startTime;
static unsigned long timeout; // Transaction timeout in microseconds. 0=disabled.
void startTransaction();
// Low-level hardware manipulation functions.
static void I2C_init();
static void I2C_setClock(unsigned long i2cClockSpeed);
static void I2C_handleInterrupt();
static void I2C_sendStart();
static void I2C_sendStop();
static void I2C_close();
public:
// setTimeout sets the timout value for I2C transactions.
// TODO: Get I2C timeout working before uncommenting the code below.
void setTimeout(unsigned long value) { (void)value; /* timeout = value; */ };
// handleInterrupt needs to be public to be called from the ISR function!
static void handleInterrupt();
#endif
};
extern I2CManagerClass I2CManager;
#endif
#endif

209
I2CManager_AVR.h Normal file
View File

@@ -0,0 +1,209 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CMANAGER_AVR_H
#define I2CMANAGER_AVR_H
#include <Arduino.h>
#include "I2CManager.h"
#include <avr/io.h>
#include <avr/interrupt.h>
/****************************************************************************
TWI State codes
****************************************************************************/
// General TWI Master staus codes
#define TWI_START 0x08 // START has been transmitted
#define TWI_REP_START 0x10 // Repeated START has been transmitted
#define TWI_ARB_LOST 0x38 // Arbitration lost
// TWI Master Transmitter staus codes
#define TWI_MTX_ADR_ACK 0x18 // SLA+W has been tramsmitted and ACK received
#define TWI_MTX_ADR_NACK 0x20 // SLA+W has been tramsmitted and NACK received
#define TWI_MTX_DATA_ACK 0x28 // Data byte has been tramsmitted and ACK received
#define TWI_MTX_DATA_NACK 0x30 // Data byte has been tramsmitted and NACK received
// TWI Master Receiver staus codes
#define TWI_MRX_ADR_ACK 0x40 // SLA+R has been tramsmitted and ACK received
#define TWI_MRX_ADR_NACK 0x48 // SLA+R has been tramsmitted and NACK received
#define TWI_MRX_DATA_ACK 0x50 // Data byte has been received and ACK tramsmitted
#define TWI_MRX_DATA_NACK 0x58 // Data byte has been received and NACK tramsmitted
// TWI Miscellaneous status codes
#define TWI_NO_STATE 0xF8 // No relevant state information available
#define TWI_BUS_ERROR 0x00 // Bus error due to an illegal START or STOP condition
#define TWI_TWBR ((F_CPU / I2C_FREQ) - 16) / 2 // TWI Bit rate Register setting.
#if defined(I2C_USE_INTERRUPTS)
#define ENABLE_TWI_INTERRUPT (1<<TWIE)
#else
#define ENABLE_TWI_INTERRUPT 0
#endif
/***************************************************************************
* Set I2C clock speed register.
***************************************************************************/
void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {
unsigned long temp = ((F_CPU / i2cClockSpeed) - 16) / 2;
for (uint8_t preScaler = 0; preScaler<=3; preScaler++) {
if (temp <= 255) {
TWBR = temp;
TWSR = (TWSR & 0xfc) | preScaler;
return;
} else
temp /= 4;
}
// Set slowest speed ~= 500 bits/sec
TWBR = 255;
TWSR |= 0x03;
}
/***************************************************************************
* Initialise I2C registers.
***************************************************************************/
void I2CManagerClass::I2C_init()
{
TWSR = 0;
TWBR = TWI_TWBR; // Set bit rate register (Baudrate). Defined in header file.
TWDR = 0xFF; // Default content = SDA released.
TWCR = (1<<TWINT); // Clear interrupt flag
pinMode(SDA, INPUT_PULLUP);
pinMode(SCL, INPUT_PULLUP);
}
/***************************************************************************
* Initiate a start bit for transmission.
***************************************************************************/
void I2CManagerClass::I2C_sendStart() {
bytesToSend = currentRequest->writeLen;
bytesToReceive = currentRequest->readLen;
// We may have initiated a stop bit before this without waiting for it.
// Wait for stop bit to be sent before sending start.
while (TWCR & (1<<TWSTO)) {}
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA); // Send Start
}
/***************************************************************************
* Initiate a stop bit for transmission (does not interrupt)
***************************************************************************/
void I2CManagerClass::I2C_sendStop() {
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
}
/***************************************************************************
* Close I2C down
***************************************************************************/
void I2CManagerClass::I2C_close() {
// disable TWI
I2C_sendStop();
while (TWCR & (1<<TWSTO)) {}
TWCR = (1<<TWINT); // clear any interrupt and stop twi.
}
/***************************************************************************
* Main state machine for I2C, called from interrupt handler or,
* if I2C_USE_INTERRUPTS isn't defined, from the I2CManagerClass::loop() function
* (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).
***************************************************************************/
void I2CManagerClass::I2C_handleInterrupt() {
if (!(TWCR & (1<<TWINT))) return; // Nothing to do.
uint8_t twsr = TWSR & 0xF8;
// Cases are ordered so that the most frequently used ones are tested first.
switch (twsr) {
case TWI_MTX_DATA_ACK: // Data byte has been transmitted and ACK received
case TWI_MTX_ADR_ACK: // SLA+W has been transmitted and ACK received
if (bytesToSend) { // Send first.
if (operation == OPERATION_SEND_P)
TWDR = GETFLASH(currentRequest->writeBuffer + (txCount++));
else
TWDR = currentRequest->writeBuffer[txCount++];
bytesToSend--;
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
} else if (bytesToReceive) { // All sent, anything to receive?
while (TWCR & (1<<TWSTO)) {} // Wait for stop to be sent
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA); // Send Start
} else { // Nothing left to send or receive
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
state = I2C_STATUS_OK;
}
break;
case TWI_MRX_DATA_ACK: // Data byte has been received and ACK transmitted
if (bytesToReceive > 0) {
currentRequest->readBuffer[rxCount++] = TWDR;
bytesToReceive--;
}
/* fallthrough */
case TWI_MRX_ADR_ACK: // SLA+R has been sent and ACK received
if (bytesToReceive <= 1) {
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT); // Send NACK after next reception
} else {
// send ack
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
}
break;
case TWI_MRX_DATA_NACK: // Data byte has been received and NACK transmitted
if (bytesToReceive > 0) {
currentRequest->readBuffer[rxCount++] = TWDR;
bytesToReceive--;
}
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
state = I2C_STATUS_OK;
break;
case TWI_START: // START has been transmitted
case TWI_REP_START: // Repeated START has been transmitted
// Set up address and R/W
if (operation == OPERATION_READ || (operation==OPERATION_REQUEST && !bytesToSend))
TWDR = (currentRequest->i2cAddress << 1) | 1; // SLA+R
else
TWDR = (currentRequest->i2cAddress << 1) | 0; // SLA+W
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
break;
case TWI_MTX_ADR_NACK: // SLA+W has been transmitted and NACK received
case TWI_MRX_ADR_NACK: // SLA+R has been transmitted and NACK received
case TWI_MTX_DATA_NACK: // Data byte has been transmitted and NACK received
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
state = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
break;
case TWI_ARB_LOST: // Arbitration lost
// Restart transaction from start.
I2C_sendStart();
break;
case TWI_BUS_ERROR: // Bus error due to an illegal START or STOP condition
default:
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
state = I2C_STATUS_TRANSMIT_ERROR;
}
}
#if defined(I2C_USE_INTERRUPTS)
ISR(TWI_vect) {
I2CManagerClass::handleInterrupt();
}
#endif
#endif /* I2CMANAGER_AVR_H */

160
I2CManager_Mega4809.h Normal file
View File

@@ -0,0 +1,160 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CMANAGER_MEGA4809_H
#define I2CMANAGER_MEGA4809_H
#include <Arduino.h>
#include "I2CManager.h"
/***************************************************************************
* Set I2C clock speed register.
***************************************************************************/
void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {
uint16_t t_rise;
if (i2cClockSpeed < 200000) {
i2cClockSpeed = 100000;
t_rise = 1000;
} else if (i2cClockSpeed < 800000) {
i2cClockSpeed = 400000;
t_rise = 300;
} else if (i2cClockSpeed < 1200000) {
i2cClockSpeed = 1000000;
t_rise = 120;
} else {
i2cClockSpeed = 100000;
t_rise = 1000;
}
uint32_t baud = (F_CPU_CORRECTED / i2cClockSpeed - F_CPU_CORRECTED / 1000 / 1000
* t_rise / 1000 - 10) / 2;
TWI0.MBAUD = (uint8_t)baud;
}
/***************************************************************************
* Initialise I2C registers.
***************************************************************************/
void I2CManagerClass::I2C_init()
{
pinMode(PIN_WIRE_SDA, INPUT_PULLUP);
pinMode(PIN_WIRE_SCL, INPUT_PULLUP);
PORTMUX.TWISPIROUTEA |= TWI_MUX;
#if defined(I2C_USE_INTERRUPTS)
TWI0.MCTRLA = TWI_RIEN_bm | TWI_WIEN_bm | TWI_ENABLE_bm;
#else
TWI0.MCTRLA = TWI_ENABLE_bm;
#endif
I2C_setClock(I2C_FREQ);
TWI0.MSTATUS = TWI_BUSSTATE_IDLE_gc;
}
/***************************************************************************
* Initiate a start bit for transmission, followed by address and R/W
***************************************************************************/
void I2CManagerClass::I2C_sendStart() {
bytesToSend = currentRequest->writeLen;
bytesToReceive = currentRequest->readLen;
// If anything to send, initiate write. Otherwise initiate read.
if (operation == OPERATION_READ || (operation == OPERATION_REQUEST & !bytesToSend))
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 1;
else
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 0;
}
/***************************************************************************
* Initiate a stop bit for transmission.
***************************************************************************/
void I2CManagerClass::I2C_sendStop() {
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
}
/***************************************************************************
* Close I2C down
***************************************************************************/
void I2CManagerClass::I2C_close() {
I2C_sendStop();
}
/***************************************************************************
* Main state machine for I2C, called from interrupt handler.
***************************************************************************/
void I2CManagerClass::I2C_handleInterrupt() {
uint8_t currentStatus = TWI0.MSTATUS;
if (currentStatus & TWI_ARBLOST_bm) {
// Arbitration lost, restart
TWI0.MSTATUS = currentStatus; // clear all flags
I2C_sendStart(); // Reinitiate request
} else if (currentStatus & TWI_BUSERR_bm) {
// Bus error
state = I2C_STATUS_BUS_ERROR;
TWI0.MSTATUS = currentStatus; // clear all flags
} else if (currentStatus & TWI_WIF_bm) {
// Master write completed
if (currentStatus & TWI_RXACK_bm) {
// Nacked, send stop.
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
state = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
} else if (bytesToSend) {
// Acked, so send next byte
if (currentRequest->operation == OPERATION_SEND_P)
TWI0.MDATA = GETFLASH(currentRequest->writeBuffer + (txCount++));
else
TWI0.MDATA = currentRequest->writeBuffer[txCount++];
bytesToSend--;
} else if (bytesToReceive) {
// Last sent byte acked and no more to send. Send repeated start, address and read bit.
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 1;
} else {
// No more data to send/receive. Initiate a STOP condition.
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
state = I2C_STATUS_OK; // Done
}
} else if (currentStatus & TWI_RIF_bm) {
// Master read completed without errors
if (bytesToReceive) {
currentRequest->readBuffer[rxCount++] = TWI0.MDATA; // Store received byte
bytesToReceive--;
} else {
// Buffer full, issue nack/stop
TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc;
state = I2C_STATUS_OK;
}
if (bytesToReceive) {
// More bytes to receive, issue ack and start another read
TWI0.MCTRLB = TWI_MCMD_RECVTRANS_gc;
} else {
// Transaction finished, issue NACK and STOP.
TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc;
state = I2C_STATUS_OK;
}
}
}
/***************************************************************************
* Interrupt handler.
***************************************************************************/
ISR(TWI0_TWIM_vect) {
I2CManagerClass::handleInterrupt();
}
#endif

224
I2CManager_NonBlocking.h Normal file
View File

@@ -0,0 +1,224 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CMANAGER_NONBLOCKING_H
#define I2CMANAGER_NONBLOCKING_H
#include <Arduino.h>
#include "I2CManager.h"
#if defined(I2C_USE_INTERRUPTS)
#include <util/atomic.h>
#else
#define ATOMIC_BLOCK(x)
#define ATOMIC_RESTORESTATE
#endif
// This module is only compiled if I2C_USE_WIRE is not defined, so undefine it here
// to get intellisense to work correctly.
#if defined(I2C_USE_WIRE)
#undef I2C_USE_WIRE
#endif
/***************************************************************************
* Initialise the I2CManagerAsync class.
***************************************************************************/
void I2CManagerClass::_initialise()
{
queueHead = queueTail = NULL;
state = I2C_STATE_FREE;
I2C_init();
}
/***************************************************************************
* Set I2C clock speed. Normally 100000 (Standard) or 400000 (Fast)
* on Arduino. Mega4809 supports 1000000 (Fast+) too.
***************************************************************************/
void I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {
I2C_setClock(i2cClockSpeed);
}
/***************************************************************************
* Helper function to start operations, if the I2C interface is free and
* there is a queued request to be processed.
***************************************************************************/
void I2CManagerClass::startTransaction() {
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
if ((state == I2C_STATE_FREE) && (queueHead != NULL)) {
state = I2C_STATE_ACTIVE;
currentRequest = queueHead;
rxCount = txCount = 0;
// Copy key fields to static data for speed.
operation = currentRequest->operation;
// Start the I2C process going.
I2C_sendStart();
startTime = micros();
}
}
}
/***************************************************************************
* Function to queue a request block and initiate operations.
***************************************************************************/
void I2CManagerClass::queueRequest(I2CRB *req) {
req->status = I2C_STATUS_PENDING;
req->nextRequest = NULL;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
if (!queueTail)
queueHead = queueTail = req; // Only item on queue
else
queueTail = queueTail->nextRequest = req; // Add to end
startTransaction();
}
}
/***************************************************************************
* Initiate a write to an I2C device (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen, I2CRB *req) {
// Make sure previous request has completed.
req->wait();
req->setWriteParams(i2cAddress, writeBuffer, writeLen);
queueRequest(req);
return I2C_STATUS_OK;
}
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write_P(uint8_t i2cAddress, const uint8_t * writeBuffer, uint8_t writeLen, I2CRB *req) {
// Make sure previous request has completed.
req->wait();
req->setWriteParams(i2cAddress, writeBuffer, writeLen);
req->operation = OPERATION_SEND_P;
queueRequest(req);
return I2C_STATUS_OK;
}
/***************************************************************************
* Initiate a read from the I2C device, optionally preceded by a write
* (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen, I2CRB *req)
{
// Make sure previous request has completed.
req->wait();
req->setRequestParams(i2cAddress, readBuffer, readLen, writeBuffer, writeLen);
queueRequest(req);
return I2C_STATUS_OK;
}
/***************************************************************************
* checkForTimeout() function, called from isBusy() and wait() to cancel
* requests that are taking too long to complete.
* This function doesn't fully work as intended so is not currently called.
* Instead we check for an I2C hang-up and report an error from
* I2CRB::wait(), but we aren't able to recover from the hang-up. Such faults
* may be caused by an I2C wire short for example.
***************************************************************************/
void I2CManagerClass::checkForTimeout() {
unsigned long currentMicros = micros();
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
I2CRB *t = queueHead;
if (state==I2C_STATE_ACTIVE && t!=0 && t==currentRequest && timeout > 0) {
// Check for timeout
if (currentMicros - startTime > timeout) {
// Excessive time. Dequeue request
queueHead = t->nextRequest;
if (!queueHead) queueTail = NULL;
currentRequest = NULL;
// Post request as timed out.
t->status = I2C_STATUS_TIMEOUT;
// Reset TWI interface so it is able to continue
// Try close and init, not entirely satisfactory but sort of works...
I2C_close(); // Shutdown and restart twi interface
I2C_init();
state = I2C_STATE_FREE;
// Initiate next queued request if any.
startTransaction();
}
}
}
}
/***************************************************************************
* Loop function, for general background work
***************************************************************************/
void I2CManagerClass::loop() {
#if !defined(I2C_USE_INTERRUPTS)
handleInterrupt();
#endif
// Timeout is now reported in I2CRB::wait(), not here.
// I've left the code, commented out, as a reminder to look at this again
// in the future.
//checkForTimeout();
}
/***************************************************************************
* Interupt handler. Call I2C state machine, and dequeue request
* if completed.
***************************************************************************/
void I2CManagerClass::handleInterrupt() {
// Update hardware state machine
I2C_handleInterrupt();
// Enable interrupts to minimise effect on other interrupt code
interrupts();
// Check if current request has completed. If there's a current request
// and state isn't active then state contains the completion status of the request.
if (state != I2C_STATE_ACTIVE && currentRequest != NULL) {
// Remove completed request from head of queue
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
I2CRB * t = queueHead;
if (t == queueHead) {
queueHead = t->nextRequest;
if (!queueHead) queueTail = queueHead;
t->nBytes = rxCount;
t->status = state;
// I2C state machine is now free for next request
currentRequest = NULL;
state = I2C_STATE_FREE;
// Start next request (if any)
I2CManager.startTransaction();
}
}
}
}
// Fields in I2CManager class specific to Non-blocking implementation.
I2CRB * volatile I2CManagerClass::queueHead = NULL;
I2CRB * volatile I2CManagerClass::queueTail = NULL;
I2CRB * volatile I2CManagerClass::currentRequest = NULL;
volatile uint8_t I2CManagerClass::state = I2C_STATE_FREE;
volatile uint8_t I2CManagerClass::txCount;
volatile uint8_t I2CManagerClass::rxCount;
volatile uint8_t I2CManagerClass::operation;
volatile uint8_t I2CManagerClass::bytesToSend;
volatile uint8_t I2CManagerClass::bytesToReceive;
volatile unsigned long I2CManagerClass::startTime;
unsigned long I2CManagerClass::timeout = 0;
#endif

128
I2CManager_Wire.h Normal file
View File

@@ -0,0 +1,128 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CMANAGER_WIRE_H
#define I2CMANAGER_WIRE_H
#include <Arduino.h>
#include <Wire.h>
#include "I2CManager.h"
// This module is only compiled if I2C_USE_WIRE is defined, so define it here
// to get intellisense to work correctly.
#if !defined(I2C_USE_WIRE)
#define I2C_USE_WIRE
#endif
/***************************************************************************
* Initialise I2C interface software
***************************************************************************/
void I2CManagerClass::_initialise() {
Wire.begin();
}
/***************************************************************************
* Set I2C clock speed. Normally 100000 (Standard) or 400000 (Fast)
* on Arduino. Mega4809 supports 1000000 (Fast+) too.
***************************************************************************/
void I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {
Wire.setClock(i2cClockSpeed);
}
/***************************************************************************
* Initiate a write to an I2C device (blocking operation on Wire)
***************************************************************************/
uint8_t I2CManagerClass::write(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {
Wire.beginTransmission(address);
if (size > 0) Wire.write(buffer, size);
rb->status = Wire.endTransmission();
return I2C_STATUS_OK;
}
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (blocking operation on Wire)
***************************************************************************/
uint8_t I2CManagerClass::write_P(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {
uint8_t ramBuffer[size];
const uint8_t *p1 = buffer;
for (uint8_t i=0; i<size; i++)
ramBuffer[i] = GETFLASH(p1++);
return write(address, ramBuffer, size, rb);
}
/***************************************************************************
* Initiate a write (optional) followed by a read from the I2C device (blocking operation on Wire)
* If fewer than the number of requested bytes are received, status is I2C_STATUS_TRUNCATED.
***************************************************************************/
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb)
{
uint8_t status = I2C_STATUS_OK;
uint8_t nBytes = 0;
if (writeSize > 0) {
Wire.beginTransmission(address);
Wire.write(writeBuffer, writeSize);
status = Wire.endTransmission(false); // Don't free bus yet
}
if (status == I2C_STATUS_OK) {
Wire.requestFrom(address, (size_t)readSize);
while (Wire.available() && nBytes < readSize)
readBuffer[nBytes++] = Wire.read();
if (nBytes < readSize) status = I2C_STATUS_TRUNCATED;
}
rb->nBytes = nBytes;
rb->status = status;
return I2C_STATUS_OK;
}
/***************************************************************************
* Function to queue a request block and initiate operations.
*
* For the Wire version, this executes synchronously, but the status is
* returned in the I2CRB as for the asynchronous version.
***************************************************************************/
void I2CManagerClass::queueRequest(I2CRB *req) {
uint8_t status;
switch (req->operation) {
case OPERATION_READ:
status = read(req->i2cAddress, req->readBuffer, req->readLen, NULL, 0, req);
break;
case OPERATION_SEND:
status = write(req->i2cAddress, req->writeBuffer, req->writeLen, req);
break;
case OPERATION_SEND_P:
status = write_P(req->i2cAddress, req->writeBuffer, req->writeLen, req);
break;
case OPERATION_REQUEST:
status = read(req->i2cAddress, req->readBuffer, req->readLen, req->writeBuffer, req->writeLen, req);
break;
}
req->status = status;
}
/***************************************************************************
* Loop function, for general background work
***************************************************************************/
void I2CManagerClass::loop() {}
// Loop function
void I2CManagerClass::checkForTimeout() {}
#endif

504
IODevice.cpp Normal file
View File

@@ -0,0 +1,504 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "IODevice.h"
#include "DIAG.h"
#include "FSH.h"
#include "IO_MCP23017.h"
#if defined(ARDUINO_ARCH_AVR) || defined(ARDUINO_ARCH_MEGAAVR)
#define USE_FAST_IO
#endif
// Link to halSetup function. If not defined, the function reference will be NULL.
extern __attribute__((weak)) void halSetup();
extern __attribute__((weak)) void mySetup(); // Deprecated function name, output warning if it's declared
//==================================================================================================================
// Static methods
//------------------------------------------------------------------------------------------------------------------
// Static functions
// Static method to initialise the IODevice subsystem.
#if !defined(IO_NO_HAL)
// Create any standard device instances that may be required, such as the Arduino pins
// and PCA9685.
void IODevice::begin() {
// Initialise the IO subsystem
ArduinoPins::create(2, NUM_DIGITAL_PINS-2); // Reserve pins for direct access
// Predefine two PCA9685 modules 0x40-0x41
// Allocates 32 pins 100-131
PCA9685::create(100, 16, 0x40);
PCA9685::create(116, 16, 0x41);
// Predefine two MCP23017 module 0x20/0x21
// Allocates 32 pins 164-195
MCP23017::create(164, 16, 0x20);
MCP23017::create(180, 16, 0x21);
// Call the begin() methods of each configured device in turn
for (IODevice *dev=_firstDevice; dev!=NULL; dev = dev->_nextDevice) {
dev->_begin();
}
_initPhase = false;
// Check for presence of deprecated mySetup() function, and output warning.
if (mySetup)
DIAG(F("WARNING: mySetup() function should be renamed to halSetup()"));
// Call user's halSetup() function (if defined in the build in myHal.cpp).
// The contents will depend on the user's system hardware configuration.
// The myHal.cpp file is a standard C++ module so has access to all of the DCC++EX APIs.
if (halSetup)
halSetup();
}
// Overarching static loop() method for the IODevice subsystem. Works through the
// list of installed devices and calls their individual _loop() method.
// Devices may or may not implement this, but if they do it is useful for things like animations
// or flashing LEDs.
// The current value of micros() is passed as a parameter, so the called loop function
// doesn't need to invoke it.
void IODevice::loop() {
unsigned long currentMicros = micros();
IODevice *lastLoopDevice = _nextLoopDevice; // So we know when to stop...
// Loop through devices until we find one ready to be serviced.
do {
if (!_nextLoopDevice) _nextLoopDevice = _firstDevice;
if (_nextLoopDevice) {
if (_nextLoopDevice->_deviceState != DEVSTATE_FAILED
&& ((long)(currentMicros - _nextLoopDevice->_nextEntryTime)) >= 0) {
// Found one ready to run, so invoke its _loop method.
_nextLoopDevice->_nextEntryTime = currentMicros;
_nextLoopDevice->_loop(currentMicros);
_nextLoopDevice = _nextLoopDevice->_nextDevice;
break;
}
// Not this one, move to next one
_nextLoopDevice = _nextLoopDevice->_nextDevice;
}
} while (_nextLoopDevice != lastLoopDevice); // Stop looking when we've done all.
// Report loop time if diags enabled
#if defined(DIAG_LOOPTIMES)
static unsigned long lastMicros = 0;
// Measure time since loop() method started.
unsigned long halElapsed = micros() - currentMicros;
// Measure time between loop() method entries.
unsigned long elapsed = currentMicros - lastMicros;
static unsigned long maxElapsed = 0, maxHalElapsed = 0;
static unsigned long lastOutputTime = 0;
static unsigned long halTotal = 0, total = 0;
static unsigned long count = 0;
const unsigned long interval = (unsigned long)5 * 1000 * 1000; // 5 seconds in microsec
// Ignore long loop counts while message is still outputting
if (currentMicros - lastOutputTime > 3000UL) {
if (elapsed > maxElapsed) maxElapsed = elapsed;
if (halElapsed > maxHalElapsed) maxHalElapsed = halElapsed;
halTotal += halElapsed;
total += elapsed;
count++;
}
if (currentMicros - lastOutputTime > interval) {
if (lastOutputTime > 0)
DIAG(F("Loop Total:%lus (%lus max) HAL:%lus (%lus max)"),
total/count, maxElapsed, halTotal/count, maxHalElapsed);
maxElapsed = maxHalElapsed = total = halTotal = count = 0;
lastOutputTime = currentMicros;
}
lastMicros = currentMicros;
#endif
}
// Display a list of all the devices on the diagnostic stream.
void IODevice::DumpAll() {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
dev->_display();
}
}
// Determine if the specified vpin is allocated to a device.
bool IODevice::exists(VPIN vpin) {
return findDevice(vpin) != NULL;
}
// check whether the pin supports notification. If so, then regular _read calls are not required.
bool IODevice::hasCallback(VPIN vpin) {
IODevice *dev = findDevice(vpin);
if (!dev) return false;
return dev->_hasCallback;
}
// Display (to diagnostics) details of the device.
void IODevice::_display() {
DIAG(F("Unknown device Vpins:%d-%d %S"),
(int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState==DEVSTATE_FAILED ? F("OFFLINE") : F(""));
}
// Find device associated with nominated Vpin and pass configuration values on to it.
// Return false if not found.
bool IODevice::configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
IODevice *dev = findDevice(vpin);
if (dev) return dev->_configure(vpin, configType, paramCount, params);
#ifdef DIAG_IO
DIAG(F("IODevice::configure(): Vpin ID %d not found!"), (int)vpin);
#endif
return false;
}
// Read value from virtual pin.
int IODevice::read(VPIN vpin) {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
if (dev->owns(vpin))
return dev->_read(vpin);
}
#ifdef DIAG_IO
DIAG(F("IODevice::read(): Vpin %d not found!"), (int)vpin);
#endif
return false;
}
// Read analogue value from virtual pin.
int IODevice::readAnalogue(VPIN vpin) {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
if (dev->owns(vpin))
return dev->_readAnalogue(vpin);
}
#ifdef DIAG_IO
DIAG(F("IODevice::readAnalogue(): Vpin %d not found!"), (int)vpin);
#endif
return false;
}
// Write value to virtual pin(s). If multiple devices are allocated the same pin
// then only the first one found will be used.
void IODevice::write(VPIN vpin, int value) {
IODevice *dev = findDevice(vpin);
if (dev) {
dev->_write(vpin, value);
return;
}
#ifdef DIAG_IO
DIAG(F("IODevice::write(): Vpin ID %d not found!"), (int)vpin);
#endif
}
// Write analogue value to virtual pin(s). If multiple devices are allocated
// the same pin then only the first one found will be used.
//
// The significance of param1 and param2 may vary from device to device.
// For servo controllers, param1 is the profile of the transition and param2
// the duration, i.e. the time that the operation is to be animated over
// in deciseconds (0-3276 sec)
//
void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) {
IODevice *dev = findDevice(vpin);
if (dev) {
dev->_writeAnalogue(vpin, value, param1, param2);
return;
}
#ifdef DIAG_IO
DIAG(F("IODevice::writeAnalogue(): Vpin ID %d not found!"), (int)vpin);
#endif
}
// isBusy, when called for a device pin is always a digital output or analogue output,
// returns input feedback state of the pin, i.e. whether the pin is busy performing
// an animation or fade over a period of time.
bool IODevice::isBusy(VPIN vpin) {
IODevice *dev = findDevice(vpin);
if (dev)
return dev->_read(vpin);
else
return false;
}
void IODevice::setGPIOInterruptPin(int16_t pinNumber) {
if (pinNumber >= 0)
pinMode(pinNumber, INPUT_PULLUP);
_gpioInterruptPin = pinNumber;
}
// Private helper function to add a device to the chain of devices.
void IODevice::addDevice(IODevice *newDevice) {
// Link new object to the end of the chain. Thereby, the first devices to be declared/created
// will be located faster by findDevice than those which are created later.
// Ideally declare/create the digital IO pins first, then servos, then more esoteric devices.
IODevice *lastDevice;
if (_firstDevice == 0)
_firstDevice = newDevice;
else {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice)
lastDevice = dev;
lastDevice->_nextDevice = newDevice;
}
newDevice->_nextDevice = 0;
// If the IODevice::begin() method has already been called, initialise device here. If not,
// the device's _begin() method will be called by IODevice::begin().
if (!_initPhase)
newDevice->_begin();
}
// Private helper function to locate a device by VPIN. Returns NULL if not found.
// This is performance-critical, so minimises the calculation and function calls necessary.
IODevice *IODevice::findDevice(VPIN vpin) {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
VPIN firstVpin = dev->_firstVpin;
if (vpin >= firstVpin && vpin < firstVpin+dev->_nPins)
return dev;
}
return NULL;
}
//==================================================================================================================
// Static data
//------------------------------------------------------------------------------------------------------------------
// Chain of callback blocks (identifying registered callback functions for state changes)
IONotifyCallback *IONotifyCallback::first = 0;
// Start of chain of devices.
IODevice *IODevice::_firstDevice = 0;
// Reference to next device to be called on _loop() method.
IODevice *IODevice::_nextLoopDevice = 0;
// Flag which is reset when IODevice::begin has been called.
bool IODevice::_initPhase = true;
//==================================================================================================================
// Instance members
//------------------------------------------------------------------------------------------------------------------
// Method to check whether the id corresponds to this device
bool IODevice::owns(VPIN id) {
return (id >= _firstVpin && id < _firstVpin + _nPins);
}
#else // !defined(IO_NO_HAL)
// Minimal implementations of public HAL interface, to support Arduino pin I/O and nothing more.
void IODevice::begin() { DIAG(F("NO HAL CONFIGURED!")); }
bool IODevice::configure(VPIN pin, ConfigTypeEnum configType, int nParams, int p[]) {
if (configType!=CONFIGURE_INPUT || nParams!=1 || pin >= NUM_DIGITAL_PINS) return false;
#ifdef DIAG_IO
DIAG(F("Arduino _configurePullup Pin:%d Val:%d"), pin, p[0]);
#endif
pinMode(pin, p[0] ? INPUT_PULLUP : INPUT);
return true;
}
void IODevice::write(VPIN vpin, int value) {
if (vpin >= NUM_DIGITAL_PINS) return;
digitalWrite(vpin, value);
pinMode(vpin, OUTPUT);
}
void IODevice::writeAnalogue(VPIN, int, uint8_t, uint16_t) {}
bool IODevice::isBusy(VPIN) { return false; }
bool IODevice::hasCallback(VPIN) { return false; }
int IODevice::read(VPIN vpin) {
if (vpin >= NUM_DIGITAL_PINS) return 0;
return !digitalRead(vpin); // Return inverted state (5v=0, 0v=1)
}
int IODevice::readAnalogue(VPIN vpin) {
pinMode(vpin, INPUT);
noInterrupts();
int value = analogRead(vpin);
interrupts();
return value;
}
void IODevice::loop() {}
void IODevice::DumpAll() {
DIAG(F("NO HAL CONFIGURED!"));
}
bool IODevice::exists(VPIN vpin) { return (vpin > 2 && vpin < NUM_DIGITAL_PINS); }
void IODevice::setGPIOInterruptPin(int16_t) {}
// Chain of callback blocks (identifying registered callback functions for state changes)
// Not used in IO_NO_HAL but must be declared.
IONotifyCallback *IONotifyCallback::first = 0;
#endif // IO_NO_HAL
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Constructor
ArduinoPins::ArduinoPins(VPIN firstVpin, int nPins) {
_firstVpin = firstVpin;
_nPins = nPins;
int arrayLen = (_nPins+7)/8;
_pinPullups = (uint8_t *)calloc(3, arrayLen);
_pinModes = (&_pinPullups[0]) + arrayLen;
_pinInUse = (&_pinPullups[0]) + 2*arrayLen;
for (int i=0; i<arrayLen; i++) {
_pinPullups[i] = 0xff; // default to pullup on, for inputs
_pinModes[i] = 0;
_pinInUse[i] = 0;
}
}
// Device-specific pin configuration. Configure should be called infrequently so simplify
// code by using the standard pinMode function.
bool ArduinoPins::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
if (configType != CONFIGURE_INPUT) return false;
if (paramCount != 1) return false;
bool pullup = params[0];
int pin = vpin;
#ifdef DIAG_IO
DIAG(F("Arduino _configurePullup Pin:%d Val:%d"), pin, pullup);
#endif
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
uint8_t index = (pin-_firstVpin) / 8;
_pinModes[index] &= ~mask; // set to input mode
if (pullup) {
_pinPullups[index] |= mask;
pinMode(pin, INPUT_PULLUP);
} else {
_pinPullups[index] &= ~mask;
pinMode(pin, INPUT);
}
_pinInUse[index] |= mask;
return true;
}
// Device-specific write function.
void ArduinoPins::_write(VPIN vpin, int value) {
int pin = vpin;
#ifdef DIAG_IO
DIAG(F("Arduino Write Pin:%d Val:%d"), pin, value);
#endif
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
uint8_t index = (pin-_firstVpin) / 8;
// First update the output state, then set into write mode if not already.
fastWriteDigital(pin, value);
if (!(_pinModes[index] & mask)) {
// Currently in read mode, change to write mode
_pinModes[index] |= mask;
// Since mode changes should be infrequent, use standard pinMode function
pinMode(pin, OUTPUT);
_pinInUse[index] |= mask;
}
}
// Device-specific read function (digital input).
int ArduinoPins::_read(VPIN vpin) {
int pin = vpin;
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
uint8_t index = (pin-_firstVpin) / 8;
if ((_pinModes[index] | ~_pinInUse[index]) & mask) {
// Currently in write mode or not initialised, change to read mode
_pinModes[index] &= ~mask;
// Since mode changes should be infrequent, use standard pinMode function
if (_pinPullups[index] & mask)
pinMode(pin, INPUT_PULLUP);
else
pinMode(pin, INPUT);
_pinInUse[index] |= mask;
}
int value = !fastReadDigital(pin); // Invert (5v=0, 0v=1)
#ifdef DIAG_IO
//DIAG(F("Arduino Read Pin:%d Value:%d"), pin, value);
#endif
return value;
}
// Device-specific readAnalogue function (analogue input)
int ArduinoPins::_readAnalogue(VPIN vpin) {
int pin = vpin;
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
uint8_t index = (pin-_firstVpin) / 8;
if (_pinModes[index] & mask) {
// Currently in write mode, change to read mode
_pinModes[index] &= ~mask;
// Since mode changes should be infrequent, use standard pinMode function
if (_pinPullups[index] & mask)
pinMode(pin, INPUT_PULLUP);
else
pinMode(pin, INPUT);
}
// Since AnalogRead is also called from interrupt code, disable interrupts
// while we're using it. There's only one ADC shared by all analogue inputs
// on the Arduino, so we don't want interruptions.
//******************************************************************************
// NOTE: If the HAL is running on a computer without the DCC signal generator,
// then interrupts needn't be disabled. Also, the DCC signal generator puts
// the ADC into fast mode, so if it isn't present, analogueRead calls will be much
// slower!!
//******************************************************************************
noInterrupts();
int value = analogRead(pin);
interrupts();
#ifdef DIAG_IO
DIAG(F("Arduino Read Pin:%d Value:%d"), pin, value);
#endif
return value;
}
void ArduinoPins::_display() {
DIAG(F("Arduino Vpins:%d-%d"), (int)_firstVpin, (int)_firstVpin+_nPins-1);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
void ArduinoPins::fastWriteDigital(uint8_t pin, uint8_t value) {
#if defined(USE_FAST_IO)
if (pin >= NUM_DIGITAL_PINS) return;
uint8_t mask = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *outPortAdr = portOutputRegister(port);
noInterrupts();
if (value)
*outPortAdr |= mask;
else
*outPortAdr &= ~mask;
interrupts();
#else
digitalWrite(pin, value);
#endif
}
bool ArduinoPins::fastReadDigital(uint8_t pin) {
#if defined(USE_FAST_IO)
if (pin >= NUM_DIGITAL_PINS) return false;
uint8_t mask = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *inPortAdr = portInputRegister(port);
// read input
bool result = (*inPortAdr & mask) != 0;
#else
bool result = digitalRead(pin);
#endif
return result;
}

371
IODevice.h Normal file
View File

@@ -0,0 +1,371 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef iodevice_h
#define iodevice_h
// Define symbol DIAG_IO to enable diagnostic output
//#define DIAG_IO Y
// Define symbol DIAG_LOOPTIMES to enable CS loop execution time to be reported
//#define DIAG_LOOPTIMES
// Define symbol IO_NO_HAL to reduce FLASH footprint when HAL features not required
// The HAL is disabled by default on Nano and Uno platforms, because of limited flash space.
#if defined(ARDUINO_AVR_NANO) || defined(ARDUINO_AVR_UNO)
#define IO_NO_HAL
#endif
// Define symbol IO_SWITCH_OFF_SERVO to set the PCA9685 output to 0 when an
// animation has completed. This switches off the servo motor, preventing
// the continuous buzz sometimes found on servos, and reducing the
// power consumption of the servo when inactive.
// It is recommended to enable this, unless it causes you problems.
#define IO_SWITCH_OFF_SERVO
#include "DIAG.h"
#include "FSH.h"
#include "I2CManager.h"
#include "inttypes.h"
typedef uint16_t VPIN;
// Limit VPIN number to max 32767. Above this number, printing often gives negative values.
// This should be enough for 99% of users.
#define VPIN_MAX 32767
#define VPIN_NONE 65535
/*
* Callback support for state change notification from an IODevice subclass to a
* handler, e.g. Sensor object handling.
*/
class IONotifyCallback {
public:
typedef void IONotifyCallbackFunction(VPIN vpin, int value);
static void add(IONotifyCallbackFunction *function) {
IONotifyCallback *blk = new IONotifyCallback(function);
if (first) blk->next = first;
first = blk;
}
static void invokeAll(VPIN vpin, int value) {
for (IONotifyCallback *blk = first; blk != NULL; blk = blk->next)
blk->invoke(vpin, value);
}
static bool hasCallback() {
return first != NULL;
}
private:
IONotifyCallback(IONotifyCallbackFunction *function) { invoke = function; };
IONotifyCallback *next = 0;
IONotifyCallbackFunction *invoke = 0;
static IONotifyCallback *first;
};
/*
* IODevice class
*
* This class is the basis of the Hardware Abstraction Layer (HAL) for
* the DCC++EX Command Station. All device classes derive from this.
*
*/
class IODevice {
public:
// Parameter values to identify type of call to IODevice::configure.
typedef enum : uint8_t {
CONFIGURE_INPUT = 1,
CONFIGURE_SERVO = 2,
CONFIGURE_OUTPUT = 3,
} ConfigTypeEnum;
typedef enum : uint8_t {
DEVSTATE_DORMANT = 0,
DEVSTATE_PROBING = 1,
DEVSTATE_INITIALISING = 2,
DEVSTATE_NORMAL = 3,
DEVSTATE_SCANNING = 4,
DEVSTATE_FAILED = 5,
} DeviceStateEnum;
// Static functions to find the device and invoke its member functions
// begin is invoked to create any standard IODevice subclass instances.
// Also, the _begin method of any existing instances is called from here.
static void begin();
// configure is used invoke an IODevice instance's _configure method
static bool configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]);
// User-friendly function for configuring an input pin.
inline static bool configureInput(VPIN vpin, bool pullupEnable) {
int params[] = {pullupEnable};
return IODevice::configure(vpin, CONFIGURE_INPUT, 1, params);
}
// User-friendly function for configuring a servo pin.
inline static bool configureServo(VPIN vpin, uint16_t activePosition, uint16_t inactivePosition, uint8_t profile=0, uint16_t duration=0, uint8_t initialState=0) {
int params[] = {(int)activePosition, (int)inactivePosition, profile, (int)duration, initialState};
return IODevice::configure(vpin, CONFIGURE_SERVO, 5, params);
}
// write invokes the IODevice instance's _write method.
static void write(VPIN vpin, int value);
// write invokes the IODevice instance's _writeAnalogue method (not applicable for digital outputs)
static void writeAnalogue(VPIN vpin, int value, uint8_t profile=0, uint16_t duration=0);
// isBusy returns true if the device is currently in an animation of some sort, e.g. is changing
// the output over a period of time.
static bool isBusy(VPIN vpin);
// check whether the pin supports notification. If so, then regular _read calls are not required.
static bool hasCallback(VPIN vpin);
// read invokes the IODevice instance's _read method.
static int read(VPIN vpin);
// read invokes the IODevice instance's _readAnalogue method.
static int readAnalogue(VPIN vpin);
// loop invokes the IODevice instance's _loop method.
static void loop();
static void DumpAll();
// exists checks whether there is a device owning the specified vpin
static bool exists(VPIN vpin);
// Enable shared interrupt on specified pin for GPIO extender modules. The extender module
// should pull down this pin when requesting a scan. The pin may be shared by multiple modules.
// Without the shared interrupt, input states are scanned periodically to detect changes on
// GPIO extender pins. If a shared interrupt pin is configured, then input states are scanned
// only when the shared interrupt pin is pulled low. The external GPIO module releases the pin
// once the GPIO port concerned has been read.
void setGPIOInterruptPin(int16_t pinNumber);
protected:
// Constructor
IODevice(VPIN firstVpin=0, int nPins=0) {
_firstVpin = firstVpin;
_nPins = nPins;
_nextEntryTime = 0;
}
// Method to perform initialisation of the device (optionally implemented within device class)
virtual void _begin() {}
// Method to configure device (optionally implemented within device class)
virtual bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
(void)vpin; (void)configType; (void)paramCount; (void)params; // Suppress compiler warning.
return false;
};
// Method to write new state (optionally implemented within device class)
virtual void _write(VPIN vpin, int value) {
(void)vpin; (void)value;
};
// Method to write an 'analogue' value (optionally implemented within device class)
virtual void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) {
(void)vpin; (void)value; (void) param1; (void)param2;
};
// Method to read digital pin state (optionally implemented within device class)
virtual int _read(VPIN vpin) {
(void)vpin;
return 0;
};
// Method to read analogue pin state (optionally implemented within device class)
virtual int _readAnalogue(VPIN vpin) {
(void)vpin;
return 0;
};
// Method to perform updates on an ongoing basis (optionally implemented within device class)
virtual void _loop(unsigned long currentMicros) {
delayUntil(currentMicros + 0x7fffffff); // Largest time in the future! Effectively disable _loop calls.
};
// Method for displaying info on DIAG output (optionally implemented within device class)
virtual void _display();
// Destructor
virtual ~IODevice() {};
// Non-virtual function
void delayUntil(unsigned long futureMicrosCount) {
_nextEntryTime = futureMicrosCount;
}
// Common object fields.
VPIN _firstVpin;
int _nPins;
// Flag whether the device supports callbacks.
bool _hasCallback = false;
// Pin number of interrupt pin for GPIO extender devices. The extender module will pull this
// pin low if an input changes state.
int16_t _gpioInterruptPin = -1;
// Static support function for subclass creation
static void addDevice(IODevice *newDevice);
// Current state of device
DeviceStateEnum _deviceState = DEVSTATE_DORMANT;
private:
// Method to check whether the vpin corresponds to this device
bool owns(VPIN vpin);
// Method to find device handling Vpin
static IODevice *findDevice(VPIN vpin);
IODevice *_nextDevice = 0;
unsigned long _nextEntryTime;
static IODevice *_firstDevice;
static IODevice *_nextLoopDevice;
static bool _initPhase;
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for PCA9685 16-channel PWM module.
*/
class PCA9685 : public IODevice {
public:
static void create(VPIN vpin, int nPins, uint8_t I2CAddress);
// Constructor
PCA9685(VPIN vpin, int nPins, uint8_t I2CAddress);
enum ProfileType : uint8_t {
Instant = 0, // Moves immediately between positions (if duration not specified)
UseDuration = 0, // Use specified duration
Fast = 1, // Takes around 500ms end-to-end
Medium = 2, // 1 second end-to-end
Slow = 3, // 2 seconds end-to-end
Bounce = 4, // For semaphores/turnouts with a bit of bounce!!
NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move.
};
private:
// Device-specific initialisation
void _begin() override;
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
// Device-specific write functions.
void _write(VPIN vpin, int value) override;
void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override;
int _read(VPIN vpin) override; // returns the digital state or busy status of the device
void _loop(unsigned long currentMicros) override;
void updatePosition(uint8_t pin);
void writeDevice(uint8_t pin, int value);
void _display() override;
uint8_t _I2CAddress; // 0x40-0x43 possible
struct ServoData {
uint16_t activePosition : 12; // Config parameter
uint16_t inactivePosition : 12; // Config parameter
uint16_t currentPosition : 12;
uint16_t fromPosition : 12;
uint16_t toPosition : 12;
uint8_t profile; // Config parameter
uint16_t stepNumber; // Index of current step (starting from 0)
uint16_t numSteps; // Number of steps in animation, or 0 if none in progress.
uint8_t currentProfile; // profile being used for current animation.
uint16_t duration; // time (tenths of a second) for animation to complete.
}; // 14 bytes per element, i.e. per pin in use
struct ServoData *_servoData [16];
static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off
static const byte FLASH _bounceProfile[30];
const unsigned int refreshInterval = 50; // refresh every 50ms
// structures for setting up non-blocking writes to servo controller
I2CRB requestBlock;
uint8_t outputBuffer[5];
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for DCC accessory decoder.
*/
class DCCAccessoryDecoder: public IODevice {
public:
static void create(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress);
// Constructor
DCCAccessoryDecoder(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress);
private:
// Device-specific write function.
void _begin() override;
void _write(VPIN vpin, int value) override;
void _display() override;
int _packedAddress;
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for arduino input/output pins.
*/
class ArduinoPins: public IODevice {
public:
static void create(VPIN firstVpin, int nPins) {
addDevice(new ArduinoPins(firstVpin, nPins));
}
// Constructor
ArduinoPins(VPIN firstVpin, int nPins);
static void fastWriteDigital(uint8_t pin, uint8_t value);
static bool fastReadDigital(uint8_t pin);
private:
// Device-specific pin configuration
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
// Device-specific write function.
void _write(VPIN vpin, int value) override;
// Device-specific read functions.
int _read(VPIN vpin) override;
int _readAnalogue(VPIN vpin) override;
void _display() override;
uint8_t *_pinPullups;
uint8_t *_pinModes; // each bit is 1 for output, 0 for input
uint8_t *_pinInUse;
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
#include "IO_MCP23008.h"
#include "IO_MCP23017.h"
#include "IO_PCF8574.h"
#endif // iodevice_h

165
IO_AnalogueInputs.h Normal file
View File

@@ -0,0 +1,165 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef io_analogueinputs_h
#define io_analogueinputs_h
// Uncomment following line to slow the scan cycle down to 1second ADC samples, with
// diagnostic output of scanned values.
//#define IO_ANALOGUE_SLOW
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "FSH.h"
/**********************************************************************************************
* ADS111x class for I2C-connected analogue input modules ADS1113, ADS1114 and ADS1115.
*
* ADS1113 and ADS1114 are restricted to 1 input. ADS1115 has a multiplexer which allows
* any of four input pins to be read by its ADC.
*
* The driver polls the device in accordance with the constant 'scanInterval' below. On first loop
* entry, the multiplexer is set to pin A0 and the ADC is triggered. On second and subsequent
* entries, the analogue value is read from the conversion register and then the multiplexer and
* ADC are set up to read the next pin.
*
* The ADS111x is set up as follows:
* Single-shot scan
* Data rate 128 samples/sec (7.8ms/sample, but scanned every 10ms)
* Comparator off
* Gain FSR=6.144V
* The gain means that the maximum input voltage of 5V (when Vss=5V) gives a reading
* of 32767*(5.0/6.144) = 26666.
*
* A device is configured by the following:
* ADS111x::create(firstVpin, nPins, i2cAddress);
* for example
* ADS111x::create(300, 1, 0x48); // single-input ADS1113
* ADS111x::create(300, 4, 0x48); // four-input ADS1115
*
* Note: The device is simple and does not need initial configuration, so it should recover from
* temporary loss of communications or power.
**********************************************************************************************/
class ADS111x: public IODevice {
public:
ADS111x(VPIN firstVpin, int nPins, uint8_t i2cAddress) {
_firstVpin = firstVpin;
_nPins = min(nPins,4);
_i2cAddress = i2cAddress;
_currentPin = 0;
for (int8_t i=0; i<_nPins; i++)
_value[i] = -1;
addDevice(this);
}
static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress) {
new ADS111x(firstVpin, nPins, i2cAddress);
}
private:
void _begin() {
// Initialise ADS device
if (I2CManager.exists(_i2cAddress)) {
_nextState = STATE_STARTSCAN;
#ifdef DIAG_IO
_display();
#endif
} else {
DIAG(F("ADS111x device not found, I2C:%x"), _i2cAddress);
_deviceState = DEVSTATE_FAILED;
}
}
void _loop(unsigned long currentMicros) override {
// Check that previous non-blocking write has completed, if not then wait
uint8_t status = _i2crb.status;
if (status == I2C_STATUS_PENDING) return; // Busy, so don't do anything.
if (status == I2C_STATUS_OK) {
switch (_nextState) {
case STATE_STARTSCAN:
// Configure ADC and multiplexer for next scan. See ADS111x datasheet for details
// of configuration register settings.
_outBuffer[0] = 0x01; // Config register address
_outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n
_outBuffer[2] = 0xA3; // 250 samples/sec, comparator off
// Write command, without waiting for completion.
I2CManager.write(_i2cAddress, _outBuffer, 3, &_i2crb);
delayUntil(currentMicros + scanInterval);
_nextState = STATE_STARTREAD;
break;
case STATE_STARTREAD:
// Reading the pin value
_outBuffer[0] = 0x00; // Conversion register address
I2CManager.read(_i2cAddress, _inBuffer, 2, _outBuffer, 1, &_i2crb); // Read register
_nextState = STATE_GETVALUE;
break;
case STATE_GETVALUE:
_value[_currentPin] = ((uint16_t)_inBuffer[0] << 8) + (uint16_t)_inBuffer[1];
#ifdef IO_ANALOGUE_SLOW
DIAG(F("ADS111x pin:%d value:%d"), _currentPin, _value[_currentPin]);
#endif
// Move to next pin
if (++_currentPin >= _nPins) _currentPin = 0;
_nextState = STATE_STARTSCAN;
break;
default:
break;
}
} else { // error status
DIAG(F("ADS111x I2C:x%d Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status));
_deviceState = DEVSTATE_FAILED;
}
}
int _readAnalogue(VPIN vpin) override {
int pin = vpin - _firstVpin;
return _value[pin];
}
void _display() override {
DIAG(F("ADS111x I2C:x%x Configured on Vpins:%d-%d %S"), _i2cAddress, _firstVpin, _firstVpin+_nPins-1,
_deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F(""));
}
// ADC conversion rate is 250SPS, or 4ms per conversion. Set the period between updates to 10ms.
// This is enough to allow the conversion to reliably complete in time.
#ifndef IO_ANALOGUE_SLOW
const unsigned long scanInterval = 10000UL; // Period between successive ADC scans in microseconds.
#else
const unsigned long scanInterval = 1000000UL; // Period between successive ADC scans in microseconds.
#endif
enum : uint8_t {
STATE_STARTSCAN,
STATE_STARTREAD,
STATE_GETVALUE,
};
uint16_t _value[4];
uint8_t _i2cAddress;
uint8_t _outBuffer[3];
uint8_t _inBuffer[2];
uint8_t _currentPin; // ADC pin currently being scanned
I2CRB _i2crb;
uint8_t _nextState;
};
#endif // io_analogueinputs_h

64
IO_DCCAccessory.cpp Normal file
View File

@@ -0,0 +1,64 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "DCC.h"
#include "IODevice.h"
#include "DIAG.h"
#include "defines.h"
#define PACKEDADDRESS(addr, subaddr) (((addr) << 2) + (subaddr))
#define ADDRESS(packedaddr) ((packedaddr) >> 2)
#define SUBADDRESS(packedaddr) ((packedaddr) % 4)
void DCCAccessoryDecoder::create(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) {
new DCCAccessoryDecoder(vpin, nPins, DCCAddress, DCCSubaddress);
}
// Constructors
DCCAccessoryDecoder::DCCAccessoryDecoder(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) {
_firstVpin = vpin;
_nPins = nPins;
_packedAddress = PACKEDADDRESS(DCCAddress, DCCSubaddress);
addDevice(this);
}
void DCCAccessoryDecoder::_begin() {
#if defined(DIAG_IO)
_display();
#endif
}
// Device-specific write function. State 1=closed, 0=thrown. Adjust for RCN-213 compliance
void DCCAccessoryDecoder::_write(VPIN id, int state) {
int packedAddress = _packedAddress + id - _firstVpin;
#ifdef DIAG_IO
DIAG(F("DCC Write Linear Address:%d State:%d"), packedAddress, state);
#endif
#if !defined(DCC_ACCESSORY_RCN_213)
state = !state;
#endif
DCC::setAccessory(ADDRESS(packedAddress), SUBADDRESS(packedAddress), state);
}
void DCCAccessoryDecoder::_display() {
int endAddress = _packedAddress + _nPins - 1;
DIAG(F("DCCAccessoryDecoder Configured on Vpins:%d-%d Addresses %d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1,
ADDRESS(_packedAddress), SUBADDRESS(_packedAddress), ADDRESS(endAddress), SUBADDRESS(endAddress));
}

255
IO_DFPlayer.h Normal file
View File

@@ -0,0 +1,255 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* DFPlayer is an MP3 player module with an SD card holder. It also has an integrated
* amplifier, so it only needs a power supply and a speaker.
*
* This driver allows the device to be controlled through IODevice::write() and
* IODevice::writeAnalogue() calls.
*
* The driver is configured as follows:
*
* DFPlayer::create(firstVpin, nPins, Serialn);
*
* Where firstVpin is the first vpin reserved for reading the device,
* nPins is the number of pins to be allocated (max 5)
* and Serialn is the name of the Serial port connected to the DFPlayer (e.g. Serial1).
*
* Example:
* In mySetup function within mySetup.cpp:
* DFPlayer::create(3500, 5, Serial1);
*
* Writing an analogue value 0-2999 to the first pin will select a numbered file from the SD card;
* Writing an analogue value 0-30 to the second pin will set the volume of the output;
* Writing a digital value to the first pin will play or stop the file;
* Reading a digital value from any pin will return true(1) if the player is playing, false(0) otherwise.
*
* From EX-RAIL, the following commands may be used:
* SET(3500) -- starts playing the first file on the SD card
* SET(3501) -- starts playing the second file on the SD card
* etc.
* RESET(3500) -- stops all playing on the player
* WAITFOR(3500) -- wait for the file currently being played by the player to complete
* SERVO(3500,23,0) -- plays file 23 at current volume
* SERVO(3500,23,30) -- plays file 23 at volume 30 (maximum)
* SERVO(3501,20,0) -- Sets the volume to 20
*
* NB The DFPlayer's serial lines are not 5V safe, so connecting the Arduino TX directly
* to the DFPlayer's RX terminal will cause lots of noise over the speaker, or worse.
* A 1k resistor in series with the module's RX terminal will alleviate this.
*/
#ifndef IO_DFPlayer_h
#define IO_DFPlayer_h
#include "IODevice.h"
class DFPlayer : public IODevice {
private:
HardwareSerial *_serial;
bool _playing = false;
uint8_t _inputIndex = 0;
unsigned long _commandSendTime; // Allows timeout processing
public:
// Constructor
DFPlayer(VPIN firstVpin, int nPins, HardwareSerial &serial) :
IODevice(firstVpin, nPins),
_serial(&serial)
{
addDevice(this);
}
static void create(VPIN firstVpin, int nPins, HardwareSerial &serial) {
new DFPlayer(firstVpin, nPins, serial);
}
protected:
void _begin() override {
_serial->begin(9600);
_deviceState = DEVSTATE_INITIALISING;
// Send a query to the device to see if it responds
sendPacket(0x42);
_commandSendTime = micros();
}
void _loop(unsigned long currentMicros) override {
// Check for incoming data on _serial, and update busy flag accordingly.
// Expected message is in the form "7F FF 06 3D xx xx xx xx xx EF"
while (_serial->available()) {
int c = _serial->read();
if (c == 0x7E)
_inputIndex = 1;
else if ((c==0xFF && _inputIndex==1)
|| (c==0x3D && _inputIndex==3)
|| (_inputIndex >=4 && _inputIndex <= 8))
_inputIndex++;
else if (c==0x06 && _inputIndex==2) {
// Valid message prefix, so consider the device online
if (_deviceState==DEVSTATE_INITIALISING) {
_deviceState = DEVSTATE_NORMAL;
#ifdef DIAG_IO
_display();
#endif
}
_inputIndex++;
} else if (c==0xEF && _inputIndex==9) {
// End of play
if (_playing) {
#ifdef DIAG_IO
DIAG(F("DFPlayer: Finished"));
#endif
_playing = false;
}
_inputIndex = 0;
} else
_inputIndex = 0; // Unrecognised character sequence, start again!
}
// Check if the initial prompt to device has timed out. Allow 1 second
if (_deviceState == DEVSTATE_INITIALISING && currentMicros - _commandSendTime > 1000000UL) {
DIAG(F("DFPlayer device not responding on serial port"));
_deviceState = DEVSTATE_FAILED;
}
delayUntil(currentMicros + 10000); // Only enter every 10ms
}
// Write with value 1 starts playing a song. The relative pin number is the file number.
// Write with value 0 stops playing.
void _write(VPIN vpin, int value) override {
int pin = vpin - _firstVpin;
if (value) {
// Value 1, start playing
#ifdef DIAG_IO
DIAG(F("DFPlayer: Play %d"), pin+1);
#endif
sendPacket(0x03, pin+1);
_playing = true;
} else {
// Value 0, stop playing
#ifdef DIAG_IO
DIAG(F("DFPlayer: Stop"));
#endif
sendPacket(0x16);
_playing = false;
}
}
// WriteAnalogue on first pin uses the nominated value as a file number to start playing, if file number > 0.
// Volume may be specified as second parameter to writeAnalogue.
// If value is zero, the player stops playing.
// WriteAnalogue on second pin sets the output volume.
void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t=0) override {
uint8_t pin = vpin - _firstVpin;
// Validate parameter.
volume = min(30,volume);
if (pin == 0) {
// Play track
if (value > 0) {
#ifdef DIAG_IO
DIAG(F("DFPlayer: Play %d"), value);
#endif
sendPacket(0x03, value); // Play track
_playing = true;
if (volume > 0) {
#ifdef DIAG_IO
DIAG(F("DFPlayer: Volume %d"), volume);
#endif
sendPacket(0x06, volume); // Set volume
}
} else {
#ifdef DIAG_IO
DIAG(F("DFPlayer: Stop"));
#endif
sendPacket(0x16); // Stop play
_playing = false;
}
} else if (pin == 1) {
// Set volume (0-30)
if (value > 30) value = 30;
else if (value < 0) value = 0;
#ifdef DIAG_IO
DIAG(F("DFPlayer: Volume %d"), value);
#endif
sendPacket(0x06, value);
}
}
// A read on any pin indicates whether the player is still playing.
int _read(VPIN) override {
return _playing;
}
void _display() override {
DIAG(F("DFPlayer Configured on Vpins:%d-%d %S"), _firstVpin, _firstVpin+_nPins-1,
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
private:
// 7E FF 06 0F 00 01 01 xx xx EF
// 0 -> 7E is start code
// 1 -> FF is version
// 2 -> 06 is length
// 3 -> 0F is command
// 4 -> 00 is no receive
// 5~6 -> 01 01 is argument
// 7~8 -> checksum = 0 - ( FF+06+0F+00+01+01 )
// 9 -> EF is end code
void sendPacket(uint8_t command, uint16_t arg = 0)
{
uint8_t out[] = { 0x7E,
0xFF,
06,
command,
00,
static_cast<uint8_t>(arg >> 8),
static_cast<uint8_t>(arg & 0x00ff),
00,
00,
0xEF };
setChecksum(out);
_serial->write(out, sizeof(out));
}
uint16_t calcChecksum(uint8_t* packet)
{
uint16_t sum = 0;
for (int i = 1; i < 7; i++)
{
sum += packet[i];
}
return -sum;
}
void setChecksum(uint8_t* out)
{
uint16_t sum = calcChecksum(out);
out[7] = (sum >> 8);
out[8] = (sum & 0xff);
}
};
#endif // IO_DFPlayer_h

129
IO_ExampleSerial.cpp Normal file
View File

@@ -0,0 +1,129 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "IO_ExampleSerial.h"
#include "FSH.h"
// Constructor
IO_ExampleSerial::IO_ExampleSerial(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) {
_firstVpin = firstVpin;
_nPins = nPins;
_pinValues = (uint16_t *)calloc(_nPins, sizeof(uint16_t));
_baud = baud;
// Save reference to serial port driver
_serial = serial;
addDevice(this);
}
// Static create method for one module.
void IO_ExampleSerial::create(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) {
new IO_ExampleSerial(firstVpin, nPins, serial, baud);
}
// Device-specific initialisation
void IO_ExampleSerial::_begin() {
_serial->begin(_baud);
#if defined(DIAG_IO)
_display();
#endif
// Send a few # characters to the output
for (uint8_t i=0; i<3; i++)
_serial->write('#');
}
// Device-specific write function. Write a string in the form "#Wm,n#"
// where m is the vpin number, and n is the value.
void IO_ExampleSerial::_write(VPIN vpin, int value) {
int pin = vpin -_firstVpin;
#ifdef DIAG_IO
DIAG(F("IO_ExampleSerial::_write Pin:%d Value:%d"), (int)vpin, value);
#endif
// Send a command string over the serial line
_serial->print('#');
_serial->print('W');
_serial->print(pin);
_serial->print(',');
_serial->print(value);
_serial->println('#');
DIAG(F("ExampleSerial Sent command, p1=%d, p2=%d"), vpin, value);
}
// Device-specific read function.
int IO_ExampleSerial::_read(VPIN vpin) {
// Return a value for the specified vpin.
int result = _pinValues[vpin-_firstVpin];
return result;
}
// Loop function to do background scanning of the input port. State
// machine parses the incoming command as it is received. Command
// is in the form "#Nm,n#" where m is the index and n is the value.
void IO_ExampleSerial::_loop(unsigned long currentMicros) {
(void)currentMicros; // Suppress compiler warnings
if (_serial->available()) {
// Input data available to read. Read a character.
char c = _serial->read();
switch (_inputState) {
case 0: // Waiting for start of command
if (c == '#') // Start of command received.
_inputState = 1;
break;
case 1: // Expecting command character
if (c == 'N') { // 'Notify' character received
_inputState = 2;
_inputValue = _inputIndex = 0;
} else
_inputState = 0; // Unexpected char, reset
break;
case 2: // reading first parameter (index)
if (isdigit(c))
_inputIndex = _inputIndex * 10 + (c-'0');
else if (c==',')
_inputState = 3;
else
_inputState = 0; // Unexpected char, reset
break;
case 3: // reading reading second parameter (value)
if (isdigit(c))
_inputValue = _inputValue * 10 - (c-'0');
else if (c=='#') { // End of command
// Complete command received, do something with it.
DIAG(F("ExampleSerial Received command, p1=%d, p2=%d"), _inputIndex, _inputValue);
if (_inputIndex < _nPins) { // Store value
_pinValues[_inputIndex] = _inputValue;
}
_inputState = 0; // Done, start again.
} else
_inputState = 0; // Unexpected char, reset
break;
}
}
}
void IO_ExampleSerial::_display() {
DIAG(F("IO_ExampleSerial Configured on VPins:%d-%d"), (int)_firstVpin,
(int)_firstVpin+_nPins-1);
}

58
IO_ExampleSerial.h Normal file
View File

@@ -0,0 +1,58 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* To declare a device instance,
* IO_ExampleSerial myDevice(1000, 10, Serial3, 9600);
* or to create programmatically,
* IO_ExampleSerial::create(1000, 10, Serial3, 9600);
*
* (uses VPINs 1000-1009, talke on Serial 3 at 9600 baud.)
*
* See IO_ExampleSerial.cpp for the protocol used over the serial line.
*
*/
#ifndef IO_EXAMPLESERIAL_H
#define IO_EXAMPLESERIAL_H
#include "IODevice.h"
class IO_ExampleSerial : public IODevice {
public:
IO_ExampleSerial(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud);
static void create(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud);
protected:
void _begin() override;
void _loop(unsigned long currentMicros) override;
void _write(VPIN vpin, int value) override;
int _read(VPIN vpin) override;
void _display() override;
private:
HardwareSerial *_serial;
uint8_t _inputState = 0;
int _inputIndex = 0;
int _inputValue = 0;
uint16_t *_pinValues; // Pointer to block of memory containing pin values
unsigned long _baud;
};
#endif // IO_EXAMPLESERIAL_H

249
IO_GPIOBase.h Normal file
View File

@@ -0,0 +1,249 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef IO_GPIOBASE_H
#define IO_GPIOBASE_H
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
// GPIOBase is defined as a class template. This allows it to be instantiated by
// subclasses with different types, according to the number of pins on the GPIO module.
// For example, GPIOBase<uint8_t> for 8 pins, GPIOBase<uint16_t> for 16 pins etc.
// A module with up to 64 pins can be handled in this way (uint64_t).
template <class T>
class GPIOBase : public IODevice {
protected:
// Constructor
GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin);
// Device-specific initialisation
void _begin() override;
// Device-specific pin configuration function.
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
// Pin write function.
void _write(VPIN vpin, int value) override;
// Pin read function.
int _read(VPIN vpin) override;
void _display() override;
void _loop(unsigned long currentMicros) override;
// Data fields
uint8_t _I2CAddress;
// Allocate enough space for all input pins
T _portInputState;
T _portOutputState;
T _portMode;
T _portPullup;
T _portInUse;
// Interval between refreshes of each input port
static const int _portTickTime = 4000;
// Virtual functions for interfacing with I2C GPIO Device
virtual void _writeGpioPort() = 0;
virtual void _readGpioPort(bool immediate=true) = 0;
virtual void _writePullups() {};
virtual void _writePortModes() {};
virtual void _setupDevice() {};
virtual void _processCompletion(uint8_t status) {
(void)status; // Suppress compiler warning
};
I2CRB requestBlock;
FSH *_deviceName;
};
// Because class GPIOBase is a template, the implementation (below) must be contained within the same
// file as the class declaration (above). Otherwise it won't compile!
// Constructor
template <class T>
GPIOBase<T>::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin) :
IODevice(firstVpin, nPins)
{
_deviceName = deviceName;
_I2CAddress = I2CAddress;
_gpioInterruptPin = interruptPin;
_hasCallback = true;
// Add device to list of devices.
addDevice(this);
}
template <class T>
void GPIOBase<T>::_begin() {
// Configure pin used for GPIO extender notification of change (if allocated)
if (_gpioInterruptPin >= 0)
pinMode(_gpioInterruptPin, INPUT_PULLUP);
I2CManager.begin();
I2CManager.setClock(400000);
if (I2CManager.exists(_I2CAddress)) {
#if defined(DIAG_IO)
_display();
#endif
_portMode = 0; // default to input mode
_portPullup = -1; // default to pullup enabled
_portInputState = -1;
_portInUse = 0;
_setupDevice();
_deviceState = DEVSTATE_NORMAL;
} else {
DIAG(F("%S I2C:x%x Device not detected"), _deviceName, _I2CAddress);
_deviceState = DEVSTATE_FAILED;
}
}
// Configuration parameters for inputs:
// params[0]: enable pullup
// params[1]: invert input (optional)
template <class T>
bool GPIOBase<T>::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
if (configType != CONFIGURE_INPUT) return false;
if (paramCount == 0 || paramCount > 1) return false;
bool pullup = params[0];
int pin = vpin - _firstVpin;
#ifdef DIAG_IO
DIAG(F("%S I2C:x%x Config Pin:%d Val:%d"), _deviceName, _I2CAddress, pin, pullup);
#endif
uint16_t mask = 1 << pin;
if (pullup)
_portPullup |= mask;
else
_portPullup &= ~mask;
// Mark that port has been accessed
_portInUse |= mask;
// Set input mode
_portMode &= ~mask;
// Call subclass's virtual function to write to device
_writePortModes();
_writePullups();
// Port change will be notified on next loop entry.
return true;
}
// Periodically read the input port
template <class T>
void GPIOBase<T>::_loop(unsigned long currentMicros) {
T lastPortStates = _portInputState;
if (_deviceState == DEVSTATE_SCANNING && !requestBlock.isBusy()) {
uint8_t status = requestBlock.status;
if (status == I2C_STATUS_OK) {
_deviceState = DEVSTATE_NORMAL;
} else {
_deviceState = DEVSTATE_FAILED;
DIAG(F("%S I2C:x%x Error:%d %S"), _deviceName, _I2CAddress, status,
I2CManager.getErrorMessage(status));
}
_processCompletion(status);
// Set unused pin and write mode pin value to 1
_portInputState |= ~_portInUse | _portMode;
// Scan for changes in input states and invoke callback (if present)
T differences = lastPortStates ^ _portInputState;
if (differences && IONotifyCallback::hasCallback()) {
// Scan for differences bit by bit
T mask = 1;
for (int pin=0; pin<_nPins; pin++) {
if (differences & mask) {
// Change detected.
IONotifyCallback::invokeAll(_firstVpin+pin, (_portInputState & mask) == 0);
}
mask <<= 1;
}
}
#ifdef DIAG_IO
if (differences)
DIAG(F("%S I2C:x%x PortStates:%x"), _deviceName, _I2CAddress, _portInputState);
#endif
}
// Check if interrupt configured. If not, or if it is active (pulled down), then
// initiate a scan.
if (_gpioInterruptPin < 0 || !digitalRead(_gpioInterruptPin)) {
// TODO: Could suppress reads if there are no pins configured as inputs!
// Read input
if (_deviceState == DEVSTATE_NORMAL) {
_readGpioPort(false); // Initiate non-blocking read
_deviceState= DEVSTATE_SCANNING;
}
}
// Delay next entry until tick elapsed.
delayUntil(currentMicros + _portTickTime);
}
template <class T>
void GPIOBase<T>::_display() {
DIAG(F("%S I2C:x%x Configured on Vpins:%d-%d %S"), _deviceName, _I2CAddress,
_firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
template <class T>
void GPIOBase<T>::_write(VPIN vpin, int value) {
int pin = vpin - _firstVpin;
T mask = 1 << pin;
#ifdef DIAG_IO
DIAG(F("%S I2C:x%x Write Pin:%d Val:%d"), _deviceName, _I2CAddress, pin, value);
#endif
// Set port mode output if currently not output mode
if (!(_portMode & mask)) {
_portInUse |= mask;
_portMode |= mask;
_writePortModes();
}
// Update port output state
if (value)
_portOutputState |= mask;
else
_portOutputState &= ~mask;
// Call subclass's virtual function to write to device.
return _writeGpioPort();
}
template <class T>
int GPIOBase<T>::_read(VPIN vpin) {
int pin = vpin - _firstVpin;
T mask = 1 << pin;
// Set port mode to input if currently output or first use
if ((_portMode | ~_portInUse) & mask) {
_portMode &= ~mask;
_portInUse |= mask;
_writePullups();
_writePortModes();
// Port won't have been read yet, so read it now.
_readGpioPort();
// Set unused pin and write mode pin value to 1
_portInputState |= ~_portInUse | _portMode;
#ifdef DIAG_IO
DIAG(F("%S I2C:x%x PortStates:%x"), _deviceName, _I2CAddress, _portInputState);
#endif
}
return (_portInputState & mask) ? 0 : 1; // Invert state (5v=0, 0v=1)
}
#endif

188
IO_HCSR04.h Normal file
View File

@@ -0,0 +1,188 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The HC-SR04 module has an ultrasonic transmitter (40kHz) and a receiver.
* It is operated through two signal pins. When the transmit pin is set to 1
* for 10us, on the falling edge the transmitter sends a short transmission of
* 8 pulses (like a sonar 'ping'). This is reflected off objects and received
* by the receiver. A pulse is sent on the receive pin whose length is equal
* to the delay between the transmission of the pulse and the detection of
* its echo. The distance of the reflecting object is calculated by halving
* the time (to allow for the out and back distance), then multiplying by the
* speed of sound (assumed to be constant).
*
* This driver polls the HC-SR04 by sending the trigger pulse and then measuring
* the length of the received pulse. If the calculated distance is less than
* the threshold, the output state returned by a read() call changes to 1. If
* the distance is greater than the threshold plus a hysteresis margin, the
* output changes to 0. The device also supports readAnalogue(), which returns
* the measured distance in cm, or 32767 if the distance exceeds the
* offThreshold.
*
* It might be thought that the measurement would be more reliable if interrupts
* were disabled while the pulse is being timed. However, this would affect
* other functions in the CS so the measurement is being performed with
* interrupts enabled. Also, we could use an interrupt pin in the Arduino for
* the timing, but the same consideration applies. In any case, the DCC
* interrupt occurs once every 58us, so any IRC code is much faster than that.
* And 58us corresponds to 1cm in the calculation, so the effect of
* interrupts is negligible.
*
* Note: The timing accuracy required for measuring the pulse length means that
* the pins have to be direct Arduino pins; GPIO pins on an IO Extender cannot
* provide the required accuracy.
*/
#ifndef IO_HCSR04_H
#define IO_HCSR04_H
#include "IODevice.h"
class HCSR04 : public IODevice {
private:
// pins must be arduino GPIO pins, not extender pins or HAL pins.
int _trigPin = -1;
int _echoPin = -1;
// Thresholds for setting active state in cm.
uint8_t _onThreshold; // cm
uint8_t _offThreshold; // cm
// Last measured distance in cm.
uint16_t _distance;
// Active=1/inactive=0 state
uint8_t _value = 0;
// Factor for calculating the distance (cm) from echo time (ms).
// Based on a speed of sound of 345 metres/second.
const uint16_t factor = 58; // ms/cm
public:
// Constructor perfroms static initialisation of the device object
HCSR04 (VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) {
_firstVpin = vpin;
_nPins = 1;
_trigPin = trigPin;
_echoPin = echoPin;
_onThreshold = onThreshold;
_offThreshold = offThreshold;
addDevice(this);
}
// Static create function provides alternative way to create object
static void create(VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) {
new HCSR04(vpin, trigPin, echoPin, onThreshold, offThreshold);
}
protected:
// _begin function called to perform dynamic initialisation of the device
void _begin() override {
pinMode(_trigPin, OUTPUT);
pinMode(_echoPin, INPUT);
ArduinoPins::fastWriteDigital(_trigPin, 0);
#if defined(DIAG_IO)
_display();
#endif
}
// _read function - just return _value (calculated in _loop).
int _read(VPIN vpin) override {
(void)vpin; // avoid compiler warning
return _value;
}
int _readAnalogue(VPIN vpin) override {
(void)vpin; // avoid compiler warning
return _distance;
}
// _loop function - read HC-SR04 once every 50 milliseconds.
void _loop(unsigned long currentMicros) override {
read_HCSR04device();
// Delay next loop entry until 50ms have elapsed.
delayUntil(currentMicros + 50000UL);
}
void _display() override {
DIAG(F("HCSR04 Configured on Vpin:%d TrigPin:%d EchoPin:%d On:%dcm Off:%dcm"),
_firstVpin, _trigPin, _echoPin, _onThreshold, _offThreshold);
}
private:
// This polls the HC-SR04 device by sending a pulse and measuring the duration of
// the pulse observed on the receive pin. In order to be kind to the rest of the CS
// software, no interrupts are used and interrupts are not disabled. The pulse duration
// is measured in a loop, using the micros() function. Therefore, interrupts from other
// sources may affect the result. However, interrupts response code in CS typically takes
// much less than the 58us frequency for the DCC interrupt, and 58us corresponds to only 1cm
// in the HC-SR04.
// To reduce chatter on the output, hysteresis is applied on reset: the output is set to 1 when the
// measured distance is less than the onThreshold, and is set to 0 if the measured distance is
// greater than the offThreshold.
//
void read_HCSR04device() {
// uint16 enough to time up to 65ms
uint16_t startTime, waitTime, currentTime, maxTime;
// If receive pin is still set on from previous call, abort the read.
if (ArduinoPins::fastReadDigital(_echoPin))
return;
// Send 10us pulse to trigger transmitter
ArduinoPins::fastWriteDigital(_trigPin, 1);
delayMicroseconds(10);
ArduinoPins::fastWriteDigital(_trigPin, 0);
// Wait for receive pin to be set
startTime = currentTime = micros();
maxTime = factor * _offThreshold * 2;
while (!ArduinoPins::fastReadDigital(_echoPin)) {
// lastTime = currentTime;
currentTime = micros();
waitTime = currentTime - startTime;
if (waitTime > maxTime) {
// Timeout waiting for pulse start, abort the read
return;
}
}
// Wait for receive pin to reset, and measure length of pulse
startTime = currentTime = micros();
maxTime = factor * _offThreshold;
while (ArduinoPins::fastReadDigital(_echoPin)) {
currentTime = micros();
waitTime = currentTime - startTime;
// If pulse is too long then set return value to zero,
// and finish without waiting for end of pulse.
if (waitTime > maxTime) {
// Pulse length longer than maxTime, reset value.
_value = 0;
_distance = 32767;
return;
}
}
// Check if pulse length is below threshold, if so set value.
//DIAG(F("HCSR04: Pulse Len=%l Distance=%d"), waitTime, distance);
_distance = waitTime / factor; // in centimetres
if (_distance < _onThreshold)
_value = 1;
}
};
#endif //IO_HCSR04_H

256
IO_LedChain.h Normal file
View File

@@ -0,0 +1,256 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* This driver is designed for the LED Driver devices which are characteristically
* driven with DATAIN, CLOCK and LATCH signals, and chained one to another by connecting
* the DATAOUT pin of one device to the next device's DATAIN pin. The devices act like a
* shift register, so the data bits are sent to the first device's DATAIN pin and clocked through
* the shift register (bit by bit). Once the shift register is loaded, the data is latched
* into the devices when the LATCH signal is pulsed.
*
* Some devices drive on/off outputs, so one bit is written to the shift register for each
* output to be driven. For example, with 16 x 1-bit device, 16 bits are sent, one for each
* LED output.
*
* Other devices allow the outputs to be driven by grey-scale, with up to 16 bit resolution.
* In this case, the number of bits sent to the shift register increases drastically; for
* 12 LEDs at 16-bit resolution, a total of 172 bits must be sent for each device in the chain.
* for 24 LEDs at 12-bit resolution, 268 bits must be sent for each device.
* The most significant bit of each value is sent first.
*
* RGB LEDs may be driven by connecting an output to each of the R/G/B wires of the LED, but
* most of the driver devices sink current to ground through the LED, so an RGB LED with a
* common anode (+ve terminal) must be used with these devices. Check the datasheet for details.
*
* Device variants known:
* TLC5947: 24 x 12-bit
* TLC5940: 16 x 12-bit
* TLC6C598: 8 x 1-bit
* TLC6C5912: 12 x 1-bit
* STP24DP05: 8 x 3-bit (RGB)
* MAX6979: 16 x 1-bit
* 74HC595: 8 x 1-bit
*
* All of these devices are able to support clock pulses of 30ns or shorter with a clock rate of
* 10MHz or faster; however, the Arduino isn't capable of running this fast, and the shortest pulse
* length that is generated is 750ns, and a peak clock rate of 186kHz. Faster rates could be
* achieved by using the SPI interface, but if any other SPI device is in use then additional
* device select circuitry would be required.
*
*/
#ifndef IO_LEDCHAIN_H
#define IO_LEDCHAIN_H
#include "IODevice.h"
class LedChain : public IODevice {
private:
#ifdef ARDUINO_ARCH_AVR
class DigPin {
private:
volatile uint8_t *ptr;
uint8_t mask;
public:
DigPin() { ptr = &mask; }
DigPin(int pinNumber) {
if (pinNumber >= 0 && pinNumber <= NUM_DIGITAL_PINS) {
int port = digitalPinToPort(pinNumber);
if (port != NOT_A_PORT) {
pinMode(pinNumber, OUTPUT);
mask = digitalPinToBitMask(pinNumber);
ptr = portOutputRegister(port);
return;
}
}
// Pin not valid, set pointer to somewhere benign
ptr = &mask;
}
void setValue(bool value) {
noInterrupts();
if (value)
*ptr |= mask;
else
*ptr &= ~mask;
interrupts();
}
void pulse() {
noInterrupts();
*ptr |= mask;
*ptr &= ~mask;
interrupts();
}
};
#else
// Fall back to digitalWrite calls.
class DigPin {
private:
int _pinNumber = 0;
public:
DigPin() {};
DigPin(int pinNumber) {
pinMode(pinNumber, OUTPUT);
_pinNumber = pinNumber;
}
void setValue(bool value) {
digitalWrite(_pinNumber, value);
}
void pulse() {
digitalWrite(_pinNumber, 1);
digitalWrite(_pinNumber, 0);
}
};
#endif
// pins must be arduino GPIO pins, not extender pins or HAL pins.
DigPin _dataPin;
DigPin _clockPin;
DigPin _latchPin;
int _bitsPerPin = 0;
byte *_values;
bool _changed = true;
int _nextRegisterPin = 0;
unsigned long _lastEntryTime = 0;
public:
// Constructor performs static initialisation of the device object
LedChain (VPIN vpin, int nPins, int dataPin, int clockPin, int latchPin, int bitsPerPin=1) {
_firstVpin = vpin;
_nPins = nPins;
_dataPin = DigPin(dataPin);
_clockPin = DigPin(clockPin);
_latchPin = DigPin(latchPin);
_bitsPerPin = bitsPerPin;
if (_bitsPerPin == 1)
_values = (byte *)calloc((_nPins+7)/8, 1); // 1 byte per 8 pins (rounded up)
else
_values = (byte *)calloc(_nPins, 2); // 2 bytes per pin.
addDevice(this);
}
// Static create function provides alternative way to create object
static void create(VPIN vpin, int nPins, int dataPin, int clockPin, int latchPin, int bitsPerPin=1) {
new LedChain (vpin, nPins, dataPin, clockPin, latchPin, bitsPerPin);
}
protected:
// _begin function called to perform dynamic initialisation of the device
void _begin() override {
_dataPin.setValue(0);
_clockPin.setValue(0);
_latchPin.setValue(0);
#if defined(DIAG_IO)
_display();
#endif
}
// Digital write - write on or off.
void _write(VPIN vpin, int value) {
if (_bitsPerPin == 1) {
int pin = vpin - _firstVpin;
uint8_t *ptr = _values + pin/8;
uint8_t mask = 1 << (pin % 8);
if (value)
*ptr |= mask;
else
*ptr &= ~mask;
} else {
// Write maximum positive value (will be truncated if too large)
writeAnalogue(vpin, value ? 0x7fff : 0);
}
_changed = true;
}
// Analogue write - write the supplied value
void _writeAnalogue(VPIN vpin, int value) {
if (_bitsPerPin == 1 ) {
_write(vpin, value);
} else {
int pin = vpin - _firstVpin;
uint16_t *ptr = (uint16_t *)(_values + pin*2);
*ptr = value;
}
_changed = true;
}
// _loop function - refresh device every 100ms if anything has changed.
void _loop(unsigned long currentMicros) override {
int count = 0;
if (_changed) {
// Remember the time that this output cycle started.
if (_nextRegisterPin == 0) _lastEntryTime = currentMicros;
if (_bitsPerPin == 1) {
int pin=_nextRegisterPin;
uint8_t *ptr = _values + pin/8;
uint8_t mask = 1;
while (true) {
// For each pin, write one bit to the shift register
uint8_t value = (*ptr & mask) ? 1 : 0;
_dataPin.setValue(value);
_clockPin.pulse();
mask <<= 1;
if (mask == 0) {
if (++count >= 2) { // max of 16 pins per loop entry
_nextRegisterPin = pin;
return; // Resume on next loop entry
}
// Move to next byte
ptr++;
mask = 1;
}
if (++pin >= _nPins) break;
}
} else {
// Multiple bits per pin - up to 16 bits stored in two bytes.
int pin=_nextRegisterPin;
uint16_t *ptr = (uint16_t *)_values + pin;
while (true) {
uint16_t value = *ptr++;
// For each pin, write the requisite number of bits to the shift register
uint16_t mask = 1 << (_bitsPerPin-1);
while (mask) {
_dataPin.setValue((value & mask) ? 1 : 0);
_clockPin.pulse();
mask >>= 1;
}
if (++pin >= _nPins) break; // finished.
if (++count >= 1) { // max of 1 pin per loop entry
_nextRegisterPin = pin;
return; // Resume on next loop entry
}
}
}
// Pulse latch pin to transfer data from shift register to outputs.
_latchPin.pulse();
//_changed = false;
}
_nextRegisterPin = 0; // Restart from the beginning on next entry
delayUntil(_lastEntryTime+100000UL); // At most one update cycle per 100ms
}
void _display() override {
DIAG(F("LedChain Configured on Vpins:%d-%d DataPin:%d ClockPin:%d LatchPin:%d BitsPerOutput:%d"),
_firstVpin, _firstVpin+_nPins-1, _dataPin, _clockPin, _latchPin, _bitsPerPin);
}
};
#endif //IO_LEDCHAIN_H

100
IO_MCP23008.h Normal file
View File

@@ -0,0 +1,100 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef IO_MCP23008_H
#define IO_MCP23008_H
#include "IO_GPIOBase.h"
class MCP23008 : public GPIOBase<uint8_t> {
public:
static void create(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1) {
new MCP23008(firstVpin, nPins, I2CAddress, interruptPin);
}
// Constructor
MCP23008(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1)
: GPIOBase<uint8_t>((FSH *)F("MCP23008"), firstVpin, min(nPins, 8), I2CAddress, interruptPin) {
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer));
outputBuffer[0] = REG_GPIO;
}
private:
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 2, REG_GPIO, _portOutputState);
}
void _writePullups() override {
// Set pullups only for in-use pins. This prevents pullup being set for a pin that
// is intended for use as an output but hasn't been written to yet.
I2CManager.write(_I2CAddress, 2, REG_GPPU, _portPullup & _portInUse);
}
void _writePortModes() override {
// Write 0 to IODIR for in-use pins that are outputs, 1 for others.
uint8_t temp = ~(_portMode & _portInUse);
I2CManager.write(_I2CAddress, 2, REG_IODIR, temp);
// Enable interrupt-on-change for in-use pins that are inputs (_portMode=0)
temp = ~_portMode & _portInUse;
I2CManager.write(_I2CAddress, 2, REG_INTCON, 0x00);
I2CManager.write(_I2CAddress, 2, REG_GPINTEN, temp);
}
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer;
I2CManager.read(_I2CAddress, &buffer, 1, 1, REG_GPIO);
_portInputState = buffer;
} else {
// Queue new request
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status == I2C_STATUS_OK)
_portInputState = inputBuffer[0];
else
_portInputState = 0xff;
}
void _setupDevice() override {
// IOCON is set ODR=1 (open drain shared interrupt pin), INTPOL=0 (active-Low)
I2CManager.write(_I2CAddress, 2, REG_IOCON, 0x04);
_writePortModes();
_writePullups();
_writeGpioPort();
}
uint8_t inputBuffer[1];
uint8_t outputBuffer[1];
enum {
// Register definitions for MCP23008
REG_IODIR=0x00,
REG_GPINTEN=0x02,
REG_INTCON=0x04,
REG_IOCON=0x05,
REG_GPPU=0x06,
REG_GPIO=0x09,
};
};
#endif

112
IO_MCP23017.h Normal file
View File

@@ -0,0 +1,112 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef io_mcp23017_h
#define io_mcp23017_h
#include "IO_GPIOBase.h"
#include "FSH.h"
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for MCP23017 16-bit I/O expander.
*/
class MCP23017 : public GPIOBase<uint16_t> {
public:
static void create(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1) {
new MCP23017(vpin, min(nPins,16), I2CAddress, interruptPin);
}
// Constructor
MCP23017(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1)
: GPIOBase<uint16_t>((FSH *)F("MCP23017"), vpin, nPins, I2CAddress, interruptPin)
{
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer));
outputBuffer[0] = REG_GPIOA;
}
private:
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 3, REG_GPIOA, _portOutputState, _portOutputState>>8);
}
void _writePullups() override {
// Set pullups only for in-use pins. This prevents pullup being set for a pin that
// is intended for use as an output but hasn't been written to yet.
uint16_t temp = _portPullup & _portInUse;
I2CManager.write(_I2CAddress, 3, REG_GPPUA, temp, temp>>8);
}
void _writePortModes() override {
// Write 0 to IODIR for in-use pins that are outputs, 1 for others.
uint16_t temp = ~(_portMode & _portInUse);
I2CManager.write(_I2CAddress, 3, REG_IODIRA, temp, temp>>8);
// Enable interrupt for in-use pins which are inputs (_portMode=0)
temp = ~_portMode & _portInUse;
I2CManager.write(_I2CAddress, 3, REG_INTCONA, 0x00, 0x00);
I2CManager.write(_I2CAddress, 3, REG_GPINTENA, temp, temp>>8);
}
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer[2];
I2CManager.read(_I2CAddress, buffer, 2, 1, REG_GPIOA);
_portInputState = ((uint16_t)buffer[1]<<8) | buffer[0];
} else {
// Queue new request
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status == I2C_STATUS_OK)
_portInputState = ((uint16_t)inputBuffer[1]<<8) | inputBuffer[0];
else
_portInputState = 0xffff;
}
void _setupDevice() override {
// IOCON is set MIRROR=1, ODR=1 (open drain shared interrupt pin)
I2CManager.write(_I2CAddress, 2, REG_IOCON, 0x44);
_writePortModes();
_writePullups();
_writeGpioPort();
}
uint8_t inputBuffer[2];
uint8_t outputBuffer[1];
enum {
REG_IODIRA = 0x00,
REG_IODIRB = 0x01,
REG_GPINTENA = 0x04,
REG_GPINTENB = 0x05,
REG_INTCONA = 0x08,
REG_INTCONB = 0x09,
REG_IOCON = 0x0A,
REG_GPPUA = 0x0C,
REG_GPPUB = 0x0D,
REG_GPIOA = 0x12,
REG_GPIOB = 0x13,
};
};
#endif

473
IO_Network.h Normal file
View File

@@ -0,0 +1,473 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Each node on the network is configured with a node number in the range 0-254.
* The remoting configuration defines, for each pin to be available remotely,
* the node number and the VPIN number on that node. The configuration must
* match in all nodes, since it is used by the sending node to identify the node
* and VPIN to which a write command is to be sent, and the VPIN number for a
* sensor/input, and on the receiving node to identify the node from which a
* sensor/input value is being sourced.
*
* The node number is also used in the network driver's address
* field. Number 255 is treated as a multicast address. All stations listen on
* their own address and on the multicast address.
*
* All nodes send regular multicast packets containing the latest values of the
* sensors as they know them. On receipt of such a packet, each node extracts
* the states of the sensors which are sourced by the originating node, and
* updates the values in its own local data. Thus, each node has a copy of the
* states of all digital input pin values that are defined in the remoting
* configuration. Multicasts are sent frequently, so if one is missed
* then, like a London bus, another will be along shortly.
*
* Commands (originating from write() or writeAnalogue() calls) are sent
* immediately, directly from the originating node to the target node. This
* is done with acknowlegements enabled to maximise the probability of
* successful delivery.
*
* Usage:
* First declare, for each remote pin in the common area, the mapping onto
* a node and VPIN number. The array below assumes that the first remote
* VPIN is 4000. The REMOTEPINS definition
* should be the same on all nodes in the network. For outputs, it is the
* definition in the sending node that dictates which node and VPIN the
* action is performed on. For inputs, the value is placed into the
* VPIN location defined in the sending node (that scans the input value),
* but the value is only accepted in the receiving node if its definition
* shows that the signal originates in the sending node.
*
* Example to go into mySetup() function in mySetup.cpp:
* REMOTEPINS rpins[] = {
* {0,30,RPIN_OUT}, //4000 Node 0 GPIO pin 30 (output)
* {1,30,RPIN_IN}, //4001 Node 1 GPIO pin 30 (input)
* {1,100,RPIN_INOUT}, //4002 Node 1 Servo (PCA9685) pin (output to servo, input busy flag)
* {1,164,RPIN_IN}, //4003 Node 1 GPIO extender (MCP23017) pin (input)
* {2,164,RPIN_IN} //4004 Node 2 GPIO extender (MCP23017) pin (input)
* }
* // FirstVPIN, nPins, thisNode, pinDefs, CEPin, CSNPin
* Network::create(4000, NUMREMOTEPINS(rpins), 0, rpins, new RF24Driver(48, 49));
*
* This example defines VPINs 4000-4004 which map onto pins on nodes 0, 1 and 2.
* The network device in this case is an nRF24L01, which has to be connected to the hardware
* MISO, MOSI, SCK and CS pins of the microcontroller; in addition, the CE and
* CSN pins on the nRF24 are connected to two pins (48 and 49 above).
*
* If any of pins 4000-4004 are referenced by turnouts, outputs or sensors, or by EX-RAIL,
* then the corresponding remote pin state will be retrieved or updated.
* For example, in EX-RAIL,
* SET(4000) on node 1 or 2 will set pin 30 on Node 0 to +5V (pin is put into output mode on first write).
* AT(4001) on node 0 or 2 will wait until the sensor attached to pin 30 on Node 1 activates.
* SERVO(4002,300,2) on node 0 or 2 will reposition the servo on Node 1 PCA9685 module to position 300, and
* AT(-4002) will wait until the servo has finished moving.
*
* The following sensor definition on node 0 will map onto VPIN 4004, i.e. Node 2 VPIN 164,
* which is the first pin on the first MCP23017:
* <S 1 4004 0>
* and when a sensor attached to the pin on node 2 is activated (pin pulled down to 0V) the following
* message will be generated on node 0:
* <Q 1>
* When the sensor deactivates, the following message will be generated on node 0:
* <q 1>
*/
#ifndef IO_NETWORK_H
#define IO_NETWORK_H
#include "IODevice.h"
#include "RF24.h"
// Macros and type for creating the remote pin definitions.
// The definitions are stored in PROGMEM to reduce RAM requirements.
// The flags byte contains, in the low 2 bits, RPIN_IN, RPIN_OUT or RPIN_INOUT.
typedef struct { uint8_t node; VPIN vpin; uint8_t flags; } RPIN;
#define REMOTEPINS static const RPIN PROGMEM
#define NUMREMOTEPINS(x) (sizeof(x)/sizeof(RPIN))
enum {
RPIN_IN=1,
RPIN_OUT=2,
RPIN_INOUT=RPIN_IN|RPIN_OUT,
};
// Define interface for network driver. This should be implemented for each supported
// network type.
// class NetInterface {
// public:
// bool begin();
// bool sendCommand(uint8_t node, const uint8_t buffer[], uint8_t size);
// bool available();
// uint8_t read(uint8_t buffer[], uint8_t size);
// void loop();
// };
// Class implementing the Application-layer network functionality.
// This is implemented as an IODevice instance so it can be easily
// plugged in to the HAL framwork.
template <class NetInterface>
class Network : public IODevice {
private:
const RPIN *_pinDefs; // May need to become a far pointer!
// Time of last loop execution
unsigned long _lastExecutionTime;
// Current digital values for remoted pins, stored as a bit field
uint8_t *_pinValues;
// Number of the current node (1-254)
uint8_t _thisNode;
// Maximum size of payload (must be 32 or less for RF24)
static const uint8_t maxPayloadSize = 32;
bool _updatePending;
int _nextSendPin;
unsigned long _lastMulticastTime;
int _firstPinToSend; // must be a multiple of 8
int _numPinsToSend; // need not be a multiple of 8
NetInterface *_netDriver;
// List of network commands
enum : uint8_t {
NET_CMD_WRITE = 0,
NET_CMD_WRITEANALOGUE = 1,
NET_CMD_VALUEUPDATE = 2,
};
// Field Positions in Network Header
enum NetHeader {
IONET_SENDNODE = 0, // for VALUEUPDATE
IONET_DESTNODE = 0, // for WRITE/WRITEANALOGUE
IONET_CMDTYPE = 1,
IONET_VPIN = 2,
IONET_VPIN_H = 2,
IONET_VPIN_L = 3,
IONET_DATA = 4,
};
public:
// Constructor performs static initialisation of the device object
Network (VPIN firstVpin, int nPins, uint8_t thisNode, const RPIN pinDefs[], NetInterface *netDriver) {
_firstVpin = firstVpin;
_nPins = nPins;
_thisNode = thisNode;
_pinDefs = pinDefs;
_pinValues = (uint8_t *)calloc((nPins+7)/8, 1); // Allocate space for input values.
_netDriver = netDriver;
addDevice(this);
// Identify which pins are allocated to this node.
_firstPinToSend = -1;
int lastPinToSend = 0;
for (int pin=0; pin<_nPins; pin++) {
uint8_t node = GETFLASH(&_pinDefs[pin].node);
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
// Check if the pin is an input on this node?
if (node == _thisNode && (flags & RPIN_IN)) {
if (_firstPinToSend==-1) _firstPinToSend = pin;
lastPinToSend = pin;
}
//DIAG(F("Node=%d FirstPin=%d, NumPins=%d"), node, _firstPinToSend, _numPinsToSend);
}
// Round down to multiple of 8 (byte boundary).
_firstPinToSend /= 8;
_firstPinToSend *= 8;
_numPinsToSend = lastPinToSend - _firstPinToSend + 1;
// Restrict to the max that fit in a packet
_numPinsToSend = min(8*(MAX_MSG_SIZE-IONET_DATA),_numPinsToSend);
//DIAG(F("FirstPin=%d, NumPins=%d"), _firstPinToSend, _numPinsToSend);
// Prepare for first transmission
_nextSendPin = _firstPinToSend;
}
// Static create function provides alternative way to create object
static void create(VPIN firstVpin, int nPins, uint8_t thisNode, const RPIN pinDefs[], NetInterface *netDriver) {
new Network(firstVpin, nPins, thisNode, pinDefs, netDriver);
}
protected:
// _begin function called to perform dynamic initialisation of the device
void _begin() override {
if (_netDriver->begin(_thisNode)) {
_display();
_deviceState = DEVSTATE_NORMAL;
_lastMulticastTime = _lastExecutionTime = micros();
_updatePending = true;
} else {
// Error in initialising
DIAG(F("Network Failed to initialise"));
_deviceState = DEVSTATE_FAILED;
}
}
// _read function - just return pin value (updated in _loop when message received from remote node)
int _read(VPIN vpin) override {
int pin = vpin - _firstVpin;
uint8_t mask = 1 << (pin & 7);
int byteIndex = pin / 8;
return (_pinValues[byteIndex] & mask) ? 1 : 0;
}
// _write (digital) - send command directly to the appropriate remote node.
void _write(VPIN vpin, int value) override {
// Send message
int pin = vpin - _firstVpin;
uint8_t node = GETFLASH(&_pinDefs[pin].node);
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
VPIN remoteVpin = GETFLASHW(&_pinDefs[pin].vpin);
if (node != _thisNode && remoteVpin != VPIN_NONE && (flags & RPIN_OUT)) {
#ifdef DIAG_IO
DIAG(F("Network: write(%d,%d)=>send(%d,\"write(%d,%d)\")"), vpin, value, node, remoteVpin, value);
#endif
netBuffer[IONET_DESTNODE] = node;
netBuffer[IONET_CMDTYPE] = NET_CMD_WRITE;
netBuffer[IONET_VPIN_H] = getMsb(remoteVpin);
netBuffer[IONET_VPIN_L] = getLsb(remoteVpin);
netBuffer[IONET_DATA] = (uint8_t)value;
// Set up to send to the specified node address
_netDriver->sendCommand(node, netBuffer, IONET_DATA+1);
}
}
// _writeAnalogue - send command directly to the appropriate remote node.
void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override {
// Send message
int pin = vpin - _firstVpin;
uint8_t node = GETFLASH(&_pinDefs[pin].node);
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
VPIN remoteVpin = GETFLASHW(&_pinDefs[pin].vpin);
if (node != _thisNode && remoteVpin != VPIN_NONE && (flags & RPIN_OUT)) {
#ifdef DIAG_IO
DIAG(F("Network: writeAnalogue(%d,%d,%d,%d)=>send(%d,\"writeAnalogue(%d,%d,...)\")"),
vpin, value, param1, param2, node, remoteVpin, value);
#endif
netBuffer[IONET_DESTNODE] = node;
netBuffer[IONET_CMDTYPE] = NET_CMD_WRITEANALOGUE;
netBuffer[IONET_VPIN_H] = getMsb(remoteVpin);
netBuffer[IONET_VPIN_L] = getLsb(remoteVpin);
netBuffer[IONET_DATA+0] = getMsb(value);
netBuffer[IONET_DATA+1] = getLsb(value);
netBuffer[IONET_DATA+2] = param1;
netBuffer[IONET_DATA+3] = getMsb(param2);
netBuffer[IONET_DATA+4] = getLsb(param2);
// Set up to send to the specified node address
_netDriver->sendCommand(node, netBuffer, IONET_DATA+5);
}
}
// _loop function - check for, and process, received data from RF24, and send any
// updates that are due.
void _loop(unsigned long currentMicros) override {
// Perform cyclic netdriver functions, including switching back to receive mode
// (for half-duplex network drivers) and receiving input packets.
_netDriver->loop();
// Check for incoming data
if (_netDriver->available())
processReceivedData();
// Force a data update broadcast every 1000ms irrespective of whether there are
// data changes or not.
if (currentMicros - _lastMulticastTime > (1000 * 1000UL))
_updatePending = true;
// Send out data update broadcasts once every 20ms if there are changes
if (currentMicros - _lastExecutionTime > (20 * 1000UL)) {
// Broadcast updates to all other nodes. The preparation is done in a number of
// successive calls, and when sendSensorUpdates() returns true it has completed.
if (sendSensorUpdates()) {
_lastExecutionTime = currentMicros; // Send complete, wait for next time
}
}
}
void _display() override {
DIAG(F("Network Configured on Vpins:%d-%d Node:%d%S"),
_firstVpin, _firstVpin+_nPins-1, _thisNode, (_deviceState==DEVSTATE_FAILED) ? F(" OFFLINE") : F(""));
}
private:
// Send sensor updates only if one or more locally sourced inputs that
// are mapped to remote VPINs have changed state.
//
bool sendSensorUpdates() {
// This loop is split into multiple loop() entries, so as not to hog
// the cpu for too long.
if (_numPinsToSend == 0) return true; // No pins to send from this node.
// Update the _pinValues bitfield to reflect the current values of local pins.
// Process maximum of 5 pins per entry.
uint8_t count = 5;
bool state;
// First time through, _nextSendPin is equal to _firstPinToSend.
for (int pin=_nextSendPin; pin<_firstPinToSend+_numPinsToSend; pin++) {
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
// Is the pin an input on this node?
if ((flags & RPIN_IN) && GETFLASH(&_pinDefs[pin].node) == _thisNode) {
// Local input pin, read and update current state of input
VPIN localVpin = GETFLASHW(&_pinDefs[pin].vpin);
if (localVpin != VPIN_NONE) {
state = IODevice::read(localVpin);
uint16_t byteIndex = pin / 8;
uint8_t bitMask = 1 << (pin & 7);
uint8_t byteValue = _pinValues[byteIndex];
bool oldState = byteValue & bitMask;
if (state != oldState) {
// Store state in remote values array
if (state)
byteValue |= bitMask;
else
byteValue &= ~bitMask;
_pinValues[byteIndex] = byteValue;
_updatePending = true;
}
if (--count == 0) {
// Done enough checks for this entry, resume on next one.
_nextSendPin = pin+1;
return false;
}
}
}
}
// When we get here, we've updated the _pinValues array. See if an
// update is due.
if (_updatePending) {
// On master and on slave, send pin states to other nodes
netBuffer[IONET_SENDNODE] = _thisNode; // Originating node
netBuffer[IONET_CMDTYPE] = NET_CMD_VALUEUPDATE;
// The packet size is 32 bytes, header is 4 bytes, so 28 bytes of data.
// We can therefore send up to 224 binary states per packet.
int byteCount = (_numPinsToSend+7)/8;
VPIN remoteVpin = _firstVpin+_firstPinToSend;
netBuffer[IONET_VPIN_H] = getMsb(remoteVpin);
netBuffer[IONET_VPIN_L] = getLsb(remoteVpin);
// Copy from pinValues array into buffer. This is why _firstPinToSend must be a multiple of 8.
memcpy(&netBuffer[IONET_DATA], &_pinValues[_firstPinToSend/8], byteCount);
// Broadcast update
_netDriver->sendCommand(255, netBuffer, IONET_DATA + byteCount);
//DIAG(F("Sent %d bytes: %x %x ..."), byteCount, netBuffer[4], netBuffer[5]);
_lastMulticastTime = micros();
_updatePending = false;
}
// Set next pin ready for next entry.
_nextSendPin = _firstPinToSend;
return true; // Done all we need to for this cycle.
}
// Read next packet from the device's input buffers. Decode the message,
// and take the appropriate action.
// The packet may be a command to do an output write (digital or analogue), or
// it may be an update for digital input signals.
// For digital input signals, the values are broadcast from the node that is
// the pin source to all the other nodes.
void processReceivedData() {
// Read packet
uint8_t size = _netDriver->read(netBuffer, sizeof(netBuffer));
if (size < IONET_DATA) return; // packet too short.
// Extract command type from packet.
uint8_t command = netBuffer[IONET_CMDTYPE];
//DIAG(F("Received %d bytes, type=%d"), size, command);
// Process received data
switch (command) {
case NET_CMD_WRITE: // Digital write command
{
uint8_t targetNode = netBuffer[IONET_DESTNODE];
if (targetNode == _thisNode && size == IONET_DATA+1) {
VPIN vpin = makeWord(netBuffer[IONET_VPIN_H], netBuffer[IONET_VPIN_L]);
uint8_t state = netBuffer[IONET_DATA];
IODevice::write(vpin, state);
}
}
break;
case NET_CMD_WRITEANALOGUE: // Analogue write command
{
uint8_t targetNode = netBuffer[IONET_DESTNODE];
if (targetNode == _thisNode && size == IONET_DATA+5) {
VPIN vpin = makeWord(netBuffer[IONET_VPIN_H], netBuffer[IONET_VPIN_L]);
int value = makeWord(netBuffer[IONET_DATA], netBuffer[IONET_DATA+1]);
uint8_t param1 = netBuffer[IONET_DATA+2];
uint16_t param2 = makeWord(netBuffer[IONET_DATA+3], netBuffer[IONET_DATA+4]);
IODevice::writeAnalogue(vpin, value, param1, param2);
// Set the local value for the pin, used by isBusy(),
// and subsequently updated by the remote node.
_pinValues[vpin-_firstVpin] = true;
}
}
break;
case NET_CMD_VALUEUPDATE: // Updates of input states (sensors etc).
{
uint8_t sendingNode = netBuffer[IONET_SENDNODE];
VPIN vpin = makeWord(netBuffer[IONET_VPIN_H], netBuffer[IONET_VPIN_L]);
//DIAG(F("Node %d Size %d VPIN %d Rx States %x"), sendingNode, size, vpin, netBuffer[IONET_DATA]);
// Read through the buffer one byte at a time.
uint8_t *buffPtr = &netBuffer[IONET_DATA];
uint8_t *bitFieldPtr = &_pinValues[(vpin-_firstVpin)/8];
int currentPin = vpin - _firstVpin;
for (int byteNo=0; byteNo<size-4 && currentPin<_nPins; byteNo++) {
// Now work through the received byte examining each bit.
uint8_t byteValue = *buffPtr++;
uint8_t bitFieldValue = *bitFieldPtr;
uint8_t bitMask = 1;
for (int bitNo=0; bitNo<8 && currentPin<_nPins; bitNo++) {
// Process incoming value if it's come from the pin source node
uint8_t pinSource = GETFLASH(&_pinDefs[currentPin].node);
if (sendingNode == pinSource) {
if (byteValue & bitMask)
bitFieldValue |= bitMask;
else
bitFieldValue &= ~bitMask;
}
bitMask <<= 1;
currentPin++;
}
// Store the modified byte back
*bitFieldPtr++ = bitFieldValue;
}
}
break;
default:
break;
}
}
// Helper functions for packing/unpacking buffers.
inline uint16_t makeWord(uint8_t msb, uint8_t lsb) {
return ((uint16_t)msb << 8) | lsb;
}
inline uint8_t getMsb(uint16_t w) {
return w >> 8;
}
inline uint8_t getLsb(uint16_t w) {
return w & 0xff;
}
// Data space for actual input and output buffer.
uint8_t netBuffer[maxPayloadSize];
};
#endif //IO_NETWORK_H

274
IO_PCA9685.cpp Normal file
View File

@@ -0,0 +1,274 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
// REGISTER ADDRESSES
static const byte PCA9685_MODE1=0x00; // Mode Register
static const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/
static const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */
// MODE1 bits
static const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */
static const byte MODE1_AI=0x20; /**< Auto-Increment enabled */
static const byte MODE1_RESTART=0x80; /**< Restart enabled */
static const float FREQUENCY_OSCILLATOR=25000000.0; /** Accurate enough for our purposes */
static const uint8_t PRESCALE_50HZ = (uint8_t)(((FREQUENCY_OSCILLATOR / (50.0 * 4096.0)) + 0.5) - 1);
static const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed
// Predeclare helper function
static void writeRegister(byte address, byte reg, byte value);
// Create device driver instance.
void PCA9685::create(VPIN firstVpin, int nPins, uint8_t I2CAddress) {
new PCA9685(firstVpin, nPins, I2CAddress);
}
// Configure a port on the PCA9685.
bool PCA9685::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
if (configType != CONFIGURE_SERVO) return false;
if (paramCount != 5) return false;
#ifdef DIAG_IO
DIAG(F("PCA9685 Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"),
vpin, params[0], params[1], params[2], params[3], params[4]);
#endif
int8_t pin = vpin - _firstVpin;
struct ServoData *s = _servoData[pin];
if (s == NULL) {
_servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData));
s = _servoData[pin];
if (!s) return false; // Check for failed memory allocation
}
s->activePosition = params[0];
s->inactivePosition = params[1];
s->profile = params[2];
s->duration = params[3];
int state = params[4];
if (state != -1) {
// Position servo to initial state
_writeAnalogue(vpin, state ? s->activePosition : s->inactivePosition, 0, 0);
}
return true;
}
// Constructor
PCA9685::PCA9685(VPIN firstVpin, int nPins, uint8_t I2CAddress) {
_firstVpin = firstVpin;
_nPins = min(nPins, 16);
_I2CAddress = I2CAddress;
// To save RAM, space for servo configuration is not allocated unless a pin is used.
// Initialise the pointers to NULL.
for (int i=0; i<_nPins; i++)
_servoData[i] = NULL;
addDevice(this);
// Initialise structure used for setting pulse rate
requestBlock.setWriteParams(_I2CAddress, outputBuffer, sizeof(outputBuffer));
}
// Device-specific initialisation
void PCA9685::_begin() {
I2CManager.begin();
I2CManager.setClock(1000000); // Nominally able to run up to 1MHz on I2C
// In reality, other devices including the Arduino will limit
// the clock speed to a lower rate.
// Initialise I/O module here.
if (I2CManager.exists(_I2CAddress)) {
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);
writeRegister(_I2CAddress, PCA9685_PRESCALE, PRESCALE_50HZ); // 50Hz clock, 20ms pulse period.
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_AI);
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_RESTART | MODE1_AI);
// In theory, we should wait 500us before sending any other commands to each device, to allow
// the PWM oscillator to get running. However, we don't do any specific wait, as there's
// plenty of other stuff to do before we will send a command.
#if defined(DIAG_IO)
_display();
#endif
} else
_deviceState = DEVSTATE_FAILED;
}
// Device-specific write function, invoked from IODevice::write().
// For this function, the configured profile is used.
void PCA9685::_write(VPIN vpin, int value) {
if (_deviceState == DEVSTATE_FAILED) return;
#ifdef DIAG_IO
DIAG(F("PCA9685 Write Vpin:%d Value:%d"), vpin, value);
#endif
int pin = vpin - _firstVpin;
if (value) value = 1;
struct ServoData *s = _servoData[pin];
if (s != NULL) {
// Use configured parameters
_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration);
} // else { /* ignorethe request */ }
}
// Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue().
// Profile is as follows:
// Bit 7: 0=Set PWM to 0% to power off servo motor when finished
// 1=Keep PWM pulses on (better when using PWM to drive an LED)
// Bits 6-0: 0 Use specified duration (defaults to 0 deciseconds)
// 1 (Fast) Move servo in 0.5 seconds
// 2 (Medium) Move servo in 1.0 seconds
// 3 (Slow) Move servo in 2.0 seconds
// 4 (Bounce) Servo 'bounces' at extremes.
//
void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) {
if (_deviceState == DEVSTATE_FAILED) return;
#ifdef DIAG_IO
DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d"),
vpin, value, profile, duration);
#endif
int pin = vpin - _firstVpin;
if (value > 4095) value = 4095;
else if (value < 0) value = 0;
struct ServoData *s = _servoData[pin];
if (s == NULL) {
// Servo pin not configured, so configure now using defaults
s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1);
if (s == NULL) return; // Check for memory allocation failure
s->activePosition = 0;
s->inactivePosition = 0;
s->currentPosition = value;
s->profile = Instant; // Use instant profile (but not this time)
}
// Animated profile. Initiate the appropriate action.
s->currentProfile = profile;
uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit.
s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds
profileValue==Medium ? 20 : // 1.0 seconds
profileValue==Slow ? 40 : // 2.0 seconds
profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds
duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms)
s->stepNumber = 0;
s->toPosition = value;
s->fromPosition = s->currentPosition;
}
// _read returns true if the device is currently in executing an animation,
// changing the output over a period of time.
int PCA9685::_read(VPIN vpin) {
if (_deviceState == DEVSTATE_FAILED) return 0;
int pin = vpin - _firstVpin;
struct ServoData *s = _servoData[pin];
if (s == NULL)
return false; // No structure means no animation!
else
return (s->stepNumber < s->numSteps);
}
void PCA9685::_loop(unsigned long currentMicros) {
for (int pin=0; pin<_nPins; pin++) {
updatePosition(pin);
}
delayUntil(currentMicros + refreshInterval * 1000UL);
}
// Private function to reposition servo
// TODO: Could calculate step number from elapsed time, to allow for erratic loop timing.
void PCA9685::updatePosition(uint8_t pin) {
struct ServoData *s = _servoData[pin];
if (s == NULL) return; // No pin configuration/state data
if (s->numSteps == 0) return; // No animation in progress
if (s->stepNumber == 0 && s->fromPosition == s->toPosition) {
// Go straight to end of sequence, output final position.
s->stepNumber = s->numSteps-1;
}
if (s->stepNumber < s->numSteps) {
// Animation in progress, reposition servo
s->stepNumber++;
if ((s->currentProfile & ~NoPowerOff) == Bounce) {
// Retrieve step positions from array in flash
byte profileValue = GETFLASH(&_bounceProfile[s->stepNumber]);
s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition);
} else {
// All other profiles - calculate step by linear interpolation between from and to positions.
s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition);
}
// Send servo command
writeDevice(pin, s->currentPosition);
} else if (s->stepNumber < s->numSteps + _catchupSteps) {
// We've finished animation, wait a little to allow servo to catch up
s->stepNumber++;
} else if (s->stepNumber == s->numSteps + _catchupSteps
&& s->currentPosition != 0) {
#ifdef IO_SWITCH_OFF_SERVO
if ((s->currentProfile & NoPowerOff) == 0) {
// Wait has finished, so switch off PWM to prevent annoying servo buzz
writeDevice(pin, 0);
}
#endif
s->numSteps = 0; // Done now.
}
}
// writeDevice takes a pin in range 0 to _nPins-1 within the device, and a value
// between 0 and 4095 for the PWM mark-to-period ratio, with 4095 being 100%.
void PCA9685::writeDevice(uint8_t pin, int value) {
#ifdef DIAG_IO
DIAG(F("PCA9685 I2C:x%x WriteDevice Pin:%d Value:%d"), _I2CAddress, pin, value);
#endif
// Wait for previous request to complete
uint8_t status = requestBlock.wait();
if (status != I2C_STATUS_OK) {
_deviceState = DEVSTATE_FAILED;
DIAG(F("PCA9685 I2C:x%x failed %S"), _I2CAddress, I2CManager.getErrorMessage(status));
} else {
// Set up new request.
outputBuffer[0] = PCA9685_FIRST_SERVO + 4 * pin;
outputBuffer[1] = 0;
outputBuffer[2] = (value == 4095 ? 0x10 : 0); // 4095=full on
outputBuffer[3] = value & 0xff;
outputBuffer[4] = value >> 8;
I2CManager.queueRequest(&requestBlock);
}
}
// Display details of this device.
void PCA9685::_display() {
DIAG(F("PCA9685 I2C:x%x Configured on Vpins:%d-%d %S"), _I2CAddress, (int)_firstVpin,
(int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
// Internal helper function for this device
static void writeRegister(byte address, byte reg, byte value) {
I2CManager.write(address, 2, reg, value);
}
// Profile for a bouncing signal or turnout
// The profile below is in the range 0-100% and should be combined with the desired limits
// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here,
// i.e. the bounce is the same on the down action as on the up action. First entry isn't used.
const byte FLASH PCA9685::_bounceProfile[30] =
{0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100};

102
IO_PCF8574.h Normal file
View File

@@ -0,0 +1,102 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The PCF8574 is a simple device; it only has one register. The device
* input/output mode and pullup are configured through this, and the
* output state is written and the input state read through it too.
*
* This is accomplished by having a weak resistor in series with the output,
* and a read-back of the other end of the resistor. As an output, the
* pin state is set to 1 or 0, and the output voltage goes to +5V or 0V
* (through the weak resistor).
*
* In order to use the pin as an input, the output is written as
* a '1' in order to pull up the resistor. Therefore the input will be
* 1 unless the pin is pulled down externally, in which case it will be 0.
*
* As a consequence of this approach, it is not possible to use the device for
* inputs without pullups.
*/
#ifndef IO_PCF8574_H
#define IO_PCF8574_H
#include "IO_GPIOBase.h"
class PCF8574 : public GPIOBase<uint8_t> {
public:
static void create(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1) {
new PCF8574(firstVpin, nPins, I2CAddress, interruptPin);
}
PCF8574(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1)
: GPIOBase<uint8_t>((FSH *)F("PCF8574"), firstVpin, min(nPins, 8), I2CAddress, interruptPin)
{
requestBlock.setReadParams(_I2CAddress, inputBuffer, 1);
}
private:
// The pin state is '1' if the pin is an input or if it is an output set to 1. Zero otherwise.
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 1, _portOutputState | ~_portMode);
}
// The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.
// Therefore, writing '1' in _writePortModes is enough to set the module to input mode
// and enable pull-up.
void _writePullups() override { }
// The pin state is '1' if the pin is an input or if it is an output set to 1. Zero otherwise.
void _writePortModes() override {
I2CManager.write(_I2CAddress, 1, _portOutputState | ~_portMode);
}
// In immediate mode, _readGpioPort reads the device GPIO port and updates _portInputState accordingly.
// When not in immediate mode, it initiates a request using the request block and returns.
// When the request completes, _processCompletion finishes the operation.
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer[1];
I2CManager.read(_I2CAddress, buffer, 1);
_portInputState = buffer[0];
} else {
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status == I2C_STATUS_OK)
_portInputState = inputBuffer[0];
else
_portInputState = 0xff;
}
// Set up device ports
void _setupDevice() override {
_writePortModes();
}
uint8_t inputBuffer[1];
};
#endif

536
IO_RF24.h Normal file
View File

@@ -0,0 +1,536 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* nRF24 default mode of operation:
* Channel: 108
* Bit rate: 2MHz
* CRC: 16-bit
* Power Level: High
*
* Each node on the network is configured with a node number in the range 0-254.
* The remoting configuration defines, for each pin to be available remotely,
* the node number and the VPIN number on that node. The configuration must
* match in all nodes, since it is used by the sending node to identify the node
* and VPIN to which a write command is to be sent, and the VPIN number for a
* sensor/input, and on the receiving node to identify the node from which a
* sensor/input value is being sourced.
*
* The node number is also used as the first byte of the nRF24's 5-byte address
* field. Number 255 is treated as a multicast address. All stations listen on
* their own address and on the multicast address.
*
* All nodes send regular multicast packets containing the latest values of the
* sensors as they know them. On receipt of such a packet, each node extracts
* the states of the sensors which are sourced by the originating node, and
* updates the values in its own local data. Thus, each node has a copy of the
* states of all digital input pin values that are defined in the remoting
* configuration. Multicasts are sent frequently, so if one is missed
* then, like a London bus, another will be along shortly.
*
* Commands (originating from write() or writeAnalogue() calls) are sent
* immediately, directly from the originating node to the target node. This
* is done with acknowlegements enabled to maximise the probability of
* successful delivery.
*
* The nRF24 device receives and acknowledges data packets autonomously.
* Therefore, this driver just needs to detect when a packet is received and
* read and process its contents. The time to read the packet is under 200us
* typically.
*
* The nRF24 is also capable of autonomously sending packets, processing
* acknowledgements, and generating retries. The driver writes the packet to
* the device and then waits for notification of completion (success, or retries
* exceeded) through the device's registers. Similarly, the time to write a
* packet is under 200us and, if we don't wait for the completion, we can allow
* the processor to do other things while the transmission is in progress.
* A write with ack can complete in under 600us, plus the time of turning the
* receiver off and on.
*
* Usage:
* First declare, for each remote pin in the common area, the mapping onto
* a node and VPIN number. The array below assumes that the first remote
* VPIN is 4000. The nRF24L01 device is connected to the standard SPI pins
* plus two others referred to as CE and CSN. The Arduino pin numbers used
* for these are specified in the create() call. The REMOTEPINS definition
* should be the same on all nodes in the network. For outputs, it is the
* definition in the sending node that dictates which node and VPIN the
* action is performed on. For inputs, the value is placed into the
* VPIN location defined in the sending node (that scans the input value),
* but the value is only accepted in the receiving node if its definition
* shows that the signal originates in the sending node.
*
* Example to go into mySetup() function in mySetup.cpp:
* REMOTEPINS rpins[] = {
* {0,30,RPIN_OUT}, //4000 Node 0 GPIO pin 30 (output)
* {1,30,RPIN_IN}, //4001 Node 1 GPIO pin 30 (input)
* {1,100,RPIN_INOUT}, //4002 Node 1 Servo (PCA9685) pin (output to servo, input busy flag)
* {1,164,RPIN_IN}, //4003 Node 1 GPIO extender (MCP23017) pin (input)
* {2,164,RPIN_IN} //4004 Node 2 GPIO extender (MCP23017) pin (input)
* }
* // FirstVPIN, nPins, thisNode, pinDefs, CEPin, CSNPin
* RF24Net::create(4000, NUMREMOTEPINS(rpins), 0, rpins, 48, 49);
*
* This example defines VPINs 4000-4004 which map onto pins on nodes 0, 1 and 2.
* The nRF24 device has to be connected to the hardware MISO, MOSI, SCK and CS pins of the
* microcontroller; in addition, the CE and CSN pins on the nRF24 are connected to
* two pins (48 and 49 above).
*
* If any of pins 4000-4004 are referenced by turnouts, outputs or sensors, or by EX-RAIL,
* then the corresponding remote pin state will be retrieved or updated.
* For example, in EX-RAIL,
* SET(4000) on node 1 or 2 will set pin 30 on Node 0 to +5V (pin is put into output mode on first write).
* AT(4001) on node 0 or 2 will wait until the sensor attached to pin 30 on Node 1 activates.
* SERVO(4002,300,2) on node 0 or 2 will reposition the servo on Node 1 PCA9685 module to position 300, and
* AT(-4002) will wait until the servo has finished moving.
*
* The following sensor definition on node 0 will map onto VPIN 4004, i.e. Node 2 VPIN 164,
* which is the first pin on the first MCP23017:
* <S 1 4004 0>
* and when a sensor attached to the pin on node 2 is activated (pin pulled down to 0V) the following
* message will be generated on node 0:
* <Q 1>
* When the sensor deactivates, the following message will be generated on node 0:
* <q 1>
*/
#ifndef IO_RF24_H
#define IO_RF24_H
#include "IODevice.h"
#include "RF24.h"
// Macros and type for creating the remote pin definitions.
// The definitions are stored in PROGMEM to reduce RAM requirements.
// The flags byte contains, in the low 2 bits, RPIN_IN, RPIN_OUT or RPIN_INOUT.
typedef struct { uint8_t node; VPIN vpin; uint8_t flags; } RPIN;
#define REMOTEPINS static const RPIN PROGMEM
#define NUMREMOTEPINS(x) (sizeof(x)/sizeof(RPIN))
enum {
RPIN_IN=1,
RPIN_OUT=2,
RPIN_INOUT=RPIN_IN|RPIN_OUT,
};
class RF24Net : public IODevice {
private:
// pins must be arduino GPIO pins, not extender pins or HAL pins.
int _cePin = -1;
int _csnPin = -1;
const RPIN *_pinDefs; // May need to become a far pointer!
// Time of last loop execution
unsigned long _lastExecutionTime;
// Current digital values for remoted pins, stored as a bit field
uint8_t *_pinValues;
// Number of the current node (0-254)
uint8_t _thisNode;
// 5-byte nRF24L01 address. First byte will contain the node number (0-254) or 255 for broadcast
byte _address[5] = {0x00, 0xCC, 0xEE, 0xEE, 0xCC};
// Maximum size of payload (must be 32 or less)
static const uint8_t maxPayloadSize = 32;
// Current node being sent sensor data and polled
uint8_t _currentSendNode = 0;
bool _sendInProgress = false;
bool _changesPending;
int _nextSendPin = 0;
unsigned long _lastMulticastTime;
int _firstPinToSend; // must be a multiple of 8
int _numPinsToSend; // need not be a multiple of 8
RF24 _radio;
// List of network commands
enum : uint8_t {
NET_CMD_WRITE,
NET_CMD_WRITEANALOGUE,
NET_CMD_VALUEUPDATE,
};
public:
// Constructor performs static initialisation of the device object
RF24Net (VPIN firstVpin, int nPins, uint8_t thisNode, const RPIN pinDefs[], int cePin, int csnPin) {
_firstVpin = firstVpin;
_nPins = nPins;
_cePin = cePin;
_csnPin = csnPin;
_thisNode = thisNode;
_pinDefs = pinDefs;
_address[0] = 0x00;
_address[1] = 0xCC;
_address[2] = 0xEE;
_address[3] = 0xEE;
_address[4] = 0xCC;
_pinValues = (uint8_t *)calloc((nPins+7)/8, 1); // Allocate space for input values.
addDevice(this);
// Identify which pins are allocated to this node.
_firstPinToSend = -1;
_numPinsToSend = 0;
for (int pin=0; pin<_nPins; pin++) {
uint8_t node = GETFLASH(&_pinDefs[pin].node);
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
// Check if the pin is an input on this node?
if (node == _thisNode && (flags & RPIN_IN)) {
if (_firstPinToSend==-1) _firstPinToSend = pin;
_numPinsToSend = pin - _firstPinToSend + 1;
}
//DIAG(F("Node=%d FirstPin=%d, NumPins=%d"), node, _firstPinToSend, _numPinsToSend);
}
// Round down to multiple of 8 (byte boundary).
_firstPinToSend /= 8;
_firstPinToSend *= 8;
_nextSendPin = _firstPinToSend;
//DIAG(F("FirstPin=%d, NumPins=%d"), _firstPinToSend, _numPinsToSend);
}
// Static create function provides alternative way to create object
static void create(VPIN firstVpin, int nPins, uint8_t thisNode, const RPIN pinDefs[], int cePin, int csnPin) {
new RF24Net(firstVpin, nPins, thisNode, pinDefs, cePin, csnPin);
}
protected:
// _begin function called to perform dynamic initialisation of the device
void _begin() override {
#if defined(DIAG_IO)
_display();
#endif
if (_radio.begin(_cePin, _csnPin)) {
// Device initialisation OK, set up parameters
_radio.setDataRate(RF24_2MBPS);
_radio.setPALevel(RF24_PA_HIGH);
_radio.setChannel(108);
_radio.enableDynamicPayloads(); // variable length packets
_radio.setAutoAck(true);
_radio.enableDynamicAck(); // required for multicast to work
_radio.setRetries(1, 5); // Retry time=1*250+250us=500us, count=5.
// Set to listen on the address 255
_address[0] = 255;
_radio.openReadingPipe(1, _address);
// Also allow receives on own node address
_address[0] = _thisNode;
_radio.openReadingPipe(2, _address);
_radio.startListening();
_display();
_deviceState = DEVSTATE_NORMAL;
} else {
// Error in initialising
DIAG(F("nRF24L01 Failed to initialise"));
_deviceState = DEVSTATE_FAILED;
}
_lastMulticastTime = _lastExecutionTime = micros();
}
// _read function - just return _value (updated in _loop when message received from remote node)
int _read(VPIN vpin) override {
int pin = vpin - _firstVpin;
uint8_t mask = 1 << (pin & 7);
int byteIndex = pin / 8;
return (_pinValues[byteIndex] & mask) ? 1 : 0;
}
// _write (digital) - send command directly to the appropriate remote node.
void _write(VPIN vpin, int value) override {
// Send message
int pin = vpin - _firstVpin;
uint8_t node = GETFLASH(&_pinDefs[pin].node);
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
VPIN remoteVpin = GETFLASHW(&_pinDefs[pin].vpin);
if (node != _thisNode && remoteVpin != VPIN_NONE && (flags & RPIN_OUT)) {
#ifdef DIAG_IO
DIAG(F("RF24: write(%d,%d)=>send(%d,\"write(%d,%d)\")"), vpin, value, node, remoteVpin, value);
#endif
outBuffer[0] = node;
outBuffer[1] = NET_CMD_WRITE;
outBuffer[2] = getMsb(remoteVpin);
outBuffer[3] = getLsb(remoteVpin);
outBuffer[4] = (uint8_t)value;
// Set up to send to the specified node address
sendCommand(node, outBuffer, 5);
}
}
// _writeAnalogue - send command directly to the appropriate remote node.
void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override {
// Send message
int pin = vpin - _firstVpin;
uint8_t node = GETFLASH(&_pinDefs[pin].node);
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
VPIN remoteVpin = GETFLASHW(&_pinDefs[pin].vpin);
if (node != _thisNode && remoteVpin != VPIN_NONE && (flags & RPIN_OUT)) {
#ifdef DIAG_IO
DIAG(F("RF24: writeAnalogue(%d,%d,%d,%d)=>send(%d,\"writeAnalogue(%d,%d,...)\")"),
vpin, value, param1, param2, node, remoteVpin, value);
#endif
outBuffer[0] = node;
outBuffer[1] = NET_CMD_WRITEANALOGUE;
outBuffer[2] = getMsb(remoteVpin);
outBuffer[3] = getLsb(remoteVpin);
outBuffer[4] = getMsb(value);
outBuffer[5] = getLsb(value);
outBuffer[6] = param1;
outBuffer[7] = getMsb(param2);
outBuffer[8] = getLsb(param2);
// Set up to send to the specified node address
sendCommand(node, outBuffer, 9);
}
}
// _loop function - check for, and process, received data from RF24, and send any
// updates that are due.
void _loop(unsigned long currentMicros) override {
// Check for incoming data
if (_radio.available(NULL))
processReceivedData();
// Force a data update broadcast every 500ms irrespective of whether there are
// data changes or not.
if (currentMicros - _lastMulticastTime > (500 * 1000UL))
_changesPending = true;
// Send out data update broadcasts once every 100ms if there are changes
if (currentMicros - _lastExecutionTime > (100 * 1000UL)) {
// Broadcast updates to all other nodes. The preparation is done in a number of
// successive calls, and when sendSensorUpdates() returns true it has completed.
if (sendSensorUpdates()) {
_lastExecutionTime = currentMicros; // Send complete, wait another 100ms
}
}
// Check if outstanding writes have completed. If so, move to Standby-I mode
// and enable the receiver.
if (_sendInProgress && _radio.isWriteFinished()) {
_sendInProgress = false;
_radio.txStandBy();
_radio.startListening();
}
}
void _display() override {
DIAG(F("nRF24L01 Configured on Vpin:%d-%d CEPin:%d CSNPin:%d"),
_firstVpin, _firstVpin+_nPins-1, _cePin, _csnPin);
}
private:
// Send sensor updates only if one or more locally sourced inputs that
// are mapped to remote VPINs have changed state.
//
bool sendSensorUpdates() {
// This loop is split into multiple loop() entries, so as not to hog
// the cpu for too long. Otherwise it could take over 2700us with 108 remote
// pins configured, for example. So we do just 5 pins per call.
// We could make digital state change notification mandatory, which would
// allow us to remove the loop altogether!
if (_numPinsToSend == 0) return true; // No pins to send from this node.
// Update the _pinValues bitfield to reflect the current values of local pins.
uint8_t count = 5;
bool state;
for (int pin=_nextSendPin; pin<_firstPinToSend+_numPinsToSend; pin++) {
uint8_t flags = GETFLASH(&_pinDefs[pin].flags);
if ((flags & RPIN_IN) && GETFLASH(&_pinDefs[pin].node) == _thisNode) {
// Local input pin, read and update current state of input
VPIN localVpin = GETFLASHW(&_pinDefs[pin].vpin);
if (localVpin != VPIN_NONE) {
state = IODevice::read(localVpin);
uint16_t byteIndex = pin / 8;
uint8_t bitMask = 1 << (pin & 7);
uint8_t byteValue = _pinValues[byteIndex];
bool oldState = byteValue & bitMask;
if (state != oldState) {
// Store state in remote values array
if (state)
byteValue |= bitMask;
else
byteValue &= ~bitMask;
_pinValues[byteIndex] = byteValue;
_changesPending = true;
//DIAG(F("RF24 VPIN:%d Val:%d"), _firstVpin+pin, state);
}
if (--count == 0) {
// Done enough checks for this entry, resume on next one.
_nextSendPin = pin+1;
return false;
}
}
}
}
if (_changesPending) {
// On master and on slave, send pin states to other nodes
outBuffer[0] = _thisNode; // Originating node
outBuffer[1] = NET_CMD_VALUEUPDATE;
// The packet size is 32 bytes, header is 4 bytes, so 28 bytes of data.
// We can therefore send up to 224 binary states per packet.
int byteCount = _numPinsToSend/8+1;
VPIN remoteVpin = _firstVpin+_firstPinToSend;
outBuffer[2] = getMsb(remoteVpin);
outBuffer[3] = getLsb(remoteVpin);
// Copy from pinValues array into buffer. This is why _firstPinToSend must be a multiple of 8.
memcpy(&outBuffer[4], &_pinValues[_firstPinToSend/8], byteCount);
// Broadcast update
sendCommand(255, outBuffer, byteCount + 4);
//DIAG(F("Sent %d bytes: %x %x ..."), byteCount, outBuffer[4], outBuffer[5]);
_lastMulticastTime = micros();
_changesPending = false;
}
// Set next pin ready for next entry.
_nextSendPin = _firstPinToSend;
return true; // Done all we need to for this cycle.
}
// Read next packet from the device's input buffers. Decode the message,
// and take the appropriate action.
// The packet may be a command to do an output write (digital or analogue), or
// it may be an update for digital input signals.
// For digital input signals, the values are propagated from the node that is
// the pin source, via the master, to all the other nodes.
void processReceivedData() {
// Read received data from input pipe
byte size = _radio.getDynamicPayloadSize();
// if (size > maxPayloadSize) return; // Packet too long to read!!
// Read packet
_radio.read(inBuffer, size);
// Extract command type from packet.
uint8_t command = inBuffer[1];
// Process received data
switch (command) {
case NET_CMD_WRITE: // Digital write command
{
uint8_t targetNode = inBuffer[0];
if (targetNode == _thisNode) {
VPIN vpin = makeWord(inBuffer[2], inBuffer[3]);
uint8_t state = inBuffer[4];
IODevice::write(vpin, state);
} else {
sendCommand(targetNode, inBuffer, size);
}
}
break;
case NET_CMD_WRITEANALOGUE: // Analogue write command
{
uint8_t targetNode = inBuffer[0];
if (targetNode == _thisNode) {
VPIN vpin = makeWord(inBuffer[2], inBuffer[3]);
int value = makeWord(inBuffer[4], inBuffer[5]);
uint8_t param1 = inBuffer[6];
uint16_t param2 = makeWord(inBuffer[7], inBuffer[8]);
IODevice::writeAnalogue(vpin, value, param1, param2);
// Set the local value for the pin, used by isBusy(),
// and subsequently updated by the remote node.
_pinValues[vpin-_firstVpin] = true;
} else {
sendCommand(targetNode, inBuffer, size);
}
}
break;
case NET_CMD_VALUEUPDATE: // Updates of input states (sensors etc).
{
uint8_t sendingNode = inBuffer[0];
//DIAG(F("Node %d Rx %x"), sendingNode, inBuffer[4]);
VPIN vpin = makeWord(inBuffer[2], inBuffer[3]);
// Read through the buffer one byte at a time.
uint8_t *buffPtr = &inBuffer[4];
uint8_t *bitFieldPtr = &_pinValues[(vpin-_firstVpin)/8];
int currentPin = vpin - _firstVpin;
for (int byteNo=0; byteNo<size-4 && currentPin<_nPins; byteNo++) {
// Now work through the byte examining each bit.
uint8_t byteValue = *buffPtr++;
uint8_t bitMask = 1;
for (int bitNo=0; bitNo<8 && currentPin<_nPins; bitNo++) {
// Process incoming value if it's come from the pin source node
uint8_t pinSource = GETFLASH(&_pinDefs[currentPin].node);
if (sendingNode == pinSource) {
if (byteValue & bitMask)
byteValue |= bitMask;
else
byteValue &= ~bitMask;
// if (pinNode == _thisNode) { // Local pin }
}
bitMask <<= 1;
currentPin++;
}
// Store the modified byte back
*bitFieldPtr++ = byteValue;
}
}
break;
default:
break;
}
}
// Wrapper functions for RF24 send functions. If node=255, then
// the packet is to be sent as a multicast without acknowledgements.
// The multicast message takes ~400us. A further 260us is required to turn
// the receiver off and on for the transmission, totalling 660us.
// If the node is not 255, then the packet will be sent directly to the
// addressed node, with acknowledgement requested. If no acknowledgement is
// received, then the device will retry up to the defined maximum number of
// retries. This will take longer than a multicast. For example, with
// setRetries(1,3) the timeout is 500us and a maximum of 3 retries are
// carried out, so the operation will take as much as 2.26 milliseconds if
// the node in question is not responding, and as little as 890us if the
// ack is received immediately (including turning receiver on/off).
//
bool sendCommand(uint8_t node, uint8_t *buffer, uint8_t len) {
_address[0] = node;
_radio.openWritingPipe(_address);
// We have to stop the receiver before we can transmit.
_radio.stopListening();
// Copy the message into the radio and start the transmitter.
// Multicast (no ack expected) if destination node is 255.
bool ok = _radio.writeFast(buffer, len, (node==255));
// We will poll the radio later on to see when the transmit queue
// has emptied. When that happens, we will go back to receive mode.
// This prevents txStandBy() from blocking while the transmission
// is in progress.
_sendInProgress = true;;
return ok;
}
// Helper functions for packing/unpacking buffers.
inline uint16_t makeWord(uint8_t msb, uint8_t lsb) {
return ((uint16_t)msb << 8) | lsb;
}
inline uint8_t getMsb(uint16_t w) {
return w >> 8;
}
inline uint8_t getLsb(uint16_t w) {
return w & 0xff;
}
// Data space for actual input and output buffers.
uint8_t inBuffer[maxPayloadSize];
uint8_t outBuffer[maxPayloadSize];
};
#endif //IO_RF24Net4_H

232
IO_S88.h Normal file
View File

@@ -0,0 +1,232 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The S88 Bus is essentially a shift register consisting of one or more
* S88 modules, connected one to another in one long chain. The register
* is read by the following steps:
* 1) The LOAD/PS line goes to HIGH, then the CLK line is pulsed HIGH. This
* tells the registers to acquire data from the latched parallel inputs. 2) With
* LOAD/PS still high, the RESET signal is pulsed HIGH, which clears the
* parallel input upstream latches. 3) With LOAD/PS LOW, the shift is performed
* by pulsing the CLK line high. On each pulse, the data in the shift register
* is presented, bit by bit, to the DATA-IN line.
*
* Example configuration in mySetup.cpp:
* #include "IO_S88.h"
* void mySetup() {
* S88bus::create(2500, 16, 40,41,42,43, 10);
* }
*
* This creates an S88 bus instance with 16 inputs (VPINs 2500-2515).
* The LOAD pin is 40, RESET is 41, CLK is 42 and DATA is 43. These four pins
* must be local GPIO pins (not on an I/O expander).
* The last parameter (bitTime=10) means that at least 10us is allowed for
* reading each bit in the shift register.
*
* Depending on the S88 modules being used and the cabling, the timing of the
* interface may have to be adjusted. This is done by the bit time parameter.
* The original S88 concept was based on 4014 shift register devices, which are
* easily capable of moderate speed transfers and would cope with a bit time
* around 2us. However, commercial S88 devices appear to be extremely slow in
* comparison and may need the order of tens or hundreds of microseconds. The
* symptom of a bit time that is too small is that, when you activate one input to
* the S88 module, the command station sees something other than the correct
* activation; e.g. no activations, multiple activations, the wrong pin
* activating etc.
*
* _acquireCycleTime determines the minimum time between successive acquire
* cycles of the inputs on the S88 bus. Bear in mind that the Sensor code
* includes anti-bounce logic which means that fleeting state changes may be
* ignored, so reducing the acquireCycleTime may not have the desired effect.
* Also, if the combination of bit time and number of pins is large, the
* specified cycle time may not be achieved.
*
*/
#ifndef IO_S88_H
#define IO_S88_H
#include "IODevice.h"
class S88bus : public IODevice {
private:
uint8_t _loadPin;
uint8_t _resetPin;
uint8_t _clockPin;
uint8_t _dataInPin;
uint8_t *_values; // Bit array, initialised in _begin method.
int _currentByteIndex;
unsigned long _lastAquireCycleStart;
uint16_t _pulseDelayTime; // Delay microsecs; can be tuned for the S88 hardware
uint16_t _shortDelayTime;
uint8_t _bitIndex;
unsigned long _lastPulseTime;
uint8_t _state;
uint8_t _maxBitsPerEntry;
// The acquire cycle time is a target maximum rate. If there are a lot of signals or the
// bit time is long, then the cycle time may be longer.
const unsigned long _acquireCycleTime = 20000; // target 20 milliseconds between acquire cycles
public:
S88bus(VPIN firstVpin, int nPins, uint8_t loadPin, uint8_t resetPin, uint8_t clockPin, uint8_t dataInPin, uint16_t bitTime) :
IODevice(firstVpin, nPins),
_loadPin(loadPin),
_resetPin(resetPin),
_clockPin(clockPin),
_dataInPin(dataInPin),
_currentByteIndex(0),
_lastAquireCycleStart(0),
_pulseDelayTime((bitTime+1)/2)
{
// Allocate memory for input values.
_values = (uint8_t *)calloc((nPins+7)/8, 1);
_shortDelayTime = (_pulseDelayTime > 10) ? 10 : _pulseDelayTime;
_state = 0;
// The program typically manages 30 microseconds per clock cycle
// with no waiting, so limit the number of bits so that loop doesn't
// take much more than 200us.
_maxBitsPerEntry = 200 / (30+bitTime) + 1;
addDevice(this);
}
static void create(VPIN firstVpin, int nPins, uint8_t loadPin, uint8_t resetPin, uint8_t clockPin, uint8_t dataInPin, uint16_t bitTime) {
new S88bus(firstVpin, nPins, loadPin, resetPin, clockPin, dataInPin, bitTime);
}
protected:
void _begin() override {
pinMode(_loadPin, OUTPUT);
pinMode(_resetPin, OUTPUT);
pinMode(_clockPin, OUTPUT);
pinMode(_dataInPin, INPUT);
#ifdef DIAG_IO
_display();
#endif
}
// Read method returns the latest aquired value for the nominated VPIN number.
int _read(VPIN vpin) override {
uint16_t pin = vpin - _firstVpin;
uint8_t mask = 1 << (pin % 8);
uint16_t byteIndex = pin / 8;
return (_values[byteIndex] & mask) ? 1 : 0;
}
// Loop method acquires the input states from the shift register.
// At the beginning of each acquisition cycle, instruct the bus registers to acquire the
// input states from the latches, then reset the latches. On
// subsequent loop entries, some of the input states are shifted from the
// registers, until they have all been read. Then the whole process
// resumes for the next acquisition cycle. The operations are spread over consecutive
// loop entries to restrict the amount of time taken in each entry.
void _loop(unsigned long currentMicros) override {
// If just starting a new read, then latch the input values into the S88
// registers. The active edge is the rising edge in each case, so we
// can use shorter delays for some transitions.
switch (_state) {
case 0: // Starting cycle. Set up LOAD and CLOCK pins.
_lastAquireCycleStart = currentMicros;
// Set LOAD pin
ArduinoPins::fastWriteDigital(_loadPin, HIGH);
_lastPulseTime = micros();
pulseDelay(_shortDelayTime);
// Pulse CLOCK pin to read inputs into registers
ArduinoPins::fastWriteDigital(_clockPin, HIGH);
// Clear CLOCK, and set up RESET pin.
pulseDelay(_pulseDelayTime);
ArduinoPins::fastWriteDigital(_clockPin, LOW);
pulseDelay(_shortDelayTime);
// Pulse RESET pin to clear inputs ready for next acquisition period
ArduinoPins::fastWriteDigital(_resetPin, HIGH);
_state = 1;
delayUntil(_lastPulseTime + _pulseDelayTime);
return;
case 1: // Clear RESET and LOAD
ArduinoPins::fastWriteDigital(_resetPin, LOW);
pulseDelay(_shortDelayTime);
ArduinoPins::fastWriteDigital(_loadPin, LOW);
// Initialise variables used in reading bits.
_currentByteIndex = _bitIndex = 0;
_state = 2;
/* fallthrough */
case 2:
// Subsequent loop entries, read each bit in turn from the shiftregister.
uint8_t bitCount = 0;
while (true) {
uint8_t mask = (1 << _bitIndex);
bool newValue = ArduinoPins::fastReadDigital(_dataInPin);
#ifdef DIAG_IO
bool oldValue = _values[_currentByteIndex] & mask;
if (newValue != oldValue) DIAG(F("S88 VPIN:%d Value:%d"),
_firstVpin+_currentByteIndex*8+_bitIndex, newValue);
#endif
if (newValue)
_values[_currentByteIndex] |= mask;
else
_values[_currentByteIndex] &= ~mask;
if (++_bitIndex == 8) {
// Byte completed, so move to next one.
_currentByteIndex++;
_bitIndex = 0;
}
// Check if this cycle is complete.
if (_currentByteIndex*8 + _bitIndex >= _nPins) {
// All bits in the shift register have been read now, so
// don't read again until next acquisition cycle time
delayUntil(_lastAquireCycleStart + _acquireCycleTime);
_state = 0;
return;
}
// Clock next bit in.
pulseDelay(_pulseDelayTime);
ArduinoPins::fastWriteDigital(_clockPin, HIGH);
pulseDelay(_pulseDelayTime);
ArduinoPins::fastWriteDigital(_clockPin, LOW);
// See if we've done all we're allowed on this entry
if (++bitCount >= _maxBitsPerEntry) {
delayUntil(_lastPulseTime + _pulseDelayTime);
return;
}
}
}
}
void _display() override {
DIAG(F("S88bus Configured on Vpins %d-%d, LOAD=%d RESET=%d CLK=%d DATAIN=%d"),
_firstVpin, _firstVpin+_nPins-1, _loadPin, _resetPin, _clockPin, _dataInPin);
}
// Helper function to delay until a minimum number of microseconds have elapsed
// since _lastPulseTime.
void pulseDelay(uint16_t duration) {
delayMicroseconds(duration);
_lastPulseTime = micros();
}
};
#endif

299
IO_VL53L0X.h Normal file
View File

@@ -0,0 +1,299 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The VL53L0X Time-Of-Flight sensor operates by sending a short laser pulse and detecting
* the reflection of the pulse. The time between the pulse and the receipt of reflections
* is measured and used to determine the distance to the reflecting object.
*
* For economy of memory and processing time, this driver includes only part of the code
* that ST provide in their API. Also, the API code isn't very clear and it is not easy
* to identify what operations are useful and what are not.
* The operation shown here doesn't include any calibration, so is probably not as accurate
* as using the full driver, but it's probably accurate enough for the purpose.
*
* The device driver allocates up to 3 vpins to the device. A digital read on the first pin
* will return a value that indicates whether the object is within the threshold range (1)
* or not (0). An analogue read on the first pin returns the last measured distance (in mm),
* the second pin returns the signal strength, and the third pin returns detected
* ambient light level. By default the device takes around 60ms to complete a ranging
* operation, so we do a 100ms cycle (10 samples per second).
*
* The VL53L0X is initially set to respond to I2C address 0x29. If you only have one module,
* you can use this address. However, the address can be modified by software. If
* you select another address, that address will be written to the device and used until the device is reset.
*
* If you have more than one module, then you will need to specify a digital VPIN (Arduino
* digital output or I/O extender pin) which you connect to the module's XSHUT pin. Now,
* when the device driver starts, the XSHUT pin is set LOW to turn the module off. Once
* all VL53L0X modules are turned off, the driver works through each module in turn by
* setting XSHUT to HIGH to turn the module on,, then writing the module's desired I2C address.
* In this way, many VL53L0X modules can be connected to the one I2C bus, each one
* using a distinct I2C address.
*
* WARNING: If the device's XSHUT pin is not connected, then it is very prone to noise,
* and the device may even reset when handled. If you're not using XSHUT, then it's
* best to tie it to +5V.
*
* The driver is configured as follows:
*
* Single VL53L0X module:
* VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold);
* Where firstVpin is the first vpin reserved for reading the device,
* nPins is 1, 2 or 3,
* i2cAddress is the address of the device (normally 0x29),
* lowThreshold is the distance at which the digital vpin state is set to 1 (in mm),
* and highThreshold is the distance at which the digital vpin state is set to 0 (in mm).
*
* Multiple VL53L0X modules:
* VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold, xshutPin);
* ...
* Where firstVpin is the first vpin reserved for reading the device,
* nPins is 1, 2 or 3,
* i2cAddress is the address of the device (any valid address except 0x29),
* lowThreshold is the distance at which the digital vpin state is set to 1 (in mm),
* highThreshold is the distance at which the digital vpin state is set to 0 (in mm),
* and xshutPin is the VPIN number corresponding to a digital output that is connected to the
* XSHUT terminal on the module.
*
* Example:
* In mySetup function within mySetup.cpp:
* VL53L0X::create(4000, 3, 0x29, 200, 250);
* Sensor::create(4000, 4000, 0); // Create a sensor
*
* When an object comes within 200mm of the sensor, a message
* <Q 4000>
* will be sent over the serial USB, and when the object moves more than 250mm from the sensor,
* a message
* <q 4000>
* will be sent.
*
*/
#ifndef IO_VL53L0X_h
#define IO_VL53L0X_h
#include "IODevice.h"
class VL53L0X : public IODevice {
private:
uint8_t _i2cAddress;
uint16_t _ambient;
uint16_t _distance;
uint16_t _signal;
uint16_t _onThreshold;
uint16_t _offThreshold;
VPIN _xshutPin;
bool _value;
uint8_t _nextState = 0;
I2CRB _rb;
uint8_t _inBuffer[12];
uint8_t _outBuffer[2];
// State machine states.
enum : uint8_t {
STATE_INIT = 0,
STATE_CONFIGUREADDRESS = 1,
STATE_SKIP = 2,
STATE_CONFIGUREDEVICE = 3,
STATE_INITIATESCAN = 4,
STATE_CHECKSTATUS = 5,
STATE_GETRESULTS = 6,
STATE_DECODERESULTS = 7,
};
// Register addresses
enum : uint8_t {
VL53L0X_REG_SYSRANGE_START=0x00,
VL53L0X_REG_RESULT_INTERRUPT_STATUS=0x13,
VL53L0X_REG_RESULT_RANGE_STATUS=0x14,
VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV=0x89,
VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS=0x8A,
};
const uint8_t VL53L0X_I2C_DEFAULT_ADDRESS=0x29;
public:
VL53L0X(VPIN firstVpin, int nPins, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {
_firstVpin = firstVpin;
_nPins = min(nPins, 3);
_i2cAddress = i2cAddress;
_onThreshold = onThreshold;
_offThreshold = offThreshold;
_xshutPin = xshutPin;
_value = 0;
addDevice(this);
}
static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {
new VL53L0X(firstVpin, nPins, i2cAddress, onThreshold, offThreshold, xshutPin);
}
protected:
void _begin() override {
if (_xshutPin == VPIN_NONE) {
// Check if device is already responding on the nominated address.
if (I2CManager.exists(_i2cAddress)) {
// Yes, it's already on this address, so skip the address initialisation.
_nextState = STATE_CONFIGUREDEVICE;
} else {
_nextState = STATE_INIT;
}
}
}
void _loop(unsigned long currentMicros) override {
uint8_t status;
switch (_nextState) {
case STATE_INIT:
// On first entry to loop, reset this module by pulling XSHUT low. All modules
// will be reset in turn.
if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 0);
_nextState = STATE_CONFIGUREADDRESS;
break;
case STATE_CONFIGUREADDRESS:
// On second entry, set XSHUT pin high to allow the module to restart.
// On the module, there is a diode in series with the XSHUT pin to
// protect the low-voltage pin against +5V.
if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 1);
// Allow the module time to restart
delay(10);
// Then write the desired I2C address to the device, while this is the only
// module responding to the default address.
I2CManager.write(VL53L0X_I2C_DEFAULT_ADDRESS, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _i2cAddress);
_nextState = STATE_SKIP;
break;
case STATE_SKIP:
// Do nothing on the third entry.
_nextState = STATE_CONFIGUREDEVICE;
break;
case STATE_CONFIGUREDEVICE:
// On next entry, check if device address has been set.
if (I2CManager.exists(_i2cAddress)) {
#ifdef DIAG_IO
_display();
#endif
// Set 2.8V mode
write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV,
read_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV) | 0x01);
} else {
DIAG(F("VL53L0X I2C:x%x device not responding"), _i2cAddress);
_deviceState = DEVSTATE_FAILED;
}
_nextState = STATE_INITIATESCAN;
break;
case STATE_INITIATESCAN:
// Not scanning, so initiate a scan
_outBuffer[0] = VL53L0X_REG_SYSRANGE_START;
_outBuffer[1] = 0x01;
I2CManager.write(_i2cAddress, _outBuffer, 2, &_rb);
_nextState = STATE_CHECKSTATUS;
break;
case STATE_CHECKSTATUS:
status = _rb.status;
if (status == I2C_STATUS_PENDING) return; // try next time
if (status != I2C_STATUS_OK) {
DIAG(F("VL53L0X I2C:x%x Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status));
_deviceState = DEVSTATE_FAILED;
_value = false;
} else
_nextState = 2;
delayUntil(currentMicros + 95000); // wait for 95 ms before checking.
_nextState = STATE_GETRESULTS;
break;
case STATE_GETRESULTS:
// Ranging completed. Request results
_outBuffer[0] = VL53L0X_REG_RESULT_RANGE_STATUS;
I2CManager.read(_i2cAddress, _inBuffer, 12, _outBuffer, 1, &_rb);
_nextState = 3;
delayUntil(currentMicros + 5000); // Allow 5ms to get data
_nextState = STATE_DECODERESULTS;
break;
case STATE_DECODERESULTS:
// If I2C write still busy, return.
status = _rb.status;
if (status == I2C_STATUS_PENDING) return; // try again next time
if (status == I2C_STATUS_OK) {
if (!(_inBuffer[0] & 1)) return; // device still busy
uint8_t deviceRangeStatus = ((_inBuffer[0] & 0x78) >> 3);
if (deviceRangeStatus == 0x0b) {
// Range status OK, so use data
_ambient = makeuint16(_inBuffer[7], _inBuffer[6]);
_signal = makeuint16(_inBuffer[9], _inBuffer[8]);
_distance = makeuint16(_inBuffer[11], _inBuffer[10]);
if (_distance <= _onThreshold)
_value = true;
else if (_distance > _offThreshold)
_value = false;
}
}
// Completed. Restart scan on next loop entry.
_nextState = STATE_INITIATESCAN;
break;
default:
break;
}
}
// For analogue read, first pin returns distance, second pin is signal strength, and third is ambient level.
int _readAnalogue(VPIN vpin) override {
int pin = vpin - _firstVpin;
switch (pin) {
case 0:
return _distance;
case 1:
return _signal;
case 2:
return _ambient;
default:
return -1;
}
}
// For digital read, return zero for all but first pin.
int _read(VPIN vpin) override {
if (vpin == _firstVpin)
return _value;
else
return 0;
}
void _display() override {
DIAG(F("VL53L0X I2C:x%x Configured on Vpins:%d-%d On:%dmm Off:%dmm %S"),
_i2cAddress, _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold,
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
private:
inline uint16_t makeuint16(byte lsb, byte msb) {
return (((uint16_t)msb) << 8) | lsb;
}
uint8_t write_reg(uint8_t reg, uint8_t data) {
// write byte to register
uint8_t outBuffer[2];
outBuffer[0] = reg;
outBuffer[1] = data;
return I2CManager.write(_i2cAddress, outBuffer, 2);
}
uint8_t read_reg(uint8_t reg) {
// read byte from register and return value
I2CManager.read(_i2cAddress, _inBuffer, 1, &reg, 1);
return _inBuffer[0];
}
};
#endif // IO_VL53L0X_h

View File

@@ -73,6 +73,11 @@ void LCDDisplay::loop() {
LCDDisplay *LCDDisplay::loop2(bool force) {
if (!lcdDisplay) return NULL;
// If output device is busy, don't do anything on this loop
// This avoids blocking while waiting for the device to complete.
if (isBusy()) return NULL;
unsigned long currentMillis = millis();
if (!force) {
@@ -159,4 +164,4 @@ void LCDDisplay::moveToNextRow() {
void LCDDisplay::skipBlankRows() {
while (!done && rowBuffer[rowNext][0] == 0)
moveToNextRow();
}
}

View File

@@ -19,53 +19,54 @@
#ifndef LCDDisplay_h
#define LCDDisplay_h
#include <Arduino.h>
#if __has_include ( "config.h")
#include "config.h"
#endif
#include "defines.h"
#include "DisplayInterface.h"
// Allow maximum message length to be overridden from config.h
#if !defined(MAX_MSG_SIZE)
#define MAX_MSG_SIZE 16
#define MAX_MSG_SIZE 20
#endif
// Set default scroll mode (overridable in config.h)
#if !defined(SCROLLMODE)
#define SCROLLMODE 1
#endif
// This class is created in LCDisplay_Implementation.h
class LCDDisplay : public Print {
class LCDDisplay : public DisplayInterface {
public:
LCDDisplay() {};
static const int MAX_LCD_ROWS = 8;
static const int MAX_LCD_COLS = MAX_MSG_SIZE;
static const long LCD_SCROLL_TIME = 3000; // 3 seconds
static LCDDisplay* lcdDisplay;
LCDDisplay();
void interfake(int p1, int p2, int p3);
// Internally handled functions
static void loop();
LCDDisplay* loop2(bool force);
void setRow(byte line);
void clear();
LCDDisplay* loop2(bool force) override;
void setRow(byte line) override;
void clear() override;
virtual size_t write(uint8_t b);
using Print::write;
size_t write(uint8_t b) override;
protected:
uint8_t lcdRows;
uint8_t lcdCols;
private:
void moveToNextRow();
void skipBlankRows();
// Relay functions to the live driver
void clearNative();
void displayNative();
void setRowNative(byte line);
void writeNative(char b);
// Relay functions to the live driver in the subclass
virtual void clearNative() = 0;
virtual void setRowNative(byte line) = 0;
virtual size_t writeNative(uint8_t b) = 0;
virtual bool isBusy() = 0;
unsigned long lastScrollTime = 0;
int8_t hotRow = 0;
int8_t hotCol = 0;
int8_t topRow = 0;
uint8_t lcdRows;
uint8_t lcdCols;
int8_t slot = 0;
int8_t rowFirst = -1;
int8_t rowNext = 0;

View File

@@ -27,29 +27,27 @@
#ifndef LCD_Implementation_h
#define LCD_Implementation_h
#include <Wire.h>
#include "LCDDisplay.h"
#include "SSD1306Ascii.h"
#include "LiquidCrystal_I2C.h"
LCDDisplay * LCDDisplay::lcdDisplay=0;
// Implement the LCDDisplay shim class as a singleton.
// Notice that the LCDDisplay class declaration (LCDDisplay.h) is independent of the library
// but the implementation is compiled here with dependencies on LCDDriver which is
// specific to the library in use.
// Thats the workaround to the drivers not all implementing a common interface.
#if defined(OLED_DRIVER)
#include "LCD_OLED.h"
#define CONDITIONAL_LCD_START for (LCDDisplay * dummy=new LCDDisplay();dummy!=NULL; dummy=dummy->loop2(true))
// The DisplayInterface class implements a displayy handler with no code (null device);
// The LCDDisplay class sub-classes DisplayInterface to provide the common display code;
// Then LCDDisplay class is subclassed to the specific device type classes:
// SSD1306AsciiWire for I2C OLED driver with SSD1306 or SH1106 controllers;
// LiquidCrystal_I2C for I2C LCD driver for HD44780 with PCF8574 'backpack'.
#if defined(OLED_DRIVER)
#define CONDITIONAL_LCD_START for (DisplayInterface * dummy=new SSD1306AsciiWire(OLED_DRIVER);dummy!=NULL; dummy=dummy->loop2(true))
#elif defined(LCD_DRIVER)
#define CONDITIONAL_LCD_START for (DisplayInterface * dummy=new LiquidCrystal_I2C(LCD_DRIVER);dummy!=NULL; dummy=dummy->loop2(true))
#elif defined(LCD_DRIVER)
#include "LCD_LCD.h"
#define CONDITIONAL_LCD_START for (LCDDisplay * dummy=new LCDDisplay();dummy!=NULL; dummy=dummy->loop2(true))
#else
#include "LCD_NONE.h"
#define CONDITIONAL_LCD_START if (true) /* NO LCD CONFIG, but do the LCD macros to get DIAGS */
#else
// Create null display handler just in case someone calls lcdDisplay->something without checking if lcdDisplay is NULL!
#define CONDITIONAL_LCD_START { new DisplayInterface(); }
#endif
#endif // LCD_Implementation_h

View File

@@ -1,73 +0,0 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// OLED Implementation of LCDDisplay class
// Note: this file is optionally included by LCD_Implementation.h
// It is NOT a .cpp file to prevent it being compiled and demanding libraries
// even when not needed.
#include "I2CManager.h"
#include "SSD1306Ascii.h"
SSD1306AsciiWire LCDDriver;
// DEVICE SPECIFIC LCDDisplay Implementation for OLED
LCDDisplay::LCDDisplay() {
// Scan for device on 0x3c and 0x3d.
I2CManager.begin();
I2CManager.setClock(400000L); // Set max supported I2C speed
for (byte address = 0x3c; address <= 0x3d; address++) {
if (I2CManager.exists(address)) {
// Device found
DIAG(F("OLED display found at 0x%x"), address);
interfake(OLED_DRIVER, 0);
const DevType *devType;
if (lcdCols == 132)
devType = &SH1106_128x64; // Actually 132x64 but treated as 128x64
else if (lcdCols == 128 && lcdRows == 4)
devType = &Adafruit128x32;
else
devType = &Adafruit128x64;
LCDDriver.begin(devType, address);
lcdDisplay = this;
LCDDriver.setFont(System5x7); // Normal 1:1 pixel scale, 8 bits high
clear();
return;
}
}
DIAG(F("OLED display not found"));
}
void LCDDisplay::interfake(int p1, int p2, int p3) {
lcdCols = p1;
lcdRows = p2 / 8;
(void)p3;
}
void LCDDisplay::clearNative() { LCDDriver.clear(); }
void LCDDisplay::setRowNative(byte row) {
// Positions text write to start of row 1..n
int y = row;
LCDDriver.setCursor(0, y);
}
void LCDDisplay::writeNative(char b) { LCDDriver.write(b); }
void LCDDisplay::displayNative() {}

10
LCN.cpp
View File

@@ -48,18 +48,16 @@ void LCN::loop() {
}
else if (ch == 't' || ch == 'T') { // Turnout opcodes
if (Diag::LCN) DIAG(F("LCN IN %d%c"),id,(char)ch);
Turnout * tt = Turnout::get(id);
if (!tt) Turnout::create(id, LCN_TURNOUT_ADDRESS, 0);
if (ch == 't') tt->data.tStatus |= STATUS_ACTIVE;
else tt->data.tStatus &= ~STATUS_ACTIVE;
if (!Turnout::exists(id)) LCNTurnout::create(id);
Turnout::setClosedStateOnly(id,ch=='t');
Turnout::turnoutlistHash++; // signals ED update of turnout data
id = 0;
}
else if (ch == 'S' || ch == 's') {
if (Diag::LCN) DIAG(F("LCN IN %d%c"),id,(char)ch);
Sensor * ss = Sensor::get(id);
if (!ss) ss = Sensor::create(id, 255,0); // impossible pin
ss->active = ch == 'S';
if (!ss) ss = Sensor::create(id, VPIN_NONE, 0); // impossible pin
ss->setState(ch == 'S');
id = 0;
}
else id = 0; // ignore any other garbage from LCN

18
LCN.h
View File

@@ -1,3 +1,21 @@
/*
* (c) 2021 Fred Decker. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef LCN_h
#define LCN_h
#include <Arduino.h>

View File

@@ -20,7 +20,7 @@
#include <Arduino.h>
#include "LiquidCrystal_I2C.h"
#include "I2CManager.h"
#include "DIAG.h"
// When the display powers up, it is configured as follows:
//
@@ -44,30 +44,30 @@
LiquidCrystal_I2C::LiquidCrystal_I2C(uint8_t lcd_Addr, uint8_t lcd_cols,
uint8_t lcd_rows) {
_Addr = lcd_Addr;
_cols = lcd_cols;
_rows = lcd_rows;
_backlightval = LCD_NOBACKLIGHT;
}
lcdRows = lcd_rows;
lcdCols = lcd_cols;
void LiquidCrystal_I2C::init() { init_priv(); }
_backlightval = 0;
void LiquidCrystal_I2C::init_priv() {
I2CManager.begin();
I2CManager.setClock(100000L); // PCF8574 is spec'd to 100kHz.
_displayfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS;
begin(_cols, _rows);
if (I2CManager.exists(lcd_Addr)) {
DIAG(F("%dx%d LCD configured on I2C:x%x"), (int)lcd_cols, (int)lcd_rows, (int)lcd_Addr);
_displayfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS;
begin();
backlight();
lcdDisplay = this;
}
}
void LiquidCrystal_I2C::begin(uint8_t cols, uint8_t lines) {
if (lines > 1) {
void LiquidCrystal_I2C::begin() {
if (lcdRows > 1) {
_displayfunction |= LCD_2LINE;
}
_numlines = lines;
(void)cols; // Suppress compiler warning.
// according to datasheet, we need at least 40ms after power rises above 2.7V
// before sending commands. Arduino can turn on way befer 4.5V so we'll allow
// before sending commands. Arduino can turn on way before 4.5V so we'll allow
// 100 milliseconds after pulling both RS and R/W and backlight pin low
expanderWrite(
_backlightval); // reset expander and turn backlight off (Bit 8 =1)
@@ -78,19 +78,19 @@ void LiquidCrystal_I2C::begin(uint8_t cols, uint8_t lines) {
// figure 24, pg 46
// we start in 8bit mode, try to set 4 bit mode
write4bits(0x03 << 4);
write4bits(0x03);
delayMicroseconds(4500); // wait min 4.1ms
// second try
write4bits(0x03 << 4);
write4bits(0x03);
delayMicroseconds(4500); // wait min 4.1ms
// third go!
write4bits(0x03 << 4);
write4bits(0x03);
delayMicroseconds(150);
// finally, set to 4-bit interface
write4bits(0x02 << 4);
write4bits(0x02);
// set # lines, font size, etc.
command(LCD_FUNCTIONSET | _displayfunction);
@@ -108,27 +108,21 @@ void LiquidCrystal_I2C::begin(uint8_t cols, uint8_t lines) {
// set the entry mode
command(LCD_ENTRYMODESET | _displaymode);
setCursor(0, 0);
setRowNative(0);
}
/********** high level commands, for the user! */
void LiquidCrystal_I2C::clear() {
void LiquidCrystal_I2C::clearNative() {
command(LCD_CLEARDISPLAY); // clear display, set cursor position to zero
delayMicroseconds(2000); // this command takes 1.52ms
}
void LiquidCrystal_I2C::setCursor(uint8_t col, uint8_t row) {
void LiquidCrystal_I2C::setRowNative(byte row) {
int row_offsets[] = {0x00, 0x40, 0x14, 0x54};
if (row > _numlines) {
row = _numlines - 1; // we count rows starting w/0
if (row >= lcdRows) {
row = lcdRows - 1; // we count rows starting w/0
}
command(LCD_SETDDRAMADDR | (col + row_offsets[row]));
}
// Turn the display on/off (quickly)
void LiquidCrystal_I2C::noDisplay() {
_displaycontrol &= ~LCD_DISPLAYON;
command(LCD_DISPLAYCONTROL | _displaycontrol);
command(LCD_SETDDRAMADDR | (row_offsets[row]));
}
void LiquidCrystal_I2C::display() {
@@ -138,7 +132,7 @@ void LiquidCrystal_I2C::display() {
// Turn the (optional) backlight off/on
void LiquidCrystal_I2C::noBacklight(void) {
_backlightval = LCD_NOBACKLIGHT;
_backlightval &= ~LCD_BACKLIGHT;
expanderWrite(0);
}
@@ -147,7 +141,7 @@ void LiquidCrystal_I2C::backlight(void) {
expanderWrite(0);
}
size_t LiquidCrystal_I2C::write(uint8_t value) {
size_t LiquidCrystal_I2C::writeNative(uint8_t value) {
send(value, Rs);
return 1;
}
@@ -194,25 +188,32 @@ inline void LiquidCrystal_I2C::command(uint8_t value) {
// a single I2C transmission.
void LiquidCrystal_I2C::send(uint8_t value, uint8_t mode) {
mode |= _backlightval;
uint8_t highnib = (value & 0xf0) | mode;
uint8_t lownib = ((value << 4) & 0xf0) | mode;
uint8_t highnib = (((value >> 4) & 0x0f) << BACKPACK_DATA_BITS) | mode;
uint8_t lownib = ((value & 0x0f) << BACKPACK_DATA_BITS) | mode;
// Send both nibbles
byte buffer[] = {(byte)(highnib|En), highnib, (byte)(lownib|En), lownib};
I2CManager.write(_Addr, buffer, sizeof(buffer));
uint8_t len = 0;
outputBuffer[len++] = highnib|En;
outputBuffer[len++] = highnib;
outputBuffer[len++] = lownib|En;
outputBuffer[len++] = lownib;
I2CManager.write(_Addr, outputBuffer, len); // Write command synchronously
}
// write 4 bits to the HD44780 LCD controller.
// write 4 data bits to the HD44780 LCD controller.
void LiquidCrystal_I2C::write4bits(uint8_t value) {
uint8_t _data = value | _backlightval;
uint8_t _data = ((value & 0x0f) << BACKPACK_DATA_BITS) | _backlightval;
// Enable must be set/reset for at least 450ns. This is well within the
// I2C clock cycle time of 2.5us at 400kHz. Data is clocked in to the
// HD44780 on the trailing edge of the Enable pin.
byte buffer[] = {(byte)(_data|En), _data};
I2CManager.write(_Addr, buffer, sizeof(buffer));
uint8_t len = 0;
outputBuffer[len++] = _data|En;
outputBuffer[len++] = _data;
I2CManager.write(_Addr, outputBuffer, len); // Write command synchronously
}
// write a byte to the PCF8574 I2C interface. We don't need to set
// the enable pin for this.
void LiquidCrystal_I2C::expanderWrite(uint8_t value) {
I2CManager.write(_Addr, 1, value | _backlightval);
outputBuffer[0] = value | _backlightval;
I2CManager.write(_Addr, outputBuffer, 1); // Write command synchronously
}

View File

@@ -22,13 +22,13 @@
#define LiquidCrystal_I2C_h
#include <Arduino.h>
#include "LCDDisplay.h"
#include "I2CManager.h"
// commands
#define LCD_CLEARDISPLAY 0x01
#define LCD_RETURNHOME 0x02
#define LCD_ENTRYMODESET 0x04
#define LCD_DISPLAYCONTROL 0x08
#define LCD_CURSORSHIFT 0x10
#define LCD_FUNCTIONSET 0x20
#define LCD_SETCGRAMADDR 0x40
#define LCD_SETDDRAMADDR 0x80
@@ -41,51 +41,42 @@
// flags for display on/off control
#define LCD_DISPLAYON 0x04
#define LCD_DISPLAYOFF 0x00
#define LCD_CURSORON 0x02
#define LCD_CURSOROFF 0x00
#define LCD_BLINKON 0x01
#define LCD_BLINKOFF 0x00
// flags for display/cursor shift
#define LCD_DISPLAYMOVE 0x08
#define LCD_CURSORMOVE 0x00
#define LCD_MOVERIGHT 0x04
#define LCD_MOVELEFT 0x00
// flags for function set
#define LCD_8BITMODE 0x10
#define LCD_4BITMODE 0x00
#define LCD_2LINE 0x08
#define LCD_1LINE 0x00
#define LCD_5x10DOTS 0x04
#define LCD_5x8DOTS 0x00
// flags for backlight control
#define LCD_BACKLIGHT 0x08
#define LCD_NOBACKLIGHT 0x00
// Bit mapping onto PCF8574 port
#define BACKPACK_Rs_BIT 0
#define BACKPACK_Rw_BIT 1
#define BACKPACK_En_BIT 2
#define BACKPACK_BACKLIGHT_BIT 3
#define BACKPACK_DATA_BITS 4 // Bits 4-7
// Equivalent mask bits
#define LCD_BACKLIGHT (1 << BACKPACK_BACKLIGHT_BIT) // Backlight enable
#define En (1 << BACKPACK_En_BIT) // Enable bit
#define Rw (1 << BACKPACK_Rw_BIT) // Read/Write bit
#define Rs (1 << BACKPACK_Rs_BIT) // Register select bit
#define En 0b00000100 // Enable bit
#define Rw 0b00000010 // Read/Write bit
#define Rs 0b00000001 // Register select bit
class LiquidCrystal_I2C : public Print {
class LiquidCrystal_I2C : public LCDDisplay {
public:
LiquidCrystal_I2C(uint8_t lcd_Addr,uint8_t lcd_cols,uint8_t lcd_rows);
void begin(uint8_t cols, uint8_t rows);
void clear();
void noDisplay();
void begin();
void clearNative() override;
void setRowNative(byte line) override;
size_t writeNative(uint8_t c) override;
void display();
void noBacklight();
void backlight();
void setCursor(uint8_t, uint8_t);
virtual size_t write(uint8_t);
void command(uint8_t);
void init();
private:
void init_priv();
void send(uint8_t, uint8_t);
void write4bits(uint8_t);
void expanderWrite(uint8_t);
@@ -93,10 +84,11 @@ private:
uint8_t _displayfunction;
uint8_t _displaycontrol;
uint8_t _displaymode;
uint8_t _numlines;
uint8_t _cols;
uint8_t _rows;
uint8_t _backlightval;
uint8_t outputBuffer[4];
// I/O is synchronous, so if this is called we're not busy!
bool isBusy() override { return false; }
};
#endif

View File

@@ -1,3 +1,24 @@
/*
* (c) 2020 Chris Harlow. All rights reserved.
* (c) 2021 Fred Decker. All rights reserved.
* (c) 2020 Harald Barth. All rights reserved.
* (c) 2020 Anthony W - Dayton. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef MotorDrivers_h
#define MotorDrivers_h
#include <Arduino.h>
@@ -61,5 +82,9 @@
#define IBT_2_WITH_ARDUINO F("IBT_2_WITH_ARDUINO_SHIELD"), \
new MotorDriver(4, 5, 6, UNUSED_PIN, A5, 41.54, 5000, UNUSED_PIN), \
new MotorDriver(11, 13, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 2000, UNUSED_PIN)
// YFROBOT Motor Shield (V3.1)
#define YFROBOT_MOTOR_SHIELD F("YFROBOT_MOTOR_SHIELD"), \
new MotorDriver(5, 4, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 2000, UNUSED_PIN), \
new MotorDriver(6, 7, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 2000, UNUSED_PIN)
#endif

149
Net_ENC28J60.h Normal file
View File

@@ -0,0 +1,149 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* ENC28J60 default mode of operation:
*
* The node number is used as the last byte of the MAC address
* field, purely so that the MAC address is unique for each distinct
* node number. The ENC28J60 driver doesn't implement ARP cache,
* since it would take up a minimum of 11 bytes per node and, with
* up to 254 nodes, this would take a significant part of the RAM.
* Instead, all transmissions are sent with MAC address allOnes, i.e.
* as a broadcast. Regular sensor state updates are broadcast anyway,
* and other write/writeAnalogue command packets are relatively
* infrequent.
*
* Usage:
* Net_ENC28J60 *encDriver = new Net_ENC28J60(10);
* Network<Net_ENC28J60>::create(4000, NUMREMOTEPINS(rpins), 1, rpins, encDriver);
*
* The ENC28J60 device has to be connected to the hardware MISO, MOSI, SCK and CS pins of the
* microcontroller. The CS pin is specified in the command above (e.g. 10).
* For details of the Network class configuration, see IO_Network.h.
*
*/
#ifndef NET_ENC28J60_H
#define NET_ENC28J60_H
#include "IO_Network.h"
#include "DIAG.h"
#include "EtherCard.h"
// The ethernet buffer is global to different protocol layers, to avoid almost
// all copying of data.
byte ENC28J60::buffer[74]; // Need space for 32 byte payload and 42 byte header.
class Net_ENC28J60 : public EtherCard {
private:
// pins must be arduino GPIO pins, not extender pins or HAL pins.
int _csPin;
// Number of the current node (1-254)
uint8_t _thisNode;
// 6-byte MAC address. Last byte will contain the node number (1-254).
byte _address[6] = {0xEE,0xCC,0xEE,0xEE,0xCC,0x00};
// 4-byte IP address. Last byte will contain the node number (1-254) or 255 for broadcast.
byte _ipAddress[4] = {192,168,1,0};
byte _gwAddress[4] = {192,168,1,254};
byte _netMask[4] = {255,255,255,0};
const uint16_t _port = 8900;
uint8_t _packetBytesAvailable;
public:
// Constructor performs static initialisation of the device object
Net_ENC28J60 (int csPin) {
_csPin = csPin;
_packetBytesAvailable = 0;
}
// Perform dynamic initialisation of the device
bool begin(uint8_t thisNode) {
_thisNode = thisNode;
_address[5] = _thisNode;
_ipAddress[3] = _thisNode;
if (ether.begin(sizeof(ENC28J60::buffer), _address, _csPin)) {
ether.staticSetup(_ipAddress, _gwAddress, 0, _netMask);
return true;
} else {
// Error in initialising
DIAG(F("ENC28J60 Failed to initialise"));
return false;
}
}
// The following function should be called regularly to handle incoming packets.
// Check if there is a received packet ready for reading.
bool available() {
uint16_t packetLen = ether.packetReceive();
if (packetLen > 0) {
// Packet received. First handle ICMP, ARP etc.
if (ether.packetLoop(packetLen)) {
// UDP packet to be handled. Check if it's our port number
byte *gbp = ENC28J60::buffer;
uint16_t destPort = (gbp[UDP_DST_PORT_H_P] << 8) + gbp[UDP_DST_PORT_L_P];
if (destPort == _port) {
// Yes, so mark that data is to be processed.
_packetBytesAvailable = packetLen;
return true;
}
}
}
_packetBytesAvailable = 0;
return false;
}
// Read packet from the ethernet buffer, and return the number of bytes
// that were read.
uint8_t read(uint8_t buffer[], uint8_t bufferSize) {
if (_packetBytesAvailable > 0) {
//DIAG(F("ReadPacket(%d)"), _packetBytesAvailable);
// Adjust length and pointer for UDP header
byte *gbp = ENC28J60::buffer;
int udpDataSize = (gbp[UDP_LEN_H_P] << 8) + gbp[UDP_LEN_L_P] - UDP_HEADER_LEN;
byte *udpFrame = &ENC28J60::buffer[UDP_DATA_P];
// Clear packet byte marker
_packetBytesAvailable = 0;
// Check if there's room for the data
if (bufferSize >= udpDataSize) {
memcpy(buffer, udpFrame, udpDataSize);
return udpDataSize;
}
}
return 0;
}
// Wrapper functions for ENC28J60 sendUdp function.
// The node parameter is either 1-254 (for specific nodes) or 255 (for broadcast).
// This aligns with the subnet broadcast IP address of "x.x.x.255".
bool sendCommand(uint8_t node, const uint8_t buffer[], uint8_t len) {
_ipAddress[3] = node;
ether.sendUdp((const char *)buffer, len, _port, _ipAddress, _port);
return true;
}
void loop() { }
};
#endif //NET_ENC28J60_H

131
Net_Ethernet.h Normal file
View File

@@ -0,0 +1,131 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Ethernet (W5100,W5200,W5500) default mode of operation:
*
* All transmissions are sent with MAC address allOnes, i.e.
* as a broadcast. Regular sensor state updates are broadcast anyway,
* and other write/writeAnalogue command packets are relatively
* infrequent.
*
* Usage:
* Net_Ethernet *etherDriver = new Net_Ethernet(10);
* Network<Net_Ethernet>::create(4000, NUMREMOTEPINS(rpins), 1, rpins, etherDriver);
*
* The W5x00 device has to be connected to the hardware MISO, MOSI, SCK and CS pins of the
* microcontroller. The CS pin is specified in the command above (e.g. 10).
* For details of the Network class configuration, see IO_Network.h.
*
*/
#ifndef NET_ETHERNET_H
#define NET_ETHERNET_H
#include "IO_Network.h"
#include "DIAG.h"
#include "Ethernet.h"
#include "EthernetUdp.h"
class Net_Ethernet {
private:
// pins must be arduino GPIO pins, not extender pins or HAL pins.
int _csPin;
// Number of the current node (1-254)
uint8_t _thisNode;
// 4-byte IP address. Last byte will contain the node number (1-254) or 255 for broadcast.
byte _ipAddress[4] = {192,168,1,0};
const uint16_t _port = 8900;
uint8_t _packetBytesAvailable;
EthernetUDP udp;
public:
// Constructor performs static initialisation of the device object
Net_Ethernet () {
_csPin = 10; // The Ethernet driver doesn't allow CS pin to be selected.
_packetBytesAvailable = 0;
}
// Perform dynamic initialisation of the device
bool begin(uint8_t thisNode) {
_thisNode = thisNode;
if (Ethernet.hardwareStatus() != EthernetNoHardware) {
// // Set IP address.
// _ipAddress[3] = thisNode;
// Ethernet.setLocalIP(_ipAddress);
// _ipAddress[3] = 254;
// Ethernet.setGatewayIP(_ipAddress);
// IPAddress mask = IPAddress(255,255,255,0);
// Ethernet.setSubnetMask(mask);
// Begin listening on receive port
udp.begin(_port);
return true;
} else {
DIAG(F("Ethernet (W5x00) no hardware found"));
return false;
}
}
// The following function should be called regularly to handle incoming packets.
// Check if there is a received packet ready for reading.
bool available() {
uint16_t packetLen = udp.parsePacket();
if (packetLen > 0) {
// Packet received.
_packetBytesAvailable = packetLen;
return true;
}
_packetBytesAvailable = 0;
return false;
}
// Read packet from the ethernet buffer, and return the number of bytes
// that were read.
uint8_t read(uint8_t buffer[], uint8_t bufferSize) {
uint8_t bytesReceived = _packetBytesAvailable;
// Clear packet byte marker
_packetBytesAvailable = 0;
if (bytesReceived > 0) {
//DIAG(F("ReadPacket(%d)"), bytesReceived);
// Check if there's room for the data
if (bufferSize >= bytesReceived) {
return udp.read(buffer, bytesReceived);
}
}
return 0;
}
// Wrapper functions for Ethernet UDP write function.
// Since we don't know the IP address of the node, just broadcast
// over the subnet.
bool sendCommand(uint8_t node, const uint8_t buffer[], uint8_t len) {
_ipAddress[3] = 255;
udp.beginPacket(_ipAddress, _port);
udp.write(buffer, len);
udp.endPacket();
return true;
}
void loop() { }
};
#endif //NET_ETHERNET_H

174
Net_RF24.h Normal file
View File

@@ -0,0 +1,174 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* nRF24 default mode of operation:
* Channel: 108
* Bit rate: 2MHz
* CRC: 16-bit
* Power Level: High
*
* The node number is used as the first byte of the nRF24's 5-byte address
* field. Number 255 is treated as a multicast address. All stations listen on
* their own address and on the multicast address.
*
* The nRF24 device receives and acknowledges data packets autonomously.
* Therefore, this driver just needs to detect when a packet is received and
* read and process its contents. The time to read the packet is under 200us
* typically.
*
* The nRF24 is also capable of autonomously sending packets, processing
* acknowledgements, and generating retries. The driver writes the packet to
* the device and then waits for notification of completion (success, or retries
* exceeded) through the device's registers. Similarly, the time to write a
* packet is under 200us and, if we don't wait for the completion, we can allow
* the processor to do other things while the transmission is in progress.
* A write with ack can complete in under 600us, plus the time of turning the
* receiver off and on.
*
* Usage:
* Net_RF24 *rf24Driver = new Net_RF24(48, 49);
* Network<Net_RF24>::create(4000, NUMREMOTEPINS(rpins), 1, rpins, rf24Driver);
*
* The nRF24 device has to be connected to the hardware MISO, MOSI, SCK and CS pins of the
* microcontroller; in addition, the CE and CSN pins on the nRF24 are connected to
* two pins (e.g. 48 and 49 above).
*
*/
#ifndef NET_RF24_H
#define NET_RF24_H
#include "IO_Network.h"
#include "DIAG.h"
class Net_RF24 : public RF24 {
private:
// pins must be arduino GPIO pins, not extender pins or HAL pins.
int _cePin;
int _csnPin;
// Number of the current node (1-254)
uint8_t _thisNode;
// 5-byte nRF24L01 address. First byte will contain the node number (0-254) or 255 for broadcast
byte _address[5];
bool _sendInProgress;
public:
// Constructor performs static initialisation of the device object
Net_RF24 (int cePin, int csnPin) {
_cePin = cePin;
_csnPin = csnPin;
// Initialise with an arbitrary address. The first byte will contain
// the node number.
_address[0] = 0x00;
_address[1] = 0xCC;
_address[2] = 0xEE;
_address[3] = 0xEE;
_address[4] = 0xCC;
}
// Perform dynamic initialisation of the device
bool begin(uint8_t thisNode) {
if (RF24::begin(_cePin, _csnPin)) {
// Device initialisation OK, set up parameters
RF24::setDataRate(RF24_2MBPS);
RF24::setPALevel(RF24_PA_HIGH);
RF24::setChannel(108);
RF24::enableDynamicPayloads(); // variable length packets
RF24::setAutoAck(true); // required for acks to work
RF24::enableDynamicAck(); // required for multicast to work
RF24::setRetries(1, 5); // Retry time=1*250+250us=500us, count=5.
_thisNode = thisNode;
// Set to listen on the address 255
_address[0] = 255;
RF24::openReadingPipe(1, _address);
// Also allow receives on own node address
_address[0] = _thisNode;
RF24::openReadingPipe(2, _address);
RF24::startListening();
_sendInProgress = false;
return true;
} else {
// Error in initialising
DIAG(F("nRF24L01 Failed to initialise"));
return false;
}
}
// Check if there is a received packet ready for reading.
bool available() {
return RF24::available();
}
// Read next packet from the device, and return the number of bytes
// that were read.
uint8_t read(uint8_t buffer[], uint8_t size) {
uint8_t len = RF24::getDynamicPayloadSize();
RF24::read(buffer, size);
return len;
}
// Wrapper functions for RF24 send functions. If node=255, then
// the packet is to be sent as a multicast without acknowledgements.
// The multicast message takes ~400us. A further 260us is required to turn
// the receiver off and on for the transmission, totalling 660us.
// If the node is not 255, then the packet will be sent directly to the
// addressed node, with acknowledgement requested. If no acknowledgement is
// received, then the device will retry up to the defined maximum number of
// retries. This will take longer than a multicast. For example, with
// setRetries(1,3) the timeout is 500us and a maximum of 3 retries are
// carried out, so the operation will take as much as 2.26 milliseconds if
// the node in question is not responding, and as little as 890us if the
// ack is received immediately (including turning receiver on/off).
//
bool sendCommand(uint8_t node, const uint8_t buffer[], uint8_t len) {
_address[0] = node;
openWritingPipe(_address);
// We have to stop the receiver before we can transmit.
RF24::stopListening();
// Copy the message into the radio and start the transmitter.
// Multicast (no ack expected) if destination node is 255.
bool ok = RF24::writeFast(buffer, len, (node==255));
// We will poll the radio later on to see when the transmit queue
// has emptied. When that happens, we will go back to receive mode.
// This prevents txStandBy() from blocking while the transmission
// is in progress.
_sendInProgress = true;;
return ok;
}
// The following function should be called regularly to ensure that the
// device goes back into listening mode when a transmission is not
// in progress. (The nRF24 is a half-duplex device and cannot be in
// transmit mode and receive mode at the same time.)
void loop() {
bool completed = RF24::isWriteFinished();
if (_sendInProgress && completed) {
_sendInProgress = false;
RF24::txStandBy();
RF24::startListening();
}
}
};
#endif //NET_RF24_H

View File

@@ -82,32 +82,50 @@ the state of any outputs being monitored or controlled by a separate interface o
**********************************************************************/
#include "Outputs.h"
#ifndef DISABLE_EEPROM
#include "EEStore.h"
#endif
#include "StringFormatter.h"
#include "IODevice.h"
///////////////////////////////////////////////////////////////////////////////
// Static function to print all output states to stream in the form "<Y id state>"
// print all output states to stream
void Output::printAll(Print *stream){
for (Output *tt = Output::firstOutput; tt != NULL; tt = tt->nextOutput)
StringFormatter::send(stream, F("<Y %d %d>\n"), tt->data.id, tt->data.oStatus);
StringFormatter::send(stream, F("<Y %d %d>\n"), tt->data.id, tt->data.active);
} // Output::printAll
void Output::activate(int s){
data.oStatus=(s>0); // if s>0, set status to active, else inactive
digitalWrite(data.pin,data.oStatus ^ bitRead(data.iFlag,0)); // set state of output pin to HIGH or LOW depending on whether bit zero of iFlag is set to 0 (ACTIVE=HIGH) or 1 (ACTIVE=LOW)
if(num>0)
EEPROM.put(num,data.oStatus);
///////////////////////////////////////////////////////////////////////////////
// Object method to activate / deactivate the Output state.
void Output::activate(uint16_t s){
s = (s>0); // Make 0 or 1
data.active = s; // if s>0, set status to active, else inactive
// set state of output pin to HIGH or LOW depending on whether bit zero of iFlag is set to 0 (ACTIVE=HIGH) or 1 (ACTIVE=LOW)
IODevice::write(data.pin, s ^ data.invert);
#ifndef DISABLE_EEPROM
// Update EEPROM if output has been stored.
if(EEStore::eeStore->data.nOutputs > 0 && num > 0)
EEPROM.put(num, data.oStatus);
#endif
}
///////////////////////////////////////////////////////////////////////////////
// Static function to locate Output object specified by ID 'n'.
// Return NULL if not found.
Output* Output::get(int n){
Output* Output::get(uint16_t n){
Output *tt;
for(tt=firstOutput;tt!=NULL && tt->data.id!=n;tt=tt->nextOutput);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////
bool Output::remove(int n){
///////////////////////////////////////////////////////////////////////////////
// Static function to delete Output object specified by ID 'n'.
// Return false if not found.
bool Output::remove(uint16_t n){
Output *tt,*pp=NULL;
for(tt=firstOutput;tt!=NULL && tt->data.id!=n;pp=tt,tt=tt->nextOutput);
@@ -125,23 +143,26 @@ bool Output::remove(int n){
}
///////////////////////////////////////////////////////////////////////////////
// Static function to load configuration and state of all Outputs from EEPROM
#ifndef DISABLE_EEPROM
void Output::load(){
struct OutputData data;
Output *tt;
for(int i=0;i<EEStore::eeStore->data.nOutputs;i++){
for(uint16_t i=0;i<EEStore::eeStore->data.nOutputs;i++){
EEPROM.get(EEStore::pointer(),data);
tt=create(data.id,data.pin,data.iFlag);
tt->data.oStatus=bitRead(tt->data.iFlag,1)?bitRead(tt->data.iFlag,2):data.oStatus; // restore status to EEPROM value is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
digitalWrite(tt->data.pin,tt->data.oStatus ^ bitRead(tt->data.iFlag,0));
pinMode(tt->data.pin,OUTPUT);
tt->num=EEStore::pointer();
// Create new object, set current state to default or to saved state from eeprom.
tt=create(data.id, data.pin, data.flags);
uint8_t state = data.setDefault ? data.defaultValue : data.active;
tt->activate(state);
if (tt) tt->num=EEStore::pointer() + offsetof(OutputData, oStatus); // Save pointer to flags within EEPROM
EEStore::advance(sizeof(tt->data));
}
}
///////////////////////////////////////////////////////////////////////////////
// Static function to store configuration and state of all Outputs to EEPROM
void Output::store(){
Output *tt;
@@ -150,19 +171,26 @@ void Output::store(){
EEStore::eeStore->data.nOutputs=0;
while(tt!=NULL){
tt->num=EEStore::pointer();
EEPROM.put(EEStore::pointer(),tt->data);
tt->num=EEStore::pointer() + offsetof(OutputData, oStatus); // Save pointer to flags within EEPROM
EEStore::advance(sizeof(tt->data));
tt=tt->nextOutput;
EEStore::eeStore->data.nOutputs++;
}
}
///////////////////////////////////////////////////////////////////////////////
#endif
Output *Output::create(int id, int pin, int iFlag, int v){
///////////////////////////////////////////////////////////////////////////////
// Static function to create an Output object
// The obscurely named parameter 'v' is 0 if called from the load() function
// and 1 if called from the <Z> command processing.
Output *Output::create(uint16_t id, VPIN pin, int iFlag, int v){
Output *tt;
if (pin > VPIN_MAX) return NULL;
if(firstOutput==NULL){
firstOutput=(Output *)calloc(1,sizeof(Output));
tt=firstOutput;
@@ -175,20 +203,21 @@ Output *Output::create(int id, int pin, int iFlag, int v){
}
if(tt==NULL) return tt;
tt->num = 0; // make sure new object doesn't get written to EEPROM until store() command
tt->data.id=id;
tt->data.pin=pin;
tt->data.iFlag=iFlag;
tt->data.oStatus=0;
tt->data.flags=iFlag;
if(v==1){
tt->data.oStatus=bitRead(tt->data.iFlag,1)?bitRead(tt->data.iFlag,2):0; // sets status to 0 (INACTIVE) is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
digitalWrite(tt->data.pin,tt->data.oStatus ^ bitRead(tt->data.iFlag,0));
pinMode(tt->data.pin,OUTPUT);
// sets status to 0 (INACTIVE) is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
if (tt->data.setDefault)
tt->data.active = tt->data.defaultValue;
else
tt->data.active = 0;
}
IODevice::write(tt->data.pin, tt->data.active ^ tt->data.invert);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////

View File

@@ -20,28 +20,45 @@
#define Outputs_h
#include <Arduino.h>
#include "IODevice.h"
struct OutputData {
uint8_t oStatus;
uint8_t id;
uint8_t pin;
uint8_t iFlag;
union {
uint8_t oStatus; // (Bit 0=Invert, Bit 1=Set state to default, Bit 2=default state, Bit 7=active)
struct {
unsigned int flags : 7; // Bit 0=Invert, Bit 1=Set state to default, Bit 2=default state
unsigned int : 1;
};
struct {
unsigned int invert : 1;
unsigned int setDefault : 1;
unsigned int defaultValue : 1;
unsigned int: 4;
unsigned int active : 1;
};
};
uint16_t id;
VPIN pin;
};
class Output{
public:
void activate(int s);
static Output* get(int);
static bool remove(int);
public:
void activate(uint16_t s);
bool isActive();
static Output* get(uint16_t);
static bool remove(uint16_t);
#ifndef DISABLE_EEPROM
static void load();
static void store();
static Output *create(int, int, int, int=0);
#endif
static Output *create(uint16_t, VPIN, int, int=0);
static Output *firstOutput;
struct OutputData data;
Output *nextOutput;
static void printAll(Print *);
private:
int num; // Chris has no idea what this is all about!
private:
uint16_t num; // EEPROM address of oStatus in OutputData struct, or zero if not stored.
}; // Output

View File

@@ -1,97 +0,0 @@
/*!
* @file PWMServoDriver.cpp
*
* @mainpage Adafruit 16-channel PWM & Servo driver, based on Adafruit_PWMServoDriver
*
* @section intro_sec Introduction
*
* This is a library for the 16-channel PWM & Servo driver.
*
* Designed specifically to work with the Adafruit PWM & Servo driver.
* This class contains a very small subset of the Adafruit version which
* is relevant to driving simple servos at 50Hz through a number of chained
* servo driver boards (ie servos 0-15 on board 0x40, 16-31 on board 0x41 etc.)
*
* @section author Author
* Chris Harlow (TPL)
* original by Limor Fried/Ladyada (Adafruit Industries).
*
* @section license License
*
* BSD license, all text above must be included in any redistribution
*/
#include <Arduino.h>
#include "PWMServoDriver.h"
#include "DIAG.h"
#include "I2CManager.h"
// REGISTER ADDRESSES
const byte PCA9685_MODE1=0x00; // Mode Register
const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/
const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */
// MODE1 bits
const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */
const byte MODE1_AI=0x20; /**< Auto-Increment enabled */
const byte MODE1_RESTART=0x80; /**< Restart enabled */
const byte PCA9685_I2C_ADDRESS=0x40; /** First PCA9685 I2C Slave Address */
const float FREQUENCY_OSCILLATOR=25000000.0; /** Accurate enough for our purposes */
const uint8_t PRESCALE_50HZ = (uint8_t)(((FREQUENCY_OSCILLATOR / (50.0 * 4096.0)) + 0.5) - 1);
const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed
/*!
* @brief Sets the PWM frequency for a chip to 50Hz for servos
*/
byte PWMServoDriver::setupFlags=0; // boards that have been initialised
byte PWMServoDriver::failFlags=0; // boards that have faild initialisation
bool PWMServoDriver::setup(int board) {
if (board>3 || (failFlags & (1<<board))) return false;
if (setupFlags & (1<<board)) return true;
I2CManager.begin();
I2CManager.setClock(MAX_I2C_SPEED);
uint8_t i2caddr=PCA9685_I2C_ADDRESS + board;
// Test if device is available
byte error = I2CManager.checkAddress(i2caddr);
if (error) {
DIAG(F("I2C Servo device 0x%x Not Found %d"),i2caddr, error);
failFlags|=1<<board;
return false;
}
//DIAG(F("PWMServoDriver::setup %x prescale=%d"),i2caddr,PRESCALE_50HZ);
writeRegister(i2caddr,PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);
writeRegister(i2caddr,PCA9685_PRESCALE, PRESCALE_50HZ);
writeRegister(i2caddr,PCA9685_MODE1,MODE1_AI);
writeRegister(i2caddr,PCA9685_MODE1, MODE1_RESTART | MODE1_AI);
setupFlags|=1<<board;
return true;
}
/*!
* @brief Sets the PWM output to a servo
*/
void PWMServoDriver::setServo(byte servoNum, uint16_t value) {
int board=servoNum/16;
int pin=servoNum%16;
if (setup(board)) {
DIAG(F("SetServo %d %d"),servoNum,value);
uint8_t buffer[] = {(uint8_t)(PCA9685_FIRST_SERVO + 4 * pin), // 4 registers per pin
0, 0, (uint8_t)(value & 0xff), (uint8_t)(value >> 8)};
if (value == 4095) buffer[2] = 0x10; // Full on
byte error=I2CManager.write(PCA9685_I2C_ADDRESS + board, buffer, sizeof(buffer));
if (error!=0) DIAG(F("SetServo error %d"),error);
}
}
void PWMServoDriver::writeRegister(uint8_t i2caddr,uint8_t hardwareRegister, uint8_t d) {
I2CManager.write(i2caddr, 2, hardwareRegister, d);
}

View File

@@ -1,21 +0,0 @@
/*!
* @file PWMServoDriver.h
*
* Used to set servo positions on an I2C bus with 1 or more PCA96685 boards.
*/
#ifndef PWMServoDriver_H
#define PWMServoDriver_H
class PWMServoDriver {
public:
static void setServo(byte servoNum, uint16_t pos);
private:
static byte setupFlags;
static byte failFlags;
static bool setup(int board);
static void writeRegister(uint8_t i2caddr,uint8_t hardwareRegister, uint8_t d);
};
#endif

View File

@@ -12,11 +12,13 @@ Both CommandStation-EX and BaseStation-Classic support much of the NMRA Digital
* simultaneous control of multiple locomotives
* 2-byte and 4-byte locomotive addressing
* 128-step speed throttling
* 28 or 128-step speed throttling
* Activate/de-activate all accessory function addresses 0-2048
* Control of all cab functions F0-F28
* Control of all cab functions F0-F28 and F29-F68
* Main Track: Write configuration variable bytes and set/clear specific configuration variable (CV) bits (aka Programming on Main or POM)
* Programming Track: Same as the main track with the addition of reading configuration variable bytes
* And many more custom features. see [What's new in CommandStation-EX?](#whats-new-in-commandstation-ex)
# Whats in this Repository?
@@ -38,11 +40,11 @@ in config.h.
## What's new in CommandStation-EX?
* WiThrottle server built in. Connect Engine Driver or WiThrottle clients directly to your Command Station
* WiThrottle server built in. Connect Engine Driver or WiThrottle clients directly to your Command Station (or through JMRI as before)
* WiFi and Ethernet shield support
* No more jumpers or soldering!
* Direct support for all the most popular motor control boards
* I2C Display support
* Direct support for all the most popular motor control boards including single pin (Arduino) or dual pin (IBT_2) type PWM inputs without the need for an adapter circuit
* I2C Display support (LCD and OLED)
* Improved short circuit detection and automatic reset from an overload
* Current reading, sensing and ACK detection settings in milliAmps instead of just pin readings
* Improved adherence to the NMRA DCC specification
@@ -50,6 +52,21 @@ in config.h.
* Railcom cutout (beta)
* Simpler, modular, faster code with an API Library for developers for easy expansion
* New features and functions in JMRI
* Ability to join MAIN and PROG tracks into one MAIN track to run your locos
* "Drive-Away" feature - Throttles with support, like Engine Driver, can allow a loco to be programmed on a usable, electrically isolated programming track and then drive off onto the main track
* Diagnostic commands to test decoders that aren't reading or writing correctly
* Support for Uno, Nano, Mega, Nano Every and Teensy microcontrollers
* User Functions: Filter regular commands (like a turnout or output command) and pass it to your own function or accessory
* Support for LCN (layout control nodes)
* mySetup.h file that acts like an Autoexec.Bat command to send startup commands to the CS
* High Accuracty Waveform option for rock steady DCC signals
* New current response outputs current in mA, overlimit current, and maximum board capable current. Support for new current meter in JMRI
* USB Browser based EX-WebThrottle
* New, simpler, function control command
* Number of locos discovery command `<#>`
* Emergency stop command <!>
* Release cabs from memory command <-> all cabs, <- CAB> for just one loco address
* Automatic slot (register) management
* Automation (coming soon)
NOTE: DCC-EX is a major rewrite to the code. We started over and rebuilt it from the ground up! For what that means to you, click [HERE](notes/rewrite.md).

1267
RF24.cpp Normal file

File diff suppressed because it is too large Load Diff

273
RF24.h Normal file
View File

@@ -0,0 +1,273 @@
/*
Copyright (C) 2011 J. Coliz <maniacbug@ymail.com>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
version 2 as published by the Free Software Foundation.
*/
/**
* @file RF24.h
*
* Class declaration for RF24 and helper enums
*/
#ifndef __RF24_H__
#define __RF24_H__
#define FAILURE_HANDLING
#define RF24_POWERUP_DELAY 5000
#define rf24_max(a, b) (a>b?a:b)
#define rf24_min(a, b) (a<b?a:b)
#define RF24_SPI_SPEED 10000000
#include <SPI.h>
#define _SPI SPIClass
typedef enum {
RF24_PA_MIN = 0,
RF24_PA_LOW,
RF24_PA_HIGH,
RF24_PA_MAX,
RF24_PA_ERROR
} rf24_pa_dbm_e;
typedef enum {
RF24_1MBPS = 0,
RF24_2MBPS,
RF24_250KBPS
} rf24_datarate_e;
typedef enum {
RF24_CRC_DISABLED = 0,
RF24_CRC_8,
RF24_CRC_16
} rf24_crclength_e;
/* Memory Map */
#define NRF_CONFIG 0x00
#define EN_AA 0x01
#define EN_RXADDR 0x02
#define SETUP_AW 0x03
#define SETUP_RETR 0x04
#define RF_CH 0x05
#define RF_SETUP 0x06
#define NRF_STATUS 0x07
#define OBSERVE_TX 0x08
#define CD 0x09
#define RX_ADDR_P0 0x0A
#define RX_ADDR_P1 0x0B
#define RX_ADDR_P2 0x0C
#define RX_ADDR_P3 0x0D
#define RX_ADDR_P4 0x0E
#define RX_ADDR_P5 0x0F
#define TX_ADDR 0x10
#define RX_PW_P0 0x11
// #define RX_PW_P1 0x12
// #define RX_PW_P2 0x13
// #define RX_PW_P3 0x14
// #define RX_PW_P4 0x15
// #define RX_PW_P5 0x16
#define FIFO_STATUS 0x17
#define DYNPD 0x1C
#define FEATURE 0x1D
/* Bit Mnemonics */
#define MASK_RX_DR 6
#define MASK_TX_DS 5
#define MASK_MAX_RT 4
#define EN_CRC 3
#define CRCO 2
#define PWR_UP 1
#define PRIM_RX 0
#define ENAA_P5 5
#define ENAA_P4 4
#define ENAA_P3 3
#define ENAA_P2 2
#define ENAA_P1 1
#define ENAA_P0 0
#define ERX_P5 5
#define ERX_P4 4
#define ERX_P3 3
#define ERX_P2 2
#define ERX_P1 1
#define ERX_P0 0
#define AW 0
#define ARD 4
#define ARC 0
#define PLL_LOCK 4
#define CONT_WAVE 7
#define RF_DR 3
#define RF_PWR 6
#define RX_DR 6
#define TX_DS 5
#define MAX_RT 4
#define RX_P_NO 1
#define TX_FULL 0
#define PLOS_CNT 4
#define ARC_CNT 0
#define TX_REUSE 6
#define FIFO_FULL 5
#define TX_EMPTY 4
#define RX_FULL 1
#define RX_EMPTY 0
#define DPL_P5 5
#define DPL_P4 4
#define DPL_P3 3
#define DPL_P2 2
#define DPL_P1 1
#define DPL_P0 0
#define EN_DPL 2
#define EN_ACK_PAY 1
#define EN_DYN_ACK 0
/* Instruction Mnemonics */
#define R_REGISTER 0x00
#define W_REGISTER 0x20
#define REGISTER_MASK 0x1F
#define ACTIVATE 0x50
#define R_RX_PL_WID 0x60
#define R_RX_PAYLOAD 0x61
#define W_TX_PAYLOAD 0xA0
#define W_ACK_PAYLOAD 0xA8
#define FLUSH_TX 0xE1
#define FLUSH_RX 0xE2
#define REUSE_TX_PL 0xE3
#define RF24_NOP 0xFF
/* Non-P omissions */
#define LNA_HCURR 0
/* P model memory Map */
#define RPD 0x09
#define W_TX_PAYLOAD_NO_ACK 0xB0
/* P model bit Mnemonics */
#define RF_DR_LOW 5
#define RF_DR_HIGH 3
#define RF_PWR_LOW 1
#define RF_PWR_HIGH 2
class RF24 {
private:
_SPI* _spi;
uint16_t ce_pin; /**< "Chip Enable" pin, activates the RX or TX role */
uint16_t csn_pin; /**< SPI Chip select */
#ifdef ARDUINO_ARCH_AVR
volatile uint8_t *cePtr;
volatile uint8_t *csnPtr;
uint8_t ceMask;
uint8_t csnMask;
#endif
uint32_t spi_speed; /**< SPI Bus Speed */
uint8_t status; /** The status byte returned from every SPI transaction */
uint8_t payload_size; /**< Fixed size of payloads */
bool dynamic_payloads_enabled; /**< Whether dynamic payloads are enabled. */
bool ack_payloads_enabled; /**< Whether ack payloads are enabled. */
uint8_t pipe0_reading_address[5]; /**< Last address set on pipe 0 for reading. */
uint8_t addr_width; /**< The address width to use - 3,4 or 5 bytes. */
uint8_t config_reg; /**< For storing the value of the NRF_CONFIG register */
bool _is_p_variant; /** For storing the result of testing the toggleFeatures() affect */
protected:
inline void beginTransaction();
inline void endTransaction();
public:
RF24(uint16_t _cepin, uint16_t _cspin, uint32_t _spi_speed = RF24_SPI_SPEED);
RF24(uint32_t _spi_speed = RF24_SPI_SPEED);
bool begin(void);
bool begin(uint16_t _cepin, uint16_t _cspin);
bool begin(_SPI* spiBus);
bool begin(_SPI* spiBus, uint16_t _cepin, uint16_t _cspin);
bool isChipConnected();
void startListening(void);
void stopListening(void);
bool available(void);
void read(void* buf, uint8_t len);
bool write(const void* buf, uint8_t len);
void openWritingPipe(const uint8_t* address);
void openReadingPipe(uint8_t number, const uint8_t* address);
void printDetails(void);
void printPrettyDetails(void);
bool available(uint8_t* pipe_num);
bool rxFifoFull();
void powerDown(void);
void powerUp(void);
bool write(const void* buf, uint8_t len, const bool multicast);
bool writeFast(const void* buf, uint8_t len);
bool writeFast(const void* buf, uint8_t len, const bool multicast);
bool writeBlocking(const void* buf, uint8_t len, uint32_t timeout);
bool isWriteFinished();
bool txStandBy();
bool txStandBy(uint32_t timeout, bool startTx = 0);
bool writeAckPayload(uint8_t pipe, const void* buf, uint8_t len);
void whatHappened(bool& tx_ok, bool& tx_fail, bool& rx_ready);
void startFastWrite(const void* buf, uint8_t len, const bool multicast, bool startTx = 1);
bool startWrite(const void* buf, uint8_t len, const bool multicast);
void reUseTX();
uint8_t flush_tx(void);
uint8_t flush_rx(void);
bool testCarrier(void);
bool testRPD(void);
bool isValid();
void closeReadingPipe(uint8_t pipe);
bool failureDetected;
void setAddressWidth(uint8_t a_width);
void setRetries(uint8_t delay, uint8_t count);
void setChannel(uint8_t channel);
uint8_t getChannel(void);
void setPayloadSize(uint8_t size);
uint8_t getPayloadSize(void);
uint8_t getDynamicPayloadSize(void);
void enableAckPayload(void);
void disableAckPayload(void);
void enableDynamicPayloads(void);
void disableDynamicPayloads(void);
void enableDynamicAck();
bool isPVariant(void);
void setAutoAck(bool enable);
void setAutoAck(uint8_t pipe, bool enable);
void setPALevel(uint8_t level, bool lnaEnable = 1);
uint8_t getPALevel(void);
uint8_t getARC(void);
bool setDataRate(rf24_datarate_e speed);
rf24_datarate_e getDataRate(void);
void setCRCLength(rf24_crclength_e length);
rf24_crclength_e getCRCLength(void);
void disableCRC(void);
void maskIRQ(bool tx_ok, bool tx_fail, bool rx_ready);
uint32_t txDelay;
uint32_t csDelay;
void startConstCarrier(rf24_pa_dbm_e level, uint8_t channel);
void stopConstCarrier(void);
void openReadingPipe(uint8_t number, uint64_t address);
void openWritingPipe(uint64_t address);
bool isAckPayloadAvailable(void);
private:
void _init_obj();
bool _init_radio();
bool _init_pins();
void csn(bool mode);
void ce(bool level);
void read_register(uint8_t reg, uint8_t* buf, uint8_t len);
uint8_t read_register(uint8_t reg);
void write_register(uint8_t reg, const uint8_t* buf, uint8_t len);
void write_register(uint8_t reg, uint8_t value, bool is_cmd_only = false);
void write_payload(const void* buf, uint8_t len, const uint8_t writeType);
void read_payload(void* buf, uint8_t len);
void toggle_features(void);
void errNotify(void);
protected:
uint8_t get_status(void);
};
#endif // __RF24_H__

23
RMFT.h Normal file
View File

@@ -0,0 +1,23 @@
#ifndef RMFT_H
#define RMFT_H
#if defined(RMFT_ACTIVE)
#include "RMFT2.h"
class RMFT {
public:
static void inline begin() {RMFT2::begin();}
static void inline loop() {RMFT2::loop();}
};
#include "RMFTMacros.h"
#else
// Dummy RMFT
class RMFT {
public:
static void inline begin() {}
static void inline loop() {}
};
#endif
#endif

769
RMFT2.cpp Normal file
View File

@@ -0,0 +1,769 @@
/*
* © 2020,2021 Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "RMFT2.h"
#include "DCC.h"
#include "DCCWaveform.h"
#include "DIAG.h"
#include "WiThrottle.h"
#include "DCCEXParser.h"
#include "Turnouts.h"
// Command parsing keywords
const int16_t HASH_KEYWORD_EXRAIL=15435;
const int16_t HASH_KEYWORD_ON = 2657;
const int16_t HASH_KEYWORD_START=23232;
const int16_t HASH_KEYWORD_RESERVE=11392;
const int16_t HASH_KEYWORD_FREE=-23052;
const int16_t HASH_KEYWORD_LATCH=1618;
const int16_t HASH_KEYWORD_UNLATCH=1353;
const int16_t HASH_KEYWORD_PAUSE=-4142;
const int16_t HASH_KEYWORD_RESUME=27609;
const int16_t HASH_KEYWORD_KILL=5218;
const int16_t HASH_KEYWORD_ROUTES=-3702;
// One instance of RMFT clas is used for each "thread" in the automation.
// Each thread manages a loco on a journey through the layout, and/or may manage a scenery automation.
// The thrrads exist in a ring, each time through loop() the next thread in the ring is serviced.
// Statics
const int16_t LOCO_ID_WAITING=-99; // waiting for loco id from prog track
int16_t RMFT2::progtrackLocoId; // used for callback when detecting a loco on prograck
bool RMFT2::diag=false; // <D EXRAIL ON>
RMFT2 * RMFT2::loopTask=NULL; // loopTask contains the address of ONE of the tasks in a ring.
RMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE.
// when pausingTask is set, that is the ONLY task that gets any service,
// and all others will have their locos stopped, then resumed after the pausing task resumes.
byte RMFT2::flags[MAX_FLAGS];
#define GET_OPCODE GETFLASH(RMFT2::RouteCode+progCounter)
#define GET_OPERAND(n) GETFLASHW(RMFT2::RouteCode+progCounter+1+(n*3))
#define SKIPOP progCounter+=3
/* static */ void RMFT2::begin() {
DCCEXParser::setRMFTFilter(RMFT2::ComandFilter);
for (int f=0;f<MAX_FLAGS;f++) flags[f]=0;
int progCounter;
// first pass startup, define any turnouts or servos, set signals red and count size.
for (progCounter=0;; SKIPOP){
byte opcode=GET_OPCODE;
if (opcode==OPCODE_ENDEXRAIL) break;
switch (opcode) {
case OPCODE_AT:
case OPCODE_AFTER:
case OPCODE_IF:
case OPCODE_IFNOT:
int16_t pin = (int16_t)GET_OPERAND(0);
if (pin<0) pin = -pin;
IODevice::configureInput((VPIN)pin,true);
}
if (opcode==OPCODE_SIGNAL) {
VPIN red=GET_OPERAND(0);
VPIN amber=GET_OPERAND(1);
VPIN green=GET_OPERAND(2);
IODevice::write(red,true);
if (amber) IODevice::write(amber,false);
IODevice::write(green,false);
continue;
}
if (opcode==OPCODE_TURNOUT) {
VPIN id=GET_OPERAND(0);
int addr=GET_OPERAND(1);
byte subAddr=GET_OPERAND(2);
DCCTurnout::create(id,addr,subAddr);
continue;
}
if (opcode==OPCODE_SERVOTURNOUT) {
int16_t id=GET_OPERAND(0);
VPIN pin=GET_OPERAND(1);
int activeAngle=GET_OPERAND(2);
int inactiveAngle=GET_OPERAND(3);
int profile=GET_OPERAND(4);
ServoTurnout::create(id,pin,activeAngle,inactiveAngle,profile);
continue;
}
if (opcode==OPCODE_PINTURNOUT) {
int16_t id=GET_OPERAND(0);
VPIN pin=GET_OPERAND(1);
VpinTurnout::create(id,pin);
continue;
}
// other opcodes are not needed on this pass
}
SKIPOP; // include ENDROUTES opcode
DIAG(F("EXRAIL %db, MAX_FLAGS=%d"), progCounter,MAX_FLAGS);
new RMFT2(0); // add the startup route
}
// This filter intercepts <> commands to do the following:
// - Implement RMFT specific commands/diagnostics
// - Reject/modify JMRI commands that would interfere with RMFT processing
void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
(void)stream; // avoid compiler warning if we don't access this parameter
bool reject=false;
switch(opcode) {
case 'D':
if (p[0]==HASH_KEYWORD_EXRAIL) { // <D EXRAIL ON/OFF>
diag = paramCount==2 && (p[1]==HASH_KEYWORD_ON || p[1]==1);
opcode=0;
}
break;
case '/': // New EXRAIL command
reject=!parseSlash(stream,paramCount,p);
opcode=0;
break;
default: // other commands pass through
break;
}
if (reject) {
opcode=0;
StringFormatter::send(stream,F("<X>"));
}
}
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
if (paramCount==0) { // STATUS
StringFormatter::send(stream, F("<* EXRAIL STATUS"));
RMFT2 * task=loopTask;
while(task) {
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d%c,SPEED=%d%c"),
(int)(task->taskId),task->progCounter,task->loco,
task->invert?'I':' ',
task->speedo,
task->forward?'F':'R'
);
task=task->next;
if (task==loopTask) break;
}
// Now stream the flags
for (int id=0;id<MAX_FLAGS; id++) {
byte flag=flags[id];
if (flag & ~TASK_FLAG) { // not interested in TASK_FLAG only. Already shown above
StringFormatter::send(stream,F("\nflags[%d} "),id);
if (flag & SECTION_FLAG) StringFormatter::send(stream,F(" RESERVED"));
if (flag & LATCH_FLAG) StringFormatter::send(stream,F(" LATCHED"));
}
}
StringFormatter::send(stream,F(" *>\n"));
return true;
}
switch (p[0]) {
case HASH_KEYWORD_PAUSE: // </ PAUSE>
if (paramCount!=1) return false;
DCC::setThrottle(0,1,true); // pause all locos on the track
pausingTask=(RMFT2 *)1; // Impossible task address
return true;
case HASH_KEYWORD_RESUME: // </ RESUME>
if (paramCount!=1) return false;
pausingTask=NULL;
{
RMFT2 * task=loopTask;
while(task) {
if (task->loco) task->driveLoco(task->speedo);
task=task->next;
if (task==loopTask) break;
}
}
return true;
case HASH_KEYWORD_START: // </ START [cab] route >
if (paramCount<2 || paramCount>3) return false;
{
int route=(paramCount==2) ? p[1] : p[2];
uint16_t cab=(paramCount==2)? 0 : p[1];
int pc=locateRouteStart(route);
if (pc<0) return false;
RMFT2* task=new RMFT2(pc);
task->loco=cab;
}
return true;
case HASH_KEYWORD_ROUTES: // </ ROUTES > JMRI withrottle support
if (paramCount>1) return false;
StringFormatter::send(stream,F("</ROUTES "));
emitWithrottleRouteList(stream);
StringFormatter::send(stream,F(">"));
return true;
default:
break;
}
// all other / commands take 1 parameter 0 to MAX_FLAGS-1
if (paramCount!=2 || p[1]<0 || p[1]>=MAX_FLAGS) return false;
switch (p[0]) {
case HASH_KEYWORD_KILL: // Kill taskid
{
RMFT2 * task=loopTask;
while(task) {
if (task->taskId==p[1]) {
delete task;
return true;
}
task=task->next;
if (task==loopTask) break;
}
}
return false;
case HASH_KEYWORD_RESERVE: // force reserve a section
setFlag(p[1],SECTION_FLAG);
return true;
case HASH_KEYWORD_FREE: // force free a section
setFlag(p[1],0,SECTION_FLAG);
return true;
case HASH_KEYWORD_LATCH:
setFlag(p[1], LATCH_FLAG);
return true;
case HASH_KEYWORD_UNLATCH:
setFlag(p[1], 0, LATCH_FLAG);
return true;
default:
return false;
}
}
// This emits Routes and Automations to Withrottle
// Automations are given a state to set the button to "handoff" which implies
// handing over the loco to the automation.
// Routes are given "Set" buttons and do not cause the loco to be handed over.
void RMFT2::emitWithrottleRouteList(Print* stream) {
StringFormatter::send(stream,F("PRT]\\[Routes}|{Route]\\[Set}|{2]\\[Handoff}|{4\nPRL"));
emitWithrottleDescriptions(stream);
StringFormatter::send(stream,F("\n"));
}
RMFT2::RMFT2(int progCtr) {
progCounter=progCtr;
// get an unused task id from the flags table
taskId=255; // in case of overflow
for (int f=0;f<MAX_FLAGS;f++) {
if (!getFlag(f,TASK_FLAG)) {
taskId=f;
setFlag(f, TASK_FLAG);
break;
}
}
delayTime=0;
loco=0;
speedo=0;
forward=true;
invert=false;
stackDepth=0;
onTurnoutId=0; // Not handling an ONTHROW/ONCLOSE
// chain into ring of RMFTs
if (loopTask==NULL) {
loopTask=this;
next=this;
}
else {
next=loopTask->next;
loopTask->next=this;
}
}
RMFT2::~RMFT2() {
driveLoco(1); // ESTOP my loco if any
setFlag(taskId,0,TASK_FLAG); // we are no longer using this id
if (next==this) loopTask=NULL;
else for (RMFT2* ring=next;;ring=ring->next) if (ring->next == this) {
ring->next=next;
loopTask=next;
break;
}
}
void RMFT2::createNewTask(int route, uint16_t cab) {
int pc=locateRouteStart(route);
if (pc<0) return;
RMFT2* task=new RMFT2(pc);
task->loco=cab;
}
int RMFT2::locateRouteStart(int16_t _route) {
if (_route==0) return 0; // Route 0 is always start of ROUTES for default startup
for (int progCounter=0;;SKIPOP) {
byte opcode=GET_OPCODE;
if (opcode==OPCODE_ENDEXRAIL) {
DIAG(F("RMFT2 sequence %d not found"), _route);
return -1;
}
if ((opcode==OPCODE_ROUTE || opcode==OPCODE_AUTOMATION || opcode==OPCODE_SEQUENCE)
&& _route==(int)GET_OPERAND(0)) return progCounter;
}
return -1;
}
void RMFT2::driveLoco(byte speed) {
if (loco<=0) return; // Prevent broadcast!
if (diag) DIAG(F("EXRAIL drive %d %d %d"),loco,speed,forward^invert);
if (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::OFF) {
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
Serial.println(F("<p1>")); // tell JMRI
}
DCC::setThrottle(loco,speed, forward^invert);
speedo=speed;
}
bool RMFT2::readSensor(uint16_t sensorId) {
// Exrail operands are unsigned but we need the signed version as inserted by the macros.
int16_t sId=(int16_t) sensorId;
VPIN vpin=abs(sId);
if (getFlag(vpin,LATCH_FLAG)) return true; // latched on
// negative sensorIds invert the logic (e.g. for a break-beam sensor which goes OFF when detecting)
bool s= IODevice::read(vpin) ^ (sId<0);
if (s && diag) DIAG(F("EXRAIL Sensor %d hit"),sId);
return s;
}
bool RMFT2::skipIfBlock() {
// returns false if killed
short nest = 1;
while (nest > 0) {
SKIPOP;
byte opcode = GET_OPCODE;
switch(opcode) {
case OPCODE_ENDEXRAIL:
kill(F("missing ENDIF"), nest);
return false;
case OPCODE_IF:
case OPCODE_IFNOT:
case OPCODE_IFRANDOM:
case OPCODE_IFRESERVE:
nest++;
break;
case OPCODE_ENDIF:
nest--;
break;
default:
break;
}
}
return true;
}
/* static */ void RMFT2::readLocoCallback(int16_t cv) {
progtrackLocoId=cv;
}
void RMFT2::loop() {
// Round Robin call to a RMFT task each time
if (loopTask==NULL) return;
loopTask=loopTask->next;
if (pausingTask==NULL || pausingTask==loopTask) loopTask->loop2();
}
void RMFT2::loop2() {
if (delayTime!=0 && millis()-delayStart < delayTime) return;
byte opcode = GET_OPCODE;
int16_t operand = GET_OPERAND(0);
// if (diag) DIAG(F("RMFT2 %d %d"),opcode,operand);
// Attention: Returning from this switch leaves the program counter unchanged.
// This is used for unfinished waits for timers or sensors.
// Breaking from this switch will step to the next step in the route.
switch ((OPCODE)opcode) {
case OPCODE_THROW:
Turnout::setClosed(operand, false);
break;
case OPCODE_CLOSE:
Turnout::setClosed(operand, true);
break;
case OPCODE_REV:
forward = false;
driveLoco(operand);
break;
case OPCODE_FWD:
forward = true;
driveLoco(operand);
break;
case OPCODE_SPEED:
driveLoco(operand);
break;
case OPCODE_INVERT_DIRECTION:
invert= !invert;
driveLoco(speedo);
break;
case OPCODE_RESERVE:
if (getFlag(operand,SECTION_FLAG)) {
driveLoco(0);
delayMe(500);
return;
}
setFlag(operand,SECTION_FLAG);
break;
case OPCODE_FREE:
setFlag(operand,0,SECTION_FLAG);
break;
case OPCODE_AT:
if (readSensor(operand)) break;
delayMe(50);
return;
case OPCODE_AFTER: // waits for sensor to hit and then remain off for 0.5 seconds. (must come after an AT operation)
if (readSensor(operand)) {
// reset timer to half a second and keep waiting
waitAfter=millis();
delayMe(50);
return;
}
if (millis()-waitAfter < 500 ) return;
break;
case OPCODE_LATCH:
setFlag(operand,LATCH_FLAG);
break;
case OPCODE_UNLATCH:
setFlag(operand,0,LATCH_FLAG);
break;
case OPCODE_SET:
IODevice::write(operand,true);
break;
case OPCODE_RESET:
IODevice::write(operand,false);
break;
case OPCODE_PAUSE:
DCC::setThrottle(0,1,true); // pause all locos on the track
pausingTask=this;
break;
case OPCODE_POM:
if (loco) DCC::writeCVByteMain(loco, operand, GET_OPERAND(1));
break;
case OPCODE_POWEROFF:
DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF);
DCC::setProgTrackSyncMain(false);
Serial.println(F("<p0>")); // Tell JMRI
break;
case OPCODE_RESUME:
pausingTask=NULL;
driveLoco(speedo);
for (RMFT2 * t=next; t!=this;t=t->next) if (t->loco >0) t->driveLoco(t->speedo);
break;
case OPCODE_IF: // do next operand if sensor set
if (!readSensor(operand)) if (!skipIfBlock()) return;
break;
case OPCODE_IFNOT: // do next operand if sensor not set
if (readSensor(operand)) if (!skipIfBlock()) return;
break;
case OPCODE_IFRANDOM: // do block on random percentage
if (random(100)>=operand) if (!skipIfBlock()) return;
break;
case OPCODE_IFRESERVE: // do block if we successfully RERSERVE
if (!getFlag(operand,SECTION_FLAG)) setFlag(operand,SECTION_FLAG);
else if (!skipIfBlock()) return;
break;
case OPCODE_ENDIF:
break;
case OPCODE_DELAY:
delayMe(operand*100L);
break;
case OPCODE_DELAYMINS:
delayMe(operand*60L*1000L);
break;
case OPCODE_RANDWAIT:
delayMe(random(operand)*100L);
break;
case OPCODE_RED:
doSignal(operand,true,false,false);
break;
case OPCODE_AMBER:
doSignal(operand,false,true,false);
break;
case OPCODE_GREEN:
doSignal(operand,false,false,true);
break;
case OPCODE_FON:
if (loco) DCC::setFn(loco,operand,true);
break;
case OPCODE_FOFF:
if (loco) DCC::setFn(loco,operand,false);
break;
case OPCODE_XFON:
DCC::setFn(operand,GET_OPERAND(1),true);
break;
case OPCODE_XFOFF:
DCC::setFn(operand,GET_OPERAND(1),false);
break;
case OPCODE_FOLLOW:
progCounter=locateRouteStart(operand);
if (progCounter<0) kill(F("FOLLOW unknown"), operand);
return;
case OPCODE_CALL:
if (stackDepth==MAX_STACK_DEPTH) {
kill(F("CALL stack"), stackDepth);
return;
}
callStack[stackDepth++]=progCounter+3;
progCounter=locateRouteStart(operand);
if (progCounter<0) kill(F("CALL unknown"),operand);
return;
case OPCODE_RETURN:
if (stackDepth==0) {
kill(F("RETURN stack"));
return;
}
progCounter=callStack[--stackDepth];
return;
case OPCODE_ENDTASK:
case OPCODE_ENDEXRAIL:
kill();
return;
case OPCODE_JOIN:
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
DCCWaveform::progTrack.setPowerMode(POWERMODE::ON);
DCC::setProgTrackSyncMain(true);
Serial.println(F("<p1 JOIN>")); // Tell JMRI
break;
case OPCODE_UNJOIN:
DCC::setProgTrackSyncMain(false);
break;
case OPCODE_READ_LOCO1: // READ_LOCO is implemented as 2 separate opcodes
progtrackLocoId=LOCO_ID_WAITING; // Nothing found yet
DCC::getLocoId(readLocoCallback);
break;
case OPCODE_READ_LOCO2:
if (progtrackLocoId==LOCO_ID_WAITING) {
delayMe(100);
return; // still waiting for callback
}
if (progtrackLocoId<0) {
kill(F("No Loco Found"),progtrackLocoId);
return; // still waiting for callback
}
loco=progtrackLocoId;
speedo=0;
forward=true;
invert=false;
break;
case OPCODE_START:
{
int newPc=locateRouteStart(operand);
if (newPc<0) break;
new RMFT2(newPc);
}
break;
case OPCODE_SENDLOCO: // cab, route
{
int newPc=locateRouteStart(GET_OPERAND(1));
if (newPc<0) break;
RMFT2* newtask=new RMFT2(newPc); // create new task
newtask->loco=operand;
}
break;
case OPCODE_SETLOCO:
{
loco=operand;
speedo=0;
forward=true;
invert=false;
}
break;
case OPCODE_SERVO: // OPCODE_SERVO,V(vpin),OPCODE_PAD,V(position),OPCODE_PAD,V(profile),OPCODE_PAD,V(duration)
IODevice::writeAnalogue(operand,GET_OPERAND(1),GET_OPERAND(2),GET_OPERAND(3));
break;
case OPCODE_WAITFOR: // OPCODE_SERVO,V(pin)
if (IODevice::isBusy(operand)) {
delayMe(100);
return;
}
break;
case OPCODE_PRINT:
printMessage(operand);
break;
case OPCODE_ROUTE:
case OPCODE_AUTOMATION:
case OPCODE_SEQUENCE:
if (diag) DIAG(F("EXRAIL begin(%d)"),operand);
break;
case OPCODE_PAD: // Just a padding for previous opcode needing >1 operad byte.
case OPCODE_SIGNAL: // Signal definition ignore at run time
case OPCODE_TURNOUT: // Turnout definition ignored at runtime
case OPCODE_SERVOTURNOUT: // Turnout definition ignored at runtime
case OPCODE_PINTURNOUT: // Turnout definition ignored at runtime
case OPCODE_ONCLOSE: // Turnout event catcers ignored here
case OPCODE_ONTHROW: // Turnout definition ignored at runtime
break;
default:
kill(F("INVOP"),operand);
}
// Falling out of the switch means move on to the next opcode
SKIPOP;
}
void RMFT2::delayMe(long delay) {
delayTime=delay;
delayStart=millis();
}
void RMFT2::setFlag(VPIN id,byte onMask, byte offMask) {
if (FLAGOVERFLOW(id)) return; // Outside range limit
byte f=flags[id];
f &= ~offMask;
f |= onMask;
flags[id]=f;
}
bool RMFT2::getFlag(VPIN id,byte mask) {
if (FLAGOVERFLOW(id)) return 0; // Outside range limit
return flags[id]&mask;
}
void RMFT2::kill(const FSH * reason, int operand) {
if (reason) DIAG(F("EXRAIL ERROR pc=%d, cab=%d, %S %d"), progCounter,loco, reason, operand);
else if (diag) DIAG(F("ENDTASK at pc=%d"), progCounter);
delete this;
}
/* static */ void RMFT2::doSignal(VPIN id,bool red, bool amber, bool green) {
// CAUTION: hides class member progCounter
for (int progCounter=0;; SKIPOP){
byte opcode=GET_OPCODE;
if (opcode==OPCODE_ENDEXRAIL) return;
if (opcode!=OPCODE_SIGNAL) continue;
VPIN redpin=GET_OPERAND(0);
if (redpin!=id)continue;
VPIN amberpin=GET_OPERAND(1);
VPIN greenpin=GET_OPERAND(2);
// If amberpin is zero, synthesise amber from red+green
IODevice::write(redpin,red || (amber && (amberpin==0)));
if (amberpin) IODevice::write(amberpin,amber);
if (greenpin) IODevice::write(greenpin,green || (amber && (amberpin==0)));
return;
}
}
void RMFT2::turnoutEvent(int16_t turnoutId, bool closed) {
// Check we dont already have a task running this turnout
RMFT2 * task=loopTask;
while(task) {
if (task->onTurnoutId==turnoutId) {
DIAG(F("Recursive ONTHROW/ONCLOSE for Turnout %d"),turnoutId);
return;
}
task=task->next;
if (task==loopTask) break;
}
// Hunt for an ONTHROW/ONCLOSE for this turnout
byte huntFor=closed ? OPCODE_ONCLOSE : OPCODE_ONTHROW ;
// caution hides class progCounter;
for (int progCounter=0;; SKIPOP){
byte opcode=GET_OPCODE;
if (opcode==OPCODE_ENDEXRAIL) return;
if (opcode!=huntFor) continue;
if (turnoutId!=(int16_t)GET_OPERAND(0)) continue;
task=new RMFT2(progCounter); // new task starts at this instruction
task->onTurnoutId=turnoutId; // flag for recursion detector
return;
}
}
void RMFT2::printMessage2(const FSH * msg) {
DIAG(F("EXRAIL(%d) %S"),loco,msg);
}
// This is called by emitRouteDescriptions to emit a withrottle description for a route or autoomation.
void RMFT2::emitRouteDescription(Print * stream, char type, int id, const FSH * description) {
StringFormatter::send(stream,F("]\\[%c%d}|{%S}|{%c"),
type,id,description, type=='R'?'2':'4');
}

117
RMFT2.h Normal file
View File

@@ -0,0 +1,117 @@
/*
* © 2020, Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef RMFT2_H
#define RMFT2_H
#include "FSH.h"
#include "IODevice.h"
// The following are the operation codes (or instructions) for a kind of virtual machine.
// Each instruction is normally 2 bytes long with an operation code followed by a parameter.
// In cases where more than one parameter is required, the first parameter is followed by one
// or more OPCODE_PAD instructions with the subsequent parameters. This wastes a byte but makes
// searching easier as a parameter can never be confused with an opcode.
//
enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
OPCODE_FWD,OPCODE_REV,OPCODE_SPEED,OPCODE_INVERT_DIRECTION,
OPCODE_RESERVE,OPCODE_FREE,
OPCODE_AT,OPCODE_AFTER,
OPCODE_LATCH,OPCODE_UNLATCH,OPCODE_SET,OPCODE_RESET,
OPCODE_IF,OPCODE_IFNOT,OPCODE_ENDIF,OPCODE_IFRANDOM,OPCODE_IFRESERVE,
OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_RANDWAIT,
OPCODE_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF,
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,
OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR,
OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN,
OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2,OPCODE_POM,
OPCODE_START,OPCODE_SETLOCO,OPCODE_SENDLOCO,
OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,
OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT,
OPCODE_PRINT,
OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,OPCODE_ENDTASK,OPCODE_ENDEXRAIL
};
// Flag bits for status of hardware and TPL
static const byte SECTION_FLAG = 0x01;
static const byte LATCH_FLAG = 0x02;
static const byte TASK_FLAG = 0x04;
static const byte MAX_STACK_DEPTH=4;
static const short MAX_FLAGS=256;
#define FLAGOVERFLOW(x) x>=MAX_FLAGS
class RMFT2 {
public:
static void begin();
static void loop();
RMFT2(int progCounter);
RMFT2(int route, uint16_t cab);
~RMFT2();
static void readLocoCallback(int16_t cv);
static void emitWithrottleRouteList(Print* stream);
static void createNewTask(int route, uint16_t cab);
static void turnoutEvent(int16_t id, bool closed);
private:
static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
static bool parseSlash(Print * stream, byte & paramCount, int16_t p[]) ;
static void streamFlags(Print* stream);
static void setFlag(VPIN id,byte onMask, byte OffMask=0);
static bool getFlag(VPIN id,byte mask);
static int locateRouteStart(int16_t _route);
static int16_t progtrackLocoId;
static void doSignal(VPIN id,bool red, bool amber, bool green);
static void emitRouteDescription(Print * stream, char type, int id, const FSH * description);
static void emitWithrottleDescriptions(Print * stream);
static RMFT2 * loopTask;
static RMFT2 * pausingTask;
void delayMe(long millisecs);
void driveLoco(byte speedo);
bool readSensor(uint16_t sensorId);
bool skipIfBlock();
bool readLoco();
void loop2();
void kill(const FSH * reason=NULL,int operand=0);
void printMessage(uint16_t id); // Built by RMFTMacros.h
void printMessage2(const FSH * msg);
static bool diag;
static const FLASH byte RouteCode[];
static byte flags[MAX_FLAGS];
// Local variables - exist for each instance/task
RMFT2 *next; // loop chain
int progCounter; // Byte offset of next route opcode in ROUTES table
unsigned long delayStart; // Used by opcodes that must be recalled before completing
unsigned long waitAfter; // Used by OPCODE_AFTER
unsigned long delayTime;
byte taskId;
uint16_t loco;
bool forward;
bool invert;
byte speedo;
int16_t onTurnoutId;
byte stackDepth;
int callStack[MAX_STACK_DEPTH];
};
#endif

311
RMFTMacros.h Normal file
View File

@@ -0,0 +1,311 @@
/*
* © 2020,2021 Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef RMFTMacros_H
#define RMFTMacros_H
// remove normal code LCD & SERIAL macros (will be restored later)
#undef LCD
#undef SERIAL
// This file will include and build the EXRAIL script and associated helper tricks.
// It does this by incliding myAutomation.h several times, each with a set of macros to
// extract the relevant parts.
// The entire automation script is contained within a byte array RMFT2::RouteCode[]
// made up of opcode and parameter pairs.
// ech opcode is a 1 byte operation plus 2 byte operand.
// The array is normally built using the macros below as this makes it easier
// to manage the cases where:
// - padding must be applied to ensure the correct alignment of the next instruction
// - large parameters must be split up
// - multiple parameters aligned correctly
// - a single macro requires multiple operations
// Descriptive texts for routes and animations are created in a sepaerate function which
// can be called to emit a list of routes/automatuions in a form suitable for Withrottle.
// PRINT(msg) and LCD(row,msg) is implemented in a separate pass to create
// a getMessageText(id) function.
// CAUTION: The macros below are multiple passed over myAutomation.h
// Pass 1 Implements aliases and
// converts descriptions to withrottle format emitter function
// Most macros are simply ignored in this pass.
#define ALIAS(name,value) const int name=value;
#define EXRAIL void RMFT2::emitWithrottleDescriptions(Print * stream) {(void)stream;
#define ROUTE(id, description) emitRouteDescription(stream,'R',id,F(description));
#define AUTOMATION(id, description) emitRouteDescription(stream,'A',id,F(description));
#define ENDEXRAIL }
#define AFTER(sensor_id)
#define AMBER(signal_id)
#define AT(sensor_id)
#define CALL(route)
#define CLOSE(id)
#define DELAY(mindelay)
#define DELAYMINS(mindelay)
#define DELAYRANDOM(mindelay,maxdelay)
#define DONE
#define ENDIF
#define ENDTASK
#define ESTOP
#define FADE(pin,value,ms)
#define FOFF(func)
#define FOLLOW(route)
#define FON(func)
#define FREE(blockid)
#define FWD(speed)
#define GREEN(signal_id)
#define IF(sensor_id)
#define IFNOT(sensor_id)
#define IFRANDOM(percent)
#define IFRESERVE(block)
#define INVERT_DIRECTION
#define JOIN
#define LATCH(sensor_id)
#define LCD(row,msg)
#define LCN(msg)
#define ONCLOSE(turnout_id)
#define ONTHROW(turnout_id)
#define PAUSE
#define PRINT(msg)
#define POM(cv,value)
#define POWEROFF
#define READ_LOCO
#define RED(signal_id)
#define RESERVE(blockid)
#define RESET(pin)
#define RESUME
#define RETURN
#define REV(speed)
#define START(route)
#define SENDLOCO(cab,route)
#define SERIAL(msg)
#define SERIAL1(msg)
#define SERIAL2(msg)
#define SERIAL3(msg)
#define SERVO(id,position,profile)
#define SERVO2(id,position,duration)
#define SETLOCO(loco)
#define SET(pin)
#define SEQUENCE(id)
#define SPEED(speed)
#define STOP
#undef SIGNAL
#define SIGNAL(redpin,amberpin,greenpin)
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile)
#define PIN_TURNOUT(id,pin)
#define THROW(id)
#define TURNOUT(id,addr,subaddr)
#define UNJOIN
#define UNLATCH(sensor_id)
#define WAITFOR(pin)
#define XFOFF(cab,func)
#define XFON(cab,func)
#include "myAutomation.h"
// setup for pass 2... Create getMessageText function
#undef ALIAS
#undef ROUTE
#undef AUTOMATION
#define ROUTE(id, description)
#define AUTOMATION(id, description)
#undef EXRAIL
#undef PRINT
#undef LCN
#undef SERIAL
#undef SERIAL1
#undef SERIAL2
#undef SERIAL3
#undef ENDEXRAIL
#undef LCD
const int StringMacroTracker1=__COUNTER__;
#define ALIAS(name,value)
#define EXRAIL void RMFT2::printMessage(uint16_t id) { switch(id) {
#define ENDEXRAIL default: DIAG(F("printMessage error %d %d"),id,StringMacroTracker1); return ; }}
#define PRINT(msg) case (__COUNTER__ - StringMacroTracker1) : printMessage2(F(msg));break;
#define LCN(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&LCN_SERIAL,F(msg));break;
#define SERIAL(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial,F(msg));break;
#define SERIAL1(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial1,F(msg));break;
#define SERIAL2(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial2,F(msg));break;
#define SERIAL3(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial3,F(msg));break;
#define LCD(id,msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::lcd(id,F(msg));break;
#include "myAutomation.h"
// Setup for Pass 3: create main routes table
#undef AFTER
#undef AMBER
#undef AT
#undef AUTOMATION
#undef CALL
#undef CLOSE
#undef DELAY
#undef DELAYMINS
#undef DELAYRANDOM
#undef DONE
#undef ENDIF
#undef ENDEXRAIL
#undef ENDTASK
#undef ESTOP
#undef EXRAIL
#undef FOFF
#undef FOLLOW
#undef FON
#undef FREE
#undef FWD
#undef GREEN
#undef IF
#undef IFNOT
#undef IFRANDOM
#undef IFRESERVE
#undef INVERT_DIRECTION
#undef JOIN
#undef LATCH
#undef LCD
#undef LCN
#undef ONCLOSE
#undef ONTHROW
#undef PAUSE
#undef POM
#undef POWEROFF
#undef PRINT
#undef READ_LOCO
#undef RED
#undef RESERVE
#undef RESET
#undef RESUME
#undef RETURN
#undef REV
#undef ROUTE
#undef START
#undef SEQUENCE
#undef SERVO
#undef SERVO2
#undef FADE
#undef SENDLOCO
#undef SERIAL
#undef SERIAL1
#undef SERIAL2
#undef SERIAL3
#undef SETLOCO
#undef SET
#undef SPEED
#undef STOP
#undef SIGNAL
#undef SERVO_TURNOUT
#undef PIN_TURNOUT
#undef THROW
#undef TURNOUT
#undef UNJOIN
#undef UNLATCH
#undef WAITFOR
#undef XFOFF
#undef XFON
// Define macros for route code creation
#define V(val) ((int16_t)(val))&0x00FF,((int16_t)(val)>>8)&0x00FF
#define NOP 0,0
#define ALIAS(name,value)
#define EXRAIL const FLASH byte RMFT2::RouteCode[] = {
#define AUTOMATION(id, description) OPCODE_AUTOMATION, V(id),
#define ROUTE(id, description) OPCODE_ROUTE, V(id),
#define SEQUENCE(id) OPCODE_SEQUENCE, V(id),
#define ENDTASK OPCODE_ENDTASK,NOP,
#define DONE OPCODE_ENDTASK,NOP,
#define ENDEXRAIL OPCODE_ENDTASK,NOP,OPCODE_ENDEXRAIL,NOP };
#define AFTER(sensor_id) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id),
#define AMBER(signal_id) OPCODE_AMBER,V(signal_id),
#define AT(sensor_id) OPCODE_AT,V(sensor_id),
#define CALL(route) OPCODE_CALL,V(route),
#define CLOSE(id) OPCODE_CLOSE,V(id),
#define DELAY(ms) OPCODE_DELAY,V(ms/100L),
#define DELAYMINS(mindelay) OPCODE_DELAYMINS,V(mindelay),
#define DELAYRANDOM(mindelay,maxdelay) OPCODE_DELAY,V(mindelay/100L),OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L),
#define ENDIF OPCODE_ENDIF,NOP,
#define ESTOP OPCODE_SPEED,V(1),
#define FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V(PCA9685::ProfileType::UseDuration|PCA9685::NoPowerOff),OPCODE_PAD,V(ms/100L),
#define FOFF(func) OPCODE_FOFF,V(func),
#define FOLLOW(route) OPCODE_FOLLOW,V(route),
#define FON(func) OPCODE_FON,V(func),
#define FREE(blockid) OPCODE_FREE,V(blockid),
#define FWD(speed) OPCODE_FWD,V(speed),
#define GREEN(signal_id) OPCODE_GREEN,V(signal_id),
#define IF(sensor_id) OPCODE_IF,V(sensor_id),
#define IFNOT(sensor_id) OPCODE_IFNOT,V(sensor_id),
#define IFRANDOM(percent) OPCODE_IFRANDOM,V(percent),
#define IFRESERVE(block) OPCODE_IFRESERVE,V(block),
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,NOP,
#define JOIN OPCODE_JOIN,NOP,
#define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id),
#define LCD(id,msg) PRINT(msg)
#define LCN(msg) PRINT(msg)
#define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id),
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
#define PAUSE OPCODE_PAUSE,NOP,
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
#define POWEROFF OPCODE_POWEROFF,NOP,
#define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2),
#define READ_LOCO OPCODE_READ_LOCO1,NOP,OPCODE_READ_LOCO2,NOP,
#define RED(signal_id) OPCODE_RED,V(signal_id),
#define RESERVE(blockid) OPCODE_RESERVE,V(blockid),
#define RESET(pin) OPCODE_RESET,V(pin),
#define RESUME OPCODE_RESUME,NOP,
#define RETURN OPCODE_RETURN,NOP,
#define REV(speed) OPCODE_REV,V(speed),
#define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route),
#define SERIAL(msg) PRINT(msg)
#define SERIAL1(msg) PRINT(msg)
#define SERIAL2(msg) PRINT(msg)
#define SERIAL3(msg) PRINT(msg)
#define START(route) OPCODE_START,V(route),
#define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::profile),OPCODE_PAD,V(0),
#define SERVO2(id,position,ms) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::Instant),OPCODE_PAD,V(ms/100L),
#define SETLOCO(loco) OPCODE_SETLOCO,V(loco),
#define SET(pin) OPCODE_SET,V(pin),
#define SPEED(speed) OPCODE_SPEED,V(speed),
#define STOP OPCODE_SPEED,V(0),
#define SIGNAL(redpin,amberpin,greenpin) OPCODE_SIGNAL,V(redpin),OPCODE_PAD,V(amberpin),OPCODE_PAD,V(greenpin),
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile),
#define PIN_TURNOUT(id,pin) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
#define THROW(id) OPCODE_THROW,V(id),
#define TURNOUT(id,addr,subaddr) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr),
#define UNJOIN OPCODE_UNJOIN,NOP,
#define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id),
#define WAITFOR(pin) OPCODE_WAITFOR,V(pin),
#define XFOFF(cab,func) OPCODE_XFOFF,V(cab),OPCODE_PAD,V(func),
#define XFON(cab,func) OPCODE_XFON,V(cab),OPCODE_PAD,V(func),
// PASS2 Build RouteCode
const int StringMacroTracker2=__COUNTER__;
#include "myAutomation.h"
// Restore normal code LCD & SERIAL macro
#undef LCD
#define LCD StringFormatter::lcd
#undef SERIAL
#define SERIAL 0x0
#endif

View File

@@ -1,8 +1,13 @@
DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Release. Release v3.1.0 is a minor release that adds additional features and fixes a number of bugs. With the number of new features, this could have easily been a major release. The team is continually improving the architecture of DCC++EX to make it more flexible and optimizing the code so as to get more perfromance from the Arduino microprocessors. This release includes all of the Point Releases from v3.0.1 to v3.0.16.
The DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Release. Release v3.1.0 is a minor release that adds additional features and fixes a number of bugs. With the number of new features, this could have easily been a major release. The team is continually improving the architecture of DCC++EX to make it more flexible and optimizing the code so as to get more performance from the Arduino (and other) microprocessors. This release includes all of the Point Releases from v3.0.1 to v3.0.16.
**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**
[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.zip)
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.tar.gz)
**Known Issues**
- **Wi-Fi** - works, but requires sending <AT> commands from a serial monitor if you want to switch between AP mode and STA station mode.
- **Wi-Fi** - works, but requires sending <AT> commands from a serial monitor if you want to switch between AP mode and STA station mode after initial setup
- **Pololu Motor Shield** - is supported with this release, but the user may have to adjust timings to enable programming mode due to limitation in its current sensing circuitry
#### Summary of key features and/or bug fixes by Point Release
@@ -134,7 +139,7 @@ DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Relea
- **Fix EEPROM bugs**
- **Number of locos discovery command** - `<#>` command
- **Support for more locomotives** - 20 locomotives on an UNO and 50 an a Mega.
- **Automatic slot managment** - slot variable in throttle/function commands are ignored and slot management is taken care of automatically. `<!>` command added to release locos from memory.
- **Automatic slot management** - slot variable in throttle/function commands are ignored and slot management is taken care of automatically. `<->` and `<- CAB>` commands added to release locos from memory and stop packets to the track.
**Key Contributors**
@@ -149,10 +154,14 @@ DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Relea
- Neil McKechnie - Worcestershire, UK (NeilMck)
- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)
- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)
- M Steve Todd - - Engine Driver and JMRI Interface
- M Steve Todd -
- Scott Catalano - Pennsylvania
- Gregor Baues - Île-de-France, France (grbba)
**Engine Driver and JMRI Interface**
- M Steve Todd
**exInstaller Software**
- Anthony W - Dayton, Ohio, USA (Dex, Dex++)
@@ -175,9 +184,9 @@ DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Relea
**Beta Testing / Release Management / Support**
- Larry Dribin - Release Management
- Keith Ledbetter
- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)
- Brad Van der Elst
- Keith Ledbetter
- BradVan der Elst
- Andrew Pye
- Mike Bowers
- Randy McKenzie
@@ -190,3 +199,8 @@ DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Relea
- Gajanatha Kobbekaduwe
- Sumner Patterson
- Paul - Virginia, USA
**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**
[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.zip)
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.tar.gz)

View File

@@ -1,6 +1,8 @@
/* Based on Arduino SSD1306Ascii Library, Copyright (C) 2015 by William Greiman
* Modifications (C) 2021 Neil McKechnie
*
* This file is part of CommandStation-EX
*
* This Library is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -19,90 +21,351 @@
#include "I2CManager.h"
#include "FSH.h"
//==============================================================================
// SSD1306/SSD1106 I2C command bytes
//------------------------------------------------------------------------------
/** Set Lower Column Start Address for Page Addressing Mode. */
static const uint8_t SSD1306_SETLOWCOLUMN = 0x00;
/** Set Higher Column Start Address for Page Addressing Mode. */
static const uint8_t SSD1306_SETHIGHCOLUMN = 0x10;
/** Set Memory Addressing Mode. */
static const uint8_t SSD1306_MEMORYMODE = 0x20;
/** Set display RAM display start line register from 0 - 63. */
static const uint8_t SSD1306_SETSTARTLINE = 0x40;
/** Set Display Contrast to one of 256 steps. */
static const uint8_t SSD1306_SETCONTRAST = 0x81;
/** Enable or disable charge pump. Follow with 0X14 enable, 0X10 disable. */
static const uint8_t SSD1306_CHARGEPUMP = 0x8D;
/** Set Segment Re-map between data column and the segment driver. */
static const uint8_t SSD1306_SEGREMAP = 0xA0;
/** Resume display from GRAM content. */
static const uint8_t SSD1306_DISPLAYALLON_RESUME = 0xA4;
/** Force display on regardless of GRAM content. */
static const uint8_t SSD1306_DISPLAYALLON = 0xA5;
/** Set Normal Display. */
static const uint8_t SSD1306_NORMALDISPLAY = 0xA6;
/** Set Inverse Display. */
static const uint8_t SSD1306_INVERTDISPLAY = 0xA7;
/** Set Multiplex Ratio from 16 to 63. */
static const uint8_t SSD1306_SETMULTIPLEX = 0xA8;
/** Set Display off. */
static const uint8_t SSD1306_DISPLAYOFF = 0xAE;
/** Set Display on. */
static const uint8_t SSD1306_DISPLAYON = 0xAF;
/**Set GDDRAM Page Start Address. */
static const uint8_t SSD1306_SETSTARTPAGE = 0xB0;
/** Set COM output scan direction normal. */
static const uint8_t SSD1306_COMSCANINC = 0xC0;
/** Set COM output scan direction reversed. */
static const uint8_t SSD1306_COMSCANDEC = 0xC8;
/** Set Display Offset. */
static const uint8_t SSD1306_SETDISPLAYOFFSET = 0xD3;
/** Sets COM signals pin configuration to match the OLED panel layout. */
static const uint8_t SSD1306_SETCOMPINS = 0xDA;
/** This command adjusts the VCOMH regulator output. */
static const uint8_t SSD1306_SETVCOMDETECT = 0xDB;
/** Set Display Clock Divide Ratio/ Oscillator Frequency. */
static const uint8_t SSD1306_SETDISPLAYCLOCKDIV = 0xD5;
/** Set Pre-charge Period */
static const uint8_t SSD1306_SETPRECHARGE = 0xD9;
/** Deactivate scroll */
static const uint8_t SSD1306_DEACTIVATE_SCROLL = 0x2E;
/** No Operation Command. */
static const uint8_t SSD1306_NOP = 0xE3;
//------------------------------------------------------------------------------
/** Set Pump voltage value: (30H~33H) 6.4, 7.4, 8.0 (POR), 9.0. */
static const uint8_t SH1106_SET_PUMP_VOLTAGE = 0x30;
/** First byte of set charge pump mode */
static const uint8_t SH1106_SET_PUMP_MODE = 0xAD;
/** Second byte charge pump on. */
static const uint8_t SH1106_PUMP_ON = 0x8B;
/** Second byte charge pump off. */
static const uint8_t SH1106_PUMP_OFF = 0x8A;
//------------------------------------------------------------------------------
// Maximum number of bytes we can send per transmission is 32.
const uint8_t SSD1306AsciiWire::blankPixels[16] =
// Sequence of blank pixels, to optimise clearing screen.
// Send a maximum of 30 pixels per transmission.
const uint8_t FLASH SSD1306AsciiWire::blankPixels[30] =
{0x40, // First byte specifies data mode
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
//==============================================================================
// this section is based on https://github.com/adafruit/Adafruit_SSD1306
/** Initialization commands for a 128x32 or 128x64 SSD1306 oled display. */
const uint8_t FLASH SSD1306AsciiWire::Adafruit128xXXinit[] = {
// Init sequence for Adafruit 128x32/64 OLED module
0x00, // Set to command mode
SSD1306_DISPLAYOFF,
SSD1306_SETDISPLAYCLOCKDIV, 0x80, // the suggested ratio 0x80
SSD1306_SETMULTIPLEX, 0x3F, // ratio 64 (initially)
SSD1306_SETDISPLAYOFFSET, 0x0, // no offset
SSD1306_SETSTARTLINE | 0x0, // line #0
SSD1306_CHARGEPUMP, 0x14, // internal vcc
SSD1306_MEMORYMODE, 0x02, // page mode
SSD1306_SEGREMAP | 0x1, // column 127 mapped to SEG0
SSD1306_COMSCANDEC, // column scan direction reversed
SSD1306_SETCOMPINS, 0X12, // set COM pins
SSD1306_SETCONTRAST, 0x7F, // contrast level 127
SSD1306_SETPRECHARGE, 0xF1, // pre-charge period (1, 15)
SSD1306_SETVCOMDETECT, 0x40, // vcomh regulator level
SSD1306_DISPLAYALLON_RESUME,
SSD1306_NORMALDISPLAY,
SSD1306_DISPLAYON
};
//------------------------------------------------------------------------------
// This section is based on https://github.com/stanleyhuangyc/MultiLCD
/** Initialization commands for a 128x64 SH1106 oled display. */
const uint8_t FLASH SSD1306AsciiWire::SH1106_132x64init[] = {
0x00, // Set to command mode
SSD1306_DISPLAYOFF,
SSD1306_SETDISPLAYCLOCKDIV, 0X80, // set osc division
SSD1306_SETMULTIPLEX, 0x3F, // ratio 64
SSD1306_SETDISPLAYOFFSET, 0X00, // set display offset
SSD1306_SETSTARTPAGE | 0X0, // set page address
SSD1306_SETSTARTLINE | 0x0, // set start line
SH1106_SET_PUMP_MODE, SH1106_PUMP_ON, // set charge pump enable
SSD1306_SEGREMAP | 0X1, // set segment remap
SSD1306_COMSCANDEC, // Com scan direction
SSD1306_SETCOMPINS, 0X12, // set COM pins
SSD1306_SETCONTRAST, 0x80, // 128
SSD1306_SETPRECHARGE, 0X1F, // set pre-charge period
SSD1306_SETVCOMDETECT, 0x40, // set vcomh
SH1106_SET_PUMP_VOLTAGE | 0X2, // 8.0 volts
SSD1306_NORMALDISPLAY, // normal / reverse
SSD1306_DISPLAYON
};
//==============================================================================
// SSD1306AsciiWire Method Definitions
//------------------------------------------------------------------------------
void SSD1306AsciiWire::clear() {
clear(0, displayWidth() - 1, 0, displayRows() - 1);
// Constructor
SSD1306AsciiWire::SSD1306AsciiWire(int width, int height) {
m_displayWidth = width;
m_displayHeight = height;
// Set size in characters in base class
lcdRows = height / 8;
lcdCols = width / 6;
m_col = 0;
m_row = 0;
m_colOffset = 0;
I2CManager.begin();
I2CManager.setClock(400000L); // Set max supported I2C speed
for (byte address = 0x3c; address <= 0x3d; address++) {
if (I2CManager.exists(address)) {
m_i2cAddr = address;
if (m_displayWidth==132 && m_displayHeight==64) {
// SH1106 display. This uses 128x64 centered within a 132x64 OLED.
m_colOffset = 2;
I2CManager.write_P(address, SH1106_132x64init, sizeof(SH1106_132x64init));
} else if (m_displayWidth==128 && (m_displayHeight==64 || m_displayHeight==32)) {
// SSD1306 128x64 or 128x32
I2CManager.write_P(address, Adafruit128xXXinit, sizeof(Adafruit128xXXinit));
if (m_displayHeight == 32)
I2CManager.write(address, 5, 0, // Set command mode
SSD1306_SETMULTIPLEX, 0x1F, // ratio 32
SSD1306_SETCOMPINS, 0x02); // sequential COM pins, disable remap
} else {
DIAG(F("OLED configuration option not recognised"));
return;
}
// Device found
DIAG(F("%dx%d OLED display configured on I2C:x%x"), width, height, address);
// Set singleton address
lcdDisplay = this;
clear();
return;
}
}
DIAG(F("OLED display not found"));
}
//------------------------------------------------------------------------------
void SSD1306AsciiWire::clear(uint8_t columnStart, uint8_t columnEnd,
uint8_t rowStart, uint8_t rowEnd) {
/* Clear screen by writing blank pixels. */
void SSD1306AsciiWire::clearNative() {
const int maxBytes = sizeof(blankPixels); // max number of bytes sendable over Wire
// Ensure only rows on display will be cleared.
if (rowEnd >= displayRows()) rowEnd = displayRows() - 1;
for (uint8_t r = rowStart; r <= rowEnd; r++) {
setCursor(columnStart, r); // Position at start of row to be erased
for (uint8_t c = columnStart; c <= columnEnd; c += maxBytes-1) {
uint8_t len = min((uint8_t)(columnEnd-c+1), maxBytes-1) + 1;
I2CManager.write(m_i2cAddr, blankPixels, len); // Write up to 15 blank columns
for (uint8_t r = 0; r <= m_displayHeight/8 - 1; r++) {
setRowNative(r); // Position at start of row to be erased
for (uint8_t c = 0; c <= m_displayWidth - 1; c += maxBytes-1) {
uint8_t len = min(m_displayWidth-c, maxBytes-1) + 1;
I2CManager.write_P(m_i2cAddr, blankPixels, len); // Write a number of blank columns
}
}
}
//------------------------------------------------------------------------------
void SSD1306AsciiWire::begin(const DevType* dev, uint8_t i2cAddr) {
m_i2cAddr = i2cAddr;
m_col = 0;
m_row = 0;
#ifdef __AVR__
const uint8_t* table = (const uint8_t*)pgm_read_word(&dev->initcmds);
#else // __AVR__
const uint8_t* table = dev->initcmds;
#endif // __AVR
uint8_t size = readFontByte(&dev->initSize);
m_displayWidth = readFontByte(&dev->lcdWidth);
m_displayHeight = readFontByte(&dev->lcdHeight);
m_colOffset = readFontByte(&dev->colOffset);
I2CManager.write_P(m_i2cAddr, table, size);
}
//------------------------------------------------------------------------------
void SSD1306AsciiWire::setContrast(uint8_t value) {
I2CManager.write(m_i2cAddr, 2,
0x00, // Set to command mode
SSD1306_SETCONTRAST, value);
}
//------------------------------------------------------------------------------
void SSD1306AsciiWire::setCursor(uint8_t col, uint8_t row) {
if (row < displayRows() && col < m_displayWidth) {
// Set cursor position (by text line)
void SSD1306AsciiWire::setRowNative(uint8_t line) {
// Calculate pixel position from line number
uint8_t row = line*8;
if (row < m_displayHeight) {
m_row = row;
m_col = col + m_colOffset;
I2CManager.write(m_i2cAddr, 4,
0x00, // Set to command mode
SSD1306_SETLOWCOLUMN | (col & 0XF),
SSD1306_SETHIGHCOLUMN | (col >> 4),
SSD1306_SETSTARTPAGE | m_row);
m_col = m_colOffset;
// Before using buffer, wait for last request to complete
requestBlock.wait();
// Build output buffer for I2C
uint8_t len = 0;
outputBuffer[len++] = 0x00; // Set to command mode
outputBuffer[len++] = SSD1306_SETLOWCOLUMN | (m_col & 0XF);
outputBuffer[len++] = SSD1306_SETHIGHCOLUMN | (m_col >> 4);
outputBuffer[len++] = SSD1306_SETSTARTPAGE | (m_row/8);
I2CManager.write(m_i2cAddr, outputBuffer, len, &requestBlock);
}
}
//------------------------------------------------------------------------------
void SSD1306AsciiWire::setFont(const uint8_t* font) {
m_font = font;
m_fontFirstChar = readFontByte(m_font + FONT_FIRST_CHAR);
m_fontCharCount = readFontByte(m_font + FONT_CHAR_COUNT);
}
//------------------------------------------------------------------------------
size_t SSD1306AsciiWire::write(uint8_t ch) {
const uint8_t* base = m_font + FONT_WIDTH_TABLE;
// Write a character to the OLED
size_t SSD1306AsciiWire::writeNative(uint8_t ch) {
const uint8_t* base = m_font;
if (ch < m_fontFirstChar || ch >= (m_fontFirstChar + m_fontCharCount))
return 0;
// Check if character would be partly or wholly off the display
if (m_col + fontWidth > m_displayWidth)
return 0;
#if defined(NOLOWERCASE)
// Adjust if lowercase is missing
if (ch >= 'a') {
if (ch <= 'z')
ch = ch - 'a' + 'A'; // Capitalise
else
ch -= 26; // Allow for missing lowercase letters
}
#endif
ch -= m_fontFirstChar;
base += fontWidth * ch;
uint8_t buffer[1+fontWidth+letterSpacing];
buffer[0] = 0x40; // set SSD1306 controller to data mode
// Before using buffer, wait for last request to complete
requestBlock.wait();
// Build output buffer for I2C
outputBuffer[0] = 0x40; // set SSD1306 controller to data mode
uint8_t bufferPos = 1;
// Copy character pixel columns
for (uint8_t i = 0; i < fontWidth; i++)
buffer[bufferPos++] = readFontByte(base++);
outputBuffer[bufferPos++] = GETFLASH(base++);
// Add blank pixels between letters
for (uint8_t i = 0; i < letterSpacing; i++)
buffer[bufferPos++] = 0;
outputBuffer[bufferPos++] = 0;
// Write the data to I2C display
I2CManager.write(m_i2cAddr, buffer, bufferPos);
I2CManager.write(m_i2cAddr, outputBuffer, bufferPos, &requestBlock);
m_col += fontWidth + letterSpacing;
return 1;
}
//------------------------------------------------------------------------------
// Font characters, 5x7 pixels, 0x61 characters starting at 0x20.
// Lower case characters optionally omitted.
const uint8_t FLASH SSD1306AsciiWire::System5x7[] = {
// Fixed width; char width table not used !!!!
// or with lowercase character omitted.
// font data
0x00, 0x00, 0x00, 0x00, 0x00, // (space)
0x00, 0x00, 0x5F, 0x00, 0x00, // !
0x00, 0x07, 0x00, 0x07, 0x00, // "
0x14, 0x7F, 0x14, 0x7F, 0x14, // #
0x24, 0x2A, 0x7F, 0x2A, 0x12, // $
0x23, 0x13, 0x08, 0x64, 0x62, // %
0x36, 0x49, 0x55, 0x22, 0x50, // &
0x00, 0x05, 0x03, 0x00, 0x00, // '
0x00, 0x1C, 0x22, 0x41, 0x00, // (
0x00, 0x41, 0x22, 0x1C, 0x00, // )
0x08, 0x2A, 0x1C, 0x2A, 0x08, // *
0x08, 0x08, 0x3E, 0x08, 0x08, // +
0x00, 0x50, 0x30, 0x00, 0x00, // ,
0x08, 0x08, 0x08, 0x08, 0x08, // -
0x00, 0x60, 0x60, 0x00, 0x00, // .
0x20, 0x10, 0x08, 0x04, 0x02, // /
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0
0x00, 0x42, 0x7F, 0x40, 0x00, // 1
0x42, 0x61, 0x51, 0x49, 0x46, // 2
0x21, 0x41, 0x45, 0x4B, 0x31, // 3
0x18, 0x14, 0x12, 0x7F, 0x10, // 4
0x27, 0x45, 0x45, 0x45, 0x39, // 5
0x3C, 0x4A, 0x49, 0x49, 0x30, // 6
0x01, 0x71, 0x09, 0x05, 0x03, // 7
0x36, 0x49, 0x49, 0x49, 0x36, // 8
0x06, 0x49, 0x49, 0x29, 0x1E, // 9
0x00, 0x36, 0x36, 0x00, 0x00, // :
0x00, 0x56, 0x36, 0x00, 0x00, // ;
0x00, 0x08, 0x14, 0x22, 0x41, // <
0x14, 0x14, 0x14, 0x14, 0x14, // =
0x41, 0x22, 0x14, 0x08, 0x00, // >
0x02, 0x01, 0x51, 0x09, 0x06, // ?
0x32, 0x49, 0x79, 0x41, 0x3E, // @
0x7E, 0x11, 0x11, 0x11, 0x7E, // A
0x7F, 0x49, 0x49, 0x49, 0x36, // B
0x3E, 0x41, 0x41, 0x41, 0x22, // C
0x7F, 0x41, 0x41, 0x22, 0x1C, // D
0x7F, 0x49, 0x49, 0x49, 0x41, // E
0x7F, 0x09, 0x09, 0x01, 0x01, // F
0x3E, 0x41, 0x41, 0x51, 0x32, // G
0x7F, 0x08, 0x08, 0x08, 0x7F, // H
0x00, 0x41, 0x7F, 0x41, 0x00, // I
0x20, 0x40, 0x41, 0x3F, 0x01, // J
0x7F, 0x08, 0x14, 0x22, 0x41, // K
0x7F, 0x40, 0x40, 0x40, 0x40, // L
0x7F, 0x02, 0x04, 0x02, 0x7F, // M
0x7F, 0x04, 0x08, 0x10, 0x7F, // N
0x3E, 0x41, 0x41, 0x41, 0x3E, // O
0x7F, 0x09, 0x09, 0x09, 0x06, // P
0x3E, 0x41, 0x51, 0x21, 0x5E, // Q
0x7F, 0x09, 0x19, 0x29, 0x46, // R
0x46, 0x49, 0x49, 0x49, 0x31, // S
0x01, 0x01, 0x7F, 0x01, 0x01, // T
0x3F, 0x40, 0x40, 0x40, 0x3F, // U
0x1F, 0x20, 0x40, 0x20, 0x1F, // V
0x7F, 0x20, 0x18, 0x20, 0x7F, // W
0x63, 0x14, 0x08, 0x14, 0x63, // X
0x03, 0x04, 0x78, 0x04, 0x03, // Y
0x61, 0x51, 0x49, 0x45, 0x43, // Z
0x00, 0x00, 0x7F, 0x41, 0x41, // [
0x02, 0x04, 0x08, 0x10, 0x20, // "\"
0x41, 0x41, 0x7F, 0x00, 0x00, // ]
0x04, 0x02, 0x01, 0x02, 0x04, // ^
0x40, 0x40, 0x40, 0x40, 0x40, // _
0x00, 0x01, 0x02, 0x04, 0x00, // `
#ifndef NOLOWERCASE
0x20, 0x54, 0x54, 0x54, 0x78, // a
0x7F, 0x48, 0x44, 0x44, 0x38, // b
0x38, 0x44, 0x44, 0x44, 0x20, // c
0x38, 0x44, 0x44, 0x48, 0x7F, // d
0x38, 0x54, 0x54, 0x54, 0x18, // e
0x08, 0x7E, 0x09, 0x01, 0x02, // f
0x08, 0x14, 0x54, 0x54, 0x3C, // g
0x7F, 0x08, 0x04, 0x04, 0x78, // h
0x00, 0x44, 0x7D, 0x40, 0x00, // i
0x20, 0x40, 0x44, 0x3D, 0x00, // j
0x00, 0x7F, 0x10, 0x28, 0x44, // k
0x00, 0x41, 0x7F, 0x40, 0x00, // l
0x7C, 0x04, 0x18, 0x04, 0x78, // m
0x7C, 0x08, 0x04, 0x04, 0x78, // n
0x38, 0x44, 0x44, 0x44, 0x38, // o
0x7C, 0x14, 0x14, 0x14, 0x08, // p
0x08, 0x14, 0x14, 0x18, 0x7C, // q
0x7C, 0x08, 0x04, 0x04, 0x08, // r
0x48, 0x54, 0x54, 0x54, 0x20, // s
0x04, 0x3F, 0x44, 0x40, 0x20, // t
0x3C, 0x40, 0x40, 0x20, 0x7C, // u
0x1C, 0x20, 0x40, 0x20, 0x1C, // v
0x3C, 0x40, 0x30, 0x40, 0x3C, // w
0x44, 0x28, 0x10, 0x28, 0x44, // x
0x0C, 0x50, 0x50, 0x50, 0x3C, // y
0x44, 0x64, 0x54, 0x4C, 0x44, // z
#endif
0x00, 0x08, 0x36, 0x41, 0x00, // {
0x00, 0x00, 0x7F, 0x00, 0x00, // |
0x00, 0x41, 0x36, 0x08, 0x00, // }
0x08, 0x08, 0x2A, 0x1C, 0x08, // ->
0x08, 0x1C, 0x2A, 0x08, 0x08, // <-
0x00, 0x06, 0x09, 0x09, 0x06 // degree symbol
};

View File

@@ -1,6 +1,8 @@
/* Based on Arduino SSD1306Ascii Library, Copyright (C) 2015 by William Greiman
* Modifications (C) 2021 Neil McKechnie
*
* This file is part of CommandStation-EX
*
* This Library is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -20,78 +22,68 @@
#define SSD1306Ascii_h
#include "Arduino.h"
#include "SSD1306font.h"
#include "SSD1306init.h"
#include "FSH.h"
#include "LCDDisplay.h"
class SSD1306AsciiWire : public Print {
#include "I2CManager.h"
#include "DIAG.h"
// Uncomment to remove lower-case letters to save 108 bytes of flash
//#define NOLOWERCASE
//------------------------------------------------------------------------------
// Constructor
class SSD1306AsciiWire : public LCDDisplay {
public:
using Print::write;
SSD1306AsciiWire() {}
// Constructor
SSD1306AsciiWire(int width, int height);
// Initialize the display controller.
void begin(const DevType* dev, uint8_t i2cAddr);
void begin(uint8_t i2cAddr);
// Clear the display and set the cursor to (0, 0).
void clear();
// Clear a region of the display.
void clear(uint8_t c0, uint8_t c1, uint8_t r0, uint8_t r1);
// The current column in pixels.
inline uint8_t col() const { return m_col; }
// The display hight in pixels.
inline uint8_t displayHeight() const { return m_displayHeight; }
// The display height in rows with eight pixels to a row.
inline uint8_t displayRows() const { return m_displayHeight / 8; }
// The display width in pixels.
inline uint8_t displayWidth() const { return m_displayWidth; }
// Set the cursor position to (0, 0).
inline void home() { setCursor(0, 0); }
// Initialize the display controller.
void init(const DevType* dev);
// the current row number with eight pixels to a row.
inline uint8_t row() const { return m_row; }
/**
* @brief Set the display contrast.
*
* @param[in] value The contrast level in th range 0 to 255.
*/
void setContrast(uint8_t value);
/**
* @brief Set the cursor position.
*
* @param[in] col The column number in pixels.
* @param[in] row the row number in eight pixel rows.
*/
void setCursor(uint8_t col, uint8_t row);
/**
* @brief Set the current font.
*
* @param[in] font Pointer to a font table.
*/
void setFont(const uint8_t* font);
/**
* @brief Display a character.
*
* @param[in] c The character to display.
* @return one for success else zero.
*/
size_t write(uint8_t c);
void clearNative() override;
// Set cursor to start of specified text line
void setRowNative(byte line) override;
// Write one character to OLED
size_t writeNative(uint8_t c) override;
bool isBusy() override { return requestBlock.isBusy(); }
private:
uint8_t m_col; // Cursor column.
uint8_t m_row; // Cursor RAM row.
uint8_t m_displayWidth; // Display width.
uint8_t m_displayHeight; // Display height.
uint8_t m_colOffset; // Column offset RAM to SEG.
const uint8_t* m_font = NULL; // Current font.
// Cursor column.
uint8_t m_col;
// Cursor RAM row.
uint8_t m_row;
// Display width.
uint8_t m_displayWidth;
// Display height.
uint8_t m_displayHeight;
// Column offset RAM to SEG.
uint8_t m_colOffset = 0;
// Current font.
const uint8_t* const m_font = System5x7;
// Only fixed size 5x7 fonts in a 6x8 cell are supported.
const uint8_t fontWidth = 5;
const uint8_t fontHeight = 7;
const uint8_t letterSpacing = 1;
uint8_t m_fontFirstChar;
uint8_t m_fontCharCount;
static const uint8_t fontWidth = 5;
static const uint8_t fontHeight = 7;
static const uint8_t letterSpacing = 1;
static const uint8_t m_fontFirstChar = 0x20;
static const uint8_t m_fontCharCount = 0x61;
uint8_t m_i2cAddr;
I2CRB requestBlock;
uint8_t outputBuffer[fontWidth+letterSpacing+1];
static const uint8_t blankPixels[];
static const uint8_t System5x7[];
static const uint8_t FLASH Adafruit128xXXinit[];
static const uint8_t FLASH SH1106_132x64init[];
};
#endif // SSD1306Ascii_h

View File

@@ -1,180 +0,0 @@
/*
*
* System5x7
*
*
* File Name : System5x7.h
* Date : 28 Oct 2008
* Font size in bytes : 470
* Font width : 5
* Font height : 7
* Font first char : 32
* Font last char : 127
* Font used chars : 94
*
* The font data are defined as
*
* struct _FONT_ {
* uint16_t font_Size_in_Bytes_over_all_included_Size_it_self;
* uint8_t font_Width_in_Pixel_for_fixed_drawing;
* uint8_t font_Height_in_Pixel_for_all_characters;
* unit8_t font_First_Char;
* uint8_t font_Char_Count;
*
* uint8_t font_Char_Widths[font_Last_Char - font_First_Char +1];
* // for each character the separate width in pixels,
* // characters < 128 have an implicit virtual right empty row
*
* uint8_t font_data[];
* // bit field of all characters
*/
#ifndef SSD1306font_H
#define SSD1306font_H
#define SYSTEM5x7_WIDTH 5
#define SYSTEM5x7_HEIGHT 7
#ifdef __AVR__
#include <avr/pgmspace.h>
/** declare a font for AVR. */
#define GLCDFONTDECL(_n) static const uint8_t __attribute__((progmem)) _n[]
#define readFontByte(addr) pgm_read_byte(addr)
#else // __AVR__
/** declare a font. */
#define GLCDFONTDECL(_n) static const uint8_t _n[]
/** Fake read from flash. */
#define readFontByte(addr) (*(const unsigned char *)(addr))
#endif // __AVR__
//------------------------------------------------------------------------------
// Font Indices
/** No longer used Big Endian length field. Now indicates font type.
*
* 00 00 (fixed width font with 1 padding pixel on right and below)
*
* 00 01 (fixed width font with no padding pixels)
*/
#define FONT_LENGTH 0
/** Maximum character width. */
#define FONT_WIDTH 2
/** Font hight in pixels */
#define FONT_HEIGHT 3
/** Ascii value of first character */
#define FONT_FIRST_CHAR 4
/** count of characters in font. */
#define FONT_CHAR_COUNT 5
/** Offset to width table. */
#define FONT_WIDTH_TABLE 6
//------------------------------------------------------------------------------
GLCDFONTDECL(System5x7) = {
0x0, 0x0, // size of zero indicates fixed width font,
0x05, // width
0x07, // height
0x20, // first char
0x61, // char count
// Fixed width; char width table not used !!!!
// font data
0x00, 0x00, 0x00, 0x00, 0x00, // (space)
0x00, 0x00, 0x5F, 0x00, 0x00, // !
0x00, 0x07, 0x00, 0x07, 0x00, // "
0x14, 0x7F, 0x14, 0x7F, 0x14, // #
0x24, 0x2A, 0x7F, 0x2A, 0x12, // $
0x23, 0x13, 0x08, 0x64, 0x62, // %
0x36, 0x49, 0x55, 0x22, 0x50, // &
0x00, 0x05, 0x03, 0x00, 0x00, // '
0x00, 0x1C, 0x22, 0x41, 0x00, // (
0x00, 0x41, 0x22, 0x1C, 0x00, // )
0x08, 0x2A, 0x1C, 0x2A, 0x08, // *
0x08, 0x08, 0x3E, 0x08, 0x08, // +
0x00, 0x50, 0x30, 0x00, 0x00, // ,
0x08, 0x08, 0x08, 0x08, 0x08, // -
0x00, 0x60, 0x60, 0x00, 0x00, // .
0x20, 0x10, 0x08, 0x04, 0x02, // /
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0
0x00, 0x42, 0x7F, 0x40, 0x00, // 1
0x42, 0x61, 0x51, 0x49, 0x46, // 2
0x21, 0x41, 0x45, 0x4B, 0x31, // 3
0x18, 0x14, 0x12, 0x7F, 0x10, // 4
0x27, 0x45, 0x45, 0x45, 0x39, // 5
0x3C, 0x4A, 0x49, 0x49, 0x30, // 6
0x01, 0x71, 0x09, 0x05, 0x03, // 7
0x36, 0x49, 0x49, 0x49, 0x36, // 8
0x06, 0x49, 0x49, 0x29, 0x1E, // 9
0x00, 0x36, 0x36, 0x00, 0x00, // :
0x00, 0x56, 0x36, 0x00, 0x00, // ;
0x00, 0x08, 0x14, 0x22, 0x41, // <
0x14, 0x14, 0x14, 0x14, 0x14, // =
0x41, 0x22, 0x14, 0x08, 0x00, // >
0x02, 0x01, 0x51, 0x09, 0x06, // ?
0x32, 0x49, 0x79, 0x41, 0x3E, // @
0x7E, 0x11, 0x11, 0x11, 0x7E, // A
0x7F, 0x49, 0x49, 0x49, 0x36, // B
0x3E, 0x41, 0x41, 0x41, 0x22, // C
0x7F, 0x41, 0x41, 0x22, 0x1C, // D
0x7F, 0x49, 0x49, 0x49, 0x41, // E
0x7F, 0x09, 0x09, 0x01, 0x01, // F
0x3E, 0x41, 0x41, 0x51, 0x32, // G
0x7F, 0x08, 0x08, 0x08, 0x7F, // H
0x00, 0x41, 0x7F, 0x41, 0x00, // I
0x20, 0x40, 0x41, 0x3F, 0x01, // J
0x7F, 0x08, 0x14, 0x22, 0x41, // K
0x7F, 0x40, 0x40, 0x40, 0x40, // L
0x7F, 0x02, 0x04, 0x02, 0x7F, // M
0x7F, 0x04, 0x08, 0x10, 0x7F, // N
0x3E, 0x41, 0x41, 0x41, 0x3E, // O
0x7F, 0x09, 0x09, 0x09, 0x06, // P
0x3E, 0x41, 0x51, 0x21, 0x5E, // Q
0x7F, 0x09, 0x19, 0x29, 0x46, // R
0x46, 0x49, 0x49, 0x49, 0x31, // S
0x01, 0x01, 0x7F, 0x01, 0x01, // T
0x3F, 0x40, 0x40, 0x40, 0x3F, // U
0x1F, 0x20, 0x40, 0x20, 0x1F, // V
0x7F, 0x20, 0x18, 0x20, 0x7F, // W
0x63, 0x14, 0x08, 0x14, 0x63, // X
0x03, 0x04, 0x78, 0x04, 0x03, // Y
0x61, 0x51, 0x49, 0x45, 0x43, // Z
0x00, 0x00, 0x7F, 0x41, 0x41, // [
0x02, 0x04, 0x08, 0x10, 0x20, // "\"
0x41, 0x41, 0x7F, 0x00, 0x00, // ]
0x04, 0x02, 0x01, 0x02, 0x04, // ^
0x40, 0x40, 0x40, 0x40, 0x40, // _
0x00, 0x01, 0x02, 0x04, 0x00, // `
0x20, 0x54, 0x54, 0x54, 0x78, // a
0x7F, 0x48, 0x44, 0x44, 0x38, // b
0x38, 0x44, 0x44, 0x44, 0x20, // c
0x38, 0x44, 0x44, 0x48, 0x7F, // d
0x38, 0x54, 0x54, 0x54, 0x18, // e
0x08, 0x7E, 0x09, 0x01, 0x02, // f
0x08, 0x14, 0x54, 0x54, 0x3C, // g
0x7F, 0x08, 0x04, 0x04, 0x78, // h
0x00, 0x44, 0x7D, 0x40, 0x00, // i
0x20, 0x40, 0x44, 0x3D, 0x00, // j
0x00, 0x7F, 0x10, 0x28, 0x44, // k
0x00, 0x41, 0x7F, 0x40, 0x00, // l
0x7C, 0x04, 0x18, 0x04, 0x78, // m
0x7C, 0x08, 0x04, 0x04, 0x78, // n
0x38, 0x44, 0x44, 0x44, 0x38, // o
0x7C, 0x14, 0x14, 0x14, 0x08, // p
0x08, 0x14, 0x14, 0x18, 0x7C, // q
0x7C, 0x08, 0x04, 0x04, 0x08, // r
0x48, 0x54, 0x54, 0x54, 0x20, // s
0x04, 0x3F, 0x44, 0x40, 0x20, // t
0x3C, 0x40, 0x40, 0x20, 0x7C, // u
0x1C, 0x20, 0x40, 0x20, 0x1C, // v
0x3C, 0x40, 0x30, 0x40, 0x3C, // w
0x44, 0x28, 0x10, 0x28, 0x44, // x
0x0C, 0x50, 0x50, 0x50, 0x3C, // y
0x44, 0x64, 0x54, 0x4C, 0x44, // z
0x00, 0x08, 0x36, 0x41, 0x00, // {
0x00, 0x00, 0x7F, 0x00, 0x00, // |
0x00, 0x41, 0x36, 0x08, 0x00, // }
0x08, 0x08, 0x2A, 0x1C, 0x08, // ->
0x08, 0x1C, 0x2A, 0x08, 0x08, // <-
0x00, 0x06, 0x09, 0x09, 0x06 // degree symbol
};
#endif

View File

@@ -1,207 +0,0 @@
/* Based on Arduino SSD1306Ascii Library, Copyright (C) 2015 by William Greiman
* Modifications (C) 2021 Neil McKechnie
*
* This Library is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This Library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with the Arduino SSD1306Ascii Library. If not, see
* <http://www.gnu.org/licenses/>.
*/
/**
* @file SSD1306init.h
* @brief Display controller initialization commands.
*/
#ifndef SSD1306init_h
#define SSD1306init_h
//------------------------------------------------------------------------------
#ifndef __AVR__
/** Handle AVR flash addressing. */
#define MEM_TYPE
#else // __AVR__
#define MEM_TYPE __attribute__ ((progmem))
#endif // __AVR__
//------------------------------------------------------------------------------
/** Set Lower Column Start Address for Page Addressing Mode. */
#define SSD1306_SETLOWCOLUMN 0x00
/** Set Higher Column Start Address for Page Addressing Mode. */
#define SSD1306_SETHIGHCOLUMN 0x10
/** Set Memory Addressing Mode. */
#define SSD1306_MEMORYMODE 0x20
/** Set display RAM display start line register from 0 - 63. */
#define SSD1306_SETSTARTLINE 0x40
/** Set Display Contrast to one of 256 steps. */
#define SSD1306_SETCONTRAST 0x81
/** Enable or disable charge pump. Follow with 0X14 enable, 0X10 disable. */
#define SSD1306_CHARGEPUMP 0x8D
/** Set Segment Re-map between data column and the segment driver. */
#define SSD1306_SEGREMAP 0xA0
/** Resume display from GRAM content. */
#define SSD1306_DISPLAYALLON_RESUME 0xA4
/** Force display on regardless of GRAM content. */
#define SSD1306_DISPLAYALLON 0xA5
/** Set Normal Display. */
#define SSD1306_NORMALDISPLAY 0xA6
/** Set Inverse Display. */
#define SSD1306_INVERTDISPLAY 0xA7
/** Set Multiplex Ratio from 16 to 63. */
#define SSD1306_SETMULTIPLEX 0xA8
/** Set Display off. */
#define SSD1306_DISPLAYOFF 0xAE
/** Set Display on. */
#define SSD1306_DISPLAYON 0xAF
/**Set GDDRAM Page Start Address. */
#define SSD1306_SETSTARTPAGE 0XB0
/** Set COM output scan direction normal. */
#define SSD1306_COMSCANINC 0xC0
/** Set COM output scan direction reversed. */
#define SSD1306_COMSCANDEC 0xC8
/** Set Display Offset. */
#define SSD1306_SETDISPLAYOFFSET 0xD3
/** Sets COM signals pin configuration to match the OLED panel layout. */
#define SSD1306_SETCOMPINS 0xDA
/** This command adjusts the VCOMH regulator output. */
#define SSD1306_SETVCOMDETECT 0xDB
/** Set Display Clock Divide Ratio/ Oscillator Frequency. */
#define SSD1306_SETDISPLAYCLOCKDIV 0xD5
/** Set Pre-charge Period */
#define SSD1306_SETPRECHARGE 0xD9
/** Deactivate scroll */
#define SSD1306_DEACTIVATE_SCROLL 0x2E
/** No Operation Command. */
#define SSD1306_NOP 0XE3
//------------------------------------------------------------------------------
/** Set Pump voltage value: (30H~33H) 6.4, 7.4, 8.0 (POR), 9.0. */
#define SH1106_SET_PUMP_VOLTAGE 0X30
/** First byte of set charge pump mode */
#define SH1106_SET_PUMP_MODE 0XAD
/** Second byte charge pump on. */
#define SH1106_PUMP_ON 0X8B
/** Second byte charge pump off. */
#define SH1106_PUMP_OFF 0X8A
//------------------------------------------------------------------------------
/**
* @struct DevType
* @brief Device initialization structure.
*/
struct DevType {
/**
* Pointer to initialization command bytes.
*/
const uint8_t* initcmds;
/**
* Number of initialization bytes.
*/
const uint8_t initSize;
/**
* Width of the diaplay in pixels.
*/
const uint8_t lcdWidth;
/**
* Height of the display in pixels.
*/
const uint8_t lcdHeight;
/**
* Column offset RAM to display. Used to pick start column of SH1106.
*/
const uint8_t colOffset;
};
//------------------------------------------------------------------------------
// this section is based on https://github.com/adafruit/Adafruit_SSD1306
/** Initialization commands for a 128x32 SSD1306 oled display. */
static const uint8_t MEM_TYPE Adafruit128x32init[] = {
// Init sequence for Adafruit 128x32 OLED module
0x00, // Set to command mode
SSD1306_DISPLAYOFF,
SSD1306_SETDISPLAYCLOCKDIV, 0x80, // the suggested ratio 0x80
SSD1306_SETMULTIPLEX, 0x1F, // ratio 32
SSD1306_SETDISPLAYOFFSET, 0x0, // no offset
SSD1306_SETSTARTLINE | 0x0, // line #0
SSD1306_CHARGEPUMP, 0x14, // internal vcc
SSD1306_MEMORYMODE, 0x02, // page mode
SSD1306_SEGREMAP | 0x1, // column 127 mapped to SEG0
SSD1306_COMSCANDEC, // column scan direction reversed
SSD1306_SETCOMPINS, 0x02, // sequential COM pins, disable remap
SSD1306_SETCONTRAST, 0x7F, // contrast level 127
SSD1306_SETPRECHARGE, 0xF1, // pre-charge period (1, 15)
SSD1306_SETVCOMDETECT, 0x40, // vcomh regulator level
SSD1306_DISPLAYALLON_RESUME,
SSD1306_NORMALDISPLAY,
SSD1306_DISPLAYON
};
/** Initialize a 128x32 SSD1306 oled display. */
static const DevType MEM_TYPE Adafruit128x32 = {
Adafruit128x32init,
sizeof(Adafruit128x32init),
128,
32,
0
};
//------------------------------------------------------------------------------
// This section is based on https://github.com/adafruit/Adafruit_SSD1306
/** Initialization commands for a 128x64 SSD1306 oled display. */
static const uint8_t MEM_TYPE Adafruit128x64init[] = {
// Init sequence for Adafruit 128x64 OLED module
0x00, // Set to command mode
SSD1306_DISPLAYOFF,
SSD1306_SETDISPLAYCLOCKDIV, 0x80, // the suggested ratio 0x80
SSD1306_SETMULTIPLEX, 0x3F, // ratio 64
SSD1306_SETDISPLAYOFFSET, 0x0, // no offset
SSD1306_SETSTARTLINE | 0x0, // line #0
SSD1306_CHARGEPUMP, 0x14, // internal vcc
SSD1306_MEMORYMODE, 0x02, // page mode
SSD1306_SEGREMAP | 0x1, // column 127 mapped to SEG0
SSD1306_COMSCANDEC, // column scan direction reversed
SSD1306_SETCOMPINS, 0x12, // alt COM pins, disable remap
SSD1306_SETCONTRAST, 0x7F, // contrast level 127
SSD1306_SETPRECHARGE, 0xF1, // pre-charge period (1, 15)
SSD1306_SETVCOMDETECT, 0x40, // vcomh regulator level
SSD1306_DISPLAYALLON_RESUME,
SSD1306_NORMALDISPLAY,
SSD1306_DISPLAYON
};
/** Initialize a 128x64 oled display. */
static const DevType MEM_TYPE Adafruit128x64 = {
Adafruit128x64init,
sizeof(Adafruit128x64init),
128,
64,
0
};
//------------------------------------------------------------------------------
// This section is based on https://github.com/stanleyhuangyc/MultiLCD
/** Initialization commands for a 128x64 SH1106 oled display. */
static const uint8_t MEM_TYPE SH1106_128x64init[] = {
0x00, // Set to command mode
SSD1306_DISPLAYOFF,
SSD1306_SETSTARTPAGE | 0X0, // set page address
SSD1306_SETCONTRAST, 0x80, // 128
SSD1306_SEGREMAP | 0X1, // set segment remap
SSD1306_NORMALDISPLAY, // normal / reverse
SSD1306_SETMULTIPLEX, 0x3F, // ratio 64
SH1106_SET_PUMP_MODE, SH1106_PUMP_ON, // set charge pump enable
SH1106_SET_PUMP_VOLTAGE | 0X2, // 8.0 volts
SSD1306_COMSCANDEC, // Com scan direction
SSD1306_SETDISPLAYOFFSET, 0X00, // set display offset
SSD1306_SETDISPLAYCLOCKDIV, 0X80, // set osc division
SSD1306_SETPRECHARGE, 0X1F, // set pre-charge period
SSD1306_SETCOMPINS, 0X12, // set COM pins
SSD1306_SETVCOMDETECT, 0x40, // set vcomh
SSD1306_DISPLAYON
};
/** Initialize a 128x64 oled SH1106 display. */
static const DevType MEM_TYPE SH1106_128x64 = {
SH1106_128x64init,
sizeof(SH1106_128x64init),
128,
64,
2 // SH1106 is a 132x64 controller. Use middle 128 columns.
};
#endif // SSD1306init_h

View File

@@ -1,5 +1,6 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021, modified by Neil McKechnie. All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -27,9 +28,9 @@ or be allowed to float HIGH if use of the Arduino Pin's internal pull-up resisto
To ensure proper voltage levels, some part of the Sensor circuitry
MUST be tied back to the same ground as used by the Arduino.
The Sensor code below utilizes exponential smoothing to "de-bounce" spikes generated by
The Sensor code below utilises "de-bounce" logic to remove spikes generated by
mechanical switches and transistors. This avoids the need to create smoothing circuitry
for each sensor. You may need to change these parameters through trial and error for your specific sensors.
for each sensor. You may need to change the parameters through trial and error for your specific sensors.
To have this sketch monitor one or more Arduino pins for sensor triggers, first define/edit/delete
sensor definitions using the following variation of the "S" command:
@@ -67,46 +68,111 @@ decide to ignore the <q ID> return and only react to <Q ID> triggers.
#include "StringFormatter.h"
#include "Sensors.h"
#ifndef DISABLE_EEPROM
#include "EEStore.h"
#endif
#include "IODevice.h"
///////////////////////////////////////////////////////////////////////////////
//
// checks one defined sensors and prints _changed_ sensor state
// checks a number of defined sensors per entry and prints _changed_ sensor state
// to stream unless stream is NULL in which case only internal
// state is updated. Then advances to next sensor which will
// be checked att next invocation.
// be checked at next invocation. Each cycle of reading all sensors will
// be initiated no more frequently than the time set by 'cycleInterval' microseconds.
//
// The list of sensors is divided such that the first part of the list
// contains sensors that support change notification via callback, and the second
// part of the list contains sensors that require cyclic polling. The start of the
// second part of the list is determined from by the 'firstPollSensor' pointer.
///////////////////////////////////////////////////////////////////////////////
void Sensor::checkAll(Print *stream){
uint16_t sensorCount = 0;
if (firstSensor == NULL) return;
if (readingSensor == NULL) readingSensor=firstSensor;
#ifdef USE_NOTIFY
// Register the event handler ONCE!
if (!inputChangeCallbackRegistered)
IONotifyCallback::add(inputChangeCallback);
inputChangeCallbackRegistered = true;
#endif
bool sensorstate = digitalRead(readingSensor->data.pin);
if (!sensorstate == readingSensor->active) { // active==true means sensorstate=0/false so sensor unchanged
// no change
if (readingSensor->latchdelay != 0) {
// enable if you want to debug contact jitter
//if (stream != NULL) StringFormatter::send(stream, F("JITTER %d %d\n"),
// readingSensor->latchdelay, readingSensor->data.snum);
readingSensor->latchdelay=0; // reset
if (firstSensor == NULL) return; // No sensors to be scanned
if (readingSensor == NULL) {
// Not currently scanning sensor list
unsigned long thisTime = micros();
if (thisTime - lastReadCycle >= cycleInterval) {
// Required time elapsed since last read cycle started,
// so initiate new scan through the sensor list
readingSensor = firstSensor;
lastReadCycle = thisTime;
}
} else if (readingSensor->latchdelay < 127) { // byte, max 255, good value unknown yet
// change but first increase anti-jitter counter
readingSensor->latchdelay++;
} else {
// make the change
readingSensor->active = !sensorstate;
readingSensor->latchdelay=0; // reset
if (stream != NULL) StringFormatter::send(stream, F("<%c %d>\n"), readingSensor->active ? 'Q' : 'q', readingSensor->data.snum);
}
readingSensor=readingSensor->nextSensor;
// Loop until either end of list is encountered or we pause for some reason
bool pause = false;
while (readingSensor != NULL && !pause) {
// Where the sensor is attached to a pin, read pin status. For sources such as LCN,
// which don't have an input pin to read, the LCN class calls setState() to update inputState when
// a message is received. The IODevice::read() call returns 1 for active pins (0v) and 0 for inactive (5v).
// Also, on HAL drivers that support change notifications, the driver calls the notification callback
// routine when an input signal change is detected, and this updates the inputState directly,
// so these inputs don't need to be polled here.
VPIN pin = readingSensor->data.pin;
if (readingSensor->pollingRequired && pin != VPIN_NONE)
readingSensor->inputState = IODevice::read(pin);
// Check if changed since last time, and process changes.
if (readingSensor->inputState == readingSensor->active) {
// no change
readingSensor->latchDelay = minReadCount; // Reset counter
} else if (readingSensor->latchDelay > 0) {
// change detected, but first decrement delay
readingSensor->latchDelay--;
} else {
// change validated, act on it.
readingSensor->active = readingSensor->inputState;
readingSensor->latchDelay = minReadCount; // Reset counter
if (stream != NULL) {
StringFormatter::send(stream, F("<%c %d>\n"), readingSensor->active ? 'Q' : 'q', readingSensor->data.snum);
pause = true; // Don't check any more sensors on this entry
}
}
// Move to next sensor in list.
readingSensor = readingSensor->nextSensor;
// Currently process max of 16 sensors per entry.
// Performance measurements taken during development indicate that, with 128 sensors configured
// on 8x 16-pin MCP23017 GPIO expanders with polling (no change notification), all inputs can be read from the devices
// within 1.4ms (400Mhz I2C bus speed), and a full cycle of checking 128 sensors for changes takes under a millisecond.
sensorCount++;
if (sensorCount >= 16) pause = true;
}
} // Sensor::checkAll
#ifdef USE_NOTIFY
// Callback from HAL (IODevice class) when a digital input change is recognised.
// Updates the inputState field, which is subsequently scanned for changes in the checkAll
// method. Ideally the <Q>/<q> message should be sent from here, instead of waiting for
// the checkAll method, but the output stream is not available at this point.
void Sensor::inputChangeCallback(VPIN vpin, int state) {
Sensor *tt;
// This bit is not ideal since it has, potentially, to look through the entire list of
// sensors to find the one that has changed. Ideally this should be improved somehow.
for (tt=firstSensor; tt!=NULL ; tt=tt->nextSensor) {
if (tt->data.pin == vpin) break;
}
if (tt != NULL) { // Sensor found
tt->inputState = (state != 0);
}
}
#endif
///////////////////////////////////////////////////////////////////////////////
//
// prints all sensor states to stream
@@ -115,40 +181,62 @@ void Sensor::checkAll(Print *stream){
void Sensor::printAll(Print *stream){
for(Sensor * tt=firstSensor;tt!=NULL;tt=tt->nextSensor){
if (stream != NULL)
if (stream != NULL) {
for(Sensor * tt=firstSensor;tt!=NULL;tt=tt->nextSensor){
StringFormatter::send(stream, F("<%c %d>\n"), tt->active ? 'Q' : 'q', tt->data.snum);
}
} // loop over all sensors
} // Sensor::printAll
///////////////////////////////////////////////////////////////////////////////
// Static Function to create/find Sensor object.
Sensor *Sensor::create(int snum, int pin, int pullUp){
Sensor *Sensor::create(int snum, VPIN pin, int pullUp){
Sensor *tt;
if(firstSensor==NULL){
firstSensor=(Sensor *)calloc(1,sizeof(Sensor));
tt=firstSensor;
} else if((tt=get(snum))==NULL){
tt=firstSensor;
while(tt->nextSensor!=NULL)
tt=tt->nextSensor;
tt->nextSensor=(Sensor *)calloc(1,sizeof(Sensor));
tt=tt->nextSensor;
}
if (pin > VPIN_MAX && pin != VPIN_NONE) return NULL;
if(tt==NULL) return tt; // problem allocating memory
remove(snum); // Unlink and free any existing sensor with the same id, before creating the new one.
tt->data.snum=snum;
tt->data.pin=pin;
tt->data.pullUp=(pullUp==0?LOW:HIGH);
tt->active=false;
tt->latchdelay=0;
pinMode(pin,INPUT); // set mode to input
digitalWrite(pin,pullUp); // don't use Arduino's internal pull-up resistors for external infrared sensors --- each sensor must have its own 1K external pull-up resistor
tt = (Sensor *)calloc(1,sizeof(Sensor));
if (!tt) return tt; // memory allocation failure
if (pin == VPIN_NONE)
tt->pollingRequired = false;
#ifdef USE_NOTIFY
else if (IODevice::hasCallback(pin))
tt->pollingRequired = false;
#endif
else
tt->pollingRequired = true;
// Add to the start of the list
tt->nextSensor = firstSensor;
firstSensor = tt;
tt->data.snum = snum;
tt->data.pin = pin;
tt->data.pullUp = pullUp;
tt->active = 0;
tt->inputState = 0;
tt->latchDelay = minReadCount;
if (pin != VPIN_NONE)
IODevice::configureInput(pin, pullUp);
// Generally, internal pull-up resistors are not, on their own, sufficient
// for external infrared sensors --- each sensor must have its own 1K external pull-up resistor
return tt;
}
///////////////////////////////////////////////////////////////////////////////
// Object method to directly change the input state, for sensors such as LCN which are updated
// by means other than by polling an input.
void Sensor::setState(int value) {
// Trigger sensor change to be reported on next checkAll loop.
inputState = (value != 0);
latchDelay = 0; // Don't wait for anti-jitter logic
}
///////////////////////////////////////////////////////////////////////////////
@@ -166,27 +254,38 @@ bool Sensor::remove(int n){
for(tt=firstSensor;tt!=NULL && tt->data.snum!=n;pp=tt,tt=tt->nextSensor);
if (tt==NULL) return false;
if(tt==firstSensor)
firstSensor=tt->nextSensor;
else
pp->nextSensor=tt->nextSensor;
// Unlink the sensor from the list
if(tt==firstSensor)
firstSensor=tt->nextSensor;
else
pp->nextSensor=tt->nextSensor;
#ifdef USE_NOTIFY
if (tt==lastSensor)
lastSensor = pp;
if (tt==firstPollSensor)
firstPollSensor = tt->nextSensor;
#endif
// Check if the sensor being deleted is the next one to be read. If so,
// make the following one the next one to be read.
if (readingSensor==tt) readingSensor=tt->nextSensor;
free(tt);
return true;
}
///////////////////////////////////////////////////////////////////////////////
#ifndef DISABLE_EEPROM
void Sensor::load(){
struct SensorData data;
Sensor *tt;
for(int i=0;i<EEStore::eeStore->data.nSensors;i++){
uint16_t i=EEStore::eeStore->data.nSensors;
while(i--){
EEPROM.get(EEStore::pointer(),data);
tt=create(data.snum,data.pin,data.pullUp);
tt=create(data.snum, data.pin, data.pullUp);
EEStore::advance(sizeof(tt->data));
}
}
@@ -206,8 +305,15 @@ void Sensor::store(){
EEStore::eeStore->data.nSensors++;
}
}
#endif
///////////////////////////////////////////////////////////////////////////////
Sensor *Sensor::firstSensor=NULL;
Sensor *Sensor::readingSensor=NULL;
unsigned long Sensor::lastReadCycle=0;
#ifdef USE_NOTIFY
Sensor *Sensor::firstPollSensor = NULL;
Sensor *Sensor::lastSensor = NULL;
bool Sensor::inputChangeCallbackRegistered = false;
#endif

View File

@@ -20,29 +20,76 @@
#define Sensor_h
#include "Arduino.h"
#include "IODevice.h"
#define SENSOR_DECAY 0.03
// Uncomment the following #define statement to use callback notification
// where the driver supports it.
// The principle of callback notification is to avoid the Sensor class
// having to poll the device driver cyclically for input values, and then scan
// for changes. Instead, when the driver scans the inputs, if it detects
// a change it invokes a callback function in the Sensor class. In the current
// implementation, the advantages are limited because (a) the Sensor class
// performs debounce checks, and (b) the Sensor class does not have a
// static reference to the output stream for sending <Q>/<q> messages
// when a change is detected. These restrictions mean that the checkAll()
// method still has to iterate through all of the Sensor objects looking
// for changes.
#define USE_NOTIFY
struct SensorData {
int snum;
uint8_t pin;
VPIN pin;
uint8_t pullUp;
};
struct Sensor{
static Sensor *firstSensor;
static Sensor *readingSensor;
class Sensor{
// The sensor list is a linked list where each sensor's 'nextSensor' field points to the next.
// The pointer is null in the last on the list.
public:
SensorData data;
boolean active;
byte latchdelay;
struct {
uint8_t active:1;
uint8_t inputState:1;
uint8_t latchDelay:6;
}; // bit 7=active; bit 6=input state; bits 5-0=latchDelay
static Sensor *firstSensor;
#ifdef USE_NOTIFY
static Sensor *firstPollSensor;
static Sensor *lastSensor;
#endif
// readingSensor points to the next sensor to be polled, or null if the poll cycle is completed for
// the period.
static Sensor *readingSensor;
// Constructor
Sensor();
Sensor *nextSensor;
void setState(int state);
#ifndef DISABLE_EEPROM
static void load();
static void store();
static Sensor *create(int, int, int);
static Sensor* get(int);
static bool remove(int);
static void checkAll(Print *);
static void printAll(Print *);
#endif
static Sensor *create(int id, VPIN vpin, int pullUp);
static Sensor* get(int id);
static bool remove(int id);
static void checkAll(Print *stream);
static void printAll(Print *stream);
static unsigned long lastReadCycle; // value of micros at start of last read cycle
static const unsigned int cycleInterval = 10000; // min time between consecutive reads of each sensor in microsecs.
// should not be less than device scan cycle time.
static const unsigned int minReadCount = 1; // number of additional scans before acting on change
// E.g. 1 means that a change is ignored for one scan and actioned on the next.
// Max value is 63
bool pollingRequired = true;
#ifdef USE_NOTIFY
static void inputChangeCallback(VPIN vpin, int state);
static bool inputChangeCallbackRegistered;
#endif
}; // Sensor
#endif

View File

@@ -1,4 +1,5 @@
/*
* © 2021 Restructured Neil McKechnie
* © 2013-2016 Gregg E. Berman
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth.
@@ -18,166 +19,592 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Turnouts.h"
#include "defines.h" // includes config.h
#ifndef DISABLE_EEPROM
#include "EEStore.h"
#include "PWMServoDriver.h"
#endif
#include "StringFormatter.h"
#include "RMFT2.h"
#include "Turnouts.h"
#include "DCC.h"
#include "LCN.h"
#ifdef EESTOREDEBUG
#include "DIAG.h"
#endif
// print all turnout states to stream
void Turnout::printAll(Print *stream){
for (Turnout *tt = Turnout::firstTurnout; tt != NULL; tt = tt->nextTurnout)
StringFormatter::send(stream, F("<H %d %d>\n"), tt->data.id, (tt->data.tStatus & STATUS_ACTIVE)!=0);
} // Turnout::printAll
/*
* Protected static data
*/
bool Turnout::activate(int n,bool state){
#ifdef EESTOREDEBUG
DIAG(F("Turnout::activate(%d,%d)"),n,state);
#endif
Turnout * tt=get(n);
if (tt==NULL) return false;
tt->activate(state);
turnoutlistHash++;
return true;
}
/* static */ Turnout *Turnout::_firstTurnout = 0;
bool Turnout::isActive(int n){
Turnout * tt=get(n);
if (tt==NULL) return false;
return tt->data.tStatus & STATUS_ACTIVE;
}
/*
* Public static data
*/
/* static */ int Turnout::turnoutlistHash = 0;
// activate is virtual here so that it can be overridden by a non-DCC turnout mechanism
void Turnout::activate(bool state) {
#ifdef EESTOREDEBUG
DIAG(F("Turnout::activate(%d)"),state);
#endif
if (data.address==LCN_TURNOUT_ADDRESS) {
// A LCN turnout is transmitted to the LCN master.
LCN::send('T',data.id,state);
return; // The tStatus will be updated by a message from the LCN master, later.
}
if (state)
data.tStatus|=STATUS_ACTIVE;
else
data.tStatus &= ~STATUS_ACTIVE;
if (data.tStatus & STATUS_PWM)
PWMServoDriver::setServo(data.tStatus & STATUS_PWMPIN, (data.inactiveAngle+(state?data.moveAngle:0)));
else
DCC::setAccessory(data.address,data.subAddress, state);
// Save state if stored in EEPROM
if (EEStore::eeStore->data.nTurnouts > 0 && num > 0)
EEPROM.put(num, data.tStatus);
}
///////////////////////////////////////////////////////////////////////////////
/* static */ unsigned long Turnout::_lastLoopEntry = 0;
/*
* Protected static functions
*/
Turnout* Turnout::get(int n){
Turnout *tt;
for(tt=firstTurnout;tt!=NULL && tt->data.id!=n;tt=tt->nextTurnout);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////
bool Turnout::remove(int n){
Turnout *tt,*pp=NULL;
for(tt=firstTurnout;tt!=NULL && tt->data.id!=n;pp=tt,tt=tt->nextTurnout);
if(tt==NULL) return false;
if(tt==firstTurnout)
firstTurnout=tt->nextTurnout;
else
pp->nextTurnout=tt->nextTurnout;
free(tt);
turnoutlistHash++;
return true;
}
///////////////////////////////////////////////////////////////////////////////
void Turnout::load(){
struct TurnoutData data;
Turnout *tt;
for(int i=0;i<EEStore::eeStore->data.nTurnouts;i++){
EEPROM.get(EEStore::pointer(),data);
if (data.tStatus & STATUS_PWM) tt=create(data.id,data.tStatus & STATUS_PWMPIN, data.inactiveAngle,data.moveAngle);
else tt=create(data.id,data.address,data.subAddress);
tt->data.tStatus=data.tStatus;
tt->num=EEStore::pointer()+offsetof(TurnoutData,tStatus); // Save pointer to status byte within EEPROM
EEStore::advance(sizeof(tt->data));
#ifdef EESTOREDEBUG
tt->print(tt);
#endif
}
}
///////////////////////////////////////////////////////////////////////////////
void Turnout::store(){
Turnout *tt;
tt=firstTurnout;
EEStore::eeStore->data.nTurnouts=0;
while(tt!=NULL){
#ifdef EESTOREDEBUG
tt->print(tt);
#endif
tt->num=EEStore::pointer()+offsetof(TurnoutData,tStatus); // Save pointer to tstatus byte within EEPROM
EEPROM.put(EEStore::pointer(),tt->data);
EEStore::advance(sizeof(tt->data));
tt=tt->nextTurnout;
EEStore::eeStore->data.nTurnouts++;
/* static */ Turnout *Turnout::get(uint16_t id) {
// Find turnout object from list.
for (Turnout *tt = _firstTurnout; tt != NULL; tt = tt->_nextTurnout)
if (tt->_turnoutData.id == id) return tt;
return NULL;
}
}
///////////////////////////////////////////////////////////////////////////////
Turnout *Turnout::create(int id, int add, int subAdd){
Turnout *tt=create(id);
tt->data.address=add;
tt->data.subAddress=subAdd;
tt->data.tStatus=0;
return(tt);
}
Turnout *Turnout::create(int id, byte pin, int activeAngle, int inactiveAngle){
Turnout *tt=create(id);
tt->data.tStatus= STATUS_PWM | (pin & STATUS_PWMPIN);
tt->data.inactiveAngle=inactiveAngle;
tt->data.moveAngle=activeAngle-inactiveAngle;
return(tt);
}
Turnout *Turnout::create(int id){
Turnout *tt=get(id);
if (tt==NULL) {
tt=(Turnout *)calloc(1,sizeof(Turnout));
tt->nextTurnout=firstTurnout;
firstTurnout=tt;
tt->data.id=id;
// Add new turnout to end of chain
/* static */ void Turnout::add(Turnout *tt) {
if (!_firstTurnout)
_firstTurnout = tt;
else {
// Find last object on chain
Turnout *ptr = _firstTurnout;
for ( ; ptr->_nextTurnout!=0; ptr=ptr->_nextTurnout) {}
// Line new object to last object.
ptr->_nextTurnout = tt;
}
turnoutlistHash++;
return tt;
turnoutlistHash++;
}
///////////////////////////////////////////////////////////////////////////////
//
// print debug info about the state of a turnout
//
#ifdef EESTOREDEBUG
void Turnout::print(Turnout *tt) {
if (tt->data.tStatus & STATUS_PWM )
DIAG(F("Turnout %d ZeroAngle %d MoveAngle %d Status %d"),tt->data.id, tt->data.inactiveAngle, tt->data.moveAngle,tt->data.tStatus & STATUS_ACTIVE);
else
DIAG(F("Turnout %d Addr %d Subaddr %d Status %d"),tt->data.id, tt->data.address, tt->data.subAddress,tt->data.tStatus & STATUS_ACTIVE);
}
/* static */ void Turnout::loop() {
// Here, we check whether the turnout state needs updating.
// For Servo and VPIN turnouts, we need to check the isBusy() status of the device
// and when it goes to 0 we can send out a turnout notification.
// We allow a small delay before we start checking isBusy(), to allow the
// operation time to start.
unsigned long currentMillis = millis();
// Check for updates once every 100ms.
if (currentMillis - _lastLoopEntry > 100) {
_lastLoopEntry = currentMillis;
Turnout *tt = _firstTurnout;
for ( ; tt != 0; tt=tt->_nextTurnout) {
if (tt->_delayResponse > 1) {
tt->_delayResponse--;
} else if (tt->_delayResponse == 1) {
// Pointer has been triggered and grace time has elapsed, so check if completed
if (!tt->isPending()) {
tt->sendResponse();
}
}
}
}
}
// Function called when the turnout has changed.
void Turnout::sendResponse() {
turnoutlistHash++; // let withrottle know something changed
#if defined(RMFT_ACTIVE)
RMFT2::turnoutEvent(_turnoutData.id, _turnoutData.closed);
#endif
Turnout *Turnout::firstTurnout=NULL;
int Turnout::turnoutlistHash=0; //bump on every change so clients know when to refresh their lists
// Send message to JMRI etc. over Serial USB. This is done here
// to ensure that the message is sent when the turnout operation
// is not initiated by a Serial command.
printState(&Serial);
// Clear flag pending operation
_delayResponse = 0;
}
// For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed;
void Turnout::printState(Print *stream) {
StringFormatter::send(stream, F("<H %d %d>\n"),
_turnoutData.id, !_turnoutData.closed);
}
// Remove nominated turnout from turnout linked list and delete the object.
/* static */ bool Turnout::remove(uint16_t id) {
Turnout *tt,*pp=NULL;
for(tt=_firstTurnout; tt!=NULL && tt->_turnoutData.id!=id; pp=tt, tt=tt->_nextTurnout) {}
if (tt == NULL) return false;
if (tt == _firstTurnout)
_firstTurnout = tt->_nextTurnout;
else
pp->_nextTurnout = tt->_nextTurnout;
delete (ServoTurnout *)tt;
turnoutlistHash++;
return true;
}
/*
* Public static functions
*/
/* static */ bool Turnout::isClosed(uint16_t id) {
Turnout *tt = get(id);
if (tt)
return tt->isClosed();
else
return false;
}
/* static */ bool Turnout::setClosedStateOnly(uint16_t id, bool closeFlag) {
Turnout *tt = get(id);
if (!tt) return false;
tt->_turnoutData.closed = closeFlag;
// I know it says setClosedStateOnly, but we need to tell others
// that the state has changed too.
#if defined(RMFT_ACTIVE)
RMFT2::turnoutEvent(id, closeFlag);
#endif
// Send message to JMRI etc. over Serial USB. This is done here
// to ensure that the message is sent when the turnout operation
// is not initiated by a Serial command.
tt->printState(&Serial);
return true;
}
// Static setClosed function is invoked from close(), throw() etc. to perform the
// common parts of the turnout operation. Code which is specific to a turnout
// type should be placed in the virtual function setClosedInternal(bool) which is
// called from here.
/* static */ bool Turnout::setClosed(uint16_t id, bool closeFlag) {
#if defined(DIAG_IO)
if (closeFlag)
DIAG(F("Turnout::close(%d)"), id);
else
DIAG(F("Turnout::throw(%d)"), id);
#endif
Turnout *tt = Turnout::get(id);
if (!tt) return false;
bool ok = tt->setClosedInternal(closeFlag);
// Check return value to see if this type of turnout is immediate
// or if we need to check for completion.
if (ok) {
tt->sendResponse();
} else {
// Set flag indicating that the operation is pending and needs to be checked for completion
// starting in 4 loop counts (starts checking at 1).
tt->_delayResponse = 5;
}
turnoutlistHash++; // let withrottle know something changed
#ifndef DISABLE_EEPROM
// Write byte containing new closed/thrown state to EEPROM if required. Note that eepromAddress
// is always zero for LCN turnouts.
if (EEStore::eeStore->data.nTurnouts > 0 && tt->_eepromAddress > 0)
EEPROM.put(tt->_eepromAddress, tt->_turnoutData.flags);
#endif
return true;
}
#ifndef DISABLE_EEPROM
// Load all turnout objects
/* static */ void Turnout::load() {
for (uint16_t i=0; i<EEStore::eeStore->data.nTurnouts; i++) {
Turnout::loadTurnout();
}
}
// Save all turnout objects
/* static */ void Turnout::store() {
EEStore::eeStore->data.nTurnouts=0;
for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout) {
tt->save();
EEStore::eeStore->data.nTurnouts++;
}
}
// Load one turnout from EEPROM
/* static */ Turnout *Turnout::loadTurnout () {
Turnout *tt = 0;
// Read turnout type from EEPROM
struct TurnoutData turnoutData;
int eepromAddress = EEStore::pointer() + offsetof(struct TurnoutData, flags); // Address of byte containing the closed flag.
EEPROM.get(EEStore::pointer(), turnoutData);
EEStore::advance(sizeof(turnoutData));
switch (turnoutData.turnoutType) {
case TURNOUT_SERVO:
// Servo turnout
tt = ServoTurnout::load(&turnoutData);
break;
case TURNOUT_DCC:
// DCC Accessory turnout
tt = DCCTurnout::load(&turnoutData);
break;
case TURNOUT_VPIN:
// VPIN turnout
tt = VpinTurnout::load(&turnoutData);
break;
default:
// If we find anything else, then we don't know what it is or how long it is,
// so we can't go any further through the EEPROM!
return NULL;
}
if (tt) {
// Save EEPROM address in object. Note that LCN turnouts always have eepromAddress of zero.
tt->_eepromAddress = eepromAddress + offsetof(struct TurnoutData, flags);
}
#ifdef EESTOREDEBUG
printAll(&Serial);
#endif
return tt;
}
#endif
// Display, on the specified stream, the current state of the turnout (1=thrown or 0=closed).
/* static */ void Turnout::printState(uint16_t id, Print *stream) {
Turnout *tt = get(id);
if (tt) tt->printState(stream);
}
/*************************************************************************************
* ServoTurnout - Turnout controlled by servo device.
*
*************************************************************************************/
// Private Constructor
ServoTurnout::ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed) :
Turnout(id, TURNOUT_SERVO, closed)
{
_servoTurnoutData.vpin = vpin;
_servoTurnoutData.thrownPosition = thrownPosition;
_servoTurnoutData.closedPosition = closedPosition;
_servoTurnoutData.profile = profile;
}
// Create function
/* static */ Turnout *ServoTurnout::create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed) {
#ifndef IO_NO_HAL
Turnout *tt = get(id);
if (tt) {
// Object already exists, check if it is usable
if (tt->isType(TURNOUT_SERVO)) {
// Yes, so set parameters
ServoTurnout *st = (ServoTurnout *)tt;
st->_servoTurnoutData.vpin = vpin;
st->_servoTurnoutData.thrownPosition = thrownPosition;
st->_servoTurnoutData.closedPosition = closedPosition;
st->_servoTurnoutData.profile = profile;
// Don't touch the _closed parameter, retain the original value.
// We don't really need to do the following, since a call to IODevice::_writeAnalogue
// will provide all the data that is required! However, if someone has configured
// a Turnout, we should ensure that the SET() RESET() and other commands that use write()
// behave consistently with the turnout commands.
IODevice::configureServo(vpin, thrownPosition, closedPosition, profile, 0, closed);
// Set position directly to specified position - we don't know where it is moving from.
IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant);
return tt;
} else {
// Incompatible object, delete and recreate
remove(id);
}
}
tt = (Turnout *)new ServoTurnout(id, vpin, thrownPosition, closedPosition, profile, closed);
IODevice::writeAnalogue(vpin, closed ? closedPosition : thrownPosition, PCA9685::Instant);
return tt;
#else
(void)id; (void)vpin; (void)thrownPosition; (void)closedPosition;
(void)profile; (void)closed; // avoid compiler warnings.
return NULL;
#endif
}
// Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point.
Turnout *ServoTurnout::load(struct TurnoutData *turnoutData) {
#ifndef DISABLE_EEPROM
ServoTurnoutData servoTurnoutData;
// Read class-specific data from EEPROM
EEPROM.get(EEStore::pointer(), servoTurnoutData);
EEStore::advance(sizeof(servoTurnoutData));
// Create new object
Turnout *tt = ServoTurnout::create(turnoutData->id, servoTurnoutData.vpin, servoTurnoutData.thrownPosition,
servoTurnoutData.closedPosition, servoTurnoutData.profile, turnoutData->closed);
return tt;
#else
(void)turnoutData;
return NULL;
#endif
}
// For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed
void ServoTurnout::print(Print *stream) {
StringFormatter::send(stream, F("<H %d SERVO %d %d %d %d %d>\n"), _turnoutData.id, _servoTurnoutData.vpin,
_servoTurnoutData.thrownPosition, _servoTurnoutData.closedPosition, _servoTurnoutData.profile,
!_turnoutData.closed);
}
// ServoTurnout-specific code for throwing or closing a servo turnout.
bool ServoTurnout::setClosedInternal(bool close) {
#ifndef IO_NO_HAL
IODevice::writeAnalogue(_servoTurnoutData.vpin,
close ? _servoTurnoutData.closedPosition : _servoTurnoutData.thrownPosition, _servoTurnoutData.profile);
_turnoutData.closed = close;
return false; // Don't send update messages yet.
#else
(void)close; // avoid compiler warnings
return true;
#endif
}
// Determine if the turnout is moving into its new state.
bool ServoTurnout::isPending() {
return IODevice::isBusy(_servoTurnoutData.vpin);
}
void ServoTurnout::save() {
#ifndef DISABLE_EEPROM
// Write turnout definition and current position to EEPROM
// First write common servo data, then
// write the servo-specific data
EEPROM.put(EEStore::pointer(), _turnoutData);
EEStore::advance(sizeof(_turnoutData));
EEPROM.put(EEStore::pointer(), _servoTurnoutData);
EEStore::advance(sizeof(_servoTurnoutData));
#endif
}
/*************************************************************************************
* DCCTurnout - Turnout controlled by DCC Accessory Controller.
*
*************************************************************************************/
#if defined(DCC_TURNOUTS_RCN_213)
const bool DCCTurnout::rcn213Compliant = true;
#else
const bool DCCTurnout::rcn213Compliant = false;
#endif
// DCCTurnoutData contains data specific to this subclass that is
// written to EEPROM when the turnout is saved.
struct DCCTurnoutData {
// DCC address (Address in bits 15-2, subaddress in bits 1-0
uint16_t address; // CS currently supports linear address 1-2048
// That's DCC accessory address 1-512 and subaddress 0-3.
} _dccTurnoutData; // 2 bytes
// Constructor
DCCTurnout::DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd) :
Turnout(id, TURNOUT_DCC, false)
{
_dccTurnoutData.address = address;
_dccTurnoutData.subAddress = subAdd;
}
// Create function
/* static */ Turnout *DCCTurnout::create(uint16_t id, uint16_t add, uint8_t subAdd) {
Turnout *tt = get(id);
if (tt) {
// Object already exists, check if it is usable
if (tt->isType(TURNOUT_DCC)) {
// Yes, so set parameters<T>
DCCTurnout *dt = (DCCTurnout *)tt;
dt->_dccTurnoutData.address = add;
dt->_dccTurnoutData.subAddress = subAdd;
// Don't touch the _closed parameter, retain the original value.
return tt;
} else {
// Incompatible object, delete and recreate
remove(id);
}
}
tt = (Turnout *)new DCCTurnout(id, add, subAdd);
return tt;
}
// Load a DCC turnout definition from EEPROM. The common Turnout data has already been read at this point.
/* static */ Turnout *DCCTurnout::load(struct TurnoutData *turnoutData) {
#ifndef DISABLE_EEPROM
DCCTurnoutData dccTurnoutData;
// Read class-specific data from EEPROM
EEPROM.get(EEStore::pointer(), dccTurnoutData);
EEStore::advance(sizeof(dccTurnoutData));
// Create new object
DCCTurnout *tt = new DCCTurnout(turnoutData->id, dccTurnoutData.address, dccTurnoutData.subAddress);
return tt;
#else
(void)turnoutData;
return NULL;
#endif
}
void DCCTurnout::print(Print *stream) {
StringFormatter::send(stream, F("<H %d DCC %d %d %d>\n"), _turnoutData.id,
_dccTurnoutData.address, _dccTurnoutData.subAddress, !_turnoutData.closed);
// Also report using classic DCC++ syntax for DCC accessory turnouts, since JMRI expects this.
StringFormatter::send(stream, F("<H %d %d %d %d>\n"), _turnoutData.id,
_dccTurnoutData.address, _dccTurnoutData.subAddress, !_turnoutData.closed);
}
bool DCCTurnout::setClosedInternal(bool close) {
// DCC++ Classic behaviour is that Throw writes a 1 in the packet,
// and Close writes a 0.
// RCN-213 specifies that Throw is 0 and Close is 1.
DCC::setAccessory(_dccTurnoutData.address, _dccTurnoutData.subAddress, close ^ !rcn213Compliant);
_turnoutData.closed = close;
return true;
}
void DCCTurnout::save() {
#ifndef DISABLE_EEPROM
// Write turnout definition and current position to EEPROM
// First write common servo data, then
// write the servo-specific data
EEPROM.put(EEStore::pointer(), _turnoutData);
EEStore::advance(sizeof(_turnoutData));
EEPROM.put(EEStore::pointer(), _dccTurnoutData);
EEStore::advance(sizeof(_dccTurnoutData));
#endif
}
/*************************************************************************************
* VpinTurnout - Turnout controlled through a HAL vpin.
*
*************************************************************************************/
// Constructor
VpinTurnout::VpinTurnout(uint16_t id, VPIN vpin, bool closed) :
Turnout(id, TURNOUT_VPIN, closed)
{
_vpinTurnoutData.vpin = vpin;
}
// Create function
/* static */ Turnout *VpinTurnout::create(uint16_t id, VPIN vpin, bool closed) {
Turnout *tt = get(id);
if (tt) {
// Object already exists, check if it is usable
if (tt->isType(TURNOUT_VPIN)) {
// Yes, so set parameters
VpinTurnout *vt = (VpinTurnout *)tt;
vt->_vpinTurnoutData.vpin = vpin;
// Don't touch the _closed parameter, retain the original value.
return tt;
} else {
// Incompatible object, delete and recreate
remove(id);
}
}
tt = (Turnout *)new VpinTurnout(id, vpin, closed);
return tt;
}
// Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point.
/* static */ Turnout *VpinTurnout::load(struct TurnoutData *turnoutData) {
#ifndef DISABLE_EEPROM
VpinTurnoutData vpinTurnoutData;
// Read class-specific data from EEPROM
EEPROM.get(EEStore::pointer(), vpinTurnoutData);
EEStore::advance(sizeof(vpinTurnoutData));
// Create new object
VpinTurnout *tt = new VpinTurnout(turnoutData->id, vpinTurnoutData.vpin, turnoutData->closed);
return tt;
#else
(void)turnoutData;
return NULL;
#endif
}
// Report 1 for thrown, 0 for closed.
void VpinTurnout::print(Print *stream) {
StringFormatter::send(stream, F("<H %d VPIN %d %d>\n"), _turnoutData.id, _vpinTurnoutData.vpin,
!_turnoutData.closed);
}
// setClosedInternal is called from the base class's setClosed() method. It returns true if the
// operation is considered to be completed, and false if it is started but not completed.
// For VPINs which are attached to servos, we ought to return false so that the confirmation
// message to JMRI is deferred until the servo movement is completed. However, if we do this
// it will also affect VPINs that are GPIO pins; when it polls the pin it will put the pin into
// input mode, which is not desirable. Hence, we return true here so that no polling takes place.
bool VpinTurnout::setClosedInternal(bool close) {
IODevice::write(_vpinTurnoutData.vpin, close);
_turnoutData.closed = close;
return true;
}
bool VpinTurnout::isPending() {
return IODevice::isBusy(_vpinTurnoutData.vpin);
}
void VpinTurnout::save() {
#ifndef DISABLE_EEPROM
// Write turnout definition and current position to EEPROM
// First write common servo data, then
// write the servo-specific data
EEPROM.put(EEStore::pointer(), _turnoutData);
EEStore::advance(sizeof(_turnoutData));
EEPROM.put(EEStore::pointer(), _vpinTurnoutData);
EEStore::advance(sizeof(_vpinTurnoutData));
#endif
}
/*************************************************************************************
* LCNTurnout - Turnout controlled by Loconet
*
*************************************************************************************/
// LCNTurnout has no specific data, and in any case is not written to EEPROM!
// struct LCNTurnoutData {
// } _lcnTurnoutData; // 0 bytes
// Constructor
LCNTurnout::LCNTurnout(uint16_t id, bool closed) :
Turnout(id, TURNOUT_LCN, closed)
{ }
// Create function
/* static */ Turnout *LCNTurnout::create(uint16_t id, bool closed) {
Turnout *tt = get(id);
if (tt) {
// Object already exists, check if it is usable
if (tt->isType(TURNOUT_LCN)) {
// Yes, so return this object
return tt;
} else {
// Incompatible object, delete and recreate
remove(id);
}
}
tt = (Turnout *)new LCNTurnout(id, closed);
return tt;
}
bool LCNTurnout::setClosedInternal(bool close) {
// Assume that the LCN command still uses 1 for throw and 0 for close...
LCN::send('T', _turnoutData.id, !close);
// The _turnoutData.closed flag should be updated by a message from the LCN master, later.
return true;
}
// LCN turnouts not saved to EEPROM.
//void save() override { }
//static Turnout *load(struct TurnoutData *turnoutData) {
// Report 1 for thrown, 0 for closed.
void LCNTurnout::print(Print *stream) {
StringFormatter::send(stream, F("<H %d LCN %d>\n"), _turnoutData.id,
!_turnoutData.closed);
}

View File

@@ -1,4 +1,6 @@
/*
* © 2021 Restructured Neil McKechnie
* © 2013-2016 Gregg E. Berman
* © 2020, Chris Harlow. All rights reserved.
*
* This file is part of Asbelos DCC API
@@ -16,46 +18,298 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef Turnouts_h
#define Turnouts_h
#include <Arduino.h>
#include "DCC.h"
#include "LCN.h"
#ifndef TURNOUTS_H
#define TURNOUTS_H
const byte STATUS_ACTIVE=0x80; // Flag as activated
const byte STATUS_PWM=0x40; // Flag as a PWM turnout
const byte STATUS_PWMPIN=0x3F; // PWM pin 0-63
const int LCN_TURNOUT_ADDRESS=-1; // spoof dcc address -1 indicates a LCN turnout
struct TurnoutData {
int id;
uint8_t tStatus; // has STATUS_ACTIVE, STATUS_PWM, STATUS_PWMPIN
union {uint8_t subAddress; char moveAngle;}; //DCC sub addrerss or PWM difference from inactiveAngle
union {int address; int inactiveAngle;}; // DCC address or PWM servo angle
//#define EESTOREDEBUG
#include "Arduino.h"
#include "IODevice.h"
// Turnout type definitions
enum {
TURNOUT_DCC = 1,
TURNOUT_SERVO = 2,
TURNOUT_VPIN = 3,
TURNOUT_LCN = 4,
};
/*************************************************************************************
* Turnout - Base class for turnouts.
*
*************************************************************************************/
class Turnout {
public:
static Turnout *firstTurnout;
protected:
/*
* Object data
*/
// The TurnoutData struct contains data common to all turnout types, that
// is written to EEPROM when the turnout is saved.
// The first byte of this struct contains the 'closed' flag which is
// updated whenever the turnout changes from thrown to closed and
// vice versa. If the turnout has been saved, then this byte is rewritten
// when changed in RAM. The 'closed' flag must be located in the first byte.
struct TurnoutData {
union {
struct {
bool closed : 1;
bool _rfu: 2;
uint8_t turnoutType : 5;
};
uint8_t flags;
};
uint16_t id;
} _turnoutData; // 3 bytes
// Address in eeprom of first byte of the _turnoutData struct (containing the closed flag).
// Set to zero if the object has not been saved in EEPROM, e.g. for newly created Turnouts, and
// for all LCN turnouts.
uint16_t _eepromAddress = 0;
// Pointer to next turnout on linked list.
Turnout *_nextTurnout = 0;
// Delay counter for responses. If non-zero, there is a pending response to be sent.
uint8_t _delayResponse = 0;
/*
* Constructor
*/
Turnout(uint16_t id, uint8_t turnoutType, bool closed) {
_turnoutData.id = id;
_turnoutData.turnoutType = turnoutType;
_turnoutData.closed = closed;
add(this);
}
/*
* Static data
*/
static Turnout *_firstTurnout;
static int _turnoutlistHash;
static unsigned long _lastLoopEntry;
/*
* Virtual functions
*/
virtual bool setClosedInternal(bool close) = 0; // Mandatory in subclass
virtual void save() {}
virtual bool isPending() { return false; };
/*
* Static functions
*/
static Turnout *get(uint16_t id);
static void add(Turnout *tt);
/*
* Other functions
*/
void sendResponse();
public:
/*
* Static data
*/
static int turnoutlistHash;
TurnoutData data;
Turnout *nextTurnout;
static bool activate(int n, bool state);
static Turnout* get(int);
static bool remove(int);
static bool isActive(int);
static void load();
static void store();
static Turnout *create(int id , int address , int subAddress);
static Turnout *create(int id , byte pin , int activeAngle, int inactiveAngle);
static Turnout *create(int id);
void activate(bool state);
static void printAll(Print *);
#ifdef EESTOREDEBUG
void print(Turnout *tt);
#endif
private:
int num; // EEPROM address of tStatus in TurnoutData struct, or zero if not stored.
}; // Turnout
static const bool useClassicTurnoutCommands;
/*
* Public base class functions
*/
inline bool isClosed() { return _turnoutData.closed; };
inline bool isThrown() { return !_turnoutData.closed; }
inline bool isType(uint8_t type) { return _turnoutData.turnoutType == type; }
inline uint16_t getId() { return _turnoutData.id; }
inline Turnout *next() { return _nextTurnout; }
void printState(Print *stream);
/*
* Virtual functions
*/
virtual void print(Print *stream) {
(void)stream; // avoid compiler warnings.
}
virtual ~Turnout() {} // Destructor
/*
* Public static functions
*/
inline static bool exists(uint16_t id) { return get(id) != 0; }
static bool remove(uint16_t id);
static bool isClosed(uint16_t id);
inline static bool isThrown(uint16_t id) {
return !isClosed(id);
}
static bool setClosed(uint16_t id, bool closeFlag);
inline static bool setClosed(uint16_t id) {
return setClosed(id, true);
}
inline static bool setThrown(uint16_t id) {
return setClosed(id, false);
}
static bool setClosedStateOnly(uint16_t id, bool close);
inline static Turnout *first() { return _firstTurnout; }
static void loop();
#ifndef DISABLE_EEPROM
// Load all turnout definitions.
static void load();
// Load one turnout definition
static Turnout *loadTurnout();
// Save all turnout definitions
static void store();
#endif
static void printAll(Print *stream) {
for (Turnout *tt = _firstTurnout; tt != 0; tt = tt->_nextTurnout)
tt->printState(stream);
}
static void printState(uint16_t id, Print *stream);
};
/*************************************************************************************
* ServoTurnout - Turnout controlled by servo device.
*
*************************************************************************************/
class ServoTurnout : public Turnout {
private:
// ServoTurnoutData contains data specific to this subclass that is
// written to EEPROM when the turnout is saved.
struct ServoTurnoutData {
VPIN vpin;
uint16_t closedPosition : 12;
uint16_t thrownPosition : 12;
uint8_t profile;
} _servoTurnoutData; // 6 bytes
// Constructor
ServoTurnout(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed);
public:
// Create function
static Turnout *create(uint16_t id, VPIN vpin, uint16_t thrownPosition, uint16_t closedPosition, uint8_t profile, bool closed=true);
// Load a Servo turnout definition from EEPROM. The common Turnout data has already been read at this point.
static Turnout *load(struct TurnoutData *turnoutData);
void print(Print *stream) override;
protected:
// ServoTurnout-specific code for throwing or closing a servo turnout.
bool setClosedInternal(bool close) override;
void save() override;
bool isPending() override;
};
/*************************************************************************************
* DCCTurnout - Turnout controlled by DCC Accessory Controller.
*
*************************************************************************************/
class DCCTurnout : public Turnout {
private:
// DCCTurnoutData contains data specific to this subclass that is
// written to EEPROM when the turnout is saved.
struct DCCTurnoutData {
// DCC address (Address in bits 15-2, subaddress in bits 1-0)
struct {
uint16_t address : 14;
uint8_t subAddress : 2;
};
} _dccTurnoutData; // 2 bytes
// Constructor
DCCTurnout(uint16_t id, uint16_t address, uint8_t subAdd);
public:
// Create function
static Turnout *create(uint16_t id, uint16_t add, uint8_t subAdd);
// Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point.
static Turnout *load(struct TurnoutData *turnoutData);
void print(Print *stream) override;
// Flag whether DCC Accessory packets are to contain 1=close/0=throw(RCN-213) or 1=throw/0-close (DCC++ Classic)
static const bool rcn213Compliant;
protected:
bool setClosedInternal(bool close) override;
void save() override;
};
/*************************************************************************************
* VpinTurnout - Turnout controlled through a HAL vpin.
*
*************************************************************************************/
class VpinTurnout : public Turnout {
private:
// VpinTurnoutData contains data specific to this subclass that is
// written to EEPROM when the turnout is saved.
struct VpinTurnoutData {
VPIN vpin;
} _vpinTurnoutData; // 2 bytes
// Constructor
VpinTurnout(uint16_t id, VPIN vpin, bool closed);
public:
// Create function
static Turnout *create(uint16_t id, VPIN vpin, bool closed=true);
// Load a VPIN turnout definition from EEPROM. The common Turnout data has already been read at this point.
static Turnout *load(struct TurnoutData *turnoutData);
void print(Print *stream) override;
protected:
bool setClosedInternal(bool close) override;
void save() override;
bool isPending() override;
};
/*************************************************************************************
* LCNTurnout - Turnout controlled by Loconet
*
*************************************************************************************/
class LCNTurnout : public Turnout {
private:
// LCNTurnout has no specific data, and in any case is not written to EEPROM!
// struct LCNTurnoutData {
// } _lcnTurnoutData; // 0 bytes
// Constructor
LCNTurnout(uint16_t id, bool closed);
public:
// Create function
static Turnout *create(uint16_t id, bool closed=true);
bool setClosedInternal(bool close) override;
// LCN turnouts not saved to EEPROM.
//void save() override { }
//static Turnout *load(struct TurnoutData *turnoutData) {
void print(Print *stream) override;
};
#endif

View File

@@ -40,6 +40,7 @@
* WiThrottle.h sets the max locos per client at 10, this is ok to increase but requires just an extra 3 bytes per loco per client.
*/
#include <Arduino.h>
#include "defines.h"
#include "WiThrottle.h"
#include "DCC.h"
#include "DCCWaveform.h"
@@ -48,12 +49,13 @@
#include "DIAG.h"
#include "GITHUB_SHA.h"
#include "version.h"
#include "RMFT2.h"
#define LOOPLOCOS(THROTTLECHAR, CAB) for (int loco=0;loco<MAX_MY_LOCO;loco++) \
if ((myLocos[loco].throttle==THROTTLECHAR || '*'==THROTTLECHAR) && (CAB<0 || myLocos[loco].cab==CAB))
WiThrottle * WiThrottle::firstThrottle=NULL;
bool WiThrottle::annotateLeftRight=false;
WiThrottle* WiThrottle::getThrottle( int wifiClient) {
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
@@ -83,6 +85,8 @@ WiThrottle::WiThrottle( int wificlientid) {
initSent=false; // prevent sending heartbeats before connection completed
heartBeatEnable=false; // until client turns it on
turnoutListHash = -1; // make sure turnout list is sent once
exRailSent=false;
mostRecentCab=0;
for (int loco=0;loco<MAX_MY_LOCO; loco++) myLocos[loco].throttle='\0';
}
@@ -116,12 +120,21 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
// Send turnout list if changed since last sent (will replace list on client)
if (turnoutListHash != Turnout::turnoutlistHash) {
StringFormatter::send(stream,F("PTL"));
for(Turnout *tt=Turnout::firstTurnout;tt!=NULL;tt=tt->nextTurnout){
StringFormatter::send(stream,F("]\\[%d}|{%d}|{%c"), tt->data.id, tt->data.id, Turnout::isActive(tt->data.id)?'4':'2');
for(Turnout *tt=Turnout::first();tt!=NULL;tt=tt->next()){
int id=tt->getId();
StringFormatter::send(stream,F("]\\[%d}|{%d}|{%c"), id, id, Turnout::isClosed(id)?'2':'4');
}
StringFormatter::send(stream,F("\n"));
turnoutListHash = Turnout::turnoutlistHash; // keep a copy of hash for later comparison
}
else if (!exRailSent) {
// Send ExRail routes list if not already sent (but not at same time as turnouts above)
exRailSent=true;
#ifdef RMFT_ACTIVE
RMFT2::emitWithrottleRouteList(stream);
#endif
}
}
while (cmd[0]) {
@@ -138,25 +151,40 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
StringFormatter::send(stream,F("PPA%x\n"),DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON);
lastPowerState = (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON); //remember power state sent for comparison later
}
#if defined(RMFT_ACTIVE)
else if (cmd[1]=='R' && cmd[2]=='A' && cmd[3]=='2' ) { // Route activate
// exrail routes are RA2Rn , Animations are RA2An
int route=getInt(cmd+5);
uint16_t cab=cmd[4]=='A' ? mostRecentCab : 0;
RMFT2::createNewTask(route, cab);
}
#endif
else if (cmd[1]=='T' && cmd[2]=='A') { // PTA accessory toggle
int id=getInt(cmd+4);
bool newstate=false;
Turnout * tt=Turnout::get(id);
if (!tt) {
if (!Turnout::exists(id)) {
// If turnout does not exist, create it
int addr = ((id - 1) / 4) + 1;
int subaddr = (id - 1) % 4;
Turnout::create(id,addr,subaddr);
DCCTurnout::create(id,addr,subaddr);
StringFormatter::send(stream, F("HmTurnout %d created\n"),id);
}
switch (cmd[3]) {
case 'T': newstate=true; break;
case 'C': newstate=false; break;
case '2': newstate=!Turnout::isActive(id);
// T and C according to RCN-213 where 0 is Stop, Red, Thrown, Diverging.
case 'T':
Turnout::setClosed(id,false);
break;
case 'C':
Turnout::setClosed(id,true);
break;
case '2':
Turnout::setClosed(id,!Turnout::isClosed(id));
break;
default :
Turnout::setClosed(id,true);
break;
}
Turnout::activate(id,newstate);
StringFormatter::send(stream, F("PTA%c%d\n"),newstate?'4':'2',id );
}
StringFormatter::send(stream, F("PTA%c%d\n"),Turnout::isClosed(id)?'2':'4',id );
}
break;
case 'N': // Heartbeat (2), only send if connection completed by 'HU' message
if (initSent) {
@@ -170,8 +198,7 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
if (cmd[1] == 'U') {
StringFormatter::send(stream,F("VN2.0\nHTDCC-EX\nRL0\n"));
StringFormatter::send(stream,F("HtDCC-EX v%S, %S, %S, %S\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));
if (annotateLeftRight) StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[Left}|{2]\\[Right}|{4\n"));
else StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[Closed}|{2]\\[Thrown}|{4\n"));
StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[THROW}|{2]\\[CLOSE}|{4\n"));
StringFormatter::send(stream,F("PPA%x\n"),DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON);
lastPowerState = (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON); //remember power state sent for comparison later
StringFormatter::send(stream,F("*%d\n"),HEARTBEAT_SECONDS);
@@ -244,6 +271,7 @@ void WiThrottle::multithrottle(RingStream * stream, byte * cmd){
if (myLocos[loco].throttle=='\0') {
myLocos[loco].throttle=throttleChar;
myLocos[loco].cab=locoid;
mostRecentCab=locoid;
StringFormatter::send(stream, F("M%c+%c%d<;>\n"), throttleChar, cmd[3] ,locoid); //tell client to add loco
//Get known Fn states from DCC
for(int fKey=0; fKey<=28; fKey++) {
@@ -278,6 +306,7 @@ void WiThrottle::locoAction(RingStream * stream, byte* aval, char throttleChar,
{
int witSpeed=getInt(aval+1);
LOOPLOCOS(throttleChar, cab) {
mostRecentCab=myLocos[loco].cab;
DCC::setThrottle(myLocos[loco].cab, WiTToDCCSpeed(witSpeed), DCC::getThrottleDirection(myLocos[loco].cab));
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, witSpeed);
}
@@ -311,7 +340,8 @@ void WiThrottle::locoAction(RingStream * stream, byte* aval, char throttleChar,
case 'R':
{
bool forward=aval[1]!='0';
LOOPLOCOS(throttleChar, cab) {
LOOPLOCOS(throttleChar, cab) {
mostRecentCab=myLocos[loco].cab;
DCC::setThrottle(myLocos[loco].cab, DCC::getThrottleSpeed(myLocos[loco].cab), forward);
StringFormatter::send(stream,F("M%cA%c%d<;>R%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, forward);
}
@@ -327,6 +357,7 @@ void WiThrottle::locoAction(RingStream * stream, byte* aval, char throttleChar,
case 'I': // Idle, set speed to 0
case 'Q': // Quit, set speed to 0
LOOPLOCOS(throttleChar, cab) {
mostRecentCab=myLocos[loco].cab;
DCC::setThrottle(myLocos[loco].cab, 0, DCC::getThrottleDirection(myLocos[loco].cab));
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, 0);
}

View File

@@ -31,7 +31,7 @@ class WiThrottle {
static void loop(RingStream * stream);
void parse(RingStream * stream, byte * cmd);
static WiThrottle* getThrottle( int wifiClient);
static bool annotateLeftRight;
private:
WiThrottle( int wifiClientId);
~WiThrottle();
@@ -53,6 +53,8 @@ class WiThrottle {
bool heartBeatEnable;
unsigned long heartBeat;
bool initSent; // valid connection established
bool exRailSent; // valid connection established
uint16_t mostRecentCab;
int turnoutListHash; // used to check for changes to turnout list
bool lastPowerState; // last power state sent to this client
int DCCToWiTSpeed(int DCCSpeed);

View File

@@ -85,7 +85,9 @@ void WifiInboundHandler::loop1() {
CommandDistributor::parse(clientId,cmd,outboundRing);
// The commit call will either write the lenbgth bytes
// OR rollback to the mark because the reply is empty or commend generated more than fits the buffer
outboundRing->commit();
if (!outboundRing->commit()) {
DIAG(F("OUTBOUND FULL processing cmd:%s"),cmd);
}
return;
}
}
@@ -243,7 +245,7 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
void WifiInboundHandler::purgeCurrentCIPSEND() {
// A CIPSEND was sent but errored... or the client closed just toss it away
if (Diag::WIFI) DIAG(F("Wifi: DROPPING CIPSEND=%d,%d"),clientPendingCIPSEND,currentReplySize);
DIAG(F("Wifi: DROPPING CIPSEND=%d,%d"),clientPendingCIPSEND,currentReplySize);
for (int i=0;i<=currentReplySize;i++) outboundRing->read();
pendingCipsend=false;
clientPendingCIPSEND=-1;

View File

@@ -1,3 +1,22 @@
/*
* (c) 2021 Fred Decker. All rights reserved.
* (c) 2020 Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef WifiInboundHandler_h
#define WifiInboundHandler_h

View File

@@ -1,7 +1,23 @@
/**********************************************************************
/*
* COPYRIGHT (c) 2020 Fred Decker
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
config.h
COPYRIGHT (c) 2020 Fred Decker
/**********************************************************************
The configuration file for DCC-EX Command Station
@@ -97,18 +113,48 @@ The configuration file for DCC-EX Command Station
//
// DEFINE LCD SCREEN USAGE BY THE BASE STATION
//
// Note: This feature requires an I2C enabled LCD screen using a PCF8574 based chipset.
// or one using a Hitachi HD44780.
// OR an I2C Oled screen.
// To enable, uncomment one of the lines below
// Note: This feature requires an I2C enabled LCD screen using a Hitachi HD44780
// controller and a commonly available PCF8574 based I2C 'backpack'.
// To enable, uncomment one of the #define lines below
// define LCD_DRIVER for I2C LCD address 0x3f,16 cols, 2 rows
// #define LCD_DRIVER 0x3F,16,2
// define LCD_DRIVER for I2C address 0x27, 16 cols, 2 rows
// #define LCD_DRIVER 0x27,16,2
//OR define OLED_DRIVER width,height in pixels (address auto detected)
// 128x32 or 128x64 I2C SSD1306-based devices are supported.
// Also 132x64 I2C SH1106 devices.
// Also 132x64 I2C SH1106 devices
// #define OLED_DRIVER 128,32
/////////////////////////////////////////////////////////////////////////////////////
// Define scroll mode as 0, 1 or 2
#define SCROLLMODE 1
/////////////////////////////////////////////////////////////////////////////////////
// DISABLE EEPROM
//
// If you do not need the EEPROM at all, you can disable all the code that saves
// data in the EEPROM. You might want to do that if you are in a Arduino UNO
// and want to use the EX-RAIL automation. Otherwise you do not have enough RAM
// to do that. Of course, then none of the EEPROM related commands works.
//
// #define DISABLE_EEPROM
/////////////////////////////////////////////////////////////////////////////////////
//
// DEFINE TURNOUTS/ACCESSORIES FOLLOW NORM RCN-213
//
// According to norm RCN-213 a DCC packet with a 1 is closed/straight
// and one with a 0 is thrown/diverging. In DCC++ Classic, and in previous
// versions of DCC++EX, a turnout throw command was implemented in the packet as
// '1' and a close command as '0'. The #define below makes the states
// match with the norm. But we don't want to cause havoc on existent layouts,
// so we define this only for new installations. If you don't want this,
// don't add it to your config.h.
//#define DCC_TURNOUTS_RCN_213
// The following #define likewise inverts the behaviour of the <a> command
// for triggering DCC Accessory Decoders, so that <a addr subaddr 0> generates a
// DCC packet with D=1 (close turnout) and <a addr subaddr 1> generates D=0
// (throw turnout).
//#define DCC_ACCESSORY_RCN_213
/////////////////////////////////////////////////////////////////////////////////////

View File

@@ -18,24 +18,50 @@
*/
#ifndef DEFINES_H
#define DEFINES_H
// defines.h relies on macros defined in config.h
// but it may have already been included (for cosmetic convenence) by the .ino
#ifndef MOTOR_SHIELD_TYPE
#if __has_include ( "config.h")
#include "config.h"
#else
#include "config.example.h"
#endif
#endif
////////////////////////////////////////////////////////////////////////////////
//
// WIFI_ON: All prereqs for running with WIFI are met
// Note: WIFI_CHANNEL may not exist in early config.h files so is added here if needed.
#if ENABLE_WIFI && (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) || defined(ARDUINO_SAMD_ZERO) || defined(TEENSYDUINO))
#define WIFI_ON true
#ifndef WIFI_CHANNEL
#define WIFI_CHANNEL 1
#endif
#if (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) || defined(ARDUINO_SAMD_ZERO) || defined(TEENSYDUINO))
#define BIG_RAM
#endif
#if ENABLE_WIFI
#if defined(BIG_RAM)
#define WIFI_ON true
#ifndef WIFI_CHANNEL
#define WIFI_CHANNEL 1
#endif
#else
#warning You have defined that you want WIFI but your hardware has not enough memory to do that, so WIFI DISABLED
#define WIFI_ON false
#endif
#else
#define WIFI_ON false
#define WIFI_ON false
#endif
#if ENABLE_ETHERNET && (defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560) || defined(ARDUINO_SAMD_ZERO) || defined(TEENSYDUINO))
#define ETHERNET_ON true
#if ENABLE_ETHERNET
#if defined(BIG_RAM)
#define ETHERNET_ON true
#else
#warning You have defined that you want ETHERNET but your hardware has not enough memory to do that, so ETHERNET DISABLED
#define ETHERNET_ON false
#endif
#else
#define ETHERNET_ON false
#define ETHERNET_ON false
#endif
#if WIFI_ON && ETHERNET_ON
@@ -48,3 +74,13 @@
// Currently only devices which can communicate at 115200 are supported.
//
#define WIFI_SERIAL_LINK_SPEED 115200
#if __has_include ( "myAutomation.h")
#if defined(BIG_RAM) || defined(DISABLE_EEPROM)
#define RMFT_ACTIVE
#else
#warning You have myAutomation.h but your hardware has not enough memory to do that, so EX-RAIL DISABLED
#endif
#endif
#endif

611
enc28j60.cpp Normal file
View File

@@ -0,0 +1,611 @@
// Microchip ENC28J60 Ethernet Interface Driver
// Author: Guido Socher
// Copyright: GPL V2
//
// Based on the enc28j60.c file from the AVRlib library by Pascal Stang.
// For AVRlib See http://www.procyonengineering.com/
// Used with explicit permission of Pascal Stang.
//
// 2010-05-20 <jc@wippler.nl>
#if ARDUINO >= 100
#include <Arduino.h> // Arduino 1.0
#else
#include <Wprogram.h> // Arduino 0022
#endif
#include <SPI.h>
#include "enc28j60.h"
uint16_t ENC28J60::bufferSize;
bool ENC28J60::broadcast_enabled = false;
bool ENC28J60::promiscuous_enabled = false;
// ENC28J60 Control Registers
// Control register definitions are a combination of address,
// bank number, and Ethernet/MAC/PHY indicator bits.
// - Register address (bits 0-4)
// - Bank number (bits 5-6)
// - MAC/PHY indicator (bit 7)
#define ADDR_MASK 0x1F
#define BANK_MASK 0x60
#define SPRD_MASK 0x80
// All-bank registers
#define EIE 0x1B
#define EIR 0x1C
#define ESTAT 0x1D
#define ECON2 0x1E
#define ECON1 0x1F
// Bank 0 registers
#define ERDPT (0x00|0x00)
#define EWRPT (0x02|0x00)
#define ETXST (0x04|0x00)
#define ETXND (0x06|0x00)
#define ERXST (0x08|0x00)
#define ERXND (0x0A|0x00)
#define ERXRDPT (0x0C|0x00)
// #define ERXWRPT (0x0E|0x00)
#define EDMAST (0x10|0x00)
#define EDMAND (0x12|0x00)
// #define EDMADST (0x14|0x00)
#define EDMACS (0x16|0x00)
// Bank 1 registers
#define EHT0 (0x00|0x20)
#define EHT1 (0x01|0x20)
#define EHT2 (0x02|0x20)
#define EHT3 (0x03|0x20)
#define EHT4 (0x04|0x20)
#define EHT5 (0x05|0x20)
#define EHT6 (0x06|0x20)
#define EHT7 (0x07|0x20)
#define EPMM0 (0x08|0x20)
#define EPMM1 (0x09|0x20)
#define EPMM2 (0x0A|0x20)
#define EPMM3 (0x0B|0x20)
#define EPMM4 (0x0C|0x20)
#define EPMM5 (0x0D|0x20)
#define EPMM6 (0x0E|0x20)
#define EPMM7 (0x0F|0x20)
#define EPMCS (0x10|0x20)
// #define EPMO (0x14|0x20)
#define EWOLIE (0x16|0x20)
#define EWOLIR (0x17|0x20)
#define ERXFCON (0x18|0x20)
#define EPKTCNT (0x19|0x20)
// Bank 2 registers
#define MACON1 (0x00|0x40|0x80)
#define MACON3 (0x02|0x40|0x80)
#define MACON4 (0x03|0x40|0x80)
#define MABBIPG (0x04|0x40|0x80)
#define MAIPG (0x06|0x40|0x80)
#define MACLCON1 (0x08|0x40|0x80)
#define MACLCON2 (0x09|0x40|0x80)
#define MAMXFL (0x0A|0x40|0x80)
#define MAPHSUP (0x0D|0x40|0x80)
#define MICON (0x11|0x40|0x80)
#define MICMD (0x12|0x40|0x80)
#define MIREGADR (0x14|0x40|0x80)
#define MIWR (0x16|0x40|0x80)
#define MIRD (0x18|0x40|0x80)
// Bank 3 registers
#define MAADR1 (0x00|0x60|0x80)
#define MAADR0 (0x01|0x60|0x80)
#define MAADR3 (0x02|0x60|0x80)
#define MAADR2 (0x03|0x60|0x80)
#define MAADR5 (0x04|0x60|0x80)
#define MAADR4 (0x05|0x60|0x80)
#define EBSTSD (0x06|0x60)
#define EBSTCON (0x07|0x60)
#define EBSTCS (0x08|0x60)
#define MISTAT (0x0A|0x60|0x80)
#define EREVID (0x12|0x60)
#define ECOCON (0x15|0x60)
#define EFLOCON (0x17|0x60)
#define EPAUS (0x18|0x60)
// ENC28J60 ERXFCON Register Bit Definitions
#define ERXFCON_UCEN 0x80
#define ERXFCON_ANDOR 0x40
#define ERXFCON_CRCEN 0x20
#define ERXFCON_PMEN 0x10
#define ERXFCON_MPEN 0x08
#define ERXFCON_HTEN 0x04
#define ERXFCON_MCEN 0x02
#define ERXFCON_BCEN 0x01
// ENC28J60 EIE Register Bit Definitions
#define EIE_INTIE 0x80
#define EIE_PKTIE 0x40
#define EIE_DMAIE 0x20
#define EIE_LINKIE 0x10
#define EIE_TXIE 0x08
#define EIE_WOLIE 0x04
#define EIE_TXERIE 0x02
#define EIE_RXERIE 0x01
// ENC28J60 EIR Register Bit Definitions
#define EIR_PKTIF 0x40
#define EIR_DMAIF 0x20
#define EIR_LINKIF 0x10
#define EIR_TXIF 0x08
#define EIR_WOLIF 0x04
#define EIR_TXERIF 0x02
#define EIR_RXERIF 0x01
// ENC28J60 ESTAT Register Bit Definitions
#define ESTAT_INT 0x80
#define ESTAT_LATECOL 0x10
#define ESTAT_RXBUSY 0x04
#define ESTAT_TXABRT 0x02
#define ESTAT_CLKRDY 0x01
// ENC28J60 ECON2 Register Bit Definitions
#define ECON2_AUTOINC 0x80
#define ECON2_PKTDEC 0x40
#define ECON2_PWRSV 0x20
#define ECON2_VRPS 0x08
// ENC28J60 ECON1 Register Bit Definitions
#define ECON1_TXRST 0x80
#define ECON1_RXRST 0x40
#define ECON1_DMAST 0x20
#define ECON1_CSUMEN 0x10
#define ECON1_TXRTS 0x08
#define ECON1_RXEN 0x04
#define ECON1_BSEL1 0x02
#define ECON1_BSEL0 0x01
// ENC28J60 MACON1 Register Bit Definitions
#define MACON1_LOOPBK 0x10
#define MACON1_TXPAUS 0x08
#define MACON1_RXPAUS 0x04
#define MACON1_PASSALL 0x02
#define MACON1_MARXEN 0x01
// ENC28J60 MACON3 Register Bit Definitions
#define MACON3_PADCFG2 0x80
#define MACON3_PADCFG1 0x40
#define MACON3_PADCFG0 0x20
#define MACON3_TXCRCEN 0x10
#define MACON3_PHDRLEN 0x08
#define MACON3_HFRMLEN 0x04
#define MACON3_FRMLNEN 0x02
#define MACON3_FULDPX 0x01
// ENC28J60 MICMD Register Bit Definitions
#define MICMD_MIISCAN 0x02
#define MICMD_MIIRD 0x01
// ENC28J60 MISTAT Register Bit Definitions
#define MISTAT_NVALID 0x04
#define MISTAT_SCAN 0x02
#define MISTAT_BUSY 0x01
// ENC28J60 EBSTCON Register Bit Definitions
#define EBSTCON_PSV2 0x80
#define EBSTCON_PSV1 0x40
#define EBSTCON_PSV0 0x20
#define EBSTCON_PSEL 0x10
#define EBSTCON_TMSEL1 0x08
#define EBSTCON_TMSEL0 0x04
#define EBSTCON_TME 0x02
#define EBSTCON_BISTST 0x01
// PHY registers
#define PHCON1 0x00
#define PHSTAT1 0x01
#define PHHID1 0x02
#define PHHID2 0x03
#define PHCON2 0x10
#define PHSTAT2 0x11
#define PHIE 0x12
#define PHIR 0x13
#define PHLCON 0x14
// ENC28J60 PHY PHCON1 Register Bit Definitions
#define PHCON1_PRST 0x8000
#define PHCON1_PLOOPBK 0x4000
#define PHCON1_PPWRSV 0x0800
#define PHCON1_PDPXMD 0x0100
// ENC28J60 PHY PHSTAT1 Register Bit Definitions
#define PHSTAT1_PFDPX 0x1000
#define PHSTAT1_PHDPX 0x0800
#define PHSTAT1_LLSTAT 0x0004
#define PHSTAT1_JBSTAT 0x0002
// ENC28J60 PHY PHCON2 Register Bit Definitions
#define PHCON2_FRCLINK 0x4000
#define PHCON2_TXDIS 0x2000
#define PHCON2_JABBER 0x0400
#define PHCON2_HDLDIS 0x0100
// ENC28J60 Packet Control Byte Bit Definitions
#define PKTCTRL_PHUGEEN 0x08
#define PKTCTRL_PPADEN 0x04
#define PKTCTRL_PCRCEN 0x02
#define PKTCTRL_POVERRIDE 0x01
// SPI operation codes
#define ENC28J60_READ_CTRL_REG 0x00
#define ENC28J60_READ_BUF_MEM 0x3A
#define ENC28J60_WRITE_CTRL_REG 0x40
#define ENC28J60_WRITE_BUF_MEM 0x7A
#define ENC28J60_BIT_FIELD_SET 0x80
#define ENC28J60_BIT_FIELD_CLR 0xA0
#define ENC28J60_SOFT_RESET 0xFF
// max frame length which the controller will accept:
// (note: maximum ethernet frame length would be 1518)
#define MAX_FRAMELEN 1500
#define SPI_SPEED 10000000
static byte Enc28j60Bank;
static byte selectPin;
static uint8_t selectMask;
volatile static uint8_t *selectPort;
void ENC28J60::initSPI () {
pinMode(selectPin, OUTPUT);
digitalWrite(selectPin, HIGH);
pinMode(MOSI, OUTPUT);
pinMode(SCK, OUTPUT);
pinMode(MISO, INPUT);
digitalWrite(MOSI, HIGH);
digitalWrite(MOSI, LOW);
digitalWrite(SCK, LOW);
#ifdef ARDUINO_ARCH_AVR
selectPort = portOutputRegister(digitalPinToPort(selectPin));
selectMask = digitalPinToBitMask(selectPin);
#endif
}
static void enableChip () {
#ifdef ARDUINO_ARCH_AVR
cli();
*selectPort &= ~selectMask;
sei();
#else
digitalWrite(selectPin, LOW);
#endif
}
static void disableChip () {
#ifdef ARDUINO_ARCH_AVR
cli();
*selectPort |= selectMask;
sei();
#else
digitalWrite(selectPin, HIGH);
#endif
}
static void beginTransaction() {
SPI.beginTransaction(SPISettings(SPI_SPEED, MSBFIRST, SPI_MODE0));
enableChip();
}
static void endTransaction() {
disableChip();
SPI.endTransaction();
}
static byte readOp (byte op, byte address) {
beginTransaction();
SPI.transfer(op | (address & ADDR_MASK));
byte result = SPI.transfer(0x00);
if (address & 0x80)
result = SPI.transfer(0x00);
endTransaction();
return result;
}
static void writeOp (byte op, byte address, byte data) {
beginTransaction();
SPI.transfer(op | (address & ADDR_MASK));
SPI.transfer(data);
endTransaction();
}
static void readBuf(uint16_t len, byte* data) {
beginTransaction();
SPI.transfer(ENC28J60_READ_BUF_MEM);
while (len--)
*data++ = SPI.transfer(0x00);
endTransaction();
}
static void writeBuf(uint16_t len, const byte* data) {
beginTransaction();
SPI.transfer(ENC28J60_WRITE_BUF_MEM);
while (len--)
SPI.transfer(*data++);
endTransaction();
}
static void SetBank (byte address) {
if ((address & BANK_MASK) != Enc28j60Bank) {
writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_BSEL1|ECON1_BSEL0);
Enc28j60Bank = address & BANK_MASK;
writeOp(ENC28J60_BIT_FIELD_SET, ECON1, Enc28j60Bank>>5);
}
}
static byte readRegByte (byte address) {
SetBank(address);
return readOp(ENC28J60_READ_CTRL_REG, address);
}
static uint16_t readReg(byte address) {
return readRegByte(address) + (readRegByte(address+1) << 8);
}
static void writeRegByte (byte address, byte data) {
SetBank(address);
writeOp(ENC28J60_WRITE_CTRL_REG, address, data);
}
static void writeReg(byte address, uint16_t data) {
writeRegByte(address, data);
writeRegByte(address + 1, data >> 8);
}
static uint16_t readPhyByte (byte address) {
writeRegByte(MIREGADR, address);
writeRegByte(MICMD, MICMD_MIIRD);
while (readRegByte(MISTAT) & MISTAT_BUSY)
;
writeRegByte(MICMD, 0x00);
return readRegByte(MIRD+1);
}
static void writePhy (byte address, uint16_t data) {
writeRegByte(MIREGADR, address);
writeReg(MIWR, data);
while (readRegByte(MISTAT) & MISTAT_BUSY)
;
}
byte ENC28J60::initialize (uint16_t size, const byte* macaddr, byte csPin) {
bufferSize = size;
selectPin = csPin;
initSPI();
disableChip();
writeOp(ENC28J60_SOFT_RESET, 0, ENC28J60_SOFT_RESET);
delay(2); // errata B7/2
while (!readOp(ENC28J60_READ_CTRL_REG, ESTAT) & ESTAT_CLKRDY)
;
writeReg(ERXST, RXSTART_INIT);
writeReg(ERXRDPT, RXSTART_INIT);
writeReg(ERXND, RXSTOP_INIT);
writeReg(ETXST, TXSTART_INIT);
writeReg(ETXND, TXSTOP_INIT);
// Stretch pulses for LED, LED_A=Link, LED_B=activity
writePhy(PHLCON, 0x476);
writeRegByte(ERXFCON, ERXFCON_UCEN|ERXFCON_CRCEN|ERXFCON_PMEN|ERXFCON_BCEN);
writeReg(EPMM0, 0x303f);
writeReg(EPMCS, 0xf7f9);
writeRegByte(MACON1, MACON1_MARXEN);
writeOp(ENC28J60_BIT_FIELD_SET, MACON3,
MACON3_PADCFG0|MACON3_TXCRCEN|MACON3_FRMLNEN);
writeReg(MAIPG, 0x0C12);
writeRegByte(MABBIPG, 0x12);
writeReg(MAMXFL, MAX_FRAMELEN);
writeRegByte(MAADR5, macaddr[0]);
writeRegByte(MAADR4, macaddr[1]);
writeRegByte(MAADR3, macaddr[2]);
writeRegByte(MAADR2, macaddr[3]);
writeRegByte(MAADR1, macaddr[4]);
writeRegByte(MAADR0, macaddr[5]);
writePhy(PHCON2, PHCON2_HDLDIS);
SetBank(ECON1);
writeOp(ENC28J60_BIT_FIELD_SET, EIE, EIE_INTIE|EIE_PKTIE);
writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_RXEN);
byte rev = readRegByte(EREVID);
// microchip forgot to step the number on the silicon when they
// released the revision B7. 6 is now rev B7. We still have
// to see what they do when they release B8. At the moment
// there is no B8 out yet
if (rev > 5) ++rev;
return rev;
}
bool ENC28J60::isLinkUp() {
return (readPhyByte(PHSTAT2) >> 2) & 1;
}
/*
struct __attribute__((__packed__)) transmit_status_vector {
uint16_t transmitByteCount;
byte transmitCollisionCount : 4;
byte transmitCrcError : 1;
byte transmitLengthCheckError : 1;
byte transmitLengthOutRangeError : 1;
byte transmitDone : 1;
byte transmitMulticast : 1;
byte transmitBroadcast : 1;
byte transmitPacketDefer : 1;
byte transmitExcessiveDefer : 1;
byte transmitExcessiveCollision : 1;
byte transmitLateCollision : 1;
byte transmitGiant : 1;
byte transmitUnderrun : 1;
uint16_t totalTransmitted;
byte transmitControlFrame : 1;
byte transmitPauseControlFrame : 1;
byte backpressureApplied : 1;
byte transmitVLAN : 1;
byte zero : 4;
};
*/
struct transmit_status_vector {
uint8_t bytes[7];
};
#if ETHERCARD_SEND_PIPELINING
#define BREAKORCONTINUE retry=0; continue;
#else
#define BREAKORCONTINUE break;
#endif
void ENC28J60::packetSend(uint16_t len) {
byte retry = 0;
#if ETHERCARD_SEND_PIPELINING
goto resume_last_transmission;
#endif
while (1) {
// latest errata sheet: DS80349C
// always reset transmit logic (Errata Issue 12)
// the Microchip TCP/IP stack implementation used to first check
// whether TXERIF is set and only then reset the transmit logic
// but this has been changed in later versions; possibly they
// have a reason for this; they don't mention this in the errata
// sheet
writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRST);
writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_TXRST);
writeOp(ENC28J60_BIT_FIELD_CLR, EIR, EIR_TXERIF|EIR_TXIF);
// prepare new transmission
if (retry == 0) {
writeReg(EWRPT, TXSTART_INIT);
writeReg(ETXND, TXSTART_INIT+len);
writeOp(ENC28J60_WRITE_BUF_MEM, 0, 0x00);
writeBuf(len, buffer);
}
// initiate transmission
writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRTS);
#if ETHERCARD_SEND_PIPELINING
if (retry == 0) return;
#endif
resume_last_transmission:
// wait until transmission has finished; referring to the data sheet and
// to the errata (Errata Issue 13; Example 1) you only need to wait until either
// TXIF or TXERIF gets set; however this leads to hangs; apparently Microchip
// realized this and in later implementations of their tcp/ip stack they introduced
// a counter to avoid hangs; of course they didn't update the errata sheet
uint16_t count = 0;
while ((readRegByte(EIR) & (EIR_TXIF | EIR_TXERIF)) == 0 && ++count < 1000U)
;
if (!(readRegByte(EIR) & EIR_TXERIF) && count < 1000U) {
// no error; start new transmission
BREAKORCONTINUE
}
// cancel previous transmission if stuck
writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_TXRTS);
#if ETHERCARD_RETRY_LATECOLLISIONS == 0
BREAKORCONTINUE
#endif
// Check whether the chip thinks that a late collision occurred; the chip
// may be wrong (Errata Issue 13); therefore we retry. We could check
// LATECOL in the ESTAT register in order to find out whether the chip
// thinks a late collision occurred but (Errata Issue 15) tells us that
// this is not working. Therefore we check TSV
transmit_status_vector tsv;
uint16_t etxnd = readReg(ETXND);
writeReg(ERDPT, etxnd+1);
readBuf(sizeof(transmit_status_vector), (byte*) &tsv);
// LATECOL is bit number 29 in TSV (starting from 0)
if (!((readRegByte(EIR) & EIR_TXERIF) && (tsv.bytes[3] & 1<<5) /*tsv.transmitLateCollision*/) || retry > 16U) {
// there was some error but no LATECOL so we do not repeat
BREAKORCONTINUE
}
retry++;
}
}
uint16_t ENC28J60::packetReceive() {
static uint16_t gNextPacketPtr = RXSTART_INIT;
static bool unreleasedPacket = false;
uint16_t len = 0;
if (unreleasedPacket) {
if (gNextPacketPtr == 0)
writeReg(ERXRDPT, RXSTOP_INIT);
else
writeReg(ERXRDPT, gNextPacketPtr - 1);
unreleasedPacket = false;
}
if (readRegByte(EPKTCNT) > 0) {
writeReg(ERDPT, gNextPacketPtr);
struct {
uint16_t nextPacket;
uint16_t byteCount;
uint16_t status;
} header;
readBuf(sizeof header, (byte*) &header);
gNextPacketPtr = header.nextPacket;
len = header.byteCount - 4; //remove the CRC count
if (len>bufferSize) len=0; // discard messages too long **NMCK**
if ((header.status & 0x80)==0)
len = 0;
else
readBuf(len, buffer);
unreleasedPacket = true;
writeOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_PKTDEC);
}
return len;
}
// Contributed by Alex M. Based on code from: http://blog.derouineau.fr
// /2011/07/putting-enc28j60-ethernet-controler-in-sleep-mode/
void ENC28J60::powerDown() {
writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_RXEN);
while(readRegByte(ESTAT) & ESTAT_RXBUSY);
while(readRegByte(ECON1) & ECON1_TXRTS);
writeOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_VRPS);
writeOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_PWRSV);
}
void ENC28J60::powerUp() {
writeOp(ENC28J60_BIT_FIELD_CLR, ECON2, ECON2_PWRSV);
while(!readRegByte(ESTAT) & ESTAT_CLKRDY);
writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_RXEN);
}
void ENC28J60::enableBroadcast (bool temporary) {
writeRegByte(ERXFCON, readRegByte(ERXFCON) | ERXFCON_BCEN);
if(!temporary)
broadcast_enabled = true;
}
void ENC28J60::disableBroadcast (bool temporary) {
if(!temporary)
broadcast_enabled = false;
if(!broadcast_enabled)
writeRegByte(ERXFCON, readRegByte(ERXFCON) & ~ERXFCON_BCEN);
}
void ENC28J60::enableMulticast () {
writeRegByte(ERXFCON, readRegByte(ERXFCON) | ERXFCON_MCEN);
}
void ENC28J60::disableMulticast () {
writeRegByte(ERXFCON, readRegByte(ERXFCON) & ~ERXFCON_MCEN);
}
void ENC28J60::enablePromiscuous (bool temporary) {
writeRegByte(ERXFCON, readRegByte(ERXFCON) & ERXFCON_CRCEN);
if(!temporary)
promiscuous_enabled = true;
}
void ENC28J60::disablePromiscuous (bool temporary) {
if(!temporary)
promiscuous_enabled = false;
if(!promiscuous_enabled) {
writeRegByte(ERXFCON, ERXFCON_UCEN|ERXFCON_CRCEN|ERXFCON_PMEN|ERXFCON_BCEN);
}
}

207
enc28j60.h Normal file
View File

@@ -0,0 +1,207 @@
// Microchip ENC28J60 Ethernet Interface Driver
// Author: Pascal Stang
// Modified by: Guido Socher
// Copyright: GPL V2
//
// This driver provides initialization and transmit/receive
// functions for the Microchip ENC28J60 10Mb Ethernet Controller and PHY.
// This chip is novel in that it is a full MAC+PHY interface all in a 28-pin
// chip, using an SPI interface to the host processor.
//
// 2010-05-20 <jc@wippler.nl>
/** @file */
#ifndef ENC28J60_H
#define ENC28J60_H
// buffer boundaries applied to internal 8K ram
// the entire available packet buffer space is allocated
#define RXSTART_INIT 0x0000 // start of RX buffer, (must be zero, Rev. B4 Errata point 5)
#define RXSTOP_INIT 0x0BFF // end of RX buffer, room for 2 packets
#define TXSTART_INIT 0x0C00 // start of TX buffer, room for 1 packet
#define TXSTOP_INIT 0x11FF // end of TX buffer
#define SCRATCH_START 0x1200 // start of scratch area
#define SCRATCH_LIMIT 0x2000 // past end of area, i.e. 3 Kb
#define SCRATCH_PAGE_SHIFT 6 // addressing is in pages of 64 bytes
#define SCRATCH_PAGE_SIZE (1 << SCRATCH_PAGE_SHIFT)
#define SCRATCH_PAGE_NUM ((SCRATCH_LIMIT-SCRATCH_START) >> SCRATCH_PAGE_SHIFT)
#define SCRATCH_MAP_SIZE (((SCRATCH_PAGE_NUM % 8) == 0) ? (SCRATCH_PAGE_NUM / 8) : (SCRATCH_PAGE_NUM/8+1))
// area in the enc memory that can be used via enc_malloc; by default 0 bytes; decrease SCRATCH_LIMIT in order
// to use this functionality
#define ENC_HEAP_START SCRATCH_LIMIT
#define ENC_HEAP_END 0x2000
/** This class provide low-level interfacing with the ENC28J60 network interface. This is used by the EtherCard class and not intended for use by (normal) end users. */
class ENC28J60 {
public:
static uint8_t buffer[]; //!< Data buffer (shared by receive and transmit)
static uint16_t bufferSize; //!< Size of data buffer
static bool broadcast_enabled; //!< True if broadcasts enabled (used to allow temporary disable of broadcast for DHCP or other internal functions)
static bool promiscuous_enabled; //!< True if promiscuous mode enabled (used to allow temporary disable of promiscuous mode)
static uint8_t* tcpOffset () { return buffer + 0x36; } //!< Pointer to the start of TCP payload
/** @brief Initialise SPI interface
* @note Configures Arduino pins as input / output, etc.
*/
static void initSPI ();
/** @brief Initialise network interface
* @param size Size of data buffer
* @param macaddr Pointer to 6 byte hardware (MAC) address
* @param csPin Arduino pin used for chip select (enable network interface SPI bus). Default = 8
* @return <i>uint8_t</i> ENC28J60 firmware version or zero on failure.
*/
static uint8_t initialize (const uint16_t size, const uint8_t* macaddr,
uint8_t csPin = 8);
/** @brief Check if network link is connected
* @return <i>bool</i> True if link is up
*/
static bool isLinkUp ();
/** @brief Sends data to network interface
* @param len Size of data to send
* @note Data buffer is shared by receive and transmit functions
*/
static void packetSend (uint16_t len);
/** @brief Copy received packets to data buffer
* @return <i>uint16_t</i> Size of received data
* @note Data buffer is shared by receive and transmit functions
*/
static uint16_t packetReceive ();
/** @brief Copy data from ENC28J60 memory
* @param page Data page of memory
* @param data Pointer to buffer to copy data to
*/
static void copyout (uint8_t page, const uint8_t* data);
/** @brief Copy data to ENC28J60 memory
* @param page Data page of memory
* @param data Pointer to buffer to copy data from
*/
static void copyin (uint8_t page, uint8_t* data);
/** @brief Get single byte of data from ENC28J60 memory
* @param page Data page of memory
* @param off Offset of data within page
* @return Data value
*/
static uint8_t peekin (uint8_t page, uint8_t off);
/** @brief Put ENC28J60 in sleep mode
*/
static void powerDown(); // contrib by Alex M.
/** @brief Wake ENC28J60 from sleep mode
*/
static void powerUp(); // contrib by Alex M.
/** @brief Enable reception of broadcast messages
* @param temporary Set true to temporarily enable broadcast
* @note This will increase load on received data handling
*/
static void enableBroadcast(bool temporary = false);
/** @brief Disable reception of broadcast messages
* @param temporary Set true to only disable if temporarily enabled
* @note This will reduce load on received data handling
*/
static void disableBroadcast(bool temporary = false);
/** @brief Enables reception of multicast messages
* @note This will increase load on received data handling
*/
static void enableMulticast ();
/** @brief Enables reception of all messages
* @param temporary Set true to temporarily enable promiscuous
* @note This will increase load significantly on received data handling
* @note All messages will be accepted, even messages with destination MAC other than own
* @note Messages with invalid CRC checksum will still be rejected
*/
static void enablePromiscuous (bool temporary = false);
/** @brief Disable reception of all messages and go back to default mode
* @param temporary Set true to only disable if temporarily enabled
* @note This will reduce load on received data handling
* @note In this mode only unicast and broadcast messages will be received
*/
static void disablePromiscuous(bool temporary = false);
/** @brief Disable reception of multicast messages
* @note This will reduce load on received data handling
*/
static void disableMulticast();
/** @brief Reset and fully initialise ENC28J60
* @param csPin Arduino pin used for chip select (enable SPI bus)
* @return <i>uint8_t</i> 0 on failure
*/
static uint8_t doBIST(uint8_t csPin = 8);
/** @brief Copies a slice from the current packet to RAM
* @param dest pointer in RAM where the data is copied to
* @param maxlength how many bytes to copy;
* @param packetOffset where within the packet to start; if less than maxlength bytes are available only the remaining bytes are copied.
* @return <i>uint16_t</i> the number of bytes that have been read
* @note At the destination at least maxlength+1 bytes should be reserved because the copied content will be 0-terminated.
*/
static uint16_t readPacketSlice(char* dest, int16_t maxlength, int16_t packetOffset);
/** @brief reserves a block of RAM in the memory of the enc chip
* @param size number of bytes to reserve
* @return <i>uint16_t</i> start address of the block within the enc memory. 0 if the remaining memory for malloc operation is less than size.
* @note There is no enc_free(), i.e., reserved blocks stay reserved for the duration of the program.
* @note The total memory available for malloc-operations is determined by ENC_HEAP_END-ENC_HEAP_START, defined in enc28j60.h; by default this is 0, i.e., you have to change these values in order to use enc_malloc().
*/
static uint16_t enc_malloc(uint16_t size);
/** @brief returns the amount of memory within the enc28j60 chip that is still available for malloc.
* @return <i>uint16_t</i> the amount of memory in bytes.
*/
static uint16_t enc_freemem();
/** @brief copies a block of data from SRAM to the enc memory
@param dest destination address within enc memory
@param source source pointer to a block of SRAM in the arduino
@param num number of bytes to copy
@note There is no sanity check. Handle with care
*/
static void memcpy_to_enc(uint16_t dest, void* source, int16_t num);
/** @brief copies a block of data from the enc memory to SRAM
@param dest destination address within SRAM
@param source source address within enc memory
@param num number of bytes to copy
*/
static void memcpy_from_enc(void* dest, uint16_t source, int16_t num);
};
// typedef ENC28J60 Ethernet; //!< Define alias Ethernet for ENC28J60
/** Workaround for Errata 13.
* The transmission hardware may drop some packets because it thinks a late collision
* occurred (which should never happen if all cable length etc. are ok). If setting
* this to 1 these packages will be retried a fixed number of times. Costs about 150bytes
* of flash.
*/
#define ETHERCARD_RETRY_LATECOLLISIONS 0
/** Enable pipelining of packet transmissions.
* If enabled the packetSend function will not block/wait until the packet is actually
* transmitted; but instead this wait is shifted to the next time that packetSend is
* called. This gives higher performance; however in combination with
* ETHERCARD_RETRY_LATECOLLISIONS this may lead to problems because a packet whose
* transmission fails because the ENC-chip thinks that it is a late collision will
* not be retried until the next call to packetSend.
*/
#define ETHERCARD_SEND_PIPELINING 1
#endif

84
myAutomation.example.h Normal file
View File

@@ -0,0 +1,84 @@
/* This is an automation example file.
* The presence of a file calle "myAutomation.h" brings EX-RAIL code into
* the command station.
* The auotomation may have multiple concurrent tasks.
* A task may
* - Act as a ROUTE setup macro for a user to drive over
* - drive a loco through an AUTOMATION
* - automate some cosmetic part of the layout without any loco.
*
* At startup, a single task is created to execute the first
* instruction after E$XRAIL.
* This task may simply follow a route, or may START
* further tasks (thats is.. send a loco out along a route).
*
* Where the loco id is not known at compile time, a new task
* can be creatd with the command:
* </ START [cab] route>
*
* A ROUTE, AUTOMATION or SEQUENCE are internally identical in ExRail terms
* but are just represented differently to a Withrottle user:
* ROUTE(n,"name") - as Route_n .. to setup a route through a layout
* AUTOMATION(n,"name") as Auto_n .. to send the current loco off along an automated journey
* SEQUENCE(n) is not visible to Withrottle.
*
*/
EXRAIL // myAutomation must start with the EXRAIL instruction
// This is the default starting route, AKA SEQUENCE(0)
SENDLOCO(3,1) // send loco 3 off along route 1
SENDLOCO(10,2) // send loco 10 off along route 2
DONE // This just ends the startup thread, leaving 2 others running.
/* SEQUENCE(1) is a simple shuttle between 2 sensors
* S20 and S21 are sensors on arduino pins 20 and 21
* S20 S21
* === START->================
*/
SEQUENCE(1)
DELAY(10000) // wait 10 seconds
FON(3) // Set Loco Function 3, Horn on
DELAY(1000) // wait 1 second
FOFF(3) // Horn off
FWD(80) // Move forward at speed 80
AT(21) // until we hit sensor id 21
STOP // then stop
DELAY(5000) // Wait 5 seconds
FON(2) // ring bell
REV(60) // reverse at speed 60
AT(20) // until we get to S20
STOP // then stop
FOFF(2) // Bell off
FOLLOW(1) // and follow sequence 1 again
/* SEQUENCE(2) is an automation example for a single loco Y shaped journey
* S31,S32,S33 are sensors, T4 is a turnout
*
* S33 T4 S31
* ===-START->=============================================
* //
* S32 //
* ======================//
*
* Train runs from START to S31, back to S32, again to S31, Back to start.
*/
SEQUENCE(2)
FWD(60) // go forward at DCC speed 60
AT(31) STOP // when we get to sensor 31
DELAY(10000) // wait 10 seconds
THROW(4) // throw turnout for route to S32
REV(45) // go backwards at speed 45
AT(32) STOP // until we arrive at sensor 32
DELAY(5000) // wait 5 seconds
FWD(50) // go forwards at speed 50
AT(31) STOP // and stop at sensor 31
DELAY(5000) // wait 5 seconds
CLOSE(4) // set turnout closed
REV(50) // reverse back to S3
AT(33) STOP
DELAY(20000) // wait 20 seconds
FOLLOW(2) // follow sequence 2... ie repeat the process
ENDEXRAIL // marks the end of the EXRAIL program.

161
myHal.cpp_example.txt Normal file
View File

@@ -0,0 +1,161 @@
// Sample myHal.cpp file.
//
// To use this file, copy it to myHal.cpp and uncomment the directives and/or
// edit them to satisfy your requirements. If you only want to use up to
// two MCP23017 GPIO Expander modules and/or up to two PCA9685 Servo modules,
// then you don't need this file as DCC++EX configures these for free!
// Note that if the file has a .cpp extension it WILL be compiled into the build
// and the halSetup() function WILL be invoked.
//
// To prevent this, temporarily rename the file to myHal.txt or similar.
//
// Include devices you need.
#include "IODevice.h"
#include "IO_HCSR04.h" // Ultrasonic range sensor
#include "IO_VL53L0X.h" // Laser time-of-flight sensor
#include "IO_DFPlayer.h" // MP3 sound player
//==========================================================================
// The function halSetup() is invoked from CS if it exists within the build.
// The setup calls are included between the open and close braces "{ ... }".
// Comments (lines preceded by "//") are optional.
//==========================================================================
void halSetup() {
//=======================================================================
// The following directive defines a PCA9685 PWM Servo driver module.
//=======================================================================
// The parameters are:
// First Vpin=100
// Number of VPINs=16 (numbered 100-115)
// I2C address of module=0x40
//PCA9685::create(100, 16, 0x40);
//=======================================================================
// The following directive defines an MCP23017 16-port I2C GPIO Extender module.
//=======================================================================
// The parameters are:
// First Vpin=196
// Number of VPINs=16 (numbered 196-211)
// I2C address of module=0x22
//MCP23017::create(196, 16, 0x22);
// Alternative form, which allows the INT pin of the module to request a scan
// by pulling Arduino pin 40 to ground. Means that the I2C isn't being polled
// all the time, only when a change takes place. Multiple modules' INT pins
// may be connected to the same Arduino pin.
//MCP23017::create(196, 16, 0x22, 40);
//=======================================================================
// The following directive defines an MCP23008 8-port I2C GPIO Extender module.
//=======================================================================
// The parameters are:
// First Vpin=300
// Number of VPINs=8 (numbered 300-307)
// I2C address of module=0x22
//MCP23008::create(300, 8, 0x22);
//=======================================================================
// The following directive defines a PCF8574 8-port I2C GPIO Extender module.
//=======================================================================
// The parameters are:
// First Vpin=200
// Number of VPINs=8 (numbered 200-207)
// I2C address of module=0x23
//PCF8574::create(200, 8, 0x23);
// Alternative form using INT pin (see above)
//PCF8574::create(200, 8, 0x23, 40);
//=======================================================================
// The following directive defines an HCSR04 ultrasonic ranging module.
//=======================================================================
// The parameters are:
// Vpin=2000 (only one VPIN per directive)
// Number of VPINs=1
// Arduino pin connected to TRIG=30
// Arduino pin connected to ECHO=31
// Minimum trigger range=20cm (VPIN goes to 1 when <20cm)
// Maximum trigger range=25cm (VPIN goes to 0 when >25cm)
// Note: Multiple devices can be configured by using a different ECHO pin
// for each one. The TRIG pin can be shared between multiple devices.
// Be aware that the 'ping' of one device may be received by another
// device and position them accordingly!
//HCSR04::create(2000, 30, 31, 20, 25);
//HCSR04::create(2001, 30, 32, 20, 25);
//=======================================================================
// The following directive defines a single VL53L0X Time-of-Flight range sensor.
//=======================================================================
// The parameters are:
// VPIN=5000
// Number of VPINs=1
// I2C address=0x29 (default for this chip)
// Minimum trigger range=200mm (VPIN goes to 1 when <20cm)
// Maximum trigger range=250mm (VPIN goes to 0 when >25cm)
//VL53L0X::create(5000, 1, 0x29, 200, 250);
// For multiple VL53L0X modules, add another parameter which is a VPIN connected to the
// module's XSHUT pin. This allows the modules to be configured, at start,
// with distinct I2C addresses. In this case, the address 0x29 is only used during
// initialisation to configure each device in turn with the desired unique I2C address.
// The examples below have the modules' XSHUT pins connected to the first two pins of
// the first MCP23017 module (164 and 165), but Arduino pins may be used instead.
// The first module here is given I2C address 0x30 and the second is 0x31.
//VL53L0X::create(5000, 1, 0x30, 200, 250, 164);
//VL53L0X::create(5001, 1, 0x31, 200, 250, 165);
//=======================================================================
// Play mp3 files from a Micro-SD card, using a DFPlayer MP3 Module.
//=======================================================================
// Parameters:
// 10000 = first VPIN allocated.
// 10 = number of VPINs allocated.
// Serial1 = name of serial port (usually Serial1 or Serial2).
// With these parameters, up to 10 files may be played on pins 10000-10009.
// Play is started from EX-RAIL with SET(10000) for first mp3 file, SET(10001)
// for second file, etc. Play may also be initiated by writing an analogue
// value to the first pin, e.g. SERVO(10000,23,0) will play the 23rd mp3 file.
// SERVO(10000,23,30) will do the same thing, as well as setting the volume to
// 30 (maximum value).
// Play is stopped by RESET(10000) (or any other allocated VPIN).
// Volume may also be set by writing an analogue value to the second pin for the player,
// e.g. SERVO(10001,30,0) sets volume to maximum (30).
// The EX-RAIL script may check for completion of play by calling WAITFOR(pin), which will only proceed to the
// following line when the player is no longer busy.
// E.g.
// SEQUENCE(1)
// AT(164) // Wait for sensor attached to pin 164 to activate
// SET(10003) // Play fourth MP3 file
// LCD(4, "Playing") // Display message on LCD/OLED
// WAITFOR(10003) // Wait for playing to finish
// LCD(4, " ") // Clear LCD/OLED line
// FOLLOW(1) // Go back to start
// DFPlayer::create(10000, 10, Serial1);
}
#endif

100
net.h Normal file
View File

@@ -0,0 +1,100 @@
// Based on the net.h file from the AVRlib library by Pascal Stang.
// Author: Guido Socher
// Copyright: GPL V2
//
// For AVRlib See http://www.procyonengineering.com/
// Used with explicit permission of Pascal Stang.
//
// 2010-05-20 <jc@wippler.nl>
// notation: _P = position of a field
// _V = value of a field
#ifndef NET_H
#define NET_H
// ******* ETH *******
#define ETH_HEADER_LEN 14
#define ETH_LEN 6
// values of certain bytes:
#define ETHTYPE_ARP_H_V 0x08
#define ETHTYPE_ARP_L_V 0x06
#define ETHTYPE_IP_H_V 0x08
#define ETHTYPE_IP_L_V 0x00
// byte positions in the ethernet frame:
//
// Ethernet type field (2bytes):
#define ETH_TYPE_H_P 12
#define ETH_TYPE_L_P 13
//
#define ETH_DST_MAC 0
#define ETH_SRC_MAC 6
// ******* ARP *******
#define ETH_ARP_OPCODE_REPLY_H_V 0x0
#define ETH_ARP_OPCODE_REPLY_L_V 0x02
#define ETH_ARP_OPCODE_REQ_H_V 0x0
#define ETH_ARP_OPCODE_REQ_L_V 0x01
// start of arp header:
#define ETH_ARP_P 0xe
//
#define ETHTYPE_ARP_L_V 0x06
// arp.dst.ip
#define ETH_ARP_DST_IP_P 0x26
// arp.opcode
#define ETH_ARP_OPCODE_H_P 0x14
#define ETH_ARP_OPCODE_L_P 0x15
// arp.src.mac
#define ETH_ARP_SRC_MAC_P 0x16
#define ETH_ARP_SRC_IP_P 0x1c
#define ETH_ARP_DST_MAC_P 0x20
#define ETH_ARP_DST_IP_P 0x26
// ******* IP *******
#define IP_HEADER_LEN 20
#define IP_LEN 4
// ip.src
#define IP_SRC_P 0x1a
#define IP_DST_P 0x1e
#define IP_HEADER_LEN_VER_P 0xe
#define IP_CHECKSUM_P 0x18
#define IP_TTL_P 0x16
#define IP_FLAGS_P 0x14
#define IP_P 0xe
#define IP_TOTLEN_H_P 0x10
#define IP_TOTLEN_L_P 0x11
#define IP_PROTO_P 0x17
#define IP_PROTO_ICMP_V 1
#define IP_PROTO_TCP_V 6
// 17=0x11
#define IP_PROTO_UDP_V 17
// ******* ICMP *******
#define ICMP_TYPE_ECHOREPLY_V 0
#define ICMP_TYPE_ECHOREQUEST_V 8
//
#define ICMP_TYPE_P 0x22
#define ICMP_CHECKSUM_P 0x24
#define ICMP_CHECKSUM_H_P 0x24
#define ICMP_CHECKSUM_L_P 0x25
#define ICMP_IDENT_H_P 0x26
#define ICMP_IDENT_L_P 0x27
#define ICMP_DATA_P 0x2a
// ******* UDP *******
#define UDP_HEADER_LEN 8
//
#define UDP_SRC_PORT_H_P 0x22
#define UDP_SRC_PORT_L_P 0x23
#define UDP_DST_PORT_H_P 0x24
#define UDP_DST_PORT_L_P 0x25
//
#define UDP_LEN_H_P 0x26
#define UDP_LEN_L_P 0x27
#define UDP_CHECKSUM_H_P 0x28
#define UDP_CHECKSUM_L_P 0x29
#define UDP_DATA_P 0x2a
#endif

View File

@@ -12,9 +12,14 @@
default_envs =
mega2560
uno
mega328
unowifiR2
nano
src_dir = .
include_dir = .
[env]
build_flags = -Wall -Wextra
[env:samd21]
platform = atmelsam
@@ -27,6 +32,42 @@ lib_deps =
monitor_speed = 115200
monitor_flags = --echo
[env:mega2560-debug]
platform = atmelavr
board = megaatmega2560
framework = arduino
lib_deps =
${env.lib_deps}
arduino-libraries/Ethernet
SPI
monitor_speed = 115200
monitor_flags = --echo
build_flags = -DDIAG_IO -DDIAG_LOOPTIMES
[env:mega2560-no-HAL]
platform = atmelavr
board = megaatmega2560
framework = arduino
lib_deps =
${env.lib_deps}
arduino-libraries/Ethernet
SPI
monitor_speed = 115200
monitor_flags = --echo
build_flags = -DIO_NO_HAL
[env:mega2560-I2C-wire]
platform = atmelavr
board = megaatmega2560
framework = arduino
lib_deps =
${env.lib_deps}
arduino-libraries/Ethernet
SPI
monitor_speed = 115200
monitor_flags = --echo
build_flags = -DI2C_USE_WIRE
[env:mega2560]
platform = atmelavr
board = megaatmega2560
@@ -59,7 +100,20 @@ lib_deps =
SPI
monitor_speed = 115200
monitor_flags = --echo
build_flags = "-DF_CPU=16000000L -DARDUINO=10813 -DARDUINO_AVR_UNO_WIFI_DEV_ED -DARDUINO_ARCH_AVR -DESP_CH_UART -DESP_CH_UART_BR=19200"g
build_flags = "-DF_CPU=16000000L -DARDUINO=10813 -DARDUINO_AVR_UNO_WIFI_DEV_ED -DARDUINO_ARCH_AVR -DESP_CH_UART -DESP_CH_UART_BR=19200"
[env:nanoevery]
platform = atmelmegaavr
board = nano_every
framework = arduino
lib_deps =
${env.lib_deps}
arduino-libraries/Ethernet
SPI
monitor_speed = 115200
monitor_flags = --echo
upload_speed = 19200
build_flags = -DDIAG_IO
[env:uno]
platform = atmelavr
@@ -71,3 +125,13 @@ lib_deps =
SPI
monitor_speed = 115200
monitor_flags = --echo
[env:nano]
platform = atmelavr
board = nanoatmega328new
board_upload.maximum_size = 32256
framework = arduino
lib_deps =
${env.lib_deps}
monitor_speed = 115200
monitor_flags = --echo

View File

@@ -1,8 +1,13 @@
DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Release. Release v3.1.0 is a minor release that adds additional features and fixes a number of bugs. With the number of new features, this could have easily been a major release. The team is continually improving the architecture of DCC++EX to make it more flexible and optimizing the code so as to get more performance from the Arduino microprocessors. This release includes all of the Point Releases from v3.0.1 to v3.0.16.
The DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Release. Release v3.1.0 is a minor release that adds additional features and fixes a number of bugs. With the number of new features, this could have easily been a major release. The team is continually improving the architecture of DCC++EX to make it more flexible and optimizing the code so as to get more performance from the Arduino (and other) microprocessors. This release includes all of the Point Releases from v3.0.1 to v3.0.16.
**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**
[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.zip)
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.tar.gz)
**Known Issues**
- **Wi-Fi** - works, but requires sending <AT> commands from a serial monitor if you want to switch between AP mode and STA station mode.
- **Wi-Fi** - works, but requires sending <AT> commands from a serial monitor if you want to switch between AP mode and STA station mode after initial setup
- **Pololu Motor Shield** - is supported with this release, but the user may have to adjust timings to enable programming mode due to limitation in its current sensing circuitry
#### Summary of key features and/or bug fixes by Point Release
@@ -134,7 +139,7 @@ DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Relea
- **Fix EEPROM bugs**
- **Number of locos discovery command** - `<#>` command
- **Support for more locomotives** - 20 locomotives on an UNO and 50 an a Mega.
- **Automatic slot managment** - slot variable in throttle/function commands are ignored and slot management is taken care of automatically. `<!>` command added to release locos from memory.
- **Automatic slot management** - slot variable in throttle/function commands are ignored and slot management is taken care of automatically. `<->` and `<- CAB>` commands added to release locos from memory and stop packets to the track.
**Key Contributors**
@@ -149,10 +154,14 @@ DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Relea
- Neil McKechnie - Worcestershire, UK (NeilMck)
- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)
- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)
- M Steve Todd - - Engine Driver and JMRI Interface
- M Steve Todd -
- Scott Catalano - Pennsylvania
- Gregor Baues - Île-de-France, France (grbba)
**Engine Driver and JMRI Interface**
- M Steve Todd
**exInstaller Software**
- Anthony W - Dayton, Ohio, USA (Dex, Dex++)
@@ -175,9 +184,9 @@ DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Relea
**Beta Testing / Release Management / Support**
- Larry Dribin - Release Management
- Keith Ledbetter
- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)
- Brad Van der Elst
- Keith Ledbetter
- BradVan der Elst
- Andrew Pye
- Mike Bowers
- Randy McKenzie
@@ -190,3 +199,8 @@ DCC-EX Team is pleased to release CommandStation-EX-v3.1.0 as a Production Relea
- Gajanatha Kobbekaduwe
- Sumner Patterson
- Paul - Virginia, USA
**Downloads (zip and tar.gz) below. These are named without version number in the folder name to make the Arduino IDE happy.**
[CommandStation-EX.zip](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.zip)
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v3.1.0-Prod/CommandStation-EX.tar.gz)

317
tcpip.cpp Normal file
View File

@@ -0,0 +1,317 @@
// IP, ARP, UDP and TCP functions.
// Author: Guido Socher
// Copyright: GPL V2
//
// The TCP implementation uses some size optimisations which are valid
// only if all data can be sent in one single packet. This is however
// not a big limitation for a microcontroller as you will anyhow use
// small web-pages. The web server must send the entire web page in one
// packet. The client "web browser" as implemented here can also receive
// large pages.
//
// 2010-05-20 <jc@wippler.nl>
#include "EtherCard.h"
#include "net.h"
#undef word // arduino nonsense
#define gPB ether.buffer
#define PINGPATTERN 0x42
// Avoid spurious pgmspace warnings - http://forum.jeelabs.net/node/327
// See also http://gcc.gnu.org/bugzilla/show_bug.cgi?id=34734
//#undef PROGMEM
//#define PROGMEM __attribute__(( section(".progmem.data") ))
//#undef PSTR
//#define PSTR(s) (__extension__({static prog_char c[] PROGMEM = (s); &c[0];}))
#define TCP_STATE_SENDSYN 1
#define TCP_STATE_SYNSENT 2
#define TCP_STATE_ESTABLISHED 3
#define TCP_STATE_NOTUSED 4
#define TCP_STATE_CLOSING 5
#define TCP_STATE_CLOSED 6
static uint8_t destmacaddr[ETH_LEN]; // storing both dns server and destination mac addresses, but at different times because both are never needed at same time.
static boolean waiting_for_dns_mac = false; //might be better to use bit flags and bitmask operations for these conditions
static boolean has_dns_mac = false;
static boolean waiting_for_dest_mac = false;
static boolean has_dest_mac = false;
static uint8_t gwmacaddr[ETH_LEN]; // Hardware (MAC) address of gateway router
static uint8_t waitgwmac; // Bitwise flags of gateway router status - see below for states
//Define gateway router ARP statuses
#define WGW_INITIAL_ARP 1 // First request, no answer yet
#define WGW_HAVE_GW_MAC 2 // Have gateway router MAC
#define WGW_REFRESHING 4 // Refreshing but already have gateway MAC
#define WGW_ACCEPT_ARP_REPLY 8 // Accept an ARP reply
const unsigned char arpreqhdr[] PROGMEM = { 0,1,8,0,6,4,0,1 }; // ARP request header
const unsigned char iphdr[] PROGMEM = { 0x45,0,0,0x82,0,0,0x40,0,0x20 }; //IP header
extern const uint8_t allOnes[] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; // Used for hardware (MAC) and IP broadcast addresses
static void fill_checksum(uint8_t dest, uint8_t off, uint16_t len,uint8_t type) {
const uint8_t* ptr = gPB + off;
uint32_t sum = type==1 ? IP_PROTO_UDP_V+len-8 :
type==2 ? IP_PROTO_TCP_V+len-8 : 0;
while(len >1) {
sum += (uint16_t) (((uint32_t)*ptr<<8)|*(ptr+1));
ptr+=2;
len-=2;
}
if (len)
sum += ((uint32_t)*ptr)<<8;
while (sum>>16)
sum = (uint16_t) sum + (sum >> 16);
uint16_t ck = ~ (uint16_t) sum;
gPB[dest] = ck>>8;
gPB[dest+1] = ck;
}
static void setMACs (const uint8_t *mac) {
EtherCard::copyMac(gPB + ETH_DST_MAC, mac);
EtherCard::copyMac(gPB + ETH_SRC_MAC, EtherCard::mymac);
}
static void setMACandIPs (const uint8_t *mac, const uint8_t *dst) {
setMACs(mac);
EtherCard::copyIp(gPB + IP_DST_P, dst);
EtherCard::copyIp(gPB + IP_SRC_P, EtherCard::myip);
}
static boolean is_lan(const uint8_t source[IP_LEN], const uint8_t destination[IP_LEN]) {
if(source[0] == 0 || destination[0] == 0) {
return false;
}
for(int i = 0; i < IP_LEN; i++)
if((source[i] & EtherCard::netmask[i]) != (destination[i] & EtherCard::netmask[i])) {
return false;
}
return true;
}
static uint8_t eth_type_is_arp_and_my_ip(uint16_t len) {
return len >= 41 && gPB[ETH_TYPE_H_P] == ETHTYPE_ARP_H_V &&
gPB[ETH_TYPE_L_P] == ETHTYPE_ARP_L_V &&
memcmp(gPB + ETH_ARP_DST_IP_P, EtherCard::myip, IP_LEN) == 0;
}
static uint8_t eth_type_is_ip_and_my_ip(uint16_t len) {
return len >= 42 && gPB[ETH_TYPE_H_P] == ETHTYPE_IP_H_V &&
gPB[ETH_TYPE_L_P] == ETHTYPE_IP_L_V &&
gPB[IP_HEADER_LEN_VER_P] == 0x45 &&
(memcmp(gPB + IP_DST_P, EtherCard::myip, IP_LEN) == 0 //not my IP
|| (memcmp(gPB + IP_DST_P, EtherCard::broadcastip, IP_LEN) == 0) //not subnet broadcast
|| (memcmp(gPB + IP_DST_P, allOnes, IP_LEN) == 0)); //not global broadcasts
//!@todo Handle multicast
}
static void fill_ip_hdr_checksum() {
gPB[IP_CHECKSUM_P] = 0;
gPB[IP_CHECKSUM_P+1] = 0;
gPB[IP_FLAGS_P] = 0x40; // don't fragment
gPB[IP_FLAGS_P+1] = 0; // fragment offset
gPB[IP_TTL_P] = 64; // ttl
fill_checksum(IP_CHECKSUM_P, IP_P, IP_HEADER_LEN,0);
}
static void make_eth_ip() {
setMACs(gPB + ETH_SRC_MAC);
EtherCard::copyIp(gPB + IP_DST_P, gPB + IP_SRC_P);
EtherCard::copyIp(gPB + IP_SRC_P, EtherCard::myip);
fill_ip_hdr_checksum();
}
static void make_arp_answer_from_request() {
setMACs(gPB + ETH_SRC_MAC);
gPB[ETH_ARP_OPCODE_H_P] = ETH_ARP_OPCODE_REPLY_H_V;
gPB[ETH_ARP_OPCODE_L_P] = ETH_ARP_OPCODE_REPLY_L_V;
EtherCard::copyMac(gPB + ETH_ARP_DST_MAC_P, gPB + ETH_ARP_SRC_MAC_P);
EtherCard::copyMac(gPB + ETH_ARP_SRC_MAC_P, EtherCard::mymac);
EtherCard::copyIp(gPB + ETH_ARP_DST_IP_P, gPB + ETH_ARP_SRC_IP_P);
EtherCard::copyIp(gPB + ETH_ARP_SRC_IP_P, EtherCard::myip);
EtherCard::packetSend(42);
}
static void make_echo_reply_from_request(uint16_t len) {
make_eth_ip();
gPB[ICMP_TYPE_P] = ICMP_TYPE_ECHOREPLY_V;
if (gPB[ICMP_CHECKSUM_P] > (0xFF-0x08))
gPB[ICMP_CHECKSUM_P+1]++;
gPB[ICMP_CHECKSUM_P] += 0x08;
EtherCard::packetSend(len);
}
void EtherCard::makeUdpReply (const char *data,uint8_t datalen,uint16_t port) {
if (datalen>220)
datalen = 220;
gPB[IP_TOTLEN_H_P] = (IP_HEADER_LEN+UDP_HEADER_LEN+datalen) >>8;
gPB[IP_TOTLEN_L_P] = IP_HEADER_LEN+UDP_HEADER_LEN+datalen;
make_eth_ip();
gPB[UDP_DST_PORT_H_P] = gPB[UDP_SRC_PORT_H_P];
gPB[UDP_DST_PORT_L_P] = gPB[UDP_SRC_PORT_L_P];
gPB[UDP_SRC_PORT_H_P] = port>>8;
gPB[UDP_SRC_PORT_L_P] = port;
gPB[UDP_LEN_H_P] = (UDP_HEADER_LEN+datalen) >> 8;
gPB[UDP_LEN_L_P] = UDP_HEADER_LEN+datalen;
gPB[UDP_CHECKSUM_H_P] = 0;
gPB[UDP_CHECKSUM_L_P] = 0;
memcpy(gPB + UDP_DATA_P, data, datalen);
fill_checksum(UDP_CHECKSUM_H_P, IP_SRC_P, 16 + datalen,1);
packetSend(UDP_HEADER_LEN+IP_HEADER_LEN+ETH_HEADER_LEN+datalen);
}
void EtherCard::udpPrepare (uint16_t sport, const uint8_t *dip, uint16_t dport) {
if(is_lan(myip, dip)) { // this works because both dns mac and destinations mac are stored in same variable - destmacaddr
setMACandIPs(destmacaddr, dip); // at different times. The program could have separate variable for dns mac, then here should be
} else { // checked if dip is dns ip and separately if dip is hisip and then use correct mac.
setMACandIPs(gwmacaddr, dip);
}
// see http://tldp.org/HOWTO/Multicast-HOWTO-2.html
// multicast or broadcast address, https://github.com/njh/EtherCard/issues/59
if ((dip[0] & 0xF0) == 0xE0 || *((unsigned long*) dip) == 0xFFFFFFFF || !memcmp(broadcastip,dip,IP_LEN))
EtherCard::copyMac(gPB + ETH_DST_MAC, allOnes);
gPB[ETH_TYPE_H_P] = ETHTYPE_IP_H_V;
gPB[ETH_TYPE_L_P] = ETHTYPE_IP_L_V;
memcpy_P(gPB + IP_P,iphdr,sizeof iphdr);
gPB[IP_TOTLEN_H_P] = 0;
gPB[IP_PROTO_P] = IP_PROTO_UDP_V;
gPB[UDP_DST_PORT_H_P] = (dport>>8);
gPB[UDP_DST_PORT_L_P] = dport;
gPB[UDP_SRC_PORT_H_P] = (sport>>8);
gPB[UDP_SRC_PORT_L_P] = sport;
gPB[UDP_LEN_H_P] = 0;
gPB[UDP_CHECKSUM_H_P] = 0;
gPB[UDP_CHECKSUM_L_P] = 0;
}
void EtherCard::udpTransmit (uint16_t datalen) {
gPB[IP_TOTLEN_H_P] = (IP_HEADER_LEN+UDP_HEADER_LEN+datalen) >> 8;
gPB[IP_TOTLEN_L_P] = IP_HEADER_LEN+UDP_HEADER_LEN+datalen;
fill_ip_hdr_checksum();
gPB[UDP_LEN_H_P] = (UDP_HEADER_LEN+datalen) >>8;
gPB[UDP_LEN_L_P] = UDP_HEADER_LEN+datalen;
fill_checksum(UDP_CHECKSUM_H_P, IP_SRC_P, 16 + datalen,1);
packetSend(UDP_HEADER_LEN+IP_HEADER_LEN+ETH_HEADER_LEN+datalen);
}
void EtherCard::sendUdp (const char *data, uint8_t datalen, uint16_t sport,
const uint8_t *dip, uint16_t dport) {
udpPrepare(sport, dip, dport);
if (datalen>220)
datalen = 220;
memcpy(gPB + UDP_DATA_P, data, datalen);
udpTransmit(datalen);
}
// make a arp request
static void client_arp_whohas(uint8_t *ip_we_search) {
setMACs(allOnes);
gPB[ETH_TYPE_H_P] = ETHTYPE_ARP_H_V;
gPB[ETH_TYPE_L_P] = ETHTYPE_ARP_L_V;
memcpy_P(gPB + ETH_ARP_P, arpreqhdr, sizeof arpreqhdr);
memset(gPB + ETH_ARP_DST_MAC_P, 0, ETH_LEN);
EtherCard::copyMac(gPB + ETH_ARP_SRC_MAC_P, EtherCard::mymac);
EtherCard::copyIp(gPB + ETH_ARP_DST_IP_P, ip_we_search);
EtherCard::copyIp(gPB + ETH_ARP_SRC_IP_P, EtherCard::myip);
EtherCard::packetSend(42);
}
uint8_t EtherCard::clientWaitingGw () {
return !(waitgwmac & WGW_HAVE_GW_MAC);
}
static uint8_t client_store_mac(uint8_t *source_ip, uint8_t *mac) {
if (memcmp(gPB + ETH_ARP_SRC_IP_P, source_ip, IP_LEN) != 0)
return 0;
EtherCard::copyMac(mac, gPB + ETH_ARP_SRC_MAC_P);
return 1;
}
// static void client_gw_arp_refresh() {
// if (waitgwmac & WGW_HAVE_GW_MAC)
// waitgwmac |= WGW_REFRESHING;
// }
void EtherCard::setGwIp (const uint8_t *gwipaddr) {
delaycnt = 0; //request gateway ARP lookup
waitgwmac = WGW_INITIAL_ARP; // causes an arp request in the packet loop
copyIp(gwip, gwipaddr);
}
void EtherCard::updateBroadcastAddress()
{
for(uint8_t i=0; i<IP_LEN; i++)
broadcastip[i] = myip[i] | ~netmask[i];
}
uint16_t EtherCard::packetLoop (uint16_t plen) {
if (plen==0) {
//Check every 65536 (no-packet) cycles whether we need to retry ARP request for gateway
if ((waitgwmac & WGW_INITIAL_ARP || waitgwmac & WGW_REFRESHING) &&
delaycnt==0 && isLinkUp()) {
client_arp_whohas(gwip);
waitgwmac |= WGW_ACCEPT_ARP_REPLY;
}
delaycnt++;
//!@todo this is trying to find mac only once. Need some timeout to make another call if first one doesn't succeed.
if(is_lan(myip, dnsip) && !has_dns_mac && !waiting_for_dns_mac) {
client_arp_whohas(dnsip);
waiting_for_dns_mac = true;
}
//!@todo this is trying to find mac only once. Need some timeout to make another call if first one doesn't succeed.
if(is_lan(myip, hisip) && !has_dest_mac && !waiting_for_dest_mac) {
client_arp_whohas(hisip);
waiting_for_dest_mac = true;
}
return 0;
}
if (eth_type_is_arp_and_my_ip(plen))
{ //Service ARP request
if (gPB[ETH_ARP_OPCODE_L_P]==ETH_ARP_OPCODE_REQ_L_V)
make_arp_answer_from_request();
if (waitgwmac & WGW_ACCEPT_ARP_REPLY && (gPB[ETH_ARP_OPCODE_L_P]==ETH_ARP_OPCODE_REPLY_L_V) && client_store_mac(gwip, gwmacaddr))
waitgwmac = WGW_HAVE_GW_MAC;
if (!has_dns_mac && waiting_for_dns_mac && client_store_mac(dnsip, destmacaddr)) {
has_dns_mac = true;
waiting_for_dns_mac = false;
}
if (!has_dest_mac && waiting_for_dest_mac && client_store_mac(hisip, destmacaddr)) {
has_dest_mac = true;
waiting_for_dest_mac = false;
}
return 0;
}
if (eth_type_is_ip_and_my_ip(plen)==0)
{ //Not IP so ignoring
//!@todo Add other protocols (and make each optional at compile time)
return 0;
}
#if ETHERCARD_ICMP
if (gPB[IP_PROTO_P]==IP_PROTO_ICMP_V && gPB[ICMP_TYPE_P]==ICMP_TYPE_ECHOREQUEST_V)
{ //Service ICMP echo request (ping)
make_echo_reply_from_request(plen);
return 0;
}
#endif
if (plen < UDP_DATA_P || gPB[IP_PROTO_P]!=IP_PROTO_UDP_V )
return 0; //from here on we are only interested in UDP-packets
return plen + UDP_DATA_P; // Offset of UDP payload.
}
void EtherCard::copyIp (uint8_t *dst, const uint8_t *src) {
memcpy(dst, src, IP_LEN);
}
void EtherCard::copyMac (uint8_t *dst, const uint8_t *src) {
memcpy(dst, src, ETH_LEN);
}

View File

@@ -3,7 +3,32 @@
#include "StringFormatter.h"
#define VERSION "3.1.0"
#define VERSION "3.2.0 rc5"
// 3.2.0 Major functional and non-functional changes.
// New HAL added for I/O (digital and analogue inputs and outputs, servos etc).
// Support for MCP23008, MCP23017 and PCF9584 I2C GPIO Extender modules.
// Support for PCA9685 PWM (servo) control modules.
// Support for analogue inputs on Arduino pins and on ADS111x I2C modules.
// Support for MP3 sound playback via DFPlayer module.
// Support for HC-SR04 Ultrasonic range sensor module.
// Support for VL53L0X Laser range sensor module (Time-Of-Flight).
// Native non-blocking I2C drivers for AVR and Nano architectures (fallback
// to blocking Wire library for other platforms).
// EEPROM layout change - deletes EEPROM contents on first start following upgrade.
// New EX-RAIL automation capability.
// Turnout class revised to expand turnout capabilities, new commands added.
// Output class now allows ID > 255.
// Configuration options to globally flip polarity of DCC Accessory states when driven
// from <a> command and <T> command.
// Increased use of display for showing loco decoder programming information.
// ...
// 3.1.7 Bugfix: Unknown locos should have speed forward
// 3.1.6 Make output ID two bytes and guess format/size of registered outputs found in EEPROM
// 3.1.5 Fix LCD corruption on power-up
// 3.1.4 Refactor OLED and LCD drivers and remove unused code
// 3.1.3 Add a loop delay to give more time for sensing an Ethernet cable connection
// 3.1.2 Eliminate wait after write when prog is joined or prog power is off
// 3.1.1 SH1106 OLED Display Offset Fix
// 3.0.16 Ignore CV1 bit 7 read rejected by decoder when identifying loco id.
// 3.0.15 only send function commands once, not 4 times
// 3.0.14 gap in ack tolerant fix, prog track power management over join fix.