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

Compare commits

...

219 Commits

Author SHA1 Message Date
peteGSX
87073b0d36 Rotary Encoder address 0x67 2024-03-23 13:31:34 +10:00
Harald Barth
0587e6fc09 version 5.2.40 2024-03-18 21:18:57 +01:00
Harald Barth
3cda869c6e Allow no shield 2024-03-18 21:17:51 +01:00
Harald Barth
59d855549e version tag 2024-03-12 11:47:25 +01:00
Harald Barth
e3081a7e56 Functions for DC frequency: Use func up to F31 part 2 2024-03-12 11:45:28 +01:00
Harald Barth
8eec85edcf version 5.2.39 2024-03-11 14:54:18 +01:00
Harald Barth
d753eb43e3 Functions for DC frequency: Use func up to F31 2024-03-11 14:52:55 +01:00
Asbelos
9aac34b403 comments 2024-03-11 12:26:28 +00:00
Asbelos
be218d3032 EXRAIL MESSAGE() 2024-03-08 20:33:11 +00:00
Asbelos
4b04a80e6f Remove unnecessary warning 2024-03-05 19:59:29 +00:00
Harald Barth
b752666899 remove warning 2024-03-04 15:20:48 +01:00
Harald Barth
3d6f41398d compile time check WIFI_PASSWORD length for reasonable value 2024-03-04 15:07:03 +01:00
Harald Barth
7503421eb6 Compile time optimization for booster mode 2024-02-26 09:11:21 +01:00
Harald Barth
274affce45 version 5.2.37 2024-02-24 20:57:38 +01:00
Harald Barth
b29a01f436 ESP32: Use the BOOSTER_INPUT define 2024-02-24 20:56:06 +01:00
Harald Barth
1101cfd637 surpress warnings 2024-02-24 17:24:55 +01:00
Harald Barth
3fa2edb0da fix warning and indent proper 2024-02-20 15:13:22 +01:00
Harald Barth
423d1932ae merge artifact 2024-02-20 15:08:41 +01:00
Harald Barth
dec39a2ae1 Merge devel-freq 2024-02-20 15:06:07 +01:00
Asbelos
821115caad Make ASSERT macro match 5.2.35 2024-02-19 20:02:01 +00:00
Harald Barth
fe9b1da8a3 version 5.2.35 2024-02-17 18:52:48 +01:00
Harald Barth
fbbedc7577 make ext acc packet format follow RCN-213 2024-02-17 18:51:13 +01:00
Asbelos
dcd332603c accessory packet issues. 2024-02-17 11:09:03 +00:00
Asbelos
7e4093f03f comment only 2024-02-16 12:45:33 +00:00
Asbelos
7ee4188d88 Tidy and version 2024-02-16 12:36:33 +00:00
Asbelos
5742b71ec6 <A> to EXRAIL DCCX_SIGNAL intercept 2024-02-16 12:20:58 +00:00
Asbelos
8705c8c33f DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) 2024-02-16 11:49:02 +00:00
Asbelos
e4904e4080 Exrail ASPECT(addr,value) 2024-02-15 20:05:27 +00:00
Asbelos
59b0e8383d <A addr value> 2024-02-15 19:51:52 +00:00
Harald Barth
784088b0df get it into nano and uno again 2024-02-11 23:19:51 +01:00
Asbelos
c780b96856 Exrail CONFIGURE_SERVO 2024-02-09 11:54:53 +00:00
Asbelos
4b97d63cf3 Remove compiler warnings 2024-02-09 10:37:51 +00:00
Asbelos
6f1df6ce8e Merge branch 'devel_railcom_Mega' into devel 2024-02-09 10:30:08 +00:00
Asbelos
eacf48380b typos in version comments 2024-02-09 10:15:23 +00:00
Asbelos
8293749ac7 JMRI_SENSOR exrail 2024-02-07 22:11:27 +00:00
Asbelos
25cb878060 remove dross 2024-02-07 21:33:06 +00:00
Asbelos
7a9e225602 fill in debug and unsupported drivers 2024-02-07 21:24:48 +00:00
Asbelos
1443ea8df9 Its alive! 2024-02-07 19:50:08 +00:00
Asbelos
cd47782052 reasonable start 2024-02-06 20:03:52 +00:00
Harald Barth
4931c5ed75 tag 2024-02-05 09:28:07 +01:00
kempe63
53fec9bc3a Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2024-02-04 18:57:50 +00:00
kempe63
4780ea63cf Prepend I2CDFPlayer with DF_ to solve Nucleo RESET directive
Prepend all I2CDFPlayer EXRail commands with DF_ to solve a Nucleo defined RESET
2024-02-04 18:57:30 +00:00
Harald Barth
5f6e18e1e7 remove mega328 from default build env list 2024-02-04 13:59:10 +01:00
Harald Barth
be40a7e274 version 5.2.30 2024-02-04 12:51:41 +01:00
Harald Barth
e7f82bdf92 WiThrottle sendIntro after initial N message as well 2024-02-04 12:49:55 +01:00
kempe63
63702ae64e Update fro myHal.cpp_example.txt
Noticed some ommissions, corrected now
2024-02-03 19:27:58 +00:00
kempe63
7cbf4de1b9 Update myHal.cpp_example.txt
Update with instructions for IO_ I2CDFPlayer
2024-02-03 19:25:41 +00:00
kempe63
3c4e4bb14d Added support for DFPLayer over I2c - IO_I2CDFPlayerh
Added IO_I2CDFPlayer.h to support DFPLayer over I2C connected to NXP SC16IS750/SC16IS752 (currently only single UART for SC16IS752)
Added enhanced IO_I2CDFPLayer enum commands to EXRAIL2.h
Added PLAYSOUND alias of ANOUT to EXRAILMacros.h
EXRail additions as per advice from Chris
2024-02-03 19:05:56 +00:00
Harald Barth
6d0740eab4 version 5.2.28 2024-01-21 21:11:57 +01:00
Harald Barth
0a52a26d50 ESP32: Can all Wifi channels. Only write Wifi password to display if it is a well known one 2024-01-21 21:09:55 +01:00
Harald Barth
daa2ffc459 tag 2024-01-20 23:36:11 +01:00
Harald Barth
9728d19b19 eliminate warning 2024-01-20 23:35:30 +01:00
Harald Barth
99a09c713f To make usage easier, use F29 to F31 for frequencies 2024-01-20 23:34:17 +01:00
Harald Barth
811bce4b2a tag 2024-01-20 22:16:26 +01:00
Harald Barth
cf1e1c92b3 Check "easy" check first 2024-01-20 22:15:47 +01:00
Colin Murdoch
a5b73c823a Added SETFREQ command
Added SETFREQ command to EXRAIL
2024-01-20 18:09:03 +00:00
peteGSX
657c08c653 Update EX-IOExpander copyright 2024-01-18 18:56:15 +10:00
Harald Barth
bc37a2d2cf version 5.2.27 2024-01-18 08:22:28 +01:00
Harald Barth
3c0704dbd1 Bugfix: allocate enough bytes for digital pins. Add more sanity checks when allocating memory 2024-01-18 08:20:33 +01:00
Asbelos
95bf5aae38 HAL defaults control 2024-01-14 20:20:22 +00:00
Asbelos
8216579f62 5.2.25 <D> returns <X> bugs 2024-01-14 02:09:22 +00:00
Asbelos
a54a262f68 5.2.24 EXRAIL asserts 2024-01-14 02:03:42 +00:00
Asbelos
a508ee7055 Fix asserts for Teensy 2024-01-10 16:08:11 +00:00
Harald Barth
20ae915eaf remove unused ccr_reg variable 2024-01-10 15:23:52 +01:00
Harald Barth
35a0bde115 Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2024-01-10 15:10:57 +01:00
Harald Barth
d24d09c37a subversion 2024-01-10 15:10:25 +01:00
Harald Barth
9ab6b3d4ea Bugfix: Ethernet fixed IP start 2024-01-10 15:09:22 +01:00
Asbelos
d8c282434c _hk in myAutomation 2024-01-10 12:11:14 +00:00
Asbelos
43648fd9f4 5.2.23 2024-01-10 12:01:40 +00:00
Asbelos
b5ddade2b3 Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2024-01-10 11:58:37 +00:00
Asbelos
2e4995cab3 Keyword Hasher _hk 2024-01-10 11:58:30 +00:00
Harald Barth
796d5c4774 version 5.2.22 2024-01-10 08:20:14 +01:00
Harald Barth
27bd444884 Numbers for automations/routes can be negative 2024-01-10 08:18:14 +01:00
Harald Barth
ca380d11dc Do not crash on turnouts or turntables without description 2024-01-10 08:15:30 +01:00
peteGSX
c336ab0bb4 Merge pull request #389 from DCC-EX:add-startup-delay
Add-startup-delay
2024-01-09 16:43:41 +10:00
peteGSX
5ac26ce505 Add missing ; and DIAG message 2024-01-09 10:49:22 +10:00
peteGSX
b51a8fe126 Add STARTUP_DELAY 2024-01-09 10:41:29 +10:00
Harald Barth
718e78fca6 version 5.2.20 2024-01-08 13:20:29 +01:00
Harald Barth
70a1b9538c Check return of Ethernet.begin() in all code variants 2024-01-08 13:19:22 +01:00
Harald Barth
39d0cbb791 version 5.2.19 2024-01-07 22:24:15 +01:00
Harald Barth
4a3d3228a9 ESP32: Use SOC_RMT_MEM_WORDS_PER_CHANNEL to determine if the RMT hardware can handle DCC 2024-01-07 22:22:38 +01:00
Harald Barth
8036ba1c48 temp version tag 2024-01-03 02:44:15 +01:00
Harald Barth
74f7af1675 Display network IP fix 2024-01-03 02:36:07 +01:00
Harald Barth
6f076720f7 temp version tag 2024-01-01 22:17:47 +01:00
Harald Barth
d899da5898 Make return type of DCC::getFn int8_t 2024-01-01 22:08:59 +01:00
Harald Barth
3ce9d2ec88 DC frequency fix broadcast messages step #7 2024-01-01 22:08:04 +01:00
Harald Barth
9ebb1c5fb1 less debug diag 2024-01-01 21:25:43 +01:00
Harald Barth
19efa749b8 Typo fix HAS vs HAVE 2023-12-31 17:57:30 +01:00
Harald Barth
36cc46e88d DC frequency dummy functions for odd architectures step #6 2023-12-31 13:52:37 +01:00
Harald Barth
bba74a08f6 Do not support obsolete <c> on memory tight arch 2023-12-31 13:22:42 +01:00
Harald Barth
ab58c38e7b motordriver frequency diag 2023-12-31 13:22:34 +01:00
Harald Barth
d4f0a7c8f3 DC frequency uno does not have timers anyway step #5 2023-12-31 13:18:28 +01:00
Harald Barth
ba0a41b6f2 DC frequency fix bit shifting (debug code) step #4 2023-12-31 10:48:48 +01:00
Harald Barth
bf17f2018b fix type and static warning step #3 2023-12-30 22:20:41 +01:00
Harald Barth
67387d2dc3 function bits to freqency step #2 2023-12-30 22:09:01 +01:00
Harald Barth
adb8b56c92 variable frequency step #1 2023-12-30 21:23:44 +01:00
Harald Barth
bd44184f57 version 5.2.17 2023-12-25 17:49:16 +01:00
Harald Barth
e7d3d92c23 as no other tasks run on core1, yield() not necessary 2023-12-25 17:40:29 +01:00
Harald Barth
e3bab887a2 simplify WifiESP32 2023-12-25 17:32:39 +01:00
Harald Barth
041a6534da more diag and inUse tests 2023-12-24 12:03:42 +01:00
pmantoine
198d762a21 Add F439ZI serial setup in WifiInterface 2023-12-22 12:29:17 +08:00
peteGSX
1398cf1999 Merge pull request #373 from DCC-EX:ex-ioexpander-no-analogue-pins-fix
Ex-ioexpander-no-analogue-pins-fix
2023-12-19 18:49:18 +10:00
peteGSX
797028b223 Ready to test 2023-12-19 07:30:15 +10:00
Harald Barth
1881d4c9ad version 5.2.15 2023-12-13 11:41:57 +01:00
Harald Barth
18116a391c move call to CommandDistributor::broadcastPower() into the TrackManager::setTrackPower(*) functions 2023-12-13 11:40:15 +01:00
Harald Barth
a1accec79a add repeats to function packets that are not reminded in accordance with accessory packets 2023-12-13 10:55:58 +01:00
Asbelos
08f0a2b37d 5.2.13 2023-11-30 19:56:58 +00:00
Asbelos
6637ea6fe7 Merge branch 'devel_reminders' into devel 2023-11-30 19:54:20 +00:00
Asbelos
a69017f8bb Optional DISABLE_FUNCTION_REMINDERS 2023-11-30 19:48:02 +00:00
Asbelos
763c9d8ae6 STEALTH 2023-11-30 11:32:39 +00:00
pmantoine
753567427e ESP32 LCD messages, STM32 minor updates 2023-11-30 14:38:16 +08:00
Asbelos
3f4099520a ESP32 update for reminders 2023-11-28 19:57:14 +00:00
Asbelos
07fd4bc309 Window 2023-11-27 16:49:02 +00:00
Harald Barth
1f05ef42d2 Change from TrackManager::returnMode to TrackManager::getMode 2023-11-27 08:15:07 +01:00
Asbelos
96fdbfdc89 Trainbrains block occupancy 2023-11-26 12:31:41 +00:00
Harald Barth
ebaf1b984e version tag 2023-11-23 22:15:03 +01:00
Harald Barth
697f228a05 Save progmem with DISABLE_VDPY on Uno 2023-11-23 22:14:24 +01:00
Harald Barth
c8e307db7a remove unused TrackManager::reportPowerChange(...) 2023-11-23 22:11:00 +01:00
Asbelos
a5ccb2e29e EXRAIL STASH 2023-11-23 14:15:58 +00:00
Asbelos
42e2e69f5f Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2023-11-23 10:41:40 +00:00
Asbelos
2075bc50e8 EXRAIL basic stash implementation 2023-11-23 10:41:35 +00:00
Harald Barth
a16214790e version 5.2.8 2023-11-23 10:49:15 +01:00
Harald Barth
784934024e Bugfix: Do not turn off all tracks on change ; give better power messages 2023-11-23 10:47:43 +01:00
Asbelos
b478056a9f Fix @ reporting on startup 2023-11-23 09:00:49 +00:00
Harald Barth
ef47257d67 version tag 2023-11-22 10:54:01 +01:00
Harald Barth
03db06f2ee Bugfix: Do not turn on track with trackmode NONE 2023-11-22 10:53:34 +01:00
Harald Barth
4308739c2b version 5.2.7 2023-11-21 23:04:05 +01:00
Harald Barth
0cfea3e1a5 Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2023-11-21 22:48:54 +01:00
Harald Barth
d0df9f3c33 version tag 2023-11-21 22:48:35 +01:00
Harald Barth
ac4af407aa On ESP32, the inversion is already done in HW 2023-11-21 22:47:48 +01:00
Asbelos
a236a205fe Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2023-11-21 21:18:11 +00:00
Asbelos
478e9661bb EXRAIL ling segment 2023-11-21 21:14:54 +00:00
Harald Barth
2c1b3e0a8f version tag 2023-11-21 21:16:42 +01:00
Harald Barth
e7c4af5d4a back out wrong const change 2023-11-21 21:16:20 +01:00
Harald Barth
263ed18b25 version tag 2023-11-21 15:37:47 +01:00
Harald Barth
4e1fad4832 Trackmanager consolidate getModeName 2023-11-21 15:37:08 +01:00
Harald Barth
29ea746062 version 5.2.6 2023-11-21 11:54:43 +01:00
Harald Barth
e6f33cfdee Trackmanager broadcast power state on track mode change 2023-11-21 11:51:26 +01:00
Harald Barth
a7096e782c version 5.2.5 2023-11-20 09:32:22 +01:00
Harald Barth
f935756538 Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2023-11-20 09:28:02 +01:00
Harald Barth
74d11ccb1e Trackmanager: Do not treat TRACK_MODE_ALL as TRACK_MODE_DC 2023-11-20 09:27:57 +01:00
Asbelos
2ba5adc8b4 5.2.3 @ and ROUTE_DISABLED 2023-11-17 10:45:36 +00:00
Harald Barth
102d6078a7 version 5.2.3 2023-11-16 08:38:39 +01:00
Harald Barth
8943f2da18 Merge branch 'devel-parsebug' into devel 2023-11-16 08:36:26 +01:00
Harald Barth
7bd2ba9b41 Bugfix: Catch stange input to parser 2023-11-16 00:27:23 +01:00
Colin Murdoch
b472230b47 Update version.h
Updated version.h
2023-11-15 19:41:56 +00:00
Colin Murdoch
6da3153dd5 Define scroll rows in config
Allow the definition of MAX_CHARACTER_ROWS in config.h
2023-11-15 19:29:24 +00:00
Asbelos
b5d9798144 Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2023-11-14 19:41:09 +00:00
Asbelos
566ce1b7f8 Virtual LCD phase 1 2023-11-14 19:41:05 +00:00
Harald Barth
1af5132e6a version 5.2.1 timestamp 2023-11-14 11:16:54 +01:00
Harald Barth
763ef8be34 prettier MAX_TRACKS 2023-11-14 11:12:14 +01:00
Harald Barth
fd6e8705c8 Merge branch 'devel' into devel-esp32boost 2023-11-14 10:56:15 +01:00
Harald Barth
503378f1bb version 5.2.1 2023-11-14 00:06:53 +01:00
Harald Barth
582ff890f4 Trackmanager rework for simpler structure 2023-11-14 00:05:18 +01:00
Harald Barth
86ed8ff8a6 remove power state from <=> answer 2023-11-13 17:16:58 +01:00
Asbelos
148d4d30f8 Fix <jB active/inactive transposed 2023-11-11 17:31:38 +00:00
Harald Barth
b3ba647b09 Merge branch 'devel' into devel-esp32boost 2023-11-11 18:26:07 +01:00
Asbelos
4c89b26c79 fix <JA /<JR confusion 2023-11-11 09:12:08 +00:00
Harald Barth
e8b9f80c8c Reformat reply to <=> 2023-11-11 09:45:28 +01:00
Harald Barth
befcfebec7 version 5.2.0 2023-11-11 08:15:15 +01:00
Harald Barth
9ce95c07aa Booster mode configured by defined booster pin. New mode name output 2023-11-11 08:03:59 +01:00
Harald Barth
d8cc0c632a Merge branch 'devel' into devel-esp32boost2 2023-11-11 07:19:15 +01:00
Asbelos
d877fc315e Fix non-routestate code eliminator 2023-11-10 23:56:41 +00:00
Asbelos
6c18226cb5 Fix non-exrail crash 2023-11-10 23:46:17 +00:00
Asbelos
1c5f299b0e Fix ESP32 cast issue 2023-11-10 23:28:41 +00:00
Harald Barth
fb14fbd81b Merge branch 'devel' into devel-esp32boost2 2023-11-11 00:05:48 +01:00
Harald Barth
2f3d489f18 ESP32: autoreverse and booster prototype 2023-11-10 23:58:30 +01:00
Asbelos
d2d7a5cd16 EXRAIL multiple ON events 2023-11-10 20:13:33 +00:00
Harald Barth
337af77a03 booster test 2023-11-10 20:33:14 +01:00
Asbelos
670645db4b Version 20 2023-11-10 19:28:29 +00:00
Asbelos
a4eabf235e EXRAIL ROUTE_STATE and ROUTE_CAPTION 2023-11-10 19:25:24 +00:00
Asbelos
2cbcecf9e6 separate routes and sequences, handle state and captions. 2023-11-09 20:25:10 +00:00
Asbelos
26cf28dff7 fixups 2023-11-09 19:27:52 +00:00
Asbelos
44351b83ae Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2023-11-09 19:26:57 +00:00
Asbelos
4e08177b7b Route state management (part 1) 2023-11-07 16:27:26 +00:00
Harald Barth
f2ff1ba22a version 5.1.19 2023-11-06 22:14:39 +01:00
Harald Barth
043e6fdb26 Only flag 2.2.0.0-dev as broken, not 2.2.0.0 2023-11-06 22:13:03 +01:00
Asbelos
24e0f189e1 fix TURNOUTL 2023-11-01 20:19:59 +00:00
Harald Barth
33b2820095 Bugfix version detection logic and better message 2023-10-28 19:21:29 +02:00
Harald Barth
7b3b16b211 Divide out C for config and D for diag commands 2023-10-23 11:45:52 +02:00
peteGSX
27a5f76a8d Merge pull request #361 from DCC-EX:separate-hal-extt-turntable
Separate-hal-extt-turntable
2023-10-17 05:17:24 +10:00
peteGSX
754bd99381 Update version 2023-10-17 05:08:04 +10:00
peteGSX
650e411a4f Add vpin parameter 2023-10-17 05:06:35 +10:00
peteGSX
0978bb0c11 Changes made, but non-functional 2023-10-16 08:12:11 +10:00
Asbelos
6eb7051fd6 LCC and signal compile-out
LCC commands in EXRAIL for OpenMRN Adapter

FIrst use of compile-out of unused features.
2023-10-13 13:59:06 +01:00
peteGSX
5726844c83 Merge pull request #360 from DCC-EX:fix-ifttposition
Fixed
2023-10-13 04:46:05 +10:00
peteGSX
0214a55b23 Fixed 2023-10-13 04:37:38 +10:00
Asbelos
7db4a9575a Merge branch 'master' into devel 2023-10-12 11:07:39 +01:00
Asbelos
8b8e9e4919 clean result from invalid <JR n> 2023-10-12 11:07:05 +01:00
peteGSX
ce84974967 Missed one i 2023-10-12 13:42:14 +10:00
peteGSX
034c441c34 Merge pull request #359 from DCC-EX:turntable-broadcast-I
Change broadcast
2023-10-12 13:35:55 +10:00
peteGSX
d5978b1578 Change broadcast 2023-10-12 13:28:39 +10:00
Colin Murdoch
ea4f90d5fc Merged in Power changes
Merge in power changes and EXRAIL command & update to version.h
2023-10-11 17:06:56 +01:00
Colin Murdoch
1181fd855d Merge branch 'devel-power-chm' into devel 2023-10-11 16:58:17 +01:00
Colin Murdoch
a092e06a6f Update .gitignore
added UserAddin.txt to gitignore
2023-10-10 12:11:49 +01:00
Colin Murdoch
68fd56e7fc Added returnDCAddr
Added function to return DC address
2023-10-10 11:52:46 +01:00
Asbelos
370dae0ab8 Merge branch 'master' into devel 2023-10-09 18:15:36 +01:00
Asbelos
bef4b2ec35 fix <JR> default roster 2023-10-09 18:09:48 +01:00
Colin Murdoch
fe618d0b85 Add getModeName()
Add facility to get the name of the track mode
2023-10-06 19:11:11 +01:00
pmantoine
2ff1619ad1 STM32 reinstate 100% duty cycle PWM 2023-10-04 14:54:06 +08:00
pmantoine
7afd4443d6 STM32 revised I2C clock setup 2023-10-02 12:04:47 +08:00
Colin Murdoch
52cfc18754 Remove Diags not needed
Tidy up Diags and responses - use HASH_KEYWORD in place of 'A'
2023-09-28 15:02:30 +01:00
Colin Murdoch
25bbfa4c68 Fix <1 JOIN>
Fixed <1 JOIN> issue in TrackManager
2023-09-27 14:46:48 +01:00
Colin Murdoch
2a46b96083 Updates to power
Updates to powere routines and EXRAIL
2023-09-26 18:02:39 +01:00
Colin Murdoch
17c004aecf Code corrections
code corrections
2023-09-25 14:32:54 +01:00
Colin Murdoch
9e3ae21bb8 Change to EXRAIL Set_Power
Change to EXRAIL SET_Power
2023-09-25 09:59:17 +01:00
Harald Barth
9333beda49 correct return when requesting D RAM 2023-09-24 20:54:17 +02:00
Colin Murdoch
aacb980dc8 Power control plus EXRAIL
Power Control <0 A> etc plus EXRAIL SET_POWER
Not yet fully tested.
2023-09-24 15:40:42 +01:00
Colin Murdoch
8052090e0f Added Single Track Power On/Off
Added power On/Off <> commands
2023-09-22 17:03:40 +01:00
Harald Barth
46289fa78c Check bad AT firmware version 2023-09-14 09:05:23 +02:00
Harald Barth
b3cafd126e sample file corrections 2023-08-30 23:26:20 +02:00
Harald Barth
c55fa9f9d2 version number update 2023-08-25 19:08:58 +02:00
Harald Barth
210d96a3e3 Bugfix: ESP32 30ms off time 2023-08-25 19:07:57 +02:00
Harald Barth
42f3c7c128 version number update 2023-08-24 10:05:31 +02:00
Harald Barth
6cd7002e91 Bugfix: execute 30ms off time before rejoin 2023-08-24 10:03:29 +02:00
peteGSX
085762e800 Add OPCODE list to DCCEXParser.cpp 2023-08-18 18:52:34 +10:00
Harald Barth
2db2b0ecc6 Committing a SHA 2023-08-07 20:27:22 +02:00
Harald Barth
fd58a749ef Committing a SHA 2023-08-07 20:25:14 +02:00
Harald Barth
3bddf4dfd1 Make 4.2.69 the 5.0.0 release 2023-08-07 19:45:45 +02:00
59 changed files with 3548 additions and 1028 deletions

2
.gitignore vendored
View File

@@ -13,3 +13,5 @@ myFilter.cpp
my*.h
!my*.example.h
compile_commands.json
newcode.txt.old
UserAddin.txt

View File

@@ -105,6 +105,7 @@ void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream
void CommandDistributor::forget(byte clientId) {
if (clients[clientId]==WITHROTTLE_TYPE) WiThrottle::forget(clientId);
clients[clientId]=NONE_TYPE;
if (virtualLCDClient==clientId) virtualLCDClient=RingStream::NO_CLIENT;
}
#endif
@@ -162,7 +163,7 @@ void CommandDistributor::broadcastTurnout(int16_t id, bool isClosed ) {
}
void CommandDistributor::broadcastTurntable(int16_t id, uint8_t position, bool moving) {
broadcastReply(COMMAND_TYPE, F("<i %d %d %d>\n"), id, position, moving);
broadcastReply(COMMAND_TYPE, F("<I %d %d %d>\n"), id, position, moving);
}
void CommandDistributor::broadcastClockTime(int16_t time, int8_t rate) {
@@ -248,27 +249,128 @@ void CommandDistributor::broadcastLoco(byte slot) {
}
void CommandDistributor::broadcastPower() {
char pstr[] = "? x";
for(byte t=0; t<TrackManager::MAX_TRACKS; t++)
if (TrackManager::getPower(t, pstr))
broadcastReply(COMMAND_TYPE, F("<p%s>\n"),pstr);
byte trackcount=0;
byte oncount=0;
byte offcount=0;
for(byte t=0; t<TrackManager::MAX_TRACKS; t++) {
if (TrackManager::isActive(t)) {
trackcount++;
// do not call getPower(t) unless isActive(t)!
if (TrackManager::getPower(t) == POWERMODE::ON)
oncount++;
else
offcount++;
}
}
//DIAG(F("t=%d on=%d off=%d"), trackcount, oncount, offcount);
char state='2';
if (oncount==0 || offcount == trackcount)
state = '0';
else if (oncount == trackcount) {
state = '1';
}
// additional info about MAIN, PROG and JOIN
bool main=TrackManager::getMainPower()==POWERMODE::ON;
bool prog=TrackManager::getProgPower()==POWERMODE::ON;
bool join=TrackManager::isJoined();
//DIAG(F("m=%d p=%d j=%d"), main, prog, join);
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';
broadcastReply(COMMAND_TYPE, F("<p%c%S>\n"),state,reason);
if (join) {
reason = F(" JOIN"); // with space at start so we can append without space
broadcastReply(COMMAND_TYPE, F("<p1 %S>\n"),reason);
} else {
if (main) {
//reason = F("MAIN");
broadcastReply(COMMAND_TYPE, F("<p1 MAIN>\n"));
}
if (prog) {
//reason = F("PROG");
broadcastReply(COMMAND_TYPE, F("<p1 PROG>\n"));
}
}
if (state != '2')
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
#ifdef CD_HANDLE_RING
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1':'0');
// send '1' if all main are on, otherwise global state (which in that case is '0' or '2')
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1': state);
#endif
LCD(2,F("Power %S%S"),state=='1'?F("On"):F("Off"),reason);
LCD(2,F("Power %S%S"),state=='1'?F("On"): ( state=='0'? F("Off") : F("SC") ),reason);
}
void CommandDistributor::broadcastRaw(clientType type, char * msg) {
broadcastReply(type, F("%s"),msg);
}
void CommandDistributor::broadcastTrackState(const FSH* format,byte trackLetter,int16_t dcAddr) {
broadcastReply(COMMAND_TYPE, format,trackLetter,dcAddr);
void CommandDistributor::broadcastMessage(char * message) {
broadcastReply(COMMAND_TYPE, F("<m \"%s\">\n"),message);
broadcastReply(WITHROTTLE_TYPE, F("Hm%s\n"),message);
}
void CommandDistributor::broadcastTrackState(const FSH* format, byte trackLetter, const FSH *modename, int16_t dcAddr) {
broadcastReply(COMMAND_TYPE, format, trackLetter, modename, dcAddr);
}
void CommandDistributor::broadcastRouteState(uint16_t routeId, byte state ) {
broadcastReply(COMMAND_TYPE, F("<jB %d %d>\n"),routeId,state);
}
void CommandDistributor::broadcastRouteCaption(uint16_t routeId, const FSH* caption ) {
broadcastReply(COMMAND_TYPE, F("<jB %d \"%S\">\n"),routeId,caption);
}
Print * CommandDistributor::getVirtualLCDSerial(byte screen, byte row) {
Print * stream=virtualLCDSerial;
#ifdef CD_HANDLE_RING
rememberVLCDClient=RingStream::NO_CLIENT;
if (!stream && virtualLCDClient!=RingStream::NO_CLIENT) {
// 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 ((rememberVLCDClient = ring->peekTargetMark()) != RingStream::NO_CLIENT) {
ring->commit();
}
ring->mark(virtualLCDClient);
stream=ring;
}
#endif
if (stream) StringFormatter::send(stream,F("<@ %d %d \""), screen,row);
return stream;
}
void CommandDistributor::commitVirtualLCDSerial() {
#ifdef CD_HANDLE_RING
if (virtualLCDClient!=RingStream::NO_CLIENT) {
StringFormatter::send(ring,F("\">\n"));
ring->commit();
if (rememberVLCDClient!=RingStream::NO_CLIENT) ring->mark(rememberVLCDClient);
return;
}
#endif
StringFormatter::send(virtualLCDSerial,F("\">\n"));
}
void CommandDistributor::setVirtualLCDSerial(Print * stream) {
#ifdef CD_HANDLE_RING
virtualLCDClient=RingStream::NO_CLIENT;
if (stream && stream->availableForWrite()==RingStream::THIS_IS_A_RINGSTREAM) {
virtualLCDClient=((RingStream *) stream)->peekTargetMark();
virtualLCDSerial=nullptr;
return;
}
#endif
virtualLCDSerial=stream;
}
Print* CommandDistributor::virtualLCDSerial=&USB_SERIAL;
byte CommandDistributor::virtualLCDClient=0xFF;
byte CommandDistributor::rememberVLCDClient=0;

View File

@@ -55,10 +55,21 @@ public :
static int16_t retClockTime();
static void broadcastPower();
static void broadcastRaw(clientType type,char * msg);
static void broadcastTrackState(const FSH* format,byte trackLetter,int16_t dcAddr);
static void broadcastTrackState(const FSH* format,byte trackLetter, const FSH* modename, int16_t dcAddr);
template<typename... Targs> static void broadcastReply(clientType type, Targs... msg);
static void forget(byte clientId);
static void broadcastRouteState(uint16_t routeId,byte state);
static void broadcastRouteCaption(uint16_t routeId,const FSH * caption);
static void broadcastMessage(char * message);
// Handling code for virtual LCD receiver.
static Print * getVirtualLCDSerial(byte screen, byte row);
static void commitVirtualLCDSerial();
static void setVirtualLCDSerial(Print * stream);
private:
static Print * virtualLCDSerial;
static byte virtualLCDClient;
static byte rememberVLCDClient;
};
#endif

View File

@@ -65,6 +65,9 @@
#ifdef EXRAIL_WARNING
#warning You have myAutomation.h but your hardware has not enough memory to do that, so EX-RAIL DISABLED
#endif
// compile time check, passwords 1 to 7 chars do not work, so do not try to compile with them at all
// remember trailing '\0', sizeof("") == 1.
#define PASSWDCHECK(S) static_assert(sizeof(S) == 1 || sizeof(S) > 8, "Password shorter than 8 chars")
void setup()
{
@@ -76,6 +79,12 @@ void setup()
DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com"));
// If user has defined a startup delay, delay here before starting IO
#if defined(STARTUP_DELAY)
DIAG(F("Delaying startup for %dms"), STARTUP_DELAY);
delay(STARTUP_DELAY);
#endif
// Initialise HAL layer before reading EEprom or setting up MotorDrivers
IODevice::begin();
@@ -87,7 +96,7 @@ void setup()
DISPLAY_START (
// This block is still executed for DIAGS if display not in use
LCD(0,F("DCC-EX v%S"),F(VERSION));
LCD(0,F("DCC-EX v" VERSION));
LCD(1,F("Lic GPLv3"));
);
@@ -96,10 +105,12 @@ void setup()
// Start Ethernet if it exists
#ifndef ARDUINO_ARCH_ESP32
#if WIFI_ON
PASSWDCHECK(WIFI_PASSWORD); // compile time check
WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
#endif // WIFI_ON
#else
// ESP32 needs wifi on always
PASSWDCHECK(WIFI_PASSWORD); // compile time check
WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
#endif // ARDUINO_ARCH_ESP32

132
DCC.cpp
View File

@@ -122,7 +122,7 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) {
DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
}
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2, byte count) {
// DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2);
byte b[4];
byte nB = 0;
@@ -133,7 +133,7 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
if (byte1!=0) b[nB++] = byte1;
b[nB++] = byte2;
DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
DCCWaveform::mainTrack.schedulePacket(b, nB, count);
}
// returns speed steps 0 to 127 (1 == emergency stop)
@@ -153,6 +153,22 @@ uint8_t DCC::getThrottleSpeedByte(int cab) {
return speedTable[reg].speedCode;
}
// returns 0 to 7 for frequency
uint8_t DCC::getThrottleFrequency(int cab) {
#if defined(ARDUINO_AVR_UNO)
(void)cab;
return 0;
#else
int reg=lookupSpeedTable(cab);
if (reg<0)
return 0; // use default frequency
// shift out first 29 bits so we have the 3 "frequency bits" left
uint8_t res = (uint8_t)(speedTable[reg].functions >>29);
//DIAG(F("Speed table %d functions %l shifted %d"), reg, speedTable[reg].functions, res);
return res;
#endif
}
// returns direction on loco
// or true/forward on "loco not found"
bool DCC::getThrottleDirection(int cab) {
@@ -183,43 +199,55 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
b[nB++] = functionNumber >>7 ; // high order bits
}
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
return true;
}
// We use the reminder table up to 28 for normal functions.
// We use 29 to 31 for DC frequency as well so up to 28
// are "real" functions and 29 to 31 are frequency bits
// controlled by function buttons
if (functionNumber > 31)
return true;
int reg = lookupSpeedTable(cab);
if (reg<0) return false;
// Take care of functions:
// Set state of function
unsigned long previous=speedTable[reg].functions;
unsigned long funcmask = (1UL<<functionNumber);
uint32_t previous=speedTable[reg].functions;
uint32_t funcmask = (1UL<<functionNumber);
if (on) {
speedTable[reg].functions |= funcmask;
} else {
speedTable[reg].functions &= ~funcmask;
}
if (speedTable[reg].functions != previous) {
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
if (functionNumber <= 28)
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
CommandDistributor::broadcastLoco(reg);
}
return true;
}
// Flip function state
// Flip function state (used from withrottle protocol)
void DCC::changeFn( int cab, int16_t functionNumber) {
if (cab<=0 || functionNumber>28) return;
if (cab<=0 || functionNumber>31) return;
int reg = lookupSpeedTable(cab);
if (reg<0) return;
unsigned long funcmask = (1UL<<functionNumber);
speedTable[reg].functions ^= funcmask;
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
if (functionNumber <= 28) {
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
}
CommandDistributor::broadcastLoco(reg);
}
int DCC::getFn( int cab, int16_t functionNumber) {
if (cab<=0 || functionNumber>28) return -1; // unknown
// Report function state (used from withrottle protocol)
// returns 0 false, 1 true or -1 for do not know
int8_t DCC::getFn( int cab, int16_t functionNumber) {
if (cab<=0 || functionNumber>31)
return -1; // unknown
int reg = lookupSpeedTable(cab);
if (reg<0) return -1;
if (reg<0)
return -1;
unsigned long funcmask = (1UL<<functionNumber);
return (speedTable[reg].functions & funcmask)? 1 : 0;
@@ -278,6 +306,57 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
}
}
bool DCC::setExtendedAccessory(int16_t address, int16_t value, byte repeats) {
/* From https://www.nmra.org/sites/default/files/s-9.2.1_2012_07.pdf
The Extended Accessory Decoder Control Packet is included for the purpose of transmitting aspect control to signal
decoders or data bytes to more complex accessory decoders. Each signal head can display one aspect at a time.
{preamble} 0 10AAAAAA 0 0AAA0AA1 0 000XXXXX 0 EEEEEEEE 1
XXXXX is for a single head. A value of 00000 for XXXXX indicates the absolute stop aspect. All other aspects
represented by the values for XXXXX are determined by the signaling system used and the prototype being
modeled.
From https://normen.railcommunity.de/RCN-213.pdf:
More information is in RCN-213 about how the address bits are organized.
preamble -0- 1 0 A7 A6 A5 A4 A3 A2 -0- 0 ^A10 ^A9 ^A8 0 A1 A0 1 -0- ....
Thus in byte packet form the format is 10AAAAAA, 0AAA0AA1, 000XXXXX
Die Adresse f<>r den ersten erweiterten Zubeh<65>rdecoder ist wie bei den einfachen
Zubeh<EFBFBD>rdecodern die Adresse 4 = 1000-0001 0111-0001 . Diese Adresse wird in
Anwenderdialogen als Adresse 1 dargestellt.
This means that the first address shown to the user as "1" is mapped
to internal address 4.
Note that the Basic accessory format mentions "By convention these
bits (bits 4-6 of the second data byte) are in ones complement" but
this note is absent from the advanced packet description. The
english translation does not mention that the address format for
the advanced packet follows the one for the basic packet but
according to the RCN-213 this is the case.
We allow for addresses from -3 to 2047-3 as that allows to address the
whole range of the 11 bits sent to track.
*/
if ((address > 2044) || (address < -3)) return false; // 2047-3, 11 bits but offset 3
if (value != (value & 0x1F)) return false; // 5 bits
address+=3; // +3 offset according to RCN-213
byte b[3];
b[0]= 0x80 // bits always on
| ((address>>2) & 0x3F); // shift out 2, mask out used bits
b[1]= 0x01 // bits always on
| (((~(address>>8)) & 0x07)<<4) // shift out 8, invert, mask 3 bits, shift up 4
| ((address & 0x03)<<1); // mask 2 bits, shift up 1
b[2]=value;
DCCWaveform::mainTrack.schedulePacket(b, sizeof(b), repeats);
return true;
}
//
// writeCVByteMain: Write a byte with PoM on main. This writes
// the 5 byte sized packet to implement this DCC function
@@ -595,7 +674,7 @@ void DCC::loop() {
void DCC::issueReminders() {
// if the main track transmitter still has a pending packet, skip this time around.
if ( DCCWaveform::mainTrack.getPacketPending()) return;
if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return;
// Move to next loco slot. If occupied, send a reminder.
int reg = lastLocoReminder+1;
if (reg > highestUsedReg) reg = 0; // Go to start of table
@@ -619,24 +698,39 @@ bool DCC::issueReminder(int reg) {
break;
case 1: // remind function group 1 (F0-F4)
if (flags & FN_GROUP_1)
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
#ifndef DISABLE_FUNCTION_REMINDERS
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),0); // 100D DDDD
#else
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4),2);
flags&= ~FN_GROUP_1; // dont send them again
#endif
break;
case 2: // remind function group 2 F5-F8
if (flags & FN_GROUP_2)
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD
#ifndef DISABLE_FUNCTION_REMINDERS
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),0); // 1011 DDDD
#else
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F),2);
flags&= ~FN_GROUP_2; // dont send them again
#endif
break;
case 3: // remind function group 3 F9-F12
if (flags & FN_GROUP_3)
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD
#ifndef DISABLE_FUNCTION_REMINDERS
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),0); // 1010 DDDD
#else
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F),2);
flags&= ~FN_GROUP_3; // dont send them again
#endif
break;
case 4: // remind function group 4 F13-F20
if (flags & FN_GROUP_4)
setFunctionInternal(loco,222, ((functions>>13)& 0xFF));
setFunctionInternal(loco,222, ((functions>>13)& 0xFF),2);
flags&= ~FN_GROUP_4; // dont send them again
break;
case 5: // remind function group 5 F21-F28
if (flags & FN_GROUP_5)
setFunctionInternal(loco,223, ((functions>>21)& 0xFF));
setFunctionInternal(loco,223, ((functions>>21)& 0xFF),2);
flags&= ~FN_GROUP_5; // dont send them again
break;
}

8
DCC.h
View File

@@ -61,16 +61,18 @@ public:
static void setThrottle(uint16_t cab, uint8_t tSpeed, bool tDirection);
static int8_t getThrottleSpeed(int cab);
static uint8_t getThrottleSpeedByte(int cab);
static uint8_t getThrottleFrequency(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 bool setFn(int cab, int16_t functionNumber, bool on);
static void changeFn(int cab, int16_t functionNumber);
static int getFn(int cab, int16_t functionNumber);
static int8_t getFn(int cab, int16_t functionNumber);
static uint32_t getFunctionMap(int cab);
static void updateGroupflags(byte &flags, int16_t functionNumber);
static void setAccessory(int address, byte port, bool gate, byte onoff = 2);
static bool setExtendedAccessory(int16_t address, int16_t value, byte repeats=3);
static bool writeTextPacket(byte *b, int nBytes);
// ACKable progtrack calls bitresults callback 0,0 or -1, cv returns value or -1
@@ -98,7 +100,7 @@ public:
int loco;
byte speedCode;
byte groupFlags;
unsigned long functions;
uint32_t functions;
};
static LOCO speedTable[MAX_LOCOS];
static int lookupSpeedTable(int locoId, bool autoCreate=true);
@@ -109,7 +111,7 @@ private:
static byte loopStatus;
static void setThrottle2(uint16_t cab, uint8_t speedCode);
static void updateLocoReminder(int loco, byte speedCode);
static void setFunctionInternal(int cab, byte fByte, byte eByte);
static void setFunctionInternal(int cab, byte fByte, byte eByte, byte count);
static bool issueReminder(int reg);
static int lastLocoReminder;
static int highestUsedReg;

View File

@@ -49,6 +49,7 @@
#include "CommandDistributor.h"
#include "TrackManager.h"
#include "DCCTimer.h"
#include "KeywordHasher.h"
#include "EXRAIL.h"
#endif

View File

@@ -45,11 +45,11 @@ Once a new OPCODE is decided upon, update this list.
0, Track power off
1, Track power on
a, DCC accessory control
A,
A, DCC extended accessory control
b, Write CV bit on main
B, Write CV bit
c, Request current command
C,
C, configure the CS
d,
D, Diagnostic commands
e, Erase EEPROM
@@ -60,15 +60,15 @@ Once a new OPCODE is decided upon, update this list.
G,
h,
H, Turnout state broadcast
i, Reserved for future use - Turntable object broadcast
I, Reserved for future use - Turntable object command and control
i, Server details string
I, Turntable object command, control, and broadcast
j, Throttle responses
J, Throttle queries
k, Reserved for future use - Potentially Railcom
K, Reserved for future use - Potentially Railcom
l, Loco speedbyte/function map broadcast
L,
m,
L, Reserved for LCC interface (implemented in EXRAIL)
m, message to throttles broadcast
M, Write DCC packet
n,
N,
@@ -115,6 +115,8 @@ Once a new OPCODE is decided upon, update this list.
#include "DCCTimer.h"
#include "EXRAIL2.h"
#include "Turntables.h"
#include "version.h"
#include "KeywordHasher.h"
// This macro can't be created easily as a portable function because the
// flashlist requires a far pointer for high flash access.
@@ -122,58 +124,9 @@ Once a new OPCODE is decided upon, update this list.
for (int16_t i=0;;i+=sizeof(flashList[0])) { \
int16_t value=GETHIGHFLASHW(flashList,i); \
if (value==INT16_MAX) break; \
if (value != 0) StringFormatter::send(stream,F(" %d"),value); \
StringFormatter::send(stream,F(" %d"),value); \
}
// These keywords are used in the <1> command. The number is what you get if you use the keyword as a parameter.
// To discover new keyword numbers , use the <$ YOURKEYWORD> command
const int16_t HASH_KEYWORD_MAIN = 11339;
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_ACK = 3113;
const int16_t HASH_KEYWORD_ON = 2657;
const int16_t HASH_KEYWORD_DCC = 6436;
const int16_t HASH_KEYWORD_SLOW = -17209;
#ifndef DISABLE_PROG
const int16_t HASH_KEYWORD_JOIN = -30750;
const int16_t HASH_KEYWORD_PROG = -29718;
const int16_t HASH_KEYWORD_PROGBOOST = -6353;
#endif
#ifndef DISABLE_EEPROM
const int16_t HASH_KEYWORD_EEPROM = -7168;
#endif
const int16_t HASH_KEYWORD_LIMIT = 27413;
const int16_t HASH_KEYWORD_MAX = 16244;
const int16_t HASH_KEYWORD_MIN = 15978;
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_TT=2688;
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_G='G';
const int16_t HASH_KEYWORD_I='I';
const int16_t HASH_KEYWORD_O='O';
const int16_t HASH_KEYWORD_P='P';
const int16_t HASH_KEYWORD_R='R';
const int16_t HASH_KEYWORD_T='T';
const int16_t HASH_KEYWORD_X='X';
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;
const int16_t HASH_KEYWORD_EXTT = 8573;
const int16_t HASH_KEYWORD_ADD = 3201;
int16_t DCCEXParser::stashP[MAX_COMMAND_PARAMS];
bool DCCEXParser::stashBusy;
Print *DCCEXParser::stashStream = NULL;
@@ -209,8 +162,10 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
case 1: // skipping spaces before a param
if (hot == ' ')
break;
if (hot == '\0' || hot == '>')
return parameterCount;
if (hot == '\0')
return -1;
if (hot == '>')
return parameterCount;
state = 2;
continue;
@@ -303,14 +258,19 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
#ifndef DISABLE_EEPROM
(void)EEPROM; // tell compiler not to warn this is unused
#endif
byte params = 0;
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 opcode = com[0];
byte params = splitValues(p, com, opcode=='M' || opcode=='P');
int16_t splitnum = splitValues(p, com, opcode=='M' || opcode=='P');
if (splitnum < 0 || splitnum >= MAX_COMMAND_PARAMS) // if arguments are broken, leave but via printing <X>
goto out;
// Because of check above we are now inside byte size
params = splitnum;
if (filterCallback)
filterCallback(stream, opcode, params, p);
if (filterRMFTCallback && opcode!='\0')
@@ -323,25 +283,22 @@ void DCCEXParser::parseOne(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==1) { // <t cab> display state
int16_t slot=DCC::lookupSpeedTable(p[0],false);
if (slot>=0)
CommandDistributor::broadcastLoco(slot);
else // send dummy state speed 0 fwd no functions.
StringFormatter::send(stream,F("<l %d -1 128 0>\n"),p[0]);
return;
}
if (params == 4)
{ // <t REGISTER CAB SPEED DIRECTION>
// ignore register p[0]
cab = p[1];
tspeed = p[2];
direction = p[3];
@@ -424,6 +381,13 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
#endif
}
return;
case 'A': // EXTENDED ACCESSORY <A address value>
// Note: if this happens to match a defined EXRAIL
// DCCX_SIGNAL, then EXRAIL will have intercepted
// this command alrerady.
if (params==2 && DCC::setExtendedAccessory(p[0],p[1])) return;
break;
case 'T': // TURNOUT <T ...>
if (parseT(stream, params, p))
@@ -552,85 +516,81 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
case '1': // POWERON <1 [MAIN|PROG|JOIN]>
{
bool main=false;
bool prog=false;
bool join=false;
if (params > 1) break;
if (params==0) { // All
main=true;
prog=true;
}
if (params==1) {
if (p[0]==HASH_KEYWORD_MAIN) { // <1 MAIN>
main=true;
if (params > 1) break;
if (params==0) { // All
TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::ON);
}
if (params==1) {
if (p[0]=="MAIN"_hk) { // <1 MAIN>
TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::ON);
}
#ifndef DISABLE_PROG
else if (p[0] == HASH_KEYWORD_JOIN) { // <1 JOIN>
main=true;
prog=true;
join=true;
}
else if (p[0]==HASH_KEYWORD_PROG) { // <1 PROG>
prog=true;
}
else if (p[0] == "JOIN"_hk) { // <1 JOIN>
TrackManager::setJoin(true);
TrackManager::setTrackPower(TRACK_MODE_MAIN|TRACK_MODE_PROG, POWERMODE::ON);
}
else if (p[0]=="PROG"_hk) { // <1 PROG>
TrackManager::setJoin(false);
TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::ON);
}
#endif
else break; // will reply <X>
else if (p[0] >= "A"_hk && p[0] <= "H"_hk) { // <1 A-H>
byte t = (p[0] - 'A');
TrackManager::setTrackPower(POWERMODE::ON, t);
//StringFormatter::send(stream, F("<p1 %c>\n"), t+'A');
}
else break; // will reply <X>
}
//TrackManager::streamTrackState(NULL,t);
return;
}
TrackManager::setJoin(join);
if (main) TrackManager::setMainPower(POWERMODE::ON);
if (prog) TrackManager::setProgPower(POWERMODE::ON);
CommandDistributor::broadcastPower();
return;
}
case '0': // POWEROFF <0 [MAIN | PROG] >
{
bool main=false;
bool prog=false;
if (params > 1) break;
if (params==0) { // All
main=true;
prog=true;
}
if (params==1) {
if (p[0]==HASH_KEYWORD_MAIN) { // <0 MAIN>
main=true;
if (params > 1) break;
if (params==0) { // All
TrackManager::setJoin(false);
TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::OFF);
}
if (params==1) {
if (p[0]=="MAIN"_hk) { // <0 MAIN>
TrackManager::setJoin(false);
TrackManager::setTrackPower(TRACK_MODE_MAIN, POWERMODE::OFF);
}
#ifndef DISABLE_PROG
else if (p[0]==HASH_KEYWORD_PROG) { // <0 PROG>
prog=true;
}
else if (p[0]=="PROG"_hk) { // <0 PROG>
TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off
TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::OFF);
}
#endif
else break; // will reply <X>
else if (p[0] >= "A"_hk && p[0] <= "H"_hk) { // <1 A-H>
byte t = (p[0] - 'A');
TrackManager::setJoin(false);
TrackManager::setTrackPower(POWERMODE::OFF, t);
//StringFormatter::send(stream, F("<p0 %c>\n"), t+'A');
}
else break; // will reply <X>
}
return;
}
TrackManager::setJoin(false);
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);
}
CommandDistributor::broadcastPower();
return;
}
case '!': // ESTOP ALL <!>
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
return;
#ifdef HAS_ENOUGH_MEMORY
case 'c': // SEND METER RESPONSES <c>
// No longer useful because of multiple tracks See <JG> and <JI>
if (params>0) break;
TrackManager::reportObsoleteCurrent(stream);
return;
#endif
case 'Q': // SENSORS <Q>
Sensor::printAll(stream);
return;
case 's': // <s>
case 's': // STATUS <s>
StringFormatter::send(stream, F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE), DCC::getMotorShieldName(), F(GITHUB_SHA));
CommandDistributor::broadcastPower(); // <s> is the only "get power status" command we have
Turnout::printAll(stream); //send all Turnout states
@@ -651,14 +611,18 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
case ' ': // < >
StringFormatter::send(stream, F("\n"));
return;
case 'C': // CONFIG <C [params]>
if (parseC(stream, params, p))
return;
break;
#ifndef DISABLE_DIAG
case 'D': // < >
case 'D': // DIAG <D [params]>
if (parseD(stream, params, p))
return;
break;
#endif
case '=': // <= Track manager control >
if (TrackManager::parseJ(stream, params, p))
case '=': // TRACK MANAGER CONTROL <= [params]>
if (TrackManager::parseEqualSign(stream, params, p))
return;
break;
@@ -695,7 +659,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
//if ((params<1) | (params>2)) break; // <J>
int16_t id=(params==2)?p[1]:0;
switch(p[0]) {
case HASH_KEYWORD_C: // <JC mmmm nn> sets time and speed
case "C"_hk: // <JC mmmm nn> sets time and speed
if (params==1) { // <JC> returns latest time
int16_t x = CommandDistributor::retClockTime();
StringFormatter::send(stream, F("<jC %d>\n"), x);
@@ -704,53 +668,47 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
CommandDistributor::setClockTime(p[1], p[2], 1);
return;
case HASH_KEYWORD_G: // <JG> current gauge limits
case "G"_hk: // <JG> current gauge limits
if (params>1) break;
TrackManager::reportGauges(stream); // <g limit...limit>
return;
case HASH_KEYWORD_I: // <JI> current values
case "I"_hk: // <JI> current values
if (params>1) break;
TrackManager::reportCurrent(stream); // <g limit...limit>
return;
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
case "A"_hk: // <JA> intercepted by EXRAIL// <JA> returns automations/routes
if (params!=1) break; // <JA>
StringFormatter::send(stream, F("<jA>\n"));
return;
case "M"_hk: // <JM> intercepted by EXRAIL
if (params>1) break; // invalid cant do
// <JM> requests stash size so say none.
StringFormatter::send(stream,F("<jM 0>\n"));
return;
case "R"_hk: // <JR> returns rosters
StringFormatter::send(stream, F("<jR"));
#ifdef EXRAIL_ACTIVE
if (params==1) {
SENDFLASHLIST(stream,RMFT2::rosterIdList)
}
else {
const FSH * functionNames= RMFT2::getRosterFunctions(id);
StringFormatter::send(stream,F(" %d \"%S\" \"%S\""),
id, RMFT2::getRosterName(id),
functionNames == NULL ? RMFT2::getRosterFunctions(0) : functionNames);
}
auto rosterName= RMFT2::getRosterName(id);
if (!rosterName) rosterName=F("");
auto functionNames= RMFT2::getRosterFunctions(id);
if (!functionNames) functionNames=RMFT2::getRosterFunctions(0);
if (!functionNames) functionNames=F("");
StringFormatter::send(stream,F(" %d \"%S\" \"%S\""),
id, rosterName, functionNames);
}
#endif
StringFormatter::send(stream, F(">\n"));
return;
case HASH_KEYWORD_T: // <JT> returns turnout list
case "T"_hk: // <JT> returns turnout list
StringFormatter::send(stream, F("<jT"));
if (params==1) { // <JT>
for ( Turnout * t=Turnout::first(); t; t=t->next()) {
@@ -777,7 +735,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
return;
// No turntables without HAL support
#ifndef IO_NO_HAL
case HASH_KEYWORD_O: // <JO returns turntable list
case "O"_hk: // <JO returns turntable list
StringFormatter::send(stream, F("<jO"));
if (params==1) { // <JO>
for (Turntable * tto=Turntable::first(); tto; tto=tto->next()) {
@@ -802,7 +760,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
}
}
return;
case HASH_KEYWORD_P: // <JP id> returns turntable position list for the turntable id
case "P"_hk: // <JP id> returns turntable position list for the turntable id
if (params==2) { // <JP id>
Turntable *tto=Turntable::get(id);
if (!tto || tto->isHidden()) {
@@ -839,15 +797,30 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
break;
#endif
case 'L': // LCC interface implemented in EXRAIL parser
break; // Will <X> if not intercepted by EXRAIL
#ifndef DISABLE_VDPY
case '@': // JMRI saying "give me virtual LCD msgs"
CommandDistributor::setVirtualLCDSerial(stream);
StringFormatter::send(stream,
F("<@ 0 0 \"DCC-EX v" VERSION "\">\n"
"<@ 0 1 \"Lic GPLv3\">\n"));
return;
#endif
default: //anything else will diagnose and drop out to <X>
if (opcode >= ' ' && opcode <= '~') {
DIAG(F("Opcode=%c params=%d"), opcode, params);
for (int i = 0; i < params; i++)
DIAG(F("p[%d]=%d (0x%x)"), i, p[i], p[i]);
break;
} else {
DIAG(F("Unprintable %x"), opcode);
}
break;
} // end of opcode switch
// Any fallout here sends an <X>
out:// Any fallout here sends an <X>
StringFormatter::send(stream, F("<X>\n"));
}
@@ -954,14 +927,14 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
switch (p[1]) {
// Turnout messages use 1=throw, 0=close.
case 0:
case HASH_KEYWORD_C:
case "C"_hk:
state = true;
break;
case 1:
case HASH_KEYWORD_T:
case "T"_hk:
state= false;
break;
case HASH_KEYWORD_X:
case "X"_hk:
{
Turnout *tt = Turnout::get(p[0]);
if (tt) {
@@ -978,14 +951,14 @@ bool DCCEXParser::parseT(Print *stream, int16_t params, int16_t p[])
}
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 (params == 6 && p[1] == "SERVO"_hk) { // <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 (params == 3 && p[1] == "VPIN"_hk) { // <T id VPIN n>
if (!VpinTurnout::create(p[0], p[2])) return false;
} else
if (params >= 3 && p[1] == HASH_KEYWORD_DCC) {
if (params >= 3 && p[1] == "DCC"_hk) {
// <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;
@@ -1044,116 +1017,153 @@ bool DCCEXParser::parseS(Print *stream, int16_t params, int16_t p[])
return false;
}
bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
{
bool DCCEXParser::parseC(Print *stream, int16_t params, int16_t p[]) {
(void)stream; // arg not used, maybe later?
if (params == 0)
return false;
bool onOff = (params > 0) && (p[1] == 1 || p[1] == HASH_KEYWORD_ON); // dont care if other stuff or missing... just means off
switch (p[0])
{
case HASH_KEYWORD_CABS: // <D CABS>
DCC::displayCabList(stream);
return true;
case HASH_KEYWORD_RAM: // <D RAM>
StringFormatter::send(stream, F("Free memory=%d\n"), DCCTimer::getMinimumFreeMemory());
return true;
#ifndef DISABLE_PROG
case HASH_KEYWORD_ACK: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value>
case "PROGBOOST"_hk:
TrackManager::progTrackBoosted=true;
return true;
#endif
case "RESET"_hk:
DCCTimer::reset();
break; // and <X> if we didnt restart
case "SPEED28"_hk:
DCC::setGlobalSpeedsteps(28);
DIAG(F("28 Speedsteps"));
return true;
case "SPEED128"_hk:
DCC::setGlobalSpeedsteps(128);
DIAG(F("128 Speedsteps"));
return true;
#if defined(HAS_ENOUGH_MEMORY) && !defined(ARDUINO_ARCH_UNO)
case "RAILCOM"_hk:
{ // <C RAILCOM ON|OFF|DEBUG >
if (params<2) return false;
bool on=false;
bool debug=false;
switch (p[1]) {
case "ON"_hk:
case 1:
on=true;
break;
case "DEBUG"_hk:
on=true;
debug=true;
break;
case "OFF"_hk:
case 0:
break;
default:
return false;
}
DIAG(F("Railcom %S")
,DCCWaveform::setRailcom(on,debug)?F("ON"):F("OFF"));
return true;
}
#endif
#ifndef DISABLE_PROG
case "ACK"_hk: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value>
if (params >= 3) {
if (p[1] == HASH_KEYWORD_LIMIT) {
if (p[1] == "LIMIT"_hk) {
DCCACK::setAckLimit(p[2]);
LCD(1, F("Ack Limit=%dmA"), p[2]); // <D ACK LIMIT 42>
} else if (p[1] == HASH_KEYWORD_MIN) {
} else if (p[1] == "MIN"_hk) {
DCCACK::setMinAckPulseDuration(p[2]);
LCD(0, F("Ack Min=%uus"), p[2]); // <D ACK MIN 1500>
} else if (p[1] == HASH_KEYWORD_MAX) {
} else if (p[1] == "MAX"_hk) {
DCCACK::setMaxAckPulseDuration(p[2]);
LCD(0, F("Ack Max=%uus"), p[2]); // <D ACK MAX 9000>
} else if (p[1] == HASH_KEYWORD_RETRY) {
} else if (p[1] == "RETRY"_hk) {
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"));
bool onOff = (params > 0) && (p[1] == 1 || p[1] == "ON"_hk); // dont care if other stuff or missing... just means off
DIAG(F("Ack diag %S"), onOff ? F("on") : F("off"));
Diag::ACK = onOff;
}
return true;
#endif
case HASH_KEYWORD_CMD: // <D CMD ON/OFF>
default: // invalid/unknown
break;
}
return false;
}
bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
{
if (params == 0)
return false;
bool onOff = (params > 0) && (p[1] == 1 || p[1] == "ON"_hk); // dont care if other stuff or missing... just means off
switch (p[0])
{
case "CABS"_hk: // <D CABS>
DCC::displayCabList(stream);
return true;
case "RAM"_hk: // <D RAM>
DIAG(F("Free memory=%d"), DCCTimer::getMinimumFreeMemory());
return true;
case "CMD"_hk: // <D CMD ON/OFF>
Diag::CMD = onOff;
return true;
#ifdef HAS_ENOUGH_MEMORY
case HASH_KEYWORD_WIFI: // <D WIFI ON/OFF>
case "WIFI"_hk: // <D WIFI ON/OFF>
Diag::WIFI = onOff;
return true;
case HASH_KEYWORD_ETHERNET: // <D ETHERNET ON/OFF>
case "ETHERNET"_hk: // <D ETHERNET ON/OFF>
Diag::ETHERNET = onOff;
return true;
case HASH_KEYWORD_WIT: // <D WIT ON/OFF>
case "WIT"_hk: // <D WIT ON/OFF>
Diag::WITHROTTLE = onOff;
return true;
case HASH_KEYWORD_LCN: // <D LCN ON/OFF>
case "LCN"_hk: // <D LCN ON/OFF>
Diag::LCN = onOff;
return true;
#endif
#ifndef DISABLE_PROG
case HASH_KEYWORD_PROGBOOST:
TrackManager::progTrackBoosted=true;
return true;
#endif
case HASH_KEYWORD_RESET:
DCCTimer::reset();
break; // and <X> if we didnt restart
#ifndef DISABLE_EEPROM
case HASH_KEYWORD_EEPROM: // <D EEPROM NumEntries>
case "EEPROM"_hk: // <D EEPROM NumEntries>
if (params >= 2)
EEStore::dump(p[1]);
return true;
#endif
case "SERVO"_hk: // <D SERVO vpin position [profile]>
case HASH_KEYWORD_SPEED28:
DCC::setGlobalSpeedsteps(28);
StringFormatter::send(stream, F("28 Speedsteps"));
return true;
case HASH_KEYWORD_SPEED128:
DCC::setGlobalSpeedsteps(128);
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]>
case "ANOUT"_hk: // <D ANOUT vpin position [profile]>
IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
break;
return true;
case HASH_KEYWORD_ANIN: // <D ANIN vpin> Display analogue input value
case "ANIN"_hk: // <D ANIN vpin> Display analogue input value
DIAG(F("VPIN=%u value=%d"), p[1], IODevice::readAnalogue(p[1]));
break;
return true;
#if !defined(IO_NO_HAL)
case HASH_KEYWORD_HAL:
if (p[1] == HASH_KEYWORD_SHOW)
case "HAL"_hk:
if (p[1] == "SHOW"_hk)
IODevice::DumpAll();
else if (p[1] == HASH_KEYWORD_RESET)
else if (p[1] == "RESET"_hk)
IODevice::reset();
break;
return true;
#endif
case HASH_KEYWORD_TT: // <D TT vpin steps activity>
case "TT"_hk: // <D TT vpin steps activity>
IODevice::writeAnalogue(p[1], p[2], params>3 ? p[3] : 0);
break;
return true;
default: // invalid/unknown
break;
return parseC(stream, params, p);
}
return false;
}
@@ -1181,7 +1191,7 @@ bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
if (tto) {
bool type = tto->isEXTT();
uint8_t position = tto->getPosition();
StringFormatter::send(stream, F("<i %d %d>\n"), type, position);
StringFormatter::send(stream, F("<I %d %d>\n"), type, position);
} else {
return false;
}
@@ -1202,12 +1212,12 @@ bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
case 3: // <I id position activity> | <I id DCC home> - rotate to position for EX-Turntable or create DCC turntable
{
Turntable *tto = Turntable::get(p[0]);
if (p[1] == HASH_KEYWORD_DCC) {
if (p[1] == "DCC"_hk) {
if (tto || p[2] < 0 || p[2] > 3600) return false;
if (!DCCTurntable::create(p[0])) return false;
Turntable *tto = Turntable::get(p[0]);
tto->addPosition(0, 0, p[2]);
StringFormatter::send(stream, F("<i>\n"));
StringFormatter::send(stream, F("<I>\n"));
} else {
if (!tto) return false;
if (!tto->isEXTT()) return false;
@@ -1219,12 +1229,12 @@ bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
case 4: // <I id EXTT vpin home> create an EXTT turntable
{
Turntable *tto = Turntable::get(p[0]);
if (p[1] == HASH_KEYWORD_EXTT) {
if (p[1] == "EXTT"_hk) {
if (tto || p[3] < 0 || p[3] > 3600) return false;
if (!EXTTTurntable::create(p[0], (VPIN)p[2])) return false;
Turntable *tto = Turntable::get(p[0]);
tto->addPosition(0, 0, p[3]);
StringFormatter::send(stream, F("<i>\n"));
StringFormatter::send(stream, F("<I>\n"));
} else {
return false;
}
@@ -1234,11 +1244,11 @@ bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
case 5: // <I id ADD position value angle> add a position
{
Turntable *tto = Turntable::get(p[0]);
if (p[1] == HASH_KEYWORD_ADD) {
if (p[1] == "ADD"_hk) {
// tto must exist, no more than 48 positions, angle 0 - 3600
if (!tto || p[2] > 48 || p[4] < 0 || p[4] > 3600) return false;
tto->addPosition(p[2], p[3], p[4]);
StringFormatter::send(stream, F("<i>\n"));
StringFormatter::send(stream, F("<I>\n"));
} else {
return false;
}

View File

@@ -49,6 +49,7 @@ struct DCCEXParser
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 parseC(Print * stream, int16_t params, int16_t p[]);
static bool parseD(Print * stream, int16_t params, int16_t p[]);
#ifndef IO_NO_HAL
static bool parseI(Print * stream, int16_t params, int16_t p[]);

View File

@@ -1,5 +1,5 @@
/*
* © 2021-2022, Harald Barth.
* © 2021-2024, Harald Barth.
*
* This file is part of DCC-EX
*
@@ -25,6 +25,18 @@
#include "DCCWaveform.h" // for MAX_PACKET_SIZE
#include "soc/gpio_sig_map.h"
// check for right type of ESP32
#include "soc/soc_caps.h"
#ifndef SOC_RMT_MEM_WORDS_PER_CHANNEL
#error This symobol should be defined
#endif
#if SOC_RMT_MEM_WORDS_PER_CHANNEL < 64
#warning This is not an ESP32-WROOM but some other unsupported variant
#warning You are outside of the DCC-EX supported hardware
#endif
static const byte RMT_CHAN_PER_DCC_CHAN = 2;
// Number of bits resulting out of X bytes of DCC payload data
// Each byte has one bit extra and at the end we have one EOF marker
#define DATA_LEN(X) ((X)*9+1)
@@ -75,12 +87,30 @@ void IRAM_ATTR interrupt(rmt_channel_t channel, void *t) {
RMTChannel::RMTChannel(pinpair pins, bool isMain) {
byte ch;
byte plen;
// Below we check if the DCC packet actually fits into the RMT hardware
// Currently MAX_PACKET_SIZE = 5 so with checksum there are
// MAX_PACKET_SIZE+1 data packets. Each need DATA_LEN (9) bits.
// To that we add the preamble length, the fencepost DCC end bit
// and the RMT EOF marker.
// SOC_RMT_MEM_WORDS_PER_CHANNEL is either 64 (original WROOM) or
// 48 (all other ESP32 like the -C3 or -S2
// The formula to get the possible MAX_PACKET_SIZE is
//
// ALLOCATED = RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL
// MAX_PACKET_SIZE = floor((ALLOCATED - PREAMBLE_LEN - 2)/9 - 1)
//
if (isMain) {
ch = 0;
plen = PREAMBLE_BITS_MAIN;
static_assert (DATA_LEN(MAX_PACKET_SIZE+1) + PREAMBLE_BITS_MAIN + 2 <= RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL,
"Number of DCC packet bits greater than ESP32 RMT memory available");
} else {
ch = 2;
ch = RMT_CHAN_PER_DCC_CHAN; // number == offset
plen = PREAMBLE_BITS_PROG;
static_assert (DATA_LEN(MAX_PACKET_SIZE+1) + PREAMBLE_BITS_PROG + 2 <= RMT_CHAN_PER_DCC_CHAN * SOC_RMT_MEM_WORDS_PER_CHANNEL,
"Number of DCC packet bits greater than ESP32 RMT memory available");
}
// preamble
@@ -115,7 +145,7 @@ RMTChannel::RMTChannel(pinpair pins, bool isMain) {
// data: max packet size today is 5 + checksum
maxDataLen = DATA_LEN(MAX_PACKET_SIZE+1); // plus checksum
data = (rmt_item32_t*)malloc(maxDataLen*sizeof(rmt_item32_t));
rmt_config_t config;
// Configure the RMT channel for TX
bzero(&config, sizeof(rmt_config_t));
@@ -123,20 +153,10 @@ RMTChannel::RMTChannel(pinpair pins, bool isMain) {
config.channel = channel = (rmt_channel_t)ch;
config.clk_div = RMT_CLOCK_DIVIDER;
config.gpio_num = (gpio_num_t)pins.pin;
config.mem_block_num = 2; // With longest DCC packet 11 inc checksum (future expansion)
// number of bits needed is 22preamble + start +
// 11*9 + extrazero + EOT = 124
// 2 mem block of 64 RMT items should be enough
config.mem_block_num = RMT_CHAN_PER_DCC_CHAN;
// use config
ESP_ERROR_CHECK(rmt_config(&config));
addPin(pins.invpin, true);
/*
// test: config another gpio pin
gpio_num_t gpioNum = (gpio_num_t)(pin-1);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[gpioNum], PIN_FUNC_GPIO);
gpio_set_direction(gpioNum, GPIO_MODE_OUTPUT);
gpio_matrix_out(gpioNum, RMT_SIG_OUT0_IDX, 0, 0);
*/
// NOTE: ESP_INTR_FLAG_IRAM is *NOT* included in this bitmask
ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, ESP_INTR_FLAG_LOWMED|ESP_INTR_FLAG_SHARED));

View File

@@ -62,6 +62,8 @@ class DCCTimer {
static bool isPWMPin(byte pin);
static void setPWM(byte pin, bool high);
static void clearPWM();
static void startRailcomTimer(byte brakePin);
static void ackRailcomTimer();
static void DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency);
static void DCCEXanalogWrite(uint8_t pin, int value);
@@ -85,6 +87,7 @@ class DCCTimer {
static void reset();
private:
static void DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency);
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

View File

@@ -29,6 +29,7 @@
#include <avr/boot.h>
#include <avr/wdt.h>
#include "DCCTimer.h"
#include "DIAG.h"
#ifdef DEBUG_ADC
#include "TrackManager.h"
#endif
@@ -39,6 +40,9 @@ INTERRUPT_CALLBACK interruptHandler=0;
#define TIMER1_A_PIN 11
#define TIMER1_B_PIN 12
#define TIMER1_C_PIN 13
#define TIMER2_A_PIN 10
#define TIMER2_B_PIN 9
#else
#define TIMER1_A_PIN 9
#define TIMER1_B_PIN 10
@@ -55,6 +59,67 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interrupts();
}
void DCCTimer::startRailcomTimer(byte brakePin) {
/* The Railcom timer is started in such a way that it
- First triggers 28uS after the last TIMER1 tick.
This provides an accurate offset (in High Accuracy mode)
for the start of the Railcom cutout.
- Sets the Railcom pin high at first tick,
because its been setup with 100% PWM duty cycle.
- Cycles at 436uS so the second tick is the
correct distance from the cutout.
- Waveform code is responsible for altering the PWM
duty cycle to 0% any time between the first and last tick.
(there will be 7 DCC timer1 ticks in which to do this.)
*/
(void) brakePin; // Ignored... works on pin 9 only
const int cutoutDuration = 430; // Desired interval in microseconds
// Set up Timer2 for CTC mode (Clear Timer on Compare Match)
TCCR2A = 0; // Clear Timer2 control register A
TCCR2B = 0; // Clear Timer2 control register B
TCNT2 = 0; // Initialize Timer2 counter value to 0
// Configure Phase and Frequency Correct PWM mode
TCCR2A = (1 << COM2B1); // enable pwm on pin 9
TCCR2A |= (1 << WGM20);
// Set Timer 2 prescaler to 32
TCCR2B = (1 << CS21) | (1 << CS20); // 32 prescaler
// Set the compare match value for desired interval
OCR2A = (F_CPU / 1000000) * cutoutDuration / 64 - 1;
// Calculate the compare match value for desired duty cycle
OCR2B = OCR2A+1; // set duty cycle to 100%= OCR2A)
// Enable Timer2 output on pin 9 (OC2B)
DDRB |= (1 << DDB1);
// TODO Fudge TCNT2 to sync with last tcnt1 tick + 28uS
// Previous TIMER1 Tick was at rising end-of-packet bit
// Cutout starts half way through first preamble
// that is 2.5 * 58uS later.
// TCNT1 ticks 8 times / microsecond
// auto microsendsToFirstRailcomTick=(58+58+29)-(TCNT1/8);
// set the railcom timer counter allowing for phase-correct
// CHris's NOTE:
// I dont kniow quite how this calculation works out but
// it does seems to get a good answer.
TCNT2=193 + (ICR1 - TCNT1)/8;
}
void DCCTimer::ackRailcomTimer() {
OCR2B= 0x00; // brake pin pwm duty cycle 0 at next tick
}
// ISR called by timer interrupt every 58uS
ISR(TIMER1_OVF_vect){ interruptHandler(); }
@@ -125,6 +190,81 @@ void DCCTimer::reset() {
}
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
}
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
#if defined(ARDUINO_AVR_UNO)
// Not worth doin something here as:
// If we are on pin 9 or 10 we are on Timer1 and we can not touch Timer1 as that is our DCC source.
// If we are on pin 5 or 6 we are on Timer 0 ad we can not touch Timer0 as that is millis() etc.
// We are most likely not on pin 3 or 11 as no known motor shield has that as brake.
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
// Speed mapping is done like this:
// No functions buttons: 000 0 -> low 131Hz
// Only F29 pressed 001 1 -> mid 490Hz
// F30 with or w/o F29 01x 2-3 -> high 3400Hz
// F31 with or w/o F29/30 1xx 4-7 -> supersonic 62500Hz
uint8_t abits;
uint8_t bbits;
if (pin == 9 || pin == 10) { // timer 2 is different
if (fbits >= 4)
abits = B00000011;
else
abits = B00000001;
if (fbits >= 4)
bbits = B0001;
else if (fbits >= 2)
bbits = B0010;
else if (fbits == 1)
bbits = B0100;
else // fbits == 0
bbits = B0110;
TCCR2A = (TCCR2A & B11111100) | abits; // set WGM0 and WGM1
TCCR2B = (TCCR2B & B11110000) | bbits; // set WGM2 and 3 bits of prescaler
DIAG(F("Timer 2 A=%x B=%x"), TCCR2A, TCCR2B);
} else { // not timer 9 or 10
abits = B01;
if (fbits >= 4)
bbits = B1001;
else if (fbits >= 2)
bbits = B0010;
else if (fbits == 1)
bbits = B0011;
else
bbits = B0100;
switch (pin) {
// case 9 and 10 taken care of above by if()
case 6:
case 7:
case 8:
// Timer4
TCCR4A = (TCCR4A & B11111100) | abits; // set WGM0 and WGM1
TCCR4B = (TCCR4B & B11100000) | bbits; // set WGM2 and WGM3 and divisor
//DIAG(F("Timer 4 A=%x B=%x"), TCCR4A, TCCR4B);
break;
case 46:
case 45:
case 44:
// Timer5
TCCR5A = (TCCR5A & B11111100) | abits; // set WGM0 and WGM1
TCCR5B = (TCCR5B & B11100000) | bbits; // set WGM2 and WGM3 and divisor
//DIAG(F("Timer 5 A=%x B=%x"), TCCR5A, TCCR5B);
break;
default:
break;
}
}
#endif
}
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
#define NUM_ADC_INPUTS 16
#else

View File

@@ -151,10 +151,26 @@ void DCCTimer::reset() {
ESP.restart();
}
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
if (f >= 16)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
else if (f == 7)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 62500);
else if (f >= 4)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 32000);
else if (f >= 3)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 16000);
else if (f >= 2)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3400);
else if (f == 1)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);
else
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 131);
}
#include "esp32-hal.h"
#include "soc/soc_caps.h"
#ifdef SOC_LEDC_SUPPORT_HS_MODE
#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM<<1)
#else
@@ -164,7 +180,7 @@ void DCCTimer::reset() {
static int8_t pin_to_channel[SOC_GPIO_PIN_COUNT] = { 0 };
static int cnt_channel = LEDC_CHANNELS;
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency) {
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency) {
if (pin < SOC_GPIO_PIN_COUNT) {
if (pin_to_channel[pin] != 0) {
ledcSetup(pin_to_channel[pin], frequency, 8);

View File

@@ -80,6 +80,15 @@ extern char *__malloc_heap_start;
interruptHandler();
}
void DCCTimer::startRailcomTimer(byte brakePin) {
// TODO: for intended operation see DCCTimerAVR.cpp
(void) brakePin;
}
void DCCTimer::ackRailcomTimer() {
// TODO: for intended operation see DCCTimerAVR.cpp
}
bool DCCTimer::isPWMPin(byte pin) {
(void) pin;
return false; // TODO what are the relevant pins?
@@ -125,6 +134,11 @@ void DCCTimer::reset() {
while(true){}
}
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
}
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
}
int16_t ADCee::ADCmax() {
return 4095;
}

View File

@@ -76,6 +76,15 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interrupts();
}
void DCCTimer::startRailcomTimer(byte brakePin) {
// TODO: for intended operation see DCCTimerAVR.cpp
(void) brakePin;
}
void DCCTimer::ackRailcomTimer() {
// TODO: for intended operation see DCCTimerAVR.cpp
}
// Timer IRQ handlers replace the dummy handlers (in cortex_handlers)
// copied from rf24 branch
void TCC0_Handler() {
@@ -156,6 +165,11 @@ void DCCTimer::reset() {
while(true) {};
}
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
}
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
}
#define NUM_ADC_INPUTS NUM_ANALOG_INPUTS
uint16_t ADCee::usedpins = 0;

View File

@@ -50,11 +50,16 @@ HardwareSerial Serial6(PA12, PA11); // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14
// via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
// On the F446RE, Serial3 and Serial5 are easy to use:
HardwareSerial Serial3(PC11, PC10); // Rx=PC11, Tx=PC10 -- USART3 - F446RE
HardwareSerial Serial5(PD2, PC12); // Rx=PC7, Tx=PC6 -- UART5 - F446RE
HardwareSerial Serial5(PD2, PC12); // Rx=PD2, Tx=PC12 -- UART5 - F446RE
// On the F446RE, Serial4 and Serial6 also use pins we can't readily map while using the Arduino pins
#elif defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)
#elif defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F446ZE) || \
defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI)
// Nucleo-144 boards don't have Serial1 defined by default
HardwareSerial Serial6(PG9, PG14); // Rx=PG9, Tx=PG14 -- USART6
HardwareSerial Serial5(PD2, PC12); // Rx=PD2, Tx=PC12 -- UART5
#if !defined(ARDUINO_NUCLEO_F412ZG)
HardwareSerial Serial2(PD6, PD5); // Rx=PD6, Tx=PD5 -- UART5
#endif
// Serial3 is defined to use USART3 by default, but is in fact used as the diag console
// via the debugger on the Nucleo-144. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
#else
@@ -196,6 +201,15 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interrupts();
}
void DCCTimer::startRailcomTimer(byte brakePin) {
// TODO: for intended operation see DCCTimerAVR.cpp
(void) brakePin;
}
void DCCTimer::ackRailcomTimer() {
// TODO: for intended operation see DCCTimerAVR.cpp
}
bool DCCTimer::isPWMPin(byte pin) {
//TODO: STM32 whilst this call to digitalPinHasPWM will reveal which pins can do PWM,
// there's no support yet for High Accuracy, so for now return false
@@ -215,9 +229,9 @@ void DCCTimer::clearPWM() {
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
volatile uint32_t *serno1 = (volatile uint32_t *)0x1FFF7A10;
volatile uint32_t *serno2 = (volatile uint32_t *)0x1FFF7A14;
// volatile uint32_t *serno3 = (volatile uint32_t *)0x1FFF7A18;
volatile uint32_t *serno1 = (volatile uint32_t *)UID_BASE;
volatile uint32_t *serno2 = (volatile uint32_t *)UID_BASE+4;
// volatile uint32_t *serno3 = (volatile uint32_t *)UID_BASE+8;
volatile uint32_t m1 = *serno1;
volatile uint32_t m2 = *serno2;
@@ -252,6 +266,23 @@ void DCCTimer::reset() {
while(true) {};
}
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
if (f >= 16)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
else if (f == 7)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 62500);
else if (f >= 4)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 32000);
else if (f >= 3)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 16000);
else if (f >= 2)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3400);
else if (f == 1)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);
else
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 131);
}
// TODO: rationalise the size of these... could really use sparse arrays etc.
static HardwareTimer * pin_timer[100] = {0};
static uint32_t channel_frequency[100] = {0};
@@ -262,7 +293,7 @@ static uint32_t pin_channel[100] = {0};
// sophisticated about detecting any clash between the timer we'd like to use for PWM and the ones
// currently used for HA so they don't interfere with one another. For now we'll just make PWM
// work well... then work backwards to integrate with HA mode if we can.
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency)
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency)
{
if (pin_timer[pin] == NULL) {
// Automatically retrieve TIM instance and channel associated to pin
@@ -306,18 +337,18 @@ void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value) {
// Calculate percentage duty cycle from value given
uint32_t duty_cycle = (value * 100 / 256) + 1;
if (pin_timer[pin] != NULL) {
if (duty_cycle == 100)
{
pin_timer[pin]->pauseChannel(pin_channel[pin]);
DIAG(F("DCCEXanalogWrite::Pausing timer channel on pin %d"), pin);
}
else
{
// if (duty_cycle == 100)
// {
// pin_timer[pin]->pauseChannel(pin_channel[pin]);
// DIAG(F("DCCEXanalogWrite::Pausing timer channel on pin %d"), pin);
// }
// else
// {
pinmap_pinout(digitalPinToPinName(pin), PinMap_TIM); // ensure the pin has been configured!
pin_timer[pin]->resumeChannel(pin_channel[pin]);
// pin_timer[pin]->resumeChannel(pin_channel[pin]);
pin_timer[pin]->setCaptureCompare(pin_channel[pin], duty_cycle, PERCENT_COMPARE_FORMAT); // DCC_EX_PWM_FREQ Hertz, duty_cycle% dutycycle
DIAG(F("DCCEXanalogWrite::Pin %d, value %d, duty cycle %d"), pin, value, duty_cycle);
}
// }
}
else
DIAG(F("DCCEXanalogWrite::Pin %d is not configured for PWM!"), pin);

View File

@@ -39,6 +39,15 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
myDCCTimer.begin(interruptHandler, DCC_SIGNAL_TIME);
}
void DCCTimer::startRailcomTimer(byte brakePin) {
// TODO: for intended operation see DCCTimerAVR.cpp
(void) brakePin;
}
void DCCTimer::ackRailcomTimer() {
// TODO: for intended operation see DCCTimerAVR.cpp
}
bool DCCTimer::isPWMPin(byte pin) {
//Teensy: digitalPinHasPWM, todo
(void) pin;
@@ -141,6 +150,11 @@ void DCCTimer::reset() {
SCB_AIRCR = 0x05FA0004;
}
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
}
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
}
int16_t ADCee::ADCmax() {
return 4095;
}

View File

@@ -106,6 +106,7 @@ void DCCWaveform::interruptHandler() {
DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
isMainTrack = isMain;
packetPending = false;
reminderWindowOpen = false;
memcpy(transmitPacket, idlePacket, sizeof(idlePacket));
state = WAVE_START;
// The +1 below is to allow the preamble generator to create the stop bit
@@ -114,8 +115,22 @@ DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
bytes_sent = 0;
bits_sent = 0;
}
volatile bool DCCWaveform::railcomActive=false; // switched on by user
volatile bool DCCWaveform::railcomDebug=false; // switched on by user
bool DCCWaveform::setRailcom(bool on, bool debug) {
if (on) {
// TODO check possible
railcomActive=true;
railcomDebug=debug;
}
else {
railcomActive=false;
railcomDebug=false;
}
return railcomActive;
}
#pragma GCC push_options
#pragma GCC optimize ("-O3")
@@ -123,13 +138,19 @@ void DCCWaveform::interrupt2() {
// calculate the next bit to be sent:
// set state WAVE_MID_1 for a 1=bit
// or WAVE_HIGH_0 for a 0 bit.
if (remainingPreambles > 0 ) {
state=WAVE_MID_1; // switch state to trigger LOW on next interrupt
remainingPreambles--;
// As we get to the end of the preambles, open the reminder window.
// This delays any reminder insertion until the last moment so
// that the reminder doesn't block a more urgent packet.
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<4 && remainingPreambles>1;
if (remainingPreambles==1) promotePendingPacket();
else if (remainingPreambles==10 && isMainTrack && railcomActive) DCCTimer::ackRailcomTimer();
// 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.
DCCTimer::updateMinimumFreeMemoryISR(22);
else DCCTimer::updateMinimumFreeMemoryISR(22);
return;
}
@@ -148,30 +169,15 @@ void DCCWaveform::interrupt2() {
if (bytes_sent >= transmitLength) {
// end of transmission buffer... repeat or switch to next message
bytes_sent = 0;
// preamble for next packet will start...
remainingPreambles = requiredPreambles;
if (transmitRepeats > 0) {
transmitRepeats--;
// set the railcom coundown to trigger half way
// through the first preamble bit.
// Note.. we are still sending the last packet bit
// and we then have to allow for the packet end bit
if (isMainTrack && railcomActive) DCCTimer::startRailcomTimer(9);
}
else if (packetPending) {
// Copy pending packet to transmit packet
// a fixed length memcpy is faster than a variable length loop for these small lengths
// for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];
memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));
transmitLength = pendingLength;
transmitRepeats = pendingRepeats;
packetPending = false;
clearResets();
}
else {
// Fortunately reset and idle packets are the same length
memcpy( transmitPacket, isMainTrack ? idlePacket : resetPacket, sizeof(idlePacket));
transmitLength = sizeof(idlePacket);
transmitRepeats = 0;
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
}
}
}
}
#pragma GCC pop_options
@@ -193,8 +199,43 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
packetPending = true;
clearResets();
}
bool DCCWaveform::getPacketPending() {
return packetPending;
bool DCCWaveform::isReminderWindowOpen() {
return reminderWindowOpen && ! packetPending;
}
void DCCWaveform::promotePendingPacket() {
// fill the transmission packet from the pending packet
// Just keep going if repeating
if (transmitRepeats > 0) {
transmitRepeats--;
return;
}
if (packetPending) {
// Copy pending packet to transmit packet
// a fixed length memcpy is faster than a variable length loop for these small lengths
// for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];
memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));
transmitLength = pendingLength;
transmitRepeats = pendingRepeats;
packetPending = false;
clearResets();
return;
}
// nothing to do, just send idles or resets
// Fortunately reset and idle packets are the same length
// Note: If railcomDebug is on, then we send resets to the main
// track instead of idles. This means that all data will be zeros
// and only the porersets will be ones, making it much
// easier to read on a logic analyser.
memcpy( transmitPacket, (isMainTrack && (!railcomDebug)) ? idlePacket : resetPacket, sizeof(idlePacket));
transmitLength = sizeof(idlePacket);
transmitRepeats = 0;
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
}
#endif
@@ -266,18 +307,24 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
}
}
bool DCCWaveform::getPacketPending() {
bool DCCWaveform::isReminderWindowOpen() {
if(isMainTrack) {
if (rmtMainChannel == NULL)
return true;
return rmtMainChannel->busy();
return false;
return !rmtMainChannel->busy();
} else {
if (rmtProgChannel == NULL)
return true;
return rmtProgChannel->busy();
return false;
return !rmtProgChannel->busy();
}
}
void IRAM_ATTR DCCWaveform::loop() {
DCCACK::checkAck(progTrack.getResets());
}
bool DCCWaveform::setRailcom(bool on, bool debug) {
// TODO... ESP32 railcom waveform
return false;
}
#endif

View File

@@ -2,7 +2,7 @@
* © 2021 M Steve Todd
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2024 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
*
@@ -33,14 +33,21 @@
// 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.
const byte PREAMBLE_BITS_MAIN = 16;
const byte 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};
enum WAVE_STATE : byte {
WAVE_START=0, // wave going high at start of bit
WAVE_MID_1=1, // middle of 1 bit
WAVE_HIGH_0=2, // first part of 0 bit high
WAVE_MID_0=3, // middle of 0 bit
WAVE_LOW_0=4, // first part of 0 bit low
WAVE_PENDING=5 // next bit not yet known
};
// NOTE: static functions are used for the overall controller, then
// one instance is created for each track.
@@ -76,11 +83,15 @@ class DCCWaveform {
};
#endif
void schedulePacket(const byte buffer[], byte byteCount, byte repeats);
bool getPacketPending();
bool isReminderWindowOpen();
void promotePendingPacket();
static bool setRailcom(bool on, bool debug);
static bool isRailcom() {return railcomActive;}
private:
#ifndef ARDUINO_ARCH_ESP32
volatile bool packetPending;
volatile bool reminderWindowOpen;
volatile byte sentResetsSincePacket;
#else
volatile uint32_t resetPacketBase;
@@ -101,6 +112,9 @@ class DCCWaveform {
byte pendingPacket[MAX_PACKET_SIZE+1]; // +1 for checksum
byte pendingLength;
byte pendingRepeats;
static volatile bool railcomActive; // switched on by user
static volatile bool railcomDebug; // switched on by user
#ifdef ARDUINO_ARCH_ESP32
static RMTChannel *rmtMainChannel;
static RMTChannel *rmtProgChannel;

View File

@@ -37,7 +37,9 @@
class Display : public DisplayInterface {
public:
Display(DisplayDevice *deviceDriver);
#if !defined (MAX_CHARACTER_ROWS)
static const int MAX_CHARACTER_ROWS = 8;
#endif
static const int MAX_CHARACTER_COLS = MAX_MSG_SIZE;
static const long DISPLAY_SCROLL_TIME = 3000; // 3 seconds

View File

@@ -54,7 +54,9 @@
xxx; \
t->refresh();}
#else
#define DISPLAY_START(xxx) {}
#define DISPLAY_START(xxx) { \
xxx; \
}
#endif
#endif // LCD_Implementation_h

View File

@@ -55,22 +55,6 @@
#include "Turntables.h"
#include "IODevice.h"
// Command parsing keywords
const int16_t HASH_KEYWORD_EXRAIL=15435;
const int16_t HASH_KEYWORD_ON = 2657;
const int16_t HASH_KEYWORD_START=23232;
const int16_t HASH_KEYWORD_RESERVE=11392;
const int16_t HASH_KEYWORD_FREE=-23052;
const int16_t HASH_KEYWORD_LATCH=1618;
const int16_t HASH_KEYWORD_UNLATCH=1353;
const int16_t HASH_KEYWORD_PAUSE=-4142;
const int16_t HASH_KEYWORD_RESUME=27609;
const int16_t HASH_KEYWORD_KILL=5218;
const int16_t HASH_KEYWORD_ALL=3457;
const int16_t HASH_KEYWORD_ROUTES=-3702;
const int16_t HASH_KEYWORD_RED=26099;
const int16_t HASH_KEYWORD_AMBER=18713;
const int16_t HASH_KEYWORD_GREEN=-31493;
// One instance of RMFT clas is used for each "thread" in the automation.
// Each thread manages a loco on a journey through the layout, and/or may manage a scenery automation.
@@ -85,8 +69,8 @@ RMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE.
// when pausingTask is set, that is the ONLY task that gets any service,
// and all others will have their locos stopped, then resumed after the pausing task resumes.
byte RMFT2::flags[MAX_FLAGS];
LookList * RMFT2::sequenceLookup=NULL;
Print * RMFT2::LCCSerial=0;
LookList * RMFT2::routeLookup=NULL;
LookList * RMFT2::onThrowLookup=NULL;
LookList * RMFT2::onCloseLookup=NULL;
LookList * RMFT2::onActivateLookup=NULL;
@@ -100,9 +84,10 @@ LookList * RMFT2::onClockLookup=NULL;
LookList * RMFT2::onRotateLookup=NULL;
#endif
LookList * RMFT2::onOverloadLookup=NULL;
#define GET_OPCODE GETHIGHFLASH(RMFT2::RouteCode,progCounter)
#define SKIPOP progCounter+=3
byte * RMFT2::routeStateArray=nullptr;
const FSH * * RMFT2::routeCaptionArray=nullptr;
int16_t * RMFT2::stashArray=nullptr;
int16_t RMFT2::maxStashId=0;
// getOperand instance version, uses progCounter from instance.
uint16_t RMFT2::getOperand(byte n) {
@@ -120,6 +105,7 @@ uint16_t RMFT2::getOperand(int progCounter,byte n) {
LookList::LookList(int16_t size) {
m_size=size;
m_loaded=0;
m_chain=nullptr;
if (size) {
m_lookupArray=new int16_t[size];
m_resultArray=new int16_t[size];
@@ -137,8 +123,35 @@ int16_t LookList::find(int16_t value) {
for (int16_t i=0;i<m_size;i++) {
if (m_lookupArray[i]==value) return m_resultArray[i];
}
return m_chain ? m_chain->find(value) :-1;
}
void LookList::chain(LookList * chain) {
m_chain=chain;
}
void LookList::handleEvent(const FSH* reason,int16_t id) {
// New feature... create multiple ONhandlers
for (int i=0;i<m_size;i++)
if (m_lookupArray[i]==id)
RMFT2::startNonRecursiveTask(reason,id,m_resultArray[i]);
}
void LookList::stream(Print * _stream) {
for (int16_t i=0;i<m_size;i++) {
_stream->print(" ");
_stream->print(m_lookupArray[i]);
}
}
int16_t LookList::findPosition(int16_t value) {
for (int16_t i=0;i<m_size;i++) {
if (m_lookupArray[i]==value) return i;
}
return -1;
}
int16_t LookList::size() {
return m_size;
}
LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
int progCounter;
@@ -171,28 +184,36 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
for (int f=0;f<MAX_FLAGS;f++) flags[f]=0;
// create lookups
sequenceLookup=LookListLoader(OPCODE_ROUTE, OPCODE_AUTOMATION,OPCODE_SEQUENCE);
routeLookup=LookListLoader(OPCODE_ROUTE, OPCODE_AUTOMATION);
routeLookup->chain(LookListLoader(OPCODE_SEQUENCE));
if (compileFeatures && FEATURE_ROUTESTATE) {
routeStateArray=(byte *)calloc(routeLookup->size(),sizeof(byte));
routeCaptionArray=(const FSH * *)calloc(routeLookup->size(),sizeof(const FSH *));
}
onThrowLookup=LookListLoader(OPCODE_ONTHROW);
onCloseLookup=LookListLoader(OPCODE_ONCLOSE);
onActivateLookup=LookListLoader(OPCODE_ONACTIVATE);
onDeactivateLookup=LookListLoader(OPCODE_ONDEACTIVATE);
onRedLookup=LookListLoader(OPCODE_ONRED);
onAmberLookup=LookListLoader(OPCODE_ONAMBER);
onGreenLookup=LookListLoader(OPCODE_ONGREEN);
onChangeLookup=LookListLoader(OPCODE_ONCHANGE);
onClockLookup=LookListLoader(OPCODE_ONTIME);
#ifndef IO_NO_HAL
onRotateLookup=LookListLoader(OPCODE_ONROTATE);
#endif
onOverloadLookup=LookListLoader(OPCODE_ONOVERLOAD);
// onLCCLookup is not the same so not loaded here.
// Second pass startup, define any turnouts or servos, set signals red
// add sequences onRoutines to the lookups
if (compileFeatures & FEATURE_SIGNAL) {
onRedLookup=LookListLoader(OPCODE_ONRED);
onAmberLookup=LookListLoader(OPCODE_ONAMBER);
onGreenLookup=LookListLoader(OPCODE_ONGREEN);
for (int sigslot=0;;sigslot++) {
VPIN sigid=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigslot*8);
if (sigid==0) break; // end of signal list
doSignal(sigid & SIGNAL_ID_MASK, SIGNAL_RED);
}
}
int progCounter;
for (progCounter=0;; SKIPOP){
@@ -213,6 +234,12 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
IODevice::configureInput((VPIN)pin,true);
break;
}
case OPCODE_STASH:
case OPCODE_CLEAR_STASH:
case OPCODE_PICKUP_STASH: {
maxStashId=max(maxStashId,((int16_t)operand));
break;
}
case OPCODE_ATGTE:
case OPCODE_ATLT:
@@ -292,203 +319,42 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
}
}
SKIPOP; // include ENDROUTES opcode
DIAG(F("EXRAIL %db, fl=%d"),progCounter,MAX_FLAGS);
if (compileFeatures & FEATURE_STASH) {
// create the stash array from the highest id found
if (maxStashId>0) stashArray=(int16_t*)calloc(maxStashId+1, sizeof(int16_t));
//TODO check EEPROM and fetch stashArray
}
DIAG(F("EXRAIL %db, fl=%d, stash=%d"),progCounter,MAX_FLAGS, maxStashId);
// Removed for 4.2.31 new RMFT2(0); // add the startup route
diag=saved_diag;
}
void RMFT2::setTurnoutHiddenState(Turnout * t) {
// turnout descriptions are in low flash F strings
t->setHidden(GETFLASH(getTurnoutDescription(t->getId()))==0x01);
// turnout descriptions are in low flash F strings
const FSH *desc = getTurnoutDescription(t->getId());
if (desc) t->setHidden(GETFLASH(desc)==0x01);
}
#ifndef IO_NO_HAL
void RMFT2::setTurntableHiddenState(Turntable * tto) {
tto->setHidden(GETFLASH(getTurntableDescription(tto->getId()))==0x01);
const FSH *desc = getTurntableDescription(tto->getId());
if (desc) tto->setHidden(GETFLASH(desc)==0x01);
}
#endif
char RMFT2::getRouteType(int16_t id) {
for (int16_t i=0;;i+=2) {
int16_t rid= GETHIGHFLASHW(routeIdList,i);
if (rid==INT16_MAX) break;
if (rid==id) return 'R';
}
for (int16_t i=0;;i+=2) {
int16_t rid= GETHIGHFLASHW(automationIdList,i);
if (rid==INT16_MAX) break;
if (rid==id) return 'A';
int16_t progCounter=routeLookup->find(id);
if (progCounter>=0) {
byte type=GET_OPCODE;
if (type==OPCODE_ROUTE) return 'R';
if (type==OPCODE_AUTOMATION) return 'A';
}
return 'X';
}
// This filter intercepts <> commands to do the following:
// - Implement RMFT specific commands/diagnostics
// - Reject/modify JMRI commands that would interfere with RMFT processing
void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
(void)stream; // avoid compiler warning if we don't access this parameter
bool reject=false;
switch(opcode) {
case 'D':
if (p[0]==HASH_KEYWORD_EXRAIL) { // <D EXRAIL ON/OFF>
diag = paramCount==2 && (p[1]==HASH_KEYWORD_ON || p[1]==1);
opcode=0;
}
break;
case '/': // New EXRAIL command
reject=!parseSlash(stream,paramCount,p);
opcode=0;
break;
default: // other commands pass through
break;
}
if (reject) {
opcode=0;
StringFormatter::send(stream,F("<X>"));
}
}
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
if (paramCount==0) { // STATUS
StringFormatter::send(stream, F("<* EXRAIL STATUS"));
RMFT2 * task=loopTask;
while(task) {
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d%c,SPEED=%d%c"),
(int)(task->taskId),task->progCounter,task->loco,
task->invert?'I':' ',
task->speedo,
task->forward?'F':'R'
);
task=task->next;
if (task==loopTask) break;
}
// Now stream the flags
for (int id=0;id<MAX_FLAGS; id++) {
byte flag=flags[id];
if (flag & ~TASK_FLAG & ~SIGNAL_MASK) { // not interested in TASK_FLAG only. Already shown above
StringFormatter::send(stream,F("\nflags[%d] "),id);
if (flag & SECTION_FLAG) StringFormatter::send(stream,F(" RESERVED"));
if (flag & LATCH_FLAG) StringFormatter::send(stream,F(" LATCHED"));
}
}
// do the signals
// flags[n] represents the state of the nth signal in the table
for (int sigslot=0;;sigslot++) {
VPIN sigid=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigslot*8);
if (sigid==0) break; // end of signal list
byte flag=flags[sigslot] & SIGNAL_MASK; // obtain signal flags for this id
StringFormatter::send(stream,F("\n%S[%d]"),
(flag == SIGNAL_RED)? F("RED") : (flag==SIGNAL_GREEN) ? F("GREEN") : F("AMBER"),
sigid & SIGNAL_ID_MASK);
}
StringFormatter::send(stream,F(" *>\n"));
return true;
}
switch (p[0]) {
case HASH_KEYWORD_PAUSE: // </ PAUSE>
if (paramCount!=1) return false;
DCC::setThrottle(0,1,true); // pause all locos on the track
pausingTask=(RMFT2 *)1; // Impossible task address
return true;
case HASH_KEYWORD_RESUME: // </ RESUME>
if (paramCount!=1) return false;
pausingTask=NULL;
{
RMFT2 * task=loopTask;
while(task) {
if (task->loco) task->driveLoco(task->speedo);
task=task->next;
if (task==loopTask) break;
}
}
return true;
case HASH_KEYWORD_START: // </ START [cab] route >
if (paramCount<2 || paramCount>3) return false;
{
int route=(paramCount==2) ? p[1] : p[2];
uint16_t cab=(paramCount==2)? 0 : p[1];
int pc=sequenceLookup->find(route);
if (pc<0) return false;
RMFT2* task=new RMFT2(pc);
task->loco=cab;
}
return true;
default:
break;
}
// check KILL ALL here, otherwise the next validation confuses ALL with a flag
if (p[0]==HASH_KEYWORD_KILL && p[1]==HASH_KEYWORD_ALL) {
while (loopTask) loopTask->kill(F("KILL ALL")); // destructor changes loopTask
return true;
}
// all other / commands take 1 parameter
if (paramCount!=2 ) return false;
switch (p[0]) {
case HASH_KEYWORD_KILL: // Kill taskid|ALL
{
if ( p[1]<0 || p[1]>=MAX_FLAGS) return false;
RMFT2 * task=loopTask;
while(task) {
if (task->taskId==p[1]) {
task->kill(F("KILL"));
return true;
}
task=task->next;
if (task==loopTask) break;
}
}
return false;
case HASH_KEYWORD_RESERVE: // force reserve a section
return setFlag(p[1],SECTION_FLAG);
case HASH_KEYWORD_FREE: // force free a section
return setFlag(p[1],0,SECTION_FLAG);
case HASH_KEYWORD_LATCH:
return setFlag(p[1], LATCH_FLAG);
case HASH_KEYWORD_UNLATCH:
return setFlag(p[1], 0, LATCH_FLAG);
case HASH_KEYWORD_RED:
doSignal(p[1],SIGNAL_RED);
return true;
case HASH_KEYWORD_AMBER:
doSignal(p[1],SIGNAL_AMBER);
return true;
case HASH_KEYWORD_GREEN:
doSignal(p[1],SIGNAL_GREEN);
return true;
default:
return false;
}
}
// This emits Routes and Automations to Withrottle
// Automations are given a state to set the button to "handoff" which implies
// handing over the loco to the automation.
// Routes are given "Set" buttons and do not cause the loco to be handed over.
RMFT2::RMFT2(int progCtr) {
progCounter=progCtr;
@@ -537,7 +403,7 @@ RMFT2::~RMFT2() {
}
void RMFT2::createNewTask(int route, uint16_t cab) {
int pc=sequenceLookup->find(route);
int pc=routeLookup->find(route);
if (pc<0) return;
RMFT2* task=new RMFT2(pc);
task->loco=cab;
@@ -550,7 +416,6 @@ void RMFT2::driveLoco(byte speed) {
power on appropriate track if DC or main if dcc
if (TrackManager::getMainPowerMode()==POWERMODE::OFF) {
TrackManager::setMainPower(POWERMODE::ON);
CommandDistributor::broadcastPower();
}
**********/
@@ -778,7 +643,20 @@ void RMFT2::loop2() {
case OPCODE_POWEROFF:
TrackManager::setPower(POWERMODE::OFF);
TrackManager::setJoin(false);
CommandDistributor::broadcastPower();
break;
case OPCODE_SET_POWER:
// operand is TRACK_POWER , trackid
//byte thistrack=getOperand(1);
switch (operand) {
case TRACK_POWER_0:
TrackManager::setTrackPower(POWERMODE::OFF, getOperand(1));
break;
case TRACK_POWER_1:
TrackManager::setTrackPower(POWERMODE::ON, getOperand(1));
break;
}
break;
case OPCODE_SET_TRACK:
@@ -786,11 +664,50 @@ void RMFT2::loop2() {
// If DC/DCX use my loco for DC address
{
TRACK_MODE mode = (TRACK_MODE)(operand>>8);
int16_t cab=(mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX) ? loco : 0;
int16_t cab=(mode & TRACK_MODE_DC) ? loco : 0;
TrackManager::setTrackMode(operand & 0x0F, mode, cab);
}
break;
case OPCODE_SETFREQ:
// Frequency is default 0, or 1, 2,3
//if (loco) DCC::setFn(loco,operand,true);
switch (operand) {
case 0: // default - all F-s off
if (loco) {
DCC::setFn(loco,29,false);
DCC::setFn(loco,30,false);
DCC::setFn(loco,31,false);
}
break;
case 1:
if (loco) {
DCC::setFn(loco,29,true);
DCC::setFn(loco,30,false);
DCC::setFn(loco,31,false);
}
break;
case 2:
if (loco) {
DCC::setFn(loco,29,false);
DCC::setFn(loco,30,true);
DCC::setFn(loco,31,false);
}
break;
case 3:
if (loco) {
DCC::setFn(loco,29,false);
DCC::setFn(loco,30,false);
DCC::setFn(loco,31,true);
}
break;
default:
; // do nothing
break;
}
break;
case OPCODE_RESUME:
pausingTask=NULL;
driveLoco(speedo);
@@ -922,9 +839,17 @@ void RMFT2::loop2() {
DCC::setAccessory(addr,subaddr,active);
break;
}
case OPCODE_ASPECT: {
// operand is address<<5 | value
int16_t address=operand>>5;
byte aspect=operand & 0x1f;
if (!signalAspectEvent(address,aspect))
DCC::setExtendedAccessory(address,aspect);
break;
}
case OPCODE_FOLLOW:
progCounter=sequenceLookup->find(operand);
progCounter=routeLookup->find(operand);
if (progCounter<0) kill(F("FOLLOW unknown"), operand);
return;
@@ -934,7 +859,7 @@ void RMFT2::loop2() {
return;
}
callStack[stackDepth++]=progCounter+3;
progCounter=sequenceLookup->find(operand);
progCounter=routeLookup->find(operand);
if (progCounter<0) kill(F("CALL unknown"),operand);
return;
@@ -959,12 +884,10 @@ void RMFT2::loop2() {
case OPCODE_JOIN:
TrackManager::setPower(POWERMODE::ON);
TrackManager::setJoin(true);
CommandDistributor::broadcastPower();
break;
case OPCODE_UNJOIN:
TrackManager::setJoin(false);
CommandDistributor::broadcastPower();
break;
case OPCODE_READ_LOCO1: // READ_LOCO is implemented as 2 separate opcodes
@@ -992,12 +915,11 @@ void RMFT2::loop2() {
case OPCODE_POWERON:
TrackManager::setMainPower(POWERMODE::ON);
TrackManager::setJoin(false);
CommandDistributor::broadcastPower();
break;
case OPCODE_START:
{
int newPc=sequenceLookup->find(operand);
int newPc=routeLookup->find(operand);
if (newPc<0) break;
new RMFT2(newPc);
}
@@ -1005,7 +927,7 @@ void RMFT2::loop2() {
case OPCODE_SENDLOCO: // cab, route
{
int newPc=sequenceLookup->find(getOperand(1));
int newPc=routeLookup->find(getOperand(1));
if (newPc<0) break;
RMFT2* newtask=new RMFT2(newPc); // create new task
newtask->loco=operand;
@@ -1020,7 +942,21 @@ void RMFT2::loop2() {
invert=false;
}
break;
case OPCODE_LCC: // short form LCC
if ((compileFeatures & FEATURE_LCC) && LCCSerial)
StringFormatter::send(LCCSerial,F("<L x%h>"),(uint16_t)operand);
break;
case OPCODE_LCCX: // long form LCC
if ((compileFeatures & FEATURE_LCC) && LCCSerial)
StringFormatter::send(LCCSerial,F("<L x%h%h%h%h>\n"),
getOperand(progCounter,1),
getOperand(progCounter,2),
getOperand(progCounter,3),
getOperand(progCounter,0)
);
break;
case OPCODE_SERVO: // OPCODE_SERVO,V(vpin),OPCODE_PAD,V(position),OPCODE_PAD,V(profile),OPCODE_PAD,V(duration)
IODevice::writeAnalogue(operand,getOperand(1),getOperand(2),getOperand(3));
@@ -1045,7 +981,47 @@ void RMFT2::loop2() {
case OPCODE_PRINT:
printMessage(operand);
break;
case OPCODE_ROUTE_HIDDEN:
manageRouteState(operand,2);
break;
case OPCODE_ROUTE_INACTIVE:
manageRouteState(operand,0);
break;
case OPCODE_ROUTE_ACTIVE:
manageRouteState(operand,1);
break;
case OPCODE_ROUTE_DISABLED:
manageRouteState(operand,4);
break;
case OPCODE_STASH:
if (compileFeatures & FEATURE_STASH)
stashArray[operand] = invert? -loco : loco;
break;
case OPCODE_CLEAR_STASH:
if (compileFeatures & FEATURE_STASH)
stashArray[operand] = 0;
break;
case OPCODE_CLEAR_ALL_STASH:
if (compileFeatures & FEATURE_STASH)
for (int i=0;i<=maxStashId;i++) stashArray[operand]=0;
break;
case OPCODE_PICKUP_STASH:
if (compileFeatures & FEATURE_STASH) {
int16_t x=stashArray[operand];
if (x>=0) {
loco=x;
invert=false;
break;
}
loco=-x;
invert=true;
}
break;
case OPCODE_ROUTE:
case OPCODE_AUTOMATION:
case OPCODE_SEQUENCE:
@@ -1058,6 +1034,7 @@ void RMFT2::loop2() {
case OPCODE_SERVOTURNOUT: // Turnout definition ignored at runtime
case OPCODE_PINTURNOUT: // Turnout definition ignored at runtime
case OPCODE_ONCLOSE: // Turnout event catchers ignored here
case OPCODE_ONLCC: // LCC event catchers ignored here
case OPCODE_ONTHROW:
case OPCODE_ONACTIVATE: // Activate event catchers ignored here
case OPCODE_ONDEACTIVATE:
@@ -1127,13 +1104,14 @@ int16_t RMFT2::getSignalSlot(int16_t id) {
}
/* static */ void RMFT2::doSignal(int16_t id,char rag) {
if (!(compileFeatures & FEATURE_SIGNAL)) return; // dont compile code below
if (diag) DIAG(F(" doSignal %d %x"),id,rag);
// Schedule any event handler for this signal change.
// Thjis will work even without a signal definition.
if (rag==SIGNAL_RED) handleEvent(F("RED"),onRedLookup,id);
else if (rag==SIGNAL_GREEN) handleEvent(F("GREEN"), onGreenLookup,id);
else handleEvent(F("AMBER"), onAmberLookup,id);
// This will work even without a signal definition.
if (rag==SIGNAL_RED) onRedLookup->handleEvent(F("RED"),id);
else if (rag==SIGNAL_GREEN) onGreenLookup->handleEvent(F("GREEN"),id);
else onAmberLookup->handleEvent(F("AMBER"),id);
int16_t sigslot=getSignalSlot(id);
if (sigslot<0) return;
@@ -1167,6 +1145,16 @@ int16_t RMFT2::getSignalSlot(int16_t id) {
return;
}
if (sigtype== DCCX_SIGNAL_FLAG) {
// redpin,amberpin,greenpin are the 3 aspects
byte value=redpin;
if (rag==SIGNAL_AMBER) value=amberpin;
if (rag==SIGNAL_GREEN) value=greenpin;
DCC::setExtendedAccessory(sigid & SIGNAL_ID_MASK,value);
return;
}
// LED or similar 3 pin signal, (all pins zero would be a virtual signal)
// If amberpin is zero, synthesise amber from red+green
const byte SIMAMBER=0x00;
@@ -1194,33 +1182,66 @@ int16_t RMFT2::getSignalSlot(int16_t id) {
}
/* static */ bool RMFT2::isSignal(int16_t id,char rag) {
if (!(compileFeatures & FEATURE_SIGNAL)) return false;
int16_t sigslot=getSignalSlot(id);
if (sigslot<0) return false;
return (flags[sigslot] & SIGNAL_MASK) == rag;
}
// signalAspectEvent returns true if the aspect is destined
// for a defined DCCX_SIGNAL which will handle all the RAG flags
// and ON* handlers.
// Otherwise false so the parser should send the command directly
bool RMFT2::signalAspectEvent(int16_t address, byte aspect ) {
if (!(compileFeatures & FEATURE_SIGNAL)) return false;
int16_t sigslot=getSignalSlot(address);
if (sigslot<0) return false; // this is not a defined signal
int16_t sigpos=sigslot*8;
VPIN sigid=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos);
VPIN sigtype=sigid & ~SIGNAL_ID_MASK;
if (sigtype!=DCCX_SIGNAL_FLAG) return false; // not a DCCX signal
// Turn an aspect change into a RED/AMBER/GREEN setting
if (aspect==GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+2)) {
doSignal(sigid,SIGNAL_RED);
return true;
}
if (aspect==GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+4)) {
doSignal(sigid,SIGNAL_AMBER);
return true;
}
if (aspect==GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+6)) {
doSignal(sigid,SIGNAL_GREEN);
return true;
}
return false; // aspect is not a defined one
}
void RMFT2::turnoutEvent(int16_t turnoutId, bool closed) {
// Hunt for an ONTHROW/ONCLOSE for this turnout
if (closed) handleEvent(F("CLOSE"),onCloseLookup,turnoutId);
else handleEvent(F("THROW"),onThrowLookup,turnoutId);
if (closed) onCloseLookup->handleEvent(F("CLOSE"),turnoutId);
else onThrowLookup->handleEvent(F("THROW"),turnoutId);
}
void RMFT2::activateEvent(int16_t addr, bool activate) {
// Hunt for an ONACTIVATE/ONDEACTIVATE for this accessory
if (activate) handleEvent(F("ACTIVATE"),onActivateLookup,addr);
else handleEvent(F("DEACTIVATE"),onDeactivateLookup,addr);
if (activate) onActivateLookup->handleEvent(F("ACTIVATE"),addr);
else onDeactivateLookup->handleEvent(F("DEACTIVATE"),addr);
}
void RMFT2::changeEvent(int16_t vpin, bool change) {
// Hunt for an ONCHANGE for this sensor
if (change) handleEvent(F("CHANGE"),onChangeLookup,vpin);
if (change) onChangeLookup->handleEvent(F("CHANGE"),vpin);
}
#ifndef IO_NO_HAL
void RMFT2::rotateEvent(int16_t turntableId, bool change) {
// Hunt or an ONROTATE for this turntable
if (change) handleEvent(F("ROTATE"),onRotateLookup,turntableId);
if (change) onRotateLookup->handleEvent(F("ROTATE"),turntableId);
}
#endif
@@ -1229,8 +1250,8 @@ void RMFT2::clockEvent(int16_t clocktime, bool change) {
if (Diag::CMD)
DIAG(F("Looking for clock event at : %d"), clocktime);
if (change) {
handleEvent(F("CLOCK"),onClockLookup,clocktime);
handleEvent(F("CLOCK"),onClockLookup,25*60+clocktime%60);
onClockLookup->handleEvent(F("CLOCK"),clocktime);
onClockLookup->handleEvent(F("CLOCK"),25*60+clocktime%60);
}
}
@@ -1239,15 +1260,11 @@ void RMFT2::powerEvent(int16_t track, bool overload) {
if (Diag::CMD)
DIAG(F("Looking for Power event on track : %c"), track);
if (overload) {
handleEvent(F("POWER"),onOverloadLookup,track);
onOverloadLookup->handleEvent(F("POWER"),track);
}
}
void RMFT2::handleEvent(const FSH* reason,LookList* handlers, int16_t id) {
int pc= handlers->find(id);
if (pc<0) return;
void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc) {
// Check we dont already have a task running this handler
RMFT2 * task=loopTask;
while(task) {
@@ -1317,6 +1334,7 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
break;
case thrunge_parse:
case thrunge_broadcast:
case thrunge_message:
case thrunge_lcd:
default: // thrunge_lcd+1, ...
if (!buffer) buffer=new StringBuffer();
@@ -1354,6 +1372,9 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
case thrunge_withrottle:
CommandDistributor::broadcastRaw(CommandDistributor::WITHROTTLE_TYPE,buffer->getString());
break;
case thrunge_message:
CommandDistributor::broadcastMessage(buffer->getString());
break;
case thrunge_lcd:
LCD(id,F("%s"),buffer->getString());
break;
@@ -1363,3 +1384,29 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
break;
}
}
void RMFT2::manageRouteState(uint16_t id, byte state) {
if (compileFeatures && FEATURE_ROUTESTATE) {
// Route state must be maintained for when new throttles connect.
// locate route id in the Routes lookup
int16_t position=routeLookup->findPosition(id);
if (position<0) return;
// set state beside it
if (routeStateArray[position]==state) return;
routeStateArray[position]=state;
CommandDistributor::broadcastRouteState(id,state);
}
}
void RMFT2::manageRouteCaption(uint16_t id,const FSH* caption) {
if (compileFeatures && FEATURE_ROUTESTATE) {
// Route state must be maintained for when new throttles connect.
// locate route id in the Routes lookup
int16_t position=routeLookup->findPosition(id);
if (position<0) return;
// set state beside it
if (routeCaptionArray[position]==caption) return;
routeCaptionArray[position]=caption;
CommandDistributor::broadcastRouteCaption(id,caption);
}
}

View File

@@ -51,24 +51,26 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2,
#endif
OPCODE_POM,
OPCODE_START,OPCODE_SETLOCO,OPCODE_SENDLOCO,OPCODE_FORGET,
OPCODE_START,OPCODE_SETLOCO,OPCODE_SETFREQ,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_PRINT,OPCODE_DCCACTIVATE,OPCODE_ASPECT,
OPCODE_ONACTIVATE,OPCODE_ONDEACTIVATE,
OPCODE_ROSTER,OPCODE_KILLALL,
OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,
OPCODE_ENDTASK,OPCODE_ENDEXRAIL,
OPCODE_SET_TRACK,
OPCODE_SET_TRACK,OPCODE_SET_POWER,
OPCODE_ONRED,OPCODE_ONAMBER,OPCODE_ONGREEN,
OPCODE_ONCHANGE,
OPCODE_ONCLOCKTIME,
OPCODE_ONTIME,
#ifndef IO_NO_HAL
OPCODE_TTADDPOSITION,OPCODE_DCCTURNTABLE,OPCODE_EXTTTURNTABLE,
OPCODE_ONROTATE,OPCODE_ROTATE,OPCODE_IFTTPOSITION,OPCODE_WAITFORTT,
#endif
OPCODE_ONROTATE,OPCODE_ROTATE,OPCODE_WAITFORTT,
OPCODE_LCC,OPCODE_LCCX,OPCODE_ONLCC,
OPCODE_ONOVERLOAD,
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
OPCODE_ROUTE_DISABLED,
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
// OPcodes below this point are skip-nesting IF operations
// placed here so that they may be skipped as a group
@@ -81,7 +83,8 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
OPCODE_IFRANDOM,OPCODE_IFRESERVE,
OPCODE_IFCLOSED,OPCODE_IFTHROWN,
OPCODE_IFRE,
OPCODE_IFLOCO
OPCODE_IFLOCO,
OPCODE_IFTTPOSITION
};
// Ensure thrunge_lcd is put last as there may be more than one display,
@@ -91,11 +94,17 @@ enum thrunger: byte {
thrunge_serial,thrunge_parse,
thrunge_serial1, thrunge_serial2, thrunge_serial3,
thrunge_serial4, thrunge_serial5, thrunge_serial6,
thrunge_lcn,
thrunge_lcn,thrunge_message,
thrunge_lcd, // Must be last!!
};
// Flag bits for compile time features.
static const byte FEATURE_SIGNAL= 0x80;
static const byte FEATURE_LCC = 0x40;
static const byte FEATURE_ROSTER= 0x20;
static const byte FEATURE_ROUTESTATE= 0x10;
static const byte FEATURE_STASH = 0x08;
// Flag bits for status of hardware and TPL
static const byte SECTION_FLAG = 0x80;
@@ -115,13 +124,20 @@ enum thrunger: byte {
class LookList {
public:
LookList(int16_t size);
void chain(LookList* chainTo);
void add(int16_t lookup, int16_t result);
int16_t find(int16_t value);
int16_t find(int16_t value); // finds result value
int16_t findPosition(int16_t value); // finds index
int16_t size();
void stream(Print * _stream);
void handleEvent(const FSH* reason,int16_t id);
private:
int16_t m_size;
int16_t m_loaded;
int16_t * m_lookupArray;
int16_t * m_resultArray;
int16_t * m_resultArray;
LookList* m_chain;
};
class RMFT2 {
@@ -139,9 +155,11 @@ class LookList {
static void clockEvent(int16_t clocktime, bool change);
static void rotateEvent(int16_t id, bool change);
static void powerEvent(int16_t track, bool overload);
static bool signalAspectEvent(int16_t address, byte aspect );
static const int16_t SERVO_SIGNAL_FLAG=0x4000;
static const int16_t ACTIVE_HIGH_SIGNAL_FLAG=0x2000;
static const int16_t DCC_SIGNAL_FLAG=0x1000;
static const int16_t DCCX_SIGNAL_FLAG=0x3000;
static const int16_t SIGNAL_ID_MASK=0x0FFF;
// Throttle Info Access functions built by exrail macros
static const byte rosterNameCount;
@@ -155,7 +173,8 @@ class LookList {
static const FSH * getRosterFunctions(int16_t id);
static const FSH * getTurntableDescription(int16_t id);
static const FSH * getTurntablePositionDescription(int16_t turntableId, uint8_t positionId);
static void startNonRecursiveTask(const FSH* reason, int16_t id,int pc);
private:
static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
static bool parseSlash(Print * stream, byte & paramCount, int16_t p[]) ;
@@ -172,7 +191,6 @@ private:
#endif
static LookList* LookListLoader(OPCODE op1,
OPCODE op2=OPCODE_ENDEXRAIL,OPCODE op3=OPCODE_ENDEXRAIL);
static void handleEvent(const FSH* reason,LookList* handlers, int16_t id);
static uint16_t getOperand(int progCounter,byte n);
static RMFT2 * loopTask;
static RMFT2 * pausingTask;
@@ -189,10 +207,11 @@ private:
uint16_t getOperand(byte n);
static bool diag;
static const HIGHFLASH byte RouteCode[];
static const HIGHFLASH3 byte RouteCode[];
static const HIGHFLASH int16_t SignalDefinitions[];
static byte flags[MAX_FLAGS];
static LookList * sequenceLookup;
static Print * LCCSerial;
static LookList * routeLookup;
static LookList * onThrowLookup;
static LookList * onCloseLookup;
static LookList * onActivateLookup;
@@ -206,6 +225,16 @@ private:
static LookList * onRotateLookup;
#endif
static LookList * onOverloadLookup;
static const int countLCCLookup;
static int onLCCLookup[];
static const byte compileFeatures;
static void manageRouteState(uint16_t id, byte state);
static void manageRouteCaption(uint16_t id, const FSH* caption);
static byte * routeStateArray;
static const FSH ** routeCaptionArray;
static int16_t * stashArray;
static int16_t maxStashId;
// Local variables - exist for each instance/task
RMFT2 *next; // loop chain
@@ -227,4 +256,27 @@ private:
byte stackDepth;
int callStack[MAX_STACK_DEPTH];
};
#define GET_OPCODE GETHIGHFLASH(RMFT2::RouteCode,progCounter)
#define SKIPOP progCounter+=3
// IO_I2CDFPlayer commands and values
enum : uint8_t{
DF_PLAY = 0x0F,
DF_VOL = 0x06,
DF_FOLDER = 0x2B, // Not a DFPlayer command, used to set folder nr where audio file is
DF_REPEATPLAY = 0x08,
DF_STOPPLAY = 0x16,
DF_EQ = 0x07, // Set equaliser, require parameter NORMAL, POP, ROCK, JAZZ, CLASSIC or BASS
DF_RESET = 0x0C,
DF_DACON = 0x1A,
DF_SETAM = 0x2A, // Set audio mixer 1 or 2 for this DFPLayer
DF_NORMAL = 0x00, // Equalizer parameters
DF_POP = 0x01,
DF_ROCK = 0x02,
DF_JAZZ = 0x03,
DF_CLASSIC = 0x04,
DF_BASS = 0x05,
};
#endif

View File

@@ -31,6 +31,7 @@
#undef ALIAS
#undef AMBER
#undef ANOUT
#undef ASPECT
#undef AT
#undef ATGTE
#undef ATLT
@@ -39,8 +40,12 @@
#undef AUTOSTART
#undef BROADCAST
#undef CALL
#undef CLEAR_STASH
#undef CLEAR_ALL_STASH
#undef CLOSE
#undef CONFIGURE_SERVO
#undef DCC_SIGNAL
#undef DCCX_SIGNAL
#undef DCC_TURNTABLE
#undef DEACTIVATE
#undef DEACTIVATEL
@@ -65,6 +70,7 @@
#undef FWD
#undef GREEN
#undef HAL
#undef HAL_IGNORE_DEFAULTS
#undef IF
#undef IFAMBER
#undef IFCLOSED
@@ -81,19 +87,24 @@
#undef IFTTPOSITION
#undef IFRE
#undef INVERT_DIRECTION
#undef JMRI_SENSOR
#undef JOIN
#undef KILLALL
#undef LATCH
#undef LCD
#undef SCREEN
#undef LCC
#undef LCCX
#undef LCN
#undef MOVETT
#undef MESSAGE
#undef ONACTIVATE
#undef ONACTIVATEL
#undef ONAMBER
#undef ONDEACTIVATE
#undef ONDEACTIVATEL
#undef ONCLOSE
#undef ONLCC
#undef ONTIME
#undef ONCLOCKTIME
#undef ONCLOCKMINS
@@ -105,6 +116,7 @@
#undef ONCHANGE
#undef PARSE
#undef PAUSE
#undef PICKUP_STASH
#undef PIN_TURNOUT
#undef PRINT
#ifndef DISABLE_PROG
@@ -123,6 +135,11 @@
#undef ROTATE
#undef ROTATE_DCC
#undef ROUTE
#undef ROUTE_ACTIVE
#undef ROUTE_INACTIVE
#undef ROUTE_HIDDEN
#undef ROUTE_DISABLED
#undef ROUTE_CAPTION
#undef SENDLOCO
#undef SEQUENCE
#undef SERIAL
@@ -138,11 +155,15 @@
#undef SERVO_SIGNAL
#undef SET
#undef SET_TRACK
#undef SET_POWER
#undef SETLOCO
#undef SETFREQ
#undef SIGNAL
#undef SIGNALH
#undef SPEED
#undef START
#undef STASH
#undef STEALTH
#undef STOP
#undef THROW
#undef TT_ADDPOSITION
@@ -169,15 +190,20 @@
#define AMBER(signal_id)
#define ANOUT(vpin,value,param1,param2)
#define AT(sensor_id)
#define ASPECT(address,value)
#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 CALL(route)
#define CLEAR_STASH(id)
#define CLEAR_ALL_STASH(id)
#define CLOSE(id)
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)
#define DCC_SIGNAL(id,add,subaddr)
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect)
#define DCC_TURNTABLE(id,home,description)
#define DEACTIVATE(addr,subaddr)
#define DEACTIVATEL(addr)
@@ -192,7 +218,7 @@
#define ENDTASK
#define ESTOP
#define EXRAIL
#define EXTT_TURNTABLE(id,vpin,i2c_address,home,description)
#define EXTT_TURNTABLE(id,vpin,home,description)
#define FADE(pin,value,ms)
#define FOFF(func)
#define FOLLOW(route)
@@ -202,6 +228,7 @@
#define FWD(speed)
#define GREEN(signal_id)
#define HAL(haltype,params...)
#define HAL_IGNORE_DEFAULTS
#define IF(sensor_id)
#define IFAMBER(signal_id)
#define IFCLOSED(turnout_id)
@@ -218,12 +245,16 @@
#define IFTTPOSITION(turntable_id,position)
#define IFRE(sensor_id,value)
#define INVERT_DIRECTION
#define JMRI_SENSOR(vpin,count...)
#define JOIN
#define KILLALL
#define LATCH(sensor_id)
#define LATCH(sensor_id)
#define LCC(eventid)
#define LCCX(senderid,eventid)
#define LCD(row,msg)
#define SCREEN(display,row,msg)
#define LCN(msg)
#define MESSAGE(msg)
#define MOVETT(id,steps,activity)
#define ONACTIVATE(addr,subaddr)
#define ONACTIVATEL(linear)
@@ -235,6 +266,7 @@
#define ONDEACTIVATE(addr,subaddr)
#define ONDEACTIVATEL(linear)
#define ONCLOSE(turnout_id)
#define ONLCC(sender,event)
#define ONGREEN(signal_id)
#define ONRED(signal_id)
#define ONROTATE(turntable_id)
@@ -244,6 +276,7 @@
#define PIN_TURNOUT(id,pin,description...)
#define PRINT(msg)
#define PARSE(msg)
#define PICKUP_STASH(id)
#ifndef DISABLE_PROG
#define POM(cv,value)
#endif
@@ -260,6 +293,11 @@
#define ROTATE_DCC(turntable_id,position)
#define ROSTER(cab,name,funcmap...)
#define ROUTE(id,description)
#define ROUTE_ACTIVE(id)
#define ROUTE_INACTIVE(id)
#define ROUTE_HIDDEN(id)
#define ROUTE_DISABLED(id)
#define ROUTE_CAPTION(id,caption)
#define SENDLOCO(cab,route)
#define SEQUENCE(id)
#define SERIAL(msg)
@@ -275,11 +313,15 @@
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...)
#define SET(pin)
#define SET_TRACK(track,mode)
#define SET_POWER(track,onoff)
#define SETLOCO(loco)
#define SETFREQ(loco,freq)
#define SIGNAL(redpin,amberpin,greenpin)
#define SIGNALH(redpin,amberpin,greenpin)
#define SPEED(speed)
#define START(route)
#define START(route)
#define STASH(id)
#define STEALTH(code...)
#define STOP
#define THROW(id)
#define TT_ADDPOSITION(turntable_id,position,value,angle,description...)

318
EXRAIL2Parser.cpp Normal file
View File

@@ -0,0 +1,318 @@
/*
* © 2021 Neil McKechnie
* © 2021-2023 Harald Barth
* © 2020-2023 Chris Harlow
* © 2022-2023 Colin Murdoch
* 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 is an extension of the RMFT2 class
// normally found in EXRAIL2.cpp
#include <Arduino.h>
#include "defines.h"
#include "EXRAIL2.h"
#include "DCC.h"
#include "KeywordHasher.h"
// This filter intercepts <> commands to do the following:
// - Implement RMFT specific commands/diagnostics
// - Reject/modify JMRI commands that would interfere with RMFT processing
void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
(void)stream; // avoid compiler warning if we don't access this parameter
bool reject=false;
switch(opcode) {
case 'D':
if (p[0]=="EXRAIL"_hk) { // <D EXRAIL ON/OFF>
diag = paramCount==2 && (p[1]=="ON"_hk || p[1]==1);
opcode=0;
}
break;
case '/': // New EXRAIL command
reject=!parseSlash(stream,paramCount,p);
opcode=0;
break;
case 'A': // <A address aspect>
if (paramCount!=2) break;
// Ask exrail if this is just changing the aspect on a
// predefined DCCX_SIGNAL. Because this will handle all
// the IFRED and ONRED type issues at the same time.
if (signalAspectEvent(p[0],p[1])) opcode=0; // all done
break;
case 'L':
// This entire code block is compiled out if LLC macros not used
if (!(compileFeatures & FEATURE_LCC)) return;
if (paramCount==0) { //<L> LCC adapter introducing self
LCCSerial=stream; // now we know where to send events we raise
// loop through all possible sent events
for (int progCounter=0;; SKIPOP) {
byte opcode=GET_OPCODE;
if (opcode==OPCODE_ENDEXRAIL) break;
if (opcode==OPCODE_LCC) StringFormatter::send(stream,F("<LS x%h>\n"),getOperand(progCounter,0));
if (opcode==OPCODE_LCCX) { // long form LCC
StringFormatter::send(stream,F("<LS x%h%h%h%h>\n"),
getOperand(progCounter,1),
getOperand(progCounter,2),
getOperand(progCounter,3),
getOperand(progCounter,0)
);
}}
// we stream the hex events we wish to listen to
// and at the same time build the event index looku.
int eventIndex=0;
for (int progCounter=0;; SKIPOP) {
byte opcode=GET_OPCODE;
if (opcode==OPCODE_ENDEXRAIL) break;
if (opcode==OPCODE_ONLCC) {
onLCCLookup[eventIndex]=progCounter; // TODO skip...
StringFormatter::send(stream,F("<LL %d x%h%h%h:%h>\n"),
eventIndex,
getOperand(progCounter,1),
getOperand(progCounter,2),
getOperand(progCounter,3),
getOperand(progCounter,0)
);
eventIndex++;
}
}
StringFormatter::send(stream,F("<LR>\n")); // Ready to rumble
opcode=0;
break;
}
if (paramCount==1) { // <L eventid> LCC event arrived from adapter
int16_t eventid=p[0];
reject=eventid<0 || eventid>=countLCCLookup;
if (!reject) startNonRecursiveTask(F("LCC"),eventid,onLCCLookup[eventid]);
opcode=0;
}
break;
case 'J': // throttle info commands
if (paramCount<1) return;
switch(p[0]) {
case "A"_hk: // <JA> returns automations/routes
if (paramCount==1) {// <JA>
StringFormatter::send(stream, F("<jA"));
routeLookup->stream(stream);
StringFormatter::send(stream, F(">\n"));
opcode=0;
return;
}
if (paramCount==2) { // <JA id>
int16_t id=p[1];
StringFormatter::send(stream,F("<jA %d %c \"%S\">\n"),
id, getRouteType(id), getRouteDescription(id));
if (compileFeatures & FEATURE_ROUTESTATE) {
// Send any non-default button states or captions
int16_t statePos=routeLookup->findPosition(id);
if (statePos>=0) {
if (routeStateArray[statePos])
StringFormatter::send(stream,F("<jB %d %d>\n"), id, routeStateArray[statePos]);
if (routeCaptionArray[statePos])
StringFormatter::send(stream,F("<jB %d \"%S\">\n"), id,routeCaptionArray[statePos]);
}
}
opcode=0;
return;
}
break;
case "M"_hk:
// NOTE: we only need to handle valid calls here because
// DCCEXParser has to have code to handle the <J<> cases where
// exrail isnt involved anyway.
// This entire code block is compiled out if STASH macros not used
if (!(compileFeatures & FEATURE_STASH)) return;
if (paramCount==1) { // <JM>
StringFormatter::send(stream,F("<jM %d>\n"),maxStashId);
opcode=0;
break;
}
if (paramCount==2) { // <JM id>
if (p[1]<=0 || p[1]>maxStashId) break;
StringFormatter::send(stream,F("<jM %d %d>\n"),
p[1],stashArray[p[1]]);
opcode=0;
break;
}
if (paramCount==3) { // <JM id cab>
if (p[1]<=0 || p[1]>maxStashId) break;
stashArray[p[1]]=p[2];
opcode=0;
break;
}
break;
default:
break;
}
default: // other commands pass through
break;
}
}
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
if (paramCount==0) { // STATUS
StringFormatter::send(stream, F("<* EXRAIL STATUS"));
RMFT2 * task=loopTask;
while(task) {
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d%c,SPEED=%d%c"),
(int)(task->taskId),task->progCounter,task->loco,
task->invert?'I':' ',
task->speedo,
task->forward?'F':'R'
);
task=task->next;
if (task==loopTask) break;
}
// Now stream the flags
for (int id=0;id<MAX_FLAGS; id++) {
byte flag=flags[id];
if (flag & ~TASK_FLAG & ~SIGNAL_MASK) { // not interested in TASK_FLAG only. Already shown above
StringFormatter::send(stream,F("\nflags[%d] "),id);
if (flag & SECTION_FLAG) StringFormatter::send(stream,F(" RESERVED"));
if (flag & LATCH_FLAG) StringFormatter::send(stream,F(" LATCHED"));
}
}
if (compileFeatures & FEATURE_SIGNAL) {
// do the signals
// flags[n] represents the state of the nth signal in the table
for (int sigslot=0;;sigslot++) {
VPIN sigid=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigslot*8);
if (sigid==0) break; // end of signal list
byte flag=flags[sigslot] & SIGNAL_MASK; // obtain signal flags for this id
StringFormatter::send(stream,F("\n%S[%d]"),
(flag == SIGNAL_RED)? F("RED") : (flag==SIGNAL_GREEN) ? F("GREEN") : F("AMBER"),
sigid & SIGNAL_ID_MASK);
}
}
if (compileFeatures & FEATURE_STASH) {
for (int i=1;i<=maxStashId;i++) {
if (stashArray[i])
StringFormatter::send(stream,F("\nSTASH[%d] Loco=%d"),
i, stashArray[i]);
}
}
StringFormatter::send(stream,F(" *>\n"));
return true;
}
switch (p[0]) {
case "PAUSE"_hk: // </ PAUSE>
if (paramCount!=1) return false;
DCC::setThrottle(0,1,true); // pause all locos on the track
pausingTask=(RMFT2 *)1; // Impossible task address
return true;
case "RESUME"_hk: // </ RESUME>
if (paramCount!=1) return false;
pausingTask=NULL;
{
RMFT2 * task=loopTask;
while(task) {
if (task->loco) task->driveLoco(task->speedo);
task=task->next;
if (task==loopTask) break;
}
}
return true;
case "START"_hk: // </ START [cab] route >
if (paramCount<2 || paramCount>3) return false;
{
int route=(paramCount==2) ? p[1] : p[2];
uint16_t cab=(paramCount==2)? 0 : p[1];
int pc=routeLookup->find(route);
if (pc<0) return false;
RMFT2* task=new RMFT2(pc);
task->loco=cab;
}
return true;
default:
break;
}
// check KILL ALL here, otherwise the next validation confuses ALL with a flag
if (p[0]=="KILL"_hk && p[1]=="ALL"_hk) {
while (loopTask) loopTask->kill(F("KILL ALL")); // destructor changes loopTask
return true;
}
// all other / commands take 1 parameter
if (paramCount!=2 ) return false;
switch (p[0]) {
case "KILL"_hk: // Kill taskid|ALL
{
if ( p[1]<0 || p[1]>=MAX_FLAGS) return false;
RMFT2 * task=loopTask;
while(task) {
if (task->taskId==p[1]) {
task->kill(F("KILL"));
return true;
}
task=task->next;
if (task==loopTask) break;
}
}
return false;
case "RESERVE"_hk: // force reserve a section
return setFlag(p[1],SECTION_FLAG);
case "FREE"_hk: // force free a section
return setFlag(p[1],0,SECTION_FLAG);
case "LATCH"_hk:
return setFlag(p[1], LATCH_FLAG);
case "UNLATCH"_hk:
return setFlag(p[1], 0, LATCH_FLAG);
case "RED"_hk:
doSignal(p[1],SIGNAL_RED);
return true;
case "AMBER"_hk:
doSignal(p[1],SIGNAL_AMBER);
return true;
case "GREEN"_hk:
doSignal(p[1],SIGNAL_GREEN);
return true;
default:
return false;
}
}

View File

@@ -59,32 +59,159 @@
// helper macro for turnout description as HIDDEN
#define HIDDEN "\x01"
// PLAYSOUND is alias of ANOUT to make the user experience of a Conductor beter for
// playing sounds with IO_I2CDFPlayer
#define PLAYSOUND ANOUT
// helper macro to strip leading zeros off time inputs
// (10#mins)%100)
#define STRIP_ZERO(value) 10##value%100
// These constants help EXRAIL macros convert Track Power e.g. SET_POWER(A ON|OFF).
//const byte TRACK_POWER_0=0, TRACK_POWER_OFF=0;
//const byte TRACK_POWER_1=1, TRACK_POWER_ON=1;
// 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 1d Detect sequence duplicates.
// This pass generates no runtime data or code
#include "EXRAIL2MacroReset.h"
#undef AUTOMATION
#define AUTOMATION(id, description) id,
#undef ROUTE
#define ROUTE(id, description) id,
#undef SEQUENCE
#define SEQUENCE(id) id,
constexpr int16_t compileTimeSequenceList[]={
#include "myAutomation.h"
0
};
constexpr int16_t stuffSize=sizeof(compileTimeSequenceList)/sizeof(int16_t) - 1;
// Compile time function to check for sequence nos.
constexpr bool hasseq(const int16_t value, const int16_t pos=0 ) {
return pos>=stuffSize? false :
compileTimeSequenceList[pos]==value
|| hasseq(value,pos+1);
}
// Compile time function to check for duplicate sequence nos.
constexpr bool hasdup(const int16_t value, const int16_t pos ) {
return pos>=stuffSize? false :
compileTimeSequenceList[pos]==value
|| hasseq(value,pos+1)
|| hasdup(compileTimeSequenceList[pos],pos+1);
}
static_assert(!hasdup(compileTimeSequenceList[0],1),"Duplicate SEQUENCE/ROUTE/AUTOMATION detected");
//pass 1s static asserts to
// - check call and follows etc for existing sequence numbers
// - check range on LATCH/UNLATCH
// This pass generates no runtime data or code
#include "EXRAIL2MacroReset.h"
#undef ASPECT
#define ASPECT(address,value) static_assert(address <=2044, "invalid Address"); \
static_assert(address>=-3, "Invalid value");
#undef CALL
#define CALL(id) static_assert(hasseq(id),"Sequence not found");
#undef FOLLOW
#define FOLLOW(id) static_assert(hasseq(id),"Sequence not found");
#undef START
#define START(id) static_assert(hasseq(id),"Sequence not found");
#undef SENDLOCO
#define SENDLOCO(cab,id) static_assert(hasseq(id),"Sequence not found");
#undef LATCH
#define LATCH(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef UNLATCH
#define UNLATCH(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef RESERVE
#define RESERVE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef FREE
#define FREE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
#undef SPEED
#define SPEED(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
#undef FWD
#define FWD(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
#undef REV
#define REV(speed) static_assert(speed>=0 && speed<128,"Speed out of valid range 0-127");
#include "myAutomation.h"
// Pass 1h Implements HAL macro by creating exrailHalSetup function
// Also allows creating EXTurntable object
#include "EXRAIL2MacroReset.h"
#undef HAL
#define HAL(haltype,params...) haltype::create(params);
#undef EXTT_TURNTABLE
#define EXTT_TURNTABLE(id,vpin,i2c_address,home,description...) EXTurntable::create(vpin,1,i2c_address);
void exrailHalSetup() {
#undef HAL_IGNORE_DEFAULTS
#define HAL_IGNORE_DEFAULTS ignore_defaults=true;
#undef JMRI_SENSOR
#define JMRI_SENSOR(vpin,count...) Sensor::createMultiple(vpin,##count);
#undef CONFIGURE_SERVO
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile) IODevice::configureServo(vpin,pos1,pos2,PCA9685::profile);
bool exrailHalSetup() {
bool ignore_defaults=false;
#include "myAutomation.h"
return ignore_defaults;
}
// Pass 1c detect compile time featurtes
#include "EXRAIL2MacroReset.h"
#undef SIGNAL
#define SIGNAL(redpin,amberpin,greenpin) | FEATURE_SIGNAL
#undef SIGNALH
#define SIGNALH(redpin,amberpin,greenpin) | FEATURE_SIGNAL
#undef SERVO_SIGNAL
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) | FEATURE_SIGNAL
#undef DCC_SIGNAL
#define DCC_SIGNAL(id,addr,subaddr) | FEATURE_SIGNAL
#undef DCCX_SIGNAL
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) | FEATURE_SIGNAL
#undef VIRTUAL_SIGNAL
#define VIRTUAL_SIGNAL(id) | FEATURE_SIGNAL
#undef LCC
#define LCC(eventid) | FEATURE_LCC
#undef LCCX
#define LCCX(senderid,eventid) | FEATURE_LCC
#undef ONLCC
#define ONLCC(senderid,eventid) | FEATURE_LCC
#undef ROUTE_ACTIVE
#define ROUTE_ACTIVE(id) | FEATURE_ROUTESTATE
#undef ROUTE_INACTIVE
#define ROUTE_INACTIVE(id) | FEATURE_ROUTESTATE
#undef ROUTE_HIDDEN
#define ROUTE_HIDDEN(id) | FEATURE_ROUTESTATE
#undef ROUTE_DISABLED
#define ROUTE_DISABLED(id) | FEATURE_ROUTESTATE
#undef ROUTE_CAPTION
#define ROUTE_CAPTION(id,caption) | FEATURE_ROUTESTATE
#undef CLEAR_STASH
#define CLEAR_STASH(id) | FEATURE_STASH
#undef CLEAR_ALL_STASH
#define CLEAR_ALL_STASH | FEATURE_STASH
#undef PICKUP_STASH
#define PICKUP_STASH(id) | FEATURE_STASH
#undef STASH
#define STASH(id) | FEATURE_STASH
const byte RMFT2::compileFeatures = 0
#include "myAutomation.h"
;
// Pass 2 create throttle route list
#include "EXRAIL2MacroReset.h"
#undef ROUTE
#define ROUTE(id, description) id,
const int16_t HIGHFLASH RMFT2::routeIdList[]= {
const int16_t HIGHFLASH RMFT2::routeIdList[]= {
#include "myAutomation.h"
INT16_MAX};
// Pass 2a create throttle automation list
@@ -126,6 +253,15 @@ const int StringMacroTracker1=__COUNTER__;
#define PRINT(msg) THRUNGE(msg,thrunge_print)
#undef LCN
#define LCN(msg) THRUNGE(msg,thrunge_lcn)
#undef MESSAGE
#define MESSAGE(msg) THRUNGE(msg,thrunge_message)
#undef ROUTE_CAPTION
#define ROUTE_CAPTION(id,caption) \
case (__COUNTER__ - StringMacroTracker1) : {\
manageRouteCaption(id,F(caption));\
return;\
}
#undef SERIAL
#define SERIAL(msg) THRUNGE(msg,thrunge_serial)
#undef SERIAL1
@@ -158,6 +294,8 @@ const int StringMacroTracker1=__COUNTER__;
lcdid=id;\
break;\
}
#undef STEALTH
#define STEALTH(code...) case (__COUNTER__ - StringMacroTracker1) : {code} return;
#undef WITHROTTLE
#define WITHROTTLE(msg) THRUNGE(msg,thrunge_withrottle)
@@ -177,6 +315,8 @@ void RMFT2::printMessage(uint16_t id) {
#include "EXRAIL2MacroReset.h"
#undef TURNOUT
#define TURNOUT(id,addr,subaddr,description...) O_DESC(id,description)
#undef TURNOUTL
#define TURNOUTL(id,addr,description...) O_DESC(id,description)
#undef PIN_TURNOUT
#define PIN_TURNOUT(id,pin,description...) O_DESC(id,description)
#undef SERVO_TURNOUT
@@ -197,7 +337,7 @@ const FSH * RMFT2::getTurnoutDescription(int16_t turnoutid) {
#undef DCC_TURNTABLE
#define DCC_TURNTABLE(id,home,description...) O_DESC(id,description)
#undef EXTT_TURNTABLE
#define EXTT_TURNTABLE(id,vpin,i2c_address,home,description...) O_DESC(id,description)
#define EXTT_TURNTABLE(id,vpin,home,description...) O_DESC(id,description)
const FSH * RMFT2::getTurntableDescription(int16_t turntableId) {
switch (turntableId) {
@@ -213,6 +353,8 @@ const FSH * RMFT2::getTurntableDescription(int16_t turntableId) {
#define TT_ADDPOSITION(turntable_id,position,value,home,description...) T_DESC(turntable_id,position,description)
const FSH * RMFT2::getTurntablePositionDescription(int16_t turntableId, uint8_t positionId) {
(void)turntableId;
(void)positionId;
#include "myAutomation.h"
return NULL;
}
@@ -266,6 +408,8 @@ const FSH * RMFT2::getRosterFunctions(int16_t id) {
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) vpin | RMFT2::SERVO_SIGNAL_FLAG,redval,amberval,greenval,
#undef DCC_SIGNAL
#define DCC_SIGNAL(id,addr,subaddr) id | RMFT2::DCC_SIGNAL_FLAG,addr,subaddr,0,
#undef DCCX_SIGNAL
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) id | RMFT2::DCCX_SIGNAL_FLAG,redAspect,amberAspect,greenAspect,
#undef VIRTUAL_SIGNAL
#define VIRTUAL_SIGNAL(id) id,0,0,0,
@@ -273,6 +417,16 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#include "myAutomation.h"
0,0,0,0 };
// Pass 9 ONLCC counter and lookup array
#include "EXRAIL2MacroReset.h"
#undef ONLCC
#define ONLCC(sender,event) +1
const int RMFT2::countLCCLookup=0
#include "myAutomation.h"
;
int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
// Last Pass : create main routes table
// Only undef the macros, not dummy them.
#define RMFT2_UNDEF_ONLY
@@ -290,6 +444,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#define ALIAS(name,value...)
#define AMBER(signal_id) OPCODE_AMBER,V(signal_id),
#define ANOUT(vpin,value,param1,param2) OPCODE_SERVO,V(vpin),OPCODE_PAD,V(value),OPCODE_PAD,V(param1),OPCODE_PAD,V(param2),
#define ASPECT(address,value) OPCODE_ASPECT,V((address<<5) | (value & 0x1F)),
#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),
@@ -298,7 +453,10 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#define AUTOSTART OPCODE_AUTOSTART,0,0,
#define BROADCAST(msg) PRINT(msg)
#define CALL(route) OPCODE_CALL,V(route),
#define CLEAR_STASH(id) OPCODE_CLEAR_STASH,V(id),
#define CLEAR_ALL_STASH OPCODE_CLEAR_ALL_STASH,V(0),
#define CLOSE(id) OPCODE_CLOSE,V(id),
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)
#ifndef IO_NO_HAL
#define DCC_TURNTABLE(id,home,description...) OPCODE_DCCTURNTABLE,V(id),OPCODE_PAD,V(home),
#endif
@@ -308,6 +466,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#define DELAYMINS(mindelay) OPCODE_DELAYMINS,V(mindelay),
#define DELAYRANDOM(mindelay,maxdelay) DELAY(mindelay) OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L),
#define DCC_SIGNAL(id,add,subaddr)
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect)
#define DONE OPCODE_ENDTASK,0,0,
#define DRIVE(analogpin) OPCODE_DRIVE,V(analogpin),
#define ELSE OPCODE_ELSE,0,0,
@@ -317,7 +476,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#define ESTOP OPCODE_SPEED,V(1),
#define EXRAIL
#ifndef IO_NO_HAL
#define EXTT_TURNTABLE(id,vpin,i2c_address,home,description...) OPCODE_EXTTTURNTABLE,V(id),OPCODE_PAD,V(vpin),OPCODE_PAD,V(i2c_address),OPCODE_PAD,V(home),
#define EXTT_TURNTABLE(id,vpin,home,description...) OPCODE_EXTTTURNTABLE,V(id),OPCODE_PAD,V(vpin),OPCODE_PAD,V(home),
#endif
#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),
@@ -328,6 +487,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#define FWD(speed) OPCODE_FWD,V(speed),
#define GREEN(signal_id) OPCODE_GREEN,V(signal_id),
#define HAL(haltype,params...)
#define HAL_IGNORE_DEFAULTS
#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),
@@ -346,17 +506,29 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#endif
#define IFRE(sensor_id,value) OPCODE_IFRE,V(sensor_id),OPCODE_PAD,V(value),
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0,
#define JMRI_SENSOR(vpin,count...)
#define JOIN OPCODE_JOIN,0,0,
#define KILLALL OPCODE_KILLALL,0,0,
#define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id),
#define LCC(eventid) OPCODE_LCC,V(eventid),
#define LCCX(sender,event) OPCODE_LCCX,V(event),\
OPCODE_PAD,V((((uint64_t)sender)>>32)&0xFFFF),\
OPCODE_PAD,V((((uint64_t)sender)>>16)&0xFFFF),\
OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF),
#define LCD(id,msg) PRINT(msg)
#define SCREEN(display,id,msg) PRINT(msg)
#define STEALTH(code...) PRINT(dummy)
#define LCN(msg) PRINT(msg)
#define MESSAGE(msg) PRINT(msg)
#define MOVETT(id,steps,activity) OPCODE_SERVO,V(id),OPCODE_PAD,V(steps),OPCODE_PAD,V(EXTurntable::activity),OPCODE_PAD,V(0),
#define ONACTIVATE(addr,subaddr) OPCODE_ONACTIVATE,V(addr<<2|subaddr),
#define ONACTIVATEL(linear) OPCODE_ONACTIVATE,V(linear+3),
#define ONAMBER(signal_id) OPCODE_ONAMBER,V(signal_id),
#define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id),
#define ONLCC(sender,event) OPCODE_ONLCC,V(event),\
OPCODE_PAD,V((((uint64_t)sender)>>32)&0xFFFF),\
OPCODE_PAD,V((((uint64_t)sender)>>16)&0xFFFF),\
OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF),
#define ONTIME(value) OPCODE_ONTIME,V(value),
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
@@ -371,6 +543,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
#define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id),
#define PAUSE OPCODE_PAUSE,0,0,
#define PICKUP_STASH(id) OPCODE_PICKUP_STASH,V(id),
#define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
#ifndef DISABLE_PROG
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
@@ -392,6 +565,11 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#define ROTATE_DCC(id,position) OPCODE_ROTATE,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(0),
#endif
#define ROUTE(id, description) OPCODE_ROUTE, V(id),
#define ROUTE_ACTIVE(id) OPCODE_ROUTE_ACTIVE,V(id),
#define ROUTE_INACTIVE(id) OPCODE_ROUTE_INACTIVE,V(id),
#define ROUTE_HIDDEN(id) OPCODE_ROUTE_HIDDEN,V(id),
#define ROUTE_DISABLED(id) OPCODE_ROUTE_DISABLED,V(id),
#define ROUTE_CAPTION(id,caption) PRINT(caption)
#define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route),
#define SEQUENCE(id) OPCODE_SEQUENCE, V(id),
#define SERIAL(msg) PRINT(msg)
@@ -407,11 +585,14 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#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 SET_POWER(track,onoff) OPCODE_SET_POWER,V(TRACK_POWER_##onoff),OPCODE_PAD, V(TRACK_NUMBER_##track),
#define SETLOCO(loco) OPCODE_SETLOCO,V(loco),
#define SETFREQ(loco,freq) OPCODE_SETLOCO,V(loco), OPCODE_SETFREQ,V(freq),
#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 START(route) OPCODE_START,V(route),
#define STASH(id) OPCODE_STASH,V(id),
#define STOP OPCODE_SPEED,V(0),
#define THROW(id) OPCODE_THROW,V(id),
#ifndef IO_NO_HAL
@@ -433,7 +614,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
// Build RouteCode
const int StringMacroTracker2=__COUNTER__;
const HIGHFLASH byte RMFT2::RouteCode[] = {
const HIGHFLASH3 byte RMFT2::RouteCode[] = {
#include "myAutomation.h"
OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 };

View File

@@ -47,6 +47,10 @@ void EthernetInterface::setup()
};
#ifdef IP_ADDRESS
static IPAddress myIP(IP_ADDRESS);
#endif
/**
* @brief Aquire IP Address from DHCP and start server
*
@@ -59,15 +63,15 @@ EthernetInterface::EthernetInterface()
DCCTimer::getSimulatedMacAddress(mac);
connected=false;
#ifdef IP_ADDRESS
Ethernet.begin(mac, IP_ADDRESS);
#else
#ifdef IP_ADDRESS
Ethernet.begin(mac, myIP);
#else
if (Ethernet.begin(mac) == 0)
{
DIAG(F("Ethernet.begin FAILED"));
return;
}
#endif
#endif
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
DIAG(F("Ethernet shield not found or W5100"));
}
@@ -136,7 +140,7 @@ bool EthernetInterface::checkLink() {
DIAG(F("Ethernet cable connected"));
connected=true;
#ifdef IP_ADDRESS
Ethernet.setLocalIP(IP_ADDRESS); // for static IP, set it again
Ethernet.setLocalIP(myIP); // for static IP, set it again
#endif
IPAddress ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static)
server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT

3
FSH.h
View File

@@ -56,6 +56,7 @@ typedef __FlashStringHelper FSH;
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
// AVR_MEGA memory deliberately placed at end of link may need _far functions
#define HIGHFLASH __attribute__((section(".fini2")))
#define HIGHFLASH3 __attribute__((section(".fini3")))
#define GETFARPTR(data) pgm_get_far_address(data)
#define GETHIGHFLASH(data,offset) pgm_read_byte_far(GETFARPTR(data)+offset)
#define GETHIGHFLASHW(data,offset) pgm_read_word_far(GETFARPTR(data)+offset)
@@ -63,6 +64,7 @@ typedef __FlashStringHelper FSH;
// AVR_UNO/NANO runtime does not support _far functions so just use _near equivalent
// as there is no progmem above 32kb anyway.
#define HIGHFLASH PROGMEM
#define HIGHFLASH3 PROGMEM
#define GETFARPTR(data) ((uint32_t)(data))
#define GETHIGHFLASH(data,offset) pgm_read_byte_near(GETFARPTR(data)+(offset))
#define GETHIGHFLASHW(data,offset) pgm_read_word_near(GETFARPTR(data)+(offset))
@@ -80,6 +82,7 @@ typedef __FlashStringHelper FSH;
typedef char FSH;
#define FLASH
#define HIGHFLASH
#define HIGHFLASH3
#define GETFARPTR(data) ((uint32_t)(data))
#define GETFLASH(addr) (*(const byte *)(addr))
#define GETHIGHFLASH(data,offset) (*(const byte *)(GETFARPTR(data)+offset))

View File

@@ -1 +1 @@
#define GITHUB_SHA "devel-202309241855Z"
#define GITHUB_SHA "devel-202403182018Z"

View File

@@ -54,6 +54,8 @@ static const FSH * guessI2CDeviceType(uint8_t address) {
return F("Time-of-flight sensor");
else if (address >= 0x3c && address <= 0x3d)
return F("OLED Display");
else if (address >= 0x48 && address <= 0x57) // SC16IS752x UART detection
return F("SC16IS75x UART");
else if (address >= 0x48 && address <= 0x4f)
return F("Analogue Inputs or PWM");
else if (address >= 0x40 && address <= 0x4f)
@@ -363,4 +365,4 @@ void I2CAddress::toHex(const uint8_t value, char *buffer) {
/* static */ bool I2CAddress::_addressWarningDone = false;
#endif
#endif

View File

@@ -110,42 +110,52 @@ void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
// Calculate a rise time appropriate to the requested bus speed
// Use 10x the rise time spec to enable integer divide of 50ns clock period
uint16_t t_rise;
uint32_t ccr_freq;
while (s->CR1 & I2C_CR1_STOP); // Prevents lockup by guarding further
// writes to CR1 while STOP is being executed!
// Disable the I2C device, as TRISE can only be programmed whilst disabled
s->CR1 &= ~(I2C_CR1_PE); // Disable I2C
s->CR1 |= I2C_CR1_SWRST; // reset the I2C
asm("nop"); // wait a bit... suggestion from online!
s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
if (i2cClockSpeed > 100000L)
if (i2cClockSpeed > 100000UL)
{
if (i2cClockSpeed > 400000L)
i2cClockSpeed = 400000L;
// if (i2cClockSpeed > 400000L)
// i2cClockSpeed = 400000L;
t_rise = 300; // nanoseconds
}
else
{
i2cClockSpeed = 100000L;
// i2cClockSpeed = 100000L;
t_rise = 1000; // nanoseconds
}
// Configure the rise time register
s->TRISE = (t_rise / (1000 / i2c_MHz)) + 1;
// Configure the rise time register - max allowed tRISE is 1000ns,
// so value = 1000ns * I2C_PERIPH_CLK MHz / 1000 + 1.
s->TRISE = (t_rise * i2c_MHz / 1000) + 1;
// Bit 15: I2C Master mode, 0=standard, 1=Fast Mode
// Bit 14: Duty, fast mode duty cycle (use 2:1)
// Bit 11-0: FREQR
if (i2cClockSpeed > 100000L) {
// In fast mode, I2C period is 3 * CCR * TPCLK1.
//APB1clk1 / 3 / i2cClockSpeed = 38, but that results in 306KHz not 400!
ccr_freq = 30; // So 30 gives 396KHz or so!
s->CCR = (uint16_t)(ccr_freq | 0x8000); // We need Fast Mode set
} else {
// In standard mode, I2C period is 2 * CCR * TPCLK1
ccr_freq = (APB1clk1 / 2 / i2cClockSpeed); // Should be 225 for 45Mhz APB1 clock
s->CCR |= (uint16_t)ccr_freq;
}
// if (i2cClockSpeed > 400000UL) {
// // In fast mode plus, I2C period is 3 * CCR * TPCLK1.
// // s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value
// s->CCR = APB1clk1 / 3 / i2cClockSpeed; // Set I2C clockspeed to start!
// s->CCR |= 0xC000; // We need Fast Mode AND DUTY bits set
// } else {
// In standard and fast mode, I2C period is 2 * CCR * TPCLK1
s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value
s->CCR |= (APB1clk1 / 2 / i2cClockSpeed); // Set I2C clockspeed to start!
// s->CCR |= (i2c_MHz * 500 / (i2cClockSpeed / 1000)); // Set I2C clockspeed to start!
// if (i2cClockSpeed > 100000UL)
// s->CCR |= 0xC000; // We need Fast Mode bits set as well
// }
// DIAG(F("I2C_init() peripheral clock is now: %d, full reg is %x"), (s->CR2 & 0xFF), s->CR2);
// DIAG(F("I2C_init() peripheral CCR is now: %d"), s->CCR);
// DIAG(F("I2C_init() peripheral TRISE is now: %d"), s->TRISE);
// Enable the I2C master mode
s->CR1 |= I2C_CR1_PE; // Enable I2C
@@ -159,6 +169,7 @@ void I2CManagerClass::I2C_init()
// Query the clockspeed from the STM32 HAL layer
APB1clk1 = HAL_RCC_GetPCLK1Freq();
i2c_MHz = APB1clk1 / 1000000UL;
// DIAG(F("I2C_init() peripheral clock speed is: %d"), i2c_MHz);
// Enable clocks
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;//(1 << 21); // Enable I2C CLOCK
// Reset the I2C1 peripheral to initial state
@@ -181,6 +192,7 @@ void I2CManagerClass::I2C_init()
GPIOB->AFR[1] |= (4<<0) | (4<<4); // PB8 on low nibble, PB9 on next nibble up
// Software reset the I2C peripheral
I2C1->CR1 &= ~I2C_CR1_PE; // Disable I2C1 peripheral
s->CR1 |= I2C_CR1_SWRST; // reset the I2C
asm("nop"); // wait a bit... suggestion from online!
s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
@@ -191,6 +203,7 @@ void I2CManagerClass::I2C_init()
// Set I2C peripheral clock frequency
// s->CR2 |= I2C_PERIPH_CLK;
s->CR2 |= i2c_MHz;
// DIAG(F("I2C_init() peripheral clock is now: %d"), s->CR2);
// set own address to 00 - not used in master mode
I2C1->OAR1 = (1 << 14); // bit 14 should be kept at 1 according to the datasheet
@@ -214,6 +227,7 @@ void I2CManagerClass::I2C_init()
s->CR2 |= (I2C_CR2_ITBUFEN | I2C_CR2_ITEVTEN | I2C_CR2_ITERREN); // Enable Buffer, Event and Error interrupts
#endif
// DIAG(F("I2C_init() setting initial I2C clock to 100KHz"));
// Calculate baudrate and set default rate for now
// Configure the Clock Control Register for 100KHz SCL frequency
// Bit 15: I2C Master mode, 0=standard, 1=Fast Mode
@@ -221,12 +235,14 @@ void I2CManagerClass::I2C_init()
// Bit 11-0: so CCR divisor would be clk / 2 / 100000 (where clk is in Hz)
// s->CCR = I2C_PERIPH_CLK * 5;
s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value
s->CCR |= (APB1clk1 / 2 / 100000UL); // i2c_MHz * 5;
// s->CCR = i2c_MHz * 5;
s->CCR |= (APB1clk1 / 2 / 100000UL); // Set a default of 100KHz I2C clockspeed to start!
// Configure the rise time register - max allowed is 1000ns, so value = 1000ns * I2C_PERIPH_CLK MHz / 1000 + 1.
// s->TRISE = I2C_PERIPH_CLK + 1; // 1000 ns / 50 ns = 20 + 1 = 21
s->TRISE = i2c_MHz + 1;
s->TRISE = (1000 * i2c_MHz / 1000) + 1;
// DIAG(F("I2C_init() peripheral clock is now: %d, full reg is %x"), (s->CR2 & 0xFF), s->CR2);
// DIAG(F("I2C_init() peripheral CCR is now: %d"), s->CCR);
// DIAG(F("I2C_init() peripheral TRISE is now: %d"), s->TRISE);
// Enable the I2C master mode
s->CR1 |= I2C_CR1_PE; // Enable I2C

View File

@@ -33,7 +33,7 @@
// Link to halSetup function. If not defined, the function reference will be NULL.
extern __attribute__((weak)) void halSetup();
extern __attribute__((weak)) void exrailHalSetup();
extern __attribute__((weak)) bool exrailHalSetup();
//==================================================================================================================
// Static methods
@@ -60,34 +60,31 @@ void IODevice::begin() {
halSetup();
// include any HAL devices defined in exrail.
bool ignoreDefaults=false;
if (exrailHalSetup)
exrailHalSetup();
ignoreDefaults=exrailHalSetup();
if (ignoreDefaults) return;
// Predefine two PCA9685 modules 0x40-0x41 if no conflicts
// Allocates 32 pins 100-131
if (checkNoOverlap(100, 16, 0x40)) {
const bool silent=true; // no message if these conflict
if (checkNoOverlap(100, 16, 0x40, silent)) {
PCA9685::create(100, 16, 0x40);
} else {
DIAG(F("Default PCA9685 at I2C 0x40 disabled due to configured user device"));
}
if (checkNoOverlap(116, 16, 0x41)) {
}
if (checkNoOverlap(116, 16, 0x41, silent)) {
PCA9685::create(116, 16, 0x41);
} else {
DIAG(F("Default PCA9685 at I2C 0x41 disabled due to configured user device"));
}
}
// Predefine two MCP23017 module 0x20/0x21 if no conflicts
// Allocates 32 pins 164-195
if (checkNoOverlap(164, 16, 0x20)) {
if (checkNoOverlap(164, 16, 0x20, silent)) {
MCP23017::create(164, 16, 0x20);
} else {
DIAG(F("Default MCP23017 at I2C 0x20 disabled due to configured user device"));
}
if (checkNoOverlap(180, 16, 0x21)) {
}
if (checkNoOverlap(180, 16, 0x21, silent)) {
MCP23017::create(180, 16, 0x21);
} else {
DIAG(F("Default MCP23017 at I2C 0x21 disabled due to configured user device"));
}
}
}
// reset() function to reinitialise all devices
@@ -339,7 +336,10 @@ IODevice *IODevice::findDeviceFollowing(VPIN vpin) {
// returns true if pins DONT overlap with existing device
// TODO: Move the I2C address reservation and checks into the I2CManager code.
// That will enable non-HAL devices to reserve I2C addresses too.
bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddress) {
// Silent is used by the default setup so that there is no message if the default
// device has already been handled by the user setup.
bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins,
I2CAddress i2cAddress, bool silent) {
#ifdef DIAG_IO
DIAG(F("Check no overlap %u %u %s"), firstPin,nPins,i2cAddress.toString());
#endif
@@ -352,14 +352,14 @@ bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddres
VPIN lastDevPin=firstDevPin+dev->_nPins-1;
bool noOverlap= firstPin>lastDevPin || lastPin<firstDevPin;
if (!noOverlap) {
DIAG(F("WARNING HAL Overlap, redefinition of Vpins %u to %u ignored."),
if (!silent) DIAG(F("WARNING HAL Overlap, redefinition of Vpins %u to %u ignored."),
firstPin, lastPin);
return false;
}
}
// Check for overlapping I2C address
if (i2cAddress && dev->_I2CAddress==i2cAddress) {
DIAG(F("WARNING HAL Overlap. i2c Addr %s ignored."),i2cAddress.toString());
if (!silent) DIAG(F("WARNING HAL Overlap. i2c Addr %s ignored."),i2cAddress.toString());
return false;
}
}

View File

@@ -166,7 +166,8 @@ public:
void setGPIOInterruptPin(int16_t pinNumber);
// Method to check if pins will overlap before creating new device.
static bool checkNoOverlap(VPIN firstPin, uint8_t nPins=1, I2CAddress i2cAddress=0);
static bool checkNoOverlap(VPIN firstPin, uint8_t nPins=1,
I2CAddress i2cAddress=0, bool silent=false);
// Method used by IODevice filters to locate slave pins that may be overlayed by their own
// pin range.
@@ -542,8 +543,10 @@ protected:
#include "IO_MCP23017.h"
#include "IO_PCF8574.h"
#include "IO_PCF8575.h"
#include "IO_PCA9555.h"
#include "IO_duinoNodes.h"
#include "IO_EXIOExpander.h"
#include "IO_trainbrains.h"
#endif // iodevice_h

View File

@@ -1,5 +1,6 @@
/*
* © 2022, Peter Cole. All rights reserved.
* © 2024, Harald Barth. All rights reserved.
*
* This file is part of EX-CommandStation
*
@@ -22,13 +23,10 @@
* This device driver will configure the device on startup, along with
* interacting with the device for all input/output duties.
*
* To create EX-IOExpander devices, these are defined in myHal.cpp:
* To create EX-IOExpander devices, these are defined in myAutomation.h:
* (Note the device driver is included by default)
*
* void halSetup() {
* // EXIOExpander::create(vpin, num_vpins, i2c_address);
* EXIOExpander::create(800, 18, 0x65);
* }
* HAL(EXIOExpander,800,18,0x65)
*
* All pins on an EX-IOExpander device are allocated according to the pin map for the specific
* device in use. There is no way for the device driver to sanity check pins are used for the
@@ -98,25 +96,45 @@ private:
_numAnaloguePins = receiveBuffer[2];
// See if we already have suitable buffers assigned
size_t digitalBytesNeeded = (_numDigitalPins + 7) / 8;
if (_digitalPinBytes < digitalBytesNeeded) {
// Not enough space, free any existing buffer and allocate a new one
if (_digitalPinBytes > 0) free(_digitalInputStates);
_digitalInputStates = (byte*) calloc(_digitalPinBytes, 1);
_digitalPinBytes = digitalBytesNeeded;
}
size_t analogueBytesNeeded = _numAnaloguePins * 2;
if (_analoguePinBytes < analogueBytesNeeded) {
// Free any existing buffers and allocate new ones.
if (_analoguePinBytes > 0) {
free(_analogueInputBuffer);
free(_analogueInputStates);
free(_analoguePinMap);
if (_numDigitalPins>0) {
size_t digitalBytesNeeded = (_numDigitalPins + 7) / 8;
if (_digitalPinBytes < digitalBytesNeeded) {
// Not enough space, free any existing buffer and allocate a new one
if (_digitalPinBytes > 0) free(_digitalInputStates);
if ((_digitalInputStates = (byte*) calloc(digitalBytesNeeded, 1)) != NULL) {
_digitalPinBytes = digitalBytesNeeded;
} else {
DIAG(F("EX-IOExpander I2C:%s ERROR alloc %d bytes"), _I2CAddress.toString(), digitalBytesNeeded);
_deviceState = DEVSTATE_FAILED;
_digitalPinBytes = 0;
return;
}
}
}
if (_numAnaloguePins>0) {
size_t analogueBytesNeeded = _numAnaloguePins * 2;
if (_analoguePinBytes < analogueBytesNeeded) {
// Free any existing buffers and allocate new ones.
if (_analoguePinBytes > 0) {
free(_analogueInputBuffer);
free(_analogueInputStates);
free(_analoguePinMap);
}
_analogueInputStates = (uint8_t*) calloc(analogueBytesNeeded, 1);
_analogueInputBuffer = (uint8_t*) calloc(analogueBytesNeeded, 1);
_analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1);
if (_analogueInputStates != NULL &&
_analogueInputBuffer != NULL &&
_analoguePinMap != NULL) {
_analoguePinBytes = analogueBytesNeeded;
} else {
DIAG(F("EX-IOExpander I2C:%s ERROR alloc analog pin bytes"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
_analoguePinBytes = 0;
return;
}
}
_analogueInputStates = (uint8_t*) calloc(analogueBytesNeeded, 1);
_analogueInputBuffer = (uint8_t*) calloc(analogueBytesNeeded, 1);
_analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1);
_analoguePinBytes = analogueBytesNeeded;
}
} else {
DIAG(F("EX-IOExpander I2C:%s ERROR configuring device"), _I2CAddress.toString());
@@ -124,8 +142,8 @@ private:
return;
}
}
// We now need to retrieve the analogue pin map
if (status == I2C_STATUS_OK) {
// We now need to retrieve the analogue pin map if there are analogue pins
if (status == I2C_STATUS_OK && _numAnaloguePins>0) {
commandBuffer[0] = EXIOINITA;
status = I2CManager.read(_I2CAddress, _analoguePinMap, _numAnaloguePins, commandBuffer, 1);
}
@@ -239,7 +257,7 @@ private:
// If we're not doing anything now, check to see if a new input transfer is due.
if (_readState == RDS_IDLE) {
if (currentMicros - _lastDigitalRead > _digitalRefresh) { // Delay for digital read refresh
if (_numDigitalPins>0 && currentMicros - _lastDigitalRead > _digitalRefresh) { // Delay for digital read refresh
// Issue new read request for digital states. As the request is non-blocking, the buffer has to
// be allocated from heap (object state).
_readCommandBuffer[0] = EXIORDD;
@@ -247,7 +265,7 @@ private:
// non-blocking read
_lastDigitalRead = currentMicros;
_readState = RDS_DIGITAL;
} else if (currentMicros - _lastAnalogueRead > _analogueRefresh) { // Delay for analogue read refresh
} else if (_numAnaloguePins>0 && currentMicros - _lastAnalogueRead > _analogueRefresh) { // Delay for analogue read refresh
// Issue new read for analogue input states
_readCommandBuffer[0] = EXIORDAN;
I2CManager.read(_I2CAddress, _analogueInputBuffer,
@@ -362,14 +380,14 @@ private:
uint8_t _minorVer = 0;
uint8_t _patchVer = 0;
uint8_t* _digitalInputStates;
uint8_t* _analogueInputStates;
uint8_t* _analogueInputBuffer; // buffer for I2C input transfers
uint8_t* _digitalInputStates = NULL;
uint8_t* _analogueInputStates = NULL;
uint8_t* _analogueInputBuffer = NULL; // buffer for I2C input transfers
uint8_t _readCommandBuffer[1];
uint8_t _digitalPinBytes = 0; // Size of allocated memory buffer (may be longer than needed)
uint8_t _analoguePinBytes = 0; // Size of allocated memory buffers (may be longer than needed)
uint8_t* _analoguePinMap;
uint8_t _digitalPinBytes = 0; // Size of allocated memory buffer (may be longer than needed)
uint8_t _analoguePinBytes = 0; // Size of allocated memory buffer (may be longer than needed)
uint8_t* _analoguePinMap = NULL;
I2CRB _i2crb;
enum {RDS_IDLE, RDS_DIGITAL, RDS_ANALOGUE}; // Read operation states

View File

@@ -83,6 +83,7 @@ void EXTurntable::_loop(unsigned long currentMicros) {
// Read returns status as obtained in our loop.
// Return false if our status value is invalid.
int EXTurntable::_read(VPIN vpin) {
(void)vpin; // surpress warning
if (_deviceState == DEVSTATE_FAILED) return 0;
if (_stepperStatus > 1) {
return false;
@@ -127,6 +128,8 @@ void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_
vpin, value, activity, duration);
DIAG(F("I2CManager write I2C Address:%d stepsMSB:%d stepsLSB:%d activity:%d"),
_I2CAddress.toString(), stepsMSB, stepsLSB, activity);
#else
(void)duration;
#endif
if (activity < 4) _stepperStatus = 1; // Tell the device driver Turntable-EX is busy
_previousStatus = _stepperStatus;

805
IO_I2CDFPlayer.h Normal file
View File

@@ -0,0 +1,805 @@
/*
* © 2023, 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 is a modified version of the IO_DFPlayer.h file
* *********************************************************************************************
*
* Dec 2023, Added NXP SC16IS752 I2C Dual UART to enable the DFPlayer connection over the I2C bus
* The SC16IS752 has 64 bytes TX & RX FIFO buffer
* First version without interrupts from I2C UART and only RX/TX are used, interrupts may not be
* needed as the RX Fifo holds the reply
*
* Jan 2024, Issue with using both UARTs simultaniously, the secod uart seems to work but the first transmit
* corrupt data. This need more analysis and experimenatation.
* Will push this driver to the dev branch with the uart fixed to 0
* Both SC16IS750 (single uart) and SC16IS752 (dual uart, but only uart 0 is enable)
*
* myHall.cpp configuration syntax:
*
* I2CDFPlayer::create(1st vPin, vPins, I2C address, xtal);
*
* Parameters:
* 1st vPin : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)
* vPins : Total number of virtual pins allocated (2 vPins are supported, one for each UART)
* 1st vPin for UART 0, 2nd for UART 1
* I2C Address : I2C address of the serial controller, in 0x format
* xtal : 0 for 1,8432Mhz, 1 for 14,7456Mhz
*
* The vPin is also a pin that can be read, it indicate if the DFPlayer has finished playing a track
*
*/
#ifndef IO_I2CDFPlayer_h
#define IO_I2CDFPlayer_h
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
// Debug and diagnostic defines, enable too many will result in slowing the driver
//#define DIAG_I2CDFplayer
//#define DIAG_I2CDFplayer_data
//#define DIAG_I2CDFplayer_reg
//#define DIAG_I2CDFplayer_playing
class I2CDFPlayer : public IODevice {
private:
const uint8_t MAXVOLUME=30;
uint8_t RETRYCOUNT = 0x03;
bool _playing = false;
uint8_t _inputIndex = 0;
unsigned long _commandSendTime; // Time (us) that last transmit took place.
unsigned long _timeoutTime;
uint8_t _recvCMD; // Last received command code byte
bool _awaitingResponse = false;
uint8_t _retryCounter = RETRYCOUNT; // Max retries before timing out
uint8_t _requestedVolumeLevel = MAXVOLUME;
uint8_t _currentVolume = MAXVOLUME;
int _requestedSong = -1; // -1=none, 0=stop, >0=file number
bool _repeat = false; // audio file is repeat playing
uint8_t _previousCmd = true;
// SC16IS752 defines
I2CAddress _I2CAddress;
I2CRB _rb;
uint8_t _UART_CH=0x00; // Fix uart ch to 0 for now
// Communication parameters for the DFPlayer are fixed at 8 bit, No parity, 1 stopbit
uint8_t WORD_LEN = 0x03; // Value LCR bit 0,1
uint8_t STOP_BIT = 0x00; // Value LCR bit 2
uint8_t PARITY_ENA = 0x00; // Value LCR bit 3
uint8_t PARITY_TYPE = 0x00; // Value LCR bit 4
uint32_t BAUD_RATE = 9600;
uint8_t PRESCALER = 0x01; // Value MCR bit 7
uint8_t TEMP_REG_VAL = 0x00;
uint8_t FIFO_RX_LEVEL = 0x00;
uint8_t RX_BUFFER = 0x00; // nr of bytes copied into _inbuffer
uint8_t FIFO_TX_LEVEL = 0x00;
bool _playCmd = false;
bool _volCmd = false;
bool _folderCmd = false;
uint8_t _requestedFolder = 0x01; // default to folder 01
uint8_t _currentFolder = 0x01; // default to folder 01
bool _repeatCmd = false;
bool _stopplayCmd = false;
bool _resetCmd = false;
bool _eqCmd = false;
uint8_t _requestedEQValue = DF_NORMAL;
uint8_t _currentEQvalue = DF_NORMAL; // start equalizer value
bool _daconCmd = false;
uint8_t _audioMixer = 0x01; // Default to output amplifier 1
bool _setamCmd = false; // Set the Audio mixer channel
uint8_t _outbuffer [11]; // DFPlayer command is 10 bytes + 1 byte register address & UART channel
uint8_t _inbuffer[10]; // expected DFPlayer return 10 bytes
unsigned long _sc16is752_xtal_freq;
unsigned long SC16IS752_XTAL_FREQ_LOW = 1843200; // To support cheap eBay/AliExpress SC16IS752 boards
unsigned long SC16IS752_XTAL_FREQ_HIGH = 14745600; // Support for higher baud rates, standard for modular EX-IO system
public:
// Constructor
I2CDFPlayer(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint8_t xtal){
_firstVpin = firstVpin;
_nPins = nPins;
_I2CAddress = i2cAddress;
if (xtal == 0){
_sc16is752_xtal_freq = SC16IS752_XTAL_FREQ_LOW;
} else { // should be 1
_sc16is752_xtal_freq = SC16IS752_XTAL_FREQ_HIGH;
}
addDevice(this);
}
public:
static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint8_t xtal) {
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new I2CDFPlayer(firstVpin, nPins, i2cAddress, xtal);
}
void _begin() override {
// check if SC16IS752 exist first, initialize and then resume DFPlayer init via SC16IS752
I2CManager.begin();
I2CManager.setClock(1000000);
if (I2CManager.exists(_I2CAddress)){
DIAG(F("SC16IS752 I2C:%s UART detected"), _I2CAddress.toString());
Init_SC16IS752(); // Initialize UART
if (_deviceState == DEVSTATE_FAILED){
DIAG(F("SC16IS752 I2C:%s UART initialization failed"), _I2CAddress.toString());
}
} else {
DIAG(F("SC16IS752 I2C:%s UART not detected"), _I2CAddress.toString());
}
#if defined(DIAG_IO)
_display();
#endif
// Now init DFPlayer
// Send a query to the device to see if it responds
_deviceState = DEVSTATE_INITIALISING;
sendPacket(0x42,0,0);
_timeoutTime = micros() + 5000000UL; // 5 second timeout
_awaitingResponse = true;
}
void _loop(unsigned long currentMicros) override {
// Read responses from device
uint8_t status = _rb.status;
if (status == I2C_STATUS_PENDING) return; // Busy, so don't do anything
if (status == I2C_STATUS_OK) {
processIncoming(currentMicros);
// Check if a command sent to device has timed out. Allow 0.5 second for response
// added retry counter, sometimes we do not sent keep alive due to other commands sent to DFPlayer
if (_awaitingResponse && (int32_t)(currentMicros - _timeoutTime) > 0) { // timeout triggered
if(_retryCounter == 0){ // retry counter out of luck, must take the device to failed state
DIAG(F("I2CDFPlayer:%s, DFPlayer not responding on UART channel: 0x%x"), _I2CAddress.toString(), _UART_CH);
_deviceState = DEVSTATE_FAILED;
_awaitingResponse = false;
_playing = false;
_retryCounter = RETRYCOUNT;
} else { // timeout and retry protection and recovery of corrupt data frames from DFPlayer
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: %s, DFPlayer timout, retry counter: %d on UART channel: 0x%x"), _I2CAddress.toString(), _retryCounter, _UART_CH);
#endif
_timeoutTime = currentMicros + 5000000UL; // Timeout if no response within 5 seconds// reset timeout
_awaitingResponse = false; // trigger sending a keep alive 0x42 in processOutgoing()
_retryCounter --; // decrement retry counter
resetRX_fifo(); // reset the RX fifo as it has corrupt data
}
}
}
status = _rb.status;
if (status == I2C_STATUS_PENDING) return; // Busy, try next time
if (status == I2C_STATUS_OK) {
// Send any commands that need to go.
processOutgoing(currentMicros);
}
delayUntil(currentMicros + 10000); // Only enter every 10ms
}
// Check for incoming data, and update busy flag and other state accordingly
void processIncoming(unsigned long currentMicros) {
// Expected message is in the form "7E FF 06 3D xx xx xx xx xx EF"
RX_fifo_lvl();
if (FIFO_RX_LEVEL >= 10) {
#ifdef DIAG_I2CDFplayer
DIAG(F("I2CDFPlayer: %s Retrieving data from RX Fifo on UART_CH: 0x%x FIFO_RX_LEVEL: %d"),_I2CAddress.toString(), _UART_CH, FIFO_RX_LEVEL);
#endif
_outbuffer[0] = REG_RHR << 3 | _UART_CH << 1;
// Only copy 10 bytes from RX FIFO, there maybe additional partial return data after a track is finished playing in the RX FIFO
I2CManager.read(_I2CAddress, _inbuffer, 10, _outbuffer, 1); // inbuffer[] has the data now
//delayUntil(currentMicros + 10000); // Allow time to get the data
RX_BUFFER = 10; // We have copied 10 bytes from RX FIFO to _inbuffer
#ifdef DIAG_I2CDFplayer_data
DIAG(F("SC16IS752: At I2C: %s, UART channel: 0x%x, RX FIFO Data"), _I2CAddress.toString(), _UART_CH);
for (int i = 0; i < sizeof _inbuffer; i++){
DIAG(F("SC16IS752: Data _inbuffer[0x%x]: 0x%x"), i, _inbuffer[i]);
}
#endif
} else {
FIFO_RX_LEVEL = 0; //set to 0, we'll read a fresh FIFO_RX_LEVEL next time
return; // No data or not enough data in rx fifo, check again next time around
}
bool ok = false;
//DIAG(F("I2CDFPlayer: RX_BUFFER: %d"), RX_BUFFER);
while (RX_BUFFER != 0) {
int c = _inbuffer[_inputIndex]; // Start at 0, increment to FIFO_RX_LEVEL
switch (_inputIndex) {
case 0:
if (c == 0x7E) ok = true;
break;
case 1:
if (c == 0xFF) ok = true;
break;
case 2:
if (c== 0x06) ok = true;
break;
case 3:
_recvCMD = c; // CMD byte
ok = true;
break;
case 6:
switch (_recvCMD) {
//DIAG(F("I2CDFPlayer: %s, _recvCMD: 0x%x _awaitingResponse: 0x0%x"),_I2CAddress.toString(), _recvCMD, _awaitingResponse);
case 0x42:
// Response to status query
_playing = (c != 0);
// Mark the device online and cancel timeout
if (_deviceState==DEVSTATE_INITIALISING) {
_deviceState = DEVSTATE_NORMAL;
#ifdef DIAG_I2CDFplayer
DIAG(F("I2CDFPlayer: %s, UART_CH: 0x0%x, _deviceState: 0x0%x"),_I2CAddress.toString(), _UART_CH, _deviceState);
#endif
#ifdef DIAG_IO
_display();
#endif
}
_awaitingResponse = false;
break;
case 0x3d:
// End of play
if (_playing) {
#ifdef DIAG_IO
DIAG(F("I2CDFPlayer: Finished"));
#endif
_playing = false;
}
break;
case 0x40:
// Error codes; 1: Module Busy
DIAG(F("I2CDFPlayer: Error %d returned from device"), c);
_playing = false;
break;
}
ok = true;
break;
case 4: case 5: case 7: case 8:
ok = true; // Skip over these bytes in message.
break;
case 9:
if (c==0xef) {
// Message finished
_retryCounter = RETRYCOUNT; // reset the retry counter as we have received a valid packet
}
break;
default:
break;
}
if (ok){
_inputIndex++; // character as expected, so increment index
RX_BUFFER --; // Decrease FIFO_RX_LEVEL with each character read from _inbuffer[_inputIndex]
} else {
_inputIndex = 0; // otherwise reset.
RX_BUFFER = 0;
}
}
RX_BUFFER = 0; //Set to 0, we'll read a new RX FIFO level again
}
// Send any commands that need to be sent
void processOutgoing(unsigned long currentMicros) {
// When two commands are sent in quick succession, the device will often fail to
// execute one. Testing has indicated that a delay of 100ms or more is required
// between successive commands to get reliable operation.
// If 100ms has elapsed since the last thing sent, then check if there's some output to do.
if (((int32_t)currentMicros - _commandSendTime) > 100000) {
if ( _resetCmd == true){
sendPacket(0x0C,0,0);
_resetCmd = false;
} else if(_volCmd == true) { // do the volme before palying a track
if(_requestedVolumeLevel >= 0 && _requestedVolumeLevel <= 30){
_currentVolume = _requestedVolumeLevel; // If _requestedVolumeLevel is out of range, sent _currentV1olume
}
sendPacket(0x06, 0x00, _currentVolume);
_volCmd = false;
} else if (_playCmd == true) {
// Change song
if (_requestedSong != -1) {
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: _requestedVolumeLevel: %u, _requestedSong: %u, _currentFolder: %u _playCmd: 0x%x"), _requestedVolumeLevel, _requestedSong, _currentFolder, _playCmd);
#endif
sendPacket(0x0F, _currentFolder, _requestedSong); // audio file in folder
_requestedSong = -1;
_playCmd = false;
}
} //else if (_requestedSong == 0) {
else if (_stopplayCmd == true) {
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: Stop playing: _stopplayCmd: 0x%x"), _stopplayCmd);
#endif
sendPacket(0x16, 0x00, 0x00); // Stop playing
_requestedSong = -1;
_repeat = false; // reset repeat
_stopplayCmd = false;
} else if (_folderCmd == true) {
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: Folder: _folderCmd: 0x%x, _requestedFolder: %d"), _stopplayCmd, _requestedFolder);
#endif
if (_currentFolder != _requestedFolder){
_currentFolder = _requestedFolder;
}
_folderCmd = false;
} else if (_repeatCmd == true) {
if(_repeat == false) { // No repeat play currently
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: Repeat: _repeatCmd: 0x%x, _requestedSong: %d, _repeat: 0x0%x"), _repeatCmd, _requestedSong, _repeat);
#endif
sendPacket(0x08, 0x00, _requestedSong); // repeat playing audio file in root folder
_requestedSong = -1;
_repeat = true;
}
_repeatCmd= false;
} else if (_daconCmd == true) { // Always turn DAC on
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: DACON: _daconCmd: 0x%x"), _daconCmd);
#endif
sendPacket(0x1A,0,0x00);
_daconCmd = false;
} else if (_eqCmd == true){ // Set Equalizer, values 0x00 - 0x05
if (_currentEQvalue != _requestedEQValue){
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: EQ: _eqCmd: 0x%x, _currentEQvalue: 0x0%x, _requestedEQValue: 0x0%x"), _eqCmd, _currentEQvalue, _requestedEQValue);
#endif
_currentEQvalue = _requestedEQValue;
sendPacket(0x07,0x00,_currentEQvalue);
}
_eqCmd = false;
} else if (_setamCmd == true){ // Set Audio mixer channel
setGPIO(); // Set the audio mixer channel
/*
if (_audioMixer == 1){ // set to audio mixer 1
if (_UART_CH == 0){
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 0 to high
} else { // must be UART 1
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 1 to high
}
//_setamCmd = false;
//UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);
} else { // set to audio mixer 2
if (_UART_CH == 0){
TEMP_REG_VAL &= (0x00 << _UART_CH); //Set GPIO pin 0 to Low
} else { // must be UART 1
TEMP_REG_VAL &= (0x00 << _UART_CH); //Set GPIO pin 1 to Low
}
//_setamCmd = false;
//UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);
}*/
_setamCmd = false;
} else if ((int32_t)currentMicros - _commandSendTime > 1000000) {
// Poll device every second that other commands aren't being sent,
// to check if it's still connected and responding.
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: Send keepalive") );
#endif
sendPacket(0x42,0,0);
if (!_awaitingResponse) {
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: Send keepalive, _awaitingResponse: 0x0%x"), _awaitingResponse );
#endif
_timeoutTime = currentMicros + 5000000UL; // Timeout if no response within 5 seconds
_awaitingResponse = true;
}
}
}
}
// Write to a vPin will do nothing
void _write(VPIN vpin, int value) override {
if (_deviceState == DEVSTATE_FAILED) return;
#ifdef DIAG_IO
DIAG(F("I2CDFPlayer: Writing to any vPin not supported"));
#endif
}
// 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.
//
// WriteAnalogue to be done on first vpin
//
//void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t=0) override {
void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t cmd=0) override {
if (_deviceState == DEVSTATE_FAILED) return;
#ifdef DIAG_IO
DIAG(F("I2CDFPlayer: VPIN:%u FileNo:%d Volume:%d Command:0x%x"), vpin, value, volume, cmd);
#endif
uint8_t pin = vpin - _firstVpin;
if (pin == 0) { // Enhanced DFPlayer commands, do nothing if not vPin 0
// Read command and value
switch (cmd){
//case NONE:
// DFPlayerCmd = cmd;
// break;
case DF_PLAY:
_playCmd = true;
_volCmd = true;
_requestedSong = value;
_requestedVolumeLevel = volume;
_playing = true;
break;
case DF_VOL:
_volCmd = true;
_requestedVolumeLevel = volume;
break;
case DF_FOLDER:
_folderCmd = true;
if (volume <= 0 || volume > 99){ // Range checking, valid values 1-99, else default to 1
_requestedFolder = 0x01; // if outside range, default to folder 01
} else {
_requestedFolder = volume;
}
break;
case DF_REPEATPLAY: // Need to check if _repeat == true, if so do nothing
if (_repeat == false) {
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: WriteAnalog Repeat: _repeat: 0x0%x, value: %d _repeatCmd: 0x%x"), _repeat, value, _repeatCmd);
#endif
_repeatCmd = true;
_requestedSong = value;
_requestedVolumeLevel = volume;
_playing = true;
}
break;
case DF_STOPPLAY:
_stopplayCmd = true;
break;
case DF_EQ:
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: WriteAnalog EQ: cmd: 0x%x, EQ value: 0x%x"), cmd, volume);
#endif
_eqCmd = true;
if (volume <= 0 || volume > 5) { // If out of range, default to NORMAL
_requestedEQValue = DF_NORMAL;
} else { // Valid EQ parameter range
_requestedEQValue = volume;
}
break;
case DF_RESET:
_resetCmd = true;
break;
case DF_DACON: // Works, but without the DACOFF command limited value, except when not relying on DFPlayer default to turn the DAC on
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: WrtieAnalog DACON: cmd: 0x%x"), cmd);
#endif
_daconCmd = true;
break;
case DF_SETAM: // Set the audio mixer channel to 1 or 2
_setamCmd = true;
#ifdef DIAG_I2CDFplayer_playing
DIAG(F("I2CDFPlayer: WrtieAnalog SETAM: cmd: 0x%x"), cmd);
#endif
if (volume <= 0 || volume > 2) { // If out of range, default to 1
_audioMixer = 1;
} else { // Valid SETAM parameter in range
_audioMixer = volume; // _audioMixer valid values 1 or 2
}
break;
default:
break;
}
}
}
// A read on any pin indicates if the player is still playing.
int _read(VPIN vpin) override {
if (_deviceState == DEVSTATE_FAILED) return false;
uint8_t pin = vpin - _firstVpin;
if (pin == 0) { // Do nothing if not vPin 0
return _playing;
}
}
void _display() override {
DIAG(F("I2CDFPlayer Configured on Vpins:%u-%u %S"), _firstVpin, _firstVpin+_nPins-1,
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
private:
// DFPlayer command frame
// 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, uint8_t arg1 = 0, uint8_t arg2 = 0) {
FIFO_TX_LEVEL = 0; // Reset FIFO_TX_LEVEL
uint8_t out[] = {
0x7E,
0xFF,
06,
command,
00,
//static_cast<uint8_t>(arg >> 8),
//static_cast<uint8_t>(arg & 0x00ff),
arg1,
arg2,
00,
00,
0xEF };
setChecksum(out);
// Prepend the DFPlayer command with REG address and UART Channel in _outbuffer
_outbuffer[0] = REG_THR << 3 | _UART_CH << 1; //TX FIFO and UART Channel
for ( int i = 1; i < sizeof(out)+1 ; i++){
_outbuffer[i] = out[i-1];
}
#ifdef DIAG_I2CDFplayer_data
DIAG(F("SC16IS752: I2C: %s Sent packet function"), _I2CAddress.toString());
for (int i = 0; i < sizeof _outbuffer; i++){
DIAG(F("SC16IS752: Data _outbuffer[0x%x]: 0x%x"), i, _outbuffer[i]);
}
#endif
TX_fifo_lvl();
if(FIFO_TX_LEVEL > 0){ //FIFO is empty
I2CManager.write(_I2CAddress, _outbuffer, sizeof(_outbuffer), &_rb);
//I2CManager.write(_I2CAddress, _outbuffer, sizeof(_outbuffer));
#ifdef DIAG_I2CDFplayer
DIAG(F("SC16IS752: I2C: %s data transmit complete on UART: 0x%x"), _I2CAddress.toString(), _UART_CH);
#endif
} else {
DIAG(F("I2CDFPlayer at: %s, TX FIFO not empty on UART: 0x%x"), _I2CAddress.toString(), _UART_CH);
_deviceState = DEVSTATE_FAILED; // This should not happen
}
_commandSendTime = micros();
}
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);
}
// SC16IS752 functions
// Initialise SC16IS752 only for this channel
// First a software reset
// Enable FIFO and clear TX & RX FIFO
// Need to set the following registers
// IOCONTROL set bit 1 and 2 to 0 indicating that they are GPIO
// IODIR set all bit to 1 indicating al are output
// IOSTATE set only bit 0 to 1 for UART 0, or only bit 1 for UART 1 //
// LCR bit 7=0 divisor latch (clock division registers DLH & DLL, they store 16 bit divisor),
// WORD_LEN, STOP_BIT, PARITY_ENA and PARITY_TYPE
// MCR bit 7=0 clock divisor devide-by-1 clock input
// DLH most significant part of divisor
// DLL least significant part of divisor
//
// BAUD_RATE, WORD_LEN, STOP_BIT, PARITY_ENA and PARITY_TYPE have been defined and initialized
//
void Init_SC16IS752(){ // Return value is in _deviceState
#ifdef DIAG_I2CDFplayer
DIAG(F("SC16IS752: Initialize I2C: %s , UART Ch: 0x%x"), _I2CAddress.toString(), _UART_CH);
#endif
//uint16_t _divisor = (SC16IS752_XTAL_FREQ / PRESCALER) / (BAUD_RATE * 16);
uint16_t _divisor = (_sc16is752_xtal_freq/PRESCALER)/(BAUD_RATE * 16); // Calculate _divisor for baudrate
TEMP_REG_VAL = 0x08; // UART Software reset
UART_WriteRegister(REG_IOCONTROL, TEMP_REG_VAL);
TEMP_REG_VAL = 0x00; // Set pins to GPIO mode
UART_WriteRegister(REG_IOCONTROL, TEMP_REG_VAL);
TEMP_REG_VAL = 0xFF; //Set all pins as output
UART_WriteRegister(REG_IODIR, TEMP_REG_VAL);
UART_ReadRegister(REG_IOSTATE); // Read current state as not to overwrite the other GPIO pins
TEMP_REG_VAL = _inbuffer[0];
setGPIO(); // Set the audio mixer channel
/*
if (_UART_CH == 0){ // Set Audio mixer channel
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 0 to high
} else { // must be UART 1
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 1 to high
}
UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);
*/
TEMP_REG_VAL = 0x07; // Reset FIFO, clear RX & TX FIFO
UART_WriteRegister(REG_FCR, TEMP_REG_VAL);
TEMP_REG_VAL = 0x00; // Set MCR to all 0, includes Clock divisor
UART_WriteRegister(REG_MCR, TEMP_REG_VAL);
TEMP_REG_VAL = 0x80 | WORD_LEN | STOP_BIT | PARITY_ENA | PARITY_TYPE;
UART_WriteRegister(REG_LCR, TEMP_REG_VAL); // Divisor latch enabled
UART_WriteRegister(REG_DLL, (uint8_t)_divisor); // Write DLL
UART_WriteRegister(REG_DLH, (uint8_t)(_divisor >> 8)); // Write DLH
UART_ReadRegister(REG_LCR);
TEMP_REG_VAL = _inbuffer[0] & 0x7F; // Disable Divisor latch enabled bit
UART_WriteRegister(REG_LCR, TEMP_REG_VAL); // Divisor latch disabled
uint8_t status = _rb.status;
if (status != I2C_STATUS_OK) {
DIAG(F("SC16IS752: I2C: %s failed %S"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));
_deviceState = DEVSTATE_FAILED;
} else {
#ifdef DIAG_IO
DIAG(F("SC16IS752: I2C: %s, _deviceState: %S"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));
#endif
_deviceState = DEVSTATE_NORMAL; // If I2C state is OK, then proceed to initialize DFPlayer
}
}
// Read the Receive FIFO Level register (RXLVL), return a single unsigned integer
// of nr of characters in the RX FIFO, bit 6:0, 7 not used, set to zero
// value from 0 (0x00) to 64 (0x40) Only display if RX FIFO has data
// The RX fifo level is used to check if there are enough bytes to process a frame
void RX_fifo_lvl(){
UART_ReadRegister(REG_RXLV);
FIFO_RX_LEVEL = _inbuffer[0];
#ifdef DIAG_I2CDFplayer
if (FIFO_RX_LEVEL > 0){
//if (FIFO_RX_LEVEL > 0 && FIFO_RX_LEVEL < 10){
DIAG(F("SC16IS752: At I2C: %s, UART channel: 0x%x, FIFO_RX_LEVEL: 0d%d"), _I2CAddress.toString(), _UART_CH, _inbuffer[0]);
}
#endif
}
// When a frame is transmitted from the DFPlayer to the serial port, and at the same time the CS is sending a 42 query
// the following two frames from the DFPlayer are corrupt. This result in the receive buffer being out of sync and the
// CS will complain and generate a timeout.
// The RX fifo has corrupt data and need to be flushed, this function does that
//
void resetRX_fifo(){
#ifdef DIAG_I2CDFplayer
DIAG(F("SC16IS752: At I2C: %s, UART channel: 0x%x, RX fifo reset"), _I2CAddress.toString(), _UART_CH);
#endif
TEMP_REG_VAL = 0x03; // Reset RX fifo
UART_WriteRegister(REG_FCR, TEMP_REG_VAL);
}
// Set or reset GPIO pin 0 and 1 depending on the UART ch
// This function may be modified in a future release to enable all 8 pins to be set or reset with EX-Rail
// for various auxilary functions
void setGPIO(){
UART_ReadRegister(REG_IOSTATE); // Get the current GPIO pins state from the IOSTATE register
TEMP_REG_VAL = _inbuffer[0];
if (_audioMixer == 1){ // set to audio mixer 1
if (_UART_CH == 0){
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 0 to high
} else { // must be UART 1
TEMP_REG_VAL |= (0x01 << _UART_CH); //Set GPIO pin 1 to high
}
} else { // set to audio mixer 2
if (_UART_CH == 0){
TEMP_REG_VAL &= ~(0x01 << _UART_CH); //Set GPIO pin 0 to Low
} else { // must be UART 1
TEMP_REG_VAL &= ~(0x01 << _UART_CH); //Set GPIO pin 1 to Low
}
}
UART_WriteRegister(REG_IOSTATE, TEMP_REG_VAL);
_setamCmd = false;
}
// Read the Tranmit FIFO Level register (TXLVL), return a single unsigned integer
// of nr characters free in the TX FIFO, bit 6:0, 7 not used, set to zero
// value from 0 (0x00) to 64 (0x40)
//
void TX_fifo_lvl(){
UART_ReadRegister(REG_TXLV);
FIFO_TX_LEVEL = _inbuffer[0];
#ifdef DIAG_I2CDFplayer
// DIAG(F("SC16IS752: At I2C: %s, UART channel: 0x%x, FIFO_TX_LEVEL: 0d%d"), _I2CAddress.toString(), _UART_CH, FIFO_TX_LEVEL);
#endif
}
//void UART_WriteRegister(I2CAddress _I2CAddress, uint8_t _UART_CH, uint8_t UART_REG, uint8_t Val, I2CRB &_rb){
void UART_WriteRegister(uint8_t UART_REG, uint8_t Val){
_outbuffer[0] = UART_REG << 3 | _UART_CH << 1;
_outbuffer[1] = Val;
#ifdef DIAG_I2CDFplayer_reg
DIAG(F("SC16IS752: Write register at I2C: %s, UART channel: 0x%x, Register: 0x%x, Data: 0b%b"), _I2CAddress.toString(), _UART_CH, UART_REG, _outbuffer[1]);
#endif
I2CManager.write(_I2CAddress, _outbuffer, 2);
}
void UART_ReadRegister(uint8_t UART_REG){
_outbuffer[0] = UART_REG << 3 | _UART_CH << 1; // _outbuffer[0] has now UART_REG and UART_CH
I2CManager.read(_I2CAddress, _inbuffer, 1, _outbuffer, 1);
// _inbuffer has the REG data
#ifdef DIAG_I2CDFplayer_reg
DIAG(F("SC16IS752: Read register at I2C: %s, UART channel: 0x%x, Register: 0x%x, Data: 0b%b"), _I2CAddress.toString(), _UART_CH, UART_REG, _inbuffer[0]);
#endif
}
// SC16IS752 General register set (from the datasheet)
enum : uint8_t{
REG_RHR = 0x00, // FIFO Read
REG_THR = 0x00, // FIFO Write
REG_IER = 0x01, // Interrupt Enable Register R/W
REG_FCR = 0x02, // FIFO Control Register Write
REG_IIR = 0x02, // Interrupt Identification Register Read
REG_LCR = 0x03, // Line Control Register R/W
REG_MCR = 0x04, // Modem Control Register R/W
REG_LSR = 0x05, // Line Status Register Read
REG_MSR = 0x06, // Modem Status Register Read
REG_SPR = 0x07, // Scratchpad Register R/W
REG_TCR = 0x06, // Transmission Control Register R/W
REG_TLR = 0x07, // Trigger Level Register R/W
REG_TXLV = 0x08, // Transmitter FIFO Level register Read
REG_RXLV = 0x09, // Receiver FIFO Level register Read
REG_IODIR = 0x0A, // Programmable I/O pins Direction register R/W
REG_IOSTATE = 0x0B, // Programmable I/O pins State register R/W
REG_IOINTENA = 0x0C, // I/O Interrupt Enable register R/W
REG_IOCONTROL = 0x0E, // I/O Control register R/W
REG_EFCR = 0x0F, // Extra Features Control Register R/W
};
// SC16IS752 Special register set
enum : uint8_t{
REG_DLL = 0x00, // Division registers R/W
REG_DLH = 0x01, // Division registers R/W
};
// SC16IS752 Enhanced regiter set
enum : uint8_t{
REG_EFR = 0X02, // Enhanced Features Register R/W
REG_XON1 = 0x04, // R/W
REG_XON2 = 0x05, // R/W
REG_XOFF1 = 0x06, // R/W
REG_XOFF2 = 0x07, // R/W
};
// DFPlayer commands and values
// Declared in this scope
enum : uint8_t{
DF_PLAY = 0x0F,
DF_VOL = 0x06,
DF_FOLDER = 0x2B, // Not a DFPlayer command, used to set folder nr where audio file is
DF_REPEATPLAY = 0x08,
DF_STOPPLAY = 0x16,
DF_EQ = 0x07, // Set equaliser, require parameter NORMAL, POP, ROCK, JAZZ, CLASSIC or BASS
DF_RESET = 0x0C,
DF_DACON = 0x1A,
DF_SETAM = 0x2A, // Set audio mixer 1 or 2 for this DFPLayer
DF_NORMAL = 0x00, // Equalizer parameters
DF_POP = 0x01,
DF_ROCK = 0x02,
DF_JAZZ = 0x03,
DF_CLASSIC = 0x04,
DF_BASS = 0x05,
};
};
#endif // IO_I2CDFPlayer_h

View File

@@ -42,9 +42,9 @@
* Defining in myAutomation.h requires the device driver to be included in addition to the HAL() statement. Examples:
*
* #include "IO_RotaryEncoder.h"
* HAL(RotaryEncoder, 700, 1, 0x70) // Define single Vpin, no feedback or position sent to rotary encoder software
* HAL(RotaryEncoder, 700, 2, 0x70) // Define two Vpins, feedback only sent to rotary encoder software
* HAL(RotaryEncoder, 700, 3, 0x70) // Define three Vpins, can send feedback and position update to rotary encoder software
* HAL(RotaryEncoder, 700, 1, 0x67) // Define single Vpin, no feedback or position sent to rotary encoder software
* HAL(RotaryEncoder, 700, 2, 0x67) // Define two Vpins, feedback only sent to rotary encoder software
* HAL(RotaryEncoder, 700, 3, 0x67) // Define three Vpins, can send feedback and position update to rotary encoder software
*
* Refer to the documentation for further information including the valid activities and examples.
*/

98
IO_trainbrains.h Normal file
View File

@@ -0,0 +1,98 @@
/*
* © 2023, Chris Harlow. All rights reserved.
* © 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_trainbrains_h
#define io_trainbrains_h
#include "IO_GPIOBase.h"
#include "FSH.h"
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for trainbrains 3-block occupancy detector.
* For details see http://trainbrains.eu
*/
enum TrackUnoccupancy
{
TRACK_UNOCCUPANCY_UNKNOWN = 0,
TRACK_OCCUPIED = 1,
TRACK_UNOCCUPIED = 2
};
class Trainbrains02 : public GPIOBase<uint16_t> {
public:
static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress) {
if (checkNoOverlap(vpin, nPins, i2cAddress)) new Trainbrains02(vpin, nPins, i2cAddress);
}
private:
// Constructor
Trainbrains02(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
: GPIOBase<uint16_t>((FSH *)F("Trainbrains02"), vpin, nPins, i2cAddress, interruptPin)
{
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer));
outputBuffer[0] = (uint8_t)_I2CAddress; // strips away the mux part.
outputBuffer[1] =14;
outputBuffer[2] =1;
outputBuffer[3] =0; // This is the channel updated at each poling call
outputBuffer[4] =0;
outputBuffer[5] =0;
outputBuffer[6] =0;
outputBuffer[7] =0;
outputBuffer[8] =0;
outputBuffer[9] =0;
}
void _writeGpioPort() override {}
void _readGpioPort(bool immediate) override {
// cycle channel on device each time
outputBuffer[3]=channelInProgress+1; // 1-origin
channelInProgress++;
if(channelInProgress>=_nPins) channelInProgress=0;
if (immediate) {
_processCompletion(I2CManager.read(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer)));
} 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) inputBuffer[6]=TRACK_UNOCCUPANCY_UNKNOWN;
if (inputBuffer[6] == TRACK_UNOCCUPIED ) _portInputState |= 0x01 <<channelInProgress;
else _portInputState &= ~(0x01 <<channelInProgress);
}
uint8_t channelInProgress=0;
uint8_t outputBuffer[10];
uint8_t inputBuffer[10];
};
#endif

57
KeywordHasher.h Normal file
View File

@@ -0,0 +1,57 @@
/*
* © 2024 Vincent Hamp and 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/>.
*/
/* Reader be aware:
This function implements the _hk data type so that a string keyword
is hashed to the same value as the DCCEXParser uses to hash incoming
keywords.
Thus "MAIN"_hk generates exactly the same run time vakue
as const int16_t HASH_KEYWORD_MAIN=11339
*/
#ifndef KeywordHAsher_h
#define KeywordHasher_h
#include <Arduino.h>
constexpr uint16_t CompiletimeKeywordHasher(const char * sv, uint16_t running=0) {
return (*sv==0) ? running : CompiletimeKeywordHasher(sv+1,
(*sv >= '0' && *sv <= '9')
? (10*running+*sv-'0') // Numeric hash
: ((running << 5) + running) ^ *sv
); //
}
constexpr int16_t operator""_hk(const char * keyword, size_t len)
{
return (int16_t) CompiletimeKeywordHasher(keyword,len*0);
}
/* Some historical values for testing:
const int16_t HASH_KEYWORD_MAIN = 11339;
const int16_t HASH_KEYWORD_SLOW = -17209;
const int16_t HASH_KEYWORD_SPEED28 = -17064;
const int16_t HASH_KEYWORD_SPEED128 = 25816;
*/
static_assert("MAIN"_hk == 11339,"Keyword hasher error");
static_assert("SLOW"_hk == -17209,"Keyword hasher error");
static_assert("SPEED28"_hk == -17064,"Keyword hasher error");
static_assert("SPEED128"_hk == 25816,"Keyword hasher error");
#endif

View File

@@ -204,7 +204,7 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
}
bool MotorDriver::isPWMCapable() {
return (!dualSignal) && DCCTimer::isPWMPin(signalPin);
return (!dualSignal) && DCCTimer::isPWMPin(signalPin);
}
@@ -325,49 +325,23 @@ uint16_t taurustones[28] = { 165, 175, 196, 220,
220, 196, 175, 165 };
#endif
#endif
void MotorDriver::setDCSignal(byte speedcode) {
void MotorDriver::setDCSignal(byte speedcode, uint8_t frequency /*default =0*/) {
if (brakePin == UNUSED_PIN)
return;
switch(brakePin) {
#if defined(ARDUINO_AVR_UNO)
// Not worth doin something here as:
// If we are on pin 9 or 10 we are on Timer1 and we can not touch Timer1 as that is our DCC source.
// If we are on pin 5 or 6 we are on Timer 0 ad we can not touch Timer0 as that is millis() etc.
// We are most likely not on pin 3 or 11 as no known motor shield has that as brake.
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
case 9:
case 10:
// Timer2 (is differnet)
TCCR2A = (TCCR2A & B11111100) | B00000001; // set WGM1=0 and WGM0=1 phase correct PWM
TCCR2B = (TCCR2B & B11110000) | B00000110; // set WGM2=0 ; set divisor on timer 2 to 1/256 for 122.55Hz
//DIAG(F("2 A=%x B=%x"), TCCR2A, TCCR2B);
break;
case 6:
case 7:
case 8:
// Timer4
TCCR4A = (TCCR4A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for normal PWM 8-bit
TCCR4B = (TCCR4B & B11100000) | B00000100; // set WGM2=0 and WGM3=0 for normal PWM 8 bit and div 1/256 for 122.55Hz
break;
case 46:
case 45:
case 44:
// Timer5
TCCR5A = (TCCR5A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for normal PWM 8-bit
TCCR5B = (TCCR5B & B11100000) | B00000100; // set WGM2=0 and WGM3=0 for normal PWM 8 bit and div 1/256 for 122.55Hz
break;
#endif
default:
break;
}
// 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;
{ // new block because of variable f
#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32)
{
int f = 131;
int f = frequency;
#ifdef VARIABLE_TONES
if (tSpeed > 2) {
if (tSpeed <= 58) {
@@ -375,19 +349,15 @@ void MotorDriver::setDCSignal(byte speedcode) {
}
}
#endif
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency to 100Hz XXX May move to setup
//DIAG(F("Brake pin %d freqency %d"), brakePin, f);
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency
DCCTimer::DCCEXanalogWrite(brakePin,brake);
#else // all AVR here
DCCTimer::DCCEXanalogWriteFrequency(brakePin, frequency); // frequency steps
analogWrite(brakePin,brake);
#endif
}
#endif
if (tSpeed <= 1) brake = 255;
else if (tSpeed >= 127) brake = 0;
else brake = 2 * (128-tSpeed);
if (invertBrake)
brake=255-brake;
#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32)
DCCTimer::DCCEXanalogWrite(brakePin,brake);
#else
analogWrite(brakePin,brake);
#endif
//DIAG(F("DCSignal %d"), speedcode);
if (HAVE_PORTA(fastSignalPin.shadowinout == &PORTA)) {
noInterrupts();
@@ -436,58 +406,26 @@ void MotorDriver::throttleInrush(bool on) {
return;
if ( !(trackMode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_EXT)))
return;
byte duty = on ? 208 : 0;
byte duty = on ? 207 : 0; // duty of 81% at 62500Hz this gives pauses of 3usec
if (invertBrake)
duty = 255-duty;
#if defined(ARDUINO_ARCH_ESP32)
if(on) {
DCCTimer::DCCEXanalogWrite(brakePin,duty);
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 62500);
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max
} else {
ledcDetachPin(brakePin);
}
#elif defined(ARDUINO_ARCH_STM32)
if(on) {
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 62500);
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max
DCCTimer::DCCEXanalogWrite(brakePin,duty);
} else {
pinMode(brakePin, OUTPUT);
}
#else
#else // all AVR here
if(on){
switch(brakePin) {
#if defined(ARDUINO_AVR_UNO)
// Not worth doin something here as:
// If we are on pin 9 or 10 we are on Timer1 and we can not touch Timer1 as that is our DCC source.
// If we are on pin 5 or 6 we are on Timer 0 ad we can not touch Timer0 as that is millis() etc.
// We are most likely not on pin 3 or 11 as no known motor shield has that as brake.
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
case 9:
case 10:
// Timer2 (is different)
TCCR2A = (TCCR2A & B11111100) | B00000011; // set WGM0=1 and WGM1=1 for fast PWM
TCCR2B = (TCCR2B & B11110000) | B00000001; // set WGM2=0 and prescaler div=1 (max)
DIAG(F("2 A=%x B=%x"), TCCR2A, TCCR2B);
break;
case 6:
case 7:
case 8:
// Timer4
TCCR4A = (TCCR4A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for fast PWM 8-bit
TCCR4B = (TCCR4B & B11100000) | B00001001; // set WGM2=1 and WGM3=0 for fast PWM 8 bit and div=1 (max)
break;
case 46:
case 45:
case 44:
// Timer5
TCCR5A = (TCCR5A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for fast PWM 8-bit
TCCR5B = (TCCR5B & B11100000) | B00001001; // set WGM2=1 and WGM3=0 for fast PWM 8 bit and div=1 (max)
break;
#endif
default:
break;
}
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max
}
analogWrite(brakePin,duty);
#endif
@@ -605,6 +543,10 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
DIAG(F("TRACK %c ALERT FAULT"), trackno + 'A');
}
setPower(POWERMODE::ALERT);
if ((trackMode & TRACK_MODE_AUTOINV) && (trackMode & (TRACK_MODE_MAIN|TRACK_MODE_EXT|TRACK_MODE_BOOST))){
DIAG(F("TRACK %c INVERT"), trackno + 'A');
invertOutput();
}
break;
}
// all well
@@ -676,8 +618,10 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
power_sample_overload_wait *= 2;
if (power_sample_overload_wait > POWER_SAMPLE_RETRY_MAX)
power_sample_overload_wait = POWER_SAMPLE_RETRY_MAX;
#ifdef EXRAIL_ACTIVE
DIAG(F("Calling EXRAIL"));
RMFT2::powerEvent(trackno, true); // Tell EXRAIL we have an overload
#endif
// power on test
DIAG(F("TRACK %c POWER RESTORE (after %4M)"), trackno + 'A', mslpc);
setPower(POWERMODE::ALERT);

View File

@@ -3,7 +3,7 @@
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020 Chris Harlow
* © 2022 Harald Barth
* © 2022,2023 Harald Barth
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -28,8 +28,21 @@
#include "DCCTimer.h"
// use powers of two so we can do logical and/or on the track modes in if clauses.
// RACK_MODE_DCX is (TRACK_MODE_DC|TRACK_MODE_INV)
template<class T> inline T operator~ (T a) { return (T)~(int)a; }
template<class T> inline T operator| (T a, T b) { return (T)((int)a | (int)b); }
template<class T> inline T operator& (T a, T b) { return (T)((int)a & (int)b); }
template<class T> inline T operator^ (T a, T b) { return (T)((int)a ^ (int)b); }
enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PROG = 4,
TRACK_MODE_DC = 8, TRACK_MODE_DCX = 16, TRACK_MODE_EXT = 32};
TRACK_MODE_DC = 8, TRACK_MODE_EXT = 16,
#ifdef ARDUINO_ARCH_ESP32
TRACK_MODE_BOOST = 32,
#else
TRACK_MODE_BOOST = 0,
#endif
TRACK_MODE_ALL = TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_EXT|TRACK_MODE_BOOST,
TRACK_MODE_INV = 64,
TRACK_MODE_DCX = TRACK_MODE_DC|TRACK_MODE_INV, TRACK_MODE_AUTOINV = 128};
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
@@ -148,7 +161,11 @@ class MotorDriver {
// otherwise the call from interrupt context can undo whatever we do
// from outside interrupt
void setBrake( bool on, bool interruptContext=false);
__attribute__((always_inline)) inline void setSignal( bool high) {
__attribute__((always_inline)) inline void setSignal( bool high) {
#ifndef ARDUINO_ARCH_ESP32
if (invertPhase)
high = !high;
#endif
if (trackPWM) {
DCCTimer::setPWM(signalPin,high);
}
@@ -168,9 +185,15 @@ class MotorDriver {
pinMode(signalPin, OUTPUT);
else
pinMode(signalPin, INPUT);
if (signalPin2 != UNUSED_PIN) {
if (on)
pinMode(signalPin2, OUTPUT);
else
pinMode(signalPin2, INPUT);
}
};
inline pinpair getSignalPin() { return pinpair(signalPin,signalPin2); };
void setDCSignal(byte speedByte);
void setDCSignal(byte speedByte, uint8_t frequency=0);
void throttleInrush(bool on);
inline void detachDCSignal() {
#if defined(__arm__)
@@ -232,6 +255,32 @@ class MotorDriver {
#endif
inline void setMode(TRACK_MODE m) {
trackMode = m;
invertOutput(trackMode & TRACK_MODE_INV);
};
inline void invertOutput() { // toggles output inversion
invertPhase = !invertPhase;
invertOutput(invertPhase);
};
inline void invertOutput(bool b) { // sets output inverted or not
if (b)
invertPhase = 1;
else
invertPhase = 0;
#if defined(ARDUINO_ARCH_ESP32)
pinpair p = getSignalPin();
uint32_t *outreg = (uint32_t *)(GPIO_FUNC0_OUT_SEL_CFG_REG + 4*p.pin);
if (invertPhase) // set or clear the invert bit in the gpio out register
*outreg |= ((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
else
*outreg &= ~((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
if (p.invpin != UNUSED_PIN) {
outreg = (uint32_t *)(GPIO_FUNC0_OUT_SEL_CFG_REG + 4*p.invpin);
if (invertPhase) // clear or set the invert bit in the gpio out register
*outreg &= ~((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
else
*outreg |= ((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
}
#endif
};
inline TRACK_MODE getMode() {
return trackMode;
@@ -263,7 +312,7 @@ class MotorDriver {
bool invertBrake; // brake pin passed as negative means pin is inverted
bool invertPower; // power pin passed as negative means pin is inverted
bool invertFault; // fault pin passed as negative means pin is inverted
bool invertPhase = 0; // phase of out pin is inverted
// Raw to milliamp conversion factors avoiding float data types.
// Milliamps=rawADCreading * sensefactorInternal / senseScale
//

View File

@@ -1,7 +1,7 @@
/*
* © 2022-2023 Paul M. Antoine
* © 2021 Fred Decker
* © 2020-2023 Harald Barth
* © 2020-2024 Harald Barth
* (c) 2020 Chris Harlow. All rights reserved.
* (c) 2021 Fred Decker. All rights reserved.
* (c) 2020 Harald Barth. All rights reserved.
@@ -57,6 +57,10 @@
// of the brake pin on the motor bridge is inverted
// (HIGH == release brake)
// You can have a CS wihout any possibility to do any track signal.
// That's strange but possible.
#define NO_SHIELD F("No shield at all")
// Arduino STANDARD Motor Shield, used on different architectures:
#if defined(ARDUINO_ARCH_SAMD) || defined(ARDUINO_ARCH_STM32)

View File

@@ -230,6 +230,13 @@ Sensor *Sensor::create(int snum, VPIN pin, int pullUp){
return tt;
}
// Creet multiple eponymous sensors based on vpin alone.
void Sensor::createMultiple(VPIN firstPin, byte count) {
for (byte i=0;i<count;i++) {
create(firstPin+i,firstPin+i,1);
}
}
///////////////////////////////////////////////////////////////////////////////
// Object method to directly change the input state, for sensors such as LCN which are updated
// by means other than by polling an input.

View File

@@ -76,6 +76,7 @@ public:
static void store();
#endif
static Sensor *create(int id, VPIN vpin, int pullUp);
static void createMultiple(VPIN firstPin, byte count=1);
static Sensor* get(int id);
static bool remove(int id);
static void checkAll();

View File

@@ -111,14 +111,15 @@ void SerialManager::loop2() {
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;
else if (inCommandPayload) {
if (bufferLength < (COMMAND_BUFFER_SIZE-1))
buffer[bufferLength++] = ch;
if (ch == '>') {
buffer[bufferLength] = '\0';
DCCEXParser::parse(serial, buffer, NULL);
inCommandPayload = false;
break;
}
}
}

View File

@@ -19,6 +19,7 @@
#include "StringFormatter.h"
#include <stdarg.h>
#include "DisplayInterface.h"
#include "CommandDistributor.h"
bool Diag::ACK=false;
bool Diag::CMD=false;
@@ -38,13 +39,28 @@ void StringFormatter::diag( const FSH* input...) {
void StringFormatter::lcd(byte row, const FSH* input...) {
va_list args;
#ifndef DISABLE_VDPY
Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(0,row);
#else
Print * virtualLCD=NULL;
#endif
// Issue the LCD as a diag first
send(&USB_SERIAL,F("<* LCD%d:"),row);
va_start(args, input);
send2(&USB_SERIAL,input,args);
send(&USB_SERIAL,F(" *>\n"));
// Unless the same serial is asking for the virtual @ respomnse
if (virtualLCD!=&USB_SERIAL) {
send(&USB_SERIAL,F("<* LCD%d:"),row);
va_start(args, input);
send2(&USB_SERIAL,input,args);
send(&USB_SERIAL,F(" *>\n"));
}
#ifndef DISABLE_VDPY
// send to virtual LCD collector (if any)
if (virtualLCD) {
va_start(args, input);
send2(virtualLCD,input,args);
CommandDistributor::commitVirtualLCDSerial();
}
#endif
DisplayInterface::setRow(row);
va_start(args, input);
send2(DisplayInterface::getDisplayHandler(),input,args);
@@ -52,6 +68,16 @@ void StringFormatter::lcd(byte row, const FSH* input...) {
void StringFormatter::lcd2(uint8_t display, byte row, const FSH* input...) {
va_list args;
// send to virtual LCD collector (if any)
#ifndef DISABLE_VDPY
Print * virtualLCD=CommandDistributor::getVirtualLCDSerial(display,row);
if (virtualLCD) {
va_start(args, input);
send2(virtualLCD,input,args);
CommandDistributor::commitVirtualLCDSerial();
}
#endif
DisplayInterface::setRow(display, row);
va_start(args, input);
@@ -117,6 +143,7 @@ void StringFormatter::send2(Print * stream,const FSH* format, va_list args) {
case 'o': stream->print(va_arg(args, int), OCT); break;
case 'x': stream->print((unsigned int)va_arg(args, unsigned int), HEX); break;
case 'X': stream->print((unsigned long)va_arg(args, unsigned long), HEX); break;
case 'h': printHex(stream,(unsigned int)va_arg(args, unsigned int)); break;
case 'M':
{ // this prints a unsigned long microseconds time in readable format
unsigned long time = va_arg(args, long);
@@ -218,4 +245,14 @@ void StringFormatter::printPadded(Print* stream, long value, byte width, bool fo
if (!formatLeft) stream->print(value, DEC);
}
// printHex prints the full 2 byte hex with leading zeros, unlike print(value,HEX)
const char FLASH hexchars[]="0123456789ABCDEF";
void StringFormatter::printHex(Print * stream,uint16_t value) {
char result[5];
for (int i=3;i>=0;i--) {
result[i]=GETFLASH(hexchars+(value & 0x0F));
value>>=4;
}
result[4]='\0';
stream->print(result);
}

View File

@@ -49,10 +49,10 @@ class StringFormatter
static void lcd2(uint8_t display, byte row, const FSH* input...);
static void printEscapes(char * input);
static void printEscape( char c);
static void printHex(Print * stream,uint16_t value);
private:
static void send2(Print * serial, const FSH* input,va_list args);
static void printPadded(Print* stream, long value, byte width, bool formatLeft);
};
#endif

View File

@@ -1,6 +1,6 @@
/*
* © 2022 Chris Harlow
* © 2022 Harald Barth
* © 2022-2024 Harald Barth
* © 2023 Colin Murdoch
* All rights reserved.
*
@@ -19,6 +19,7 @@
* 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"
#include "TrackManager.h"
#include "FSH.h"
#include "DCCWaveform.h"
@@ -26,7 +27,9 @@
#include "MotorDriver.h"
#include "DCCTimer.h"
#include "DIAG.h"
#include"CommandDistributor.h"
#include "CommandDistributor.h"
#include "DCCEXParser.h"
#include "KeywordHasher.h"
// Virtualised Motor shield multi-track hardware Interface
#define FOR_EACH_TRACK(t) for (byte t=0;t<=lastTrack;t++)
@@ -34,22 +37,11 @@
FOR_EACH_TRACK(t) \
if (track[t]->getMode()==findmode) \
track[t]->function;
#ifndef DISABLE_PROG
const int16_t HASH_KEYWORD_PROG = -29718;
#endif
const int16_t HASH_KEYWORD_MAIN = 11339;
const int16_t HASH_KEYWORD_OFF = 22479;
const int16_t HASH_KEYWORD_NONE = -26550;
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];
int16_t TrackManager::trackDCAddr[MAX_TRACKS];
POWERMODE TrackManager::mainPowerGuess=POWERMODE::OFF;
byte TrackManager::lastTrack=0;
int8_t TrackManager::lastTrack=-1;
bool TrackManager::progTrackSyncMain=false;
bool TrackManager::progTrackBoosted=false;
int16_t TrackManager::joinRelay=UNUSED_PIN;
@@ -86,7 +78,7 @@ void TrackManager::sampleCurrent() {
if (!waiting) {
// look for a valid track to sample or until we are around
while (true) {
if (track[tr]->getMode() & ( TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_DCX|TRACK_MODE_EXT )) {
if (track[tr]->getMode() & ( TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_BOOST|TRACK_MODE_EXT )) {
track[tr]->startCurrentFromHW();
// for scope debug track[1]->setBrake(1);
waiting = true;
@@ -166,12 +158,6 @@ void TrackManager::setDCCSignal( bool on) {
HAVE_PORTF(PORTF=shadowPORTF);
}
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) {
@@ -196,17 +182,20 @@ void TrackManager::setPROGSignal( bool on) {
void TrackManager::setDCSignal(int16_t cab, byte speedbyte) {
FOR_EACH_TRACK(t) {
if (trackDCAddr[t]!=cab && cab != 0) continue;
if (track[t]->getMode()==TRACK_MODE_DC) track[t]->setDCSignal(speedbyte);
else if (track[t]->getMode()==TRACK_MODE_DCX) track[t]->setDCSignal(speedbyte ^ 128);
if (track[t]->getMode() & TRACK_MODE_DC)
track[t]->setDCSignal(speedbyte, DCC::getThrottleFrequency(trackDCAddr[t]));
}
}
bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr) {
if (trackToSet>lastTrack || track[trackToSet]==NULL) return false;
// Remember track mode we came from for later
TRACK_MODE oldmode = track[trackToSet]->getMode();
//DIAG(F("Track=%c Mode=%d"),trackToSet+'A', mode);
// DC tracks require a motorDriver that can set brake!
if (mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX) {
if (mode & TRACK_MODE_DC) {
#if defined(ARDUINO_AVR_UNO)
DIAG(F("Uno has no PWM timers available for DC"));
return false;
@@ -222,25 +211,41 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
pinpair p = track[trackToSet]->getSignalPin();
//DIAG(F("Track=%c remove pin %d"),trackToSet+'A', p.pin);
gpio_reset_pin((gpio_num_t)p.pin);
pinMode(p.pin, OUTPUT); // gpio_reset_pin may reset to input
if (p.invpin != UNUSED_PIN) {
//DIAG(F("Track=%c remove ^pin %d"),trackToSet+'A', p.invpin);
gpio_reset_pin((gpio_num_t)p.invpin);
pinMode(p.invpin, OUTPUT); // gpio_reset_pin may reset to input
}
#ifdef BOOSTER_INPUT
if (mode & TRACK_MODE_BOOST) {
//DIAG(F("Track=%c mode boost pin %d"),trackToSet+'A', p.pin);
pinMode(BOOSTER_INPUT, INPUT);
gpio_matrix_in(BOOSTER_INPUT, SIG_IN_FUNC228_IDX, false); //pads 224 to 228 available as loopback
gpio_matrix_out(p.pin, SIG_IN_FUNC228_IDX, false, false);
if (p.invpin != UNUSED_PIN) {
gpio_matrix_out(p.invpin, SIG_IN_FUNC228_IDX, true /*inverted*/, false);
}
} else // elseif clause continues
#endif
if (mode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_DC)) {
// gpio_reset_pin may reset to input
pinMode(p.pin, OUTPUT);
if (p.invpin != UNUSED_PIN)
pinMode(p.invpin, OUTPUT);
}
#endif
#ifndef DISABLE_PROG
if (mode==TRACK_MODE_PROG) {
if (mode & TRACK_MODE_PROG) {
#else
if (false) {
#endif
// only allow 1 track to be prog
FOR_EACH_TRACK(t)
if (track[t]->getMode()==TRACK_MODE_PROG && t != trackToSet) {
if ( (track[t]->getMode() & TRACK_MODE_PROG) && t != trackToSet) {
track[t]->setPower(POWERMODE::OFF);
track[t]->setMode(TRACK_MODE_NONE);
track[t]->makeProgTrack(false); // revoke prog track special handling
streamTrackState(NULL,t);
streamTrackState(NULL,t);
}
track[trackToSet]->makeProgTrack(true); // set for prog track special handling
} else {
@@ -248,22 +253,25 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
}
track[trackToSet]->setMode(mode);
trackDCAddr[trackToSet]=dcAddr;
streamTrackState(NULL,trackToSet);
// When a track is switched, we must clear any side effects of its previous
// state, otherwise trains run away or just dont move.
// This can be done BEFORE the PWM-Timer evaluation (methinks)
if (!(mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX)) {
if (!(mode & TRACK_MODE_DC)) {
// DCC tracks need to have set the PWM to zero or they will not work.
track[trackToSet]->detachDCSignal();
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);
// BOOST:
// Leave it as is
// otherwise:
// EXT is a special case where the signal pin is
// turned off. So unless that is set, the signal
// pin should be turned on
if (!(mode & TRACK_MODE_BOOST))
track[trackToSet]->enableSignal(!(mode & TRACK_MODE_EXT));
#ifndef ARDUINO_ARCH_ESP32
// re-evaluate HighAccuracy mode
@@ -273,7 +281,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
// 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 (track[t]->getMode()==TRACK_MODE_DC || track[t]->getMode()==TRACK_MODE_DCX) {
if (track[t]->getMode() & TRACK_MODE_DC) {
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
@@ -281,7 +289,7 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
track[t]->trackPWM=false; // this track sure can not run with PWM
//DIAG(F("Track %c trackPWM 0 (not capable)"), t+'A');
}
} else if (track[t]->getMode()==TRACK_MODE_MAIN || track[t]->getMode()==TRACK_MODE_PROG) {
} else if (track[t]->getMode() & (TRACK_MODE_MAIN |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;
@@ -299,98 +307,135 @@ bool TrackManager::setTrackMode(byte trackToSet, TRACK_MODE mode, int16_t dcAddr
#else
// For ESP32 we just reinitialize the DCC Waveform
DCCWaveform::begin();
// setMode() again AFTER Waveform::begin() of ESP32 fixes INVERTED signal
track[trackToSet]->setMode(mode);
#endif
// This block must be AFTER the PWM-Timer modifications
if (mode==TRACK_MODE_DC || mode==TRACK_MODE_DCX) {
if (mode & TRACK_MODE_DC) {
// 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);
}
// 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);
// Turn off power if we changed the mode of this track
if (mode != oldmode)
track[trackToSet]->setPower(POWERMODE::OFF);
streamTrackState(NULL,trackToSet);
//DIAG(F("TrackMode=%d"),mode);
return true;
}
void TrackManager::applyDCSpeed(byte t) {
uint8_t speedByte=DCC::getThrottleSpeedByte(trackDCAddr[t]);
if (track[t]->getMode()==TRACK_MODE_DCX)
speedByte = speedByte ^ 128; // reverse direction bit
track[t]->setDCSignal(speedByte);
track[t]->setDCSignal(DCC::getThrottleSpeedByte(trackDCAddr[t]),
DCC::getThrottleFrequency(trackDCAddr[t]));
}
bool TrackManager::parseJ(Print *stream, int16_t params, int16_t p[])
bool TrackManager::parseEqualSign(Print *stream, int16_t params, int16_t p[])
{
if (params==0) { // <=> List track assignments
FOR_EACH_TRACK(t)
streamTrackState(stream,t);
return true;
}
p[0]-=HASH_KEYWORD_A; // convert A... to 0....
p[0]-="A"_hk; // 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>
if (params==2 && p[1]=="MAIN"_hk) // <= id MAIN>
return setTrackMode(p[0],TRACK_MODE_MAIN);
#ifndef DISABLE_PROG
if (params==2 && p[1]==HASH_KEYWORD_PROG) // <= id PROG>
if (params==2 && p[1]=="PROG"_hk) // <= id PROG>
return setTrackMode(p[0],TRACK_MODE_PROG);
#endif
if (params==2 && (p[1]==HASH_KEYWORD_OFF || p[1]==HASH_KEYWORD_NONE)) // <= id OFF> <= id NONE>
if (params==2 && (p[1]=="OFF"_hk || p[1]=="NONE"_hk)) // <= id OFF> <= id NONE>
return setTrackMode(p[0],TRACK_MODE_NONE);
if (params==2 && p[1]==HASH_KEYWORD_EXT) // <= id EXT>
if (params==2 && p[1]=="EXT"_hk) // <= id EXT>
return setTrackMode(p[0],TRACK_MODE_EXT);
#ifdef BOOSTER_INPUT
if (TRACK_MODE_BOOST != 0 && // compile time optimization
params==2 && p[1]=="BOOST"_hk) // <= id BOOST>
return setTrackMode(p[0],TRACK_MODE_BOOST);
#endif
if (params==2 && p[1]=="AUTO"_hk) // <= id AUTO>
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODE_AUTOINV);
if (params==3 && p[1]==HASH_KEYWORD_DC && p[2]>0) // <= id DC cab>
if (params==2 && p[1]=="INV"_hk) // <= id AUTO>
return setTrackMode(p[0], track[p[0]]->getMode() | TRACK_MODE_INV);
if (params==3 && p[1]=="DC"_hk && 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]);
if (params==3 && p[1]=="DCX"_hk && p[2]>0) // <= id DCX cab>
return setTrackMode(p[0],TRACK_MODE_DC|TRACK_MODE_INV,p[2]);
return false;
}
void TrackManager::streamTrackState(Print* stream, byte t) {
// null stream means send to commandDistributor for broadcast
if (track[t]==NULL) return;
auto format=F("");
switch(track[t]->getMode()) {
case TRACK_MODE_MAIN:
format=F("<= %c MAIN>\n");
break;
#ifndef DISABLE_PROG
case TRACK_MODE_PROG:
format=F("<= %c PROG>\n");
break;
#endif
case TRACK_MODE_NONE:
format=F("<= %c NONE>\n");
break;
case TRACK_MODE_EXT:
format=F("<= %c EXT>\n");
break;
case TRACK_MODE_DC:
format=F("<= %c DC %d>\n");
break;
case TRACK_MODE_DCX:
format=F("<= %c DCX %d>\n");
break;
default:
break; // unknown, dont care
const FSH* TrackManager::getModeName(TRACK_MODE tm) {
const FSH *modename=F("---");
if (tm & TRACK_MODE_MAIN) {
if(tm & TRACK_MODE_AUTOINV)
modename=F("MAIN A");
else if (tm & TRACK_MODE_INV)
modename=F("MAIN I>\n");
else
modename=F("MAIN");
}
if (stream) StringFormatter::send(stream,format,'A'+t,trackDCAddr[t]);
else CommandDistributor::broadcastTrackState(format,'A'+t,trackDCAddr[t]);
#ifndef DISABLE_PROG
else if (tm & TRACK_MODE_PROG)
modename=F("PROG");
#endif
else if (tm & TRACK_MODE_NONE)
modename=F("NONE");
else if(tm & TRACK_MODE_EXT)
modename=F("EXT");
else if(tm & TRACK_MODE_BOOST) {
if(tm & TRACK_MODE_AUTOINV)
modename=F("BOOST A");
else if (tm & TRACK_MODE_INV)
modename=F("BOOST I");
else
modename=F("BOOST");
}
else if (tm & TRACK_MODE_DC) {
if (tm & TRACK_MODE_INV)
modename=F("DCX");
else
modename=F("DC");
}
return modename;
}
// null stream means send to commandDistributor for broadcast
void TrackManager::streamTrackState(Print* stream, byte t) {
const FSH *format;
if (track[t]==NULL) return;
TRACK_MODE tm = track[t]->getMode();
if (tm & TRACK_MODE_DC)
format=F("<= %c %S %d>\n");
else
format=F("<= %c %S>\n");
const FSH *modename=getModeName(tm);
if (stream) { // null stream means send to commandDistributor for broadcast
StringFormatter::send(stream,format,'A'+t, modename, trackDCAddr[t]);
} else {
CommandDistributor::broadcastTrackState(format,'A'+t, modename, trackDCAddr[t]);
CommandDistributor::broadcastPower();
}
}
byte TrackManager::nextCycleTrack=MAX_TRACKS;
@@ -405,13 +450,13 @@ void TrackManager::loop() {
if (nextCycleTrack>lastTrack) nextCycleTrack=0;
if (track[nextCycleTrack]==NULL) return;
MotorDriver * motorDriver=track[nextCycleTrack];
bool useProgLimit=dontLimitProg? false: track[nextCycleTrack]->getMode()==TRACK_MODE_PROG;
bool useProgLimit=dontLimitProg ? false : (bool)(track[nextCycleTrack]->getMode() & TRACK_MODE_PROG);
motorDriver->checkPowerOverload(useProgLimit, nextCycleTrack);
}
MotorDriver * TrackManager::getProgDriver() {
FOR_EACH_TRACK(t)
if (track[t]->getMode()==TRACK_MODE_PROG) return track[t];
if (track[t]->getMode() & TRACK_MODE_PROG) return track[t];
return NULL;
}
@@ -419,64 +464,113 @@ MotorDriver * TrackManager::getProgDriver() {
std::vector<MotorDriver *>TrackManager::getMainDrivers() {
std::vector<MotorDriver *> v;
FOR_EACH_TRACK(t)
if (track[t]->getMode()==TRACK_MODE_MAIN) v.push_back(track[t]);
if (track[t]->getMode() & TRACK_MODE_MAIN) v.push_back(track[t]);
return v;
}
#endif
void TrackManager::setPower2(bool setProg,POWERMODE mode) {
if (!setProg) mainPowerGuess=mode;
FOR_EACH_TRACK(t) {
MotorDriver * driver=track[t];
if (!driver) continue;
switch (track[t]->getMode()) {
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_NONE:
break;
}
// Set track power for all tracks with this mode
void TrackManager::setTrackPower(TRACK_MODE trackmodeToMatch, POWERMODE powermode) {
bool didChange=false;
FOR_EACH_TRACK(t) {
MotorDriver *driver=track[t];
TRACK_MODE trackmodeOfTrack = driver->getMode();
if (trackmodeToMatch & trackmodeOfTrack) {
if (powermode != driver->getPower())
didChange=true;
if (powermode == POWERMODE::ON) {
if (trackmodeOfTrack & TRACK_MODE_DC) {
driver->setBrake(true); // DC starts with brake on
applyDCSpeed(t); // speed match DCC throttles
} else {
// toggle brake before turning power on - resets overcurrent error
// on the Pololu board if brake is wired to ^D2.
driver->setBrake(true);
driver->setBrake(false); // DCC runs with brake off
}
}
driver->setPower(powermode);
}
}
POWERMODE TrackManager::getProgPower() {
FOR_EACH_TRACK(t)
if (track[t]->getMode()==TRACK_MODE_PROG)
return track[t]->getPower();
return POWERMODE::OFF;
}
if (didChange)
CommandDistributor::broadcastPower();
}
// Set track power for this track, inependent of mode
void TrackManager::setTrackPower(POWERMODE powermode, byte t) {
MotorDriver *driver=track[t];
if (driver == NULL) { // track is not defined at all
DIAG(F("Error: Track %c does not exist"), t+'A');
return;
}
TRACK_MODE trackmode = driver->getMode();
POWERMODE oldpower = driver->getPower();
if (trackmode & TRACK_MODE_NONE) {
driver->setBrake(true); // Track is unused. Brake is good to have.
powermode = POWERMODE::OFF; // Track is unused. Force it to OFF
} else if (trackmode & TRACK_MODE_DC) { // includes inverted DC (called DCX)
if (powermode == POWERMODE::ON) {
driver->setBrake(true); // DC starts with brake on
applyDCSpeed(t); // speed match DCC throttles
}
} else /* MAIN PROG EXT BOOST */ {
if (powermode == POWERMODE::ON) {
// toggle brake before turning power on - resets overcurrent error
// on the Pololu board if brake is wired to ^D2.
driver->setBrake(true);
driver->setBrake(false); // DCC runs with brake off
}
}
driver->setPower(powermode);
if (oldpower != driver->getPower())
CommandDistributor::broadcastPower();
}
// returns state of the one and only prog track
POWERMODE TrackManager::getProgPower() {
FOR_EACH_TRACK(t)
if (track[t]->getMode() & TRACK_MODE_PROG)
return track[t]->getPower(); // optimize: there is max one prog track
return POWERMODE::OFF;
}
// returns on if all are on. returns off otherwise
POWERMODE TrackManager::getMainPower() {
POWERMODE result = POWERMODE::OFF;
FOR_EACH_TRACK(t) {
if (track[t]->getMode() & TRACK_MODE_MAIN) {
POWERMODE p = track[t]->getPower();
if (p == POWERMODE::OFF)
return POWERMODE::OFF; // done and out
if (p == POWERMODE::ON)
result = POWERMODE::ON;
}
}
return result;
}
bool TrackManager::getPower(byte t, char s[]) {
if (t > lastTrack)
return false;
if (track[t]) {
s[0] = track[t]->getPower() == POWERMODE::ON ? '1' : '0';
s[2] = t + 'A';
return true;
}
return false;
}
void TrackManager::reportObsoleteCurrent(Print* stream) {
// This function is for backward JMRI compatibility only
// It reports the first track only, as main, regardless of track settings.
// <c MeterName value C/V unit min max res warn>
#ifdef HAS_ENOUGH_MEMORY
int maxCurrent=track[0]->raw2mA(track[0]->getRawCurrentTripValue());
StringFormatter::send(stream, F("<c CurrentMAIN %d C Milli 0 %d 1 %d>\n"),
track[0]->raw2mA(track[0]->getCurrentRaw(false)), maxCurrent, maxCurrent);
track[0]->raw2mA(track[0]->getCurrentRaw(false)), maxCurrent, maxCurrent);
#else
(void)stream;
#endif
}
void TrackManager::reportCurrent(Print* stream) {
@@ -510,7 +604,7 @@ void TrackManager::setJoin(bool joined) {
#ifdef ARDUINO_ARCH_ESP32
if (joined) {
FOR_EACH_TRACK(t) {
if (track[t]->getMode()==TRACK_MODE_PROG) {
if (track[t]->getMode() & TRACK_MODE_PROG) {
tempProgTrack = t;
setTrackMode(t, TRACK_MODE_MAIN);
break;
@@ -538,3 +632,17 @@ bool TrackManager::isPowerOn(byte t) {
return true;
}
bool TrackManager::isProg(byte t) {
if (track[t]->getMode() & TRACK_MODE_PROG)
return true;
return false;
}
TRACK_MODE TrackManager::getMode(byte t) {
return (track[t]->getMode());
}
int16_t TrackManager::returnDCAddr(byte t) {
return (trackDCAddr[t]);
}

View File

@@ -1,6 +1,6 @@
/*
* © 2022 Chris Harlow
* © 2022 Harald Barth
* © 2022-2024 Harald Barth
* © 2023 Colin Murdoch
*
* All rights reserved.
@@ -39,10 +39,14 @@ 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;
// These constants help EXRAIL macros convert Track Power e.g. SET_POWER(A ON|OFF).
const byte TRACK_POWER_0=0, TRACK_POWER_OFF=0;
const byte TRACK_POWER_1=1, TRACK_POWER_ON=1;
class TrackManager {
public:
static void Setup(const FSH * shieldName,
MotorDriver * track0,
MotorDriver * track0=NULL,
MotorDriver * track1=NULL,
MotorDriver * track2=NULL,
MotorDriver * track3=NULL,
@@ -53,26 +57,32 @@ class TrackManager {
);
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();
#ifdef ARDUINO_ARCH_ESP32
static std::vector<MotorDriver *>getMainDrivers();
static std::vector<MotorDriver *>getMainDrivers();
#endif
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 void setTrackPower(POWERMODE mode, byte t);
static void setTrackPower(TRACK_MODE trackmode, POWERMODE powermode);
static void setMainPower(POWERMODE mode) {setTrackPower(TRACK_MODE_MAIN, mode);}
static void setProgPower(POWERMODE mode) {setTrackPower(TRACK_MODE_PROG, 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 bool parseEqualSign(Print * stream, int16_t params, int16_t p[]);
static void loop();
static POWERMODE getMainPower() {return mainPowerGuess;}
static POWERMODE getMainPower();
static POWERMODE getProgPower();
static inline POWERMODE getPower(byte t) { return track[t]->getPower(); }
static bool getPower(byte t, char s[]);
static void setJoin(bool join);
static bool isJoined() { return progTrackSyncMain;}
static inline bool isActive (byte tr) {
if (tr > lastTrack) return false;
return track[tr]->getMode() & (TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_BOOST|TRACK_MODE_EXT);}
static void setJoinRelayPin(byte joinRelayPin);
static void sampleCurrent();
static void reportGauges(Print* stream);
@@ -80,6 +90,10 @@ class TrackManager {
static void reportObsoleteCurrent(Print* stream);
static void streamTrackState(Print* stream, byte t);
static bool isPowerOn(byte t);
static bool isProg(byte t);
static TRACK_MODE getMode(byte t);
static int16_t returnDCAddr(byte t);
static const FSH* getModeName(TRACK_MODE Mode);
static int16_t joinRelay;
static bool progTrackSyncMain; // true when prog track is a siding switched to main
@@ -94,12 +108,11 @@ class TrackManager {
private:
static void addTrack(byte t, MotorDriver* driver);
static byte lastTrack;
static int8_t lastTrack;
static byte nextCycleTrack;
static POWERMODE mainPowerGuess;
static void applyDCSpeed(byte t);
static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC or TRACK_MODE_DCX
static int16_t trackDCAddr[MAX_TRACKS]; // dc address if TRACK_MODE_DC
#ifdef ARDUINO_ARCH_ESP32
static byte tempProgTrack; // holds the prog track number during join
#endif

View File

@@ -247,22 +247,23 @@ DCCTurntable::DCCTurntable(uint16_t id) : Turntable(id, TURNTABLE_DCC) {}
StringFormatter::send(stream, F("<i %d DCCTURNTABLE>\n"), _turntableData.id);
}
// EX-Turntable specific code for moving to the specified position
bool DCCTurntable::setPositionInternal(uint8_t position, uint8_t activity) {
// EX-Turntable specific code for moving to the specified position
bool DCCTurntable::setPositionInternal(uint8_t position, uint8_t activity) {
(void) activity;
#ifndef IO_NO_HAL
int16_t value = getPositionValue(position);
if (position == 0 || !value) return false; // Return false if it's not a valid position
// Set position via device driver
int16_t addr=value>>3;
int16_t subaddr=(value>>1) & 0x03;
bool active=value & 0x01;
_previousPosition = _turntableData.position;
_turntableData.position = position;
DCC::setAccessory(addr, subaddr, active);
int16_t value = getPositionValue(position);
if (position == 0 || !value) return false; // Return false if it's not a valid position
// Set position via device driver
int16_t addr=value>>3;
int16_t subaddr=(value>>1) & 0x03;
bool active=value & 0x01;
_previousPosition = _turntableData.position;
_turntableData.position = position;
DCC::setAccessory(addr, subaddr, active);
#else
(void)position;
(void)position;
#endif
return true;
}
return true;
}
#endif

View File

@@ -185,7 +185,7 @@ public:
for (Turntable *tto = _firstTurntable; tto != 0; tto = tto->_nextTurntable)
if (!tto->isHidden()) {
gotOne = true;
StringFormatter::send(stream, F("<i %d %d>\n"), tto->getId(), tto->getPosition());
StringFormatter::send(stream, F("<I %d %d>\n"), tto->getId(), tto->getPosition());
}
return gotOne;
}

View File

@@ -150,7 +150,6 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
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
@@ -188,6 +187,7 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
}
break;
case 'N': // Heartbeat (2), only send if connection completed by 'HU' message
sendIntro(stream);
StringFormatter::send(stream, F("*%d\n"), heartrateSent ? HEARTBEAT_SECONDS : HEARTBEAT_PRELOAD); // return timeout value
break;
case 'M': // multithrottle
@@ -195,7 +195,7 @@ void WiThrottle::parse(RingStream * stream, byte * cmdx) {
break;
case 'H': // send initial connection info after receiving "HU" message
if (cmd[1] == 'U') {
sendIntro(stream);
sendIntro(stream);
}
break;
case 'Q': //
@@ -496,16 +496,17 @@ void WiThrottle::getLocoCallback(int16_t locoid) {
TrackManager::setJoin(true); // <1 JOIN> so we can drive loco away
DIAG(F("LocoCallback commit success"));
stashStream->commit();
CommandDistributor::broadcastPower();
}
void WiThrottle::sendIntro(Print* stream) {
if (introSent) // sendIntro only once
return;
introSent=true;
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);
// set heartbeat to 2 seconds because we need to sync the metadata (1 second is too short!)
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);
// set heartbeat to 2 seconds because we need to sync the metadata (1 second is too short!)
StringFormatter::send(stream,F("*%d\nHMConnecting..\n"), HEARTBEAT_PRELOAD);
}
@@ -570,7 +571,7 @@ void WiThrottle::sendRoutes(Print* stream) {
void WiThrottle::sendFunctions(Print* stream, byte loco) {
int16_t locoid=myLocos[loco].cab;
int fkeys=29;
int fkeys=32; // upper limit (send functions 0 to 31)
myLocos[loco].functionToggles=1<<2; // F2 (HORN) is a non-toggle
#ifdef EXRAIL_ACTIVE
@@ -620,7 +621,7 @@ void WiThrottle::sendFunctions(Print* stream, byte loco) {
#endif
for(int fKey=0; fKey<fkeys; fKey++) {
int fstate=DCC::getFn(locoid,fKey);
int8_t fstate=DCC::getFn(locoid,fKey);
if (fstate>=0) StringFormatter::send(stream,F("M%cA%c%d<;>F%d%d\n"),myLocos[loco].throttle,LorS(locoid),locoid,fstate,fKey);
}
}

View File

@@ -74,25 +74,39 @@ class NetworkClient {
public:
NetworkClient(WiFiClient c) {
wifi = c;
};
bool ok() {
return (inUse && wifi.connected());
};
bool recycle(WiFiClient c) {
if (inUse == true) return false;
// return false here until we have
// implemented a LRU timer
// if (LRU too recent) return false;
return false;
wifi = c;
inUse = true;
};
bool active(byte clientId) {
if (!inUse)
return false;
if(!wifi.connected()) {
DIAG(F("Remove client %d"), clientId);
CommandDistributor::forget(clientId);
wifi.stop();
inUse = false;
return false;
}
return true;
}
bool recycle(WiFiClient c) {
if (wifi == c) {
if (inUse == true)
DIAG(F("WARNING: Duplicate"));
else
DIAG(F("Returning"));
inUse = true;
return true;
}
if (inUse == false) {
wifi = c;
inUse = true;
return true;
}
return false;
};
WiFiClient wifi;
bool inUse = true;
private:
bool inUse;
};
static std::vector<NetworkClient> clients; // a list to hold all clients
@@ -150,6 +164,8 @@ bool WifiESP::setup(const char *SSid,
if (haveSSID && havePassword && !forceAP) {
WiFi.setHostname(hostname); // Strangely does not work unless we do it HERE!
WiFi.mode(WIFI_STA);
WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); // Scan all channels so we find strongest
// (default in Wifi library is first match)
#ifdef SERIAL_BT_COMMANDS
WiFi.setSleep(true);
#else
@@ -163,7 +179,9 @@ bool WifiESP::setup(const char *SSid,
delay(500);
}
if (WiFi.status() == WL_CONNECTED) {
DIAG(F("Wifi STA IP %s"),WiFi.localIP().toString().c_str());
// DIAG(F("Wifi STA IP %s"),WiFi.localIP().toString().c_str());
DIAG(F("Wifi in STA mode"));
LCD(7, F("IP: %s"), WiFi.localIP().toString().c_str());
wifiUp = true;
} else {
DIAG(F("Could not connect to Wifi SSID %s"),SSid);
@@ -188,7 +206,7 @@ bool WifiESP::setup(const char *SSid,
if (!haveSSID || forceAP) {
// prepare all strings
String strSSID(forceAP ? SSid : "DCCEX_");
String strPass(forceAP ? password : "PASS_");
String strPass( (forceAP && havePassword) ? password : "PASS_");
if (!forceAP) {
String strMac = WiFi.macAddress();
strMac.remove(0,9);
@@ -209,8 +227,13 @@ bool WifiESP::setup(const char *SSid,
if (WiFi.softAP(strSSID.c_str(),
havePassword ? password : strPass.c_str(),
channel, false, 8)) {
DIAG(F("Wifi AP SSID %s PASS %s"),strSSID.c_str(),havePassword ? password : strPass.c_str());
DIAG(F("Wifi AP IP %s"),WiFi.softAPIP().toString().c_str());
// DIAG(F("Wifi AP SSID %s PASS %s"),strSSID.c_str(),havePassword ? password : strPass.c_str());
DIAG(F("Wifi in AP mode"));
LCD(5, F("Wifi: %s"), strSSID.c_str());
if (!havePassword)
LCD(6, F("PASS: %s"),strPass.c_str());
// DIAG(F("Wifi AP IP %s"),WiFi.softAPIP().toString().c_str());
LCD(7, F("IP: %s"),WiFi.softAPIP().toString().c_str());
wifiUp = true;
APmode = true;
} else {
@@ -276,37 +299,26 @@ void WifiESP::loop() {
// really no good way to check for LISTEN especially in AP mode?
wl_status_t wlStatus;
if (APmode || (wlStatus = WiFi.status()) == WL_CONNECTED) {
// loop over all clients and remove inactive
for (clientId=0; clientId<clients.size(); clientId++){
// check if client is there and alive
if(clients[clientId].inUse && !clients[clientId].wifi.connected()) {
DIAG(F("Remove client %d"), clientId);
CommandDistributor::forget(clientId);
clients[clientId].wifi.stop();
clients[clientId].inUse = false;
//Do NOT clients.erase(clients.begin()+clientId) as
//that would mix up clientIds for later.
}
}
if (server->hasClient()) {
WiFiClient client;
while (client = server->available()) {
for (clientId=0; clientId<clients.size(); clientId++){
if (clients[clientId].recycle(client)) {
DIAG(F("Recycle client %d %s"), clientId, client.remoteIP().toString().c_str());
DIAG(F("Recycle client %d %s:%d"), clientId, client.remoteIP().toString().c_str(),client.remotePort());
break;
}
}
if (clientId>=clients.size()) {
NetworkClient nc(client);
clients.push_back(nc);
DIAG(F("New client %d, %s"), clientId, client.remoteIP().toString().c_str());
DIAG(F("New client %d, %s:%d"), clientId, client.remoteIP().toString().c_str(),client.remotePort());
}
}
}
// loop over all connected clients
// this removes as a side effect inactive clients when checking ::active()
for (clientId=0; clientId<clients.size(); clientId++){
if(clients[clientId].ok()) {
if(clients[clientId].active(clientId)) {
int len;
if ((len = clients[clientId].wifi.available()) > 0) {
// read data from client
@@ -344,7 +356,7 @@ void WifiESP::loop() {
}
// buffer filled, end with '\0' so we can use it as C string
buffer[count]='\0';
if((unsigned int)clientId <= clients.size() && clients[clientId].ok()) {
if((unsigned int)clientId <= clients.size() && clients[clientId].active(clientId)) {
if (Diag::CMD || Diag::WITHROTTLE)
DIAG(F("SEND %d:%s"), clientId, buffer);
clients[clientId].wifi.write(buffer,count);
@@ -377,8 +389,9 @@ void WifiESP::loop() {
// prio task. On core1 this is not a problem
// as there the wdt is disabled by the
// arduio IDE startup routines.
if (xPortGetCoreID() == 0)
if (xPortGetCoreID() == 0) {
feedTheDog0();
yield();
yield();
}
}
#endif //ESP32

View File

@@ -68,7 +68,9 @@ Stream * WifiInterface::wifiStream;
#define NUM_SERIAL 3
#define SERIAL1 Serial3
#define SERIAL3 Serial5
#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE) || defined(ARDUINO_NUCLEO_F412ZG)
#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) \
|| defined(ARDUINO_NUCLEO_F446ZE) || defined(ARDUINO_NUCLEO_F412ZG) \
|| defined(ARDUINO_NUCLEO_F439ZI)
#define NUM_SERIAL 2
#define SERIAL1 Serial6
#else
@@ -201,17 +203,19 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password,
// Display the AT version information
StringFormatter::send(wifiStream, F("AT+GMR\r\n"));
if (checkForOK(2000, F("AT version:"), true, false)) {
char version[] = "0.0.0.0";
for (int i=0; i<8;i++) {
char version[] = "0.0.0.0-xxx";
for (int i=0; i<11;i++) {
while(!wifiStream->available());
version[i]=wifiStream->read();
StringFormatter::printEscape(version[i]);
if ((version[0] == '0') ||
(version[0] == '2' && version[2] == '0') ||
(version[0] == '2' && version[2] == '2' && version[4] == '0' && version[6] == '0')) {
SSid = F("DCCEX_SAYS_BROKEN_FIRMWARE");
forceAP = true;
}
}
if ((version[0] == '0') ||
(version[0] == '2' && version[2] == '0') ||
(version[0] == '2' && version[2] == '2' && version[4] == '0' && version[6] == '0'
&& version[7] == '-' && version[8] == 'd' && version[9] == 'e' && version[10] == 'v')) {
DIAG(F("You need to up/downgrade the ESP firmware"));
SSid = F("UPDATE_ESP_FIRMWARE");
forceAP = true;
}
}
checkForOK(2000, true, false);

View File

@@ -167,6 +167,14 @@ The configuration file for DCC-EX Command Station
// * #define SCROLLMODE 2 is by row (move up 1 row at a time).
#define SCROLLMODE 1
// In order to avoid wasting memory the current scroll buffer is limited
// to 8 lines. Some users wishing to display additional information
// such as TrackManager power states have requested additional rows aware
// of the warning that this will take extra RAM. if you wish to include additional rows
// uncomment the following #define and set the number of lines you need.
//#define MAX_CHARACTER_ROWS 12
/////////////////////////////////////////////////////////////////////////////////////
// DISABLE EEPROM
//
@@ -191,6 +199,18 @@ The configuration file for DCC-EX Command Station
//
// #define DISABLE_PROG
/////////////////////////////////////////////////////////////////////////////////////
// DISABLE / ENABLE VDPY
//
// The Virtual display "VDPY" feature is by default enabled everywhere
// but on Uno and Nano. If you think you can fit it (for example
// having disabled some of the features above) you can enable it with
// ENABLE_VDPY. You can even disable it on all other CPUs with
// DISABLE_VDPY
//
// #define DISABLE_VDPY
// #define ENABLE_VDPY
/////////////////////////////////////////////////////////////////////////////////////
// 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
@@ -202,6 +222,14 @@ The configuration file for DCC-EX Command Station
// 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.
/////////////////////////////////////////////////////////////////////////////////////
// Some newer 32bit microcontrollers boot very quickly, so powering on I2C and other
// peripheral devices at the same time may result in the CommandStation booting too
// quickly to detect them.
// To work around this, uncomment the STARTUP_DELAY line below and set a value in
// milliseconds that works for your environment, default is 3000 (3 seconds).
// #define STARTUP_DELAY 3000
/////////////////////////////////////////////////////////////////////////////////////
//
// DEFINE TURNOUTS/ACCESSORIES FOLLOW NORM RCN-213
@@ -266,6 +294,12 @@ The configuration file for DCC-EX Command Station
//
//#define SERIAL_BT_COMMANDS
// BOOSTER PIN INPUT ON ESP32
// On ESP32 you have the possibility to define a pin as booster input
// Arduio pin D2 is GPIO 26 on ESPDuino32
//
//#define BOOSTER_INPUT 26
// SABERTOOTH
//
// This is a very special option and only useful if you happen to have a

View File

@@ -219,11 +219,10 @@
// 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)
#if defined(DISABLE_DIAG) && defined(DISABLE_EEPROM) && defined(DISABLE_PROG)
#warning you have sacrificed DIAG for HAL
#else
#define IO_NO_HAL
#endif
#define IO_NO_HAL // HAL too big whatever you disable otherwise
#ifndef ENABLE_VDPY
#define DISABLE_VDPY
#endif
#endif
#if __has_include ( "myAutomation.h")

View File

@@ -24,6 +24,8 @@
//#include "IO_TouchKeypad.h // Touch keypad with 16 keys
//#include "IO_EXTurntable.h" // Turntable-EX turntable controller
//#include "IO_EXFastClock.h" // FastClock driver
//#include "IO_PCA9555.h" // 16-bit I/O expander (NXP & Texas Instruments).
//#include "IO_I2CDFPlayer.h" // DFPlayer over I2C
//==========================================================================
// The function halSetup() is invoked from CS if it exists within the build.
@@ -233,6 +235,31 @@ void halSetup() {
// DFPlayer::create(10000, 10, Serial1);
//=======================================================================
// Play mp3 files from a Micro-SD card, using a DFPlayer MP3 Module on a SC16IS750/SC16IS752 I2C UART
//=======================================================================
// DFPlayer via NXP SC16IS752 I2C Dual UART.
// I2C address range 0x48 - 0x57
//
// Generic format:
// I2CDFPlayer::create(1st vPin, vPins, I2C address, xtal);
// Parameters:
// 1st vPin : First virtual pin that EX-Rail can control to play a sound, use PLAYSOUND command (alias of ANOUT)
// vPins : Total number of virtual pins allocated (1 vPin is supported currently)
// 1st vPin for UART 0
// I2C Address : I2C address of the serial controller, in 0x format
// xtal : 0 for 1.8432Mhz, 1 for 14.7456Mhz
//
// The vPin is also a pin that can be read with the WAITFOR(vPin) command indicating if the DFPlayer has finished playing a track
//
// I2CDFPlayer::create(10000, 1, 0x48, 1);
//
// Configuration example on a multiplexer
// I2CDFPlayer::create(10000, 1, {I2CMux_0, SubBus_0, 0x48}, 1);
//=======================================================================
// 16-pad capacitative touch key pad based on TP229 IC.
//=======================================================================
@@ -284,12 +311,13 @@ void halSetup() {
//=======================================================================
// The parameters are:
// firstVpin = First available Vpin to allocate
// numPins= Number of Vpins to allocate, can be either 1 or 2
// i2cAddress = Available I2C address (default 0x70)
// numPins= Number of Vpins to allocate, can be either 1 to 3
// i2cAddress = Available I2C address (default 0x67)
//RotaryEncoder::create(firstVpin, numPins, i2cAddress);
//RotaryEncoder::create(700, 1, 0x70);
//RotaryEncoder::create(701, 2, 0x71);
//RotaryEncoder::create(700, 1, 0x67);
//RotaryEncoder::create(700, 2, 0x67);
//RotaryEncoder::create(700, 3, 0x67);
//=======================================================================
// The following directive defines an EX-FastClock instance.

View File

@@ -12,7 +12,6 @@
default_envs =
mega2560
uno
mega328
unowifiR2
nano
samd21-dev-usb
@@ -149,10 +148,7 @@ build_flags =
platform = atmelavr
board = uno
framework = arduino
lib_deps =
${env.lib_deps}
arduino-libraries/Ethernet
SPI
lib_deps = ${env.lib_deps}
monitor_speed = 115200
monitor_echo = yes
build_flags = -mcall-prologues
@@ -165,6 +161,7 @@ framework = arduino
lib_deps = ${env.lib_deps}
monitor_speed = 115200
monitor_echo = yes
build_flags = -mcall-prologues
[env:ESP32]
platform = espressif32

View File

@@ -3,7 +3,86 @@
#include "StringFormatter.h"
#define VERSION "5.1.10"
#define VERSION "5.2.41"
// 5.2.41 - Update rotary encoder default address to 0x67
// 5.2.40 - Allow no shield
// 5.2.39 - Functions for DC frequency: Use func up to F31
// 5.2.38 - Exrail MESSAGE("text") to send a user message to all
// connected throttles (uses <m "text"> and withrottle Hmtext.
// 5.2.37 - Bugfix ESP32: Use BOOSTER_INPUT define
// 5.2.36 - Variable frequency for DC mode
// 5.2.35 - Bugfix: Make DCC Extended Accessories follow RCN-213
// 5.2.34 - <A address aspect> Command fopr DCC Extended Accessories
// - Exrail ASPECT(address,aspect) for above.
// - EXRAIL DCCX_SIGNAL(Address,redAspect,amberAspect,greenAspect)
// - Exrail intercept <A ...> for DCC Signals.
// 5.2.33 - Exrail CONFIGURE_SERVO(vpin,pos1,pos2,profile)
// 5.2.32 - Railcom Cutout (Initial trial Mega2560 only)
// 5.2.31 - Exrail JMRI_SENSOR(vpin [,count]) creates <S> types.
// 5.2.30 - Bugfix: WiThrottle sendIntro after initial N message as well
// 5.2.29 - Added IO_I2CDFPlayer.h to support DFPLayer over I2C connected to NXP SC16IS750/SC16IS752 (currently only single UART for SC16IS752)
// - Added enhanced IO_I2CDFPLayer enum commands to EXRAIL2.h
// - Added PLAYSOUND alias of ANOUT to EXRAILMacros.h
// - Added UART detection to I2CManager.cpp
// 5.2.28 - ESP32: Can all Wifi channels.
// - ESP32: Only write Wifi password to display if it is a well known one
// 5.2.27 - Bugfix: IOExpander memory allocation
// 5.2.26 - Silently ignore overridden HAL defaults
// - include HAL_IGNORE_DEFAULTS macro in EXRAIL
// 5.2.25 - Fix bug causing <X> after working <D commands
// 5.2.24 - Exrail macro asserts to catch
// : duplicate/missing automation/route/sequence/call ids
// : latches and reserves out of range
// : speeds out of range
// 5.2.23 - KeywordHasher _hk (no functional change)
// 5.2.22 - Bugfixes: Empty turnout descriptions ok; negative route numbers valid.
// 5.2.21 - Add STARTUP_DELAY config option to delay CS bootup
// 5.2.20 - Check return of Ethernet.begin()
// 5.2.19 - ESP32: Determine if the RMT hardware can handle DCC
// 5.2.18 - Display network IP fix
// 5.2.17 - ESP32 simplify network logic
// 5.2.16 - Bugfix to allow for devices using the EX-IOExpander protocol to have no analogue or no digital pins
// 5.2.15 - move call to CommandDistributor::broadcastPower() into the TrackManager::setTrackPower(*) functions
// - add repeats to function packets that are not reminded in accordance with accessory packets
// 5.2.14 - Reminder window DCC packet optimization
// - Optional #define DISABLE_FUNCTION_REMINDERS
// 5.2.13 - EXRAIL STEALTH
// 5.2.12 - ESP32 add AP mode LCD messages with SSID/PW for
// - STM32 change to UID_BASE constants in DCCTimerSTM32 rather than raw hex addresses for UID registers
// - STM32 extra UART/USARTs for larger Nucleo models
// 5.2.11 - Change from TrackManager::returnMode to TrackManager::getMode
// 5.2.10 - Include trainbrains.eu block unoccupancy driver
// - include IO_PCA9555
// 5.2.9 - Bugfix LCD startup with no LCD, uses <@
// 5.2.9 - EXRAIL STASH feature
// 5.2.8 - Bugfix: Do not turn off all tracks on change
// give better power messages
// 5.2.7 - Bugfix: EXRAIL ling segment
// - Bugfix: Back out wrongly added const
// - Bugfix ESP32: Do not inverse DCX direction signal twice
// 5.2.6 - Trackmanager broadcast power state on track mode change
// 5.2.5 - Trackmanager: Do not treat TRACK_MODE_ALL as TRACK_MODE_DC
// 5.2.4 - LCD macro will not do diag if that duplicates @ to same target.
// - Added ROUTE_DISABLED macro in EXRAIL
// 5.2.3 - Bugfix: Catch stange input to parser
// 5.2.2 - Added option to allow MAX_CHARACTER_ROWS to be defined in config.h
// 5.2.1 - Trackmanager rework for simpler structure
// 5.2.0 - ESP32: Autoreverse and booster mode support
// 5.1.21 - EXRAIL invoke multiple ON handlers for same event
// 5.1.20 - EXRAIL Tidy and ROUTE_STATE, ROUTE_CAPTION
// 5.1.19 - Only flag 2.2.0.0-dev as broken, not 2.2.0.0
// 5.1.18 - TURNOUTL bugfix
// 5.1.17 - Divide out C for config and D for diag commands
// 5.1.16 - Remove I2C address from EXTT_TURNTABLE macro to work with MUX, requires separate HAL macro to create
// 5.1.15 - LCC/Adapter support and Exrail feature-compile-out.
// 5.1.14 - Fixed IFTTPOSITION
// 5.1.13 - Changed turntable broadcast from i to I due to server string conflict
// 5.1.12 - Added Power commands <0 A> & <1 A> etc. and update to <=>
// Added EXRAIL SET_POWER(track, ON/OFF)
// Fixed a problem whereby <1 MAIN> also powered on PROG track
// Added functions to TrackManager.cpp to allow UserAddin code for power display on OLED/LCD
// Added - returnMode(byte t), returnDCAddr(byte t) & getModeName(byte Mode)
// 5.1.11 - STM32F4xx revised I2C clock setup, no correctly sets clock and has fully variable frequency selection
// 5.1.10 - STM32F4xx DCCEXanalogWrite to handle PWM generation for TrackManager DC/DCX
// - STM32F4xx DCC 58uS timer now using non-PWM output timers where possible
// - ESP32 brakeCanPWM check now detects UNUSED_PIN