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

Compare commits

...

458 Commits

Author SHA1 Message Date
Harald Barth
a7ea96b392 improve parameter check for 'a' command 2022-07-06 21:12:10 +02:00
Harald Barth
ca3ed95624 fix broken 'a' command after broken commit c7b3817 2022-07-06 20:35:58 +02:00
Harald Barth
5cef1ac864 answer always with hearbeat number 2022-07-05 15:09:32 +02:00
Harald Barth
6687c6f46d when brake pin has inverted sense, honor that when running in DC mode as well 2022-07-04 23:15:02 +02:00
Harald Barth
0406ca69cf add comment Pololu TB9051FTG 2022-07-04 22:52:37 +02:00
Harald Barth
695b776493 add Pololu TB9051FTG 2022-07-04 22:52:37 +02:00
Harald Barth
9a98d10a86 rename fakePORT* to shadowPORT* 2022-06-13 23:18:10 +02:00
Harald Barth
4833eaac65 move all arch dependent port register declarations to MotorDriver.h 2022-06-13 23:15:35 +02:00
Harald Barth
744713769c fix merge error 2022-06-11 22:23:33 +02:00
Harald Barth
7fba96417f Merge branch 'TM_nofloat' into TM-mergetest 2022-06-11 22:14:56 +02:00
Harald Barth
5eb9678437 Merge branch 'master' into TrackManager-PORTX 2022-06-11 21:40:08 +02:00
Harald Barth
2385d0809c copyright adjustments 2022-06-11 20:38:44 +02:00
Harald Barth
808aa9aba9 show high accuracy status with track mode 2022-06-11 20:35:52 +02:00
Harald Barth
4297ed5572 remove global usePWM flag 2022-06-11 20:09:21 +02:00
Harald Barth
40dfda47c7 make usePWM per track (trackPWM) 2nd half 2022-06-11 19:57:45 +02:00
Asbelos
16fafccf15 Command Distributor
Improve and split multi-language responses. Remove dependency on RingStream.
2022-06-10 12:22:28 +01:00
Harald Barth
62e471606d remember the base case 2022-06-07 00:18:48 +02:00
Harald Barth
0ab96d28c4 make compile on Uno 2022-06-06 23:45:28 +02:00
Harald Barth
d37e303bdc tag version 2022-06-06 23:34:40 +02:00
Harald Barth
b24f6b27c6 protect setSignal() changes in setDCSignal from being changed back during interrupt vis setDCCSignal 2022-06-06 23:14:35 +02:00
Asbelos
2b2012ef1d float memory saver 2022-06-06 17:37:23 +01:00
Harald Barth
016bc37b53 clean up getThrottleSpeed functions 2022-06-05 23:07:03 +02:00
habazut
661d042744 Merge pull request #241 from DCC-EX/240-command-shows-flags-without-signal-aspect-information
Fix </> command for signals
2022-05-28 11:24:02 +02:00
Harald Barth
06e7ad5c53 prevent usage of pins for DC than can not do PWM 2022-05-25 09:28:28 +02:00
Harald Barth
1c78792dda Revert "test to remove port code"
This reverts commit cfcd61174d.
2022-05-25 07:41:47 +02:00
Harald Barth
cfcd61174d test to remove port code 2022-05-24 23:33:46 +02:00
Harald Barth
55561188e1 reset speed and loosen brake 2022-05-24 08:07:33 +02:00
Asbelos
5f568c05b9 Merge branch '240-command-shows-flags-without-signal-aspect-information' into TrackManager 2022-05-23 14:35:57 +01:00
Harald Barth
55196c2e7d tag 2022-05-23 00:04:35 +02:00
Harald Barth
c7b38170c1 Parse ONOFF with 4 param <a> command 2022-05-22 23:39:46 +02:00
Harald Barth
d3b72dc4fc Send onoff packets from setAccessory 2022-05-22 23:05:09 +02:00
Harald Barth
17fb921678 Explain better and change naming but same functionality 2022-05-22 22:43:06 +02:00
Harald Barth
867e3b3930 Reset track signal when leaving PWM and use port registers in DC mode as well 2022-05-21 10:19:25 +02:00
Harald Barth
79ef114c0d protect from setting unused pin 2022-05-21 09:51:38 +02:00
Asbelos
ebebd0dc11 Improved display and loop time for signals. 2022-05-19 09:03:28 +01:00
Asbelos
506b65d0ea Fix </> command for signals 2022-05-18 17:44:41 +01:00
Harald Barth
6d2a9e3b36 add EXT as a new track mode 2022-05-18 09:40:53 +02:00
Harald Barth
632d777fe7 version 2022-05-13 16:21:15 +02:00
Harald Barth
ff73a60874 Parse strings with more than one command (<s><Q>) correct 2022-05-13 16:18:47 +02:00
Harald Barth
f8a19de9fb tag it 2022-05-13 01:22:00 +02:00
Harald Barth
f0e8419fea tag it 2022-05-13 01:16:40 +02:00
Harald Barth
8f9da49cc8 Merge branch 'TrackManager' into TrackManager-PORTX 2022-05-13 00:21:02 +02:00
Harald Barth
d7a17b10b4 use ugly macros to make PORTX code portable 2022-05-13 00:05:25 +02:00
Harald Barth
0268304d41 fix type warning 2022-05-12 21:09:43 +02:00
Harald Barth
f66f5785f5 reorder statements in addTrack 2022-05-12 20:59:31 +02:00
Harald Barth
6d382fa0f4 TackManager: Make OFF=zero and when DCC then HA must be off 2022-05-12 20:56:23 +02:00
Harald Barth
af0d381e45 shadow PORTX (PoC) 2022-05-10 23:42:21 +02:00
Harald Barth
4a56998553 inline setSignal ; bugfix HA switching code by doing clearPWM 2022-05-10 23:37:24 +02:00
Ash-4
357560b226 Update version.h
Space character needed after 4.1.1 for JMRI parsing.
JMRI applies updated functions based on the version.
2022-05-07 10:51:00 -05:00
Harald Barth
589336eac3 better bugfix for bitfield in turnout struct 2022-05-07 08:47:34 +02:00
Ash-4
e13afd064d Merge pull request #234 from DCC-EX/ServoSignal
struct TurnoutData now consistent with 4.0.0 EEPROM
2022-05-04 14:37:40 -05:00
Ash-4
2d37947246 Update version.h 2022-05-04 14:33:08 -05:00
Ash-4
9367d708f7 Merge branch 'master' into ServoSignal 2022-05-04 14:09:58 -05:00
Ash-4
a614a616fa struct TurnoutData to enable EEPROM from v 4.0 2022-05-04 13:44:12 -05:00
Fred
2fd7a31ae4 Update version.h 2022-05-03 21:06:24 -04:00
Fred
977802f160 Servo signal (#227)
Prepping for version 4.1

SERVO_SIGNAL definition in EXRAIL
SERVO_SIGNAL(vpin, redpos, amberpos, greenpos)

use RED/AMBER/GREEN as for led signals.

* SIGNALH, ATGTE, ATLT

UNTESTED

* Automatic ALIAS(name)

and _ in keywords

* EXRAIL FORGET current loco

* EXRAIL </KILL ALL>

* EXRAIL VIRTUAL_TURNOUT

* Cleanup version.h

* Update version.h (#223)

Rewrite & Updated the 4.0.0 Section

* </KILL ALL> fix

* Incoming LCN turnout throw.

* KILLALL macro

and DIAGNOSTIC messages when KILL command used.

* EXRAIL PARSE

* Rebuild throttle info getters

UNTESTED... create different methods to obtain throttle info without being withrottle specific.

Also implements turnout description of "*" as hidden.

* J command parsing

JA JR JT commands parsed
EXRAIL sets hidden turnout state
HIDDEN description macro
Turnouts hidden flag bit
UNO seems OK, MEGA UNTESTED

* Assist notes draft & syntax tweaks

* Throttle notes

* uno memory saver

* JA JR and <t cab>

* Subtle corrections

* Update version.h

* I2C code corrections

Corrections to I2C code:
1) I2CManager_Mega4809.h: Correct bitwise 'and' to logical 'and' - no impact.
2) I2CManager_Wire.h: Ensure that error codes from Wire subsystem are passed back to caller in queueRequest().

* RAG Ifs and cmds

* IF block perf/memory

* Allow negative route ids.

* correct GREEN keyword

* Update version.h

* myFilter auto detect

* Update version.h

* fix weak ref to myFilter

* ACK defaults now 50-2000-20000

* Update version.h

* Improved SIGNALs startup and diagnostics

* Update IO_PCA9685.cpp

* Allow turnout id 0

* Position servo pin used as GPIO

* NoPowerOff LEDS

* CALLBACK parameter optional for Write

* WRITE CV ON PROG <W CV VALUE>

Callback parameters are now optional on PROG

* Updated CV read command <R cv>

Equivalent to <V cv 0>  uses the verify callback.

Co-authored-by: Asbelos <asbelos@btinternet.com>
Co-authored-by: Kcsmith0708 <kcsmith0708@wowway.com>
Co-authored-by: Neil McKechnie <neilmck999@gmail.com>
Co-authored-by: Ash-4 <81280775+Ash-4@users.noreply.github.com>
2022-05-03 16:53:33 -04:00
Harald Barth
ac32cd5528 guess value should be 0 not random bute in RAM 2022-05-03 08:38:35 +02:00
Ash-4
e721457844 Updated CV read command <R cv>
Updated CV read command <R cv>
2022-05-02 19:14:04 -05:00
Ash-4
7b40bd3290 Updated CV read command <R cv>
Equivalent to <V cv 0>  uses the verify callback.
2022-05-02 18:58:03 -05:00
Harald Barth
3e8649f9a1 zero transmit not pending repeats when ack is found 2022-05-02 22:15:02 +02:00
Ash-4
6994139e57 Merge pull request #230 from DCC-EX/ServoSignal
WRITE CV ON PROG <W CV VALUE>
2022-04-30 22:33:54 -05:00
Harald Barth
7a2fd90bfc set the reset packet counter of the prog track to 0 every time the track is turned on 2022-04-30 23:24:51 +02:00
Ash-4
b2df10a99a WRITE CV ON PROG <W CV VALUE>
Callback parameters are now optional on PROG
2022-04-29 23:23:15 -05:00
Ash-4
0dc91451d9 CALLBACK parameter optional for Write 2022-04-29 23:14:27 -05:00
Ash-4
43bac3f78e ACK defaults now 50-2000-20000 2022-04-29 19:24:56 -05:00
Asbelos
cd0b8790b6 Merge branch 'ServoSignal' into TrackManager 2022-04-29 23:07:04 +01:00
Asbelos
85c437b108 NoPowerOff LEDS 2022-04-29 23:06:44 +01:00
Asbelos
228553013b Merge branch 'ServoSignal' into TrackManager 2022-04-29 20:53:48 +01:00
Asbelos
f7d64d5449 Position servo pin used as GPIO 2022-04-29 19:34:08 +01:00
Asbelos
e7fb3648b0 Allow turnout id 0 2022-04-29 19:33:44 +01:00
Asbelos
acd6e7560f Merge branch 'ServoSignal' into TrackManager 2022-04-29 17:09:13 +01:00
Asbelos
c58a126dfc Update IO_PCA9685.cpp 2022-04-29 17:08:42 +01:00
Asbelos
5bdbe3895d spelling 2022-04-29 14:55:50 +01:00
Asbelos
bcd1335b08 Merge branch 'ServoSignal' into TrackManager 2022-04-29 13:58:26 +01:00
Asbelos
afd94f0645 Improved SIGNALs startup and diagnostics 2022-04-29 11:56:17 +01:00
Ash-4
ad97592788 Update version.h 2022-04-27 10:38:00 -05:00
Ash-4
431208d191 ACK defaults now 50-2000-20000 2022-04-27 10:32:21 -05:00
Asbelos
724dea22d5 Merge branch 'ServoSignal' into TrackManager 2022-04-20 09:10:44 +01:00
Asbelos
17eb7c560e fix weak ref to myFilter 2022-04-20 09:10:27 +01:00
Asbelos
21d1f482cf Merge branch 'ServoSignal' into TrackManager 2022-04-19 11:35:17 +01:00
Asbelos
ff4dd2f1cd Update version.h 2022-04-19 11:34:57 +01:00
Asbelos
9cf70f5870 myFilter auto detect 2022-04-19 09:35:03 +01:00
Asbelos
9273265036 Merge branch 'ServoSignal' into TrackManager 2022-04-18 16:59:02 +01:00
Asbelos
14834d47a5 Update version.h 2022-04-18 16:50:05 +01:00
Asbelos
6515f1b512 correct GREEN keyword 2022-04-18 16:47:07 +01:00
Asbelos
64cae26333 Allow negative route ids. 2022-04-18 16:46:13 +01:00
Asbelos
920fcbc095 IF block perf/memory 2022-04-17 10:10:22 +01:00
Asbelos
45f690eb4d RAG Ifs and cmds 2022-04-17 09:58:32 +01:00
Neil McKechnie
766fdc43ac I2C code corrections
Corrections to I2C code:
1) I2CManager_Mega4809.h: Correct bitwise 'and' to logical 'and' - no impact.
2) I2CManager_Wire.h: Ensure that error codes from Wire subsystem are passed back to caller in queueRequest().
2022-04-16 23:35:58 +01:00
Asbelos
32fdb014ef Update version.h 2022-04-12 23:32:23 +01:00
Asbelos
8522e05b13 merge issue with prog split 2022-04-12 23:28:21 +01:00
Asbelos
1b0d700009 Merge branch 'ThrottleInfo' into TrackManager 2022-04-12 23:16:41 +01:00
Asbelos
28a4406044 Subtle corrections 2022-04-12 23:10:29 +01:00
Asbelos
20b12bcb7c JA JR and <t cab> 2022-04-12 18:47:06 +01:00
Asbelos
e13175635c uno memory saver 2022-04-12 17:05:55 +01:00
Asbelos
b41ca2f44a Throttle notes 2022-04-12 17:05:44 +01:00
Asbelos
bfb88bb30a Assist notes draft & syntax tweaks 2022-04-08 16:13:15 +01:00
Asbelos
5846e0fe23 J command parsing
JA JR JT commands parsed
EXRAIL sets hidden turnout state
HIDDEN description macro
Turnouts hidden flag bit
UNO seems OK, MEGA UNTESTED
2022-04-08 11:41:50 +01:00
Asbelos
4c8b7f8517 Rebuild throttle info getters
UNTESTED... create different methods to obtain throttle info without being withrottle specific.

Also implements turnout description of "*" as hidden.
2022-04-03 11:19:04 +01:00
Asbelos
aaa3e7a83c Merge branch 'ServoSignal' into TrackManager 2022-03-31 22:20:33 +01:00
Asbelos
ece342f037 DC power fix 2022-03-31 22:19:13 +01:00
Asbelos
7c1c6dafa1 EXRAIL PARSE 2022-03-31 22:04:40 +01:00
Asbelos
731d838e83 KILLALL macro
and DIAGNOSTIC messages when KILL command used.
2022-03-31 21:52:43 +01:00
Asbelos
566f8ada23 Incoming LCN turnout throw. 2022-03-31 15:46:50 +01:00
Asbelos
a4c2ab7566 Merge branch 'master' into ServoSignal 2022-03-31 15:40:31 +01:00
Asbelos
f9e36e6693 Merge branch 'ServoSignal' into TrackManager 2022-03-31 10:13:53 +01:00
Asbelos
8085d03d65 </KILL ALL> fix 2022-03-31 10:11:34 +01:00
Asbelos
7dd680ccd5 Brake fix 2022-03-31 10:03:27 +01:00
Fred
ef50665c16 Update TrackManager.md
Add note section about zero stretching
2022-03-30 09:28:52 -04:00
Fred
0ab3fe07c5 Update FUNDING.yml 2022-03-29 11:59:40 -04:00
Fred
a37ca6b6b6 Update FUNDING.yml 2022-03-28 19:43:06 -04:00
Fred
5b12c2864d Create FUNDING.yml
Add patreon as our first funding site
2022-03-28 19:40:18 -04:00
Fred
90ca262cd9 Update label-sponsors.yml
Fix actions
2022-03-28 17:48:48 -04:00
Fred
b29eedf772 Create label-sponsors.yml file
When users who are sponsors submit a PR or an issue, a "sponsors" label will appear next to their name
2022-03-28 17:44:16 -04:00
Asbelos
3f283620d3 Include default brake pins for standard shield. 2022-03-28 15:21:07 +01:00
Asbelos
5adabcd1af Merge branch 'ServoSignal' into TrackManager 2022-03-28 14:56:27 +01:00
Asbelos
2727332be3 Update version.h 2022-03-28 14:53:16 +01:00
Asbelos
49e0a0e5f5 DC track change fixes 2022-03-28 14:44:41 +01:00
Helmut Fischer
71cd3fc292 README.md: dead link to rewrite (#217)
Corrected dead link to `notes/rewrite.md' with appropriat text.
2022-03-24 11:37:29 -04:00
Helmut Fischer
331538549f README.md: removed misleading "folder/subforlders" (#218)
Removed misleading mention of "folder named CommandStation-EX and its subforlders"
2022-03-24 11:36:34 -04:00
Kcsmith0708
78810d0e36 Update version.h (#220)
* Update version.h

Updated 4.0.2 release features
cleaned up 4.0.0 feature list

* Update version.h

Added and Updated 4.0.0 Features

* Update version.h

modified 4.0.0 new JMRI features

* Update version.h

Updated4.0.0 release information
2022-03-24 11:35:00 -04:00
Kcsmith0708
6826e01bd3 Update version.h (#223)
Rewrite & Updated the 4.0.0 Section
2022-03-24 11:34:11 -04:00
Kcsmith0708
931e348c3d Update version.h (#222)
Cleaned up & Updated 4.0.0 Section for public release
2022-03-24 11:32:24 -04:00
Asbelos
2cd0c169ce Cleanup version.h 2022-03-24 13:56:01 +00:00
Asbelos
92c2768c0b EXRAIL VIRTUAL_TURNOUT 2022-03-24 11:56:06 +00:00
Asbelos
0040f5caf6 EXRAIL </KILL ALL> 2022-03-24 11:10:09 +00:00
Asbelos
349f5d5362 EXRAIL FORGET current loco 2022-03-24 10:40:49 +00:00
Asbelos
a10fca2b12 TM Power setting fixes 2022-03-23 17:06:15 +00:00
Asbelos
99c7ff6c3f EXPERIMENTAL vpin for motorDriver power
No need to use fastpin for power, so we can allow a remoted vpin which helps when TrafficManger is running short of pins
2022-03-23 12:30:21 +00:00
Asbelos
2a87a6e997 Created TrackManager.md 2022-03-23 12:09:41 +00:00
Asbelos
dea55fec79 Merge branch 'ServoSignal' into TrackManager 2022-03-21 16:32:45 +00:00
Asbelos
269e1b36ea Automatic ALIAS(name)
and _ in keywords
2022-03-21 16:29:35 +00:00
Asbelos
865c8dd3bd DCX Track mode
And unified SET_TRACK Exrail macro
2022-03-19 16:26:29 +00:00
Asbelos
be186b967b CODE TIDY
Moved join code out of DCCWaveform to reduce footprint for ESP32 waveform replacement.
2022-03-19 11:22:31 +00:00
Asbelos
4f2dc0934f prevent DC addr 0 2022-03-18 20:03:19 +00:00
Asbelos
75b16c9047 Change track manager cmd to =
And fix the wrong param number at the same time!
2022-03-18 16:41:52 +00:00
Asbelos
cd952c6ede Merge branch 'ServoSignal' into TrackManager 2022-03-18 13:47:07 +00:00
Asbelos
367e400d75 SIGNALH, ATGTE, ATLT
UNTESTED
2022-03-18 13:46:07 +00:00
Asbelos
f5fb1540f0 Merge branch 'master' into ServoSignal 2022-03-18 13:43:15 +00:00
Asbelos
7e3dcb8e8c Correct merge issues from master 2022-03-07 18:46:29 +00:00
Asbelos
4437f870b6 Merge branch 'master' into TrackManager 2022-03-07 18:40:32 +00:00
Asbelos
ac3ffd2a36 Exrail BROADCAST and POWERON version 4.0.1 (#216)
* EXRAIL BROADCAST("msg") UNTESTED

* Add POWERON to EXRAIL

* POWERON only powers main, join will do both

* Update Version 4.0.1

* Broadcast jopin after driveaway

* rollback of previous edit  line 535 WiThrottle.cpp

* restructure GetLocoCallback() for better readability and put broadcastPower() at right place

Co-authored-by: Ash-4 <81280775+Ash-4@users.noreply.github.com>
Co-authored-by: Harald Barth <haba@kth.se>
2022-03-07 11:30:47 -05:00
Fred
c15ea6e083 Update config.example.h
Add description of display scroll modes
2022-03-07 08:05:02 -05:00
Chris Mayo
90092b4ad5 Remove EXRAIL/ENDEXRAIL from myAutomation.example.h (#215)
Use "startup sequence" to describe the initial instructions.
2022-03-06 10:07:44 -05:00
Chris Mayo
fa4f5f08ef Comment Typos (#214)
* Example config comment typos

* Code comment typos
2022-03-06 10:05:35 -05:00
Harald Barth
f7e2c0ca99 add ESP defines 2022-03-04 23:37:27 +01:00
Harald Barth
2890a7928b restart with AVR WDT or ESP.restart() 2022-03-04 23:36:31 +01:00
Harald Barth
03372f21e2 rename FAIL to CALLFAIL because of conflict in ESP32 IDE 2022-03-04 23:35:14 +01:00
Asbelos
678cccbd95 Add DC signal generation code. 2022-03-02 15:34:01 +00:00
Asbelos
524afc6caf move more cpu specifics 2022-03-02 14:24:49 +00:00
Asbelos
6fc223d80b Timer stuff with incomplete teensy 2022-03-01 12:52:25 +00:00
Asbelos
dd9152864b Missing DCC startup!!! + EXRAIL POWERON catchup 2022-02-28 10:38:26 +00:00
Asbelos
4f781074eb tidy and shorten loops 2022-02-28 09:32:26 +00:00
Asbelos
b29b8c999e keyword values 2022-02-24 11:58:40 +00:00
Asbelos
99e636974a SET_TRACK_DC/SET_TRACK_DCC
EXRAIL macros for TrackManager
SET_TRACK_DC sets the track to DC using the cab address of the current loco so you can drive..
2022-02-24 11:50:22 +00:00
Asbelos
74bbe595fc DC hooks
still requires pin jiggling
2022-02-23 16:21:45 +00:00
Asbelos
1afb4753ec Cleaner ack check
And drop CPU specific stuff no longer needed.
2022-02-23 16:03:15 +00:00
Asbelos
a7740d652d It builds....
massive track reorganization
2022-02-23 15:44:34 +00:00
Asbelos
8db937e985 Initial Track Manager code 2022-02-22 01:27:27 +00:00
Fred
3038f29dac Update release_notes_v4.0.0.md 2022-02-17 15:02:49 -05:00
Fred
e57cd1e2aa Update release_notes.md 2022-02-17 15:00:59 -05:00
Fred
c47dd101a5 Update release_notes_v4.0.0.md 2022-02-17 12:29:59 -05:00
Chris Mayo
cf905ce2f3 Clarify config.h entry for an OLED with a SH1106 (#210)
See:
79763a3 ("SH1106 OLED Display Offset Fix (#169)", 2021-06-10)
97f3450 ("Simplify OLED driver initialisation.", 2021-11-24)
2022-02-17 12:21:24 -05:00
Kebbin
4bfcaa605a update release_notes.md (#212) 2022-02-17 08:00:11 -05:00
Fred
557b71c036 Frightrisk update v4 (#209)
* Add Haba's bugfix for analog current reading with interrupts

* Update verson and release notes for 4.0

* remove unwanted changes to newer files
2022-02-16 12:18:43 -05:00
Fred
d0b01e3f03 Frightrisk ver4 (#205)
* Add release notes for verion 4.0

* Add archive release notes

* fix for SHA to modify previous commit instead of using new SHA

* new format for supported boards to allow easy addition of new boards

* Update release_notes.md (#206)

Formatting lines
Server Roster clarification and release 4,0 bug fix  vs 3.1 edit.
added support members

* Some duplication removal and rewording (#207)

WiThrottle and JMRI-related items

Co-authored-by: Fred <fndecker@gmail.com>

* rename config.json to installer.json, fixes for boards(may still be broken)

Co-authored-by: dexslab <dex35803@gmail.com>
Co-authored-by: Kcsmith0708 <kcsmith0708@wowway.com>
Co-authored-by: mstevetodd <mstevetodd@mstevetodd.com>
2022-02-16 12:02:11 -05:00
Dex
bd9a04572d Committing a SHA 2022-02-11 14:53:16 +00:00
Dex
a26d988acc add config.json to master repo for installer (#204) 2022-02-11 09:52:56 -05:00
Asbelos
4300a3fdac UNTESTED SERVO_SIGNAL
SERVO_SIGNAL definition in EXRAIL
SERVO_SIGNAL(vpin, redpos, amberpos, greenpos)

use RED/AMBER/GREEN as for led signals.
2022-02-06 13:56:51 +00:00
Asbelos
22bf2b69f1 Committing a SHA 2022-02-04 15:33:26 +00:00
Asbelos
58ef7d25db Remove myAutomation2.h
This file was never intended for release.
2022-02-04 15:33:04 +00:00
Harald Barth
1ec3b173fd Committing a SHA 2022-01-31 22:25:59 +00:00
Harald Barth
07c0004996 Merge branch 'master' of https://github.com/DCC-EX/CommandStation-EX 2022-01-31 22:54:38 +01:00
FrightRisk
99a62883c7 Committing a SHA 2022-01-31 02:51:10 +00:00
FrightRisk
b7af6be1f2 Fix remaining rmft remnants 2022-01-30 21:50:43 -05:00
Fred
eb6b14b848 Committing a SHA 2022-01-31 02:39:51 +00:00
Fred
fa261ec39f Update version.h 2022-01-30 21:39:37 -05:00
Harald Barth
4ebf7978d8 rename all RMFT_ACTIVE to EXTAIL_ACTVE 2022-01-31 03:00:29 +01:00
Harald Barth
a0a635c167 version 2022-01-31 00:05:13 +01:00
Harald Barth
ca0c34f9fa Merge branch 'currentsenseirq' 2022-01-31 00:03:35 +01:00
Harald Barth
b6501c7e3e revert to write ERROR 2022-01-30 23:58:34 +01:00
Harald Barth
8af74f7082 protect analog read with cli() 2022-01-30 23:56:16 +01:00
Fred
75598b2b8b Committing a SHA 2022-01-30 17:31:41 +00:00
Fred
bd7c8bf78e Rename RMFT files and references to EXRAIL (#201)
Make naming consistent with our marketing of ex-rail for files and defines

* Rename RMFT.h to EXRAIL.h

* Rename RMFT2.cpp to EXRAIL2.cpp

* Rename RMFT2.h to EXRAIL2.h

* Rename RMFT2MacroReset.h to EXRAIL2MacroReset.h

* Rename RMFTMacros.h to EXRAILMacros.h

* Rename RMFT references to EXRAIL
2022-01-30 12:31:26 -05:00
Harald Barth
05545321a9 Committing a SHA 2022-01-23 17:55:49 +00:00
Harald Barth
03f7014c02 spell example right 2022-01-23 18:55:00 +01:00
Harald Barth
306f100085 Merge branch 'master' of https://github.com/DCC-EX/CommandStation-EX 2022-01-17 19:01:58 +01:00
Asbelos
509014bb6a Committing a SHA 2022-01-17 15:56:37 +00:00
Asbelos
f577c11eb7 Correct ack diag msgs
Bug caused by unsigned ints >32k being displayed as negative int.
2022-01-17 15:56:16 +00:00
Harald Barth
73ea7a1479 compiles on Nano Every 2022-01-15 20:23:58 +01:00
Harald Barth
f807339eec suggested code readability improvement by compiler warning 2022-01-15 20:21:28 +01:00
Harald Barth
b4aa47a451 Committing a SHA 2022-01-15 16:24:52 +00:00
Harald Barth
1416f83f8c Serial check, mostly for Teensy, does not impact startup if Serial is available 2022-01-15 17:24:18 +01:00
Asbelos
d5955a36bf Committing a SHA 2022-01-11 11:22:42 +00:00
Asbelos
35f7ac3d77 Teensy compatibility issues 2022-01-11 11:22:20 +00:00
Harald Barth
41cda58bef Committing a SHA 2022-01-08 20:47:13 +00:00
Harald Barth
0b8c455594 update version 2022-01-08 21:45:46 +01:00
Harald Barth
e0a7c4d155 Fixed regression: Shields with common fault pin works again 2022-01-08 21:44:32 +01:00
Harald Barth
5f878b5911 Committing a SHA 2022-01-07 01:41:00 +00:00
Harald Barth
61390cb0e2 update copyright notes typo 2022-01-07 02:36:34 +01:00
Harald Barth
d45585ce3d update copyright notes 2022-01-07 02:28:35 +01:00
Harald Barth
434c292fd9 typo 2022-01-07 00:11:29 +01:00
Harald Barth
b0915e8332 format/indentation change only 2022-01-06 23:03:57 +01:00
Asbelos
1934fdd0e1 Merge branch 'EXRAILPlus' of https://github.com/DCC-EX/CommandStation-EX into EXRAILPlus 2022-01-05 10:00:16 +00:00
Asbelos
ff102dbf88 ACTIVATEL/DEACTIVATEL error 2022-01-05 10:00:11 +00:00
Harald Barth
85f0712d31 balance if/endif 2022-01-05 01:23:36 +01:00
Asbelos
14ede75643 Merge remote-tracking branch 'origin/mDNS' into EXRAILPlus 2022-01-04 21:28:52 +00:00
Asbelos
503d4c56cf Merge branch 'EXRAILPlus' of https://github.com/DCC-EX/CommandStation-EX into EXRAILPlus 2022-01-04 21:26:29 +00:00
Asbelos
e78c3001cf pesky comma in RANDWAIT 2022-01-04 21:26:21 +00:00
Harald Barth
b0e81eec46 Merge branch 'EXRAILPlus' of https://github.com/DCC-EX/CommandStation-EX into EXRAILPlus 2022-01-04 21:16:34 +01:00
Harald Barth
823420615e add makerblock orion uno with integrated H-bridge 2022-01-04 21:16:06 +01:00
Asbelos
9cd3e4b7c1 ON handler fake recursion 2022-01-04 20:09:56 +00:00
Asbelos
024313deac Avoid warning if no roster 2022-01-04 19:46:52 +00:00
Harald Barth
bbd569cc88 add mDNS 2022-01-04 19:47:57 +01:00
Asbelos
e3bca1592c Automatic delay accuracy adjust 2022-01-03 19:15:44 +00:00
Asbelos
7017c6bbf5 Reduced RAM/PROGMEM and CPU for signals. 2022-01-03 12:43:06 +00:00
Asbelos
230a119cd0 ATTIMEOUT / IFTIMEOUT 2022-01-03 10:15:10 +00:00
Asbelos
1ad4e57332 ELSE in EXRAIL 2022-01-02 19:41:57 +00:00
Harald Barth
a806af6f2f do not broadcast at create slot 2022-01-01 12:08:28 +01:00
Asbelos
582d30916e Withrottle connect speedup 2021-12-29 15:13:37 +00:00
Asbelos
06a07a49cd Add IFTHROWN/IFCLOSED to Exrail 2021-12-29 11:15:31 +00:00
Asbelos
a003d54fdd tidying 2021-12-28 19:06:47 +00:00
Asbelos
7fc2d32ad3 Avoid compiler bug on some versions
https://github.com/arduino/ArduinoCore-avr/issues/39
2021-12-28 18:31:13 +00:00
Asbelos
a45a43f6d4 roster/functions 2021-12-28 13:47:40 +00:00
Asbelos
b7077565b9 Roster list part 1 2021-12-26 18:24:04 +00:00
Harald Barth
00e3c80b44 Committing a SHA 2021-12-21 12:28:31 +00:00
Harald Barth
7a7ca6a436 rc8 2021-12-21 13:26:16 +01:00
Asbelos
cc1cdc35ec one-off error in CIPSEND drop 2021-12-21 13:24:00 +01:00
Asbelos
7b8fa200f2 Merge branch 'Broadcast' into EXRAILPlus 2021-12-21 10:17:06 +00:00
Asbelos
52cc1ecd7b one-off error in CIPSEND drop 2021-12-21 10:16:45 +00:00
Asbelos
a4fcff902c Merge branch 'Broadcast' into EXRAILPlus 2021-12-21 09:14:50 +00:00
Asbelos
0912ad484a less broadcast noise
Avoids erroneous broadcast of all slots with no loco on ESTOP.
Avoids sending <l> states and <q>  to withrottles
2021-12-21 09:14:27 +00:00
Asbelos
b05cbc1fdf Correct <+> command any serial 2021-12-20 10:36:17 +00:00
Asbelos
52e7929b08 Correcting <+> command any-serial 2021-12-20 10:33:48 +00:00
Asbelos
c15d536e9b Merge branch 'Broadcast' into EXRAILPlus 2021-12-20 10:21:44 +00:00
Asbelos
e24e1669f7 broadcast EXRAIL unjoin 2021-12-19 20:37:42 +00:00
Asbelos
cbf9f39ea6 AT passthrough from any HardwareSerial stream
IE cant passthrough from wifi!
2021-12-19 10:24:18 +00:00
Asbelos
65ce238bfb Merge branch 'ATpassthrough' into Broadcast 2021-12-18 22:06:31 +00:00
Asbelos
10828bc6b8 catch bad params in F 2021-12-17 21:19:55 +00:00
Asbelos
aa40231ac7 catch bad param count in F 2021-12-17 21:19:16 +00:00
Asbelos
89cf6016e8 Fixup UNO 2021-12-17 20:09:38 +00:00
Asbelos
988510112d uno 2021-12-17 20:07:33 +00:00
Asbelos
2c47c309dc Merge branch 'Broadcast' into EXRAILPlus 2021-12-16 12:37:09 +00:00
Asbelos
f755c291d5 Turnout typos and power broadcast 2021-12-16 12:32:14 +00:00
Asbelos
94a2839bca simplify LCD power state 2021-12-16 12:11:38 +00:00
Asbelos
0eacda0cf9 Improved error msg 2021-12-16 11:23:34 +00:00
Asbelos
6bfe18bb21 Parser hex code save 2021-12-16 11:23:20 +00:00
Asbelos
82092075bf Merge branch 'Broadcast' into EXRAILPlus 2021-12-16 10:40:58 +00:00
Asbelos
1b07d0a5c6 Simplify Withrottle function changes 2021-12-16 10:28:41 +00:00
Asbelos
e5c66a2755 Fixup functionMap and remove duplicates 2021-12-15 22:04:09 +00:00
Asbelos
0947467bfa Correct functionmap length
And remove withrottle replies that would be generated by the broadcast.
2021-12-15 20:53:55 +00:00
Asbelos
4d809b85b3 Clean up exrail warning on nanos 2021-12-15 20:08:24 +00:00
Asbelos
2ddf583fbc Merge branch 'Broadcast' into EXRAILPlus 2021-12-15 19:59:59 +00:00
Asbelos
bb2c85d973 Merge branch 'master' into EXRAILPlus 2021-12-15 19:56:55 +00:00
Asbelos
b0c9806f3b Withrottle broadcast functions and speeds 2021-12-15 19:51:01 +00:00
Asbelos
96933ed516 Broadcast if group changed 2021-12-14 11:50:59 +00:00
Asbelos
985f0e777c fixup power broacast 2021-12-13 21:16:58 +00:00
Asbelos
2049cc89b3 Emit EXRAIL power changes 2021-12-07 00:57:08 +00:00
Asbelos
18695888dd Fixing broadcast 2021-12-07 00:24:48 +00:00
Asbelos
b8293d07f2 Speed broadcast 2021-12-05 18:06:28 +00:00
Asbelos
a4fc10d466 Wifi/Ethernet warnings 2021-12-05 12:24:46 +00:00
Asbelos
0a40ef5ceb Merge branch 'master' into Broadcast 2021-12-05 12:13:39 +00:00
Asbelos
0f36ccdc57 Broadcast changes (1) UNTESTED 2021-12-05 12:08:59 +00:00
Harald Barth
92591c8a2e Committing a SHA 2021-12-02 07:36:44 +00:00
Harald Barth
0f728c1c15 3 diffenent defines to fix RCN-213 compat 2021-12-02 08:35:42 +01:00
Harald Barth
b5af39dfc9 Merge branch 'RCN213-fixes' into master 2021-12-02 08:31:33 +01:00
Harald Barth
4924cc7779 update version 2021-11-30 20:11:45 +01:00
Harald Barth
7d665fe577 update version 2021-11-30 20:08:58 +01:00
Harald Barth
5c18f4a19d Merge branch 'short-long-addr' into master 2021-11-30 20:07:52 +01: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
Asbelos
0237c9721f Chgange IFANALOG to IFGTE/IFLT 2021-11-30 13:52:22 +00: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
Asbelos
259696a117 IFANALOG(pin, value) 2021-11-28 12:09:36 +00:00
Asbelos
4a8065d33b Turnout Descriptions
UNTESTED
Also allows alias inside EXRAIL
Allows self-guarded code
Ignores EXRAIL and ENDEXRAIL keywords as unnecessary.
2021-11-27 11:29:26 +00:00
Harald Barth
43538d3b32 smaller code 2021-11-26 19:32:45 +01:00
Asbelos
c363ea4714 Merge branch 'master' into EXRAILPlus 2021-11-26 09:01:33 +00:00
Harald Barth
fd43a9b88b defines to reverse accessories and turnouts renamed 2021-11-25 23:10:03 +01:00
Harald Barth
8a17965cd2 type and correct include 2021-11-25 19:55:48 +01:00
Asbelos
a4e94610e6 one shot DRIVE
UNTESTED
2021-11-25 11:45:45 +00:00
Asbelos
92d6a15ee5 ONACTIVATE catchers etc
UNTESTED SO FAR
2021-11-25 11:36:05 +00:00
Harald Barth
3bddeeda3e better long/short addr handling under <R>; configurable long/short border 2021-11-25 00:10:11 +01: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
Asbelos
ef1719f6fc DRIVE (part 1 experimental) 2021-11-24 11:56:55 +00:00
Asbelos
39c7bf3983 Activate and remove NOP macros 2021-11-22 11:10:26 +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
Asbelos
0018ba676b AUTOSTART macro
Starts a new task at this point during initialisation.  (no need to put a separate start command at the beginning)
2021-11-19 13:00:21 +00:00
Asbelos
5cb427f774 Lookups(2) UNTESTED
Fast lookup code
2021-11-18 14:57:09 +00:00
Asbelos
4ea458b140 lookups(1)
Faster runtime lookups at the expense of some ram
2021-11-18 10:42:54 +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
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
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
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
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
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
Asbelos
683f9d33fe Ignore <+> from wifi or ethernet 2021-06-30 22:01:18 +01:00
Asbelos
33b5f4fdf0 <+> command passthrough 2021-06-30 18:05:03 +01:00
105 changed files with 12248 additions and 2880 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: DCC-EX
patreon: dccex

14
.github/workflows/label-sponsors.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: Label sponsors
on:
pull_request:
types: [opened]
issues:
types: [opened]
jobs:
build:
name: is-sponsor-label
runs-on: ubuntu-latest
steps:
- uses: JasonEtco/is-sponsor-label-action@v1.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -24,10 +24,11 @@ jobs:
sha=$(git rev-parse --short "$GITHUB_SHA")
echo "#define GITHUB_SHA \"$sha\"" > GITHUB_SHA.h
- uses: EndBug/add-and-commit@v4 # You can change this to use a specific version
- uses: EndBug/add-and-commit@v8 # You can change this to use a specific version
with:
add: 'GITHUB_SHA.h'
message: 'Committing a SHA'
commit: --amend
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Leave this line unchanged

10
.gitignore vendored
View File

@@ -7,5 +7,13 @@ Release/*
.pio/
.vscode/
config.h
.vscode/extensions.json
.vscode/*
mySetup.h
mySetup.cpp
myHal.cpp
myAutomation.h
myFilter.cpp
myAutomation.h
myFilter.cpp
myLayout.h
.vscode/extensions.json

View File

@@ -3,5 +3,8 @@
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

View File

@@ -1,6 +1,9 @@
/*
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
*
* © 2022 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020 Gregor Baues
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
@@ -16,16 +19,145 @@
* 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 "CommandDistributor.h"
#include "SerialManager.h"
#include "WiThrottle.h"
#include "DIAG.h"
#include "defines.h"
#include "DCCWaveform.h"
#include "DCC.h"
#include "TrackManager.h"
DCCEXParser * CommandDistributor::parser=0;
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * streamer) {
if (buffer[0] == '<') {
if (!parser) parser = new DCCEXParser();
parser->parse(streamer, buffer, streamer);
#ifdef BIG_RAM
// use a buffer to allow broadcast
#define BUFFER broadcastBufferWriter
#define FLUSH broadcastBufferWriter->flush();
#define SHOVE(type) broadcastToClients(type);
StringBuffer * CommandDistributor::broadcastBufferWriter=new StringBuffer();
#else
// on a UNO/NANO write direct to Serial and ignore flush/shove
#define BUFFER &Serial
#define FLUSH
#define SHOVE(type)
#endif
#ifdef CD_HANDLE_RING
// wifi or ethernet ring streams with multiple client types
RingStream * CommandDistributor::ring=0;
byte CommandDistributor::ringClient=NO_CLIENT;
CommandDistributor::clientType CommandDistributor::clients[8]={
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE};
// Parse is called by Withrottle or Ethernet interface to determine which
// protocol the client is using and call the appropriate part of dcc++Ex
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream) {
ring=stream;
ringClient=stream->peekTargetMark();
if (buffer[0] == '<') {
clients[clientId]=COMMAND_TYPE;
DCCEXParser::parse(stream, buffer, ring);
} else {
clients[clientId]=WITHROTTLE_TYPE;
WiThrottle::getThrottle(clientId)->parse(ring, buffer);
}
else WiThrottle::getThrottle(clientId)->parse(streamer, buffer);
ringClient=NO_CLIENT;
}
void CommandDistributor::forget(byte clientId) {
clients[clientId]=NONE_TYPE;
}
#endif
// This will not be called on a uno
void CommandDistributor::broadcastToClients(clientType type) {
/* Boadcast to Serials */
if (type==COMMAND_TYPE) SerialManager::broadcast(broadcastBufferWriter->getString());
#ifdef CD_HANDLE_RING
// If we are broadcasting from a wifi/eth process we need to complete its output
// before merging broadcasts in the ring, then reinstate it in case
// the process continues to output to its client.
if (ringClient!=NO_CLIENT) ring->commit();
/* loop through ring clients */
for (byte clientId=0; clientId<sizeof(clients); clientId++) {
if (clients[clientId]==type) {
ring->mark(clientId);
ring->print(broadcastBufferWriter->getString());
ring->commit();
}
}
if (ringClient!=NO_CLIENT) ring->mark(ringClient);
#endif
}
// Public broadcast functions below
void CommandDistributor::broadcastSensor(int16_t id, bool on ) {
FLUSH
StringFormatter::send(BUFFER,F("<%c %d>\n"), on?'Q':'q', id);
SHOVE(COMMAND_TYPE)
}
void CommandDistributor::broadcastTurnout(int16_t id, bool isClosed ) {
// For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed;
// The string below contains serial and Withrottle protocols which should
// be safe for both types.
FLUSH
StringFormatter::send(BUFFER,F("<H %d %d>\n"),id, !isClosed);
SHOVE(COMMAND_TYPE)
#ifdef CD_HANDLE_RING
FLUSH
StringFormatter::send(BUFFER,F("PTA%c%d\n"), isClosed?'2':'4', id);
SHOVE(WITHROTTLE_TYPE)
#endif
}
void CommandDistributor::broadcastLoco(byte slot) {
DCC::LOCO * sp=&DCC::speedTable[slot];
FLUSH
StringFormatter::send(BUFFER,F("<l %d %d %d %l>\n"),
sp->loco,slot,sp->speedCode,sp->functions);
SHOVE(COMMAND_TYPE)
#ifdef CD_HANDLE_RING
WiThrottle::markForBroadcast(sp->loco);
#endif
}
void CommandDistributor::broadcastPower() {
bool main=TrackManager::getMainPower()==POWERMODE::ON;
bool prog=TrackManager::getProgPower()==POWERMODE::ON;
bool join=TrackManager::isJoined();
const FSH * reason=F("");
char state='1';
if (main && prog && join) reason=F(" JOIN");
else if (main && prog);
else if (main) reason=F(" MAIN");
else if (prog) reason=F(" PROG");
else state='0';
FLUSH
StringFormatter::send(BUFFER,F("<p %c%S>\n"),state,reason);
SHOVE(COMMAND_TYPE)
#ifdef CD_HANDLE_RING
FLUSH
StringFormatter::send(BUFFER,F("PPA%c\n"), main?'1':'0');
SHOVE(WITHROTTLE_TYPE)
#endif
LCD(2,F("Power %S%S"),state=='1'?F("On"):F("Off"),reason);
}
void CommandDistributor::broadcastText(const FSH * msg) {
FLUSH
StringFormatter::send(BUFFER,F("%S"),msg);
SHOVE(COMMAND_TYPE)
#ifdef CD_HANDLE_RING
FLUSH
StringFormatter::send(BUFFER,F("Hm%S\n"), msg);
SHOVE(WITHROTTLE_TYPE)
#endif
}

View File

@@ -1,6 +1,9 @@
/*
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
*
* © 2022 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020 Gregor Baues
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
@@ -20,13 +23,34 @@
#define CommandDistributor_h
#include "DCCEXParser.h"
#include "RingStream.h"
#include "StringBuffer.h"
#include "defines.h"
#if WIFI_ON | ETHERNET_ON
// Command Distributor must handle a RingStream of clients
#define CD_HANDLE_RING
#endif
class CommandDistributor {
public :
static void parse(byte clientId,byte* buffer, RingStream * streamer);
static void parse(byte clientId,byte* buffer, RingStream * ring);
static void broadcastLoco(byte slot);
static void broadcastSensor(int16_t id, bool value);
static void broadcastTurnout(int16_t id, bool isClosed);
static void broadcastPower();
static void broadcastText(const FSH * msg);
static void forget(byte clientId);
private:
static DCCEXParser * parser;
enum clientType: byte {NONE_TYPE,COMMAND_TYPE,WITHROTTLE_TYPE};
static void broadcastToClients(clientType type);
static StringBuffer * broadcastBufferWriter;
#ifdef CD_HANDLE_RING
static RingStream * ring;
static const byte NO_CLIENT=255;
static byte ringClient;
static clientType clients[8];
#endif
};
#endif

View File

@@ -1,33 +1,35 @@
////////////////////////////////////////////////////////////////////////////////////
// DCC-EX CommandStation-EX Please see https://DCC-EX.com
// DCC-EX CommandStation-EX Please see https://DCC-EX.com
//
// This file is the main sketch for the Command Station.
//
// CONFIGURATION:
//
// CONFIGURATION:
// Configuration is normally performed by editing a file called config.h.
// This file is NOT shipped with the code so that if you pull a later version
// of the code, your configuration will not be overwritten.
//
// If you used the automatic installer program, config.h will have been created automatically.
//
// To obtain a starting copy of config.h please copy the file config.example.h which is
// shipped with the code and may be updated as new features are added.
//
//
// To obtain a starting copy of config.h please copy the file config.example.h which is
// shipped with the code and may be updated as new features are added.
//
// If config.h is not found, config.example.h will be used with all defaults.
////////////////////////////////////////////////////////////////////////////////////
#if __has_include ( "config.h")
#include "config.h"
#else
#warning config.h not found. Using defaults from config.example.h
#warning config.h not found. Using defaults from config.example.h
#include "config.example.h"
#endif
/*
* © 2020,2021 Chris Harlow, Harald Barth, David Cutting,
* Fred Decker, Gregor Baues, Anthony W - Dayton All rights reserved.
*
* © 2021 Neil McKechnie
* © 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
@@ -44,13 +46,16 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "DCCEX.h"
// Create a serial command parser for the USB connection,
// This supports JMRI or manual diagnostics and commands
// to be issued from the USB serial console.
DCCEXParser serialParser;
#ifdef WIFI_WARNING
#warning You have defined that you want WiFi but your hardware has not enough memory to do that, so WiFi DISABLED
#endif
#ifdef ETHERNET_WARNING
#warning You have defined that you want Ethernet but your hardware has not enough memory to do that, so Ethernet DISABLED
#endif
#ifdef EXRAIL_WARNING
#warning You have myAutomation.h but your hardware has not enough memory to do that, so EX-RAIL DISABLED
#endif
void setup()
{
@@ -58,15 +63,15 @@ void setup()
// Responsibility 1: Start the usb connection for diagnostics
// This is normally Serial but uses SerialUSB on a SAMD processor
Serial.begin(115200);
SerialManager::init();
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
// This block is still executed for DIAGS if LCD not in use
LCD(0,F("DCC++ EX v%S"),F(VERSION));
LCD(1,F("Lic GPLv3"));
}
LCD(1,F("Lic GPLv3"));
}
// Responsibility 2: Start all the communications before the DCC engine
// Start the WiFi interface on a MEGA, Uno cannot currently handle WiFi
@@ -79,29 +84,35 @@ void setup()
EthernetInterface::setup();
#endif // ETHERNET_ON
// Initialise HAL layer before reading EEprom or setting up MotorDrivers
IODevice::begin();
// Responsibility 3: Start the DCC engine.
// 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);
TrackManager::Setup(MOTOR_SHIELD_TYPE);
#if defined(RMFT_ACTIVE)
RMFT::begin();
#endif
// Start RMFT aka EX-RAIL (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"
#undef SETUP
#endif
#define SETUP(cmd) DCCEXParser::parse(F(cmd))
#include "mySetup.h"
#undef SETUP
#endif
#if defined(LCN_SERIAL)
LCN_SERIAL.begin(115200);
LCN::init(LCN_SERIAL);
LCN_SERIAL.begin(115200);
LCN::init(LCN_SERIAL);
#endif
LCD(1,F("Ready"));
LCD(3,F("Ready"));
CommandDistributor::broadcastPower();
}
void loop()
@@ -113,9 +124,9 @@ void loop()
DCC::loop();
// Responsibility 2: handle any incoming commands on USB connection
serialParser.loop(Serial);
SerialManager::loop();
// Responsibility 3: Optionally handle any incoming WiFi traffic
// Responsibility 3: Optionally handle any incoming WiFi traffic
#if WIFI_ON
WifiInterface::loop();
#endif
@@ -123,23 +134,25 @@ void loop()
EthernetInterface::loop();
#endif
#if defined(RMFT_ACTIVE)
RMFT::loop();
#endif
RMFT::loop(); // ignored if no automation
#if defined(LCN_SERIAL)
LCN::loop();
#if defined(LCN_SERIAL)
LCN::loop();
#endif
LCDDisplay::loop(); // ignored if LCD not in use
// Report any decrease in memory (will automatically trigger on first call)
static int ramLowWatermark = __INT_MAX__; // replaced on first loop
LCDDisplay::loop(); // ignored if LCD not in use
int freeNow = minimumFreeMemory();
if (freeNow < ramLowWatermark)
{
// Handle/update IO devices.
IODevice::loop();
Sensor::checkAll(); // Update and print changes
// Report any decrease in memory (will automatically trigger on first call)
static int ramLowWatermark = __INT_MAX__; // replaced on first loop
int freeNow = DCCTimer::getMinimumFreeMemory();
if (freeNow < ramLowWatermark) {
ramLowWatermark = freeNow;
LCD(2,F("Free RAM=%5db"), ramLowWatermark);
LCD(3,F("Free RAM=%5db"), ramLowWatermark);
}
}

661
DCC.cpp

File diff suppressed because it is too large Load Diff

133
DCC.h
View File

@@ -1,5 +1,10 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021 Mike S
* © 2021 Fred Decker
* © 2021 Herb Morton
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -23,76 +28,48 @@
#include "MotorDrivers.h"
#include "FSH.h"
typedef void (*ACK_CALLBACK)(int16_t result);
enum ackOp : byte
{ // Program opcodes for the ack Manager
BASELINE, // ensure enough resets sent before starting and obtain baseline current
W0,
W1, // issue write bit (0..1) packet
WB, // issue write byte packet
VB, // Issue validate Byte packet
V0, // Issue validate bit=0 packet
V1, // issue validate bit=1 packlet
WACK, // wait for ack (or absence of ack)
ITC1, // If True Callback(1) (if prevous WACK got an ACK)
ITC0, // If True callback(0);
ITCB, // If True callback(byte)
ITCB7, // If True callback(byte &0x7F)
NAKFAIL, // if false callback(-1)
FAIL, // callback(-1)
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
SETCV, // sets cv number to next prog byte
SETBYTE, // sets current byte to next prog byte
SETBYTEH, // sets current byte to word high byte
SETBYTEL, // sets current byte to word low byte
STASHLOCOID, // keeps current byte value for later
COMBINELOCOID, // combines current value with stashed value and returns it
ITSKIP, // skip to SKIPTARGET if ack true
SKIPTARGET = 0xFF // jump to target
};
enum CALLBACK_STATE : byte {
AFTER_WRITE, // Start callback sequence after something was written to the decoder
WAITING_100, // Waiting for 100mS of stable power
WAITING_30, // waiting to 30ms of power off gap.
READY, // Ready to complete callback
};
#include "defines.h"
#ifndef HIGHEST_SHORT_ADDR
#define HIGHEST_SHORT_ADDR 127
#else
#if HIGHEST_SHORT_ADDR > 127
#error short addr greater than 127 does not make sense
#endif
#endif
#include "DCCACK.h"
const uint16_t LONG_ADDR_MARKER = 0x4000;
// Allocations with memory implications..!
// Base system takes approx 900 bytes + 8 per loco. Turnouts, Sensors etc are dynamically created
#ifdef ARDUINO_AVR_UNO
const byte MAX_LOCOS = 20;
#else
#if defined(HAS_ENOUGH_MEMORY)
const byte MAX_LOCOS = 50;
#else
const byte MAX_LOCOS = 30;
#endif
class DCC
{
public:
static void begin(const FSH * motorShieldName, MotorDriver *mainDriver, MotorDriver *progDriver);
static void setJoinRelayPin(byte joinRelayPin);
static void begin(const FSH * motorShieldName);
static void loop();
// Public DCC API functions
static void setThrottle(uint16_t cab, uint8_t tSpeed, bool tDirection);
static uint8_t getThrottleSpeed(int cab);
static int8_t getThrottleSpeed(int cab);
static uint8_t getThrottleSpeedByte(int cab);
static bool getThrottleDirection(int cab);
static void writeCVByteMain(int cab, int cv, byte bValue);
static void writeCVBitMain(int cab, int cv, byte bNum, bool bValue);
static void setFunction(int cab, byte fByte, byte eByte);
static void setFn(int cab, int16_t functionNumber, bool on);
static int changeFn(int cab, int16_t functionNumber, bool pressed);
static void changeFn(int cab, int16_t functionNumber);
static int getFn(int cab, int16_t functionNumber);
static uint32_t getFunctionMap(int cab);
static void updateGroupflags(byte &flags, int16_t functionNumber);
static void setAccessory(int aAdd, byte aNum, bool activate);
static void setAccessory(int address, byte port, bool gate, byte onoff = 2);
static bool writeTextPacket(byte *b, int nBytes);
static void setProgTrackSyncMain(bool on); // when true, prog track becomes driveable
static void setProgTrackBoost(bool on); // when true, special prog track current limit does not apply
// ACKable progtrack calls bitresults callback 0,0 or -1, cv returns value or -1
static void readCV(int16_t cv, ACK_CALLBACK callback);
static void readCVBit(int16_t cv, byte bitNum, ACK_CALLBACK callback); // -1 for error
@@ -108,13 +85,11 @@ public:
static void forgetLoco(int cab); // removes any speed reminders for this loco
static void forgetAllLocos(); // removes all speed reminders
static void displayCabList(Print *stream);
static FSH *getMotorShieldName();
static inline void setGlobalSpeedsteps(byte s) {
globalSpeedsteps = s;
};
private:
struct LOCO
{
int loco;
@@ -122,7 +97,12 @@ private:
byte groupFlags;
unsigned long functions;
};
static byte joinRelay;
static LOCO speedTable[MAX_LOCOS];
static int lookupSpeedTable(int locoId, bool autoCreate=true);
static byte cv1(byte opcode, int cv);
static byte cv2(int cv);
private:
static byte loopStatus;
static void setThrottle2(uint16_t cab, uint8_t speedCode);
static void updateLocoReminder(int loco, byte speedCode);
@@ -132,29 +112,9 @@ private:
static FSH *shieldName;
static byte globalSpeedsteps;
static LOCO speedTable[MAX_LOCOS];
static byte cv1(byte opcode, int cv);
static byte cv2(int cv);
static int lookupSpeedTable(int locoId);
static void issueReminders();
static void callback(int value);
// ACK MANAGER
static ackOp const *ackManagerProg;
static byte ackManagerByte;
static byte ackManagerBitNum;
static int ackManagerCv;
static int ackManagerWord;
static byte ackManagerStash;
static bool ackReceived;
static bool ackManagerRejoin;
static ACK_CALLBACK ackManagerCallback;
static CALLBACK_STATE callbackState;
static void ackManagerSetup(int cv, byte bitNumOrbyteValue, ackOp const program[], ACK_CALLBACK callback);
static void ackManagerSetup(int wordval, ackOp const program[], ACK_CALLBACK callback);
static void ackManagerLoop();
static bool checkResets( uint8_t numResets);
static const int PROG_REPEATS = 8; // repeats of programming commands (some decoders need at least 8 to be reliable)
// NMRA codes #
static const byte SET_SPEED = 0x3f;
@@ -169,31 +129,4 @@ private:
static const byte BIT_OFF = 0x00;
};
#ifdef ARDUINO_AVR_MEGA // is using Mega 1280, define as Mega 2560 (pinouts and functionality are identical)
#define ARDUINO_AVR_MEGA2560
#endif
#if defined(ARDUINO_AVR_UNO)
#define ARDUINO_TYPE "UNO"
#elif defined(ARDUINO_AVR_NANO)
#define ARDUINO_TYPE "NANO"
#elif defined(ARDUINO_AVR_MEGA2560)
#define ARDUINO_TYPE "MEGA"
#elif defined(ARDUINO_ARCH_MEGAAVR)
#define ARDUINO_TYPE "MEGAAVR"
#elif defined(ARDUINO_TEENSY32)
#define ARDUINO_TYPE "TEENSY32"
#elif defined(ARDUINO_TEENSY35)
#define ARDUINO_TYPE "TEENSY35"
#elif defined(ARDUINO_TEENSY36)
#define ARDUINO_TYPE "TEENSY36"
#elif defined(ARDUINO_TEENSY40)
#define ARDUINO_TYPE "TEENSY40"
#elif defined(ARDUINO_TEENSY41)
#define ARDUINO_TYPE "TEENSY41"
#else
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH AN ARDUINO UNO, NANO 328, OR ARDUINO MEGA 1280/2560
#endif
#endif

468
DCCACK.cpp Normal file
View File

@@ -0,0 +1,468 @@
/*
* © 2021 M Steve Todd
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2022 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 "DCCACK.h"
#include "DIAG.h"
#include "DCC.h"
#include "DCCWaveform.h"
#include "TrackManager.h"
unsigned int DCCACK::minAckPulseDuration = 2000; // micros
unsigned int DCCACK::maxAckPulseDuration = 20000; // micros
MotorDriver * DCCACK::progDriver=NULL;
ackOp const * DCCACK::ackManagerProg;
ackOp const * DCCACK::ackManagerProgStart;
byte DCCACK::ackManagerByte;
byte DCCACK::ackManagerByteVerify;
byte DCCACK::ackManagerStash;
int DCCACK::ackManagerWord;
byte DCCACK::ackManagerRetry;
byte DCCACK::ackRetry = 2;
int16_t DCCACK::ackRetrySum;
int16_t DCCACK::ackRetryPSum;
int DCCACK::ackManagerCv;
byte DCCACK::ackManagerBitNum;
bool DCCACK::ackReceived;
bool DCCACK::ackManagerRejoin;
volatile uint8_t DCCACK::numAckGaps=0;
volatile uint8_t DCCACK::numAckSamples=0;
uint8_t DCCACK::trailingEdgeCounter=0;
unsigned int DCCACK::ackPulseDuration; // micros
unsigned long DCCACK::ackPulseStart; // micros
volatile bool DCCACK::ackDetected;
unsigned long DCCACK::ackCheckStart; // millis
volatile bool DCCACK::ackPending;
bool DCCACK::autoPowerOff;
int DCCACK::ackThreshold;
int DCCACK::ackLimitmA = 50;
int DCCACK::ackMaxCurrent;
unsigned int DCCACK::ackCheckDuration; // millis
CALLBACK_STATE DCCACK::callbackState=READY;
ACK_CALLBACK DCCACK::ackManagerCallback;
void DCCACK::Setup(int cv, byte byteValueOrBitnum, ackOp const program[], ACK_CALLBACK callback) {
progDriver=TrackManager::getProgDriver();
if (progDriver==NULL) {
callback(-3); // we dont have a prog track!
return;
}
if (!progDriver->canMeasureCurrent()) {
callback(-2); // our prog track cant measure current
return;
}
progDriver->setResetCounterPointer(&(DCCWaveform::progTrack.sentResetsSincePacket));
ackManagerRejoin=TrackManager::isJoined();
if (ackManagerRejoin ) {
// Change from JOIN must zero resets packet.
TrackManager::setJoin(false);
DCCWaveform::progTrack.sentResetsSincePacket = 0;
}
autoPowerOff=false;
if (progDriver->getPower() == POWERMODE::OFF) {
autoPowerOff=true; // power off afterwards
if (Diag::ACK) DIAG(F("Auto Prog power on"));
progDriver->setPower(POWERMODE::ON);
/* TODO !!! in MotorDriver surely!
if (MotorDriver::commonFaultPin)
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
DCCWaveform::progTrack.sentResetsSincePacket = 0;
**/
}
ackManagerCv = cv;
ackManagerProg = program;
ackManagerProgStart = program;
ackManagerRetry = ackRetry;
ackManagerByte = byteValueOrBitnum;
ackManagerByteVerify = byteValueOrBitnum;
ackManagerBitNum=byteValueOrBitnum;
ackManagerCallback = callback;
}
void DCCACK::Setup(int wordval, ackOp const program[], ACK_CALLBACK callback) {
ackManagerWord=wordval;
Setup(0, 0, program, callback);
}
const byte RESET_MIN=8; // tuning of reset counter before sending message
// checkRessets return true if the caller should yield back to loop and try later.
bool DCCACK::checkResets(uint8_t numResets) {
return DCCWaveform::progTrack.sentResetsSincePacket < numResets;
}
// Operations applicable to PROG track ONLY.
// (yes I know I could have subclassed the main track but...)
void DCCACK::setAckBaseline() {
int baseline=progDriver->getCurrentRaw();
ackThreshold= baseline + progDriver->mA2raw(ackLimitmA);
if (Diag::ACK) DIAG(F("ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %uus and %uus"),
baseline,progDriver->raw2mA(baseline),
ackThreshold,progDriver->raw2mA(ackThreshold),
minAckPulseDuration, maxAckPulseDuration);
}
void DCCACK::setAckPending() {
ackMaxCurrent=0;
ackPulseStart=0;
ackPulseDuration=0;
ackDetected=false;
ackCheckStart=millis();
numAckSamples=0;
numAckGaps=0;
ackPending=true; // interrupt routines will now take note
}
byte DCCACK::getAck() {
if (ackPending) return (2); // still waiting
if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%uuS samples=%d gaps=%d"),ackDetected?F("ACK"):F("NO-ACK"), ackCheckDuration,
ackMaxCurrent,progDriver->raw2mA(ackMaxCurrent), ackPulseDuration, numAckSamples, numAckGaps);
if (ackDetected) return (1); // Yes we had an ack
return(0); // pending set off but not detected means no ACK.
}
void DCCACK::loop() {
while (ackManagerProg) {
byte opcode=GETFLASH(ackManagerProg);
// breaks from this switch will step to next prog entry
// returns from this switch will stay on same entry
// (typically waiting for a reset counter or ACK waiting, or when all finished.)
switch (opcode) {
case BASELINE:
if (progDriver->getPower()==POWERMODE::OVERLOAD) return;
if (checkResets(autoPowerOff || ackManagerRejoin ? 20 : 3)) return;
setAckBaseline();
callbackState=AFTER_READ;
break;
case W0: // write 0 bit
case W1: // write 1 bit
{
if (checkResets(RESET_MIN)) return;
if (Diag::ACK) DIAG(F("W%d cv=%d bit=%d"),opcode==W1, ackManagerCv,ackManagerBitNum);
byte instruction = WRITE_BIT | (opcode==W1 ? BIT_ON : BIT_OFF) | ackManagerBitNum;
byte message[] = {DCC::cv1(BIT_MANIPULATE, ackManagerCv), DCC::cv2(ackManagerCv), instruction };
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
setAckPending();
callbackState=AFTER_WRITE;
}
break;
case WB: // write byte
{
if (checkResets( RESET_MIN)) return;
if (Diag::ACK) DIAG(F("WB cv=%d value=%d"),ackManagerCv,ackManagerByte);
byte message[] = {DCC::cv1(WRITE_BYTE, ackManagerCv), DCC::cv2(ackManagerCv), ackManagerByte};
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
setAckPending();
callbackState=AFTER_WRITE;
}
break;
case VB: // Issue validate Byte packet
{
if (checkResets( RESET_MIN)) return;
if (Diag::ACK) DIAG(F("VB cv=%d value=%d"),ackManagerCv,ackManagerByte);
byte message[] = { DCC::cv1(VERIFY_BYTE, ackManagerCv), DCC::cv2(ackManagerCv), ackManagerByte};
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
setAckPending();
}
break;
case V0:
case V1: // Issue validate bit=0 or bit=1 packet
{
if (checkResets(RESET_MIN)) return;
if (Diag::ACK) DIAG(F("V%d cv=%d bit=%d"),opcode==V1, ackManagerCv,ackManagerBitNum);
byte instruction = VERIFY_BIT | (opcode==V0?BIT_OFF:BIT_ON) | ackManagerBitNum;
byte message[] = {DCC::cv1(BIT_MANIPULATE, ackManagerCv), DCC::cv2(ackManagerCv), instruction };
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
setAckPending();
}
break;
case WACK: // wait for ack (or absence of ack)
{
byte ackState=2; // keep polling
ackState=getAck();
if (ackState==2) return; // keep polling
ackReceived=ackState==1;
break; // we have a genuine ACK result
}
case ITC0:
case ITC1: // If True Callback(0 or 1) (if prevous WACK got an ACK)
if (ackReceived) {
callback(opcode==ITC0?0:1);
return;
}
break;
case ITCB: // If True callback(byte)
if (ackReceived) {
callback(ackManagerByte);
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);
return;
}
break;
case NAKFAIL: // If nack callback(-1)
if (!ackReceived) {
callback(-1);
return;
}
break;
case CALLFAIL: // callback(-1)
callback(-1);
return;
case BIV: // ackManagerByte initial value
ackManagerByte = ackManagerByteVerify;
break;
case STARTMERGE:
ackManagerBitNum=7;
ackManagerByte=0;
break;
case MERGE: // Merge previous Validate zero wack response with byte value and update bit number (use for reading CV bytes)
ackManagerByte <<= 1;
// ackReceived means bit is zero.
if (!ackReceived) ackManagerByte |= 1;
ackManagerBitNum--;
break;
case SETBIT:
ackManagerProg++;
ackManagerBitNum=GETFLASH(ackManagerProg);
break;
case SETCV:
ackManagerProg++;
ackManagerCv=GETFLASH(ackManagerProg);
break;
case SETBYTE:
ackManagerProg++;
ackManagerByte=GETFLASH(ackManagerProg);
break;
case SETBYTEH:
ackManagerByte=highByte(ackManagerWord);
break;
case SETBYTEL:
ackManagerByte=lowByte(ackManagerWord);
break;
case STASHLOCOID:
ackManagerStash=ackManagerByte; // stash value from CV17
break;
case COMBINELOCOID:
// ackManagerStash is cv17, ackManagerByte is CV 18
callback( LONG_ADDR_MARKER | ( ackManagerByte + ((ackManagerStash - 192) << 8)));
return;
case ITSKIP:
if (!ackReceived) break;
// SKIP opcodes until SKIPTARGET found
while (opcode!=SKIPTARGET) {
ackManagerProg++;
opcode=GETFLASH(ackManagerProg);
}
break;
case SKIPTARGET:
break;
default:
DIAG(F("!! ackOp %d FAULT!!"),opcode);
callback( -1);
return;
} // end of switch
ackManagerProg++;
}
}
void DCCACK::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
// Rule 2: If we are re-joining the main track we must power off for 30mS
switch (callbackState) {
case AFTER_READ:
if (ackManagerRejoin && autoPowerOff) {
progDriver->setPower(POWERMODE::OFF);
callbackStart=millis();
callbackState=WAITING_30;
if (Diag::ACK) DIAG(F("OFF 30mS"));
} else {
callbackState=READY;
}
break;
case AFTER_WRITE: // first attempt to callback after a write operation
if (!ackManagerRejoin && !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"));
break;
case WAITING_100: // waiting for 100mS
if (millis()-callbackStart < 100) break;
// stable after power maintained for 100mS
// If we are going to power off anyway, it doesnt matter
// but if we will keep the power on, we must off it for 30mS
if (autoPowerOff) callbackState=READY;
else { // Need to cycle power off and on
progDriver->setPower(POWERMODE::OFF);
callbackStart=millis();
callbackState=WAITING_30;
if (Diag::ACK) DIAG(F("OFF 30mS"));
}
break;
case WAITING_30: // waiting for 30mS with power off
if (millis()-callbackStart < 30) break;
//power has been off for 30mS
progDriver->setPower(POWERMODE::ON);
callbackState=READY;
break;
case READY: // ready after read, or write after power delay and off period.
// power off if we powered it on
if (autoPowerOff) {
if (Diag::ACK) DIAG(F("Auto Prog power off"));
progDriver->setPower(POWERMODE::OFF);
/* TODO
if (MotorDriver::commonFaultPin)
DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
**/
}
// Restore <1 JOIN> to state before BASELINE
if (ackManagerRejoin) {
TrackManager::setJoin(true);
if (Diag::ACK) DIAG(F("Auto JOIN"));
}
ackManagerProg=NULL; // no more steps to execute
if (Diag::ACK) DIAG(F("Callback(%d)"),value);
(ackManagerCallback)( value);
}
}
void DCCACK::checkAck(byte sentResetsSincePacket) {
if (!ackPending) return;
// This function operates in interrupt() time so must be fast and can't DIAG
if (sentResetsSincePacket > 6) { //ACK timeout
ackCheckDuration=millis()-ackCheckStart;
ackPending = false;
return;
}
int current=progDriver->getCurrentRawInInterrupt();
numAckSamples++;
if (current > ackMaxCurrent) ackMaxCurrent=current;
// An ACK is a pulse lasting between minAckPulseDuration and maxAckPulseDuration uSecs (refer @haba)
if (current>ackThreshold) {
if (trailingEdgeCounter > 0) {
numAckGaps++;
trailingEdgeCounter = 0;
}
if (ackPulseStart==0) ackPulseStart=micros(); // leading edge of pulse detected
return;
}
// not in pulse
if (ackPulseStart==0) return; // keep waiting for leading edge
// if we reach to this point, we have
// detected trailing edge of pulse
if (trailingEdgeCounter == 0) {
ackPulseDuration=micros()-ackPulseStart;
}
// but we do not trust it yet and return (which will force another
// measurement) and first the third time around with low current
// the ack detection will be finalized.
if (trailingEdgeCounter < 2) {
trailingEdgeCounter++;
return;
}
trailingEdgeCounter = 0;
if (ackPulseDuration>=minAckPulseDuration && ackPulseDuration<=maxAckPulseDuration) {
ackCheckDuration=millis()-ackCheckStart;
ackDetected=true;
ackPending=false;
DCCWaveform::progTrack.clearRepeats(); // shortcut remaining repeat packets
return; // we have a genuine ACK result
}
ackPulseStart=0; // We have detected a too-short or too-long pulse so ignore and wait for next leading edge
}

156
DCCACK.h Normal file
View File

@@ -0,0 +1,156 @@
/*
* © 2021 M Steve Todd
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2022 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 DCCACK_h
#define DCCACK_h
#include "MotorDriver.h"
typedef void (*ACK_CALLBACK)(int16_t result);
enum ackOp : byte
{ // Program opcodes for the ack Manager
BASELINE, // ensure enough resets sent before starting and obtain baseline current
W0,
W1, // issue write bit (0..1) packet
WB, // issue write byte packet
VB, // Issue validate Byte packet
V0, // Issue validate bit=0 packet
V1, // issue validate bit=1 packlet
WACK, // wait for ack (or absence of ack)
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)
CALLFAIL, // 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
SETCV, // sets cv number to next prog byte
SETBYTE, // sets current byte to next prog byte
SETBYTEH, // sets current byte to word high byte
SETBYTEL, // sets current byte to word low byte
STASHLOCOID, // keeps current byte value for later
COMBINELOCOID, // combines current value with stashed value and returns it
ITSKIP, // skip to SKIPTARGET if ack true
SKIPTARGET = 0xFF // jump to target
};
enum CALLBACK_STATE : byte {
AFTER_READ, // Start callback sequence after something was read from the decoder
AFTER_WRITE, // Start callback sequence after something was written to the decoder
WAITING_100, // Waiting for 100mS of stable power
WAITING_30, // waiting to 30ms of power off gap.
READY, // Ready to complete callback
};
class DCCACK {
public:
static byte getAck(); //prog track only 0=NACK, 1=ACK 2=keep waiting
static void checkAck(byte sentResetsSincePacket); // Interrupt time ack checker
static inline void setAckLimit(int mA) {
ackLimitmA = mA;
}
static inline void setMinAckPulseDuration(unsigned int i) {
minAckPulseDuration = i;
}
static inline void setMaxAckPulseDuration(unsigned int i) {
maxAckPulseDuration = i;
}
static void Setup(int cv, byte byteValueOrBitnum, ackOp const program[], ACK_CALLBACK callback);
static void Setup(int wordval, ackOp const program[], ACK_CALLBACK callback);
static void loop();
static bool isActive() { return ackManagerProg!=NULL;}
static inline int16_t setAckRetry(byte retry) {
ackRetry = retry;
ackRetryPSum = ackRetrySum;
ackRetrySum = 0; // reset running total
return ackRetryPSum;
};
private:
static const byte SET_SPEED = 0x3f;
static const byte WRITE_BYTE = 0x7C;
static const byte VERIFY_BYTE = 0x74;
static const byte BIT_MANIPULATE = 0x78;
static const byte WRITE_BIT = 0xF0;
static const byte VERIFY_BIT = 0xE0;
static const byte BIT_ON = 0x08;
static const byte BIT_OFF = 0x00;
static void setAckBaseline();
static void setAckPending();
static void callback(int value);
static const int PROG_REPEATS = 8; // repeats of programming commands (some decoders need at least 8 to be reliable)
// ACK management (Prog track only)
static void checkAck();
static bool checkResets(uint8_t numResets);
static volatile bool ackPending;
static volatile bool ackDetected;
static int ackThreshold;
static int ackLimitmA;
static int ackMaxCurrent;
static unsigned long ackCheckStart; // millis
static unsigned int ackCheckDuration; // millis
static unsigned int ackPulseDuration; // micros
static unsigned long ackPulseStart; // micros
static unsigned int minAckPulseDuration ; // micros
static unsigned int maxAckPulseDuration ; // micros
static MotorDriver* progDriver;
static volatile uint8_t numAckGaps;
static volatile uint8_t numAckSamples;
static uint8_t trailingEdgeCounter;
static ackOp const * ackManagerProg;
static ackOp const * ackManagerProgStart;
static byte ackManagerByte;
static byte ackManagerByteVerify;
static byte ackManagerStash;
static int ackManagerWord;
static byte ackManagerRetry;
static byte ackRetry;
static int16_t ackRetrySum;
static int16_t ackRetryPSum;
static int ackManagerCv;
static byte ackManagerBitNum;
static bool ackReceived;
static bool ackManagerRejoin;
static bool autoPowerOff;
static CALLBACK_STATE callbackState;
static ACK_CALLBACK ackManagerCallback;
};
#endif

22
DCCEX.h
View File

@@ -1,7 +1,8 @@
/*
* (c) 2020 Chris Harlow. All rights reserved.
* (c) 2021 Fred Decker. All rights reserved.
* (c) 2020 Harald Barth. All rights reserved.
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
@@ -29,6 +30,7 @@
#include "DCC.h"
#include "DIAG.h"
#include "DCCEXParser.h"
#include "SerialManager.h"
#include "version.h"
#include "WifiInterface.h"
#if ETHERNET_ON == true
@@ -36,11 +38,13 @@
#endif
#include "LCD_Implementation.h"
#include "LCN.h"
#include "freeMemory.h"
#if __has_include ( "myAutomation.h")
#include "RMFT.h"
#define RMFT_ACTIVE
#endif
#include "IODevice.h"
#include "Turnouts.h"
#include "Sensors.h"
#include "Outputs.h"
#include "CommandDistributor.h"
#include "TrackManager.h"
#include "DCCTimer.h"
#include "EXRAIL.h"
#endif

View File

@@ -1,6 +1,12 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth.
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021 Herb Morton
* © 2020-2022 Harald Barth
* © 2020-2021 M Steve Todd
* © 2020-2021 Fred Decker
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
@@ -24,13 +30,19 @@
#include "Turnouts.h"
#include "Outputs.h"
#include "Sensors.h"
#include "freeMemory.h"
#include "GITHUB_SHA.h"
#include "version.h"
#include "defines.h"
#include "CommandDistributor.h"
#include "EEStore.h"
#include "DIAG.h"
#include "TrackManager.h"
#include "DCCTimer.h"
#include "EXRAIL2.h"
#ifdef HAS_AVR_WDT
#include <avr/wdt.h>
#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
@@ -40,76 +52,50 @@ 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_A='A';
const int16_t HASH_KEYWORD_C='C';
const int16_t HASH_KEYWORD_R='R';
const int16_t HASH_KEYWORD_T='T';
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;
const int16_t HASH_KEYWORD_WIFI = -5583;
const int16_t HASH_KEYWORD_ETHERNET = -30767;
const int16_t HASH_KEYWORD_WIT = 31594;
int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS];
bool DCCEXParser::stashBusy;
Print *DCCEXParser::stashStream = NULL;
RingStream *DCCEXParser::stashRingStream = NULL;
byte DCCEXParser::stashTarget=0;
// This is a JMRI command parser, one instance per incoming stream
// This is a JMRI command parser.
// It doesnt know how the string got here, nor how it gets back.
// It knows nothing about hardware or tracks... it just parses strings and
// calls the corresponding DCC api.
// Non-DCC things like turnouts, pins and sensors are handled in additional JMRI interface classes.
DCCEXParser::DCCEXParser() {}
void DCCEXParser::flush()
{
if (Diag::CMD)
DIAG(F("Buffer flush"));
bufferLength = 0;
inCommandPayload = false;
}
void DCCEXParser::loop(Stream &stream)
{
while (stream.available())
{
if (bufferLength == MAX_BUFFER)
{
flush();
}
char ch = stream.read();
if (ch == '<')
{
inCommandPayload = true;
bufferLength = 0;
buffer[0] = '\0';
}
else if (ch == '>')
{
buffer[bufferLength] = '\0';
parse(&stream, buffer, NULL); // Parse this (No ringStream for serial)
inCommandPayload = false;
break;
}
else if (inCommandPayload)
{
buffer[bufferLength++] = ch;
}
}
Sensor::checkAll(&stream); // Update and print changes
}
int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte *cmd)
int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte *cmd, bool usehex)
{
byte state = 1;
byte parameterCount = 0;
@@ -147,11 +133,16 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
case 3: // building a parameter
if (hot >= '0' && hot <= '9')
{
runningValue = 10 * runningValue + (hot - '0');
runningValue = (usehex?16:10) * runningValue + (hot - '0');
break;
}
if (hot >= 'a' && hot <= 'z') hot=hot-'a'+'A'; // uppercase a..z
if (hot >= 'A' && hot <= 'Z')
if (usehex && hot>='A' && hot<='F') {
// treat A..F as hex not keyword
runningValue = 16 * runningValue + (hot - 'A' + 10);
break;
}
if (hot=='_' || (hot >= 'A' && hot <= 'Z'))
{
// Since JMRI got modified to send keywords in some rare cases, we need this
// Super Kluge to turn keywords into a hash value that can be recognised later
@@ -168,69 +159,12 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
return parameterCount;
}
int16_t DCCEXParser::splitHexValues(int16_t result[MAX_COMMAND_PARAMS], const byte *cmd)
{
byte state = 1;
byte parameterCount = 0;
int16_t runningValue = 0;
const byte *remainingCmd = cmd + 1; // skips the opcode
// clear all parameters in case not enough found
for (int16_t i = 0; i < MAX_COMMAND_PARAMS; i++)
result[i] = 0;
while (parameterCount < MAX_COMMAND_PARAMS)
{
byte hot = *remainingCmd;
switch (state)
{
case 1: // skipping spaces before a param
if (hot == ' ')
break;
if (hot == '\0' || hot == '>')
return parameterCount;
state = 2;
continue;
case 2: // checking first hex digit
runningValue = 0;
state = 3;
continue;
case 3: // building a parameter
if (hot >= '0' && hot <= '9')
{
runningValue = 16 * runningValue + (hot - '0');
break;
}
if (hot >= 'A' && hot <= 'F')
{
runningValue = 16 * runningValue + 10 + (hot - 'A');
break;
}
if (hot >= 'a' && hot <= 'f')
{
runningValue = 16 * runningValue + 10 + (hot - 'a');
break;
}
if (hot==' ' || hot=='>' || hot=='\0') {
result[parameterCount] = runningValue;
parameterCount++;
state = 1;
continue;
}
return -1; // invalid hex digit
}
remainingCmd++;
}
return parameterCount;
}
FILTER_CALLBACK DCCEXParser::filterCallback = 0;
extern __attribute__((weak)) void myFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
FILTER_CALLBACK DCCEXParser::filterCallback = myFilter;
FILTER_CALLBACK DCCEXParser::filterRMFTCallback = 0;
AT_COMMAND_CALLBACK DCCEXParser::atCommandCallback = 0;
// deprecated
void DCCEXParser::setFilter(FILTER_CALLBACK filter)
{
filterCallback = filter;
@@ -246,6 +180,7 @@ void DCCEXParser::setAtCommandCallback(AT_COMMAND_CALLBACK callback)
// Parse an F() string
void DCCEXParser::parse(const FSH * cmd) {
DIAG(F("SETUP(\"%S\")"),cmd);
int size=strlen_P((char *)cmd)+1;
char buffer[size];
strcpy_P(buffer,(char *)cmd);
@@ -253,17 +188,34 @@ 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)
void DCCEXParser::parse(Print *stream, byte *com, RingStream *ringStream) {
// This function can get stings of the form "<C OMM AND>" or "C OMM AND"
// found is true first after the leading "<" has been passed
bool found = (com[0] != '<');
for (byte *c=com; c[0] != '\0'; c++) {
if (found) {
parseOne(stream, c, ringStream);
found=false;
}
if (c[0] == '<')
found = true;
}
}
void DCCEXParser::parseOne(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];
while (com[0] == '<' || com[0] == ' ')
com++; // strip off any number of < or spaces
byte params = splitValues(p, com);
byte opcode = com[0];
byte params = splitValues(p, com, opcode=='M' || opcode=='P');
if (filterCallback)
filterCallback(stream, opcode, params, p);
if (filterRMFTCallback && opcode!='\0')
@@ -276,10 +228,23 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
return; // filterCallback asked us to ignore
case 't': // THROTTLE <t [REGISTER] CAB SPEED DIRECTION>
{
if (params==1) { // <t cab> display state
int16_t slot=DCC::lookupSpeedTable(p[0],false);
if (slot>=0) {
DCC::LOCO * sp=&DCC::speedTable[slot];
StringFormatter::send(stream,F("<l %d %d %d %l>\n"),
sp->loco,slot,sp->speedCode,sp->functions);
}
else // send dummy state speed 0 fwd no functions.
StringFormatter::send(stream,F("<l %d -1 128 0>\n"),p[0]);
return;
}
int16_t cab;
int16_t tspeed;
int16_t direction;
if (params == 4)
{ // <t REGISTER CAB SPEED DIRECTION>
cab = p[1];
@@ -311,10 +276,9 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
break; // invalid direction code
DCC::setThrottle(cab, tspeed, direction);
if (params == 4)
if (params == 4) // send obsolete format T response
StringFormatter::send(stream, F("<T %d %d %d>\n"), p[0], p[2], p[3]);
else
StringFormatter::send(stream, F("<O>\n"));
// speed change will be broadcast anyway in new <l > format
return;
}
case 'f': // FUNCTION <f CAB BYTE1 [BYTE2]>
@@ -322,30 +286,45 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
return;
break;
case 'a': // ACCESSORY <a ADDRESS SUBADDRESS ACTIVATE> or <a LINEARADDRESS ACTIVATE>
case 'a': // ACCESSORY <a ADDRESS SUBADDRESS ACTIVATE [ONOFF]> or <a LINEARADDRESS ACTIVATE>
{
int address;
byte subaddress;
byte activep;
byte onoff;
if (params==2) { // <a LINEARADDRESS ACTIVATE>
address=(p[0] - 1) / 4 + 1;
subaddress=(p[0] - 1) % 4;
activep=1;
activep=1;
onoff=2; // send both
}
else if (params==3) { // <a ADDRESS SUBADDRESS ACTIVATE>
address=p[0];
subaddress=p[1];
activep=2;
activep=2;
onoff=2; // send both
}
else if (params==4) { // <a ADDRESS SUBADDRESS ACTIVATE ONOFF>
address=p[0];
subaddress=p[1];
activep=2;
if ((p[3] < 0) || (p[3] > 1)) // invalid onoff 0|1
break;
onoff=p[3];
}
else break; // invalid no of parameters
if (
((address & 0x01FF) != address) // invalid address (limit 9 bits )
|| ((subaddress & 0x03) != subaddress) // invalid subaddress (limit 2 bits )
|| ((p[activep] & 0x01) != p[activep]) // invalid activate 0|1
) break;
DCC::setAccessory(address, subaddress,p[activep]==1);
((address & 0x01FF) != address) // invalid address (limit 9 bits)
|| ((subaddress & 0x03) != subaddress) // invalid subaddress (limit 2 bits)
|| (p[activep] > 1) || (p[activep] < 0) // invalid activate 0|1
) break;
// Honour the configuration option (config.h) which allows the <a> command to be reversed
#ifdef DCC_ACCESSORY_COMMAND_REVERSE
DCC::setAccessory(address, subaddress,p[activep]==0,onoff);
#else
DCC::setAccessory(address, subaddress,p[activep]==1,onoff);
#endif
}
return;
@@ -374,8 +353,8 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
case 'M': // WRITE TRANSPARENT DCC PACKET MAIN <M REG X1 ... X9>
case 'P': // WRITE TRANSPARENT DCC PACKET PROG <P REG X1 ... X9>
// Re-parse the command using a hex-only splitter
params=splitHexValues(p,com)-1; // drop REG
// NOTE: this command was parsed in HEX instead of decimal
params--; // drop REG
if (params<1) break;
{
byte packet[params];
@@ -392,7 +371,9 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
break;
if (params == 1) // <W id> Write new loco id (clearing consist and managing short/long)
DCC::setLocoId(p[0],callback_Wloco);
else // WRITE CV ON PROG <W CV VALUE [CALLBACKNUM] [CALLBACKSUB]>
else if (params == 4) // WRITE CV ON PROG <W CV VALUE [CALLBACKNUM] [CALLBACKSUB]>
DCC::writeCVByte(p[0], p[1], callback_W4);
else // WRITE CV ON PROG <W CV VALUE>
DCC::writeCVByte(p[0], p[1], callback_W);
return;
@@ -420,6 +401,13 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
return;
case 'R': // READ CV ON PROG
if (params == 1)
{ // <R CV> -- uses verify callback
if (!stashCallback(stream, p, ringStream))
break;
DCC::verifyCVByte(p[0], 0, callback_Vbyte);
return;
}
if (params == 3)
{ // <R CV CALLBACKNUM CALLBACKSUB>
if (!stashCallback(stream, p, ringStream))
@@ -436,69 +424,81 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
}
break;
case '1': // POWERON <1 [MAIN|PROG]>
case '0': // POWEROFF <0 [MAIN | PROG] >
if (params > 1)
break;
case '1': // POWERON <1 [MAIN|PROG|JOIN]>
{
POWERMODE mode = opcode == '1' ? POWERMODE::ON : POWERMODE::OFF;
DCC::setProgTrackSyncMain(false); // Only <1 JOIN> will set this on, all others set it off
if (params == 0 ||
(MotorDriver::commonFaultPin && p[0] != HASH_KEYWORD_JOIN)) // commonFaultPin prevents individual track handling
{
DCCWaveform::mainTrack.setPowerMode(mode);
DCCWaveform::progTrack.setPowerMode(mode);
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);
return;
}
switch (p[0])
{
case HASH_KEYWORD_MAIN:
DCCWaveform::mainTrack.setPowerMode(mode);
StringFormatter::send(stream, F("<p%c MAIN>\n"), opcode);
return;
case HASH_KEYWORD_PROG:
DCCWaveform::progTrack.setPowerMode(mode);
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);
return;
case HASH_KEYWORD_JOIN:
DCCWaveform::mainTrack.setPowerMode(mode);
DCCWaveform::progTrack.setPowerMode(mode);
if (mode == POWERMODE::ON)
{
DCC::setProgTrackSyncMain(true);
StringFormatter::send(stream, F("<p1 JOIN>\n"), opcode);
}
else
StringFormatter::send(stream, F("<p0>\n"));
return;
}
break;
bool main=false;
bool prog=false;
bool join=false;
if (params > 1) break;
if (params==0 || MotorDriver::commonFaultPin) { // <1> or tracks can not be handled individually
main=true;
prog=true;
}
if (params==1) {
if (p[0] == HASH_KEYWORD_JOIN) { // <1 JOIN>
main=true;
prog=true;
join=true;
}
else if (p[0]==HASH_KEYWORD_MAIN) { // <1 MAIN>
main=true;
}
else if (p[0]==HASH_KEYWORD_PROG) { // <1 PROG>
prog=true;
}
else break; // will reply <X>
}
if (main) TrackManager::setMainPower(POWERMODE::ON);
if (prog) TrackManager::setProgPower(POWERMODE::ON);
TrackManager::setJoin(join);
CommandDistributor::broadcastPower();
return;
}
case '0': // POWEROFF <0 [MAIN | PROG] >
{
bool main=false;
bool prog=false;
if (params > 1) break;
if (params==0 || MotorDriver::commonFaultPin) { // <0> or tracks can not be handled individually
main=true;
prog=true;
}
if (params==1) {
if (p[0]==HASH_KEYWORD_MAIN) { // <0 MAIN>
main=true;
}
else if (p[0]==HASH_KEYWORD_PROG) { // <0 PROG>
prog=true;
}
else break; // will reply <X>
}
if (main) TrackManager::setMainPower(POWERMODE::OFF);
if (prog) {
TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off
TrackManager::setProgPower(POWERMODE::OFF);
}
TrackManager::setJoin(false);
CommandDistributor::broadcastPower();
return;
}
case '!': // ESTOP ALL <!>
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
return;
case 'c': // SEND METER RESPONSES <c>
// <c MeterName value C/V unit min max res warn>
StringFormatter::send(stream, F("<c CurrentMAIN %d C Milli 0 %d 1 %d>\n"), DCCWaveform::mainTrack.getCurrentmA(),
DCCWaveform::mainTrack.getMaxmA(), DCCWaveform::mainTrack.getTripmA());
StringFormatter::send(stream, F("<a %d>\n"), DCCWaveform::mainTrack.get1024Current()); //'a' message deprecated, remove once JMRI 4.22 is available
return;
// No longer supported because of multiple tracks <c MeterName value C/V unit min max res warn>
break;
case 'Q': // SENSORS <Q>
Sensor::printAll(stream);
return;
case 's': // <s>
StringFormatter::send(stream, F("<p%d>\n"), DCCWaveform::mainTrack.getPowerMode() == POWERMODE::ON);
StringFormatter::send(stream, F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));
Turnout::printAll(stream); //send all Turnout states
Output::printAll(stream); //send all Output states
@@ -506,6 +506,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 +516,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;
@@ -525,6 +526,11 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
return;
return;
case '=': // <= Track manager control >
if (TrackManager::parseJ(stream, params, p))
return;
break;
case '#': // NUMBER OF LOCOSLOTS <#>
StringFormatter::send(stream, F("<# %d>\n"), MAX_LOCOS);
return;
@@ -536,19 +542,83 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
return;
case 'F': // New command to call the new Loco Function API <F cab func 1|0>
if(params!=3) break;
if (Diag::CMD)
DIAG(F("Setting loco %d F%d %S"), p[0], p[1], p[2] ? F("ON") : F("OFF"));
DCC::setFn(p[0], p[1], p[2] == 1);
return;
#if WIFI_ON
case '+': // Complex Wifi interface command (not usual parse)
if (atCommandCallback) {
DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
DCCWaveform::progTrack.setPowerMode(POWERMODE::OFF);
atCommandCallback(com);
if (atCommandCallback && !ringStream) {
TrackManager::setPower(POWERMODE::OFF);
atCommandCallback((HardwareSerial *)stream,com);
return;
}
break;
#endif
case 'J' : // throttle info access
{
if ((params<1) | (params>2)) break; // <J>
int16_t id=(params==2)?p[1]:0;
switch(p[0]) {
case HASH_KEYWORD_A: // <JA> returns automations/routes
StringFormatter::send(stream, F("<jA"));
if (params==1) {// <JA>
#ifdef EXRAIL_ACTIVE
sendFlashList(stream,RMFT2::routeIdList);
sendFlashList(stream,RMFT2::automationIdList);
#endif
}
else { // <JA id>
StringFormatter::send(stream,F(" %d %c \"%S\""),
id,
#ifdef EXRAIL_ACTIVE
RMFT2::getRouteType(id), // A/R
RMFT2::getRouteDescription(id)
#else
'X',F("")
#endif
);
}
StringFormatter::send(stream, F(">\n"));
return;
case HASH_KEYWORD_R: // <JR> returns rosters
StringFormatter::send(stream, F("<jR"));
#ifdef EXRAIL_ACTIVE
if (params==1) sendFlashList(stream,RMFT2::rosterIdList);
else StringFormatter::send(stream,F(" %d \"%S\" \"%S\""),
id, RMFT2::getRosterName(id), RMFT2::getRosterFunctions(id));
#endif
StringFormatter::send(stream, F(">\n"));
return;
case HASH_KEYWORD_T: // <JT> returns turnout list
StringFormatter::send(stream, F("<jT"));
if (params==1) { // <JT>
for ( Turnout * t=Turnout::first(); t; t=t->next()) {
if (t->isHidden()) continue;
StringFormatter::send(stream, F(" %d"),t->getId());
}
}
else { // <JT id>
Turnout * t=Turnout::get(id);
if (!t || t->isHidden()) StringFormatter::send(stream, F(" %d X"),id);
else StringFormatter::send(stream, F(" %d %c \"%S\""),
id,t->isThrown()?'T':'C',
#ifdef EXRAIL_ACTIVE
RMFT2::getTurnoutDescription(id)
#else
F("")
#endif
);
}
StringFormatter::send(stream, F(">\n"));
return;
default: break;
} // switch(p[1])
break; // case J
}
default: //anything else will diagnose and drop out to <X>
DIAG(F("Opcode=%c params=%d"), opcode, params);
@@ -562,6 +632,14 @@ void DCCEXParser::parse(Print *stream, byte *com, RingStream * ringStream)
StringFormatter::send(stream, F("<X>\n"));
}
void DCCEXParser::sendFlashList(Print * stream,const int16_t flashList[]) {
for (int16_t i=0;;i++) {
int16_t value=GETFLASHW(flashList+i);
if (value==0) return;
StringFormatter::send(stream,F(" %d"),value);
}
}
bool DCCEXParser::parseZ(Print *stream, int16_t params, int16_t p[])
{
@@ -579,10 +657,8 @@ bool DCCEXParser::parseZ(Print *stream, int16_t params, int16_t p[])
return true;
case 3: // <Z ID PIN IFLAG>
if (p[0] < 0 ||
p[1] > 255 || p[1] <= 1 || // Pins 0 and 1 are Serial to USB
p[2] < 0 || p[2] > 7 )
return false;
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"));
@@ -600,7 +676,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;
}
@@ -659,11 +735,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
}
@@ -674,24 +749,60 @@ 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;
default:
return false; // will <x>
return true;
}
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;
}
}
@@ -713,13 +824,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;
@@ -739,20 +850,23 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
return true;
case HASH_KEYWORD_RAM: // <D RAM>
StringFormatter::send(stream, F("Free memory=%d\n"), minimumFreeMemory());
StringFormatter::send(stream, F("Free memory=%d\n"), DCCTimer::getMinimumFreeMemory());
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]);
DCCACK::setAckLimit(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]);
DCCACK::setMinAckPulseDuration(p[2]);
LCD(0, F("Ack Min=%uus"), 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]);
DCCACK::setMaxAckPulseDuration(p[2]);
LCD(0, F("Ack Max=%uus"), 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], DCCACK::setAckRetry(p[2])); // <D ACK RETRY 2>
}
} else {
StringFormatter::send(stream, F("Ack diag %S\n"), onOff ? F("on") : F("off"));
@@ -764,37 +878,45 @@ 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);
return true;
TrackManager::progTrackBoosted=true;
return true;
case HASH_KEYWORD_RESET:
{
#ifdef HAS_AVR_WDT
wdt_enable( WDTO_15MS); // set Arduino watchdog timer for 15ms
delay(50); // wait for the prescaller time to expire
delay(50); // wait for the prescaller time to expire
#else
ESP.restart();
#endif
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);
@@ -806,6 +928,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;
}
@@ -841,7 +979,14 @@ void DCCEXParser::commitAsyncReplyStream() {
void DCCEXParser::callback_W(int16_t result)
{
StringFormatter::send(getAsyncReplyStream(),
F("<r%d|%d|%d %d>\n"), stashP[2], stashP[3], stashP[0], result == 1 ? stashP[1] : -1);
F("<r %d %d>\n"), stashP[0], result == 1 ? stashP[1] : -1);
commitAsyncReplyStream();
}
void DCCEXParser::callback_W4(int16_t result)
{
StringFormatter::send(getAsyncReplyStream(),
F("<r%d|%d|%d %d>\n"), stashP[2], stashP[3], stashP[0], result == 1 ? stashP[1] : -1);
commitAsyncReplyStream();
}
@@ -868,10 +1013,21 @@ void DCCEXParser::callback_R(int16_t result)
commitAsyncReplyStream();
}
void DCCEXParser::callback_Rloco(int16_t result)
{
StringFormatter::send(getAsyncReplyStream(), F("<r %d>\n"), result);
commitAsyncReplyStream();
void DCCEXParser::callback_Rloco(int16_t result) {
const FSH * detail;
if (result<=0) {
detail=F("<r %d>\n");
} else {
bool longAddr=result & LONG_ADDR_MARKER; //long addr
if (longAddr)
result = result^LONG_ADDR_MARKER;
if (longAddr && result <= HIGHEST_SHORT_ADDR)
detail=F("<r LONG %d UNSUPPORTED>\n");
else
detail=F("<r %d>\n");
}
StringFormatter::send(getAsyncReplyStream(), detail, result);
commitAsyncReplyStream();
}
void DCCEXParser::callback_Wloco(int16_t result)

View File

@@ -1,5 +1,8 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -23,15 +26,14 @@
#include "RingStream.h"
typedef void (*FILTER_CALLBACK)(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
typedef void (*AT_COMMAND_CALLBACK)(const byte * command);
typedef void (*AT_COMMAND_CALLBACK)(HardwareSerial * stream,const byte * command);
struct DCCEXParser
{
DCCEXParser();
void loop(Stream & stream);
void parse(Print * stream, byte * command, RingStream * ringStream);
void parse(const FSH * cmd);
void flush();
static void parse(Print * stream, byte * command, RingStream * ringStream);
static void parse(const FSH * cmd);
static void parseOne(Print * stream, byte * command, RingStream * ringStream);
static void setFilter(FILTER_CALLBACK filter);
static void setRMFTFilter(FILTER_CALLBACK filter);
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
@@ -40,17 +42,13 @@ struct DCCEXParser
private:
static const int16_t MAX_BUFFER=50; // longest command sent in
byte bufferLength=0;
bool inCommandPayload=false;
byte buffer[MAX_BUFFER+2];
int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command);
int16_t splitHexValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command);
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command, bool usehex);
bool parseT(Print * stream, int16_t params, int16_t p[]);
bool parseZ(Print * stream, int16_t params, int16_t p[]);
bool parseS(Print * stream, int16_t params, int16_t p[]);
bool parsef(Print * stream, int16_t params, int16_t p[]);
bool parseD(Print * stream, int16_t params, int16_t p[]);
static bool parseT(Print * stream, int16_t params, int16_t p[]);
static bool parseZ(Print * stream, int16_t params, int16_t p[]);
static bool parseS(Print * stream, int16_t params, int16_t p[]);
static bool parsef(Print * stream, int16_t params, int16_t p[]);
static bool parseD(Print * stream, int16_t params, int16_t p[]);
static Print * getAsyncReplyStream();
static void commitAsyncReplyStream();
@@ -61,8 +59,9 @@ struct DCCEXParser
static RingStream * stashRingStream;
static int16_t stashP[MAX_COMMAND_PARAMS];
bool stashCallback(Print * stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream);
static bool stashCallback(Print * stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream);
static void callback_W(int16_t result);
static void callback_W4(int16_t result);
static void callback_B(int16_t result);
static void callback_R(int16_t result);
static void callback_Rloco(int16_t result);
@@ -73,6 +72,7 @@ struct DCCEXParser
static FILTER_CALLBACK filterRMFTCallback;
static AT_COMMAND_CALLBACK atCommandCallback;
static void funcmap(int16_t cab, byte value, byte fstart, byte fstop);
static void sendFlashList(Print * stream,const int16_t flashList[]);
};

View File

@@ -1,213 +0,0 @@
/*
* © 2021, Chris Harlow & David Cutting. All rights reserved.
*
* This file is part of Asbelos DCC 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 timer class is used to manage the single timer required to handle the DCC waveform.
* All timer access comes through this class so that it can be compiled for
* various hardware CPU types.
*
* DCCEX works on a single timer interrupt at a regular 58uS interval.
* The DCCWaveform class generates the signals to the motor shield
* based on this timer.
*
* If the motor drivers are BOTH configured to use the correct 2 pins for the architecture,
* (see isPWMPin() function. )
* then this allows us to use a hardware driven pin switching arrangement which is
* achieved by setting the duty cycle of the NEXT clock interrupt to 0% or 100% depending on
* the required pin state. (see setPWM())
* This is more accurate than the software interrupt but at the expense of
* limiting the choice of available pins.
* Fortunately, a standard motor shield on a Mega uses pins that qualify for PWM...
* Other shields may be jumpered to PWM pins or run directly using the software interrupt.
*
* Because the PWM-based waveform is effectively set half a cycle after the software version,
* it is not acceptable to drive the two tracks on different methiods or it would cause
* problems for <1 JOIN> etc.
*
*/
#include "DCCTimer.h"
const int DCC_SIGNAL_TIME=58; // this is the 58uS DCC 1-bit waveform half-cycle
const long CLOCK_CYCLES=(F_CPU / 1000000 * DCC_SIGNAL_TIME) >>1;
INTERRUPT_CALLBACK interruptHandler=0;
#ifdef ARDUINO_ARCH_MEGAAVR
// Arduino unoWifi Rev2 and nanoEvery architectire
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
noInterrupts();
ADC0.CTRLC = (ADC0.CTRLC & 0b00110000) | 0b01000011; // speed up analogRead sample time
TCB0.CTRLB = TCB_CNTMODE_INT_gc & ~TCB_CCMPEN_bm; // timer compare mode with output disabled
TCB0.CTRLA = TCB_CLKSEL_CLKDIV2_gc; // 8 MHz ~ 0.125 us
TCB0.CCMP = CLOCK_CYCLES -1; // 1 tick less for timer reset
TCB0.INTFLAGS = TCB_CAPT_bm; // clear interrupt request flag
TCB0.INTCTRL = TCB_CAPT_bm; // Enable the interrupt
TCB0.CNT = 0;
TCB0.CTRLA |= TCB_ENABLE_bm; // start
interrupts();
}
// ISR called by timer interrupt every 58uS
ISR(TCB0_INT_vect){
TCB0.INTFLAGS = TCB_CAPT_bm;
interruptHandler();
}
bool DCCTimer::isPWMPin(byte pin) {
(void) pin;
return false; // TODO what are the relevant pins?
}
void DCCTimer::setPWM(byte pin, bool high) {
(void) pin;
(void) high;
// TODO what are the relevant pins?
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
memcpy(mac,(void *) &SIGROW.SERNUM0,6); // serial number
mac[0] &= 0xFE;
mac[0] |= 0x02;
}
#elif defined(TEENSYDUINO)
IntervalTimer myDCCTimer;
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
myDCCTimer.begin(interruptHandler, DCC_SIGNAL_TIME);
}
bool DCCTimer::isPWMPin(byte pin) {
//Teensy: digitalPinHasPWM, todo
(void) pin;
return false; // TODO what are the relevant pins?
}
void DCCTimer::setPWM(byte pin, bool high) {
// TODO what are the relevant pins?
(void) pin;
(void) high;
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
#if defined(__IMXRT1062__) //Teensy 4.0 and Teensy 4.1
uint32_t m1 = HW_OCOTP_MAC1;
uint32_t m2 = HW_OCOTP_MAC0;
mac[0] = m1 >> 8;
mac[1] = m1 >> 0;
mac[2] = m2 >> 24;
mac[3] = m2 >> 16;
mac[4] = m2 >> 8;
mac[5] = m2 >> 0;
#else
read_mac(mac);
#endif
}
#if !defined(__IMXRT1062__)
void DCCTimer::read_mac(byte mac[6]) {
read(0xe,mac,0);
read(0xf,mac,3);
}
// http://forum.pjrc.com/threads/91-teensy-3-MAC-address
void DCCTimer::read(uint8_t word, uint8_t *mac, uint8_t offset) {
FTFL_FCCOB0 = 0x41; // Selects the READONCE command
FTFL_FCCOB1 = word; // read the given word of read once area
// launch command and wait until complete
FTFL_FSTAT = FTFL_FSTAT_CCIF;
while(!(FTFL_FSTAT & FTFL_FSTAT_CCIF));
*(mac+offset) = FTFL_FCCOB5; // collect only the top three bytes,
*(mac+offset+1) = FTFL_FCCOB6; // in the right orientation (big endian).
*(mac+offset+2) = FTFL_FCCOB7; // Skip FTFL_FCCOB4 as it's always 0.
}
#endif
#else
// Arduino nano, uno, mega etc
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
#define TIMER1_A_PIN 11
#define TIMER1_B_PIN 12
#define TIMER1_C_PIN 13
#else
#define TIMER1_A_PIN 9
#define TIMER1_B_PIN 10
#endif
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
noInterrupts();
ADCSRA = (ADCSRA & 0b11111000) | 0b00000100; // speed up analogRead sample time
TCCR1A = 0;
ICR1 = CLOCK_CYCLES;
TCNT1 = 0;
TCCR1B = _BV(WGM13) | _BV(CS10); // Mode 8, clock select 1
TIMSK1 = _BV(TOIE1); // Enable Software interrupt
interrupts();
}
// ISR called by timer interrupt every 58uS
ISR(TIMER1_OVF_vect){ interruptHandler(); }
// Alternative pin manipulation via PWM control.
bool DCCTimer::isPWMPin(byte pin) {
return pin==TIMER1_A_PIN
|| pin==TIMER1_B_PIN
#ifdef TIMER1_C_PIN
|| pin==TIMER1_C_PIN
#endif
;
}
void DCCTimer::setPWM(byte pin, bool high) {
if (pin==TIMER1_A_PIN) {
TCCR1A |= _BV(COM1A1);
OCR1A= high?1024:0;
}
else if (pin==TIMER1_B_PIN) {
TCCR1A |= _BV(COM1B1);
OCR1B= high?1024:0;
}
#ifdef TIMER1_C_PIN
else if (pin==TIMER1_C_PIN) {
TCCR1A |= _BV(COM1C1);
OCR1C= high?1024:0;
}
#endif
}
#include <avr/boot.h>
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
for (byte i=0; i<6; i++) {
mac[i]=boot_signature_byte_get(0x0E + i);
}
mac[0] &= 0xFE;
mac[0] |= 0x02;
}
#endif

View File

@@ -1,6 +1,8 @@
/*
* (c) 2021 Mike S. All rights reserved.
* (c) 2021 Fred Decker. All rights reserved.
* © 2021 Mike S
* © 2021 Harald Barth
* © 2021 Fred Decker
* All rights reserved.
*
* This file is part of CommandStation-EX
*
@@ -18,6 +20,34 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/* There are several different implementations of this class which the compiler will select
according to the hardware.
*/
/* This timer class is used to manage the single timer required to handle the DCC waveform.
* All timer access comes through this class so that it can be compiled for
* various hardware CPU types.
*
* DCCEX works on a single timer interrupt at a regular 58uS interval.
* The DCCWaveform class generates the signals to the motor shield
* based on this timer.
*
* If the motor drivers are BOTH configured to use the correct 2 pins for the architecture,
* (see isPWMPin() function. )
* then this allows us to use a hardware driven pin switching arrangement which is
* achieved by setting the duty cycle of the NEXT clock interrupt to 0% or 100% depending on
* the required pin state. (see setPWM())
* This is more accurate than the software interrupt but at the expense of
* limiting the choice of available pins.
* Fortunately, a standard motor shield on a Mega uses pins that qualify for PWM...
* Other shields may be jumpered to PWM pins or run directly using the software interrupt.
*
* Because the PWM-based waveform is effectively set half a cycle after the software version,
* it is not acceptable to drive the two tracks on different methiods or it would cause
* problems for <1 JOIN> etc.
*
*/
#ifndef DCCTimer_h
#define DCCTimer_h
#include "Arduino.h"
@@ -30,11 +60,68 @@ class DCCTimer {
static void getSimulatedMacAddress(byte mac[6]);
static bool isPWMPin(byte pin);
static void setPWM(byte pin, bool high);
#if (defined(TEENSYDUINO) && !defined(__IMXRT1062__))
static void read_mac(byte mac[6]);
static void read(uint8_t word, uint8_t *mac, uint8_t offset);
#endif
private:
static void clearPWM();
// Update low ram level. Allow for extra bytes to be specified
// by estimation or inspection, that may be used by other
// called subroutines. Must be called with interrupts disabled.
//
// Although __brkval may go up and down as heap memory is allocated
// and freed, this function records only the worst case encountered.
// So even if all of the heap is freed, the reported minimum free
// memory will not increase.
//
static void inline updateMinimumFreeMemoryISR(unsigned char extraBytes=0) {
int spare = freeMemory()-extraBytes;
if (spare < 0) spare = 0;
if (spare < minimum_free_memory) minimum_free_memory = spare;
}
static int getMinimumFreeMemory();
private:
static int freeMemory();
static volatile int minimum_free_memory;
static const int DCC_SIGNAL_TIME=58; // this is the 58uS DCC 1-bit waveform half-cycle
static const long CLOCK_CYCLES=(F_CPU / 1000000 * DCC_SIGNAL_TIME) >>1;
};
////////////////////////////////////////////////////////////////////////////////
// Create a cpu type we can share and
// gigure out if we have enough memory for advanced features
// so define HAS_ENOUGH_MEMORY until proved otherwise.
#define HAS_ENOUGH_MEMORY
#define HAS_AVR_WDT
#if defined(ARDUINO_AVR_UNO)
#define ARDUINO_TYPE "UNO"
#undef HAS_ENOUGH_MEMORY
#elif defined(ARDUINO_AVR_NANO)
#define ARDUINO_TYPE "NANO"
#undef HAS_ENOUGH_MEMORY
#elif defined(ARDUINO_AVR_MEGA)
#define ARDUINO_TYPE "MEGA"
#elif defined(ARDUINO_AVR_MEGA2560)
#define ARDUINO_TYPE "MEGA"
#elif defined(ARDUINO_ARCH_MEGAAVR)
#define ARDUINO_TYPE "MEGAAVR"
#elif defined(ARDUINO_TEENSY32)
#define ARDUINO_TYPE "TEENSY32"
#elif defined(ARDUINO_TEENSY35)
#define ARDUINO_TYPE "TEENSY35"
#elif defined(ARDUINO_TEENSY36)
#define ARDUINO_TYPE "TEENSY36"
#elif defined(ARDUINO_TEENSY40)
#define ARDUINO_TYPE "TEENSY40"
#elif defined(ARDUINO_TEENSY41)
#define ARDUINO_TYPE "TEENSY41"
#elif defined(ARDUINO_ARCH_ESP8266)
#define ARDUINO_TYPE "ESP8266"
#undef HAS_AVR_WDT
#elif defined(ARDUINO_ARCH_ESP32)
#define ARDUINO_TYPE "ESP32"
#undef HAS_AVR_WDT
#else
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN DCCTimer.h
#endif
#endif

117
DCCTimerAVR.cpp Normal file
View File

@@ -0,0 +1,117 @@
/*
* © 2021 Mike S
* © 2021 Harald Barth
* © 2021 Fred Decker
* © 2021 Chris Harlow
* © 2021 David Cutting
* All rights reserved.
*
* This file is part of Asbelos DCC 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/>.
*/
// ATTENTION: this file only compiles on a UNO or MEGA
// Please refer to DCCTimer.h for general comments about how this class works
// This is to avoid repetition and duplication.
#ifdef ARDUINO_ARCH_AVR
#include <avr/boot.h>
#include "DCCTimer.h"
INTERRUPT_CALLBACK interruptHandler=0;
// Arduino nano, uno, mega etc
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
#define TIMER1_A_PIN 11
#define TIMER1_B_PIN 12
#define TIMER1_C_PIN 13
#else
#define TIMER1_A_PIN 9
#define TIMER1_B_PIN 10
#endif
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
noInterrupts();
ADCSRA = (ADCSRA & 0b11111000) | 0b00000100; // speed up analogRead sample time
TCCR1A = 0;
ICR1 = CLOCK_CYCLES;
TCNT1 = 0;
TCCR1B = _BV(WGM13) | _BV(CS10); // Mode 8, clock select 1
TIMSK1 = _BV(TOIE1); // Enable Software interrupt
interrupts();
}
// ISR called by timer interrupt every 58uS
ISR(TIMER1_OVF_vect){ interruptHandler(); }
// Alternative pin manipulation via PWM control.
bool DCCTimer::isPWMPin(byte pin) {
return pin==TIMER1_A_PIN
|| pin==TIMER1_B_PIN
#ifdef TIMER1_C_PIN
|| pin==TIMER1_C_PIN
#endif
;
}
void DCCTimer::setPWM(byte pin, bool high) {
if (pin==TIMER1_A_PIN) {
TCCR1A |= _BV(COM1A1);
OCR1A= high?1024:0;
}
else if (pin==TIMER1_B_PIN) {
TCCR1A |= _BV(COM1B1);
OCR1B= high?1024:0;
}
#ifdef TIMER1_C_PIN
else if (pin==TIMER1_C_PIN) {
TCCR1A |= _BV(COM1C1);
OCR1C= high?1024:0;
}
#endif
}
void DCCTimer::clearPWM() {
TCCR1A= 0;
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
for (byte i=0; i<6; i++) {
mac[i]=boot_signature_byte_get(0x0E + i);
}
mac[0] &= 0xFE;
mac[0] |= 0x02;
}
volatile int DCCTimer::minimum_free_memory=__INT_MAX__;
// Return low memory value...
int DCCTimer::getMinimumFreeMemory() {
noInterrupts(); // Disable interrupts to get volatile value
int retval = minimum_free_memory;
interrupts();
return retval;
}
extern char *__brkval;
extern char *__malloc_heap_start;
int DCCTimer::freeMemory() {
char top;
return __brkval ? &top - __brkval : &top - __malloc_heap_start;
}
#endif

129
DCCTimerESP.cpp Normal file
View File

@@ -0,0 +1,129 @@
/*
* © 2020-2022 Harald Barth
*
* 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/>.
*/
// ATTENTION: this file only compiles on an ESP8266 and ESP32
// On ESP32 we do not even use the functions but they are here for completeness sake
// Please refer to DCCTimer.h for general comments about how this class works
// This is to avoid repetition and duplication.
#ifdef ARDUINO_ARCH_ESP8266
#include "DCCTimer.h"
INTERRUPT_CALLBACK interruptHandler=0;
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
timer1_disable();
// There seem to be differnt ways to attach interrupt handler
// ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL);
// ETS_FRC_TIMER1_NMI_INTR_ATTACH(interruptHandler);
// Let us choose the one from the API
timer1_attachInterrupt(interruptHandler);
// not exactly sure of order:
timer1_enable(TIM_DIV1, TIM_EDGE, TIM_LOOP);
timer1_write(CLOCK_CYCLES);
}
// We do not support to use PWM to make the Waveform on ESP
bool IRAM_ATTR DCCTimer::isPWMPin(byte pin) {
return false;
}
void IRAM_ATTR DCCTimer::setPWM(byte pin, bool high) {
}
void IRAM_ATTR DCCTimer::clearPWM() {
}
// Fake this as it should not be used
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
mac[0] = 0xFE;
mac[1] = 0xBE;
mac[2] = 0xEF;
mac[3] = 0xC0;
mac[4] = 0xFF;
mac[5] = 0xEE;
}
volatile int DCCTimer::minimum_free_memory=__INT_MAX__;
// Return low memory value...
int DCCTimer::getMinimumFreeMemory() {
noInterrupts(); // Disable interrupts to get volatile value
int retval = minimum_free_memory;
interrupts();
return retval;
}
int DCCTimer::freeMemory() {
return ESP.getFreeHeap();
}
#endif
////////////////////////////////////////////////////////////////////////
#ifdef ARDUINO_ARCH_ESP32
#include "DCCTimer.h"
INTERRUPT_CALLBACK interruptHandler=0;
// https://www.visualmicro.com/page/Timer-Interrupts-Explained.aspx
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler = callback;
hw_timer_t *timer = NULL;
timer = timerBegin(0, 2, true); // prescaler can be 2 to 65536 so choose 2
timerAttachInterrupt(timer, interruptHandler, true);
timerAlarmWrite(timer, CLOCK_CYCLES / 6, true); // divide by prescaler*3 (Clockbase is 80Mhz and not F_CPU 240Mhz)
timerAlarmEnable(timer);
}
// We do not support to use PWM to make the Waveform on ESP
bool IRAM_ATTR DCCTimer::isPWMPin(byte pin) {
return false;
}
void IRAM_ATTR DCCTimer::setPWM(byte pin, bool high) {
}
// Fake this as it should not be used
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
mac[0] = 0xFE;
mac[1] = 0xBE;
mac[2] = 0xEF;
mac[3] = 0xC0;
mac[4] = 0xFF;
mac[5] = 0xEE;
}
volatile int DCCTimer::minimum_free_memory=__INT_MAX__;
// Return low memory value...
int DCCTimer::getMinimumFreeMemory() {
noInterrupts(); // Disable interrupts to get volatile value
int retval = minimum_free_memory;
interrupts();
return retval;
}
int DCCTimer::freeMemory() {
return ESP.getFreeHeap();
}
#endif

121
DCCTimerMEGAAVR.cpp Normal file
View File

@@ -0,0 +1,121 @@
/*
* © 2021 Mike S
* © 2021 Harald Barth
* © 2021 Fred Decker
* © 2021 Chris Harlow
* © 2021 David Cutting
* All rights reserved.
*
* This file is part of Asbelos DCC 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 timer class is used to manage the single timer required to handle the DCC waveform.
* All timer access comes through this class so that it can be compiled for
* various hardware CPU types.
*
* DCCEX works on a single timer interrupt at a regular 58uS interval.
* The DCCWaveform class generates the signals to the motor shield
* based on this timer.
*
* If the motor drivers are BOTH configured to use the correct 2 pins for the architecture,
* (see isPWMPin() function. )
* then this allows us to use a hardware driven pin switching arrangement which is
* achieved by setting the duty cycle of the NEXT clock interrupt to 0% or 100% depending on
* the required pin state. (see setPWM())
* This is more accurate than the software interrupt but at the expense of
* limiting the choice of available pins.
* Fortunately, a standard motor shield on a Mega uses pins that qualify for PWM...
* Other shields may be jumpered to PWM pins or run directly using the software interrupt.
*
* Because the PWM-based waveform is effectively set half a cycle after the software version,
* it is not acceptable to drive the two tracks on different methiods or it would cause
* problems for <1 JOIN> etc.
*
*/
// ATTENTION: this file only compiles on a UnoWifiRev3 or NanoEvery
// Please refer to DCCTimer.h for general comments about how this class works
// This is to avoid repetition and duplication.
#ifdef ARDUINO_ARCH_MEGAAVR
#include "DCCTimer.h"
INTERRUPT_CALLBACK interruptHandler=0;
extern char *__brkval;
extern char *__malloc_heap_start;
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
noInterrupts();
ADC0.CTRLC = (ADC0.CTRLC & 0b00110000) | 0b01000011; // speed up analogRead sample time
TCB0.CTRLB = TCB_CNTMODE_INT_gc & ~TCB_CCMPEN_bm; // timer compare mode with output disabled
TCB0.CTRLA = TCB_CLKSEL_CLKDIV2_gc; // 8 MHz ~ 0.125 us
TCB0.CCMP = CLOCK_CYCLES -1; // 1 tick less for timer reset
TCB0.INTFLAGS = TCB_CAPT_bm; // clear interrupt request flag
TCB0.INTCTRL = TCB_CAPT_bm; // Enable the interrupt
TCB0.CNT = 0;
TCB0.CTRLA |= TCB_ENABLE_bm; // start
interrupts();
}
// ISR called by timer interrupt every 58uS
ISR(TCB0_INT_vect){
TCB0.INTFLAGS = TCB_CAPT_bm;
interruptHandler();
}
bool DCCTimer::isPWMPin(byte pin) {
(void) pin;
return false; // TODO what are the relevant pins?
}
void DCCTimer::setPWM(byte pin, bool high) {
(void) pin;
(void) high;
// TODO what are the relevant pins?
}
void DCCTimer::clearPWM() {
// Do nothing unless we implent HA
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
memcpy(mac,(void *) &SIGROW.SERNUM0,6); // serial number
mac[0] &= 0xFE;
mac[0] |= 0x02;
}
volatile int DCCTimer::minimum_free_memory=__INT_MAX__;
// Return low memory value...
int DCCTimer::getMinimumFreeMemory() {
noInterrupts(); // Disable interrupts to get volatile value
int retval = minimum_free_memory;
interrupts();
return retval;
}
extern char *__brkval;
extern char *__malloc_heap_start;
int DCCTimer::freeMemory() {
char top;
return __brkval ? &top - __brkval : &top - __malloc_heap_start;
}
#endif

126
DCCTimerTEENSY.cpp Normal file
View File

@@ -0,0 +1,126 @@
/*
* © 2021 Mike S
* © 2021 Harald Barth
* © 2021 Fred Decker
* © 2021 Chris Harlow
* © 2021 David Cutting
* All rights reserved.
*
* This file is part of Asbelos DCC 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/>.
*/
// ATTENTION: this file only compiles on a TEENSY
// Please refer to DCCTimer.h for general comments about how this class works
// This is to avoid repetition and duplication.
#ifdef TEENSYDUINO
#include "DCCTimer.h"
INTERRUPT_CALLBACK interruptHandler=0;
IntervalTimer myDCCTimer;
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
myDCCTimer.begin(interruptHandler, DCC_SIGNAL_TIME);
}
bool DCCTimer::isPWMPin(byte pin) {
//Teensy: digitalPinHasPWM, todo
(void) pin;
return false; // TODO what are the relevant pins?
}
void DCCTimer::setPWM(byte pin, bool high) {
// TODO what are the relevant pins?
(void) pin;
(void) high;
}
void DCCTimer::clearPWM() {
// Do nothing unless we implent HA
}
#if defined(__IMXRT1062__) //Teensy 4.0 and Teensy 4.1
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
uint32_t m1 = HW_OCOTP_MAC1;
uint32_t m2 = HW_OCOTP_MAC0;
mac[0] = m1 >> 8;
mac[1] = m1 >> 0;
mac[2] = m2 >> 24;
mac[3] = m2 >> 16;
mac[4] = m2 >> 8;
mac[5] = m2 >> 0;
}
#else
// http://forum.pjrc.com/threads/91-teensy-3-MAC-address
void teensyRead(uint8_t word, uint8_t *mac, uint8_t offset) {
FTFL_FCCOB0 = 0x41; // Selects the READONCE command
FTFL_FCCOB1 = word; // read the given word of read once area
// launch command and wait until complete
FTFL_FSTAT = FTFL_FSTAT_CCIF;
while(!(FTFL_FSTAT & FTFL_FSTAT_CCIF));
*(mac+offset) = FTFL_FCCOB5; // collect only the top three bytes,
*(mac+offset+1) = FTFL_FCCOB6; // in the right orientation (big endian).
*(mac+offset+2) = FTFL_FCCOB7; // Skip FTFL_FCCOB4 as it's always 0.
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
teensyRead(0xe,mac,0);
teensyRead(0xf,mac,3);
}
#endif
#if !defined(__IMXRT1062__)
static inline int freeMemory() {
char top;
return &top - reinterpret_cast<char*>(sbrk(0));
}
#else
#if defined(ARDUINO_TEENSY40)
static const unsigned DTCM_START = 0x20000000UL;
static const unsigned OCRAM_START = 0x20200000UL;
static const unsigned OCRAM_SIZE = 512;
static const unsigned FLASH_SIZE = 1984;
#elif defined(ARDUINO_TEENSY41)
static const unsigned DTCM_START = 0x20000000UL;
static const unsigned OCRAM_START = 0x20200000UL;
static const unsigned OCRAM_SIZE = 512;
static const unsigned FLASH_SIZE = 7936;
#if TEENSYDUINO>151
extern "C" uint8_t external_psram_size;
#endif
#endif
static inline int freeMemory() {
extern unsigned long _ebss;
extern unsigned long _sdata;
extern unsigned long _estack;
const unsigned DTCM_START = 0x20000000UL;
unsigned dtcm = (unsigned)&_estack - DTCM_START;
unsigned stackinuse = (unsigned) &_estack - (unsigned) __builtin_frame_address(0);
unsigned varsinuse = (unsigned)&_ebss - (unsigned)&_sdata;
unsigned freemem = dtcm - (stackinuse + varsinuse);
return freemem;
}
#endif
#endif

View File

@@ -1,8 +1,12 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth.
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
* 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
@@ -17,56 +21,65 @@
* 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")
#ifndef ARDUINO_ARCH_ESP32
// This code is replaced entirely on an ESP32
#include <Arduino.h>
#include "DCCWaveform.h"
#include "TrackManager.h"
#include "DCCTimer.h"
#include "DCCACK.h"
#include "DIAG.h"
#include "freeMemory.h"
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
bool DCCWaveform::progTrackSyncMain=false;
bool DCCWaveform::progTrackBoosted=false;
int DCCWaveform::progTripValue=0;
volatile uint8_t DCCWaveform::numAckGaps=0;
volatile uint8_t DCCWaveform::numAckSamples=0;
uint8_t DCCWaveform::trailingEdgeCounter=0;
void DCCWaveform::begin(MotorDriver * mainDriver, MotorDriver * progDriver) {
mainTrack.motorDriver=mainDriver;
progTrack.motorDriver=progDriver;
progTripValue = progDriver->mA2raw(TRIP_CURRENT_PROG); // need only calculate once hence static
mainTrack.setPowerMode(POWERMODE::OFF);
progTrack.setPowerMode(POWERMODE::OFF);
// Fault pin config for odd motor boards (example pololu)
MotorDriver::commonFaultPin = ((mainDriver->getFaultPin() == progDriver->getFaultPin())
&& (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"));
// This bitmask has 9 entries as each byte is trasmitted as a zero + 8 bits.
const byte bitMask[] = {0x00, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
const byte idlePacket[] = {0xFF, 0x00, 0xFF};
const byte resetPacket[] = {0x00, 0x00, 0x00};
// For each state of the wave nextState=stateTransform[currentState]
const WAVE_STATE stateTransform[]={
/* WAVE_START -> */ WAVE_PENDING,
/* WAVE_MID_1 -> */ WAVE_START,
/* WAVE_HIGH_0 -> */ WAVE_MID_0,
/* WAVE_MID_0 -> */ WAVE_LOW_0,
/* WAVE_LOW_0 -> */ WAVE_START,
/* WAVE_PENDING (should not happen) -> */ WAVE_PENDING};
// For each state of the wave, signal pin is HIGH or LOW
const bool signalTransform[]={
/* WAVE_START -> */ HIGH,
/* WAVE_MID_1 -> */ LOW,
/* WAVE_HIGH_0 -> */ HIGH,
/* WAVE_MID_0 -> */ LOW,
/* WAVE_LOW_0 -> */ LOW,
/* WAVE_PENDING (should not happen) -> */ LOW};
void DCCWaveform::begin() {
DCCTimer::begin(DCCWaveform::interruptHandler);
}
void DCCWaveform::loop(bool ackManagerActive) {
mainTrack.checkPowerOverload(false);
progTrack.checkPowerOverload(ackManagerActive);
void DCCWaveform::loop() {
// empty placemarker in case ESP32 needs something here
}
#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
byte sigMain=signalTransform[mainTrack.state];
byte sigProg=progTrackSyncMain? sigMain : signalTransform[progTrack.state];
byte sigProg=TrackManager::progTrackSyncMain? sigMain : signalTransform[progTrack.state];
// Set the signal state for both tracks
mainTrack.motorDriver->setSignal(sigMain);
progTrack.motorDriver->setSignal(sigProg);
TrackManager::setDCCSignal(sigMain);
TrackManager::setPROGSignal(sigProg);
// Move on in the state engine
mainTrack.state=stateTransform[mainTrack.state];
@@ -76,10 +89,10 @@ void DCCWaveform::interruptHandler() {
// WAVE_PENDING means we dont yet know what the next bit is
if (mainTrack.state==WAVE_PENDING) mainTrack.interrupt2();
if (progTrack.state==WAVE_PENDING) progTrack.interrupt2();
else if (progTrack.ackPending) progTrack.checkAck();
else DCCACK::checkAck(progTrack.sentResetsSincePacket);
}
#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.
@@ -87,9 +100,6 @@ void DCCWaveform::interruptHandler() {
// When the current buffer is exhausted, either the pending buffer (if there is one waiting) or an idle buffer.
// This bitmask has 9 entries as each byte is trasmitted as a zero + 8 bits.
const byte bitMask[] = {0x00, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
isMainTrack = isMain;
@@ -101,102 +111,12 @@ DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
requiredPreambles = preambleBits+1;
bytes_sent = 0;
bits_sent = 0;
sampleDelay = 0;
lastSampleTaken = millis();
ackPending=false;
}
POWERMODE DCCWaveform::getPowerMode() {
return powerMode;
}
void DCCWaveform::setPowerMode(POWERMODE mode) {
powerMode = mode;
bool ison = (mode == POWERMODE::ON);
motorDriver->setPower( ison);
}
void DCCWaveform::checkPowerOverload(bool ackManagerActive) {
if (millis() - lastSampleTaken < sampleDelay) return;
lastSampleTaken = millis();
int tripValue= motorDriver->getRawCurrentTripValue();
if (!isMainTrack && !ackManagerActive && !progTrackSyncMain && !progTrackBoosted)
tripValue=progTripValue;
switch (powerMode) {
case POWERMODE::OFF:
sampleDelay = POWER_SAMPLE_OFF_WAIT;
break;
case POWERMODE::ON:
// Check current
lastCurrent=motorDriver->getCurrentRaw();
if (lastCurrent < 0) {
// We have a fault pin condition to take care of
lastCurrent = -lastCurrent;
setPowerMode(POWERMODE::OVERLOAD); // Turn off, decide later how fast to turn on again
if (MotorDriver::commonFaultPin) {
if (lastCurrent <= tripValue) {
setPowerMode(POWERMODE::ON); // maybe other track
}
// 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"));
} else {
DIAG(F("*** %S FAULT PIN ACTIVE - OVERLOAD ***"), isMainTrack ? F("MAIN") : F("PROG"));
if (lastCurrent < tripValue) {
lastCurrent = tripValue; // exaggerate
}
}
}
if (lastCurrent < tripValue) {
sampleDelay = POWER_SAMPLE_ON_WAIT;
if(power_good_counter<100)
power_good_counter++;
else
if (power_sample_overload_wait>POWER_SAMPLE_OVERLOAD_WAIT) power_sample_overload_wait=POWER_SAMPLE_OVERLOAD_WAIT;
} else {
setPowerMode(POWERMODE::OVERLOAD);
unsigned int mA=motorDriver->raw2mA(lastCurrent);
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);
if (power_sample_overload_wait >= 10000)
power_sample_overload_wait = 10000;
else
power_sample_overload_wait *= 2;
}
break;
case POWERMODE::OVERLOAD:
// Try setting it back on after the OVERLOAD_WAIT
setPowerMode(POWERMODE::ON);
sampleDelay = POWER_SAMPLE_ON_WAIT;
// Debug code....
DIAG(F("*** %S TRACK POWER RESET delay=%d ***"), isMainTrack ? F("MAIN") : F("PROG"), sampleDelay);
break;
default:
sampleDelay = 999; // cant get here..meaningless statement to avoid compiler warning.
}
}
// For each state of the wave nextState=stateTransform[currentState]
const WAVE_STATE DCCWaveform::stateTransform[]={
/* WAVE_START -> */ WAVE_PENDING,
/* WAVE_MID_1 -> */ WAVE_START,
/* WAVE_HIGH_0 -> */ WAVE_MID_0,
/* WAVE_MID_0 -> */ WAVE_LOW_0,
/* WAVE_LOW_0 -> */ WAVE_START,
/* WAVE_PENDING (should not happen) -> */ WAVE_PENDING};
// For each state of the wave, signal pin is HIGH or LOW
const bool DCCWaveform::signalTransform[]={
/* WAVE_START -> */ HIGH,
/* WAVE_MID_1 -> */ LOW,
/* WAVE_HIGH_0 -> */ HIGH,
/* WAVE_MID_0 -> */ LOW,
/* WAVE_LOW_0 -> */ LOW,
/* WAVE_PENDING (should not happen) -> */ LOW};
#pragma GCC optimize ("-O3")
void DCCWaveform::interrupt2() {
// calculate the next bit to be sent:
// set state WAVE_MID_1 for a 1=bit
@@ -207,7 +127,7 @@ void DCCWaveform::interrupt2() {
remainingPreambles--;
// Update free memory diagnostic as we don't have anything else to do this time.
// Allow for checkAck and its called functions using 22 bytes more.
updateMinimumFreeMemory(22);
DCCTimer::updateMinimumFreeMemoryISR(22);
return;
}
@@ -253,8 +173,6 @@ void DCCWaveform::interrupt2() {
}
}
// Wait until there is no packet pending, then make this pending
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum
@@ -272,86 +190,4 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
packetPending = true;
sentResetsSincePacket=0;
}
// Operations applicable to PROG track ONLY.
// (yes I know I could have subclassed the main track but...)
void DCCWaveform::setAckBaseline() {
if (isMainTrack) return;
int baseline=motorDriver->getCurrentRaw();
ackThreshold= baseline + motorDriver->mA2raw(ackLimitmA);
if (Diag::ACK) DIAG(F("ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %dus and %dus"),
baseline,motorDriver->raw2mA(baseline),
ackThreshold,motorDriver->raw2mA(ackThreshold),
minAckPulseDuration, maxAckPulseDuration);
}
void DCCWaveform::setAckPending() {
if (isMainTrack) return;
ackMaxCurrent=0;
ackPulseStart=0;
ackPulseDuration=0;
ackDetected=false;
ackCheckStart=millis();
numAckSamples=0;
numAckGaps=0;
ackPending=true; // interrupt routines will now take note
}
byte DCCWaveform::getAck() {
if (ackPending) return (2); // still waiting
if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%duS samples=%d gaps=%d"),ackDetected?F("ACK"):F("NO-ACK"), ackCheckDuration,
ackMaxCurrent,motorDriver->raw2mA(ackMaxCurrent), ackPulseDuration, numAckSamples, numAckGaps);
if (ackDetected) return (1); // Yes we had an ack
return(0); // pending set off but not detected means no ACK.
}
void DCCWaveform::checkAck() {
// This function operates in interrupt() time so must be fast and can't DIAG
if (sentResetsSincePacket > 6) { //ACK timeout
ackCheckDuration=millis()-ackCheckStart;
ackPending = false;
return;
}
int current=motorDriver->getCurrentRaw();
numAckSamples++;
if (current > ackMaxCurrent) ackMaxCurrent=current;
// An ACK is a pulse lasting between minAckPulseDuration and maxAckPulseDuration uSecs (refer @haba)
if (current>ackThreshold) {
if (trailingEdgeCounter > 0) {
numAckGaps++;
trailingEdgeCounter = 0;
}
if (ackPulseStart==0) ackPulseStart=micros(); // leading edge of pulse detected
return;
}
// not in pulse
if (ackPulseStart==0) return; // keep waiting for leading edge
// if we reach to this point, we have
// detected trailing edge of pulse
if (trailingEdgeCounter == 0) {
ackPulseDuration=micros()-ackPulseStart;
}
// but we do not trust it yet and return (which will force another
// measurement) and first the third time around with low current
// the ack detection will be finalized.
if (trailingEdgeCounter < 2) {
trailingEdgeCounter++;
return;
}
trailingEdgeCounter = 0;
if (ackPulseDuration>=minAckPulseDuration && ackPulseDuration<=maxAckPulseDuration) {
ackCheckDuration=millis()-ackCheckStart;
ackDetected=true;
ackPending=false;
transmitRepeats=0; // shortcut remaining repeat packets
return; // we have a genuine ACK result
}
ackPulseStart=0; // We have detected a too-short or too-long pulse so ignore and wait for next leading edge
}
#endif

View File

@@ -1,8 +1,12 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth.
* © 2021 M Steve Todd
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
* 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
@@ -22,106 +26,39 @@
#include "MotorDriver.h"
// Wait times for power management. Unit: milliseconds
const int POWER_SAMPLE_ON_WAIT = 100;
const int POWER_SAMPLE_OFF_WAIT = 1000;
const int POWER_SAMPLE_OVERLOAD_WAIT = 20;
// Number of preamble bits.
const int PREAMBLE_BITS_MAIN = 16;
const int PREAMBLE_BITS_PROG = 22;
const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum.
// The WAVE_STATE enum is deliberately numbered because a change of order would be catastrophic
// to the transform array.
enum WAVE_STATE : byte {WAVE_START=0,WAVE_MID_1=1,WAVE_HIGH_0=2,WAVE_MID_0=3,WAVE_LOW_0=4,WAVE_PENDING=5};
// NOTE: static functions are used for the overall controller, then
// one instance is created for each track.
enum class POWERMODE : byte { OFF, ON, OVERLOAD };
const byte idlePacket[] = {0xFF, 0x00, 0xFF};
const byte resetPacket[] = {0x00, 0x00, 0x00};
class DCCWaveform {
public:
DCCWaveform( byte preambleBits, bool isMain);
static void begin(MotorDriver * mainDriver, MotorDriver * progDriver);
static void loop(bool ackManagerActive);
static void begin();
static void loop();
static DCCWaveform mainTrack;
static DCCWaveform progTrack;
void beginTrack();
void setPowerMode(POWERMODE);
POWERMODE getPowerMode();
void checkPowerOverload(bool ackManagerActive);
inline int get1024Current() {
if (powerMode == POWERMODE::ON)
return (int)(lastCurrent*(long int)1024/motorDriver->getRawCurrentTripValue());
return 0;
}
inline int getCurrentmA() {
if (powerMode == POWERMODE::ON)
return motorDriver->raw2mA(lastCurrent);
return 0;
}
inline int getMaxmA() {
if (maxmA == 0) { //only calculate this for first request, it doesn't change
maxmA = motorDriver->raw2mA(motorDriver->getRawCurrentTripValue()); //TODO: replace with actual max value or calc
}
return maxmA;
}
inline int getTripmA() {
if (tripmA == 0) { //only calculate this for first request, it doesn't change
tripmA = motorDriver->raw2mA(motorDriver->getRawCurrentTripValue());
}
return tripmA;
}
inline void clearRepeats() { transmitRepeats=0; }
void schedulePacket(const byte buffer[], byte byteCount, byte repeats);
volatile bool packetPending;
volatile byte sentResetsSincePacket;
volatile bool autoPowerOff=false;
void setAckBaseline(); //prog track only
void setAckPending(); //prog track only
byte getAck(); //prog track only 0=NACK, 1=ACK 2=keep waiting
static bool progTrackSyncMain; // true when prog track is a siding switched to main
static bool progTrackBoosted; // true when prog track is not current limited
inline void doAutoPowerOff() {
if (autoPowerOff) {
setPowerMode(POWERMODE::OFF);
autoPowerOff=false;
}
};
inline bool canMeasureCurrent() {
return motorDriver->canMeasureCurrent();
};
inline void setAckLimit(int mA) {
ackLimitmA = mA;
}
inline void setMinAckPulseDuration(unsigned int i) {
minAckPulseDuration = i;
}
inline void setMaxAckPulseDuration(unsigned int i) {
maxAckPulseDuration = i;
}
private:
// For each state of the wave nextState=stateTransform[currentState]
static const WAVE_STATE stateTransform[6];
// For each state of the wave, signal pin is HIGH or LOW
static const bool signalTransform[6];
private:
static void interruptHandler();
void interrupt2();
void checkAck();
bool isMainTrack;
MotorDriver* motorDriver;
// Transmission controller
byte transmitPacket[MAX_PACKET_SIZE+1]; // +1 for checksum
byte transmitLength;
@@ -134,38 +71,6 @@ class DCCWaveform {
byte pendingPacket[MAX_PACKET_SIZE+1]; // +1 for checksum
byte pendingLength;
byte pendingRepeats;
int lastCurrent;
static int progTripValue;
int maxmA;
int tripmA;
// current sampling
POWERMODE powerMode;
unsigned long lastSampleTaken;
unsigned int sampleDelay;
// Trip current for programming track, 250mA. Change only if you really
// need to be non-NMRA-compliant because of decoders that are not either.
static const int TRIP_CURRENT_PROG=250;
unsigned long power_sample_overload_wait = POWER_SAMPLE_OVERLOAD_WAIT;
unsigned int power_good_counter = 0;
// ACK management (Prog track only)
volatile bool ackPending;
volatile bool ackDetected;
int ackThreshold;
int ackLimitmA = 60;
int ackMaxCurrent;
unsigned long ackCheckStart; // millis
unsigned int ackCheckDuration; // millis
unsigned int ackPulseDuration; // micros
unsigned long ackPulseStart; // micros
unsigned int minAckPulseDuration = 4000; // micros
unsigned int maxAckPulseDuration = 8500; // micros
volatile static uint8_t numAckGaps;
volatile static uint8_t numAckSamples;
static uint8_t trailingEdgeCounter;
};
#endif

6
DIAG.h
View File

@@ -1,7 +1,9 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021 Fred Decker
* © 2020 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
* 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

View File

@@ -1,5 +1,7 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
* © 2021 Neil McKechnie
* © 2021 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
@@ -19,4 +21,4 @@
#include "DisplayInterface.h"
DisplayInterface *DisplayInterface::lcdDisplay = 0;
DisplayInterface *DisplayInterface::lcdDisplay = 0;

View File

@@ -1,5 +1,7 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
* © 2021 Neil McKechnie
* © 2021 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
@@ -32,4 +34,4 @@ public:
static DisplayInterface *lcdDisplay;
};
#endif
#endif

View File

@@ -1,9 +1,12 @@
/*
* © 2021 Neil McKechnie
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* © 2013-2016 Gregg E. Berman
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth.
* All rights reserved.
*
* This file is part of Asbelos DCC API
* 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
@@ -18,89 +21,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,6 +1,9 @@
/*
* (c) 2020 Chris Harlow. All rights reserved.
* (c) 2020 Harald Barth. All rights reserved.
* © 2021 Neil McKechnie
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
@@ -17,6 +20,7 @@
* 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
@@ -29,7 +33,7 @@ extern ExternalEEPROM EEPROM;
#include <EEPROM.h>
#endif
#define EESTORE_ID "DCC++"
#define EESTORE_ID "DCC++1"
struct EEStoreData{
char id[sizeof(EESTORE_ID)];
@@ -52,3 +56,4 @@ struct EEStore{
};
#endif
#endif // DISABLE_EEPROM

23
EXRAIL.h Normal file
View File

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

1104
EXRAIL2.cpp Normal file

File diff suppressed because it is too large Load Diff

180
EXRAIL2.h Normal file
View File

@@ -0,0 +1,180 @@
/*
* © 2021 Neil McKechnie
* © 2020-2022 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 EXRAIL2_H
#define EXRAIL2_H
#include "FSH.h"
#include "IODevice.h"
#include "Turnouts.h"
// The following are the operation codes (or instructions) for a kind of virtual machine.
// Each instruction is normally 3 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_AUTOSTART,
OPCODE_ATGTE,OPCODE_ATLT,
OPCODE_ATTIMEOUT1,OPCODE_ATTIMEOUT2,
OPCODE_LATCH,OPCODE_UNLATCH,OPCODE_SET,OPCODE_RESET,
OPCODE_ENDIF,OPCODE_ELSE,
OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_DELAYMS,OPCODE_RANDWAIT,
OPCODE_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF,
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,OPCODE_DRIVE,
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_FORGET,
OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,OPCODE_POWERON,
OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT,
OPCODE_PRINT,OPCODE_DCCACTIVATE,
OPCODE_ONACTIVATE,OPCODE_ONDEACTIVATE,
OPCODE_ROSTER,OPCODE_KILLALL,
OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,
OPCODE_ENDTASK,OPCODE_ENDEXRAIL,
OPCODE_SET_TRACK,
// OPcodes below this point are skip-nesting IF operations
// placed here so that they may be skipped as a group
// see skipIfBlock()
IF_TYPE_OPCODES, // do not move this...
OPCODE_IFRED,OPCODE_IFAMBER,OPCODE_IFGREEN,
OPCODE_IFGTE,OPCODE_IFLT,
OPCODE_IFTIMEOUT,
OPCODE_IF,OPCODE_IFNOT,
OPCODE_IFRANDOM,OPCODE_IFRESERVE,
OPCODE_IFCLOSED, OPCODE_IFTHROWN
};
// Flag bits for status of hardware and TPL
static const byte SECTION_FLAG = 0x80;
static const byte LATCH_FLAG = 0x40;
static const byte TASK_FLAG = 0x20;
static const byte SPARE_FLAG = 0x10;
static const byte SIGNAL_MASK = 0x0C;
static const byte SIGNAL_RED = 0x08;
static const byte SIGNAL_AMBER = 0x0C;
static const byte SIGNAL_GREEN = 0x04;
static const byte MAX_STACK_DEPTH=4;
static const short MAX_FLAGS=256;
#define FLAGOVERFLOW(x) x>=MAX_FLAGS
class LookList {
public:
LookList(int16_t size);
void add(int16_t lookup, int16_t result);
int16_t find(int16_t value);
private:
int16_t m_size;
int16_t m_loaded;
int16_t * m_lookupArray;
int16_t * m_resultArray;
};
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 createNewTask(int route, uint16_t cab);
static void turnoutEvent(int16_t id, bool closed);
static void activateEvent(int16_t addr, bool active);
static const int16_t SERVO_SIGNAL_FLAG=0x4000;
static const int16_t ACTIVE_HIGH_SIGNAL_FLAG=0x2000;
static const int16_t SIGNAL_ID_MASK=0x0FFF;
// Throttle Info Access functions built by exrail macros
static const byte rosterNameCount;
static const int16_t FLASH routeIdList[];
static const int16_t FLASH automationIdList[];
static const int16_t FLASH rosterIdList[];
static const FSH * getRouteDescription(int16_t id);
static char getRouteType(int16_t id);
static const FSH * getTurnoutDescription(int16_t id);
static const FSH * getRosterName(int16_t id);
static const FSH * getRosterFunctions(int16_t id);
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 int16_t progtrackLocoId;
static void doSignal(VPIN id,char rag);
static bool isSignal(VPIN id,char rag);
static int16_t getSignalSlot(VPIN id);
static void setTurnoutHiddenState(Turnout * t);
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 const FLASH int16_t SignalDefinitions[];
static byte flags[MAX_FLAGS];
static LookList * sequenceLookup;
static LookList * onThrowLookup;
static LookList * onCloseLookup;
static LookList * onActivateLookup;
static LookList * onDeactivateLookup;
// 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 delayTime;
union {
unsigned long waitAfter; // Used by OPCODE_AFTER
unsigned long timeoutStart; // Used by OPCODE_ATTIMEOUT
};
bool timeoutFlag;
byte taskId;
uint16_t loco;
bool forward;
bool invert;
byte speedo;
int16_t onTurnoutId;
int16_t onActivateAddr;
byte stackDepth;
int callStack[MAX_STACK_DEPTH];
};
#endif

230
EXRAIL2MacroReset.h Normal file
View File

@@ -0,0 +1,230 @@
/*
* © 2021-2022 Chris Harlow
* © 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/>.
*/
// This file cleans and resets the RMFT2 Macros.
// It is used between passes to reduce complexity in RMFT2Macros.h
// DO NOT add an include guard to this file.
// Undefine all RMFT macros
#undef ACTIVATE
#undef ACTIVATEL
#undef AFTER
#undef ALIAS
#undef AMBER
#undef AT
#undef ATGTE
#undef ATLT
#undef ATTIMEOUT
#undef AUTOMATION
#undef AUTOSTART
#undef BROADCAST
#undef CALL
#undef CLOSE
#undef DEACTIVATE
#undef DEACTIVATEL
#undef DELAY
#undef DELAYMINS
#undef DELAYRANDOM
#undef DONE
#undef DRIVE
#undef ELSE
#undef ENDEXRAIL
#undef ENDIF
#undef ENDTASK
#undef ESTOP
#undef EXRAIL
#undef FADE
#undef FOFF
#undef FOLLOW
#undef FON
#undef FORGET
#undef FREE
#undef FWD
#undef GREEN
#undef IF
#undef IFAMBER
#undef IFCLOSED
#undef IFGREEN
#undef IFGTE
#undef IFLT
#undef IFNOT
#undef IFRANDOM
#undef IFRED
#undef IFRESERVE
#undef IFTHROWN
#undef IFTIMEOUT
#undef INVERT_DIRECTION
#undef JOIN
#undef KILLALL
#undef LATCH
#undef LCD
#undef LCN
#undef ONACTIVATE
#undef ONACTIVATEL
#undef ONDEACTIVATE
#undef ONDEACTIVATEL
#undef ONCLOSE
#undef ONTHROW
#undef PARSE
#undef PAUSE
#undef PIN_TURNOUT
#undef PRINT
#undef POM
#undef POWEROFF
#undef POWERON
#undef READ_LOCO
#undef RED
#undef RESERVE
#undef RESET
#undef RESUME
#undef RETURN
#undef REV
#undef ROSTER
#undef ROUTE
#undef SENDLOCO
#undef SEQUENCE
#undef SERIAL
#undef SERIAL1
#undef SERIAL2
#undef SERIAL3
#undef SERVO
#undef SERVO2
#undef SERVO_TURNOUT
#undef SERVO_SIGNAL
#undef SET
#undef SET_TRACK
#undef SETLOCO
#undef SIGNAL
#undef SIGNALH
#undef SPEED
#undef START
#undef STOP
#undef THROW
#undef TURNOUT
#undef UNJOIN
#undef UNLATCH
#undef VIRTUAL_TURNOUT
#undef WAITFOR
#undef XFOFF
#undef XFON
#ifndef RMFT2_UNDEF_ONLY
#define ACTIVATE(addr,subaddr)
#define ACTIVATEL(addr)
#define AFTER(sensor_id)
#define ALIAS(name,value...)
#define AMBER(signal_id)
#define AT(sensor_id)
#define ATGTE(sensor_id,value)
#define ATLT(sensor_id,value)
#define ATTIMEOUT(sensor_id,timeout_ms)
#define AUTOMATION(id,description)
#define AUTOSTART
#define BROADCAST(msg)
#define CALL(route)
#define CLOSE(id)
#define DEACTIVATE(addr,subaddr)
#define DEACTIVATEL(addr)
#define DELAY(mindelay)
#define DELAYMINS(mindelay)
#define DELAYRANDOM(mindelay,maxdelay)
#define DONE
#define DRIVE(analogpin)
#define ELSE
#define ENDEXRAIL
#define ENDIF
#define ENDTASK
#define ESTOP
#define EXRAIL
#define FADE(pin,value,ms)
#define FOFF(func)
#define FOLLOW(route)
#define FON(func)
#define FORGET
#define FREE(blockid)
#define FWD(speed)
#define GREEN(signal_id)
#define IF(sensor_id)
#define IFAMBER(signal_id)
#define IFCLOSED(turnout_id)
#define IFGREEN(signal_id)
#define IFGTE(sensor_id,value)
#define IFLT(sensor_id,value)
#define IFNOT(sensor_id)
#define IFRANDOM(percent)
#define IFRED(signal_id)
#define IFTHROWN(turnout_id)
#define IFRESERVE(block)
#define IFTIMEOUT
#define INVERT_DIRECTION
#define JOIN
#define KILLALL
#define LATCH(sensor_id)
#define LCD(row,msg)
#define LCN(msg)
#define ONACTIVATE(addr,subaddr)
#define ONACTIVATEL(linear)
#define ONDEACTIVATE(addr,subaddr)
#define ONDEACTIVATEL(linear)
#define ONCLOSE(turnout_id)
#define ONTHROW(turnout_id)
#define PAUSE
#define PIN_TURNOUT(id,pin,description...)
#define PRINT(msg)
#define PARSE(msg)
#define POM(cv,value)
#define POWEROFF
#define POWERON
#define READ_LOCO
#define RED(signal_id)
#define RESERVE(blockid)
#define RESET(pin)
#define RESUME
#define RETURN
#define REV(speed)
#define ROUTE(id,description)
#define ROSTER(cab,name,funcmap...)
#define SENDLOCO(cab,route)
#define SEQUENCE(id)
#define SERIAL(msg)
#define SERIAL1(msg)
#define SERIAL2(msg)
#define SERIAL3(msg)
#define SERVO(id,position,profile)
#define SERVO2(id,position,duration)
#define SERVO_SIGNAL(vpin,redpos,amberpos,greenpos)
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...)
#define SET(pin)
#define SET_TRACK(track,mode)
#define SETLOCO(loco)
#define SIGNAL(redpin,amberpin,greenpin)
#define SIGNALH(redpin,amberpin,greenpin)
#define SPEED(speed)
#define START(route)
#define STOP
#define THROW(id)
#define TURNOUT(id,addr,subaddr,description...)
#define UNJOIN
#define UNLATCH(sensor_id)
#define VIRTUAL_TURNOUT(id,description...)
#define WAITFOR(pin)
#define XFOFF(cab,func)
#define XFON(cab,func)
#endif

315
EXRAILMacros.h Normal file
View File

@@ -0,0 +1,315 @@
/*
* © 2021 Neil McKechnie
* © 2020-2022 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 EXRAILMacros_H
#define EXRAILMacros_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 including 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
// helper macro for turnout descriptions, creates NULL for missing description
#define O_DESC(id, desc) case id: return ("" desc)[0]?F("" desc):NULL;
// helper macro for turnout description as HIDDEN
#define HIDDEN "\x01"
// Pass 1 Implements aliases
#include "EXRAIL2MacroReset.h"
#undef ALIAS
#define ALIAS(name,value...) const int name= 1##value##0 ==10 ? -__COUNTER__ : value##0/10;
#include "myAutomation.h"
// Pass 2 create throttle route list
#include "EXRAIL2MacroReset.h"
#undef ROUTE
#define ROUTE(id, description) id,
const int16_t FLASH RMFT2::routeIdList[]= {
#include "myAutomation.h"
0};
// Pass 2a create throttle automation list
#include "EXRAIL2MacroReset.h"
#undef AUTOMATION
#define AUTOMATION(id, description) id,
const int16_t FLASH RMFT2::automationIdList[]= {
#include "myAutomation.h"
0};
// Pass 3 Create route descriptions:
#undef ROUTE
#define ROUTE(id, description) case id: return F(description);
#undef AUTOMATION
#define AUTOMATION(id, description) case id: return F(description);
const FSH * RMFT2::getRouteDescription(int16_t id) {
switch(id) {
#include "myAutomation.h"
default: break;
}
return F("");
}
// Pass 4... Create Text sending functions
#include "EXRAIL2MacroReset.h"
const int StringMacroTracker1=__COUNTER__;
#undef BROADCAST
#define BROADCAST(msg) case (__COUNTER__ - StringMacroTracker1) : CommandDistributor::broadcastText(F(msg));break;
#undef PARSE
#define PARSE(msg) case (__COUNTER__ - StringMacroTracker1) : DCCEXParser::parse(F(msg));break;
#undef PRINT
#define PRINT(msg) case (__COUNTER__ - StringMacroTracker1) : printMessage2(F(msg));break;
#undef LCN
#define LCN(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&LCN_SERIAL,F(msg));break;
#undef SERIAL
#define SERIAL(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial,F(msg));break;
#undef SERIAL1
#define SERIAL1(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial1,F(msg));break;
#undef SERIAL2
#define SERIAL2(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial2,F(msg));break;
#undef SERIAL3
#define SERIAL3(msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::send(&Serial3,F(msg));break;
#undef LCD
#define LCD(id,msg) case (__COUNTER__ - StringMacroTracker1) : StringFormatter::lcd(id,F(msg));break;
void RMFT2::printMessage(uint16_t id) {
switch(id) {
#include "myAutomation.h"
default: break ;
}
}
// Pass 5: Turnout descriptions (optional)
#include "EXRAIL2MacroReset.h"
#undef TURNOUT
#define TURNOUT(id,addr,subaddr,description...) O_DESC(id,description)
#undef PIN_TURNOUT
#define PIN_TURNOUT(id,pin,description...) O_DESC(id,description)
#undef SERVO_TURNOUT
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) O_DESC(id,description)
#undef VIRTUAL_TURNOUT
#define VIRTUAL_TURNOUT(id,description...) O_DESC(id,description)
const FSH * RMFT2::getTurnoutDescription(int16_t turnoutid) {
switch (turnoutid) {
#include "myAutomation.h"
default:break;
}
return NULL;
}
// Pass 6: Roster IDs (count)
#include "EXRAIL2MacroReset.h"
#undef ROSTER
#define ROSTER(cabid,name,funcmap...) +1
const byte RMFT2::rosterNameCount=0
#include "myAutomation.h"
;
// Pass 6: Roster IDs
#include "EXRAIL2MacroReset.h"
#undef ROSTER
#define ROSTER(cabid,name,funcmap...) cabid,
const int16_t FLASH RMFT2::rosterIdList[]={
#include "myAutomation.h"
0};
// Pass 7: Roster names getter
#include "EXRAIL2MacroReset.h"
#undef ROSTER
#define ROSTER(cabid,name,funcmap...) case cabid: return F(name);
const FSH * RMFT2::getRosterName(int16_t id) {
switch(id) {
#include "myAutomation.h"
default: break;
}
return F("");
}
// Pass to get roster functions
#undef ROSTER
#define ROSTER(cabid,name,funcmap...) case cabid: return F("" funcmap);
const FSH * RMFT2::getRosterFunctions(int16_t id) {
switch(id) {
#include "myAutomation.h"
default: break;
}
return F("");
}
// Pass 8 Signal definitions
#include "EXRAIL2MacroReset.h"
#undef SIGNAL
#define SIGNAL(redpin,amberpin,greenpin) redpin,redpin,amberpin,greenpin,
#undef SIGNALH
#define SIGNALH(redpin,amberpin,greenpin) redpin | RMFT2::ACTIVE_HIGH_SIGNAL_FLAG,redpin,amberpin,greenpin,
#undef SERVO_SIGNAL
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) vpin | RMFT2::SERVO_SIGNAL_FLAG,redval,amberval,greenval,
const FLASH int16_t RMFT2::SignalDefinitions[] = {
#include "myAutomation.h"
0,0,0,0 };
// Last Pass : create main routes table
// Only undef the macros, not dummy them.
#define RMFT2_UNDEF_ONLY
#include "EXRAIL2MacroReset.h"
// Define internal helper macros.
// Everything we generate here has to be compile-time evaluated to
// a constant.
#define V(val) (byte)(((int16_t)(val))&0x00FF),(byte)(((int16_t)(val)>>8)&0x00FF)
// Define macros for route code creation
#define ACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1 | 1),
#define ACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1 | 1),
#define AFTER(sensor_id) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id),
#define ALIAS(name,value...)
#define AMBER(signal_id) OPCODE_AMBER,V(signal_id),
#define AT(sensor_id) OPCODE_AT,V(sensor_id),
#define ATGTE(sensor_id,value) OPCODE_ATGTE,V(sensor_id),OPCODE_PAD,V(value),
#define ATLT(sensor_id,value) OPCODE_ATLT,V(sensor_id),OPCODE_PAD,V(value),
#define ATTIMEOUT(sensor_id,timeout) OPCODE_ATTIMEOUT1,0,0,OPCODE_ATTIMEOUT2,V(sensor_id),OPCODE_PAD,V(timeout/100L),
#define AUTOMATION(id, description) OPCODE_AUTOMATION, V(id),
#define AUTOSTART OPCODE_AUTOSTART,0,0,
#define BROADCAST(msg) PRINT(msg)
#define CALL(route) OPCODE_CALL,V(route),
#define CLOSE(id) OPCODE_CLOSE,V(id),
#define DEACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1),
#define DEACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1),
#define DELAY(ms) ms<30000?OPCODE_DELAYMS:OPCODE_DELAY,V(ms/(ms<30000?1L:100L)),
#define DELAYMINS(mindelay) OPCODE_DELAYMINS,V(mindelay),
#define DELAYRANDOM(mindelay,maxdelay) DELAY(mindelay) OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L),
#define DONE OPCODE_ENDTASK,0,0,
#define DRIVE(analogpin) OPCODE_DRIVE,V(analogpin),
#define ELSE OPCODE_ELSE,0,0,
#define ENDEXRAIL
#define ENDIF OPCODE_ENDIF,0,0,
#define ENDTASK OPCODE_ENDTASK,0,0,
#define ESTOP OPCODE_SPEED,V(1),
#define EXRAIL
#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 FORGET OPCODE_FORGET,0,0,
#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 IFAMBER(signal_id) OPCODE_IFAMBER,V(signal_id),
#define IFCLOSED(turnout_id) OPCODE_IFCLOSED,V(turnout_id),
#define IFGREEN(signal_id) OPCODE_IFGREEN,V(signal_id),
#define IFGTE(sensor_id,value) OPCODE_IFGTE,V(sensor_id),OPCODE_PAD,V(value),
#define IFLT(sensor_id,value) OPCODE_IFLT,V(sensor_id),OPCODE_PAD,V(value),
#define IFNOT(sensor_id) OPCODE_IFNOT,V(sensor_id),
#define IFRANDOM(percent) OPCODE_IFRANDOM,V(percent),
#define IFRED(signal_id) OPCODE_IFRED,V(signal_id),
#define IFRESERVE(block) OPCODE_IFRESERVE,V(block),
#define IFTHROWN(turnout_id) OPCODE_IFTHROWN,V(turnout_id),
#define IFTIMEOUT OPCODE_IFTIMEOUT,0,0,
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0,
#define JOIN OPCODE_JOIN,0,0,
#define KILLALL OPCODE_KILLALL,0,0,
#define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id),
#define LCD(id,msg) PRINT(msg)
#define LCN(msg) PRINT(msg)
#define ONACTIVATE(addr,subaddr) OPCODE_ONACTIVATE,V(addr<<2|subaddr),
#define ONACTIVATEL(linear) OPCODE_ONACTIVATE,V(linear+3),
#define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id),
#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
#define PAUSE OPCODE_PAUSE,0,0,
#define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
#define POWEROFF OPCODE_POWEROFF,0,0,
#define POWERON OPCODE_POWERON,0,0,
#define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2),
#define PARSE(msg) PRINT(msg)
#define READ_LOCO OPCODE_READ_LOCO1,0,0,OPCODE_READ_LOCO2,0,0,
#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,0,0,
#define RETURN OPCODE_RETURN,0,0,
#define REV(speed) OPCODE_REV,V(speed),
#define ROSTER(cabid,name,funcmap...)
#define ROUTE(id, description) OPCODE_ROUTE, V(id),
#define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route),
#define SEQUENCE(id) OPCODE_SEQUENCE, V(id),
#define SERIAL(msg) PRINT(msg)
#define SERIAL1(msg) PRINT(msg)
#define SERIAL2(msg) PRINT(msg)
#define SERIAL3(msg) PRINT(msg)
#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 SERVO_SIGNAL(vpin,redpos,amberpos,greenpos)
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile),
#define SET(pin) OPCODE_SET,V(pin),
#define SET_TRACK(track,mode) OPCODE_SET_TRACK,V(TRACK_MODE_##mode <<8 | TRACK_NUMBER_##track),
#define SETLOCO(loco) OPCODE_SETLOCO,V(loco),
#define SIGNAL(redpin,amberpin,greenpin)
#define SIGNALH(redpin,amberpin,greenpin)
#define SPEED(speed) OPCODE_SPEED,V(speed),
#define START(route) OPCODE_START,V(route),
#define STOP OPCODE_SPEED,V(0),
#define THROW(id) OPCODE_THROW,V(id),
#define TURNOUT(id,addr,subaddr,description...) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr),
#define UNJOIN OPCODE_UNJOIN,0,0,
#define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id),
#define VIRTUAL_TURNOUT(id,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(0),
#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),
// Build RouteCode
const int StringMacroTracker2=__COUNTER__;
const FLASH byte RMFT2::RouteCode[] = {
#include "myAutomation.h"
OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 };
// Restore normal code LCD & SERIAL macro
#undef LCD
#define LCD StringFormatter::lcd
#undef SERIAL
#define SERIAL 0x0
#endif

View File

@@ -1,5 +1,9 @@
/*
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020 Gregor Baues
* All rights reserved.
*
* This file is part of DCC-EX/CommandStation-EX
*
@@ -17,12 +21,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"
@@ -176,6 +174,7 @@ void EthernetInterface::loop()
for (int socket = 0; socket<MAX_SOCK_NUM; socket++) {
if (clients[socket] && !clients[socket].connected()) {
clients[socket].stop();
CommandDistributor::forget(socket);
if (Diag::ETHERNET) DIAG(F("Ethernet: disconnect %d "), socket);
}
}

View File

@@ -1,5 +1,10 @@
/*
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Chris Harlow
* © 2020 Gregor Baues
* All rights reserved.
*
* This file is part of DCC-EX/CommandStation-EX
*
@@ -22,15 +27,11 @@
#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>
//#include <avr/pgmspace.h>
#if defined (ARDUINO_TEENSY41)
#include <NativeEthernet.h> //TEENSY Ethernet Treiber
#include <NativeEthernetUdp.h>

5
FSH.h
View File

@@ -1,5 +1,8 @@
/*
* (c) 2021 Fred Decker. All rights reserved.
* © 2021 Neil McKechnie
* © 2021 Harald Barth
* © 2021 Fred Decker
* All rights reserved.
*
* This file is part of CommandStation-EX
*

View File

@@ -1 +1 @@
#define GITHUB_SHA "e3cbaf5"
#define GITHUB_SHA "TM-PORTX-20220613"

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.
* The read/write/write_P functions return I2C_STATUS_OK always, and the
* completion status of the operation is in the request block, as for
* the non-blocking version.
***************************************************************************/
void I2CManagerClass::queueRequest(I2CRB *req) {
switch (req->operation) {
case OPERATION_READ:
read(req->i2cAddress, req->readBuffer, req->readLen, NULL, 0, req);
break;
case OPERATION_SEND:
write(req->i2cAddress, req->writeBuffer, req->writeLen, req);
break;
case OPERATION_SEND_P:
write_P(req->i2cAddress, req->writeBuffer, req->writeLen, req);
break;
case OPERATION_REQUEST:
read(req->i2cAddress, req->readBuffer, req->readLen, req->writeBuffer, req->writeLen, req);
break;
}
}
/***************************************************************************
* Loop function, for general background work
***************************************************************************/
void I2CManagerClass::loop() {}
// Loop function
void I2CManagerClass::checkForTimeout() {}
#endif

506
IODevice.cpp Normal file
View File

@@ -0,0 +1,506 @@
/*
* © 2021 Neil McKechnie
* © 2021 Harald Barth
* 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

68
IO_DCCAccessory.cpp Normal file
View File

@@ -0,0 +1,68 @@
/*
* © 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;
#if defined(HAL_ACCESSORY_COMMAND_REVERSE)
state = !state;
#ifdef DIAG_IO
DIAG(F("DCC Write Linear Address:%d State:%d (inverted)"), packedAddress, state);
#endif
#else
#ifdef DIAG_IO
DIAG(F("DCC Write Linear Address:%d State:%d"), packedAddress, state);
#endif
#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

253
IO_GPIOBase.h Normal file
View File

@@ -0,0 +1,253 @@
/*
* © 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;
#if defined(ARDUINO_ARCH_ESP32)
// workaround: Has somehow no min function for all types
static inline T min(T a, int b) { return a < b ? a : b; };
#endif
};
// 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

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

276
IO_PCA9685.cpp Normal file
View File

@@ -0,0 +1,276 @@
/*
* © 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) {
#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 {
/* simulate digital pin on PWM */
_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0);
}
}
// 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) {
#ifdef DIAG_IO
DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"),
vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F(""));
#endif
if (_deviceState == DEVSTATE_FAILED) return;
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 = 4095;
s->inactivePosition = 0;
s->currentPosition = value;
s->profile = Instant | NoPowerOff; // 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

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

@@ -74,6 +74,10 @@ 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) {

View File

@@ -19,15 +19,12 @@
#ifndef LCDDisplay_h
#define LCDDisplay_h
#include <Arduino.h>
#include "defines.h"
#include "DisplayInterface.h"
#if __has_include ( "config.h")
#include "config.h"
#endif
// 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)
@@ -39,17 +36,18 @@
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
// 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;
size_t write(uint8_t b);
size_t write(uint8_t b) override;
protected:
uint8_t lcdRows;
@@ -63,6 +61,7 @@ protected:
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;

View File

@@ -1,33 +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/>.
*/
#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(); }

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() {}

16
LCN.cpp
View File

@@ -48,18 +48,20 @@ 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;
Turnout::turnoutlistHash++; // signals ED update of turnout data
if (!Turnout::exists(id)) LCNTurnout::create(id);
Turnout::setClosedStateOnly(id,ch=='t');
id = 0;
}
else if (ch == 'y' || ch == 'Y') { // Turnout opcodes
if (Diag::LCN) DIAG(F("LCN IN %d%c"),id,(char)ch);
Turnout::setClosed(id,ch=='y');
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

4
LCN.h
View File

@@ -1,5 +1,7 @@
/*
* (c) 2021 Fred Decker. All rights reserved.
* © 2021 Harald Barth
* © 2021 Fred Decker
* All rights reserved.
*
* This file is part of CommandStation-EX
*

View File

@@ -119,7 +119,7 @@ void LiquidCrystal_I2C::clearNative() {
void LiquidCrystal_I2C::setRowNative(byte row) {
int row_offsets[] = {0x00, 0x40, 0x14, 0x54};
if (row > lcdRows) {
if (row >= lcdRows) {
row = lcdRows - 1; // we count rows starting w/0
}
command(LCD_SETDDRAMADDR | (row_offsets[row]));
@@ -196,7 +196,7 @@ void LiquidCrystal_I2C::send(uint8_t value, uint8_t mode) {
outputBuffer[len++] = highnib;
outputBuffer[len++] = lownib|En;
outputBuffer[len++] = lownib;
I2CManager.write(_Addr, outputBuffer, len);
I2CManager.write(_Addr, outputBuffer, len); // Write command synchronously
}
// write 4 data bits to the HD44780 LCD controller.
@@ -208,12 +208,12 @@ void LiquidCrystal_I2C::write4bits(uint8_t value) {
uint8_t len = 0;
outputBuffer[len++] = _data|En;
outputBuffer[len++] = _data;
I2CManager.write(_Addr, outputBuffer, len);
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) {
outputBuffer[0] = value | _backlightval;
I2CManager.write(_Addr, outputBuffer, 1);
I2CManager.write(_Addr, outputBuffer, 1); // Write command synchronously
}

View File

@@ -66,19 +66,17 @@ class LiquidCrystal_I2C : public LCDDisplay {
public:
LiquidCrystal_I2C(uint8_t lcd_Addr,uint8_t lcd_cols,uint8_t lcd_rows);
void begin();
void clearNative();
void setRowNative(byte line);
size_t writeNative(uint8_t c);
void clearNative() override;
void setRowNative(byte line) override;
size_t writeNative(uint8_t c) override;
void display();
void noBacklight();
void backlight();
void command(uint8_t);
void init();
private:
void init_priv();
void send(uint8_t, uint8_t);
void write4bits(uint8_t);
void expanderWrite(uint8_t);
@@ -89,6 +87,8 @@ private:
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,7 +1,11 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2022 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
* 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
@@ -21,24 +25,38 @@
#include "DCCTimer.h"
#include "DIAG.h"
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
#define isHIGH(fastpin) (*fastpin.inout & fastpin.maskHIGH)
#define isLOW(fastpin) (!isHIGH(fastpin))
bool MotorDriver::usePWM=false;
bool MotorDriver::commonFaultPin=false;
MotorDriver::MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin,
volatile portreg_t shadowPORTA;
volatile portreg_t shadowPORTB;
volatile portreg_t shadowPORTC;
MotorDriver::MotorDriver(VPIN power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin,
byte current_pin, float sense_factor, unsigned int trip_milliamps, byte fault_pin) {
powerPin=power_pin;
getFastPin(F("POWER"),powerPin,fastPowerPin);
pinMode(powerPin, OUTPUT);
IODevice::write(powerPin,LOW);// set to OUTPUT and off
signalPin=signal_pin;
getFastPin(F("SIG"),signalPin,fastSignalPin);
pinMode(signalPin, OUTPUT);
fastSignalPin.shadowinout = NULL;
if (HAVE_PORTA(fastSignalPin.inout == &PORTA)) {
DIAG(F("Found PORTA pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTA;
}
if (HAVE_PORTB(fastSignalPin.inout == &PORTB)) {
DIAG(F("Found PORTB pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTB;
}
if (HAVE_PORTC(fastSignalPin.inout == &PORTC)) {
DIAG(F("Found PORTC pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTC;
}
signalPin2=signal_pin2;
if (signalPin2!=UNUSED_PIN) {
dualSignal=true;
@@ -52,8 +70,9 @@ MotorDriver::MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8
invertBrake=brake_pin < 0;
brakePin=invertBrake ? 0-brake_pin : brake_pin;
getFastPin(F("BRAKE"),brakePin,fastBrakePin);
// if brake is used for railcom cutout we need to do PORTX register trick here as well
pinMode(brakePin, OUTPUT);
setBrake(false);
setBrake(true); // start with brake on in case we hace DC stuff going on
}
else brakePin=UNUSED_PIN;
@@ -69,15 +88,28 @@ MotorDriver::MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8
pinMode(faultPin, INPUT);
}
senseFactor=sense_factor;
// This conversion performed at compile time so the remainder of the code never needs
// float calculations or libraray code.
senseFactorInternal=sense_factor * senseScale;
tripMilliamps=trip_milliamps;
rawCurrentTripValue=(int)(trip_milliamps / sense_factor);
rawCurrentTripValue=mA2raw(trip_milliamps);
if (currentPin==UNUSED_PIN)
DIAG(F("MotorDriver ** WARNING ** No current or short detection"));
else
DIAG(F("MotorDriver currentPin=A%d, senseOffset=%d, rawCurentTripValue(relative to offset)=%d"),
else {
DIAG(F("MotorDriver currentPin=A%d, senseOffset=%d, rawCurrentTripValue(relative to offset)=%d"),
currentPin-A0, senseOffset,rawCurrentTripValue);
// self testing diagnostic for the non-float converters... may be removed when happy
// DIAG(F("senseFactorInternal=%d raw2mA(1000)=%d mA2Raw(1000)=%d"),
// senseFactorInternal, raw2mA(1000),mA2raw(1000));
}
// prepare values for current detection
sampleDelay = 0;
lastSampleTaken = millis();
progTripValue = mA2raw(TRIP_CURRENT_PROG);
}
bool MotorDriver::isPWMCapable() {
@@ -85,15 +117,15 @@ bool MotorDriver::isPWMCapable() {
}
void MotorDriver::setPower(bool on) {
void MotorDriver::setPower(POWERMODE mode) {
bool on=mode==POWERMODE::ON;
if (on) {
// toggle brake before turning power on - resets overcurrent error
// on the Pololu board if brake is wired to ^D2.
setBrake(true);
setBrake(false);
setHIGH(fastPowerPin);
IODevice::write(powerPin,HIGH);
if (resetsCounterP != NULL)
*resetsCounterP = 0;
}
else setLOW(fastPowerPin);
else IODevice::write(powerPin,LOW);
powerMode=mode;
}
// setBrake applies brake if on == true. So to get
@@ -110,26 +142,6 @@ void MotorDriver::setBrake(bool on) {
else setLOW(fastBrakePin);
}
void MotorDriver::setSignal( bool high) {
if (usePWM) {
DCCTimer::setPWM(signalPin,high);
}
else {
if (high) {
setHIGH(fastSignalPin);
if (dualSignal) setLOW(fastSignalPin2);
}
else {
setLOW(fastSignalPin);
if (dualSignal) setHIGH(fastSignalPin2);
}
}
}
#if defined(ARDUINO_TEENSY32) || defined(ARDUINO_TEENSY35)|| defined(ARDUINO_TEENSY36)
volatile unsigned int overflow_count=0;
#endif
bool MotorDriver::canMeasureCurrent() {
return currentPin!=UNUSED_PIN;
}
@@ -144,34 +156,76 @@ bool MotorDriver::canMeasureCurrent() {
int MotorDriver::getCurrentRaw() {
if (currentPin==UNUSED_PIN) return 0;
int current;
#if defined(ARDUINO_TEENSY40) || defined(ARDUINO_TEENSY41)
bool irq = disableInterrupts();
// This function should NOT be called in an interruot so we
// dont need to fart about saving and restoring CPU specific
// interrupt registers.
noInterrupts();
current = analogRead(currentPin)-senseOffset;
enableInterrupts(irq);
#elif defined(ARDUINO_TEENSY32) || defined(ARDUINO_TEENSY35)|| defined(ARDUINO_TEENSY36)
unsigned char sreg_backup;
sreg_backup = SREG; /* save interrupt enable/disable state */
cli();
current = analogRead(currentPin)-senseOffset;
overflow_count = 0;
SREG = sreg_backup; /* restore interrupt state */
#else
current = analogRead(currentPin)-senseOffset;
#endif
interrupts();
if (current<0) current=0-current;
if ((faultPin != UNUSED_PIN) && isLOW(fastFaultPin) && isHIGH(fastPowerPin))
if ((faultPin != UNUSED_PIN) && isLOW(fastFaultPin) && powerMode==POWERMODE::ON)
return (current == 0 ? -1 : -current);
return current;
// IMPORTANT: This function can be called in Interrupt() time within the 56uS timer
// The default analogRead takes ~100uS which is catastrphic
// so DCCTimer has set the sample time to be much faster.
}
unsigned int MotorDriver::raw2mA( int raw) {
return (unsigned int)(raw * senseFactor);
void MotorDriver::setDCSignal(byte speedcode) {
if (brakePin == UNUSED_PIN)
return;
// spedcoode is a dcc speed & direction
byte tSpeed=speedcode & 0x7F; // DCC Speed with 0,1 stop and speed steps 2 to 127
byte tDir=speedcode & 0x80;
byte brake;
if (tSpeed <= 1) brake = 255;
else if (tSpeed >= 127) brake = 0;
else brake = 2 * (128-tSpeed);
if (invertBrake)
brake=255-brake;
analogWrite(brakePin,brake);
// as the port registers can be shadowed to get syncronized DCC signals
// we need to take care of that and we have to turn off interrupts during
// that time as otherwise setDCCSignal() which is called from interrupt
// contect can undo whatever we do here.
if (fastSignalPin.shadowinout != NULL) {
if (HAVE_PORTA(fastSignalPin.shadowinout == &PORTA)) {
noInterrupts();
HAVE_PORTA(shadowPORTA=PORTA);
setSignal(tDir);
HAVE_PORTA(PORTA=shadowPORTA);
interrupts();
} else if (HAVE_PORTB(fastSignalPin.shadowinout == &PORTB)) {
noInterrupts();
HAVE_PORTB(shadowPORTB=PORTB);
setSignal(tDir);
HAVE_PORTB(PORTB=shadowPORTB);
interrupts();
} else if (HAVE_PORTC(fastSignalPin.shadowinout == &PORTC)) {
noInterrupts();
HAVE_PORTC(shadowPORTC=PORTC);
setSignal(tDir);
HAVE_PORTC(PORTC=shadowPORTC);
interrupts();
}
} else {
setSignal(tDir);
}
}
int MotorDriver::mA2raw( unsigned int mA) {
return (int)(mA / senseFactor);
int MotorDriver::getCurrentRawInInterrupt() {
// IMPORTANT: This function must be called in Interrupt() time within the 56uS timer
// The default analogRead takes ~100uS which is catastrphic
// so DCCTimer has set the sample time to be much faster.
if (currentPin==UNUSED_PIN) return 0;
return analogRead(currentPin)-senseOffset;
}
unsigned int MotorDriver::raw2mA( int raw) {
return (int32_t)raw * senseFactorInternal / senseScale;
}
unsigned int MotorDriver::mA2raw( unsigned int mA) {
return (int32_t)mA * senseScale / senseFactorInternal;
}
void MotorDriver::getFastPin(const FSH* type,int pin, bool input, FASTPIN & result) {
@@ -186,3 +240,65 @@ void MotorDriver::getFastPin(const FSH* type,int pin, bool input, FASTPIN & res
result.maskLOW = ~result.maskHIGH;
// DIAG(F(" port=0x%x, inoutpin=0x%x, isinput=%d, mask=0x%x"),port, result.inout,input,result.maskHIGH);
}
void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
if (millis() - lastSampleTaken < sampleDelay) return;
lastSampleTaken = millis();
int tripValue= useProgLimit?progTripValue:getRawCurrentTripValue();
// Trackname for diag messages later
switch (powerMode) {
case POWERMODE::OFF:
sampleDelay = POWER_SAMPLE_OFF_WAIT;
break;
case POWERMODE::ON:
// Check current
lastCurrent=getCurrentRaw();
if (lastCurrent < 0) {
// We have a fault pin condition to take care of
lastCurrent = -lastCurrent;
setPower(POWERMODE::OVERLOAD); // Turn off, decide later how fast to turn on again
if (commonFaultPin) {
if (lastCurrent <= tripValue) {
setPower(POWERMODE::ON); // maybe other track
}
// 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 %d"), trackno);
} else {
DIAG(F("TRACK %d FAULT PIN ACTIVE - OVERLOAD"), trackno);
if (lastCurrent < tripValue) {
lastCurrent = tripValue; // exaggerate
}
}
}
if (lastCurrent < tripValue) {
sampleDelay = POWER_SAMPLE_ON_WAIT;
if(power_good_counter<100)
power_good_counter++;
else
if (power_sample_overload_wait>POWER_SAMPLE_OVERLOAD_WAIT) power_sample_overload_wait=POWER_SAMPLE_OVERLOAD_WAIT;
} else {
setPower(POWERMODE::OVERLOAD);
unsigned int mA=raw2mA(lastCurrent);
unsigned int maxmA=raw2mA(tripValue);
power_good_counter=0;
sampleDelay = power_sample_overload_wait;
DIAG(F("TRACK %d POWER OVERLOAD current=%d max=%d offtime=%d"), trackno, mA, maxmA, sampleDelay);
if (power_sample_overload_wait >= 10000)
power_sample_overload_wait = 10000;
else
power_sample_overload_wait *= 2;
}
break;
case POWERMODE::OVERLOAD:
// Try setting it back on after the OVERLOAD_WAIT
setPower(POWERMODE::ON);
sampleDelay = POWER_SAMPLE_ON_WAIT;
// Debug code....
DIAG(F("TRACK %d POWER RESET delay=%d"), trackno, sampleDelay);
break;
default:
sampleDelay = 999; // cant get here..meaningless statement to avoid compiler warning.
}
}

View File

@@ -1,7 +1,11 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020 Chris Harlow
* © 2022 Harald Barth
* All rights reserved.
*
* This file is part of Asbelos DCC API
* 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
@@ -19,6 +23,38 @@
#ifndef MotorDriver_h
#define MotorDriver_h
#include "FSH.h"
#include "IODevice.h"
#include "DCCTimer.h"
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
#define isHIGH(fastpin) (*fastpin.inout & fastpin.maskHIGH)
#define isLOW(fastpin) (!isHIGH(fastpin))
#define TOKENPASTE(x, y) x ## y
#define TOKENPASTE2(x, y) TOKENPASTE(x, y)
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
#define HAVE_PORTA(X) X
#define HAVE_PORTB(X) X
#define HAVE_PORTC(X) X
#endif
#if defined(ARDUINO_AVR_UNO)
#define HAVE_PORTB(X) X
#endif
// if macros not defined as pass-through we define
// them here as someting that is valid as a
// statement and evaluates to false.
#ifndef HAVE_PORTA
#define HAVE_PORTA(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
#ifndef HAVE_PORTB
#define HAVE_PORTB(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
#ifndef HAVE_PORTC
#define HAVE_PORTC(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
// Virtualised Motor shield 1-track hardware Interface
@@ -26,63 +62,118 @@
#define UNUSED_PIN 127 // inside int8_t
#endif
#if defined(__IMXRT1062__)
struct FASTPIN {
volatile uint32_t *inout;
uint32_t maskHIGH;
uint32_t maskLOW;
};
#if defined(__IMXRT1062__) || defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
typedef uint32_t portreg_t;
#else
struct FASTPIN {
volatile uint8_t *inout;
uint8_t maskHIGH;
uint8_t maskLOW;
};
typedef uint8_t portreg_t;
#endif
struct FASTPIN {
volatile portreg_t *inout;
portreg_t maskHIGH;
portreg_t maskLOW;
volatile portreg_t *shadowinout;
};
// The port registers that are shadowing
// the real port registers. These are
// defined in Motordriver.cpp
extern volatile portreg_t shadowPORTA;
extern volatile portreg_t shadowPORTB;
extern volatile portreg_t shadowPORTC;
enum class POWERMODE : byte { OFF, ON, OVERLOAD };
class MotorDriver {
public:
MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin,
MotorDriver(VPIN power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin,
byte current_pin, float senseFactor, unsigned int tripMilliamps, byte faultPin);
virtual void setPower( bool on);
virtual void setSignal( bool high);
virtual void setPower( POWERMODE mode);
virtual POWERMODE getPower() { return powerMode;}
__attribute__((always_inline)) inline void setSignal( bool high) {
if (trackPWM) {
DCCTimer::setPWM(signalPin,high);
}
else {
if (high) {
setHIGH(fastSignalPin);
if (dualSignal) setLOW(fastSignalPin2);
}
else {
setLOW(fastSignalPin);
if (dualSignal) setHIGH(fastSignalPin2);
}
}
};
inline void enableSignal(bool on) {
if (on)
pinMode(signalPin, OUTPUT);
else
pinMode(signalPin, INPUT);
};
virtual void setBrake( bool on);
virtual void setDCSignal(byte speedByte);
virtual int getCurrentRaw();
virtual int getCurrentRawInInterrupt();
virtual unsigned int raw2mA( int raw);
virtual int mA2raw( unsigned int mA);
virtual unsigned int mA2raw( unsigned int mA);
inline bool brakeCanPWM() {
return ((brakePin!=UNUSED_PIN) && (digitalPinToTimer(brakePin)));
}
inline int getRawCurrentTripValue() {
return rawCurrentTripValue;
}
bool isPWMCapable();
bool canMeasureCurrent();
static bool usePWM;
bool trackPWM; // this track uses PWM timer to generate the DCC waveform
static bool commonFaultPin; // This is a stupid motor shield which has only a common fault pin for both outputs
inline byte getFaultPin() {
return faultPin;
}
inline void setResetCounterPointer(volatile byte *bp) { // load resetPacketCounter pointer
resetsCounterP = bp;
}
void checkPowerOverload(bool useProgLimit, byte trackno);
private:
volatile byte *resetsCounterP = NULL; // points to the resetPacketCounter if this is a prog track
void getFastPin(const FSH* type,int pin, bool input, FASTPIN & result);
void getFastPin(const FSH* type,int pin, FASTPIN & result) {
getFastPin(type, pin, 0, result);
}
byte powerPin, signalPin, signalPin2, currentPin, faultPin, brakePin;
FASTPIN fastPowerPin,fastSignalPin, fastSignalPin2, fastBrakePin,fastFaultPin;
VPIN powerPin;
byte signalPin, signalPin2, currentPin, faultPin, brakePin;
FASTPIN fastSignalPin, fastSignalPin2, fastBrakePin,fastFaultPin;
bool dualSignal; // true to use signalPin2
bool invertBrake; // brake pin passed as negative means pin is inverted
float senseFactor;
// Raw to milliamp conversion factors avoiding float data types.
// Milliamps=rawADCreading * sensefactorInternal / senseScale
//
// senseScale is chosen as 256 to give enough scale for 2 decimal place
// raw->mA conversion with an ultra fast optimised integer multiplication
int senseFactorInternal; // set to senseFactor * senseScale
static const int senseScale=256;
int senseOffset;
unsigned int tripMilliamps;
int rawCurrentTripValue;
#if defined(ARDUINO_TEENSY40) || defined(ARDUINO_TEENSY41)
static bool disableInterrupts() {
uint32_t primask;
__asm__ volatile("mrs %0, primask\n" : "=r" (primask)::);
__disable_irq();
return (primask == 0) ? true : false;
}
static void enableInterrupts(bool doit) {
if (doit) __enable_irq();
}
#endif
// current sampling
POWERMODE powerMode;
unsigned long lastSampleTaken;
unsigned int sampleDelay;
int progTripValue;
int lastCurrent;
int maxmA;
int tripmA;
// Wait times for power management. Unit: milliseconds
static const int POWER_SAMPLE_ON_WAIT = 100;
static const int POWER_SAMPLE_OFF_WAIT = 1000;
static const int POWER_SAMPLE_OVERLOAD_WAIT = 20;
// Trip current for programming track, 250mA. Change only if you really
// need to be non-NMRA-compliant because of decoders that are not either.
static const int TRIP_CURRENT_PROG=250;
unsigned long power_sample_overload_wait = POWER_SAMPLE_OVERLOAD_WAIT;
unsigned int power_good_counter = 0;
};
#endif

View File

@@ -1,4 +1,6 @@
/*
* © 2021 Fred Decker
* © 2020-2022 Harald Barth
* (c) 2020 Chris Harlow. All rights reserved.
* (c) 2021 Fred Decker. All rights reserved.
* (c) 2020 Harald Barth. All rights reserved.
@@ -45,8 +47,8 @@
//
// Arduino standard Motor Shield
#define STANDARD_MOTOR_SHIELD F("STANDARD_MOTOR_SHIELD"), \
new MotorDriver(3, 12, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 2000, UNUSED_PIN), \
new MotorDriver(11, 13, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 2000, UNUSED_PIN)
new MotorDriver(3, 12, UNUSED_PIN, 9, A0, 2.99, 2000, UNUSED_PIN), \
new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 2.99, 2000, UNUSED_PIN)
// Pololu Motor Shield
#define POLOLU_MOTOR_SHIELD F("POLOLU_MOTOR_SHIELD"), \
@@ -63,6 +65,13 @@
// new MotorDriver(2, 8, UNUSED_PIN, -10, A1, 18, 3000, 12)
// See Pololu dial_mc33926_shield_schematic.pdf and truth table on page 17 of the MC33926 data sheet.
// Pololu Dual TB9051FTG Motor Shield
// This is the shield without modifications which means
// no HA waveform and no RailCom on an Arduino Mega 2560
#define POLOLU_TB9051FTG F("POLOLU_TB9051FTG"), \
new MotorDriver(2, 7, UNUSED_PIN, -9, A0, 10, 2500, 6), \
new MotorDriver(4, 8, UNUSED_PIN, -10, A1, 10, 2500, 12)
// Firebox Mk1
#define FIREBOX_MK1 F("FIREBOX_MK1"), \
new MotorDriver(3, 6, 7, UNUSED_PIN, A5, 9.766, 5500, UNUSED_PIN), \
@@ -87,4 +96,22 @@
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)
// Makeblock ORION UNO like sized board with integrated motor driver
// This is like an Uno with H-bridge and RJ12 contacts instead of pin rows.
// No current sense. Barrel connector max 12V, Vmotor max 15V. 1.1A polyfuse as output protection.
// Main is marked M1 and near RJ12 #5
// Prog is marked M2 and near RJ12 #4
// For details see
// http://docs.makeblock.com/diy-platform/en/electronic-modules/main-control-boards/makeblock-orion.html
#define ORION_UNO_INTEGRATED_SHIELD F("ORION_UNO_INTEGRATED_SHIELD"), \
new MotorDriver(6, 7, UNUSED_PIN, UNUSED_PIN, UNUSED_PIN, 1.0, 1100, UNUSED_PIN), \
new MotorDriver(5, 4, UNUSED_PIN, UNUSED_PIN, UNUSED_PIN, 1.0, 1100, UNUSED_PIN)
// This is an example how to setup a motor shield definition for a motor shield connected
// to an NANO EVERY board. You have to make the connectons from the shield to the board
// as in this example or adjust the values yourself.
#define NANOEVERY_EXAMPLE F("NANOEVERY_EXAMPLE"), \
new MotorDriver(5, 6, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 2000, UNUSED_PIN),\
new MotorDriver(9, 10, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 2000, UNUSED_PIN)
#endif

View File

@@ -1,5 +1,9 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021 Neil McKechnie
* © 2021 Harald Barth
* © 2020-2021 Fred Decker
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -82,30 +86,48 @@ 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(uint16_t n){
Output *tt;
for(tt=firstOutput;tt!=NULL && tt->data.id!=n;tt=tt->nextOutput);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////
// 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;
@@ -125,52 +147,26 @@ bool Output::remove(uint16_t n){
}
///////////////////////////////////////////////////////////////////////////////
// Static function to load configuration and state of all Outputs from EEPROM
#ifndef DISABLE_EEPROM
void Output::load(){
struct BrokenOutputData bdata;
struct OutputData data;
Output *tt;
bool isBroken=1;
// This is a scary kluge. As we have two formats in EEPROM due to an
// earlier bug, we don't know which we encounter now. So we guess
// that if in all entries this byte has value of 7 or lower this is
// an iFlag and thus the broken format. Otherwise it would be a pin
// id. If someone uses only pins 0 to 7 of their arduino, they
// loose. This is (if you look at an arduino) however unlikely.
for(uint16_t i=0;i<EEStore::eeStore->data.nOutputs;i++){
EEPROM.get(EEStore::pointer()+ i*sizeof(struct BrokenOutputData),bdata);
if (bdata.iFlag > 7) { // it's a pin and not an iFlag!
isBroken=0;
break;
}
}
if ( isBroken ) {
for(uint16_t i=0;i<EEStore::eeStore->data.nOutputs;i++){
EEPROM.get(EEStore::pointer(),bdata);
tt=create(bdata.id,bdata.pin,bdata.iFlag);
tt->data.oStatus=bitRead(tt->data.iFlag,1)?bitRead(tt->data.iFlag,2):bdata.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();
EEStore::advance(sizeof(struct BrokenOutputData));
}
} else {
struct OutputData data;
EEPROM.get(EEStore::pointer(),data);
// 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);
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();
EEStore::advance(sizeof(struct OutputData));
}
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;
@@ -179,19 +175,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(uint16_t id, uint8_t pin, uint8_t iFlag, uint8_t 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;
@@ -204,20 +207,21 @@ Output *Output::create(uint16_t id, uint8_t pin, uint8_t iFlag, uint8_t 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

@@ -1,5 +1,8 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021 Harald Barth
* © 2021 Fred Decker
* © 2020 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -20,37 +23,45 @@
#define Outputs_h
#include <Arduino.h>
#include "IODevice.h"
struct OutputData {
uint8_t oStatus;
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;
uint8_t pin;
uint8_t iFlag;
VPIN pin;
};
struct BrokenOutputData {
uint8_t oStatus;
uint8_t id;
uint8_t pin;
uint8_t iFlag;
};
class Output{
public:
void activate(int s);
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(uint16_t, uint8_t, uint8_t, uint8_t=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; // EEPROM pointer (Chris has no idea what this is all about!)
uint16_t num; // EEPROM address of oStatus in OutputData struct, or zero if not stored.
}; // Output

View File

@@ -1,109 +0,0 @@
/*
* (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/>.
*/
/*!
* @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)
*
*/
#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

@@ -17,12 +17,12 @@ Both CommandStation-EX and BaseStation-Classic support much of the NMRA Digital
* 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 manu more custom features. see [What's new in CommandStation-EX?](#whats-new-in-commandstation-ex)
* And many more custom features. see [What's new in CommandStation-EX?](#whats-new-in-commandstation-ex)
# Whats in this Repository?
This repository, CommandStation-EX, contains a complete DCC++ EX Commmand Station sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano. All sketch files are in the folder named CommandStation-EX and its subforlders.
This repository, CommandStation-EX, contains a complete DCC++ EX Commmand Station sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
To utilize this sketch, you can use the following:
@@ -69,7 +69,7 @@ in config.h.
* 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).
NOTE: DCC-EX is a major rewrite to the code. We started over and rebuilt it from the ground up! For what that means, you can read [HERE](https://dcc-ex.com/about/rewrite.html).
# More information
You can learn more at the [DCC++ EX website](https://dcc-ex.com/)

View File

@@ -0,0 +1,75 @@
Throttle Assist updates for versiuon 4.?
Chris Harlow April 2022
There are a number of additional throttle information commands that have been implemented to assist throttle authors to obtain information from the Command Station in order to implement turnout, route/automation and roster features which are already found in the Withrottle implementations.
These commands are new and not overlapped with the existing commands which are probabaly due to be obsoleted as they are over complex and unfit for purpose.
Turnouts:
The conventional turnout definition commands and the ```<H>``` responses do not contain information about the turnout description which may have been provided in an EXRAIL script. A turnout description is much more user friendly than T123 and having a list helps the throttle UI build a suitable set of buttons.
```<JT>``` command returns a list of turnout ids. The throttle should be uninterested in the turnout technology used but needs to know the ids it can throw/close and monitor the current state.
e.g. response ```<jT 1 17 22 19>```
```<JT 17>`` requests info on turnout 17.
e.g. response ```<jT 17 T "Coal yard exit">``` or ```<jT 17 C "Coal yard exit">```
(T=thrown, C=closed)
or ```<jT 17 C "">``` indicating turnout description not given.
or ```<jT 17 X>``` indicating turnout unknown (or possibly hidden.)
Note: It is still the throttles responsibility to monitor the status broadcasts.
(TBD I'm thinking that the existing broadcast is messy and needs cleaning up)
However, I'm not keen on dynamically created/deleted turnouts so I have no intention of providing a command that indicates the turnout list has been updated since the throttle started.
Also note that turnouts marked in EXRAIL with the HIDDEN keyword instead of a "description" will NOT show up in these commands.
Automations/Routes
A throttle need to know which EXRAIL Automations and Routes it can show the user.
```<JA>``` Returns a list of Automations/Routes
e.g. ```<jA 13 16 23>```
Indicates route/automation ids.
Information on each route needs to be obtained by
```<JA 13>```
returns e.g. ```<jA 13 R "description">``` for a route
or ```<jA 13 A "description">``` for an automation.
or ```<jA 13 X>``` for id not found
Whats the difference:
A Route is just a call to an EXRAIL ROUTE, traditionally to set some turnouts or signals but can be used to perform any kind of EXRAIL function... but its not expecting to know the loco.
Thus a route can be triggered by sending in for example ```</START 13>```.
An Automation is a handoff of the last accessed loco id to an EXRAIL AUTOMATION which would typically drive the loco away.
Thus an Automation expects a start command with a cab id
e.g. ```</START 13 3>```
Roster Information:
The ```<JR>``` command requests a list of cab ids from the roster.
e.g. responding ```<jR 3 200 6336>```
or <jR> for none.
Each Roster entry had a name and function map obtained by:
```<JR 200>``` reply like ```<jR 200 "Thomas" "whistle/*bell/squeal/panic">
Refer to EXRAIL ROSTER command for function map format.
Obtaining throttle status.
```<t cabid>``` Requests a deliberate update on the cab speed/functions in the same format as the cab broadcast.
```<l cabid slot speedbyte functionMap>```
Note that a slot of -1 indicates that the cab is not in the reminders table and this comand will not reserve a slot until such time as the cab is throttled.
COMMANDS TO AVOID
```<f cab func1 func2>``` Use ```<F cab function 1/0>```
```<t slot cab speed dir>``` Just drop the slot number
```<T commands>``` other than ```<T id 0/1>```
```<s>```
```<c>```

View File

@@ -0,0 +1,139 @@
# DCC++EX Track Manager
Chris Harlow 2022/03/23
**If you are only interested in a standard setup using just a DCC track and PROG track, then you DO NOT need to read the rest of this document.**
What follows is for advanced users interested in managing power districts and/or running DC locomotives through DCC++EX.
## What is the Track Manager
Track Manger (TM from now on) is an integral part of DCC++EX software that is responsible for:
- Managing track power state.
- Monitoring track overloads and shorts.
- Routing the DCC main or prog track waveforms to the correct Motor Driver and thus track.
- Managing the JOIN feature.
- Intercepting throttle commands to locos running on DC tracks.
- Handling user or EXRAIL commands to switch track status.
In the default scenario of a single DCC track and a PROG track, the TM behaves as for the previous versions of DCC++EX so if thats what you want, you dont need to mess with it.
The TM is able to handle up to 8 separate track domains. Each domain requires a hardware driver to supply track voltage. A typical motor driver shield supplies two tracks, which is what we have used in the past as main and prog.
Unlike the previous version of DCC++EX, where the shield channel A was always the DCC main and channel B was always the DCC prog track, TM allows :
- None, any or all the tracks can be DCC Main.
- None or ONE track may be DCC prog at any given time.
- Any track may be powered on or off independently of the others.
- Any track may be disconnected from the DCC signal and used as a DC track with a given loco address. (See DC discussion later)
With such flexibility comes responsibility... the potential for making mistakes means taking extra care with your configuration!
**NOTE** TM does NOT use "zero stretching" to control your DC motor. Instead, it uses true Pulse Width Modulation (PWM) to efficiently run your loco using the same method a decoder uses to control a DCC loco's motor. DC locos can even run better on TM than they can on a normal analog throttle, especially at low speed, since it is always applying the full track voltage, albeit in pulses of varying duration.
## Using the Track Manager (DCC)
TM names the tracks A to H. In a default setup, you will normally have tracks A and B where A will default to be the DCC main signal and B will be the DCC prog.
There is a new user command `<=>` which is used to control the TM but the `<0>` and `<1>` commands operate as before.
- `<=>` lists the current track settings.
In a default setup this will normally return
```
<=A DCC>
<=B PROG>
```
- `<=t DCC>` sets track t (A..H) to use the DCC main track. For example `<=C DCC>` sets track C. All tracks that are set to DCC will receive the same DCC signal waveform.
- `<=t PROG>` Sets track t (A..H) to be the one and only PROG track. Any previous PROG track is turned off.
- `<=t OFF>` turns off the track t. It will not power on with `<1>` because it will not know what signal to send.
In an all-DCC environment it is unlikely that you will need to do anything other than setting any additional tracks (C...H) as DCC in your `mySetup.h` file.
Bear in mind that a track may actually be only connected to DCC accessories such as signals and turnouts... your layout, your choice.
Note that when setting a track to PROG or OFF, its power is switched off automatically. (The PROG track manages power on an as-needed basis under normal circumstances.
When setting a track to MAIN (or DC, DCX see later) the power is applied according to the most recent `<1>` or `<0>` command as being the most compatible with previous versions.
## using the Track Manager (DC)
TM allows any or all of your tracks to be individually selected as a DC track which responds to throttle commands on any given loco address. So for example if track A is set to DC address 55, then any throttle commands to loco 55 will be transmitted as DC onto track A and thus a DC loco can be driven along that track. almost exactly as if it was DCC.
Your throttle (JMRI, EX-Webthrottle, Withrottle, Engine Driver etc etc) do not know or care that this is a DC loco so nothing needs to change.
For a simple Command Station setup to run just two DC tracks instead of DCC, you only need to assign DC addresses to tracks A and B. If you want DCC on track A and DC on track B, you just need to set track B to a suitable DC address.
The command to set a track to a DC address is as follows
- `<=t DC a>` Sets track t (A..H) to use loco address a. e.g. <=A DC 3>
A simple 2 separate loop DC track, wired the traditional way in opposite directions, may be set like this to use loco addresses 1 and 2.
```
<=A DC 1>
<=A DC 2>
```
### Crossing between DC tracks
There are some slightly mind-bending issues to be addressed, especially if you want to be able to cross between two separate DC tracks or use your layout in DCC or DC mode. This is because the control of DC loco direction is relative to the TRACK and not the LOCO. (you turn a DC loco round on the track and it continues in the same geographical direction. You turn a DCC loco around and it continues to go forwards or backwards in the opposite geographical direction.)
Generally DC tracks are wired so that two mainline tracks are in opposite direction which makes operation easy BUT crossovers between tracks will cause shorts unless you have very complex switching arrangements.
This is generally incompatible with DCC wiring which expects to be able to cross between tracks with impunity because they are all wired with the same polarity.
To get over this issue TM allows the polarity of a DC track to be swapped so that tracks wired for DCC may be switched to DC with a polarity chosen at run time according to your operations. So, for example, you may have two loops with a crossing between them. Normally you need them in opposite directions, but when you need to drive over the crossing, you need to switch one or other track so that they are at the same polarity.
(This is a good case for using EXRAIL to help)
The command `<=t DCX a>` will set track t (A..H) to be DC but with reversed polarity compared with a track set to DC.
Its perfectly OK to cross between DC tracks by setting them to the same loco address and making sure you get the polarity right!
## Connecting Hardware
Each track requires hardware to control it
- Power on/off
- Polarity (direction, signal etc)
- Brake (shorts tracks together)
- Current (analog reading)
The standard motor shields provide this for two separate tracks and are predictable and easy to use. However STACKING shields is not a viable way of adding more tracks because it prevents the software from gaining access to the individual track pins. Similarly, wiring all the signal pins together for example, will give you a shared DCC signal but it will eliminate any possibility of switching the track purpose at run time. So, you are going to have to understand enough to wire track drivers to various pins if you wish to extend beyond 2 tracks and take advantage of TM.
You will also need to consider the implications of differing electronic implementations that would cause unexpected issues when a loco moves between tracks. We know this works fine for a typical shield because we use `<1 JOIN>` quite happily but this may be different if you mix hardware types..... (NOT MY PROBLEM !)
The easiest way to consider the wiring is to treat each track individually (either as a separate driver or as half of a shield).
You will require,for each track, on the Arduino:
- A GPIO pin (or a HAL vpin perhaps on an I2C extender, code TBA!!!) to switch power.
- A GPIO pin to switch the signal direction
- A GPIO pin with PWM capability to switch the Brake (you may omit this if you dont want any DC capability)
- Optionally An Analog pin to read the current (unless your hardware cant do that, perhaps its just feeding a booster)
- Optionally a GPIO fault pin if thats how your hardware works. (NOT recommended as you're going to run out of pins)
IF you have no more than 3 tracks and you can arrange for the signal pins to be one of 11,12,13 on a Mega, THEN there is a slight advantage internally and the waveform will be super-sharp.
**Hardware that has two signal pins still needs some code thought!!!!!!!!**
## Configuring the Software
Configuring the software to provide more tracks is a simple extension of the existing method of customising the #define of MOTOR_SHIELD_TYPE in config.h
Since there can be no standard setup of your wiring and hardware choices, it will be necessary to create your custom built MOTOR_SHIELD_TYPE in the manner described in MotorDrivers.h and simply continue to add more `new MotorDriver(` definitions to the list, providing all the pin numbers and electronic limits for each track. (or even shorten the list to 1)
## Using EXRAIL to control Track Manager
EXRAIL has a single additional command that can be used to automate TM.
- `SET_TRACK(t,mode)`
where t is the track letter A..H and mode is one of
- `OFF` track is switched off
- `DCC` track gets DCC signal
- `PROG` track gets DCC prog signal
- `DC` track is set to DC mode with the cab address of the currently executing EXRAIL sequence.
- `DCX` as DC but with reversed polarity.
DC/DCX are designed so that you can be automating a DCC loco, drive it onto a separate track and switch to DC without having to know the cab address. (e.g AUTOMATION)
If however you are just running a ROUTE you can always do something like this:
```
ROUTE(77,"Set track G to DC 123")
SETLOCO(123)
SET_TRACK(G,DC)
DONE
```
## Where and How for the Code.
The TM code is primarily in TrackManager.cpp which is responsible for coordinating the track settings and commands.
Each individual track is handled by an instance of MotorDriver created from the MOTOPR_SHIELD_TYPE definition in config.h
Many functions formerly in the DCCWaveform code have been moved to TrackManager or MotorDriver, notably the power control and checking. This makes the code easier to follow.

View File

@@ -0,0 +1,284 @@
Version 4.0 Release Notes
*************************
The DCC-EX Team is pleased to release CommandStation-EX-v4.0.0 as a Production Release. Release v4.0.0 is a Major release that adds significant new product design, plus Automation features and bug fixes. The team continues 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.2.0 to v3.2.0 rc13.
**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/v4.0.0-Prod/CommandStation-EX.zip)
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v0.0.0-Prod/CommandStation-EX.tar.gz)
**Known Issues**
- **Wi-Fi** - 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 limitations in its current sensing circuitry
**All New Major DCC++EX 4.0.0 features**
- **New HAL Hardware Abstraction Layer API** that automatically detects and greatly simplifies interfacing to many predefined accessory boards for servos, signals & sensors and added I/O (digital and analog inputs and outputs, servos etc).
- HAL Support for;
- MCP23008, MCP23017 and PCF9584 I2C GPIO Extender modules.
- PCA9685 PWM (servo & signal) control modules.
- Analogue inputs on Arduino pins and on ADS111x I2C modules.
- MP3 sound playback via DFPlayer module.
- HC-SR04 Ultrasonic range sensor module.
- VL53L0X Laser range sensor module (Time-Of-Flight).
- A new `<D HAL SHOW>` command to list the HAL devices attached to the command station
**New Command Station Broadcast throttle logic**
- Synchronizes multiple WiThrottles and PC based JMRI Throttles for direction, speed and F-key updates
**New Discovered Servers on WiFi Throttles**
- Our New multicast Dynamic Network Server (mDNS) enhancement allows us to display the available WiFi server connections to a DCC++EX Command Station. Selecting it allows your WiThrottle App to connect to and load Server Rosters and function keys to your throttle from the new DCC++EX Command Station Server Roster.
**New DCC++EX 4.0.0 with EX-RAIL Extended Railroad Automation Instruction Language**
- Use to control your entire layout or as a separate accessory/animation controller
- Awesome, cleverly powerful yet simple user friendly scripting language for user built Automation & Routing scripts.
- You can control Engines, Sensors, Turnouts, Signals, Outputs and Accessories that are entered into your new myAutomation.h file, then uploaded into the DCC++EX Command Station.
- EX-RAIL scripts are automatically displayed as Automation {Handoff} and Route {Set} buttons on supported WiFi Throttle Apps.
**New EX-RAIL Roster Feature**
- List and store user defined engine roster & function keys inside the command station, and automatically load them in WiFi Throttle Apps.
- When choosing “DCC++EX” from discovered servers an Engine Driver or WiThrottle is directly connected to the Command Station.
- The EX-RAIL ROSTER command allows all the engine numbers, names and function keys youve listed in your myAutomation.h file to automatically upload the Command Station's Server Roster into your Engine Driver and WiThrottle Apps.
**New JMRI 4.99.2 and above specific DCC++EX 4.0 features**
- Enhanced JMRI DCC++ Configure Base Station pane for building and maintaining Sensor, Turnout and Output devices, or these can automatically be populated from the DCC++EX Command Station's mySetup.h file into JMRI.
- JMRI now supports multiple serial connected DCC++EX Command Stations, to display and track separate "Send DCC++ Command" and "DCC++ Traffic" Monitors for each Command Station at the same time.
For example: Use an Uno DCC++EX DecoderPro Programming Station {DCC++Prg} on a desktop programming track and a second Mega DCC++EX EX-RAIL Command Station for Operations {DCC++Ops} on the layout with an additional `<JOINED>` programming spur or siding track for acquiring an engine and Drive Away onto the mainline (see the DriveAway feature for more information).
**DCC++EX 4.0.0 additional product enhancements**
- Additional Motor Shields and Motor Board {boosters) supported
- Additional Accessory boards supported for GPIO expansion, Sensors, Servos & Signals
- Additional diagnostic commands like D ACK RETRY and D EXRAIL ON events, D HAL SHOW devices and D SERVO positions, and D RESET the command station while maintaining the serial connection with JMRI
- Automatic retry on failed ACK detection to give decoders another chance
- New EX-RAIL / slash command allows JMRI to directly communicate with many EX-RAIL scripts
- Turnout class revised to expand turnout capabilities and allow turnout names/descriptors to display in WiThrottle Apps.
- Build turnouts through either or both mySetup.h and myAutomation.h files, and have them automatically passed to, and populate, JMRI Turnout Tables
- Turnout user names display in Engine Driver & WiThrottles
- 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.
- Can disable EEPROM memory code to allow room for DCC++EX 4.0 to fit on a Uno Command Station
- Can define border between long and short addresses
- 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.
**4.0.0 Bug Fixes**
- Compiles on Nano Every
- Diagnostic display of ack pulses >32ms
- Current read from wrong ADC during interrupt
- AT(+) Command Pass Through
- CiDAP WiFi Drop out and the WiThrottle F-key looping error corrected
- One-off error in CIPSEND drop
- Common Fault Pin Error
- Uno Memory Utilization optimized
#### Summary of Release 3.1.0 key features and/or bug fixes by Point Release
**Summary of the key new features added to CommandStation-EX V3.0.16**
- Ignore CV1 bit 7 read if rejected by a non NMRA compliant decoder when identifying loco id
**Summary of the key new features added to CommandStation-EX V3.0.15**
- Send function commands just once instead of repeating them 4 times
**Summary of the key new features added to CommandStation-EX V3.0.14**
- Add feature to tolerate decoders that incorrectly have gaps in their ACK pulse
- Provide proper track power management when joining and unjoining tracks with <1 JOIN>
**Summary of the key new features added to CommandStation-EX V3.0.13**
- Fix for CAB Functions greater than 127
**Summary of the key new features added to CommandStation-EX V3.0.12**
- Fixed clear screen issue for nanoEvery and nanoWifi
**Summary of the key new features added to CommandStation-EX V3.0.11**
- Reorganized files for support of 128 speed steps
**Summary of the key new features added to CommandStation-EX V3.0.10**
- Added Support for the Teensy 3.2, 3.5, 3.6, 4.0 and 4.1 MCUs
- No functional change just changes to avoid complier warnings for Teensy/nanoEvery
**Summary of the key new features added to CommandStation-EX V3.0.9**
- Rearranges serial newlines for the benefit of JMRI
- Major update for efficiencies in displays (LCD, OLED)
- Add I2C Support functions
**Summary of the key new features added to CommandStation-EX V3.0.8**
- Wraps <* *> around DIAGS for the benefit of JMRI
**Summary of the key new features added to CommandStation-EX V3.0.7**
- Implemented support for older 28 apeed step decoders - Option to turn on 28 step speed decoders in addition to 128. If set, all locos will use 28 steps.
- Improved overload messages with raw values (relative to offset)
**Summary of the key new features added to CommandStation-EX V3.0.6**
- Prevent compiler warning about deprecated B constants
- Fix Bug that did not let us transmit 5 byte sized packets - 5 Byte commands like PoM (programming on main) were not being sent correctly
- Support for Huge function numbers (DCC BinaryStateControl) - Support Functions beyond F28
- <!> ESTOP all - New command to emergency stop all locos on the main track
- <- [cab]> estop and forget cab/all cabs - Stop and remove loco from the CS. Stops the repeating throttle messages
- `<D RESET>` command to reboot Arduino
- Automatic sensor offset detect
- Improved startup msgs from Motor Drivers (accuracy and auto sense factors)
- Drop post-write verify - No need to double check CV writes. Writes are now even faster.
- Allow current sense pin set to UNUSED_PIN - No need to ground an unused analog current pin. Produce startup warning and callback -2 for prog track cmds.
**Summary of the key new features added to CommandStation-EX V3.0.5**
- Fix Fn Key startup with loco ID and fix state change for F16-28
- Removed ethernet mac config and made it automatic
- Show wifi ip and port on lcd
- Auto load config.example.h with warning
- Dropped example .ino files
- Corrected .ino comments
- Add Pololu fault pin handling
- Waveform speed/simplicity improvements
- Improved pin speed in waveform
- Portability to nanoEvery and UnoWifiRev2 CPUs
- Analog read speed improvements
- Drop need for DIO2 library
- Improved current check code
- Linear command
- Removed need for ArduinoTimers files
- Removed option to choose different timer
- Added EX-RAIL hooks for automation in future version
- Fixed Turnout list
- Allow command keywords in mixed case
- Dropped unused memstream
- PWM pin accuracy if requirements met
**Summary of the key new features added to CommandStation-EX V3.0.4**
- "Drive-Away" Feature - added so that throttles 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
- WiFi Startup Fixes
**Summary of the key new features added to CommandStation-EX V3.0.3**
- Command to write loco address and clear consist
- Command will allow for consist address
- Startup commands implemented
**Summary of the key new features added to CommandStation-EX V3.0.2:**
- Create new output for current in mA for `<c>` command - New current response outputs current in mA, overlimit current, and maximum board capable current
- Simultaneously update JMRI to handle new current meter
**Summary of the key new features added to CommandStation-EX V3.0.1:**
- Add back fix for jitter
- Add Turnouts, Outputs and Sensors to `<s>` command output
**CommandStation-EX V3.0.0:**
**Release v3.0.0 was a major rewrite if earlier versions of DCC++. The code base was re-architeced and core changes were made to the Waveform generator to reduce overhead and make better use of Arduino.** **Summary of the key new features added in Release v3.0.0 include:**
- **New USB Browser Based Throttle** - WebThrottle-EX is a full front-end to controller to control the CS to run trains.
- **WiFi Support** - AP and station modes supported. Auto-detection of an ESP8266 WiFi module with AT firmware on a Mega's serial port. Connection to JMRI and WiThrottle clients.
- **Withrottle Integrations** - Act as a host for up to four WiThrottle clients concurrently.
- **Add LCD/OLED support** - OLED supported on Mega only
- **Improved CV programming routines** - checks for length of CV pulse, and breaks out of the wait state once it has received an ACK, now reading one CV per second.
- **Improved current sensing** - rewrote current sensing routines for safer operation. Current thresholds based on milliamps, not magic numbers
- **Individual track power control** - Ability to toggle power on either or both tracks, and to "JOIN" the tracks and make them output the same waveform for multiple power districts.
- **Single or Dual-Pin PWM output** - Allows control of H-bridges with PH/EN or dual PWM inputs
- **New, simpler function command** - `<F>` command allows setting functions based on their number, not based on a code as in `<f>`
- **Function reminders** - Function reminders are sent in addition to speed reminders
- **Functions to F28** - All NMRA functions are now supported
- **Filters and user functions** - Ability to filter commands in the parser and execute custom code based on them. (ex: Redirect Turnout commands via NRF24)
- **Diagnostic `<D>` commands** - See documentation for a full list of new diagnostic commands
- **Rewrote DCC++ Parser** - more efficient operation, accepts multi-char input and uses less RAM
- **Rewritten waveform generator** - capable of using any pin for DCC waveform out, eliminating the need for jumpers
- **Rewritten packet generator** - Simplify and make smaller, remove idea of "registers" from original code
- **Add free RAM messages** - Free RAM messages are now printed whenever there is a decerase in available RAM
- **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 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**
**Project Lead**
- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)
**CommandStation-EX Developers**
- Chris Harlow - Bournemouth, UK (UKBloke)
- Harald Barth - Stockholm, Sweden (Haba)
- 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 - Oregon, USA (MSteveTodd)
- 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++)
**Website and Documentation**
- Mani Kumar - Bangalor, India (Mani / Mani Kumar)
- Fred Decker - Holly Springs, North Carolina, USA (FlightRisk)
- Dave Cutting - Logan, Utah, USA (Dave Cutting/ David Cutting)
- Roger Beschizza - Dorset, UK (Roger Beschizza)
- Keith Ledbetter - Chicago, Illinois, USA (Keith Ledbetter)
- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)
- Colin Grabham - Central NSW, Australia (Kebbin)
**WebThrotle-EX**
- Fred Decker - Holly Springs, NC (FlightRisk/FrightRisk)
- Mani Kumar - Bangalor, India (Mani /Mani Kumar)
- Matt H - Somewhere in Europe
**Beta Testing / Release Management / Support**
- Larry Dribin - Release Management
- Kevin Smith - Rochester Hills, Michigan USA (KC Smith)
- Herb Morton - Kingwood Texas, USA (Ash++)
- Keith Ledbetter
- Brad Van der Elst
- Andrew Pye
- Mike Bowers
- Randy McKenzie
- Roberto Bravin
- Sam Brigden
- Alan Lautenslager
- Martin Bafver
- Mário André Silva
- Anthony Kochevar
- 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/v4.0.0-Prod/CommandStation-EX.zip)
[CommandStation-EX.tar.gz](https://github.com/DCC-EX/CommandStation-EX/releases/download/v4.0.0-Prod/CommandStation-EX.tar.gz)

View File

@@ -1,5 +1,6 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of DCC-EX CommandStation-EX
*
@@ -103,3 +104,12 @@ bool RingStream::commit() {
_buffer[_mark]=lowByte(_count);
return true; // commit worked
}
void RingStream::flush() {
_pos_write=0;
_pos_read=0;
_buffer[0]=0;
}
void RingStream::printBuffer(Print * stream) {
_buffer[_pos_write]='\0';
stream->print((char *)_buffer);
}

View File

@@ -1,7 +1,8 @@
#ifndef RingStream_h
#define RingStream_h
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of DCC-EX CommandStation-EX
*
@@ -34,7 +35,8 @@ class RingStream : public Print {
void mark(uint8_t b);
bool commit();
uint8_t peekTargetMark();
void printBuffer(Print * streamer);
void flush();
private:
int _len;
int _pos_write;

View File

@@ -89,121 +89,6 @@ 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};
//==============================================================================
// SSD1306AsciiWire Method Definitions
//------------------------------------------------------------------------------
// Constructor
SSD1306AsciiWire::SSD1306AsciiWire(int width, int height) {
// Set size in characters in base class
lcdRows = height / 8;
lcdCols = width / 6;
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("%dx%d OLED display configured on I2C:x%x"), width, height, address);
if (width == 132)
begin(&SH1106_132x64, address);
else if (height == 32)
begin(&Adafruit128x32, address);
else
begin(&Adafruit128x64, address);
// Set singleton address
lcdDisplay = this;
clear();
return;
}
}
DIAG(F("OLED display not found"));
}
/* Clear screen by writing blank pixels. */
void SSD1306AsciiWire::clearNative() {
const int maxBytes = sizeof(blankPixels); // max number of bytes sendable over Wire
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
}
}
}
// Initialise device
void SSD1306AsciiWire::begin(const DevType* dev, uint8_t i2cAddr) {
m_i2cAddr = i2cAddr;
m_col = 0;
m_row = 0;
const uint8_t* table = (const uint8_t*)GETFLASHW(&dev->initcmds);
uint8_t size = GETFLASH(&dev->initSize);
m_displayWidth = GETFLASH(&dev->lcdWidth);
m_displayHeight = GETFLASH(&dev->lcdHeight);
m_colOffset = GETFLASH(&dev->colOffset);
I2CManager.write_P(m_i2cAddr, table, size);
if (m_displayHeight == 32)
I2CManager.write(m_i2cAddr, 5, 0, // Set command mode
SSD1306_SETMULTIPLEX, 0x1F, // ratio 32
SSD1306_SETCOMPINS, 0x02); // sequential COM pins, disable remap
}
//------------------------------------------------------------------------------
// 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 = m_colOffset;
// 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);
}
}
//------------------------------------------------------------------------------
// 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;
// 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++)
outputBuffer[bufferPos++] = GETFLASH(base++);
// Add blank pixels between letters
for (uint8_t i = 0; i < letterSpacing; i++)
outputBuffer[bufferPos++] = 0;
// Write the data to I2C display
I2CManager.write(m_i2cAddr, outputBuffer, bufferPos);
m_col += fontWidth + letterSpacing;
return 1;
}
//==============================================================================
// this section is based on https://github.com/adafruit/Adafruit_SSD1306
@@ -230,23 +115,6 @@ const uint8_t FLASH SSD1306AsciiWire::Adafruit128xXXinit[] = {
SSD1306_DISPLAYON
};
/** Initialize a 128x32 SSD1306 oled display. */
const DevType FLASH SSD1306AsciiWire::Adafruit128x32 = {
Adafruit128xXXinit,
sizeof(Adafruit128xXXinit),
128,
32,
0
};
/** Initialize a 128x64 oled display. */
const DevType FLASH SSD1306AsciiWire::Adafruit128x64 = {
Adafruit128xXXinit,
sizeof(Adafruit128xXXinit),
128,
64,
0
};
//------------------------------------------------------------------------------
// This section is based on https://github.com/stanleyhuangyc/MultiLCD
@@ -271,15 +139,123 @@ const uint8_t FLASH SSD1306AsciiWire::SH1106_132x64init[] = {
SSD1306_DISPLAYON
};
/** Initialize a 132x64 oled SH1106 display. */
const DevType FLASH SSD1306AsciiWire::SH1106_132x64 = {
SH1106_132x64init,
sizeof(SH1106_132x64init),
128,
64,
2 // SH1106 is a 132x64 controller but most OLEDs are only attached
// to columns 2-129.
};
//==============================================================================
// SSD1306AsciiWire Method Definitions
//------------------------------------------------------------------------------
// 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"));
}
/* Clear screen by writing blank pixels. */
void SSD1306AsciiWire::clearNative() {
const int maxBytes = sizeof(blankPixels); // max number of bytes sendable over Wire
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
}
}
}
//------------------------------------------------------------------------------
// 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 = 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);
}
}
//------------------------------------------------------------------------------
// 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;
// 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++)
outputBuffer[bufferPos++] = GETFLASH(base++);
// Add blank pixels between letters
for (uint8_t i = 0; i < letterSpacing; i++)
outputBuffer[bufferPos++] = 0;
// Write the data to I2C display
I2CManager.write(m_i2cAddr, outputBuffer, bufferPos, &requestBlock);
m_col += fontWidth + letterSpacing;
return 1;
}
//------------------------------------------------------------------------------

View File

@@ -32,21 +32,6 @@
//#define NOLOWERCASE
//------------------------------------------------------------------------------
// 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 display 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;
};
// Constructor
class SSD1306AsciiWire : public LCDDisplay {
public:
@@ -55,24 +40,18 @@ class SSD1306AsciiWire : public LCDDisplay {
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 clearNative();
void clearNative() override;
// Set cursor to start of specified text line
void setRowNative(byte line);
// Initialize the display controller.
void init(const DevType* dev);
void setRowNative(byte line) override;
// Write one character to OLED
size_t writeNative(uint8_t c);
size_t writeNative(uint8_t c) override;
// Display characteristics / initialisation
static const DevType FLASH Adafruit128x32;
static const DevType FLASH Adafruit128x64;
static const DevType FLASH SH1106_132x64;
bool isBusy() override { return requestBlock.isBusy(); }
private:
// Cursor column.
@@ -97,6 +76,7 @@ class SSD1306AsciiWire : public LCDDisplay {
uint8_t m_i2cAddr;
I2CRB requestBlock;
uint8_t outputBuffer[fontWidth+letterSpacing+1];
static const uint8_t blankPixels[];

View File

@@ -1,7 +1,10 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021 Neil McKechnie
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
* 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
@@ -27,9 +30,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:
@@ -66,47 +69,111 @@ decide to ignore the <q ID> return and only react to <Q ID> triggers.
**********************************************************************/
#include "StringFormatter.h"
#include "CommandDistributor.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){
void Sensor::checkAll(){
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
CommandDistributor::broadcastSensor(readingSensor->data.snum,readingSensor->active);
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 +182,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 +255,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(uint16_t 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 +306,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

@@ -1,5 +1,8 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021 Neil McKechnie
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -20,29 +23,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();
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

83
SerialManager.cpp Normal file
View File

@@ -0,0 +1,83 @@
/*
* © 2021 Chris Harlow
* © 2022 Harald Barth
* All rights reserved.
*
* This file is part of DCC++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 "SerialManager.h"
#include "DCCEXParser.h"
SerialManager * SerialManager::first=NULL;
SerialManager::SerialManager(Stream * myserial) {
serial=myserial;
next=first;
first=this;
bufferLength=0;
inCommandPayload=false;
}
void SerialManager::init() {
while (!Serial && millis() < 5000); // wait max 5s for Serial to start
Serial.begin(115200);
new SerialManager(&Serial);
#ifdef SERIAL3_COMMANDS
Serial3.begin(115200);
new SerialManager(&Serial3);
#endif
#ifdef SERIAL2_COMMANDS
Serial2.begin(115200);
new SerialManager(&Serial2);
#endif
#ifdef SERIAL1_COMMANDS
Serial1.begin(115200);
new SerialManager(&Serial1);
#endif
}
void SerialManager::broadcast(char * stringBuffer) {
for (SerialManager * s=first;s;s=s->next) s->broadcast2(stringBuffer);
}
void SerialManager::broadcast2(char * stringBuffer) {
serial->print(stringBuffer);
}
void SerialManager::loop() {
for (SerialManager * s=first;s;s=s->next) s->loop2();
}
void SerialManager::loop2() {
while (serial->available()) {
char ch = serial->read();
if (ch == '<') {
inCommandPayload = true;
bufferLength = 0;
buffer[0] = '\0';
}
else if (ch == '>') {
buffer[bufferLength] = '\0';
DCCEXParser::parse(serial, buffer, NULL);
inCommandPayload = false;
break;
}
else if (inCommandPayload) {
if (bufferLength < (COMMAND_BUFFER_SIZE-1)) buffer[bufferLength++] = ch;
}
}
}

View File

@@ -1,7 +1,8 @@
/*
* (c) 2020 Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
/*
* © 2021 Chris Harlow
* All rights reserved.
*
* This file is part of DCC++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
@@ -16,24 +17,33 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*!
* @file PWMServoDriver.h
*
* Used to set servo positions on an I2C bus with 1 or more PCA96685 boards.
*/
#ifndef PWMServoDriver_H
#define PWMServoDriver_H
#ifndef SerialManager_h
#define SerialManager_h
#include "Arduino.h"
#include "defines.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);
};
#ifndef COMMAND_BUFFER_SIZE
#define COMMAND_BUFFER_SIZE 100
#endif
class SerialManager {
public:
static void init();
static void loop();
static void broadcast(char * stringBuffer);
private:
static SerialManager * first;
SerialManager(Stream * myserial);
void loop2();
void broadcast2(char * stringBuffer);
Stream * serial;
SerialManager * next;
byte bufferLength;
byte buffer[COMMAND_BUFFER_SIZE];
bool inCommandPayload;
};
#endif

View File

@@ -1,7 +1,8 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
* © 2022 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
* This file is part of DCC-EX 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
@@ -17,11 +18,28 @@
* 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 "StringBuffer.h"
#include "DIAG.h"
StringBuffer::StringBuffer() {
flush();
};
char * StringBuffer::getString() {
return _buffer;
}
void StringBuffer::flush() {
_pos_write=0;
_buffer[0]='\0';
}
size_t StringBuffer::write(uint8_t b) {
if (_pos_write>=buffer_max) return 0;
_buffer[_pos_write] = b;
++_pos_write;
_buffer[_pos_write]='\0';
return 1;
}

View File

@@ -1,8 +1,8 @@
/*
* © 2020, Harald Barth
* © 2021, Neil McKechnie
/*
* © 2022 Chris Harlow
* All rights reserved.
*
* This file is part of DCC-EX
* This file is part of DCC++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
@@ -18,8 +18,21 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef freeMemory_h
#define freeMemory_h
void updateMinimumFreeMemory(unsigned char extraBytes=0);
int minimumFreeMemory();
#endif
#ifndef StringBuffer_h
#define StringBuffer_h
#include <Arduino.h>
class StringBuffer : public Print {
public:
StringBuffer();
// Override Print default
virtual size_t write(uint8_t b);
void flush();
char * getString();
private:
static const int buffer_max=64; // enough for long text msgs to throttles
int16_t _pos_write;
char _buffer[buffer_max+1];
};
#endif

View File

@@ -99,11 +99,12 @@ void StringFormatter::send2(Print * stream,const FSH* format, va_list args) {
case 'E': printEscapes(stream,(const FSH*)va_arg(args, char*)); break;
case 'S': stream->print((const FSH*)va_arg(args, char*)); break;
case 'd': printPadded(stream,va_arg(args, int), formatWidth, formatLeft); break;
case 'u': printPadded(stream,va_arg(args, unsigned int), formatWidth, formatLeft); break;
case 'l': printPadded(stream,va_arg(args, long), formatWidth, formatLeft); break;
case 'b': stream->print(va_arg(args, int), BIN); break;
case 'o': stream->print(va_arg(args, int), OCT); break;
case 'x': stream->print(va_arg(args, int), HEX); break;
case 'f': stream->print(va_arg(args, double), 2); break;
//case 'f': stream->print(va_arg(args, double), 2); break;
//format width prefix
case '-':
formatLeft=true;

363
TrackManager.cpp Normal file
View File

@@ -0,0 +1,363 @@
/*
* © 2022 Chris Harlow
* © 2022 Harald Barth
* All rights reserved.
*
* This file is part of DCC++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 "TrackManager.h"
#include "FSH.h"
#include "DCCWaveform.h"
#include "DCC.h"
#include "MotorDriver.h"
#include "DCCTimer.h"
#include "DIAG.h"
// Virtualised Motor shield multi-track hardware Interface
#define FOR_EACH_TRACK(t) for (byte t=0;t<=lastTrack;t++)
#define APPLY_BY_MODE(findmode,function) \
FOR_EACH_TRACK(t) \
if (trackMode[t]==findmode) \
track[t]->function;
const int16_t HASH_KEYWORD_PROG = -29718;
const int16_t HASH_KEYWORD_MAIN = 11339;
const int16_t HASH_KEYWORD_OFF = 22479;
const int16_t HASH_KEYWORD_DC = 2183;
const int16_t HASH_KEYWORD_DCX = 6463; // DC reversed polarity
const int16_t HASH_KEYWORD_EXT = 8201; // External DCC signal
const int16_t HASH_KEYWORD_A = 65; // parser makes single chars the ascii.
MotorDriver * TrackManager::track[MAX_TRACKS];
TRACK_MODE TrackManager::trackMode[MAX_TRACKS];
int16_t TrackManager::trackDCAddr[MAX_TRACKS];
POWERMODE TrackManager::mainPowerGuess=POWERMODE::OFF;
byte TrackManager::lastTrack=0;
bool TrackManager::progTrackSyncMain=false;
bool TrackManager::progTrackBoosted=false;
int16_t TrackManager::joinRelay=UNUSED_PIN;
// The setup call is done this way so that the tracks can be in a list
// from the config... the tracks default to NULL in the declaration
void TrackManager::Setup(const FSH * shieldname,
MotorDriver * track0, MotorDriver * track1, MotorDriver * track2,
MotorDriver * track3, MotorDriver * track4, MotorDriver * track5,
MotorDriver * track6, MotorDriver * track7 ) {
addTrack(0,track0);
addTrack(1,track1);
addTrack(2,track2);
addTrack(3,track3);
addTrack(4,track4);
addTrack(5,track5);
addTrack(6,track6);
addTrack(7,track7);
// Default the first 2 tracks (which may be null) and perform HA waveform check.
setTrackMode(0,TRACK_MODE_MAIN);
setTrackMode(1,TRACK_MODE_PROG);
// TODO Fault pin config for odd motor boards (example pololu)
// MotorDriver::commonFaultPin = ((mainDriver->getFaultPin() == progDriver->getFaultPin())
// && (mainDriver->getFaultPin() != UNUSED_PIN));
DCC::begin(shieldname);
}
void TrackManager::addTrack(byte t, MotorDriver* driver) {
trackMode[t]=TRACK_MODE_OFF;
track[t]=driver;
if (driver) {
track[t]->setPower(POWERMODE::OFF);
lastTrack=t;
}
}
// setDCCSignal(), called from interrupt context
// does assume ports are shadowed if they can be
void TrackManager::setDCCSignal( bool on) {
HAVE_PORTA(shadowPORTA=PORTA);
HAVE_PORTB(shadowPORTB=PORTB);
HAVE_PORTC(shadowPORTC=PORTC);
APPLY_BY_MODE(TRACK_MODE_MAIN,setSignal(on));
HAVE_PORTA(PORTA=shadowPORTA);
HAVE_PORTB(PORTB=shadowPORTB);
HAVE_PORTC(PORTC=shadowPORTC);
}
void TrackManager::setCutout( bool on) {
(void) on;
// TODO Cutout needs fake ports as well
// TODO APPLY_BY_MODE(TRACK_MODE_MAIN,setCutout(on));
}
// setPROGSignal(), called from interrupt context
// does assume ports are shadowed if they can be
void TrackManager::setPROGSignal( bool on) {
HAVE_PORTA(shadowPORTA=PORTA);
HAVE_PORTB(shadowPORTB=PORTB);
HAVE_PORTC(shadowPORTC=PORTC);
APPLY_BY_MODE(TRACK_MODE_PROG,setSignal(on));
HAVE_PORTA(PORTA=shadowPORTA);
HAVE_PORTB(PORTB=shadowPORTB);
HAVE_PORTC(PORTC=shadowPORTC);
}
// setDCSignal(), called from normal context
// MotorDriver::setDCSignal handles shadowed IO port changes.
// with interrupts turned off around the critical section
void TrackManager::setDCSignal(int16_t cab, byte speedbyte) {
FOR_EACH_TRACK(t) {
if (trackDCAddr[t]!=cab) continue;
if (trackMode[t]==TRACK_MODE_DC) track[t]->setDCSignal(speedbyte);
else if (trackMode[t]==TRACK_MODE_DCX) track[t]->setDCSignal(speedbyte ^ 128);
}
}
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) {
if (trackToSet>lastTrack || track[trackToSet]==NULL) return false;
//DIAG(F("Track=%c"),trackToSet+'A');
// DC tracks require a motorDriver that can set brake!
if ((mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX)
&& !track[trackToSet]->brakeCanPWM()) {
DIAG(F("Brake pin can't PWM: No DC"));
return false;
}
if (mode==TRACK_MODE_PROG) {
// only allow 1 track to be prog
FOR_EACH_TRACK(t)
if (trackMode[t]==TRACK_MODE_PROG && t != trackToSet) {
track[t]->setPower(POWERMODE::OFF);
trackMode[t]=TRACK_MODE_OFF;
}
} else {
track[trackToSet]->setResetCounterPointer(NULL); // only the prog track has this pointer set
}
trackMode[trackToSet]=mode;
trackDCAddr[trackToSet]=dcAddr;
// When a track is switched, we must clear any side effects of its previous
// state, otherwise trains run away or just dont move.
if (mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX) {
// DC tracks need to be given speed of the throttle for that cab address
// otherwise will not match other tracks on same cab.
// This also needs to allow for inverted DCX
applyDCSpeed(trackToSet);
}
else {
// DCC tracks need to have set the PWM to zero or they will not work.
// 128 is speed=0 and dir=0 and then loosen brake.
track[trackToSet]->setDCSignal(128);
track[trackToSet]->setBrake(false);
}
// EXT is a special case where the signal pin is
// turned off. So unless that is set, the signal
// pin should be turned on
track[trackToSet]->enableSignal(mode != TRACK_MODE_EXT);
// re-evaluate HighAccuracy mode
// We can only do this is all main and prog tracks agree
bool canDo=true;
FOR_EACH_TRACK(t) {
// DC tracks must not have the DCC PWM switched on
// so we globally turn it off if one of the PWM
// capable tracks is now DC or DCX.
if (trackMode[t]==TRACK_MODE_DC || trackMode[t]==TRACK_MODE_DCX) {
if (track[t]->isPWMCapable()) {
canDo=false; // this track is capable but can not run PWM
break; // in this mode, so abort and prevent globally below
} else {
track[t]->trackPWM=false; // this track sure can not run with PWM
//DIAG(F("Track %c trackPWM 0 (not capable)"), t+'A');
}
} else if (trackMode[t]==TRACK_MODE_MAIN || trackMode[t]==TRACK_MODE_PROG) {
track[t]->trackPWM = track[t]->isPWMCapable(); // trackPWM is still a guess here
//DIAG(F("Track %c trackPWM %d"), t+'A', track[t]->trackPWM);
canDo &= track[t]->trackPWM;
}
}
if (!canDo) {
// if we discover that HA mode was globally impossible
// we must adjust the trackPWM capabilities
FOR_EACH_TRACK(t) {
track[t]->trackPWM=false;
//DIAG(F("Track %c trackPWM 0 (global override)"), t+'A');
}
DCCTimer::clearPWM(); // has to be AFTER trackPWM changes because if trackPWM==true this is undone for that track
}
// Normal running tracks are set to the global power state
track[trackToSet]->setPower(
(mode==TRACK_MODE_MAIN || mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX || mode==TRACK_MODE_EXT) ?
mainPowerGuess : POWERMODE::OFF);
//DIAG(F("TrackMode=%d"),mode);
return true;
}
void TrackManager::applyDCSpeed(byte t) {
uint8_t speedByte=DCC::getThrottleSpeedByte(trackDCAddr[t]);
if (trackMode[t]==TRACK_MODE_DCX)
speedByte = speedByte ^ 128; // reverse direction bit
track[t]->setDCSignal(speedByte);
}
bool TrackManager::parseJ(Print *stream, int16_t params, int16_t p[])
{
if (params==0) { // <=> List track assignments
FOR_EACH_TRACK(t)
if (track[t]!=NULL) {
StringFormatter::send(stream,F("<= %c "),'A'+t);
switch(trackMode[t]) {
case TRACK_MODE_MAIN:
StringFormatter::send(stream,F("MAIN"));
if (track[t]->trackPWM)
StringFormatter::send(stream,F("+"));
break;
case TRACK_MODE_PROG:
StringFormatter::send(stream,F("PROG"));
if (track[t]->trackPWM)
StringFormatter::send(stream,F("+"));
break;
case TRACK_MODE_OFF:
StringFormatter::send(stream,F("OFF"));
break;
case TRACK_MODE_EXT:
StringFormatter::send(stream,F("EXT"));
break;
case TRACK_MODE_DC:
StringFormatter::send(stream,F("DC %d"),trackDCAddr[t]);
break;
case TRACK_MODE_DCX:
StringFormatter::send(stream,F("DCX %d"),trackDCAddr[t]);
break;
default:
break; // unknown, dont care
}
StringFormatter::send(stream,F(">\n"));
}
return true;
}
p[0]-=HASH_KEYWORD_A; // convert A... to 0....
if (params>1 && (p[0]<0 || p[0]>=MAX_TRACKS))
return false;
if (params==2 && p[1]==HASH_KEYWORD_MAIN) // <= id MAIN>
return setTrackMode(p[0],TRACK_MODE_MAIN);
if (params==2 && p[1]==HASH_KEYWORD_PROG) // <= id PROG>
return setTrackMode(p[0],TRACK_MODE_PROG);
if (params==2 && p[1]==HASH_KEYWORD_OFF) // <= id OFF>
return setTrackMode(p[0],TRACK_MODE_OFF);
if (params==2 && p[1]==HASH_KEYWORD_EXT) // <= id EXT>
return setTrackMode(p[0],TRACK_MODE_EXT);
if (params==3 && p[1]==HASH_KEYWORD_DC && p[2]>0) // <= id DC cab>
return setTrackMode(p[0],TRACK_MODE_DC,p[2]);
if (params==3 && p[1]==HASH_KEYWORD_DCX && p[2]>0) // <= id DCX cab>
return setTrackMode(p[0],TRACK_MODE_DCX,p[2]);
return false;
}
byte TrackManager::nextCycleTrack=MAX_TRACKS;
void TrackManager::loop() {
DCCWaveform::loop();
DCCACK::loop();
bool dontLimitProg=DCCACK::isActive() || progTrackSyncMain || progTrackBoosted;
nextCycleTrack++;
if (nextCycleTrack>lastTrack) nextCycleTrack=0;
if (track[nextCycleTrack]==NULL) return;
MotorDriver * motorDriver=track[nextCycleTrack];
bool useProgLimit=dontLimitProg? false: trackMode[nextCycleTrack]==TRACK_MODE_PROG;
motorDriver->checkPowerOverload(useProgLimit, nextCycleTrack);
}
MotorDriver * TrackManager::getProgDriver() {
FOR_EACH_TRACK(t)
if (trackMode[t]==TRACK_MODE_PROG) return track[t];
return NULL;
}
void TrackManager::setPower2(bool setProg,POWERMODE mode) {
if (!setProg) mainPowerGuess=mode;
FOR_EACH_TRACK(t) {
MotorDriver * driver=track[t];
if (!driver) continue;
switch (trackMode[t]) {
case TRACK_MODE_MAIN:
if (setProg) break;
// toggle brake before turning power on - resets overcurrent error
// on the Pololu board if brake is wired to ^D2.
// XXX see if we can make this conditional
driver->setBrake(true);
driver->setBrake(false); // DCC runs with brake off
driver->setPower(mode);
break;
case TRACK_MODE_DC:
case TRACK_MODE_DCX:
if (setProg) break;
driver->setBrake(true); // DC starts with brake on
applyDCSpeed(t); // speed match DCC throttles
driver->setPower(mode);
break;
case TRACK_MODE_PROG:
if (!setProg) break;
driver->setBrake(true);
driver->setBrake(false);
driver->setPower(mode);
break;
case TRACK_MODE_EXT:
driver->setBrake(true);
driver->setBrake(false);
driver->setPower(mode);
break;
case TRACK_MODE_OFF:
break;
}
}
}
POWERMODE TrackManager::getProgPower() {
FOR_EACH_TRACK(t)
if (trackMode[t]==TRACK_MODE_PROG)
return track[t]->getPower();
return POWERMODE::OFF;
}
void TrackManager::setJoinRelayPin(byte joinRelayPin) {
joinRelay=joinRelayPin;
if (joinRelay!=UNUSED_PIN) {
pinMode(joinRelay,OUTPUT);
digitalWrite(joinRelay,LOW); // LOW is relay disengaged
}
}
void TrackManager::setJoin(bool joined) {
progTrackSyncMain=joined;
if (joinRelay!=UNUSED_PIN) digitalWrite(joinRelay,joined?HIGH:LOW);
}

89
TrackManager.h Normal file
View File

@@ -0,0 +1,89 @@
/*
* © 2022 Chris Harlow
* © 2022 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 TrackManager_h
#define TrackManager_h
#include "FSH.h"
#include "MotorDriver.h"
// Virtualised Motor shield multi-track hardware Interface
enum TRACK_MODE : byte {TRACK_MODE_OFF, TRACK_MODE_MAIN, TRACK_MODE_PROG,
TRACK_MODE_DC, TRACK_MODE_DCX, TRACK_MODE_EXT};
// These constants help EXRAIL macros say SET_TRACK(2,mode) OR SET_TRACK(C,mode) etc.
const byte TRACK_NUMBER_0=0, TRACK_NUMBER_A=0;
const byte TRACK_NUMBER_1=1, TRACK_NUMBER_B=1;
const byte TRACK_NUMBER_2=2, TRACK_NUMBER_C=2;
const byte TRACK_NUMBER_3=3, TRACK_NUMBER_D=3;
const byte TRACK_NUMBER_4=4, TRACK_NUMBER_E=4;
const byte TRACK_NUMBER_5=5, TRACK_NUMBER_F=5;
const byte TRACK_NUMBER_6=6, TRACK_NUMBER_G=6;
const byte TRACK_NUMBER_7=7, TRACK_NUMBER_H=7;
class TrackManager {
public:
static void Setup(const FSH * shieldName,
MotorDriver * track0,
MotorDriver * track1=NULL,
MotorDriver * track2=NULL,
MotorDriver * track3=NULL,
MotorDriver * track4=NULL,
MotorDriver * track5=NULL,
MotorDriver * track6=NULL,
MotorDriver * track7=NULL
);
static void setDCCSignal( bool on);
static void setCutout( bool on);
static void setPROGSignal( bool on);
static void setDCSignal(int16_t cab, byte speedbyte);
static MotorDriver * getProgDriver();
static void setPower2(bool progTrack,POWERMODE mode);
static void setPower(POWERMODE mode) {setMainPower(mode); setProgPower(mode);}
static void setMainPower(POWERMODE mode) {setPower2(false,mode);}
static void setProgPower(POWERMODE mode) {setPower2(true,mode);}
static const int16_t MAX_TRACKS=8;
static bool setTrackMode(byte track, TRACK_MODE mode, int16_t DCaddr=0);
static bool parseJ(Print * stream, int16_t params, int16_t p[]);
static void loop();
static POWERMODE getMainPower() {return mainPowerGuess;}
static POWERMODE getProgPower();
static void setJoin(bool join);
static bool isJoined() { return progTrackSyncMain;}
static void setJoinRelayPin(byte joinRelayPin);
static int16_t joinRelay;
static bool progTrackSyncMain; // true when prog track is a siding switched to main
static bool progTrackBoosted; // true when prog track is not current limited
private:
static void addTrack(byte t, MotorDriver* driver);
static byte lastTrack;
static byte nextCycleTrack;
static POWERMODE mainPowerGuess;
static void applyDCSpeed(byte t);
static MotorDriver* track[MAX_TRACKS];
static TRACK_MODE trackMode[MAX_TRACKS];
static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC or TRACK_MODE_DCX
};
#endif

View File

@@ -1,9 +1,13 @@
/*
* © 2021 Neil McKechnie
* © 2021 M Steve Todd
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* © 2013-2016 Gregg E. Berman
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth.
* All rights reserved.
*
* This file is part of Asbelos DCC API
* 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
@@ -18,166 +22,518 @@
* 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 "CommandDistributor.h"
#include "EXRAIL2.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;
/*
* Protected static functions
*/
// 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);
}
///////////////////////////////////////////////////////////////////////////////
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(uint16_t 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++;
}
// 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;
}
///////////////////////////////////////////////////////////////////////////////
//
// 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);
/* 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(EXRAIL_ACTIVE)
RMFT2::turnoutEvent(id, closeFlag);
#endif
CommandDistributor::broadcastTurnout(id, closeFlag);
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 %d Addr %d Subaddr %d Status %d"),tt->data.id, tt->data.address, tt->data.subAddress,tt->data.tStatus & STATUS_ACTIVE);
}
DIAG(F("Turnout::throw(%d)"), id);
#endif
Turnout *tt = Turnout::get(id);
if (!tt) return false;
bool ok = tt->setClosedInternal(closeFlag);
if (ok) {
#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
Turnout *Turnout::firstTurnout=NULL;
int Turnout::turnoutlistHash=0; //bump on every change so clients know when to refresh their lists
#if defined(EXRAIL_ACTIVE)
RMFT2::turnoutEvent(id, closeFlag);
#endif
// Send message to JMRI etc.
CommandDistributor::broadcastTurnout(id, closeFlag);
}
return ok;
}
#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
/*************************************************************************************
* 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;
#else
(void)close; // avoid compiler warnings
#endif
return true;
}
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);
}
bool VpinTurnout::setClosedInternal(bool close) {
IODevice::write(_vpinTurnoutData.vpin, close);
_turnoutData.closed = close;
return true;
}
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,7 +1,13 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021 Neil McKechnie
* © 2021 M Steve Todd
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2022 Chris Harlow
* © 2013-2016 Gregg E. Berman
* All rights reserved.
*
* This file is part of Asbelos DCC API
* 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
@@ -16,46 +22,288 @@
* 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"
#include "StringFormatter.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;
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
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 hidden : 1;
bool _rfu : 1;
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;
/*
* Constructor
*/
Turnout(uint16_t id, uint8_t turnoutType, bool closed) {
_turnoutData.id = id;
_turnoutData.turnoutType = turnoutType;
_turnoutData.closed = closed;
_turnoutData.hidden=false;
add(this);
}
/*
* Static data
*/
static Turnout *_firstTurnout;
static int _turnoutlistHash;
/*
* Virtual functions
*/
virtual bool setClosedInternal(bool close) = 0; // Mandatory in subclass
virtual void save() {}
/*
* Static functions
*/
static void add(Turnout *tt);
public:
static Turnout *get(uint16_t id);
/*
* Static data
*/
static int turnoutlistHash;
static const bool useClassicTurnoutCommands;
/*
* Public base class functions
*/
inline bool isClosed() { return _turnoutData.closed; };
inline bool isThrown() { return !_turnoutData.closed; }
inline bool isHidden() { return _turnoutData.hidden; }
inline void setHidden(bool h) { _turnoutData.hidden=h; }
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; }
#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)
if (!tt->isHidden()) StringFormatter::send(stream, F("<H %d %d>\n"),tt->getId(), tt->isThrown());
}
};
/*************************************************************************************
* 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;
};
/*************************************************************************************
* 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;
};
/*************************************************************************************
* 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

@@ -1,8 +1,12 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2020-2022 Harald Barth
* © 2020-2021 M Steve Todd
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
* 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
@@ -40,6 +44,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 +53,15 @@
#include "DIAG.h"
#include "GITHUB_SHA.h"
#include "version.h"
#include "EXRAIL2.h"
#include "CommandDistributor.h"
#include "TrackManager.h"
#include "DCCTimer.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 +91,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';
}
@@ -107,99 +117,153 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d)<-[%e]"),millis(),clientid,cmd);
if (initSent) {
// Send power state if different than last sent
bool currentPowerState = (DCCWaveform::mainTrack.getPowerMode()==POWERMODE::ON);
if (lastPowerState != currentPowerState) {
StringFormatter::send(stream,F("PPA%x\n"),currentPowerState);
lastPowerState = currentPowerState;
}
// 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()){
if (tt->isHidden()) continue;
int id=tt->getId();
const FSH * tdesc=NULL;
#ifdef EXRAIL_ACTIVE
tdesc=RMFT2::getTurnoutDescription(id);
#endif
char tchar=Turnout::isClosed(id)?'2':'4';
if (tdesc==NULL) // turnout with no description
StringFormatter::send(stream,F("]\\[%d}|{T%d}|{T%c"), id,id,tchar);
else
StringFormatter::send(stream,F("]\\[%d}|{%S}|{%c"), id,tdesc,tchar);
}
StringFormatter::send(stream,F("\n"));
turnoutListHash = Turnout::turnoutlistHash; // keep a copy of hash for later comparison
}
}
while (cmd[0]) {
switch (cmd[0]) {
case '*': // heartbeat control
if (cmd[1]=='+') heartBeatEnable=true;
else if (cmd[1]=='-') heartBeatEnable=false;
break;
case 'P':
if (cmd[1]=='P' && cmd[2]=='A' ) { //PPA power mode
DCCWaveform::mainTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
if (MotorDriver::commonFaultPin) // commonFaultPin prevents individual track handling
DCCWaveform::progTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
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
}
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 does not exist, create it
int addr = ((id - 1) / 4) + 1;
int subaddr = (id - 1) % 4;
Turnout::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);
}
Turnout::activate(id,newstate);
StringFormatter::send(stream, F("PTA%c%d\n"),newstate?'4':'2',id );
}
break;
case 'N': // Heartbeat (2), only send if connection completed by 'HU' message
if (initSent) {
StringFormatter::send(stream, F("*%d\n"),HEARTBEAT_SECONDS); // return timeout value
}
break;
case 'M': // multithrottle
multithrottle(stream, cmd);
break;
case 'H': // send initial connection info after receiving "HU" message
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("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);
initSent = true;
}
break;
case 'Q': //
LOOPLOCOS('*', -1) { // tell client to drop any locos still assigned to this WiThrottle
if (myLocos[loco].throttle!='\0') {
StringFormatter::send(stream, F("M%c-%c%d<;>\n"), myLocos[loco].throttle, LorS(myLocos[loco].cab), myLocos[loco].cab);
}
}
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d) Quit"),millis(),clientid);
delete this;
break;
}
// skip over cmd until 0 or past \r or \n
while(*cmd !='\0' && *cmd != '\r' && *cmd !='\n') cmd++;
if (*cmd!='\0') cmd++; // skip \r or \n
}
}
int WiThrottle::getInt(byte * cmd) {
int i=0;
while (cmd[0]>='0' && cmd[0]<='9') {
i=i*10 + (cmd[0]-'0');
cmd++;
else if (!exRailSent) {
// Send EX-RAIL routes list if not already sent (but not at same time as turnouts above)
exRailSent=true;
#ifdef EXRAIL_ACTIVE
StringFormatter::send(stream,F("PRT]\\[Routes}|{Route]\\[Set}|{2]\\[Handoff}|{4\nPRL"));
for (byte pass=0;pass<2;pass++) {
// first pass automations, second pass routes.
for (int ix=0;;ix++) {
int16_t id=GETFLASHW((pass?RMFT2::automationIdList:RMFT2::routeIdList)+ix);
if (id==0) break;
const FSH * desc=RMFT2::getRouteDescription(id);
StringFormatter::send(stream,F("]\\[%c%d}|{%S}|{%c"),
pass?'A':'R',id,desc, pass?'4':'2');
}
return i;
}
StringFormatter::send(stream,F("\n"));
#endif
// allow heartbeat to slow down once all metadata sent
StringFormatter::send(stream,F("*%d\n"),HEARTBEAT_SECONDS);
}
}
while (cmd[0]) {
switch (cmd[0]) {
case '*': // heartbeat control
if (cmd[1]=='+') heartBeatEnable=true;
else if (cmd[1]=='-') heartBeatEnable=false;
break;
case 'P':
if (cmd[1]=='P' && cmd[2]=='A' ) { //PPA power mode
TrackManager::setMainPower(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
/* TODO
if (MotorDriver::commonFaultPin) // commonFaultPin prevents individual track handling
DCCWaveform::progTrack.setPowerMode(cmd[3]=='1'?POWERMODE::ON:POWERMODE::OFF);
*/
CommandDistributor::broadcastPower();
}
#if defined(EXRAIL_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);
if (!Turnout::exists(id)) {
// If turnout does not exist, create it
int addr = ((id - 1) / 4) + 1;
int subaddr = (id - 1) % 4;
DCCTurnout::create(id,addr,subaddr);
StringFormatter::send(stream, F("HmTurnout %d created\n"),id);
}
switch (cmd[3]) {
// 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;
}
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
StringFormatter::send(stream, F("*%d\n"), initSent ? HEARTBEAT_SECONDS : 1); // return timeout value
break;
case 'M': // multithrottle
multithrottle(stream, cmd);
break;
case 'H': // send initial connection info after receiving "HU" message
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));
StringFormatter::send(stream,F("PTT]\\[Turnouts}|{Turnout]\\[THROW}|{2]\\[CLOSE}|{4\n"));
StringFormatter::send(stream,F("PPA%x\n"),TrackManager::getMainPower()==POWERMODE::ON);
#ifdef EXRAIL_ACTIVE
StringFormatter::send(stream,F("RL%d"), RMFT2::rosterNameCount);
for (int16_t r=0;r<RMFT2::rosterNameCount;r++) {
int16_t cabid=GETFLASHW(RMFT2::rosterIdList+r);
StringFormatter::send(stream,F("]\\[%S}|{%d}|{%c"),
RMFT2::getRosterName(cabid),cabid,cabid<128?'S':'L');
}
stream->write('\n'); // end roster
#endif
// set heartbeat to 1 second because we need to sync the metadata
StringFormatter::send(stream,F("*1\n"));
initSent = true;
}
break;
case 'Q': //
LOOPLOCOS('*', -1) { // tell client to drop any locos still assigned to this WiThrottle
if (myLocos[loco].throttle!='\0') {
StringFormatter::send(stream, F("M%c-%c%d<;>\n"), myLocos[loco].throttle, LorS(myLocos[loco].cab), myLocos[loco].cab);
}
}
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d) Quit"),millis(),clientid);
delete this;
break;
}
// skip over cmd until 0 or past \r or \n
while(*cmd !='\0' && *cmd != '\r' && *cmd !='\n') cmd++;
if (*cmd!='\0') cmd++; // skip \r or \n
}
}
int WiThrottle::getInt(byte * cmd) {
int i=0;
bool negate=cmd[0]=='-';
if (negate) cmd++;
while (cmd[0]>='0' && cmd[0]<='9') {
i=i*10 + (cmd[0]-'0');
cmd++;
}
if (negate) i=0-i;
return i ;
}
int WiThrottle::getLocoId(byte * cmd) {
@@ -209,139 +273,186 @@ int WiThrottle::getLocoId(byte * cmd) {
}
void WiThrottle::multithrottle(RingStream * stream, byte * cmd){
char throttleChar=cmd[1];
int locoid=getLocoId(cmd+3); // -1 for *
byte * aval=cmd;
while(*aval !=';' && *aval !='\0') aval++;
if (*aval) aval+=2; // skip ;>
// DIAG(F("Multithrottle aval=%c cab=%d"), aval[0],locoid);
switch(cmd[2]) {
case '+': // add loco request
if (cmd[3]=='*') {
// M+* means get loco from prog track, then join tracks ready to drive away
// Stash the things the callback will need later
stashStream= stream;
stashClient=stream->peekTargetMark();
stashThrottleChar=throttleChar;
stashInstance=this;
// ask DCC to call us back when the loco id has been read
DCC::getLocoId(getLocoCallback); // will remove any previous join
return; // return nothing in stream as response is sent later in the callback
}
//return error if address zero requested
if (locoid==0) {
StringFormatter::send(stream, F("HMAddress '0' not supported!\n"), cmd[3] ,locoid);
return;
}
//return error if L or S from request doesn't match DCC++ assumptions
if (cmd[3] != LorS(locoid)) {
StringFormatter::send(stream, F("HMLength '%c' not valid for %d!\n"), cmd[3] ,locoid);
return;
}
//use first empty "slot" on this client's list, will be added to DCC registration list
for (int loco=0;loco<MAX_MY_LOCO;loco++) {
if (myLocos[loco].throttle=='\0') {
myLocos[loco].throttle=throttleChar;
myLocos[loco].cab=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++) {
char throttleChar=cmd[1];
int locoid=getLocoId(cmd+3); // -1 for *
byte * aval=cmd;
while(*aval !=';' && *aval !='\0') aval++;
if (*aval) aval+=2; // skip ;>
// DIAG(F("Multithrottle aval=%c cab=%d"), aval[0],locoid);
switch(cmd[2]) {
case '+': // add loco request
if (cmd[3]=='*') {
// M+* means get loco from prog track, then join tracks ready to drive away
// Stash the things the callback will need later
stashStream= stream;
stashClient=stream->peekTargetMark();
stashThrottleChar=throttleChar;
stashInstance=this;
// ask DCC to call us back when the loco id has been read
DCC::getLocoId(getLocoCallback); // will remove any previous join
return; // return nothing in stream as response is sent later in the callback
}
//return error if address zero requested
if (locoid==0) {
StringFormatter::send(stream, F("HMAddress '0' not supported!\n"), cmd[3] ,locoid);
return;
}
//return error if L or S from request doesn't match DCC++ assumptions
if (cmd[3] != LorS(locoid)) {
StringFormatter::send(stream, F("HMLength '%c' not valid for %d!\n"), cmd[3] ,locoid);
return;
}
//use first empty "slot" on this client's list, will be added to DCC registration list
for (int loco=0;loco<MAX_MY_LOCO;loco++) {
if (myLocos[loco].throttle=='\0') {
myLocos[loco].throttle=throttleChar;
myLocos[loco].cab=locoid;
myLocos[loco].functionMap=DCC::getFunctionMap(locoid);
myLocos[loco].broadcastPending=true; // means speed/dir will be sent later
mostRecentCab=locoid;
StringFormatter::send(stream, F("M%c+%c%d<;>\n"), throttleChar, cmd[3] ,locoid); //tell client to add loco
int fkeys=29;
myLocos[loco].functionToggles=1<<2; // F2 (HORN) is a non-toggle
#ifdef EXRAIL_ACTIVE
const char * functionNames=(char *) RMFT2::getRosterFunctions(locoid);
if (!functionNames) {
// no roster, use presets as above
}
else if (GETFLASH(functionNames)=='\0') {
// "" = Roster but no functions given
fkeys=0;
}
else {
// we have function names...
// scan names list emitting names, counting functions and
// flagging non-toggling things like horn.
myLocos[loco].functionToggles =0;
StringFormatter::send(stream, F("M%cL%c%d<;>]\\["), throttleChar,cmd[3],locoid);
fkeys=0;
bool firstchar=true;
for (int fx=0;;fx++) {
char c=GETFLASH(functionNames+fx);
if (c=='\0') {
fkeys++;
break;
}
if (c=='/') {
fkeys++;
StringFormatter::send(stream,F("]\\["));
firstchar=true;
}
else if (firstchar && c=='*') {
myLocos[loco].functionToggles |= 1UL<<fkeys;
firstchar=false;
}
else {
firstchar=false;
stream->write(c);
}
}
StringFormatter::send(stream,F("\n"));
}
#endif
for(int fKey=0; fKey<fkeys; fKey++) {
int fstate=DCC::getFn(locoid,fKey);
if (fstate>=0) StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"),throttleChar,cmd[3],locoid,fstate,fKey);
}
StringFormatter::send(stream, F("M%cA%c%d<;>V%d\n"), throttleChar, cmd[3], locoid, DCCToWiTSpeed(DCC::getThrottleSpeed(locoid)));
StringFormatter::send(stream, F("M%cA%c%d<;>R%d\n"), throttleChar, cmd[3], locoid, DCC::getThrottleDirection(locoid));
StringFormatter::send(stream, F("M%cA%c%d<;>s1\n"), throttleChar, cmd[3], locoid); //default speed step 128
return;
}
}
StringFormatter::send(stream, F("HMMax locos (%d) exceeded, %d not added!\n"), MAX_MY_LOCO ,locoid);
break;
case '-': // remove loco(s) from this client (leave in DCC registration)
LOOPLOCOS(throttleChar, locoid) {
myLocos[loco].throttle='\0';
StringFormatter::send(stream, F("M%c-%c%d<;>\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab);
}
break;
case 'A':
locoAction(stream,aval, throttleChar, locoid);
}
if (fstate>=0) StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"),throttleChar,cmd[3],locoid,fstate,fKey);
}
//speed and direction will be published at next broadcast cycle
StringFormatter::send(stream, F("M%cA%c%d<;>s1\n"), throttleChar, cmd[3], locoid); //default speed step 128
return;
}
}
StringFormatter::send(stream, F("HMMax locos (%d) exceeded, %d not added!\n"), MAX_MY_LOCO ,locoid);
break;
case '-': // remove loco(s) from this client (leave in DCC registration)
LOOPLOCOS(throttleChar, locoid) {
myLocos[loco].throttle='\0';
StringFormatter::send(stream, F("M%c-%c%d<;>\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab);
}
break;
case 'A':
locoAction(stream,aval, throttleChar, locoid);
}
}
void WiThrottle::locoAction(RingStream * stream, byte* aval, char throttleChar, int cab){
// Note cab=-1 for all cabs in the consist called throttleChar.
// DIAG(F("Loco Action aval=%c%c throttleChar=%c, cab=%d"), aval[0],aval[1],throttleChar, cab);
switch (aval[0]) {
case 'V': // Vspeed
{
int witSpeed=getInt(aval+1);
LOOPLOCOS(throttleChar, 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);
}
}
break;
case 'F': //F onOff function
{
bool funcstate;
bool pressed=aval[1]=='1';
int fKey = getInt(aval+2);
LOOPLOCOS(throttleChar, cab) {
funcstate = DCC::changeFn(myLocos[loco].cab, fKey, pressed);
if(funcstate==0 || funcstate==1)
StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"), throttleChar, LorS(myLocos[loco].cab),
myLocos[loco].cab, funcstate, fKey);
}
}
break;
case 'q':
if (aval[1]=='V') { //qV
LOOPLOCOS(throttleChar, cab) {
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, DCCToWiTSpeed(DCC::getThrottleSpeed(myLocos[loco].cab)));
}
}
else if (aval[1]=='R') { // qR
LOOPLOCOS(throttleChar, cab) {
StringFormatter::send(stream,F("M%cA%c%d<;>R%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, DCC::getThrottleDirection(myLocos[loco].cab));
}
}
break;
case 'R':
{
bool forward=aval[1]!='0';
LOOPLOCOS(throttleChar, 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);
}
}
break;
case 'X':
//Emergency Stop (speed code 1)
LOOPLOCOS(throttleChar, cab) {
DCC::setThrottle(myLocos[loco].cab, 1, DCC::getThrottleDirection(myLocos[loco].cab));
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"), throttleChar, LorS(myLocos[loco].cab), myLocos[loco].cab, -1);
}
break;
case 'I': // Idle, set speed to 0
case 'Q': // Quit, set speed to 0
LOOPLOCOS(throttleChar, 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);
}
break;
}
// Note cab=-1 for all cabs in the consist called throttleChar.
// DIAG(F("Loco Action aval=%c%c throttleChar=%c, cab=%d"), aval[0],aval[1],throttleChar, cab);
(void) stream;
switch (aval[0]) {
case 'V': // Vspeed
{
int witSpeed=getInt(aval+1);
LOOPLOCOS(throttleChar, cab) {
mostRecentCab=myLocos[loco].cab;
DCC::setThrottle(myLocos[loco].cab, WiTToDCCSpeed(witSpeed), DCC::getThrottleDirection(myLocos[loco].cab));
// SetThrottle will cause speed change broadcast
}
}
break;
case 'F': // Function key pressed/released
{
bool pressed=aval[1]=='1';
int fKey = getInt(aval+2);
LOOPLOCOS(throttleChar, cab) {
bool unsetOnRelease = myLocos[loco].functionToggles & (1L<<fKey);
if (unsetOnRelease) DCC::setFn(myLocos[loco].cab,fKey, pressed);
else if (pressed) DCC::changeFn(myLocos[loco].cab, fKey);
}
break;
}
case 'q':
if (aval[1]=='V' || aval[1]=='R' ) { //qV or qR
// just flag the loco for broadcast and it will happen.
LOOPLOCOS(throttleChar, cab) {
myLocos[loco].broadcastPending=true;
}
}
break;
case 'R':
{
bool forward=aval[1]!='0';
LOOPLOCOS(throttleChar, cab) {
mostRecentCab=myLocos[loco].cab;
int8_t speed = DCC::getThrottleSpeed(myLocos[loco].cab);
if (speed < 0) //can not find any speed for this cab
speed = 0;
DCC::setThrottle(myLocos[loco].cab, speed, forward);
// setThrottle will cause a broadcast so notification will be sent
}
}
break;
case 'X':
//Emergency Stop (speed code 1)
LOOPLOCOS(throttleChar, cab) {
DCC::setThrottle(myLocos[loco].cab, 1, DCC::getThrottleDirection(myLocos[loco].cab));
// setThrottle will cause a broadcast so notification will be sent
}
break;
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));
// setThrottle will cause a broadcast so notification will be sent
}
break;
}
}
// convert between DCC++ speed values and WiThrottle speed values
// convert between DCC++ speed values and WiThrottle speed values
int WiThrottle::DCCToWiTSpeed(int DCCSpeed) {
if (DCCSpeed == 0) return 0; //stop is stop
if (DCCSpeed == 1) return -1; //eStop value
return DCCSpeed - 1; //offset others by 1
}
// convert between WiThrottle speed values and DCC++ speed values
// convert between WiThrottle speed values and DCC++ speed values
int WiThrottle::WiTToDCCSpeed(int WiTSpeed) {
if (WiTSpeed == 0) return 0; //stop is stop
if (WiTSpeed == -1) return 1; //eStop value
@@ -349,24 +460,17 @@ int WiThrottle::WiTToDCCSpeed(int WiTSpeed) {
}
void WiThrottle::loop(RingStream * stream) {
// for each WiThrottle, check the heartbeat
// for each WiThrottle, check the heartbeat and broadcast needed
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
wt->checkHeartbeat();
// TODO... any broadcasts to be done
(void)stream;
/* MUST follow this model in this loop.
* stream->mark();
* send 1 digit client id, and any data
* stream->commit()
*/
wt->checkHeartbeat(stream);
}
void WiThrottle::checkHeartbeat() {
void WiThrottle::checkHeartbeat(RingStream * stream) {
// if eStop time passed... eStop any locos still assigned to this client and then drop the connection
if(heartBeatEnable && (millis()-heartBeat > ESTOP_SECONDS*1000)) {
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d) eStop(%ds) timeout, drop connection"), millis(), clientid, ESTOP_SECONDS);
if (Diag::WITHROTTLE) DIAG(F("%l WiThrottle(%d) eStop(%ds) timeout, drop connection"), millis(), clientid, ESTOP_SECONDS);
LOOPLOCOS('*', -1) {
if (myLocos[loco].throttle!='\0') {
if (Diag::WITHROTTLE) DIAG(F("%l eStopping cab %d"),millis(),myLocos[loco].cab);
@@ -374,15 +478,65 @@ void WiThrottle::checkHeartbeat() {
}
}
delete this;
}
return;
}
// send any outstanding speed/direction/function changes for this clients locos
// Changes may have been caused by this client, or another non-Withrottle or Exrail
bool streamHasBeenMarked=false;
LOOPLOCOS('*', -1) {
if (myLocos[loco].throttle!='\0' && myLocos[loco].broadcastPending) {
if (!streamHasBeenMarked) {
stream->mark(clientid);
streamHasBeenMarked=true;
}
myLocos[loco].broadcastPending=false;
int cab=myLocos[loco].cab;
char lors=LorS(cab);
char throttle=myLocos[loco].throttle;
StringFormatter::send(stream,F("M%cA%c%d<;>V%d\n"),
throttle, lors , cab, DCCToWiTSpeed(DCC::getThrottleSpeed(cab)));
StringFormatter::send(stream,F("M%cA%c%d<;>R%d\n"),
throttle, lors , cab, DCC::getThrottleDirection(cab));
// compare the DCC functionmap with the local copy and send changes
uint32_t dccFunctionMap=DCC::getFunctionMap(cab);
uint32_t myFunctionMap=myLocos[loco].functionMap;
myLocos[loco].functionMap=dccFunctionMap;
// loop the maps sending any bit changed
// Loop is terminated as soon as no changes are left
for (byte fn=0;dccFunctionMap!=myFunctionMap;fn++) {
if ((dccFunctionMap&1) != (myFunctionMap&1)) {
StringFormatter::send(stream,F("M%cA%c%d<;>F%c%d\n"),
throttle, lors , cab, (dccFunctionMap&1)?'1':'0',fn);
}
// shift just checked bit off end of both maps
dccFunctionMap>>=1;
myFunctionMap>>=1;
}
}
}
if (streamHasBeenMarked) stream->commit();
}
void WiThrottle::markForBroadcast(int cab) {
for (WiThrottle* wt=firstThrottle; wt!=NULL ; wt=wt->nextThrottle)
wt->markForBroadcast2(cab);
}
void WiThrottle::markForBroadcast2(int cab) {
LOOPLOCOS('*', cab) {
myLocos[loco].broadcastPending=true;
}
}
char WiThrottle::LorS(int cab) {
return (cab<127)?'S':'L';
return (cab<=HIGHEST_SHORT_ADDR)?'S':'L';
}
// Drive Away feature. Callback handling
RingStream * WiThrottle::stashStream;
WiThrottle * WiThrottle::stashInstance;
byte WiThrottle::stashClient;
@@ -390,13 +544,33 @@ char WiThrottle::stashThrottleChar;
void WiThrottle::getLocoCallback(int16_t locoid) {
stashStream->mark(stashClient);
if (locoid<0) StringFormatter::send(stashStream,F("HMNo loco found on prog track\n"));
else {
char addcmd[20]={'M',stashThrottleChar,'+',LorS(locoid) };
itoa(locoid,addcmd+4,10);
stashInstance->multithrottle(stashStream, (byte *)addcmd);
DCCWaveform::progTrack.setPowerMode(POWERMODE::ON);
DCC::setProgTrackSyncMain(true); // <1 JOIN> so we can drive loco away
if (locoid<=0) {
StringFormatter::send(stashStream,F("HMNo loco found on prog track\n"));
stashStream->commit(); // done here, commit and return
return;
}
// short or long
char addrchar;
if (locoid & LONG_ADDR_MARKER) { // maker bit indicates long addr
locoid = locoid ^ LONG_ADDR_MARKER; // remove marker bit to get real long addr
if (locoid <= HIGHEST_SHORT_ADDR ) { // out of range for long addr
StringFormatter::send(stashStream,F("HMLong addr %d <= %d unsupported\n"), locoid, HIGHEST_SHORT_ADDR);
stashStream->commit(); // done here, commit and return
return;
}
addrchar = 'L';
} else {
addrchar = 'S';
}
char addcmd[20]={'M',stashThrottleChar,'+', addrchar};
itoa(locoid,addcmd+4,10);
stashInstance->multithrottle(stashStream, (byte *)addcmd);
TrackManager::setMainPower(POWERMODE::ON);
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
stashStream->commit();
CommandDistributor::broadcastPower();
}

View File

@@ -1,5 +1,7 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2021 Mike S
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -24,6 +26,9 @@
struct MYLOCO {
char throttle; //indicates which throttle letter on client, often '0','1' or '2'
int cab; //address of this loco
bool broadcastPending;
uint32_t functionMap;
uint32_t functionToggles;
};
class WiThrottle {
@@ -31,14 +36,15 @@ class WiThrottle {
static void loop(RingStream * stream);
void parse(RingStream * stream, byte * cmd);
static WiThrottle* getThrottle( int wifiClient);
static bool annotateLeftRight;
static void markForBroadcast(int cab);
private:
WiThrottle( int wifiClientId);
~WiThrottle();
static const int MAX_MY_LOCO=10; // maximum number of locos assigned to a single client
static const int HEARTBEAT_SECONDS=4; // heartbeat at 4secs to provide messaging transport
static const int ESTOP_SECONDS=8; // eStop if no incoming messages for more than 8secs
static const int HEARTBEAT_SECONDS=10; // heartbeat at 4secs to provide messaging transport
static const int ESTOP_SECONDS=20; // eStop if no incoming messages for more than 8secs
static WiThrottle* firstThrottle;
static int getInt(byte * cmd);
static int getLocoId(byte * cmd);
@@ -53,6 +59,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);
@@ -60,8 +68,8 @@ class WiThrottle {
void multithrottle(RingStream * stream, byte * cmd);
void locoAction(RingStream * stream, byte* aval, char throttleChar, int cab);
void accessory(RingStream *, byte* cmd);
void checkHeartbeat();
void checkHeartbeat(RingStream * stream);
void markForBroadcast2(int cab);
// callback stuff to support prog track acquire
static RingStream * stashStream;
static WiThrottle * stashInstance;

View File

@@ -1,4 +1,7 @@
/*
* © 2021 Fred Decker
* © 2021 Fred Decker
* © 2020-2021 Chris Harlow
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth.
*
@@ -85,7 +88,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;
}
}
@@ -150,7 +155,7 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
if (ch=='E' || ch=='l') { // ERROR or "link is not valid"
if (clientPendingCIPSEND>=0) {
// A CIPSEND was errored... just toss it away
purgeCurrentCIPSEND();
purgeCurrentCIPSEND();
}
loopState=SKIPTOEND;
break;
@@ -229,6 +234,7 @@ WifiInboundHandler::INBOUND_STATE WifiInboundHandler::loop2() {
if (ch=='C') {
// got "x C" before CLOSE or CONNECTED, or CONNECT FAILED
if (runningClientId==clientPendingCIPSEND) purgeCurrentCIPSEND();
else CommandDistributor::forget(runningClientId);
}
loopState=SKIPTOEND;
break;
@@ -243,8 +249,9 @@ 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);
for (int i=0;i<=currentReplySize;i++) outboundRing->read();
CommandDistributor::forget(clientPendingCIPSEND);
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,4 +1,6 @@
/*
* © 2021 Harald Barth
* © 2021 Fred Decker
* (c) 2021 Fred Decker. All rights reserved.
* (c) 2020 Chris Harlow. All rights reserved.
*

View File

@@ -1,26 +1,28 @@
/*
© 2020, Chris Harlow. All rights reserved.
© 2020, Harald Barth.
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/>.
*/
* © 2021 Fred Decker
* © 2020-2022 Harald Barth
* © 2020-2022 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 ARDUINO_AVR_UNO_WIFI_REV2
// This code is NOT compiled on a unoWifiRev2 processor which uses a different architecture
#include "WifiInterface.h" /* config.h included there */
#include <avr/pgmspace.h>
//#include <avr/pgmspace.h>
#include "DIAG.h"
#include "StringFormatter.h"
@@ -75,13 +77,13 @@ bool WifiInterface::setup(long serial_link_speed,
(void) channel;
#endif
#if NUM_SERIAL > 0
#if NUM_SERIAL > 0 && !defined(SERIAL1_COMMANDS)
Serial1.begin(serial_link_speed);
wifiUp = setup(Serial1, wifiESSID, wifiPassword, hostname, port, channel);
#endif
// Other serials are tried, depending on hardware.
#if NUM_SERIAL > 1
#if NUM_SERIAL > 1 && !defined(SERIAL2_COMMANDS)
if (wifiUp == WIFI_NOAT)
{
Serial2.begin(serial_link_speed);
@@ -89,7 +91,7 @@ bool WifiInterface::setup(long serial_link_speed,
}
#endif
#if NUM_SERIAL > 2
#if NUM_SERIAL > 2 && !defined(SERIAL3_COMMANDS)
if (wifiUp == WIFI_NOAT)
{
Serial3.begin(serial_link_speed);
@@ -277,10 +279,16 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password,
StringFormatter::send(wifiStream, F("AT+CIPSERVER=0\r\n")); // turn off tcp server (to clean connections before CIPMUX=1)
checkForOK(1000, true); // ignore result in case it already was off
StringFormatter::send(wifiStream, F("AT+CIPMUX=1\r\n")); // configure for multiple connections
if (!checkForOK(1000, true)) return WIFI_DISCONNECTED;
if(!oldCmd) { // no idea to test this on old firmware
StringFormatter::send(wifiStream, F("AT+MDNS=1,\"%S\",\"withrottle\",%d\r\n"),
hostname, port); // mDNS responder
checkForOK(1000, true); // dont care if not supported
}
StringFormatter::send(wifiStream, F("AT+CIPSERVER=1,%d\r\n"), port); // turn on server on port
if (!checkForOK(1000, true)) return WIFI_DISCONNECTED;
#endif //DONT_TOUCH_WIFI_CONF
@@ -316,19 +324,45 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password,
// This function is used to allow users to enter <+ commands> through the DCCEXParser
// <+command> sends AT+command to the ES and returns to the caller.
// Once the user has made whatever changes to the AT commands, a <+X> command can be used
// to force on the connectd flag so that the loop will start picking up wifi traffic.
// If the settings are corrupted <+RST> will clear this and then you must restart the arduino.
// Using the <+> command with no command string causes the code to enter an echo loop so that all
// input is directed to the ES and all ES output written to the USB Serial.
// The sequence "!!!" returns the Arduino to the normal loop mode
void WifiInterface::ATCommand(const byte * command) {
void WifiInterface::ATCommand(HardwareSerial * stream,const byte * command) {
command++;
if (*command=='\0') { // User gave <+> command
stream->print(F("\nES AT command passthrough mode, use ! to exit\n"));
while(stream->available()) stream->read(); // Drain serial input first
bool startOfLine=true;
while(true) {
while (wifiStream->available()) stream->write(wifiStream->read());
if (stream->available()) {
int cx=stream->read();
// A newline followed by !!! is an exit
if (cx=='\n' || cx=='\r') startOfLine=true;
else if (startOfLine && cx=='!') break;
else startOfLine=false;
stream->write(cx);
wifiStream->write(cx);
}
}
stream->print(F("Passthrough Ended"));
return;
}
if (*command=='X') {
connected = true;
DIAG(F("++++++ Wifi Connction forced on ++++++++"));
connected = true;
DIAG(F("++++++ Wifi Connction forced on ++++++++"));
}
else {
StringFormatter:: send(wifiStream, F("AT+%s\r\n"), command);
checkForOK(10000, true);
StringFormatter:: send(wifiStream, F("AT+%s\r\n"), command);
checkForOK(10000, true);
}
}

View File

@@ -1,7 +1,8 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2020-2021 Chris Harlow
* © 2020, Harald Barth.
*
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
@@ -22,7 +23,7 @@
#include "FSH.h"
#include "DCCEXParser.h"
#include <Arduino.h>
#include <avr/pgmspace.h>
//#include <avr/pgmspace.h>
enum wifiSerialState { WIFI_NOAT, WIFI_DISCONNECTED, WIFI_CONNECTED };
@@ -37,7 +38,7 @@ public:
const int port,
const byte channel);
static void loop();
static void ATCommand(const byte *command);
static void ATCommand(HardwareSerial * stream,const byte *command);
private:
static wifiSerialState setup(Stream &setupStream, const FSH *SSSid, const FSH *password,

View File

@@ -1,5 +1,8 @@
/*
* COPYRIGHT (c) 2020 Fred Decker
* © 2021 Neil McKechnie
* © 2020-2021 Harald Barth
* © 2020-2021 Fred Decker
* © 2020-2021 Chris Harlow
*
* This file is part of CommandStation-EX
*
@@ -26,7 +29,7 @@ The configuration file for DCC-EX Command Station
/////////////////////////////////////////////////////////////////////////////////////
// NOTE: Before connecting these boards and selecting one in this software
// check the quick install guides!!! Some of these boards require a voltage
// generating resitor on the current sense pin of the device. Failure to select
// generating resistor on the current sense pin of the device. Failure to select
// the correct resistor could damage the sense pin on your Arduino or destroy
// the device.
//
@@ -34,6 +37,7 @@ The configuration file for DCC-EX Command Station
//
// STANDARD_MOTOR_SHIELD : Arduino Motor shield Rev3 based on the L298 with 18V 2A per channel
// POLOLU_MOTOR_SHIELD : Pololu MC33926 Motor Driver (not recommended for prog track)
// POLOLU_TB9051FTG : Pololu Dual TB9051FTG Motor Driver
// FUNDUMOTO_SHIELD : Fundumoto Shield, no current sensing (not recommended, no short protection)
// FIREBOX_MK1 : The Firebox MK1
// FIREBOX_MK1S : The Firebox MK1S
@@ -82,7 +86,7 @@ The configuration file for DCC-EX Command Station
// WIFI_PASSWORD is the network password for your home network or if
// you want to change the password from default AP mode password
// to the AP password you want.
// Your password may not conain ``"'' (double quote, ASCII 0x22).
// Your password may not contain ``"'' (double quote, ASCII 0x22).
#define WIFI_PASSWORD "Your network passwd"
//
// WIFI_HOSTNAME: You probably don't need to change this
@@ -113,18 +117,84 @@ 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.
// Use 132,64 for a SH1106-based I2C device with a 128x64 display.
// #define OLED_DRIVER 128,32
/////////////////////////////////////////////////////////////////////////////////////
// Define scroll mode as 0, 1 or 2
// * #define SCROLLMODE 0 is scroll continuous (fill screen if poss),
// * #define SCROLLMODE 1 is by page (alternate between pages),
// * #define SCROLLMODE 2 is by row (move up 1 row at a time).
#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 work.
//
// #define DISABLE_EEPROM
/////////////////////////////////////////////////////////////////////////////////////
// REDEFINE WHERE SHORT/LONG ADDR break is. According to NMRA the last short address
// is 127 and the first long address is 128. There are manufacturers which have
// another view. Lenz CS for example have considered addresses long from 100. If
// you want to change to that mode, do
//#define HIGHEST_SHORT_ADDR 99
// If you want to run all your locos addressed long format, you could even do a
//#define HIGHEST_SHORT_ADDR 0
// We do not support to use the same address, for example 100(long) and 100(short)
// at the same time, there must be a border.
/////////////////////////////////////////////////////////////////////////////////////
//
// 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
// By default, the driver which defines a DCC accessory decoder
// does send out the same state change on the DCC packet as it
// receives. This means a VPIN state=1 sends D=1 (close turnout
// or signal green) in the DCC packet. This can be reversed if
// necessary.
//#define HAL_ACCESSORY_COMMAND_REVERSE
// If you have issues with that the direction of the accessory commands is
// reversed (for example when converting from another CS to DCC-EX) then
// you can use this to reverse the sense of all accessory commmands sent
// over DCC++. This #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
//
// HANDLING MULTIPLE SERIAL THROTTLES
// The command station always operates with the default Serial port.
// Diagnostics are only emitted on the default serial port and not broadcast.
// Other serial throttles may be added to the Serial1, Serial2, Serial3 ports
// which may or may not exist on your CPU. (Mega has all 3)
// To monitor a throttle on one or more serial ports, uncomment the defines below.
// NOTE: do not define here the WiFi shield serial port or your wifi will not work.
//
//#define SERIAL1_COMMANDS
//#define SERIAL2_COMMANDS
//#define SERIAL3_COMMANDS
/////////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,41 +1,71 @@
/*
© 2020, Harald Barth.
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
*
* 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 file is part of CommandStation-EX
#ifndef DEFINES_H
#define DEFINES_H
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/>.
*/
// 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) || defined(ARDUINO_AVR_NANO_EVERY) || defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32)
#define BIG_RAM
#endif
#if ENABLE_WIFI
#if defined(BIG_RAM)
#define WIFI_ON true
#ifndef WIFI_CHANNEL
#define WIFI_CHANNEL 1
#endif
#else
#define WIFI_WARNING
#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
#define ETHERNET_WARNING
#define ETHERNET_ON false
#endif
#else
#define ETHERNET_ON false
#define ETHERNET_ON false
#endif
#if WIFI_ON && ETHERNET_ON
@@ -48,3 +78,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 EXRAIL_ACTIVE
#else
#define EXRAIL_WARNING
#endif
#endif
#endif

View File

@@ -1,111 +0,0 @@
/*
* © 2020, Harald Barth
* © 2021, Neil McKechnie
*
* This file is part of Asbelos DCC-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 "freeMemory.h"
// thanks go to https://github.com/mpflaga/Arduino-MemoryFree
#if defined(__arm__)
extern "C" char* sbrk(int);
#elif defined(__AVR__)
extern char *__brkval;
extern char *__malloc_heap_start;
#else
#error Unsupported board type
#endif
static volatile int minimum_free_memory = __INT_MAX__;
#if !defined(__IMXRT1062__)
static inline int freeMemory() {
char top;
#if defined(__arm__)
return &top - reinterpret_cast<char*>(sbrk(0));
#elif defined(__AVR__)
return __brkval ? &top - __brkval : &top - __malloc_heap_start;
#else
#error bailed out already above
#endif
}
// Return low memory value.
int minimumFreeMemory() {
byte sreg_save = SREG;
noInterrupts(); // Disable interrupts
int retval = minimum_free_memory;
SREG = sreg_save; // Restore interrupt state
return retval;
}
#else
#if defined(ARDUINO_TEENSY40)
static const unsigned DTCM_START = 0x20000000UL;
static const unsigned OCRAM_START = 0x20200000UL;
static const unsigned OCRAM_SIZE = 512;
static const unsigned FLASH_SIZE = 1984;
#elif defined(ARDUINO_TEENSY41)
static const unsigned DTCM_START = 0x20000000UL;
static const unsigned OCRAM_START = 0x20200000UL;
static const unsigned OCRAM_SIZE = 512;
static const unsigned FLASH_SIZE = 7936;
#if TEENSYDUINO>151
extern "C" uint8_t external_psram_size;
#endif
#endif
static inline int freeMemory() {
extern unsigned long _ebss;
extern unsigned long _sdata;
extern unsigned long _estack;
const unsigned DTCM_START = 0x20000000UL;
unsigned dtcm = (unsigned)&_estack - DTCM_START;
unsigned stackinuse = (unsigned) &_estack - (unsigned) __builtin_frame_address(0);
unsigned varsinuse = (unsigned)&_ebss - (unsigned)&_sdata;
unsigned freemem = dtcm - (stackinuse + varsinuse);
return freemem;
}
// Return low memory value.
int minimumFreeMemory() {
//byte sreg_save = SREG;
//noInterrupts(); // Disable interrupts
int retval = minimum_free_memory;
//SREG = sreg_save; // Restore interrupt state
return retval;
}
#endif
// Update low ram level. Allow for extra bytes to be specified
// by estimation or inspection, that may be used by other
// called subroutines. Must be called with interrupts disabled.
//
// Although __brkval may go up and down as heap memory is allocated
// and freed, this function records only the worst case encountered.
// So even if all of the heap is freed, the reported minimum free
// memory will not increase.
//
void updateMinimumFreeMemory(unsigned char extraBytes) {
int spare = freeMemory()-extraBytes;
if (spare < 0) spare = 0;
if (spare < minimum_free_memory) minimum_free_memory = spare;
}

270
installer.json Normal file
View File

@@ -0,0 +1,270 @@
[
{
"Name": "BaseStationClassic",
"Git": "DCC-EX/BaseStation-Classic",
"Libraries": [
{
"Name": "Ethernet",
"Repo": "arduino-libraries/Ethernet",
"Location": "libraries/Ethernet",
"LibraryDownloadAvailable": true
}
],
"SupportedBoards": [
{
"Name": "Uno",
"FQBN": "arduino:avr:uno",
"Platforms": [
{
"Architecture": "avr",
"Package": "arduino"
}
],
"SupportedMotoShields": [
{
"Name": "Arduino Motor Shield",
"Declaration": "STANDARD_MOTOR_SHIELD"
},
{
"Name": "Pololu MC33926 Motor Shield",
"Declaration": "POLOLU_MOTOR_SHIELD"
}
],
"ExtraLibraries": []
},
{
"Name": "Mega",
"FQBN": "arduino:avr:mega",
"Platforms": [
{
"Architecture": "avr",
"Package": "arduino"
}
],
"SupportedMotoShields": [
{
"Name": "Arduino Motor Shield",
"Declaration": "STANDARD_MOTOR_SHIELD"
},
{
"Name": "Pololu MC33926 Motor Shield",
"Declaration": "POLOLU_MOTOR_SHIELD"
}
],
"ExtraLibraries": []
}
],
"DisplayName": "Base Station Classic",
"InputFileLocation": "DCCpp",
"AllowAdvanced": false,
"ConfigFile": "DCCpp/Config.h"
},
{
"Name": "CommandStation-EX",
"Git": "DCC-EX/CommandStation-EX",
"Libraries": [
{
"Name": "Ethernet",
"Repo": "arduino-libraries/Ethernet",
"Location": "libraries/Ethernet",
"LibraryDownloadAvailable": true
},
{
"Name": "DIO2",
"Repo": "",
"Location": "libraries/DIO2",
"LibraryDownloadAvailable": true
}
],
"SupportedBoards": [
{
"Name": "Uno",
"FQBN": "arduino:avr:uno",
"Platforms": [
{
"Architecture": "avr",
"Package": "arduino"
}
],
"SupportedMotoShields": [
{
"Name": "Arduino Motor Shield",
"Declaration": "STANDARD_MOTOR_SHIELD"
},
{
"Name": "Pololu MC33926 Motor Shield",
"Declaration": "POLOLU_MOTOR_SHIELD"
}
],
"ExtraLibraries": []
},
{
"Name": "Mega",
"FQBN": "arduino:avr:mega",
"Platforms": [
{
"Architecture": "avr",
"Package": "arduino"
}
],
"SupportedMotoShields": [
{
"Name": "Arduino Motor Shield",
"Declaration": "STANDARD_MOTOR_SHIELD"
},
{
"Name": "Pololu MC33926 Motor Shield",
"Declaration": "POLOLU_MOTOR_SHIELD"
}
],
"ExtraLibraries": []
},
{
"Name": "Mega Wifi",
"FQBN": "arduino:avr:mega",
"Platforms": [
{
"Architecture": "avr",
"Package": "arduino"
}
],
"SupportedMotoShields": [
{
"Name": "Arduino Motor Shield",
"Declaration": "STANDARD_MOTOR_SHIELD"
},
{
"Name": "Pololu MC33926 Motor Shield",
"Declaration": "POLOLU_MOTOR_SHIELD"
}
],
"ExtraLibraries": []
},
{
"Name": "Nano",
"FQBN": "arduino:avr:nano",
"Platforms": [
{
"Architecture": "avr",
"Package": "arduino"
}
],
"SupportedMotoShields": [
{
"Name": "Arduino Motor Shield",
"Declaration": "STANDARD_MOTOR_SHIELD"
},
{
"Name": "Pololu MC33926 Motor Shield",
"Declaration": "POLOLU_MOTOR_SHIELD"
}
],
"ExtraLibraries": []
},
{
"Name": "Nano Every",
"FQBN": "arduino:megaavr:nanoevery",
"Platforms": [
{
"Architecture": "megaavr",
"Package": "arduino"
}
],
"SupportedMotoShields": [
{
"Name": "Arduino Motor Shield",
"Declaration": "STANDARD_MOTOR_SHIELD"
},
{
"Name": "Pololu MC33926 Motor Shield",
"Declaration": "POLOLU_MOTOR_SHIELD"
}
],
"ExtraLibraries": []
},
{
"Name": "Teensy 3.x",
"FQBN": "teensy:avr:teensy31",
"Platforms": [
{
"Architecture": "teensy",
"Package": "arduino"
}
],
"SupportedMotoShields": [
{
"Name": "Arduino Motor Shield",
"Declaration": "STANDARD_MOTOR_SHIELD"
},
{
"Name": "Pololu MC33926 Motor Shield",
"Declaration": "POLOLU_MOTOR_SHIELD"
}
],
"ExtraLibraries": []
},
{
"Name": "Teensy 4.x",
"FQBN": "teensy:avr:teensy41",
"Platforms": [
{
"Architecture": "teensy",
"Package": "arduino"
}
],
"SupportedMotoShields": [
{
"Name": "Arduino Motor Shield",
"Declaration": "STANDARD_MOTOR_SHIELD"
},
{
"Name": "Pololu MC33926 Motor Shield",
"Declaration": "POLOLU_MOTOR_SHIELD"
}
],
"ExtraLibraries": []
},
{
"Name": "SAMD21",
"FQBN": "SparkFun:samd",
"Platforms": [
{
"Architecture": "avr",
"Package": "arduino"
},
{
"Architecture": "samd",
"Package": "arduino"
},
{
"Architecture": "samd",
"Package": "SparkFun"
}
],
"SupportedMotoShields": [
{
"Name": "Arduino Motor Shield",
"Declaration": "STANDARD_MOTOR_SHIELD"
},
{
"Name": "Pololu MC33926 Motor Shield",
"Declaration": "POLOLU_MOTOR_SHIELD"
},
{
"Name": "FireBox MK1",
"Declaration": "FIREBOX_MK1"
},
{
"Name": "FireBox MK1S",
"Declaration": "FIREBOX_MK1S"
}
],
"ExtraLibraries": []
}
],
"DisplayName": "CommandStation EX",
"InputFileLocation": "",
"AllowAdvanced": true,
"ConfigFile": "config.h"
}
]

Some files were not shown because too many files have changed in this diff Show More