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

Compare commits

...

245 Commits

Author SHA1 Message Date
Ash-4
3bf1fe6513 PCF8574 output pin initialization parameter 2025-05-02 23:55:20 -05:00
Ash-4
d7e7abb4c5 PCF8574 output pin initialization parameter 2025-05-02 23:51:15 -05:00
Ash-4
efe1b48c8a PCF8574 output pin initialization parameter 2025-05-01 23:18:07 -05:00
Asbelos
54f8aaf116 5.5.25 IO_Bitmap 2025-05-01 09:29:05 +01:00
Asbelos
fefc4c6035 5.5.24 SensorCAM i2c. 2025-05-01 07:18:09 +01:00
Asbelos
6e5668c258 5.5.23
Reminder loop idle optimisation
2025-04-29 15:24:30 +01:00
Asbelos
a623a79c6a Reminder idle reduction 2025-04-29 15:09:28 +01:00
Asbelos
b243ba1784 5.5.22 2025-04-23 12:51:15 +01:00
Asbelos
2e51dc73fd CV20 and 5ms queue 2025-04-23 09:12:57 +01:00
Asbelos
d7685cb732 ESP32 IDF version check 2025-04-23 08:54:44 +01:00
Asbelos
8ba358465b SerialManager input overflow check 2025-04-23 08:53:38 +01:00
Asbelos
3d272a8b1e Join/power order change from master 2025-04-23 08:49:58 +01:00
Harald Barth
83a5c52a0d build for relevant platforms: Mega, ESP32, Nucleos 2025-03-23 10:51:07 +01:00
Harald Barth
45af57ebf2 version 5.5.21 2025-03-23 09:51:51 +01:00
Harald Barth
3f8ecf2a52 Revert "Merge branch 'devel-Ash-F439sync' into devel"
This reverts commit 3d794c59d8, reversing
changes made to 84918cbf36.
2025-03-23 09:49:54 +01:00
Asbelos
764639ed79 EXRAIL Assert SET/RESET fix 2025-03-22 23:58:54 +00:00
Asbelos
e56e4826ec Railcom Collector interface 2025-03-19 18:45:20 +00:00
Asbelos
3aa5cbcdfc Squashed commit of the following:
commit ec4c6b9c02
Author: Asbelos <asbelos@btinternet.com>
Date:   Wed Mar 5 00:30:13 2025 +0000

    Cleanup to new structure

commit 620ad6275b
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Feb 28 01:16:03 2025 +0000

    Railcom3
2025-03-19 18:06:02 +00:00
Asbelos
6d8ca67a2b STASH upgrade 2025-03-12 11:14:49 +00:00
Asbelos
5d18c910fa More EXRAIL asserts 2025-03-10 10:34:21 +00:00
Asbelos
a4c71889c6 Squashed commit of the following:
commit 5995f62456c7a614bb4607928e78bfc9f907af0d
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Mar 8 17:21:11 2025 +0000

    5.5.17 exrail asserts

commit d6fed968a220a0b4328e60390489f38a544831bc
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Mar 8 17:09:26 2025 +0000

    Additional asserts in EXRAIL
2025-03-08 17:21:49 +00:00
peteGSX
cb24a4dec7 Fix gitignore for docs 2025-03-08 09:14:12 +10:00
peteGSX
cda3b3ca1c Fix workflow 2025-03-08 08:50:05 +10:00
peteGSX
ed21284930 Add requirements.txt 2025-03-08 08:47:38 +10:00
peteGSX
6d9951c871 Fix workflow branch 2025-03-08 08:45:30 +10:00
peteGSX
56add464ac Add EXRAIL docs and workflow 2025-03-08 08:44:34 +10:00
Ash-4
3d794c59d8 Merge branch 'devel-Ash-F439sync' into devel 2025-03-06 23:05:32 -06:00
Asbelos
84918cbf36 DOXYGEN in Exrail 2025-03-05 01:32:57 +00:00
pmantoine
0294409214 STM32 update platformio.ini for F439ZI 2025-03-03 11:18:31 +11:00
Harald Barth
2a007f99dd version 5.5.15 2025-03-02 22:44:01 +01:00
Harald Barth
3095de4672 Merge branch 'devel-more-ether' into devel 2025-03-02 22:19:09 +01:00
Asbelos
dcfb3f061d Squashed commit of the following:
commit 87d0a94443a830510923829fe87e89442c7f3bbe
Author: Asbelos <asbelos@btinternet.com>
Date:   Sun Feb 16 14:40:09 2025 +0000

    Update version.h

commit 8c051710c6
Author: Asbelos <asbelos@btinternet.com>
Date:   Sun Feb 16 10:57:54 2025 +0000

    Now working

commit 6095bf2b3b
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Feb 15 01:06:55 2025 +0000

    Untested dcc q implementation

commit 52ecab10a7
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Feb 15 00:22:50 2025 +0000

    Initial dcc queue manager
2025-02-16 14:40:57 +00:00
Ash-4
7f488de06e Nucleo-F4 Timer sync F439 build flag 2025-02-11 01:09:11 -06:00
pmantoine
c99eac6ada STM32 platformio.ini PeripheralPins update 2025-02-11 11:47:58 +11:00
pmantoine
ed69a51e97 STM32 platformio.ini update for PeripheralPins 2025-02-11 11:46:48 +11:00
pmantoine
f2a7577313 Update STM32 core to v19.0.0 for PeripheralPins 2025-02-08 16:46:59 +08:00
Ash-4
56a339a598 Nucleo-F4 Timer Sync - speed fix 2025-02-01 17:28:50 -06:00
Ash-4
a3bd5ac86f DC mode timer sync STM32 2025-01-29 00:54:16 -06:00
Ash-4
da66469faa DC mode timer sync STM32 2025-01-29 00:34:45 -06:00
Ash-4
5d1b3a7a03 DC mode timer sync STM32 and JL command 2025-01-28 16:06:08 -06:00
Asbelos
f3b87877ef 5.5.12 Websockets (wifi) 2025-01-23 12:18:45 +00:00
Asbelos
07691e3985 Squashed commit of the following:
commit 04b8e20773
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Jan 18 10:13:32 2025 +0000

    Fix websocket binary mask issue

    Includes <D WEBSOCKET ON>

commit 5600382ae4
Merge: 5941866 137008c
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Jan 18 08:58:30 2025 +0000

    Merge branch 'devel' into devel-websockets

commit 59418668e2
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Jan 17 13:16:29 2025 +0000

    WIfiESP websock fix

commit c4e2146bd1
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Jan 16 08:37:38 2025 +0000

    websockets broadcast

commit 2591d100ca
Author: Asbelos <asbelos@btinternet.com>
Date:   Wed Jan 15 19:38:41 2025 +0000

    browser barf on \n

commit 67f836e886
Author: Asbelos <asbelos@btinternet.com>
Date:   Wed Jan 15 09:44:04 2025 +0000

    5.5.5 + websockets

    Ethernet not yet included
2025-01-23 10:46:39 +00:00
Asbelos
d14aa46d51 5.5.11 2025-01-21 10:45:56 +00:00
Asbelos
2b50e31e50 Merge branch 'master' into devel 2025-01-21 10:38:48 +00:00
Asbelos
ee8f6eea1f CamParser fix 2025-01-21 10:29:26 +00:00
Asbelos
fb6070784e changeFn fix 2025-01-21 10:18:09 +00:00
Asbelos
2f1d5b993c revert gitignore 2025-01-21 10:00:26 +00:00
Asbelos
9054d8d9f5 Merge branch 'master-fn31' 2025-01-21 09:35:58 +00:00
Harald Barth
865f75dda4 version 5.4.2 2025-01-20 22:41:47 +01:00
Harald Barth
b40fa779a6 revert part of commit 3c725a which did fix bug but reverse direction 2025-01-20 22:40:43 +01:00
Asbelos
2115ada2a1 5.4.2 bugfix fn31 flip 2025-01-20 20:03:21 +00:00
kempe63
58b180603a IO_I2CDFPlayer.h update and test
- Test: IO_I2CDFPlayer.h inserted 10mS deleay in Init_SC16IS752() just after soft-reset for board with 1.8432 Mhz xtal
- IO_I2CDFPlayer.h: fixed 2 compiler errors as the compilers are getting stricter
2025-01-20 19:10:20 +00:00
Asbelos
95d90aa337 5.5.8 Cam parser cleanup 2025-01-19 13:00:34 +00:00
Harald Barth
137008ceb3 version 5.5.7 2025-01-17 19:28:33 +01:00
Harald Barth
4ec9a62ab6 ESP32 bugfix packet buffer race (devel branch) 2025-01-17 19:25:24 +01:00
Harald Barth
830de850a9 version 5.4.1 2025-01-17 19:14:32 +01:00
Harald Barth
c28965c58d ESP32 bugfix packet buffer race 2025-01-17 19:12:11 +01:00
Asbelos
08076443cb ERSP32 Build fix 2025-01-14 09:12:50 +00:00
Asbelos
748ddcde8c Railcom Implementation
HAL driver, Railcom interpreter, block management, <r command.
See release notes
2025-01-13 14:40:59 +00:00
Asbelos
dc84f560e6 Version 5.5.4 2025-01-12 20:39:02 +00:00
Asbelos
680f765775 RAILCOM cutout (mega) 2025-01-12 20:37:39 +00:00
Asbelos
7e8841611d Split ESP32 waveform code 2025-01-12 15:12:02 +00:00
Asbelos
2503158e32 EXRAIL upgrades
ESTOPALL, XPOM
ONBLOCKENTER, ONBLOCKEXIT
Bug fixes for direction/speed sync
2025-01-12 13:24:12 +00:00
Asbelos
f031add7a0 DCC time broadcast and DS1307 clock chip 2025-01-12 11:47:43 +00:00
Harald Barth
0476b9c1d8 Merge branch 'master' of https://github.com/DCC-EX/CommandStation-EX 2025-01-10 20:16:37 +01:00
Harald Barth
ba9ca1ccad sha 2025-01-10 20:15:20 +01:00
Asbelos
32066a3bfa Momentum notes 2025-01-10 11:47:41 +00:00
Asbelos
e20935848f Momentum 2025-01-10 11:27:47 +00:00
Harald Barth
6f0ff49945 tag devel 2025-01-09 21:44:22 +01:00
Harald Barth
c389fe9d3b tag 2025-01-09 21:42:01 +01:00
Harald Barth
79c30ec516 For 5.4.0: reduce number of compile targets 2025-01-09 21:35:52 +01:00
Harald Barth
147fe15e04 version 5.2.96 2025-01-09 20:41:46 +01:00
Harald Barth
b5491f9b52 EXRAIL additions XFWD() and XREV() 2025-01-09 20:40:07 +01:00
Harald Barth
6f1c7a9e98 Documentation improvements in config.example.h 2025-01-05 20:18:44 +01:00
Harald Barth
42986c3b2d 5.2.95 release candidate for 5.4 2025-01-02 19:49:22 +01:00
Harald Barth
c1046ddcc0 Merge branch 'master-merge' into devel-merge 2025-01-02 19:10:12 +01:00
Harald Barth
818240b349 version tag 2024-12-28 15:46:32 +01:00
Harald Barth
3c725afab4 Less confusion and simpler code around the RCN213 defines 2024-12-28 15:45:27 +01:00
Harald Barth
13488e1e93 version tag 2024-12-22 23:22:24 +01:00
Harald Barth
6cc3b4c6bf Merge branch 'devel-esp32-progfix' into devel 2024-12-22 23:14:12 +01:00
Harald Barth
43fe772661 remove diag 2024-12-22 23:13:53 +01:00
Harald Barth
cafd53a0e5 clear progTrackSyncMain (join flag) when prog track is removed 2nd fix 2024-12-22 23:12:45 +01:00
Harald Barth
d4a99b5db5 version 5.2.93 2024-12-22 13:59:21 +01:00
Harald Barth
3ead534c81 Merge branch 'devel-esp32-progfix' into devel 2024-12-22 13:57:28 +01:00
Harald Barth
84bc098157 seperate out the templates that make it possible to use bitwise operations on enums 2024-12-21 16:08:57 +01:00
Harald Barth
8329fd83ce clear progTrackSyncMain (join flag) when prog track is removed 2024-12-21 15:42:15 +01:00
Harald Barth
4f16091670 take whole if clause out when DISABLE_PROG is active 2024-12-21 15:19:23 +01:00
Asbelos
377f10e1c5 5.2.92 2024-12-19 13:19:34 +00:00
Asbelos
016a20259a FADE fix and Track id diag. 2024-12-19 12:06:03 +00:00
pmantoine
c93dd75323 Upgrade ststm32 to v18.0.0 for PeripheralPins issue 2024-12-12 15:18:37 +08:00
Harald Barth
519cabffb6 version tag 2024-12-05 22:10:55 +01:00
Harald Barth
aa4306123d more explaining 2024-12-05 22:10:29 +01:00
Harald Barth
ccbff56355 rename to STM32lwipopts.h.copyme because of #include hell 2024-12-05 22:04:44 +01:00
Harald Barth
e0aa16ff2c adjust max default TCP clients to 9 because of bug in LwIP 2024-12-05 22:03:55 +01:00
pmantoine
6b0dc272ea Fixes for Mega, HTONS/L macro 2024-11-27 19:59:15 +08:00
Asbelos
14724aeb2a Nweopixel overlap check 2024-11-24 14:47:19 +00:00
Harald Barth
9602c32ea7 mDNS malloc error fix 2024-11-16 20:50:25 +01:00
pmantoine
8f48e2ed94 Bugfix EXRAIL EXTT_TURNTABLE description now optional 2024-11-11 09:06:56 +08:00
Harald Barth
1235c288dc make it compile on all default platforms 2024-11-10 21:11:40 +01:00
Harald Barth
001c4664c1 as MDNS_Generic does not work and we now have our own, remove it 2024-11-10 20:32:06 +01:00
Harald Barth
9786ea9b3a fix compiler warnings 2024-11-10 20:25:20 +01:00
Harald Barth
6a35daab6b remove dead code 2024-11-10 20:20:25 +01:00
Harald Barth
afff10df28 add copyright notices 2024-11-10 20:15:45 +01:00
Harald Barth
b9ce166028 remove everything that are not our changes 2024-11-10 20:13:40 +01:00
Harald Barth
0a96320fd0 tidy up values into name, serviceName, serviceProto and packet generation 2024-11-10 19:48:52 +01:00
Harald Barth
9a6e1707e7 add own mDNS files 2024-11-10 11:31:49 +01:00
Harald Barth
64a34b3a32 proto works 2024-11-10 11:31:14 +01:00
Harald Barth
6710c47f03 Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2024-11-09 13:02:07 +01:00
Harald Barth
420d14567d for tag 2024-11-09 13:00:02 +01:00
Harald Barth
19f4869401 multicast receive tests 2024-11-09 12:57:49 +01:00
Asbelos
953b8054f5 5.2.89 2024-11-06 01:11:18 +00:00
Asbelos
8081bfdf1e warnings cleared 2024-11-06 01:05:35 +00:00
Asbelos
03bd1e897a SET/RESET range 2024-11-05 12:23:06 +00:00
Harald Barth
bcdf9cb1c5 Add STM32lwipopts.h 2024-11-04 17:35:34 +01:00
Harald Barth
701f4e852f Variable number of TCP clients 2024-11-04 17:28:51 +01:00
Harald Barth
657fb7009c tag 2024-11-04 17:27:34 +01:00
peteGSX
d8f6d91408 Merge pull request #425 from DCC-EX:fix-ex-tt-home-angle
Fix validated
2024-11-04 16:46:27 +10:00
peteGSX
dbb15c6aaa Fix validated 2024-11-04 16:39:54 +10:00
peteGSX
614802c756 Update angle variable 2024-11-04 08:24:04 +10:00
Harald Barth
e131a9cce8 Ethernet: Reject additonal connection instead of looping on OVERFLOW 2024-11-02 21:25:21 +01:00
Asbelos
5efe385f2e Sensorcam 2024-11-02 13:25:35 +00:00
Asbelos
c50f3e016c No functional change
Unused parameter removal & end of file tidying by the Arduino IDE
2024-10-27 13:53:34 +00:00
pmantoine
535dcabcec Initial working IO_TCA8418 driver 2024-10-24 13:19:07 +08:00
Harald Barth
1d18d5dea5 explain RMT variations 2024-10-12 21:41:40 +02:00
Asbelos
21c01ab69a TM1638 support 2024-10-10 19:42:27 +01:00
Asbelos
fa00e9e11b Squashed commit of the following:
commit f13824164b
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Oct 10 16:07:42 2024 +0100

    _s7 keyword generator

commit 8a7dc2643c
Author: Asbelos <asbelos@btinternet.com>
Date:   Mon Oct 7 10:54:05 2024 +0100

    comments

commit 801cddfef7
Author: Asbelos <asbelos@btinternet.com>
Date:   Sun Oct 6 13:24:07 2024 +0100

    simpler macro insert

commit 5883f474ee
Author: Asbelos <asbelos@btinternet.com>
Date:   Sun Oct 6 13:18:29 2024 +0100

    Auto include

commit 312fc255e4
Author: Asbelos <asbelos@btinternet.com>
Date:   Sun Oct 6 13:12:51 2024 +0100

    Cleanup to one class

commit 3094074349
Author: Asbelos <asbelos@btinternet.com>
Date:   Sun Oct 6 10:34:16 2024 +0100

    peeled back

commit aa2a6ad119
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Oct 5 18:27:43 2024 +0100

    all fastpins

commit 931baf4b6d
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Oct 5 16:28:03 2024 +0100

    Partial lib extract

commit 47bc3b55fc
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Oct 4 15:41:51 2024 +0100

    fixes and SEG7 macro

commit 3f26ca2d1a
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Oct 4 14:33:23 2024 +0100

    enums for exrail easy

commit 7e7c00594b
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Oct 4 13:16:57 2024 +0100

    Working

commit fc4df87848
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Oct 4 09:27:46 2024 +0100

    leds and buttons
2024-10-10 19:38:35 +01:00
Harald Barth
ece2ac3ccf revert last 3 commits 2024-10-06 08:00:07 +02:00
Barry Daniel
ea2e5ab8e9 Delete CamParser.cpp 2024-10-06 15:07:52 +10:00
Barry Daniel
480eb1bfde Delete myHal.cpp 2024-10-06 15:07:15 +10:00
Barry Daniel
21dca05257 Add files via upload 2024-10-06 14:54:37 +10:00
pmantoine
f5014f5595 Further STM32 TrackManager fix 2024-10-03 09:28:55 +08:00
pmantoine
33c8ed19a9 Various STM32 related fixes 2024-09-30 16:13:00 +08:00
Harald Barth
0e99ad143b version 5.2.82 2024-09-30 10:07:07 +02:00
Harald Barth
01533e2cd2 TrackManager and EXRAIL: Introduce more consistent names for <= ...> and SET_TRACK 2024-09-30 10:05:47 +02:00
pmantoine
07ab7286ba STM32 Ethernet support 2024-09-28 19:48:11 +08:00
Asbelos
dc481a2f0c EthernetInterface 2024-09-23 09:34:26 +01:00
Asbelos
692f97e480 Merge branch 'devel_fozzie2' into devel 2024-09-23 09:16:39 +01:00
Asbelos
7fb7751f19 Merge branch 'devel_fozzie2' of https://github.com/DCC-EX/CommandStation-EX into devel_fozzie2 2024-09-23 08:50:13 +01:00
Asbelos
546ddd8139 STRCHR_P for sensorcam 2024-09-23 08:42:49 +01:00
Harald Barth
4aa353edbc version 5.2.79 2024-09-22 21:03:36 +02:00
Harald Barth
c1d6ee2804 Merge branch 'wifirestart' into devel 2024-09-22 21:01:16 +02:00
Harald Barth
14360b4198 serial manager loop that handles quoted strings 2024-09-22 20:58:17 +02:00
Harald Barth
dd898d3c16 WiFiESP32 reconfig prototype 2024-09-22 20:58:17 +02:00
Asbelos
277431e84c NeoPixel support
commit 2bbb5c1119
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Sep 20 12:13:21 2024 +0100

    EXRAIL use neopixel range instead of loop

commit 3aabb51888
Author: Asbelos <asbelos@btinternet.com>
Date:   Wed Sep 18 17:06:00 2024 +0100

    Neopixel signals with blue-tint

    See Release Notes file

commit 8e6fe6df21
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Sep 12 08:35:26 2024 +0100

    HAL write range

commit 66e57b5ab2
Author: Asbelos <asbelos@btinternet.com>
Date:   Sun Sep 8 09:26:37 2024 +0100

    Killblink on neopixel set.

commit 360c426675
Merge: dd16e0d b026417
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Sep 7 16:45:29 2024 +0100

    Merge branch 'devel' into devel-pauls-i2c-devices

commit dd16e0da97
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Sep 7 13:00:26 2024 +0100

    Notes

commit e823db3d24
Author: Asbelos <asbelos@btinternet.com>
Date:   Sat Sep 7 11:16:30 2024 +0100

    Neopixel change to 8,8,8

commit d3d6cc97fb
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Sep 6 13:25:44 2024 +0100

    Neopixel <o> cmd

commit 03d8d5f93d
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Sep 6 08:08:18 2024 +0100

    Its working!!

commit 235f3c82b5
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Sep 5 22:02:29 2024 +0100

    Update IO_NeoPixel.h

commit 530b77bbab
Author: Asbelos <asbelos@btinternet.com>
Date:   Tue Sep 3 15:04:40 2024 +0100

    NEOPIXEL driver and macros

commit 2a895fbbd5
Author: Asbelos <asbelos@btinternet.com>
Date:   Tue Sep 3 11:26:17 2024 +0100

    First compile neopixel driver

commit c6f2db7909
Merge: a7df84b 7395aa4
Author: Asbelos <asbelos@btinternet.com>
Date:   Tue Sep 3 10:07:12 2024 +0100

    Merge branch 'devel' into devel-pauls-i2c-devices

commit a7df84b01c
Author: Asbelos <asbelos@btinternet.com>
Date:   Tue Sep 3 09:56:05 2024 +0100

    NEOPIXEL EXRAIL

commit ead6e5afa1
Author: Asbelos <asbelos@btinternet.com>
Date:   Tue Sep 3 09:55:36 2024 +0100

    NEOPIXEL EXRAIL

commit 0cb175544e
Author: pmantoine <pma-github@milleng.com.au>
Date:   Sat Feb 24 17:29:10 2024 +0800

    More TCA8418

commit 2082051801
Author: pmantoine <pma-github@milleng.com.au>
Date:   Sat Feb 24 13:02:34 2024 +0800

    TCA8418 initial HAL driver scaffolding
2024-09-22 12:37:38 +01:00
Harald Barth
fe2f705fa9 version 5.2.77 2024-09-12 14:22:36 +02:00
Harald Barth
2606d73d93 Implement WiThrottle "force function" subcommand "f" 2024-09-12 14:20:24 +02:00
Harald Barth
ec42c09e06 Merge branch 'devel_fozzie2' of https://github.com/DCC-EX/CommandStation-EX into devel_fozzie2 2024-09-11 11:25:21 +02:00
Harald Barth
4ab77c21ed tag 2024-09-11 11:21:32 +02:00
Harald Barth
b53384ab51 If anyone ever wants to run a SABERTOOTH motor controller from a Mega2560 2024-09-07 23:31:02 +02:00
Harald Barth
b026417efb EXTRAIL: Propagate a failed loco addr read to EXRAIL so it can be used as IFLOCO(-1) 2024-09-06 09:28:40 +02:00
Harald Barth
7ffbd9d0e8 Use port variable 2024-09-05 13:01:54 +02:00
Harald Barth
6fa5511670 version 2024-09-04 09:13:52 +02:00
Harald Barth
c07ac38ab1 EXRAIL: Catch CV read errors in the callback 2024-09-04 09:11:51 +02:00
Asbelos
4174c2a4ab Merge branch 'devel_fozzie2' of https://github.com/DCC-EX/CommandStation-EX into devel_fozzie2 2024-08-30 09:34:25 +01:00
pmantoine
30236f9b36 STM32 Ethernet fixed 2024-08-30 11:52:27 +08:00
Harald Barth
7395aa4af8 version 2024-08-29 13:46:44 +02:00
Harald Barth
2397b773d7 Bugfix: Enable CommandDistributor even for serials 4 to 6 2024-08-29 13:44:51 +02:00
Harald Barth
9a08f2df63 ESP32: Make Serial2 possible for commands 2024-08-29 13:41:37 +02:00
Asbelos
8245208b2b stm32 compiles 2024-08-25 17:26:33 +01:00
Asbelos
4ed2ee9adc mDNS restored on mega 2024-08-25 16:50:49 +01:00
Asbelos
06a353cfa0 stm32 merge (mdns disabled} 2024-08-25 16:29:59 +01:00
Harald Barth
dfe9e6b69f platformio eth debug target 2024-08-25 17:01:58 +02:00
Asbelos
4d84eccac3 LCD and link warning 2024-08-20 19:51:45 +01:00
Harald Barth
edb02a00ce allow static IP (again) 2024-08-19 08:59:32 +02:00
Asbelos
5db19a0fb8 Ethgernet simplification 2024-08-18 20:32:05 +01:00
Harald Barth
b62661c337 tag 2024-08-17 23:09:09 +02:00
Harald Barth
048ba3fd1e replace socket.connected() with check for return value of socket.read() 2024-08-17 20:18:59 +02:00
Harald Barth
c8c3697fa0 write buffer 2024-08-09 15:02:11 +02:00
Harald Barth
8c3c5dfe33 do not flush 2024-08-09 14:34:30 +02:00
Harald Barth
92288603bf do not available, just try to read 2024-08-09 12:01:53 +02:00
Harald Barth
80c8b3ef62 better name 2024-08-09 11:57:54 +02:00
Harald Barth
127f3acce5 send whole outbuffer 2024-08-09 11:46:33 +02:00
Harald Barth
690c629e6d looptimer more diag in EthernetInterface 2024-08-09 09:16:56 +02:00
Harald Barth
e328ea5c5d Merge branch 'devel' into devel-fozzie 2024-08-08 10:52:50 +02:00
Harald Barth
ed853eef1d version 5.2.74 2024-08-08 10:49:59 +02:00
Harald Barth
05e77c924e Revert momentum additions, squashed
commit 4e57a80265.
2024-08-08 10:45:44 +02:00
Harald Barth
923b031d06 Gittag 2024-08-07 21:14:07 +02:00
Harald Barth
7e29011d63 looptimer test 1 2024-08-07 21:13:44 +02:00
Harald Barth
c5c5609fc6 ESP32: Turn always on the JOINed PROG track when it acts as MAIN 2024-08-06 07:30:01 +02:00
pmantoine
9c263062e4 STM32 bugfix PORTG and PORTH shadow ports 2024-08-04 18:08:27 +08:00
pmantoine
f39fd89fbd STM32 bugfix for PORTG and PORTH with thanks to Ash 2024-07-25 13:58:04 +08:00
Asbelos
4e57a80265 Squashed commit of the following:
commit 3ac2fff70d
Author: Asbelos <asbelos@btinternet.com>
Date:   Tue Jul 23 15:40:36 2024 +0100

    Create momentum.md

commit a08195332f
Author: Asbelos <asbelos@btinternet.com>
Date:   Mon Jul 22 21:57:47 2024 +0100

    Cleanup of DCC Class reminders

commit 002ec5f176
Author: Asbelos <asbelos@btinternet.com>
Date:   Mon Jul 22 12:42:43 2024 +0100

    Cleaning access to speedByte

commit 854ddb0c6c
Author: Asbelos <asbelos@btinternet.com>
Date:   Sun Jul 21 10:15:07 2024 +0100

    Fix momentum algorithm

commit 916d3baf63
Merge: ab72a75 27dc805
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Jul 19 10:14:06 2024 +0100

    Merge branch 'devel' into devel_momentum

commit ab72a75d8f
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Jul 19 08:33:50 2024 +0100

    EXRAIL MOMENTUM

commit 8a623aa1cb
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Jul 18 20:31:58 2024 +0100

     Momentum
2024-07-23 15:42:35 +01:00
Asbelos
27dc8059d7 Broadcast loco forgets. 2024-07-19 09:29:43 +01:00
Asbelos
dc2eae499f RocoDriver->EncoderThrottle 2024-07-18 09:39:32 +01:00
Asbelos
c518dcdc0b IO_RocoDriver 2024-07-12 13:18:26 +01:00
Asbelos
e6047f6693 Revert <l> for F31 2024-07-12 10:25:11 +01:00
Asbelos
96c4757cc6 EXTAIL AFTER debounce time 2024-07-10 10:58:22 +01:00
Asbelos
60e564df51 SETFREQ and <F DCFREQ 2024-07-10 09:57:03 +01:00
Asbelos
a8b4e39733 Speedup SETFREQ
Avoid calling the DCC packets for setfreq macro.
2024-07-04 17:20:37 +01:00
Ash-4
d705626f4a <0 PROG> updated to undo JOIN
Update of the commit message for 5.2.64
2024-06-30 21:29:34 -05:00
Ash-4
c97284c15f inrush overfault on stm32EC-Ash 2024-06-30 20:32:08 -05:00
pmantoine
df1f365c1e Add WIFI_LED option for ESP32, edits for config.example.h 2024-06-29 16:22:23 +08:00
Harald Barth
023c004842 version 5.2.62 2024-06-18 22:21:51 +02:00
Harald Barth
2481f1c5d6 Allow acks longer than 65535us and specify ack length in the <C ACK MAX 20 MS> format 2024-06-18 22:18:23 +02:00
Asbelos
7dadecb5df Typo in KeywordHasher 2024-06-13 16:09:28 +01:00
Asbelos
6ef312b510 5.2.61 2024-06-13 13:08:40 +01:00
Asbelos
97f9fb4813 Squashed commit of the following:
commit a2b3ee8b5d52c2eefa461ace8f95c7f782a58efc
Merge: fc1217b 3d6c935
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Jun 13 11:58:00 2024 +0100

    Merge branch 'devel' into devel_merg

commit fc1217b8fa27a83174a4cf3bb82666f075103637
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Jun 13 11:57:12 2024 +0100

    Update EXRAIL2Parser.cpp

commit b89508671c
Author: Asbelos <asbelos@btinternet.com>
Date:   Wed Jun 12 16:25:17 2024 +0100

    Separate <L> polling cycle

commit 9f1257bc6c
Merge: a2fb585 5f65fd5
Author: Asbelos <asbelos@btinternet.com>
Date:   Wed Jun 12 10:57:09 2024 +0100

    Merge branch 'devel' into devel_merg

commit a2fb58584f
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri May 31 19:49:39 2024 +0100

    ACON/ACOF 32 bit + 1=OFF

commit fca4ea052e
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri May 31 12:09:38 2024 +0100

    Rename to ACON/ACOF terminology

commit 0d07aa6271
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu May 30 19:59:29 2024 +0100

    MERG macris in exrail
2024-06-13 13:01:33 +01:00
Harald Barth
3d6c935308 EXCSB1 motor driver definitions 2024-06-11 23:10:18 +02:00
Harald Barth
fba9a30813 ESP32: Espressif deprecated ADC_ATTEN_DB_11 2024-06-11 23:09:41 +02:00
Harald Barth
5f65fd5944 tag with date 2024-06-02 21:45:43 +02:00
Harald Barth
a26610bc7f ESP32: More version locking 2024-06-02 21:44:25 +02:00
Harald Barth
4e491a1e56 Typo 2024-06-02 21:17:30 +02:00
Harald Barth
430161ef60 ESP32: Refuse IDF5 2024-06-02 21:14:46 +02:00
Harald Barth
264a53dacf ESP32: Refuse IDF5 2024-06-02 21:10:57 +02:00
Harald Barth
0c96d4ffc2 version 5.2.60 2024-05-23 22:46:47 +02:00
Harald Barth
843fa42692 Remove inrush throttle after half good time so that we go to mode overload if problem persists 2024-05-23 22:41:50 +02:00
Harald Barth
b17dc5a0dd Bugfix: Opcode AFTEROVERLOAD does not have an argument that is a pin and needs to be initialized 2024-05-23 22:39:43 +02:00
pmantoine
449a5f1670 STM32 updates for serial ports etc. 2024-05-22 07:16:25 +08:00
Asbelos
06b8995861 ALIAS(named pins) 2024-05-21 20:04:11 +01:00
Harald Barth
2172d2e175 make WDT time longer to work around bootloader bug 2024-05-11 08:46:25 +02:00
Harald Barth
86291cbec4 signal id of 0 does not work 2024-05-11 07:45:28 +02:00
Harald Barth
66791b19f5 remove stupid comment 2024-05-11 07:43:24 +02:00
Harald Barth
6689a1d35f version 5.2.57 2024-05-09 17:11:09 +02:00
Harald Barth
91818ed80c apply mode by binart bit match and not by equality 2024-05-09 17:06:33 +02:00
Harald Barth
86310aea4f getSignalSlot minor typo (and indent/comments) fix 2024-05-09 15:18:00 +02:00
pmantoine
a610e83f6e Bugfix and refactor for EXRAIL getSignalSlot 2024-05-07 18:20:37 +08:00
pmantoine
1449dc7bac EXRAIL move isSignal to public for STEALTH 2024-05-07 15:12:37 +08:00
pmantoine
bd11cfbf8b Bugfix EXRAIL active high signal handling 2024-05-07 11:29:49 +08:00
pmantoine
16214fad66 EX-Fastclock bugfix for address check 2024-05-07 11:13:19 +08:00
pmantoine
76ad3ee48d STM32 bug fixes, port usage 2024-05-07 11:10:39 +08:00
Asbelos
742b100f65 Comments only 2024-05-02 09:48:18 +01:00
pmantoine
83d4930124 Fix F446ZE ADCee support and add STM32 ports G and H 2024-04-26 19:04:20 +08:00
Harald Barth
b4e7982099 remove forgotten #define DIAG_IO 2024-04-22 08:12:08 +02:00
Harald Barth
3af2f67792 version 5.2.51 2024-04-21 19:41:30 +02:00
Harald Barth
c382bd33bc Distinguish between sighandle and sigid 2024-04-21 19:03:24 +02:00
Asbelos
ebe8f62cf0 ONBUTTON/ONSENSOR use latch 2024-04-13 10:16:26 +01:00
Asbelos
7dafe0383d EXRAIL ONBUTTON 2024-04-13 09:14:55 +01:00
Asbelos
4aa97e1731 Squashed commit of the following:
commit 3fc90c916c
Merge: 132e2d0 91e60b3
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Apr 12 15:08:49 2024 +0100

    Merge branch 'devel' into devel_chris

commit 132e2d0de2
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Apr 12 15:07:31 2024 +0100

    Revert "Merge branch 'master' into devel_chris"

    This reverts commit 23845f2df2, reversing
    changes made to 76755993f1.

commit 23845f2df2
Merge: 7675599 28d60d4
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Apr 12 14:38:22 2024 +0100

    Merge branch 'master' into devel_chris

commit 76755993f1
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Apr 12 14:37:34 2024 +0100

    ONSENSOR/ONBUTTON

commit 8987d622e6
Author: Asbelos <asbelos@btinternet.com>
Date:   Tue Apr 9 20:44:47 2024 +0100

    doc note

commit 8f0a5c1ec0
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Apr 4 09:45:58 2024 +0100

    Exrail notes

commit 94083b9ab8
Merge: 72ef199 02bf50b
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Apr 4 09:08:26 2024 +0100

    Merge branch 'devel' into devel_chris

commit 72ef199315
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Apr 4 09:06:50 2024 +0100

    TOGGLE_TURNOUT

commit e69b777a2f
Author: Asbelos <asbelos@btinternet.com>
Date:   Wed Apr 3 15:17:40 2024 +0100

    BLINK command

commit c7ed47400d
Author: Asbelos <asbelos@btinternet.com>
Date:   Tue Apr 2 10:12:45 2024 +0100

    FTOGGLE,XFTOGGLE

commit 7a93cf7be8
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Mar 29 13:21:35 2024 +0000

    EXRAIL STEALTH_GLOBAL

commit 28d60d4984
Author: Peter Akers <akersp62@gmail.com>
Date:   Fri Feb 16 18:02:40 2024 +1000

    Update README.md

commit 3b162996ad
Author: peteGSX <peteracole@outlook.com.au>
Date:   Sun Jan 21 07:13:53 2024 +1000

    EX-IO fixes in version

commit fb414a7a50
Author: Harald Barth <haba@kth.se>
Date:   Thu Jan 18 08:20:33 2024 +0100

    Bugfix: allocate enough bytes for digital pins. Add more sanity checks when allocating memory

commit 818e05b425
Author: Harald Barth <haba@kth.se>
Date:   Wed Jan 10 08:37:54 2024 +0100

    version 5.0.8

commit c5168f030f
Author: Harald Barth <haba@kth.se>
Date:   Wed Jan 10 08:15:30 2024 +0100

    Do not crash on turnouts without description

commit 387ea019bd
Author: Harald Barth <haba@kth.se>
Date:   Mon Nov 6 22:11:56 2023 +0100

    version 5.0.7

commit a981f83bb9
Author: Harald Barth <haba@kth.se>
Date:   Mon Nov 6 22:11:31 2023 +0100

    Only flag 2.2.0.0-dev as broken, not 2.2.0.0

commit 749a859db5
Author: Asbelos <asbelos@btinternet.com>
Date:   Wed Nov 1 20:13:05 2023 +0000

    Bugfix TURNOUTL

commit 659c58b307
Author: Harald Barth <haba@kth.se>
Date:   Sat Oct 28 19:20:33 2023 +0200

    version 5.0.5

commit 0b9ec7460b
Author: Harald Barth <haba@kth.se>
Date:   Sat Oct 28 19:18:59 2023 +0200

    Bugfix version detection logic and better message
2024-04-13 08:12:35 +01:00
pmantoine
91e60b3716 HALDisplay bug fix 2024-04-12 17:25:00 +08:00
Asbelos
8a5a832b1d Reduced EXRAIL diag noise 2024-04-09 20:59:57 +01:00
Asbelos
5ea6feb11a Squashed commit of the following:
commit 8987d622e6
Author: Asbelos <asbelos@btinternet.com>
Date:   Tue Apr 9 20:44:47 2024 +0100

    doc note

commit 8f0a5c1ec0
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Apr 4 09:45:58 2024 +0100

    Exrail notes

commit 94083b9ab8
Merge: 72ef199 02bf50b
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Apr 4 09:08:26 2024 +0100

    Merge branch 'devel' into devel_chris

commit 72ef199315
Author: Asbelos <asbelos@btinternet.com>
Date:   Thu Apr 4 09:06:50 2024 +0100

    TOGGLE_TURNOUT

commit e69b777a2f
Author: Asbelos <asbelos@btinternet.com>
Date:   Wed Apr 3 15:17:40 2024 +0100

    BLINK command

commit c7ed47400d
Author: Asbelos <asbelos@btinternet.com>
Date:   Tue Apr 2 10:12:45 2024 +0100

    FTOGGLE,XFTOGGLE

commit 7a93cf7be8
Author: Asbelos <asbelos@btinternet.com>
Date:   Fri Mar 29 13:21:35 2024 +0000

    EXRAIL STEALTH_GLOBAL
2024-04-09 20:45:28 +01:00
Harald Barth
263c3d01e3 DISABLE_DIAG by default for Uno and Nano 2024-04-07 09:26:32 +02:00
Asbelos
182479c07b Consist version. 2024-04-06 23:49:26 +01:00
Asbelos
3317b4666e Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2024-04-06 23:41:33 +01:00
Asbelos
f41f61dd5f <W CONSIST cmd 2024-04-06 23:41:25 +01:00
Peter Akers
28d60d4984 Update README.md 2024-02-16 18:02:40 +10:00
peteGSX
3b162996ad EX-IO fixes in version 2024-01-21 07:13:53 +10:00
Harald Barth
fb414a7a50 Bugfix: allocate enough bytes for digital pins. Add more sanity checks when allocating memory 2024-01-20 21:45:09 +01:00
Harald Barth
818e05b425 version 5.0.8 2024-01-10 08:37:54 +01:00
Harald Barth
c5168f030f Do not crash on turnouts without description 2024-01-10 08:25:34 +01:00
Harald Barth
387ea019bd version 5.0.7 2023-11-06 22:11:56 +01:00
Harald Barth
a981f83bb9 Only flag 2.2.0.0-dev as broken, not 2.2.0.0 2023-11-06 22:11:31 +01:00
Asbelos
749a859db5 Bugfix TURNOUTL 2023-11-01 20:13:05 +00:00
Harald Barth
659c58b307 version 5.0.5 2023-10-28 19:20:33 +02:00
Harald Barth
0b9ec7460b Bugfix version detection logic and better message 2023-10-28 19:18:59 +02:00
132 changed files with 12710 additions and 1390 deletions

36
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Docs
on:
push:
branches:
- devel
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Install Requirements
run: |
cd docs
python -m pip install --upgrade pip
pip3 install -r requirements.txt
sudo apt-get install doxygen
- name: Build Prod docs
run: |
cd docs
make html
touch _build/html/.nojekyll
- name: Deploy
uses: JamesIves/github-pages-deploy-action@ba1486788b0490a235422264426c45848eac35c6
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages # The branch the action should deploy to.
folder: docs/_build/html # The folder the action should deploy.

3
.gitignore vendored
View File

@@ -15,3 +15,6 @@ my*.h
compile_commands.json
newcode.txt.old
UserAddin.txt
_build
venv
.DS_Store

141
CamParser.cpp Normal file
View File

@@ -0,0 +1,141 @@
/*
* © 2023-2025, Barry Daniel
* © 2025 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/>.
*/
//sensorCAM parser.cpp version 3.06 Jan 2025
#include "DCCEXParser.h"
#include "CamParser.h"
#include "FSH.h"
const int16_t ver=30177;
const int16_t ve =2899;
// The CAMVPINS array will be filled by IO_EXSensorCam HAL drivers calling
// the CamParser::addVpin() function.
// The CAMBaseVpin is the one to be used when commands are given without a vpin.
VPIN CamParser::CAMBaseVpin = 0; // no vpins yet known
VPIN CamParser::CAMVPINS[] = {0,0,0,0}; // determines max # CAM's
int CamParser::vpcount=sizeof(CAMVPINS)/sizeof(CAMVPINS[0]);
void CamParser::parse(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
if (opcode!='N') return; // this is not for us.
if (parseN(stream,paramCount,p)) opcode=0; // we have consumed this
// If we fail, the caller will <X> the <N command.
}
bool CamParser::parseN(Print * stream, byte paramCount, int16_t p[]) {
(void)stream; // probably unused parameter
if (CAMBaseVpin==0) CAMBaseVpin=CAMVPINS[0]; // default to CAM 1.
VPIN vpin=CAMBaseVpin; //use current CAM selection
if (paramCount==0) {
DIAG(F("Cam base vpin:%d"),CAMBaseVpin);
for (auto i=0;i<vpcount;i++){
if (CAMVPINS[i]==0) break;
DIAG(F("EXSensorCam #%d vpin %d"),i+1,CAMVPINS[i]);
}
return true;
}
uint8_t camop=p[0]; // cam oprerator
int param1=0;
int16_t param3=9999; // =0 could invoke parameter changes. & -1 gives later errors
if(camop=='C'){
if(p[1]>=100) CAMBaseVpin=p[1];
if(p[1]<=vpcount && p[1]>0) CAMBaseVpin=CAMVPINS[p[1]-1];
DIAG(F("CAM base Vpin: %c %d "),p[0],CAMBaseVpin);
return true;
}
if (camop<100) { //switch CAM# if p[1] dictates
if(p[1]>=100 && p[1]<=(vpcount*100+99)) { //limits to CAM# 1 to 4 for now
vpin=CAMVPINS[p[1]/100-1];
CAMBaseVpin=vpin;
DIAG(F("switching to CAM %d baseVpin:%d"),p[1]/100,vpin);
p[1]=p[1]%100; //strip off CAM #
}
}
if (CAMBaseVpin==0) {DIAG(F("<n Error: Invalid CAM selected, default to CAM1>"));
return false; // cam not defined
}
// send UPPER case to sensorCAM to flag binary data from a DCCEX-CS parser
switch(paramCount) {
case 1: //<N ver> produces '^'
if((camop == 'V') || (p[0] == ve) || (p[0] == ver) ) camop='^';
if (STRCHR_P((const char *)F("EFGMQRVW^"),camop) == nullptr) return false;
if (camop=='Q') param3=10; //<NQ> for activation state of all 10 banks of sensors
if (camop=='F') camop=']'; //<NF> for Reset/Finish webCAM.
break; // F Coded as ']' else conflicts with <Nf %%>
case 2: //<N camop p1>
if (STRCHR_P((const char *)F("ABFHILMNOPQRSTUV"),camop)==nullptr) return false;
param1=p[1];
break;
case 3: //<N vpin rowY colx > or <N cmd p1 p2>
if (p[0]>=100) { //vpin - i.e. NOT 'A' through 'Z'
if (p[1]>236 || p[1]<0) return false; //row
if (p[2]>316 || p[2]<0) return false; //column
camop=0x80; // special 'a' case for IO_SensorCAM
vpin = p[0];
}else if (STRCHR_P((const char *)F("IJMNT"),camop) == nullptr) return false;
camop=p[0];
param1 = p[1];
param3 = p[2];
break;
case 4: //<N a id row col>
if (camop!='A') return false; //must start with 'a'
if (p[3]>316 || p[3]<0) return false;
if (p[2]>236 || p[2]<0) return false;
if (p[1]>97 || p[1]<0) return false; //treat as bsNo.
vpin = vpin + (p[1]/10)*8 + p[1]%10; //translate p[1]
camop=0x80; // special 'a' case for IO_SensorCAM
param1=p[2]; // row
param3=p[3]; // col
break;
default:
return false;
}
DIAG(F("CamParser: %d %c %d %d"),vpin,camop,param1,param3);
IODevice::writeAnalogue(vpin,param1,camop,param3);
return true;
}
void CamParser::addVpin(VPIN pin) {
// called by IO_EXSensorCam starting up a camera on a vpin
byte slot=255;
for (auto i=0;i<vpcount && slot==255;i++) {
if (CAMVPINS[i]==0) {
slot=i;
CAMVPINS[slot]=pin;
}
}
if (slot==255) {
DIAG(F("No more than %d cameras supported"),vpcount);
return;
}
if (slot==0) CAMBaseVpin=pin;
DIAG(F("CamParser Registered cam #%dvpin %d"),slot+1,pin);
// tell the DCCEXParser that we wish to filter commands
DCCEXParser::setCamParserFilter(&parse);
}

40
CamParser.h Normal file
View File

@@ -0,0 +1,40 @@
/*
* © 2023-2025, Barry Daniel
* © 2025 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef CamParser_H
#define CamParser_H
#include <Arduino.h>
#include "IODevice.h"
class CamParser {
public:
static void parse(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
static void addVpin(VPIN pin);
private:
static bool parseN(Print * stream, byte paramCount, int16_t p[]);
static VPIN CAMBaseVpin;
static VPIN CAMVPINS[];
static int vpcount;
};
#endif

View File

@@ -1,6 +1,6 @@
/*
* © 2022 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020-2025 Chris Harlow
* © 2020 Gregor Baues
* © 2022 Colin Murdoch
* All rights reserved.
@@ -31,19 +31,21 @@
#include "DCC.h"
#include "TrackManager.h"
#include "StringFormatter.h"
#include "Websockets.h"
// variables to hold clock time
int16_t lastclocktime;
int8_t lastclockrate;
#if WIFI_ON || ETHERNET_ON || defined(SERIAL1_COMMANDS) || defined(SERIAL2_COMMANDS) || defined(SERIAL3_COMMANDS)
#if WIFI_ON || ETHERNET_ON || defined(SERIAL1_COMMANDS) || defined(SERIAL2_COMMANDS) || defined(SERIAL3_COMMANDS) || defined(SERIAL4_COMMANDS) || defined(SERIAL5_COMMANDS) || defined(SERIAL6_COMMANDS)
// use a buffer to allow broadcast
StringBuffer * CommandDistributor::broadcastBufferWriter=new StringBuffer();
template<typename... Targs> void CommandDistributor::broadcastReply(clientType type, Targs... msg){
broadcastBufferWriter->flush();
StringFormatter::send(broadcastBufferWriter, msg...);
broadcastToClients(type);
if (type==COMMAND_TYPE) broadcastToClients(WEBSOCKET_TYPE);
}
#else
// on a single USB connection config, write direct to Serial and ignore flush/shove
@@ -56,14 +58,17 @@ template<typename... Targs> void CommandDistributor::broadcastReply(clientType t
#ifdef CD_HANDLE_RING
// wifi or ethernet ring streams with multiple client types
RingStream * CommandDistributor::ring=0;
CommandDistributor::clientType CommandDistributor::clients[8]={
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE};
CommandDistributor::clientType CommandDistributor::clients[MAX_NUM_TCP_CLIENTS]={ NONE_TYPE }; // 0 is and must be NONE_TYPE
// Parse is called by Withrottle or Ethernet interface to determine which
// protocol the client is using and call the appropriate part of dcc++Ex
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream) {
if (Diag::WIFI && Diag::CMD)
DIAG(F("Parse C=%d T=%d B=%s"),clientId, clients[clientId], buffer);
if (clientId>=sizeof (clients)) {
// Caution, diag dump of buffer could corrupt ringstream
// if headed by websocket bytes.
DIAG(F("::parse invalid client=%d"),clientId);
return;
}
ring=stream;
// First check if the client is not known
@@ -72,22 +77,40 @@ void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream
// client is using the DCC++ protocol where all commands start
// with '<'
if (clients[clientId] == NONE_TYPE) {
auto websock=Websockets::checkConnectionString(clientId,buffer,stream);
if (websock) {
clients[clientId]=WEBSOCK_CONNECTING_TYPE;
// websockets will have replied already
return;
}
if (buffer[0] == '<')
clients[clientId]=COMMAND_TYPE;
else
clients[clientId]=WITHROTTLE_TYPE;
}
// after first inbound transmission the websocket is connected
if (clients[clientId]==WEBSOCK_CONNECTING_TYPE)
clients[clientId]=WEBSOCKET_TYPE;
// mark buffer that is sent to parser
ring->mark(clientId);
// When type is known, send the string
// to the right parser
if (clients[clientId] == COMMAND_TYPE) {
ring->mark(clientId);
DCCEXParser::parse(stream, buffer, ring);
} else if (clients[clientId] == WITHROTTLE_TYPE) {
ring->mark(clientId);
WiThrottle::getThrottle(clientId)->parse(ring, buffer);
}
else if (clients[clientId] == WEBSOCKET_TYPE) {
buffer=Websockets::unmask(clientId,ring, buffer);
if (!buffer) return; // unmask may have handled it alrerday (ping/pong)
// mark ring with client flagged as websocket for transmission later
ring->mark(clientId | Websockets::WEBSOCK_CLIENT_MARKER);
DCCEXParser::parse(stream, buffer, ring);
}
if (ring->peekTargetMark()!=RingStream::NO_CLIENT) {
// The commit call will either write the length bytes
@@ -131,7 +154,7 @@ void CommandDistributor::broadcastToClients(clientType type) {
for (byte clientId=0; clientId<sizeof(clients); clientId++) {
if (clients[clientId]==type) {
//DIAG(F("CD mark client %d"), clientId);
ring->mark(clientId);
ring->mark(clientId | (type==WEBSOCKET_TYPE? Websockets::WEBSOCK_CLIENT_MARKER : 0));
ring->print(broadcastBufferWriter->getString());
//DIAG(F("CD commit client %d"), clientId);
ring->commit();
@@ -185,10 +208,15 @@ void CommandDistributor::setClockTime(int16_t clocktime, int8_t clockrate, byte
{
case 1:
if (clocktime != lastclocktime){
auto difference = clocktime - lastclocktime;
if (difference<0) difference+=1440;
DCC::setTime(clocktime,clockrate,difference>2);
// CAH. DIAG removed because LCD does it anyway.
LCD(6,F("Clk Time:%d Sp %d"), clocktime, clockrate);
// look for an event for this time
#ifdef EXRAIL_ACTIVE
RMFT2::clockEvent(clocktime,1);
#endif
// Now tell everyone else what the time is.
CommandDistributor::broadcastClockTime(clocktime, clockrate);
lastclocktime = clocktime;
@@ -207,9 +235,13 @@ int16_t CommandDistributor::retClockTime() {
return lastclocktime;
}
void CommandDistributor::broadcastLoco(byte slot) {
DCC::LOCO * sp=&DCC::speedTable[slot];
broadcastReply(COMMAND_TYPE, F("<l %d %d %d %l>\n"), sp->loco,slot,sp->speedCode,sp->functions);
void CommandDistributor::broadcastLoco(DCC::LOCO* sp) {
if (!sp) {
broadcastReply(COMMAND_TYPE,F("<l 0 -1 128 0>\n"));
return;
}
broadcastReply(COMMAND_TYPE, F("<l %d 0 %d %l>\n"),
sp->loco,sp->targetSpeed,sp->functions);
#ifdef SABERTOOTH
if (Serial2 && sp->loco == SABERTOOTH) {
static uint8_t rampingmode = 0;
@@ -248,6 +280,10 @@ void CommandDistributor::broadcastLoco(byte slot) {
#endif
}
void CommandDistributor::broadcastForgetLoco(int16_t loco) {
broadcastReply(COMMAND_TYPE, F("<l %d 0 1 0>\n<- %d>\n"), loco,loco);
}
void CommandDistributor::broadcastPower() {
char pstr[] = "? x";
for(byte t=0; t<TrackManager::MAX_TRACKS; t++)
@@ -276,6 +312,9 @@ void CommandDistributor::broadcastPower() {
state = '1';
}
if (state != '2')
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
// additional info about MAIN, PROG and JOIN
bool main=TrackManager::getMainPower()==POWERMODE::ON;
bool prog=TrackManager::getProgPower()==POWERMODE::ON;
@@ -284,7 +323,7 @@ void CommandDistributor::broadcastPower() {
const FSH * reason=F("");
if (join) {
reason = F(" JOIN"); // with space at start so we can append without space
broadcastReply(COMMAND_TYPE, F("<p1 %S>\n"),reason);
broadcastReply(COMMAND_TYPE, F("<p1%S>\n"),reason);
} else {
if (main) {
//reason = F("MAIN");
@@ -295,9 +334,6 @@ void CommandDistributor::broadcastPower() {
broadcastReply(COMMAND_TYPE, F("<p1 PROG>\n"));
}
}
if (state != '2')
broadcastReply(COMMAND_TYPE, F("<p%c>\n"),state);
#ifdef CD_HANDLE_RING
// 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);
@@ -373,4 +409,3 @@ void CommandDistributor::setVirtualLCDSerial(Print * stream) {
Print* CommandDistributor::virtualLCDSerial=&USB_SERIAL;
byte CommandDistributor::virtualLCDClient=0xFF;
byte CommandDistributor::rememberVLCDClient=0;

View File

@@ -1,6 +1,6 @@
/*
* © 2022 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020-2025 Chris Harlow
* © 2020 Gregor Baues
* © 2022 Colin Murdoch
*
@@ -28,6 +28,7 @@
#include "StringBuffer.h"
#include "defines.h"
#include "EXRAIL2.h"
#include "DCC.h"
#if WIFI_ON | ETHERNET_ON
// Command Distributor must handle a RingStream of clients
@@ -36,17 +37,18 @@
class CommandDistributor {
public:
enum clientType: byte {NONE_TYPE,COMMAND_TYPE,WITHROTTLE_TYPE};
enum clientType: byte {NONE_TYPE = 0,COMMAND_TYPE,WITHROTTLE_TYPE,WEBSOCK_CONNECTING_TYPE,WEBSOCKET_TYPE}; // independent of other types, NONE_TYPE must be 0
private:
static void broadcastToClients(clientType type);
static StringBuffer * broadcastBufferWriter;
#ifdef CD_HANDLE_RING
static RingStream * ring;
static clientType clients[8];
static clientType clients[MAX_NUM_TCP_CLIENTS];
#endif
public :
static void parse(byte clientId,byte* buffer, RingStream * ring);
static void broadcastLoco(byte slot);
static void broadcastLoco(DCC::LOCO * slot);
static void broadcastForgetLoco(int16_t loco);
static void broadcastSensor(int16_t id, bool value);
static void broadcastTurnout(int16_t id, bool isClosed);
static void broadcastTurntable(int16_t id, uint8_t position, bool moving);

View File

@@ -141,6 +141,23 @@ void setup()
CommandDistributor::broadcastPower();
}
/**************** for future reference
void looptimer(unsigned long timeout, const FSH* message)
{
static unsigned long lasttimestamp = 0;
unsigned long now = micros();
if (timeout != 0) {
unsigned long diff = now - lasttimestamp;
if (diff > timeout) {
DIAG(message);
DIAG(F("DeltaT=%L"), diff);
lasttimestamp = micros();
return;
}
}
lasttimestamp = now;
}
*********************************************/
void loop()
{
// The main sketch has responsibilities during loop()
@@ -148,14 +165,15 @@ void loop()
// Responsibility 1: Handle DCC background processes
// (loco reminders and power checks)
DCC::loop();
// Responsibility 2: handle any incoming commands on USB connection
SerialManager::loop();
// Responsibility 3: Optionally handle any incoming WiFi traffic
#ifndef ARDUINO_ARCH_ESP32
#if WIFI_ON
WifiInterface::loop();
#endif //WIFI_ON
#else //ARDUINO_ARCH_ESP32
#ifndef WIFI_TASK_ON_CORE0

600
DCC.cpp
View File

@@ -5,7 +5,7 @@
* © 2021 Herb Morton
* © 2020-2022 Harald Barth
* © 2020-2021 M Steve Todd
* © 2020-2021 Chris Harlow
* © 2020-2025 Chris Harlow
* All rights reserved.
*
* This file is part of DCC-EX
@@ -37,6 +37,8 @@
#include "CommandDistributor.h"
#include "TrackManager.h"
#include "DCCTimer.h"
#include "Railcom.h"
#include "DCCQueue.h"
// This module is responsible for converting API calls into
// messages to be sent to the waveform generator.
@@ -60,6 +62,8 @@ const byte FN_GROUP_5=0x10;
FSH* DCC::shieldName=NULL;
byte DCC::globalSpeedsteps=128;
#define SLOTLOOP for (auto slot=&speedTable[0];slot!=&speedTable[MAX_LOCOS];slot++)
void DCC::begin() {
StringFormatter::send(&USB_SERIAL,F("<iDCC-EX V-%S / %S / %S G-%S>\n"), F(VERSION), F(ARDUINO_TYPE), shieldName, F(GITHUB_SHA));
#ifndef DISABLE_EEPROM
@@ -72,13 +76,49 @@ void DCC::begin() {
#endif
}
byte DCC::defaultMomentumA=0;
byte DCC::defaultMomentumD=0;
bool DCC::linearAcceleration=false;
byte DCC::getMomentum(LOCO * slot) {
auto target=slot->targetSpeed & 0x7f;
auto current=slot->speedCode & 0x7f;
if (target > current) {
// accelerating
auto momentum=slot->momentumA==MOMENTUM_USE_DEFAULT ? defaultMomentumA : slot->momentumA;
// if nonlinear acceleration, momentum is reduced according to
// gap between throttle and speed.
// ie. Loco takes accelerates faster if high throttle
if (momentum==0 || linearAcceleration) return momentum;
auto powerDifference= (target-current)/8;
if (momentum-powerDifference <0) return 0;
return momentum-powerDifference;
}
return slot->momentumD==MOMENTUM_USE_DEFAULT ? defaultMomentumD : slot->momentumD;
}
void DCC::setThrottle( uint16_t cab, uint8_t tSpeed, bool tDirection) {
if (tSpeed==1) {
if (cab==0) {
estopAll(); // ESTOP broadcast fix
return;
}
}
byte speedCode = (tSpeed & 0x7F) + tDirection * 128;
setThrottle2(cab, speedCode);
TrackManager::setDCSignal(cab,speedCode); // in case this is a dcc track on this addr
// retain speed for loco reminders
updateLocoReminder(cab, speedCode );
LOCO * slot=lookupSpeedTable(cab);
if (slot->targetSpeed==speedCode) return;
slot->targetSpeed=speedCode;
byte momentum=getMomentum(slot);
if (momentum && tSpeed!=1) { // not ESTOP
// we dont throttle speed, we just let the reminders take it to target
slot->momentum_base=millis();
}
else { // Momentum not involved, throttle now.
slot->speedCode = speedCode;
setThrottle2(cab, speedCode);
TrackManager::setDCSignal(cab,speedCode); // in case this is a dcc track on this addr
}
CommandDistributor::broadcastLoco(slot);
}
void DCC::setThrottle2( uint16_t cab, byte speedCode) {
@@ -118,11 +158,11 @@ void DCC::setThrottle2( uint16_t cab, byte speedCode) {
b[nB++] = speedCode; // for encoding see setThrottle
}
DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
if ((speedCode & 0x7F) == 1) DCCQueue::scheduleEstopPacket(b, nB, 4, cab); // highest priority
else DCCQueue::scheduleDCCSpeedPacket( b, nB, 0, cab);
}
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2, byte count) {
void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
// DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2);
byte b[4];
byte nB = 0;
@@ -133,24 +173,28 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2, byte count) {
if (byte1!=0) b[nB++] = byte1;
b[nB++] = byte2;
DCCWaveform::mainTrack.schedulePacket(b, nB, count);
DCCQueue::scheduleDCCPacket(b, nB, 0, cab);
}
// returns speed steps 0 to 127 (1 == emergency stop)
// or -1 on "loco not found"
int8_t DCC::getThrottleSpeed(int cab) {
int reg=lookupSpeedTable(cab);
if (reg<0) return -1;
return speedTable[reg].speedCode & 0x7F;
return getThrottleSpeedByte(cab) & 0x7F;
}
// returns speed code byte
// or 128 (speed 0, dir forward) on "loco not found".
// This is the throttle set speed
uint8_t DCC::getThrottleSpeedByte(int cab) {
int reg=lookupSpeedTable(cab);
if (reg<0)
return 128;
return speedTable[reg].speedCode;
LOCO * slot=lookupSpeedTable(cab,false);
return slot?slot->targetSpeed:128;
}
// returns speed code byte for loco.
// This is the most recently send DCC speed packet byte
// or 128 (speed 0, dir forward) on "loco not found".
uint8_t DCC::getLocoSpeedByte(int cab) {
LOCO* slot=lookupSpeedTable(cab,false);
return slot?slot->speedCode:128;
}
// returns 0 to 7 for frequency
@@ -159,12 +203,11 @@ uint8_t DCC::getThrottleFrequency(int cab) {
(void)cab;
return 0;
#else
int reg=lookupSpeedTable(cab);
if (reg<0)
return 0; // use default frequency
LOCO* slot=lookupSpeedTable(cab);
if (!slot) 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);
uint8_t res = (uint8_t)(slot->functions >>29);
//DIAG(F("Speed table %d functions %l shifted %d"), reg, slot->functions, res);
return res;
#endif
}
@@ -172,9 +215,7 @@ uint8_t DCC::getThrottleFrequency(int cab) {
// returns direction on loco
// or true/forward on "loco not found"
bool DCC::getThrottleDirection(int cab) {
int reg=lookupSpeedTable(cab);
if (reg<0) return true;
return (speedTable[reg].speedCode & 0x80) !=0;
return getThrottleSpeedByte(cab) & 0x80;
}
// Set function to value on or off
@@ -198,7 +239,7 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
b[nB++] = (functionNumber & 0x7F) | (on ? 0x80 : 0); // low order bits and state flag
b[nB++] = functionNumber >>7 ; // high order bits
}
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
}
// 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
@@ -207,37 +248,30 @@ bool DCC::setFn( int cab, int16_t functionNumber, bool on) {
if (functionNumber > 31)
return true;
int reg = lookupSpeedTable(cab);
if (reg<0) return false;
LOCO * slot = lookupSpeedTable(cab);
// Take care of functions:
// Set state of function
uint32_t previous=speedTable[reg].functions;
uint32_t previous=slot->functions;
uint32_t funcmask = (1UL<<functionNumber);
if (on) {
speedTable[reg].functions |= funcmask;
slot->functions |= funcmask;
} else {
speedTable[reg].functions &= ~funcmask;
slot->functions &= ~funcmask;
}
if (speedTable[reg].functions != previous) {
if (slot->functions != previous) {
if (functionNumber <= 28)
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
CommandDistributor::broadcastLoco(reg);
updateGroupflags(slot->groupFlags, functionNumber);
CommandDistributor::broadcastLoco(slot);
}
return true;
}
// Flip function state (used from withrottle protocol)
void DCC::changeFn( int cab, int16_t functionNumber) {
if (cab<=0 || functionNumber>31) return;
int reg = lookupSpeedTable(cab);
if (reg<0) return;
unsigned long funcmask = (1UL<<functionNumber);
speedTable[reg].functions ^= funcmask;
if (functionNumber <= 28) {
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
}
CommandDistributor::broadcastLoco(reg);
auto currentValue=getFn(cab,functionNumber);
if (currentValue<0) return; // function not valid for change
setFn(cab,functionNumber, currentValue?false:true);
}
// Report function state (used from withrottle protocol)
@@ -245,12 +279,10 @@ void DCC::changeFn( int cab, int16_t functionNumber) {
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;
auto slot = lookupSpeedTable(cab);
unsigned long funcmask = (1UL<<functionNumber);
return (speedTable[reg].functions & funcmask)? 1 : 0;
return (slot->functions & funcmask)? 1 : 0;
}
// Set the group flag to say we have touched the particular group.
@@ -267,8 +299,22 @@ void DCC::updateGroupflags(byte & flags, int16_t functionNumber) {
uint32_t DCC::getFunctionMap(int cab) {
if (cab<=0) return 0; // unknown pretend all functions off
int reg = lookupSpeedTable(cab);
return (reg<0)?0:speedTable[reg].functions;
auto slot = lookupSpeedTable(cab,false);
return slot?slot->functions:0;
}
// saves DC frequency (0..3) in spare functions 29,30,31
void DCC::setDCFreq(int cab,byte freq) {
if (cab==0 || freq>3) return;
auto slot=lookupSpeedTable(cab,true);
// drop and replace F29,30,31 (top 3 bits)
auto newFunctions=slot->functions & 0x1FFFFFFFUL;
if (freq==1) newFunctions |= (1UL<<29); // F29
else if (freq==2) newFunctions |= (1UL<<30); // F30
else if (freq==3) newFunctions |= (1UL<<31); // F31
if (newFunctions==slot->functions) return; // no change
slot->functions=newFunctions;
CommandDistributor::broadcastLoco(slot);
}
void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
@@ -294,16 +340,17 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
// second byte is of the form 1AAACPPG, where C is 1 for on, PP the ports 0 to 3 and G the gate (coil).
b[0] = address % 64 + 128;
b[1] = ((((address / 64) % 8) << 4) + (port % 4 << 1) + gate % 2) ^ 0xF8;
if (onoff != 0) {
DCCWaveform::mainTrack.schedulePacket(b, 2, 3); // Repeat on packet three times
#if defined(EXRAIL_ACTIVE)
RMFT2::activateEvent(address<<2|port,gate);
#endif
}
if (onoff != 1) {
if (onoff==0) { // off packet only
b[1] &= ~0x08; // set C to 0
DCCWaveform::mainTrack.schedulePacket(b, 2, 3); // Repeat off packet three times
}
DCCQueue::scheduleDCCPacket(b, 2, 3);
} else if (onoff==1) { // on packet only
DCCQueue::scheduleDCCPacket(b, 2, 3);
} else { // auto timed on then off
DCCQueue::scheduleAccOnOffPacket(b, 2, 3, 100); // On then off after 100mS
}
#if defined(EXRAIL_ACTIVE)
if (onoff !=0) RMFT2::activateEvent(address<<2|port,gate);
#endif
}
bool DCC::setExtendedAccessory(int16_t address, int16_t value, byte repeats) {
@@ -353,7 +400,7 @@ whole range of the 11 bits sent to track.
| (((~(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);
DCCQueue::scheduleDCCPacket(b, sizeof(b), repeats);
return true;
}
@@ -372,7 +419,26 @@ void DCC::writeCVByteMain(int cab, int cv, byte bValue) {
b[nB++] = cv2(cv);
b[nB++] = bValue;
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
}
//
// readCVByteMain: Read a byte with PoM on main.
// This requires Railcom active
//
void DCC::readCVByteMain(int cab, int cv, ACK_CALLBACK callback) {
byte b[5];
byte nB = 0;
if (cab > HIGHEST_SHORT_ADDR)
b[nB++] = highByte(cab) | 0xC0; // convert train number into a two-byte address
b[nB++] = lowByte(cab);
b[nB++] = cv1(READ_BYTE_MAIN, cv); // any CV>1023 will become modulus(1024) due to bit-mask of 0x03
b[nB++] = cv2(cv);
b[nB++] = 0;
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
Railcom::anticipate(cab,cv,callback);
}
//
@@ -393,7 +459,45 @@ void DCC::writeCVBitMain(int cab, int cv, byte bNum, bool bValue) {
b[nB++] = cv2(cv);
b[nB++] = WRITE_BIT | (bValue ? BIT_ON : BIT_OFF) | bNum;
DCCWaveform::mainTrack.schedulePacket(b, nB, 4);
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
}
bool DCC::setTime(uint16_t minutes,uint8_t speed, bool suddenChange) {
/* see rcn-122
5 Global commands
These commands are sent and begin exclusively with a broadcast address 0
always with {synchronous bits} 0 0000-0000 … and end with the checksum
... PPPPPPPP 1. Therefore, only the bytes of the commands and not that of
shown below whole package shown. The commands can be used by vehicle and
accessory decoders alike.
5.1 Time command
This command is four bytes long and has the format:
1100-0001 CCxx-xxxx xxxx-xxxxx xxxx-xxxx
CC indicates what data is transmitted in the packet:
CC = 00 Model Time
1100-0001 00MM-MMMM WWWH-HHHH U0BB-BBBB with:
MMMMMM = Minutes, Value range: 0..59
WWW = Day of the Week, Value range: 0 = Monday, 1 = Tuesday, 2 = Wednesday,
3 = Thursday, 4 = Friday, 5 = Saturday, 6 = Sunday, 7 = Weekday
is not supported.
HHHHH = Hours, value range: 0..23
U =
Update, i.e. the time has changed suddenly, e.g. by a new one timetable to start.
Up to 4 can occur per sudden change commands can be marked like this.
BBBBBB = Acceleration factor, value range 0..63. An acceleration factor of 0 means the
model clock has been stopped, a factor of 1 corresponds to real time, at 2 the
clock runs twice as fast, at three times as fast as real time, etc.
*/
if (minutes>=1440 || speed>63 ) return false;
byte b[5];
b[0]=0; // broadcast address
b[1]=0b11000001; // 1100-0001 (model time)
b[2]=minutes % 60 ; // MM
b[3]= 0b11100000 | (minutes/60); // 111H-HHHH weekday not supported
b[4]= (suddenChange ? 0b10000000 : 0) | speed;
DCCQueue::scheduleDCCPacket(b, sizeof(b), 2);
return true;
}
FSH* DCC::getMotorShieldName() {
@@ -515,6 +619,7 @@ const ackOp FLASH LOCO_ID_PROG[] = {
V0, WACK, MERGE,
V0, WACK, MERGE,
VB, WACK, NAKSKIP, // bad read of cv20, assume its 0
BAD20SKIP, // detect invalid cv20 value and ignore
STASHLOCOID, // keep cv 20 until we have cv19 as well.
SETCV, (ackOp)19,
STARTMERGE, // Setup to read cv 19
@@ -615,6 +720,21 @@ const ackOp FLASH SHORT_LOCO_ID_PROG[] = {
CALLFAIL
};
// for CONSIST_ID_PROG the 20,19 values are already calculated
const ackOp FLASH CONSIST_ID_PROG[] = {
BASELINE,
SETCV,(ackOp)20,
SETBYTEH, // high byte to CV 20
WB,WACK,ITSKIP,
FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it
SKIPTARGET,
SETCV,(ackOp)19,
SETBYTEL, // low byte of word
WB,WACK,ITC1, // If ACK, we are done - callback(1) means Ok
VB,WACK,ITC1, // Some decoders do not ack and need verify
CALLFAIL
};
const ackOp FLASH LONG_LOCO_ID_PROG[] = {
BASELINE,
// Clear consist CV 19,20
@@ -689,94 +809,176 @@ void DCC::setLocoId(int id,ACK_CALLBACK callback) {
DCCACK::Setup(id | 0xc000,LONG_LOCO_ID_PROG, callback);
}
void DCC::setConsistId(int id,bool reverse,ACK_CALLBACK callback) {
if (id<0 || id>10239) { //0x27FF according to standard
callback(-1);
return;
}
byte cv20;
byte cv19;
if (id<=HIGHEST_SHORT_ADDR) {
cv19=id;
cv20=0;
}
else {
cv20=id/100;
cv19=id%100;
}
if (reverse) cv19|=0x80;
DCCACK::Setup((cv20<<8)|cv19, CONSIST_ID_PROG, callback);
}
void DCC::forgetLoco(int cab) { // removes any speed reminders for this loco
setThrottle2(cab,1); // ESTOP this loco if still on track
int reg=lookupSpeedTable(cab, false);
if (reg>=0) {
speedTable[reg].loco=0;
setThrottle2(cab,1); // ESTOP if this loco still on track
auto slot=lookupSpeedTable(cab, false);
if (slot) {
slot->loco=-1; // no longer used but not end of world
CommandDistributor::broadcastForgetLoco(cab);
}
}
void DCC::forgetAllLocos() { // removes all speed reminders
setThrottle2(0,1); // ESTOP all locos still on track
for (int i=0;i<MAX_LOCOS;i++) speedTable[i].loco=0;
for (int i=0;i<MAX_LOCOS;i++) {
if (speedTable[i].loco) CommandDistributor::broadcastForgetLoco(speedTable[i].loco);
speedTable[i].loco=0; // no longer used and looks like end
}
}
byte DCC::loopStatus=0;
void DCC::loop() {
TrackManager::loop(); // power overload checks
issueReminders();
if (DCCWaveform::mainTrack.isReminderWindowOpen()) {
// Now is a good time to choose a packet to be sent
// Either highest priority from the queues or a reminder
if (!DCCQueue::scheduleNext(false)) {
// none pending,
issueReminders();
DCCQueue::scheduleNext(true); // send any pending and force an idle if none
}
}
}
void DCC::issueReminders() {
// if the main track transmitter still has a pending packet, skip this time around.
if (!DCCWaveform::mainTrack.isReminderWindowOpen()) return;
while(true) {
// Move to next loco slot. If occupied, send a reminder.
int reg = lastLocoReminder+1;
if (reg > highestUsedReg) reg = 0; // Go to start of table
if (speedTable[reg].loco > 0) {
// have found loco to remind
if (issueReminder(reg))
lastLocoReminder = reg;
} else
lastLocoReminder = reg;
// slot.loco is -1 for deleted locos, 0 for end of list.
for (auto slot=nextLocoReminder;slot->loco;slot++) {
if (slot->loco<0) continue; // deleted loco, skip it
if (issueReminder(slot)) {
nextLocoReminder=slot+1; // remember next one to check
return; // reminder sent, exit
}
}
// we have reached the end of the table, so we can move on to
// the next loop state and start from the top.
// There are 0-9 loop states.. speed,f1,speed,f2,speed,f3,speed,f4,speed,f5
loopStatus++;
if (loopStatus>9) loopStatus=0; // reset to 0
// try looking from the start of the table down to where we started last time
for (auto slot=&speedTable[0];slot<nextLocoReminder;slot++) {
if (slot->loco<0) continue; // deleted loco, skip it
if (issueReminder(slot)) {
nextLocoReminder=slot+1; // remember next one to check
return; // reminder sent, exit
}
}
// if we get here then we can update the loop status and start again
if (loopStatus==0) return; // nothing found at all
}
}
bool DCC::issueReminder(int reg) {
unsigned long functions=speedTable[reg].functions;
int loco=speedTable[reg].loco;
byte flags=speedTable[reg].groupFlags;
int16_t normalize(byte speed) {
if (speed & 0x80) return speed & 0x7F;
return 0-1-speed;
}
byte dccalize(int16_t speed) {
if (speed>127) return 0xFF; // 127 forward
if (speed<-127) return 0x7F; // 127 reverse
if (speed >=0) return speed | 0x80;
// negative speeds... -1==dcc 0, -2==dcc 1
return (int16_t)-1 - speed;
}
bool DCC::issueReminder(LOCO * slot) {
unsigned long functions=slot->functions;
int loco=slot->loco;
byte flags=slot->groupFlags;
switch (loopStatus) {
case 0:
// DIAG(F("Reminder %d speed %d"),loco,speedTable[reg].speedCode);
setThrottle2(loco, speedTable[reg].speedCode);
break;
case 2:
case 4:
case 6:
case 8: {
// calculate any momentum change going on
auto sc=slot->speedCode;
if (slot->targetSpeed!=sc) {
// calculate new speed code
auto now=millis();
int16_t delay=now-slot->momentum_base;
auto millisPerNotch=MOMENTUM_FACTOR * (int16_t)getMomentum(slot);
// allow for momentum change to 0 while accelerating/slowing
auto ticks=(millisPerNotch>0)?(delay/millisPerNotch):500;
if (ticks>0) {
auto current=normalize(sc); // -128..+127
auto target=normalize(slot->targetSpeed);
// DIAG(F("Momentum l=%d ti=%d sc=%d c=%d t=%d"),loco,ticks,sc,current,target);
if (current<target) { // accelerate
current+=ticks;
if (current>target) current=target;
}
else { // slow
current-=ticks;
if (current<target) current=target;
}
sc=dccalize(current);
//DIAG(F("c=%d newsc=%d"),current,sc);
slot->speedCode=sc;
TrackManager::setDCSignal(loco,sc); // in case this is a dcc track on this addr
slot->momentum_base=now;
}
}
// DIAG(F("Reminder %d speed %d"),loco,slot->speedCode);
setThrottle2(loco, sc);
}
return true; // reminder sent
case 1: // remind function group 1 (F0-F4)
if (flags & FN_GROUP_1)
#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
if (flags & FN_GROUP_1) {
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
return true; // reminder sent
}
break;
case 2: // remind function group 2 F5-F8
if (flags & FN_GROUP_2)
#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
case 3: // remind function group 2 F5-F8
if (flags & FN_GROUP_2) {
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD
return true; // reminder sent
}
break;
case 3: // remind function group 3 F9-F12
if (flags & FN_GROUP_3)
#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
case 5: // remind function group 3 F9-F12
if (flags & FN_GROUP_3) {
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD
return true; // reminder sent
}
break;
case 4: // remind function group 4 F13-F20
if (flags & FN_GROUP_4)
setFunctionInternal(loco,222, ((functions>>13)& 0xFF),2);
flags&= ~FN_GROUP_4; // dont send them again
case 7: // remind function group 4 F13-F20
if (flags & FN_GROUP_4) {
setFunctionInternal(loco,222, ((functions>>13)& 0xFF));
return true;
}
break;
case 5: // remind function group 5 F21-F28
if (flags & FN_GROUP_5)
setFunctionInternal(loco,223, ((functions>>21)& 0xFF),2);
flags&= ~FN_GROUP_5; // dont send them again
case 9: // remind function group 5 F21-F28
if (flags & FN_GROUP_5) {
setFunctionInternal(loco,223, ((functions>>21)& 0xFF));
return true; // reminder sent
}
break;
}
loopStatus++;
// if we reach status 6 then this loco is done so
// reset status to 0 for next loco and return true so caller
// moves on to next loco.
if (loopStatus>5) loopStatus=0;
return loopStatus==0;
return false; // no reminder sent
}
@@ -793,70 +995,128 @@ byte DCC::cv2(int cv) {
return lowByte(cv);
}
int DCC::lookupSpeedTable(int locoId, bool autoCreate) {
DCC::LOCO * DCC::lookupSpeedTable(int locoId, bool autoCreate) {
// determine speed reg for this loco
int firstEmpty = MAX_LOCOS;
int reg;
for (reg = 0; reg < MAX_LOCOS; reg++) {
if (speedTable[reg].loco == locoId) break;
if (speedTable[reg].loco == 0 && firstEmpty == MAX_LOCOS) firstEmpty = reg;
LOCO * firstEmpty=nullptr;
SLOTLOOP {
if (firstEmpty==nullptr && slot->loco<=0) firstEmpty=slot;
if (slot->loco == locoId) return slot;
if (slot->loco==0) break;
}
// return -1 if not found and not auto creating
if (reg== MAX_LOCOS && !autoCreate) return -1;
if (reg == MAX_LOCOS) reg = firstEmpty;
if (reg >= MAX_LOCOS) {
DIAG(F("Too many locos"));
return -1;
if (!autoCreate) return nullptr;
if (firstEmpty==nullptr) {
// return last slot if full
DIAG(F("Too many locos, reusing last slot"));
firstEmpty=&speedTable[MAX_LOCOS-1];
}
if (reg==firstEmpty){
speedTable[reg].loco = locoId;
speedTable[reg].speedCode=128; // default direction forward
speedTable[reg].groupFlags=0;
speedTable[reg].functions=0;
}
if (reg > highestUsedReg) highestUsedReg = reg;
return reg;
// fill first empty slot with new entry
firstEmpty->loco = locoId;
firstEmpty->speedCode=128; // default direction forward
firstEmpty->targetSpeed=128; // default direction forward
firstEmpty->groupFlags=0;
firstEmpty->functions=0;
firstEmpty->momentumA=MOMENTUM_USE_DEFAULT;
firstEmpty->momentumD=MOMENTUM_USE_DEFAULT;
return firstEmpty;
}
void DCC::updateLocoReminder(int loco, byte speedCode) {
if (loco==0) {
// broadcast stop/estop but dont change direction
for (int reg = 0; reg <= highestUsedReg; reg++) {
if (speedTable[reg].loco==0) continue;
byte newspeed=(speedTable[reg].speedCode & 0x80) | (speedCode & 0x7f);
if (speedTable[reg].speedCode != newspeed) {
speedTable[reg].speedCode = newspeed;
CommandDistributor::broadcastLoco(reg);
}
}
return;
bool DCC::setMomentum(int locoId,int16_t accelerating, int16_t decelerating) {
if (locoId<0) return false;
if (locoId==0) {
if (accelerating<0 || decelerating<0) return false;
defaultMomentumA=accelerating/MOMENTUM_FACTOR;
defaultMomentumD=decelerating/MOMENTUM_FACTOR;
return true;
}
// -1 is ok and means this loco should use the default.
if (accelerating<-1 || decelerating<-1) return false;
if (accelerating/MOMENTUM_FACTOR >= MOMENTUM_USE_DEFAULT ||
decelerating/MOMENTUM_FACTOR >= MOMENTUM_USE_DEFAULT) return false;
// Values stored are 255=MOMENTUM_USE_DEFAULT, or millis/MOMENTUM_FACTOR.
// This is to keep the values in a byte rather than int16
// thus saving 2 bytes RAM per loco slot.
LOCO* slot=lookupSpeedTable(locoId,true);
slot->momentumA=(accelerating<0)? MOMENTUM_USE_DEFAULT: (accelerating/MOMENTUM_FACTOR);
slot->momentumD=(decelerating<0)? MOMENTUM_USE_DEFAULT: (decelerating/MOMENTUM_FACTOR);
return true;
}
// determine speed reg for this loco
int reg=lookupSpeedTable(loco);
if (reg>=0 && speedTable[reg].speedCode!=speedCode) {
speedTable[reg].speedCode = speedCode;
CommandDistributor::broadcastLoco(reg);
void DCC::estopAll() {
setThrottle2(0,1); // estop all locos
TrackManager::setDCSignal(0,1);
// remind stop/estop but dont change direction
SLOTLOOP {
if (slot->loco<=0) continue;
byte newspeed=(slot->targetSpeed & 0x80) | 0x01;
slot->speedCode = newspeed;
slot->targetSpeed = newspeed;
CommandDistributor::broadcastLoco(slot);
}
}
DCC::LOCO DCC::speedTable[MAX_LOCOS];
int DCC::lastLocoReminder = 0;
int DCC::highestUsedReg = 0;
DCC::LOCO * DCC::nextLocoReminder = &DCC::speedTable[0];
void DCC::displayCabList(Print * stream) {
StringFormatter::send(stream,F("<*\n"));
int used=0;
for (int reg = 0; reg <= highestUsedReg; reg++) {
if (speedTable[reg].loco>0) {
SLOTLOOP {
if (slot->loco==0) break; // no more locos
if (slot->loco>0) {
used ++;
StringFormatter::send(stream,F("cab=%d, speed=%d, dir=%c \n"),
speedTable[reg].loco, speedTable[reg].speedCode & 0x7f,(speedTable[reg].speedCode & 0x80) ? 'F':'R');
StringFormatter::send(stream,F("cab=%d, speed=%d, target=%d, momentum=%d/%d, block=%d\n"),
slot->loco, slot->speedCode, slot->targetSpeed,
slot->momentumA, slot->momentumD, slot->blockOccupied);
}
}
StringFormatter::send(stream,F("Used=%d, max=%d\n"),used,MAX_LOCOS);
StringFormatter::send(stream,F("Used=%d, max=%d, momentum=%d/%d *>\n"),
used,MAX_LOCOS, DCC::defaultMomentumA,DCC::defaultMomentumD);
}
void DCC::setLocoInBlock(int loco, uint16_t blockid, bool exclusive) {
// update block loco is in, tell exrail leaving old block, and entering new.
// NOTE: The loco table scanning is really inefficient and needs rewriting
// This was done once in the momentum poc.
#ifdef EXRAIL_ACTIVE
auto slot=lookupSpeedTable(loco,true);
if (!slot) return;
auto oldBlock=slot->blockOccupied;
if (oldBlock==blockid) return;
if (oldBlock) RMFT2::blockEvent(oldBlock,loco,false);
slot->blockOccupied=blockid;
if (blockid) RMFT2::blockEvent(blockid,loco,true);
if (exclusive) {
SLOTLOOP {
if (slot->loco==0) break; // no more locos
if (slot->loco>0) {
if (slot->loco!=loco && slot->blockOccupied==blockid) {
RMFT2::blockEvent(blockid,slot->loco,false);
slot->blockOccupied=0;
}
}
}
}
#endif
}
void DCC::clearBlock(uint16_t blockid) {
// Railcom reports block empty... tell Exrail about all leavers
#ifdef EXRAIL_ACTIVE
SLOTLOOP {
if (slot->loco==0) break; // no more locos
if (slot->loco>0) {
if (slot->blockOccupied==blockid) {
RMFT2::blockEvent(blockid,slot->loco,false);
slot->blockOccupied=0;
}
}
}
#endif
}

37
DCC.h
View File

@@ -3,7 +3,7 @@
* © 2021 Fred Decker
* © 2021 Herb Morton
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020-2025 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
@@ -59,17 +59,22 @@ public:
// Public DCC API functions
static void setThrottle(uint16_t cab, uint8_t tSpeed, bool tDirection);
static void estopAll();
static int8_t getThrottleSpeed(int cab);
static uint8_t getThrottleSpeedByte(int cab);
static uint8_t getLocoSpeedByte(int cab); // may lag throttle
static uint8_t getThrottleFrequency(int cab);
static bool getThrottleDirection(int cab);
static void writeCVByteMain(int cab, int cv, byte bValue);
static void readCVByteMain(int cab, int cv, ACK_CALLBACK callback);
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 int8_t getFn(int cab, int16_t functionNumber);
static uint32_t getFunctionMap(int cab);
static void setDCFreq(int cab,byte freq);
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);
@@ -82,10 +87,12 @@ public:
static void writeCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback);
static void verifyCVByte(int16_t cv, byte byteValue, ACK_CALLBACK callback);
static void verifyCVBit(int16_t cv, byte bitNum, bool bitValue, ACK_CALLBACK callback);
static bool setTime(uint16_t minutes,uint8_t speed, bool suddenChange);
static void setLocoInBlock(int loco, uint16_t blockid, bool exclusive);
static void clearBlock(uint16_t blockid);
static void getLocoId(ACK_CALLBACK callback);
static void setLocoId(int id,ACK_CALLBACK callback);
static void setConsistId(int id,bool reverse,ACK_CALLBACK callback);
// Enhanced API functions
static void forgetLoco(int cab); // removes any speed reminders for this loco
static void forgetAllLocos(); // removes all speed reminders
@@ -101,20 +108,31 @@ public:
byte speedCode;
byte groupFlags;
uint32_t functions;
// Momentum management variables
uint32_t momentum_base; // millis() when speed modified under momentum
byte momentumA, momentumD;
byte targetSpeed; // speed set by throttle
uint16_t blockOccupied; // railcom detected block
};
static const int16_t MOMENTUM_FACTOR=7;
static const byte MOMENTUM_USE_DEFAULT=255;
static bool linearAcceleration;
static byte getMomentum(LOCO * slot);
static LOCO speedTable[MAX_LOCOS];
static int lookupSpeedTable(int locoId, bool autoCreate=true);
static LOCO * lookupSpeedTable(int locoId, bool autoCreate=true);
static byte cv1(byte opcode, int cv);
static byte cv2(int cv);
static bool setMomentum(int locoId,int16_t accelerating, int16_t decelerating);
private:
static byte loopStatus;
static byte defaultMomentumA; // Accelerating
static byte defaultMomentumD; // Accelerating
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, byte count);
static bool issueReminder(int reg);
static int lastLocoReminder;
static int highestUsedReg;
static void setFunctionInternal(int cab, byte fByte, byte eByte);
static bool issueReminder(LOCO * slot);
static LOCO* nextLocoReminder;
static FSH *shieldName;
static byte globalSpeedsteps;
@@ -125,6 +143,7 @@ private:
// NMRA codes #
static const byte SET_SPEED = 0x3f;
static const byte WRITE_BYTE_MAIN = 0xEC;
static const byte READ_BYTE_MAIN = 0xE4;
static const byte WRITE_BIT_MAIN = 0xE8;
static const byte WRITE_BYTE = 0x7C;
static const byte VERIFY_BYTE = 0x74;

View File

@@ -27,8 +27,8 @@
#include "DCCWaveform.h"
#include "TrackManager.h"
unsigned int DCCACK::minAckPulseDuration = 2000; // micros
unsigned int DCCACK::maxAckPulseDuration = 20000; // micros
unsigned long DCCACK::minAckPulseDuration = 2000; // micros
unsigned long DCCACK::maxAckPulseDuration = 20000; // micros
MotorDriver * DCCACK::progDriver=NULL;
ackOp const * DCCACK::ackManagerProg;
@@ -50,8 +50,8 @@ volatile uint8_t DCCACK::numAckSamples=0;
uint8_t DCCACK::trailingEdgeCounter=0;
unsigned int DCCACK::ackPulseDuration; // micros
unsigned long DCCACK::ackPulseStart; // micros
unsigned long DCCACK::ackPulseDuration; // micros
unsigned long DCCACK::ackPulseStart; // micros
volatile bool DCCACK::ackDetected;
unsigned long DCCACK::ackCheckStart; // millis
volatile bool DCCACK::ackPending;
@@ -67,16 +67,24 @@ CALLBACK_STATE DCCACK::callbackState=READY;
ACK_CALLBACK DCCACK::ackManagerCallback;
void DCCACK::Setup(int cv, byte byteValueOrBitnum, ackOp const program[], ACK_CALLBACK callback) {
// On ESP32 the joined track is hidden from sight (it has type MAIN)
// and because of that we need first check if track was joined and
// then unjoin if necessary. This requires that the joined flag is
// cleared when the prog track is removed.
ackManagerRejoin=TrackManager::isJoined();
//DIAG(F("Joined is %d"), ackManagerRejoin);
if (ackManagerRejoin) {
// Change from JOIN must zero resets packet.
TrackManager::setJoin(false);
DCCWaveform::progTrack.clearResets();
}
progDriver=TrackManager::getProgDriver();
//DIAG(F("Progdriver is %d"), progDriver);
if (progDriver==NULL) {
TrackManager::setJoin(ackManagerRejoin);
if (ackManagerRejoin) {
DIAG(F("Joined but no Prog track"));
TrackManager::setJoin(false);
}
callback(-3); // we dont have a prog track!
return;
}
@@ -127,7 +135,7 @@ bool DCCACK::checkResets(uint8_t numResets) {
void DCCACK::setAckBaseline() {
int baseline=progDriver->getCurrentRaw();
ackThreshold= baseline + progDriver->mA2raw(ackLimitmA);
if (Diag::ACK) DIAG(F("ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %uus and %uus"),
if (Diag::ACK) DIAG(F("ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %lus and %lus"),
baseline,progDriver->raw2mA(baseline),
ackThreshold,progDriver->raw2mA(ackThreshold),
minAckPulseDuration, maxAckPulseDuration);
@@ -146,7 +154,7 @@ void DCCACK::setAckPending() {
byte DCCACK::getAck() {
if (ackPending) return (2); // still waiting
if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%uuS samples=%d gaps=%d"),ackDetected?F("ACK"):F("NO-ACK"), ackCheckDuration,
if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%luS samples=%d gaps=%d"),ackDetected?F("ACK"):F("NO-ACK"), ackCheckDuration,
ackMaxCurrent,progDriver->raw2mA(ackMaxCurrent), ackPulseDuration, numAckSamples, numAckGaps);
if (ackDetected) return (1); // Yes we had an ack
return(0); // pending set off but not detected means no ACK.
@@ -339,6 +347,20 @@ void DCCACK::loop() {
opcode=GETFLASH(ackManagerProg);
}
break;
case BAD20SKIP:
if (ackManagerByte > 120) {
// skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255)
if (Diag::ACK) DIAG(F("XX cv20=%d "),ackManagerByte);
while (opcode!=SKIPTARGET) {
ackManagerProg++;
opcode=GETFLASH(ackManagerProg);
}
}
break;
case FAIL_IF_NONZERO_NAK: // fail if writing long address to decoder that cant support it
if (ackManagerByte==0) break;
callback(-4);
return;
case SKIPTARGET:
break;
default:
@@ -483,4 +505,3 @@ void DCCACK::checkAck(byte sentResetsSincePacket) {
}
ackPulseStart=0; // We have detected a too-short or too-long pulse so ignore and wait for next leading edge
}

View File

@@ -58,6 +58,8 @@ enum ackOp : byte
ITSKIP, // skip to SKIPTARGET if ack true
NAKSKIP, // skip to SKIPTARGET if ack false
COMBINE1920, // combine cvs 19 and 20 and callback
BAD20SKIP, // skip to SKIPTARGET if cv20 is >120 (some decoders respond with 255)
FAIL_IF_NONZERO_NAK, // fail if writing long address to decoder that cant support it
SKIPTARGET = 0xFF // jump to target
};
@@ -79,10 +81,10 @@ class DCCACK {
static inline void setAckLimit(int mA) {
ackLimitmA = mA;
}
static inline void setMinAckPulseDuration(unsigned int i) {
static inline void setMinAckPulseDuration(unsigned long i) {
minAckPulseDuration = i;
}
static inline void setMaxAckPulseDuration(unsigned int i) {
static inline void setMaxAckPulseDuration(unsigned long i) {
maxAckPulseDuration = i;
}
@@ -126,11 +128,11 @@ class DCCACK {
static unsigned long ackCheckStart; // millis
static unsigned int ackCheckDuration; // millis
static unsigned int ackPulseDuration; // micros
static unsigned long ackPulseDuration; // micros
static unsigned long ackPulseStart; // micros
static unsigned int minAckPulseDuration ; // micros
static unsigned int maxAckPulseDuration ; // micros
static unsigned long minAckPulseDuration ; // micros
static unsigned long maxAckPulseDuration ; // micros
static MotorDriver* progDriver;
static volatile uint8_t numAckGaps;
static volatile uint8_t numAckSamples;

View File

@@ -2,11 +2,11 @@
* © 2022 Paul M Antoine
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021 Herb Morton
* © 2021-2024 Herb Morton
* © 2020-2023 Harald Barth
* © 2020-2021 M Steve Todd
* © 2020-2021 Fred Decker
* © 2020-2021 Chris Harlow
* © 2020-2025 Chris Harlow
* © 2022 Colin Murdoch
* All rights reserved.
*
@@ -64,15 +64,16 @@ Once a new OPCODE is decided upon, update this list.
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
k, Block exit (Railcom)
K, Block enter (Railcom)
l, Loco speedbyte/function map broadcast
L, Reserved for LCC interface (implemented in EXRAIL)
m, message to throttles broadcast
m, message to throttles (broadcast output)
m, set momentum
M, Write DCC packet
n,
N,
o,
n, Reserved for SensorCam
N, Reserved for Sensorcam
o, Neopixel driver (see also IO_NeoPixel.h)
O, Output broadcast
p, Broadcast power state
P, Write DCC packet
@@ -91,10 +92,10 @@ Once a new OPCODE is decided upon, update this list.
w, Write CV on main
W, Write CV
x,
X, Invalid command
y,
X, Invalid command response
y,
Y, Output broadcast
z,
z, Direct output
Z, Output configuration/control
*/
@@ -117,6 +118,11 @@ Once a new OPCODE is decided upon, update this list.
#include "Turntables.h"
#include "version.h"
#include "KeywordHasher.h"
#include "CamParser.h"
#include "Stash.h"
#ifdef ARDUINO_ARCH_ESP32
#include "WifiESP32.h"
#endif
// This macro can't be created easily as a portable function because the
// flashlist requires a far pointer for high flash access.
@@ -140,12 +146,12 @@ byte DCCEXParser::stashTarget=0;
// Non-DCC things like turnouts, pins and sensors are handled in additional JMRI interface classes.
int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte *cmd, bool usehex)
int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], byte *cmd, bool usehex)
{
byte state = 1;
byte parameterCount = 0;
int16_t runningValue = 0;
const byte *remainingCmd = cmd + 1; // skips the opcode
byte *remainingCmd = cmd + 1; // skips the opcode
bool signNegative = false;
// clear all parameters in case not enough found
@@ -155,7 +161,6 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
while (parameterCount < MAX_COMMAND_PARAMS)
{
byte hot = *remainingCmd;
switch (state)
{
@@ -164,12 +169,29 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
break;
if (hot == '\0')
return -1;
if (hot == '>')
if (hot == '>') {
*remainingCmd = '\0'; // terminate the cmd string with 0 instead of '>'
return parameterCount;
}
state = 2;
continue;
case 2: // checking sign
case 2: // checking sign or quoted string
#ifdef HAS_ENOUGH_MEMORY
if (hot == '"') {
// this inserts an extra parameter 0x7777 in front
// of each string parameter as a marker that can
// be checked that a string parameter follows
// This clashes of course with the real value
// 0x7777 which we hope is used seldom
result[parameterCount] = (int16_t)0x7777;
parameterCount++;
result[parameterCount] = (int16_t)(remainingCmd - cmd + 1);
parameterCount++;
state = 4;
break;
}
#endif
signNegative = false;
runningValue = 0;
state = 3;
@@ -200,6 +222,16 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
parameterCount++;
state = 1;
continue;
#ifdef HAS_ENOUGH_MEMORY
case 4: // skipover text
if (hot == '\0') // We did run to end of buffer without finding the "
return -1;
if (hot == '"') {
*remainingCmd = '\0'; // overwrite " in command buffer with the end-of-string
state = 1;
}
break;
#endif
}
remainingCmd++;
}
@@ -209,6 +241,7 @@ int16_t DCCEXParser::splitValues(int16_t result[MAX_COMMAND_PARAMS], const byte
extern __attribute__((weak)) void myFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
FILTER_CALLBACK DCCEXParser::filterCallback = myFilter;
FILTER_CALLBACK DCCEXParser::filterRMFTCallback = 0;
FILTER_CALLBACK DCCEXParser::filterCamParserCallback = 0;
AT_COMMAND_CALLBACK DCCEXParser::atCommandCallback = 0;
// deprecated
@@ -220,6 +253,10 @@ void DCCEXParser::setRMFTFilter(FILTER_CALLBACK filter)
{
filterRMFTCallback = filter;
}
void DCCEXParser::setCamParserFilter(FILTER_CALLBACK filter)
{
filterCamParserCallback = filter;
}
void DCCEXParser::setAtCommandCallback(AT_COMMAND_CALLBACK callback)
{
atCommandCallback = callback;
@@ -237,17 +274,22 @@ void DCCEXParser::parse(const FSH * cmd) {
// See documentation on DCC class for info on this section
void DCCEXParser::parse(Print *stream, byte *com, RingStream *ringStream) {
// This function can get stings of the form "<C OMM AND>" or "C OMM AND"
// found is true first after the leading "<" has been passed
// This function can get stings of the form "<C OMM AND>" or "C OMM AND>"
// found is true first after the leading "<" has been passed which results
// in parseOne() getting c="C OMM AND>"
byte *cForLater = NULL;
bool found = (com[0] != '<');
for (byte *c=com; c[0] != '\0'; c++) {
if (found) {
parseOne(stream, c, ringStream);
cForLater = c;
found=false;
}
if (c[0] == '<')
if (c[0] == '<') {
if (cForLater) parseOne(stream, cForLater, ringStream);
found = true;
}
}
if (cForLater) parseOne(stream, cForLater, ringStream);
}
void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
@@ -275,6 +317,8 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
filterCallback(stream, opcode, params, p);
if (filterRMFTCallback && opcode!='\0')
filterRMFTCallback(stream, opcode, params, p);
if (filterCamParserCallback && opcode!='\0')
filterCamParserCallback(stream, opcode, params, p);
// Functions return from this switch if complete, break from switch implies error <X> to send
switch (opcode)
@@ -288,12 +332,9 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
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 (p[0]<=0) break;
CommandDistributor::broadcastLoco(DCC::lookupSpeedTable(p[0],false));
return;
}
if (params == 4)
@@ -374,7 +415,8 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
|| (p[activep] > 1) || (p[activep] < 0) // invalid activate 0|1
) break;
// Honour the configuration option (config.h) which allows the <a> command to be reversed
#ifdef DCC_ACCESSORY_COMMAND_REVERSE
// Because of earlier confusion we need to do the same thing under both defines
#if defined(DCC_ACCESSORY_COMMAND_REVERSE)
DCC::setAccessory(address, subaddress,p[activep]==0,onoff);
#else
DCC::setAccessory(address, subaddress,p[activep]==1,onoff);
@@ -394,14 +436,43 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
return;
break;
case 'z': // direct pin manipulation
#ifndef IO_NO_HAL
case 'o': // Neopixel pin manipulation
if (p[0]==0) break;
{
VPIN vpin=p[0]>0 ? p[0]:-p[0];
bool setON=p[0]>0;
if (params==1) { // <o [-]vpin>
IODevice::write(vpin,setON);
return;
}
if (params==2) { // <o [-]vpin count>
IODevice::writeRange(vpin,setON,p[1]);
return;
}
if (params==4 || params==5) { // <z [-]vpin r g b [count]>
auto count=p[4]?p[4]:1;
if (p[1]<0 || p[1]>0xFF) break;
if (p[2]<0 || p[2]>0xFF) break;
if (p[3]<0 || p[3]>0xFF) break;
// strange parameter mangling... see IO_NeoPixel.h NeoPixel::_writeAnalogue
int colour_RG=(p[1]<<8) | p[2];
uint16_t colour_B=p[3];
IODevice::writeAnalogueRange(vpin,colour_RG,setON,colour_B,count);
return;
}
}
break;
#endif
case 'z': // direct pin manipulation
if (p[0]==0) break;
if (params==1) { // <z vpin | -vpin>
if (p[0]>0) IODevice::write(p[0],HIGH);
else IODevice::write(-p[0],LOW);
return;
}
if (params>=2 && params<=4) { // <z vpin ana;og profile duration>
if (params>=2 && params<=4) { // <z vpin analog profile duration>
// unused params default to 0
IODevice::writeAnalogue(p[0],p[1],p[2],p[3]);
return;
@@ -425,12 +496,35 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
DCC::writeCVByteMain(p[0], p[1], p[2]);
return;
#ifdef HAS_ENOUGH_MEMORY
case 'r': // READ CV on MAIN <r CAB CV> Requires Railcom
if (params != 2)
break;
if (!DCCWaveform::isRailcom()) break;
if (!stashCallback(stream, p, ringStream)) break;
DCC::readCVByteMain(p[0], p[1],callback_r);
return;
#endif
case 'b': // WRITE CV BIT ON MAIN <b CAB CV BIT VALUE>
if (params != 4)
break;
DCC::writeCVBitMain(p[0], p[1], p[2], p[3]);
return;
#endif
case 'm': // <m cabid momentum [braking]>
// <m LINEAR|POWER>
if (params==1) {
if (p[0]=="LINEAR"_hk) DCC::linearAcceleration=true;
else if (p[0]=="POWER"_hk) DCC::linearAcceleration=false;
else break;
return;
}
if (params<2 || params>3) break;
if (params==2) p[2]=p[1];
if (DCC::setMomentum(p[0],p[1],p[2])) return;
break;
case 'M': // WRITE TRANSPARENT DCC PACKET MAIN <M REG X1 ... X9>
#ifndef DISABLE_PROG
@@ -458,6 +552,9 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
DCC::setLocoId(p[0],callback_Wloco);
else if (params == 4) // WRITE CV ON PROG <W CV VALUE [CALLBACKNUM] [CALLBACKSUB]>
DCC::writeCVByte(p[0], p[1], callback_W4);
else if ((params==2 || params==3 ) && p[0]=="CONSIST"_hk ) {
DCC::setConsistId(p[1],p[2]=="REVERSE"_hk,callback_Wconsist);
}
else if (params == 2) // WRITE CV ON PROG <W CV VALUE>
DCC::writeCVByte(p[0], p[1], callback_W);
else
@@ -518,7 +615,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
{
if (params > 1) break;
if (params==0) { // All
TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::ON);
TrackManager::setTrackPower(TRACK_ALL, POWERMODE::ON);
}
if (params==1) {
if (p[0]=="MAIN"_hk) { // <1 MAIN>
@@ -551,7 +648,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
if (params > 1) break;
if (params==0) { // All
TrackManager::setJoin(false);
TrackManager::setTrackPower(TRACK_MODE_ALL, POWERMODE::OFF);
TrackManager::setTrackPower(TRACK_ALL, POWERMODE::OFF);
}
if (params==1) {
if (p[0]=="MAIN"_hk) { // <0 MAIN>
@@ -560,6 +657,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
}
#ifndef DISABLE_PROG
else if (p[0]=="PROG"_hk) { // <0 PROG>
TrackManager::setJoin(false);
TrackManager::progTrackBoosted=false; // Prog track boost mode will not outlive prog track off
TrackManager::setTrackPower(TRACK_MODE_PROG, POWERMODE::OFF);
}
@@ -576,7 +674,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
}
case '!': // ESTOP ALL <!>
DCC::setThrottle(0,1,1); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
DCC::estopAll(); // this broadcasts speed 1(estop) and sets all reminders to speed 1.
return;
#ifdef HAS_ENOUGH_MEMORY
@@ -612,9 +710,22 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
StringFormatter::send(stream, F("\n"));
return;
case 'C': // CONFIG <C [params]>
if (parseC(stream, params, p))
return;
break;
#if defined(ARDUINO_ARCH_ESP32)
// currently this only works on ESP32
#if defined(HAS_ENOUGH_MEMORY)
if (p[0] == "WIFI"_hk) { // <C WIFI SSID PASSWORD>
if (params != 5) // the 5 params 0 to 4 are (kinda): WIFI_hk 0x7777 &SSID 0x7777 &PASSWORD
break;
if (p[1] == 0x7777 && p[3] == 0x7777) {
WifiESP::setup((const char*)(com + p[2]), (const char*)(com + p[4]), WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
}
return;
}
#endif
#endif //ESP32
if (parseC(stream, params, p))
return;
break;
#ifndef DISABLE_DIAG
case 'D': // DIAG <D [params]>
if (parseD(stream, params, p))
@@ -638,6 +749,13 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
case 'F': // New command to call the new Loco Function API <F cab func 1|0>
if(params!=3) break;
if (p[1]=="DCFREQ"_hk) { // <F cab DCFREQ 0..3>
if (p[2]<0 || p[2]>3) break;
DCC::setDCFreq(p[0],p[2]);
return;
}
if (Diag::CMD)
DIAG(F("Setting loco %d F%d %S"), p[0], p[1], p[2] ? F("ON") : F("OFF"));
if (DCC::setFn(p[0], p[1], p[2] == 1)) return;
@@ -655,7 +773,7 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
case 'J' : // throttle info access
{
if ((params<1) | (params>3)) break; // <J>
if (params<1) break; // <J>
//if ((params<1) | (params>2)) break; // <J>
int16_t id=(params==2)?p[1]:0;
switch(p[0]) {
@@ -683,11 +801,10 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
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 "M"_hk: // <JM> Stash management
if (parseJM(stream, params, p))
return;
break;
case "R"_hk: // <JR> returns rosters
StringFormatter::send(stream, F("<jR"));
@@ -796,9 +913,10 @@ void DCCEXParser::parseOne(Print *stream, byte *com, RingStream * ringStream)
return;
break;
#endif
case '/': // implemented in EXRAIL parser
case 'L': // LCC interface implemented in EXRAIL parser
break; // Will <X> if not intercepted by EXRAIL
case 'N': // interface implemented in CamParser
break; // Will <X> if not intercepted by filters
#ifndef DISABLE_VDPY
case '@': // JMRI saying "give me virtual LCD msgs"
@@ -1069,15 +1187,24 @@ bool DCCEXParser::parseC(Print *stream, int16_t params, int16_t p[]) {
#ifndef DISABLE_PROG
case "ACK"_hk: // <D ACK ON/OFF> <D ACK [LIMIT|MIN|MAX|RETRY] Value>
if (params >= 3) {
long duration;
if (p[1] == "LIMIT"_hk) {
DCCACK::setAckLimit(p[2]);
LCD(1, F("Ack Limit=%dmA"), p[2]); // <D ACK LIMIT 42>
LCD(1, F("Ack Limit=%dmA"), p[2]); // <D ACK LIMIT 42>
} else if (p[1] == "MIN"_hk) {
DCCACK::setMinAckPulseDuration(p[2]);
LCD(0, F("Ack Min=%uus"), p[2]); // <D ACK MIN 1500>
if (params == 4 && p[3] == "MS"_hk)
duration = p[2] * 1000L;
else
duration = p[2];
DCCACK::setMinAckPulseDuration(duration);
LCD(0, F("Ack Min=%lus"), duration); // <D ACK MIN 1500>
} else if (p[1] == "MAX"_hk) {
DCCACK::setMaxAckPulseDuration(p[2]);
LCD(0, F("Ack Max=%uus"), p[2]); // <D ACK MAX 9000>
if (params == 4 && p[3] == "MS"_hk) // <D ACK MAX 80 MS>
duration = p[2] * 1000L;
else
duration = p[2];
DCCACK::setMaxAckPulseDuration(duration);
LCD(0, F("Ack Max=%lus"), duration); // <D ACK MAX 9000>
} 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>
@@ -1090,8 +1217,7 @@ bool DCCEXParser::parseC(Print *stream, int16_t params, int16_t p[]) {
}
return true;
#endif
default: // invalid/unknown
default: // invalid/unknown
break;
}
return false;
@@ -1117,6 +1243,10 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
return true;
#ifdef HAS_ENOUGH_MEMORY
case "RAILCOM"_hk: // <D RAILCOM ON/OFF>
Diag::RAILCOM = onOff;
return true;
case "WIFI"_hk: // <D WIFI ON/OFF>
Diag::WIFI = onOff;
return true;
@@ -1132,6 +1262,10 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
case "LCN"_hk: // <D LCN ON/OFF>
Diag::LCN = onOff;
return true;
case "WEBSOCKET"_hk: // <D WEBSOCKET ON/OFF>
Diag::WEBSOCKET = onOff;
return true;
#endif
#ifndef DISABLE_EEPROM
case "EEPROM"_hk: // <D EEPROM NumEntries>
@@ -1261,6 +1395,40 @@ bool DCCEXParser::parseI(Print *stream, int16_t params, int16_t p[])
}
#endif
bool DCCEXParser::parseJM(Print *stream, int16_t params, int16_t p[]) {
switch (params) {
case 1: // <JM> list all stashed automations
Stash::list(stream);
return true;
case 2: // <JM id> get stash value
Stash::list(stream, p[1]);
return true;
case 3: //
if (p[1]=="CLEAR"_hk) {
if (p[2]=="ALL"_hk) { // <JM CLEAR ALL>
Stash::clearAll();
return true;
}
Stash::clear(p[2]); // <JM CLEAR id>
return true;
}
Stash::set(p[1], p[2]); // <JM id loco>
return true;
case 4: // <JM CLEAR ANY id>
if (p[1]=="CLEAR"_hk && p[2]=="ANY"_hk) {
// <JM CLEAR ANY id>
Stash::clearAny(p[3]);
return true;
}
default: break;
}
return false;
}
// CALLBACKS must be static
bool DCCEXParser::stashCallback(Print *stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream)
{
@@ -1324,6 +1492,12 @@ void DCCEXParser::callback_R(int16_t result)
commitAsyncReplyStream();
}
void DCCEXParser::callback_r(int16_t result)
{
StringFormatter::send(getAsyncReplyStream(), F("<r %d %d %d >\n"), stashP[0], stashP[1], result);
commitAsyncReplyStream();
}
void DCCEXParser::callback_Rloco(int16_t result) {
const FSH * detail;
if (result<=0) {
@@ -1347,3 +1521,12 @@ void DCCEXParser::callback_Wloco(int16_t result)
StringFormatter::send(getAsyncReplyStream(), F("<w %d>\n"), result);
commitAsyncReplyStream();
}
void DCCEXParser::callback_Wconsist(int16_t result)
{
if (result==-4) DIAG(F("Long Consist %d not supported by decoder"),stashP[1]);
if (result==1) result=stashP[1]; // pick up original requested id from command
StringFormatter::send(getAsyncReplyStream(), F("<w CONSIST %d%S>\n"),
result, stashP[2]=="REVERSE"_hk ? F(" REVERSE") : F(""));
commitAsyncReplyStream();
}

View File

@@ -1,7 +1,7 @@
/*
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Chris Harlow
* © 2020-2025 Chris Harlow
* All rights reserved.
*
* This file is part of Asbelos DCC API
@@ -37,13 +37,14 @@ struct DCCEXParser
static void parseOne(Print * stream, byte * command, RingStream * ringStream);
static void setFilter(FILTER_CALLBACK filter);
static void setRMFTFilter(FILTER_CALLBACK filter);
static void setCamParserFilter(FILTER_CALLBACK filter);
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
static const int MAX_COMMAND_PARAMS=10; // Must not exceed this
private:
static const int16_t MAX_BUFFER=50; // longest command sent in
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command, bool usehex);
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], byte * command, bool usehex);
static bool parseT(Print * stream, int16_t params, int16_t p[]);
static bool parseZ(Print * stream, int16_t params, int16_t p[]);
@@ -51,6 +52,7 @@ struct DCCEXParser
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[]);
static bool parseJM(Print * stream, int16_t params, int16_t p[]);
#ifndef IO_NO_HAL
static bool parseI(Print * stream, int16_t params, int16_t p[]);
#endif
@@ -68,13 +70,16 @@ struct DCCEXParser
static void callback_W(int16_t result);
static void callback_W4(int16_t result);
static void callback_B(int16_t result);
static void callback_R(int16_t result);
static void callback_R(int16_t result); // prog
static void callback_r(int16_t result); // main
static void callback_Rloco(int16_t result);
static void callback_Wloco(int16_t result);
static void callback_Wconsist(int16_t result);
static void callback_Vbit(int16_t result);
static void callback_Vbyte(int16_t result);
static FILTER_CALLBACK filterCallback;
static FILTER_CALLBACK filterRMFTCallback;
static FILTER_CALLBACK filterCamParserCallback;
static AT_COMMAND_CALLBACK atCommandCallback;
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
static void sendFlashList(Print * stream,const int16_t flashList[]);

234
DCCQueue.cpp Normal file
View File

@@ -0,0 +1,234 @@
/*
* © 2025 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/>.
*/
/* What does this queue manager do:
1. It provides a high priority queue and a low priority queue.
2. It manages situations where multiple loco speed commands are in the queue.
3. It allows an ESTOP to jump the queue and eliminate any outstanding speed commands that would later undo the stop.
4. It allows for coil on/off accessory commands to be synchronized to a given time delay.
5. It prevents transmission of sequential packets to the same loco id
*/
#include "Arduino.h"
#include "defines.h"
#include "DCCQueue.h"
#include "DCCWaveform.h"
#include "DIAG.h"
// create statics
DCCQueue* DCCQueue::lowPriorityQueue=new DCCQueue();
DCCQueue* DCCQueue::highPriorityQueue=new DCCQueue();
PendingSlot* DCCQueue::recycleList=nullptr;
uint16_t DCCQueue::lastSentPacketLocoId=0; // used to prevent two packets to the same loco in a row
DCCQueue::DCCQueue() {
head=nullptr;
tail=nullptr;
}
void DCCQueue::addQueue(PendingSlot* p) {
if (tail) tail->next=p;
else head=p;
tail=p;
p->next=nullptr;
}
void DCCQueue::jumpQueue(PendingSlot* p) {
p->next=head;
head=p;
if (!tail) tail=p;
}
void DCCQueue::recycle(PendingSlot* p) {
p->next=recycleList;
recycleList=p;
}
// Packet joins end of low priority queue.
void DCCQueue::scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco) {
lowPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco));
}
// Packet replaces existing loco speed packet or joins end of high priority queue.
void DCCQueue::scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco) {
for (auto p=highPriorityQueue->head;p;p=p->next) {
if (p->locoId==loco) {
// replace existing packet
if (length>sizeof(p->packet)) {
DIAG(F("DCC bad packet length=%d"),length);
length=sizeof(p->packet); // limit to size of packet
}
memcpy(p->packet,packet,length);
p->packetLength=length;
p->packetRepeat=repeats;
return;
}
}
highPriorityQueue->addQueue(getSlot(NORMAL_PACKET,packet,length,repeats,loco));
}
// ESTOP -
// any outstanding throttle packet for this loco (all if loco=0) discarded
// Packet joins start of queue,
void DCCQueue::scheduleEstopPacket(byte* packet, byte length, byte repeats,uint16_t loco) {
// DIAG(F("DCC ESTOP loco=%d"),loco);
// kill any existing throttle packets for this loco (or all locos if broadcast)
// this will also remove any estop packets for this loco (or all locos if broadcast) but they will be replaced
PendingSlot * previous=nullptr;
auto p=highPriorityQueue->head;
while(p) {
if (p->type!=ACC_OFF_PACKET && (loco==0 || p->locoId==loco)) {
// drop this packet from the highPriority queue
if (previous) previous->next=p->next;
else highPriorityQueue->head=p->next;
recycle(p); // recycle this slot
// address next packet
p=previous?previous->next : highPriorityQueue->head;
}
else {
previous=p;
p=p->next;
}
}
// add the estop packet to the start of the queue
highPriorityQueue->jumpQueue(getSlot(NORMAL_PACKET,packet,length,repeats,0));
}
// Accessory coil-On Packet joins end of queue as normal.
// When dequeued, packet is retained at start of queue
// but modified to coil-off and given the delayed start.
// getNext will ignore this packet until the requested start time.
void DCCQueue::scheduleAccOnOffPacket(byte* packet, byte length, byte repeats,int16_t delayms) {
auto p=getSlot(ACC_ON_PACKET,packet,length,repeats,0);
p->delayOff=delayms;
lowPriorityQueue->addQueue(p);
};
// Schedule the next dcc packet from the queues or an idle packet if none pending.
const byte idlePacket[] = {0xFF, 0x00};
bool DCCQueue::scheduleNext(bool force) {
if (highPriorityQueue->scheduleNextInternal()) return true;
if (lowPriorityQueue->scheduleNextInternal()) return true;
if (force) {
// This will arise when there is nothing available to be sent that will not compromise the rules
// typically this will only happen when there is only one loco in the reminders as the closely queued
// speed and function reminders must be separated by at least one packet not sent to that loco.
DCCWaveform::mainTrack.schedulePacket(idlePacket,sizeof(idlePacket),0);
lastSentPacketLocoId=0;
return true;
}
return false;
}
bool DCCQueue::scheduleNextInternal() {
PendingSlot* previous=nullptr;
for (auto p=head;p;previous=p,p=p->next) {
// skip over pending ACC_OFF packets which are still delayed
if (p->type == ACC_OFF_PACKET && millis()<p->startTime) continue;
if (p->locoId) {
// Prevent two consecutive packets to the same loco.
// this also means repeats cant be done by waveform
if (p->locoId==lastSentPacketLocoId) continue; // try again later
DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,0);
lastSentPacketLocoId=p->locoId;
if (p->packetRepeat) {
p->packetRepeat--;
return true; // leave this packet in the queue
}
}
else {
// Non loco packets can repeat automatically
DCCWaveform::mainTrack.schedulePacket(p->packet,p->packetLength,p->packetRepeat);
lastSentPacketLocoId=0;
}
// remove this slot from the queue
if (previous) previous->next=p->next;
else head=p->next;
if (!head) tail=nullptr;
// special cases handling
if (p->type == ACC_ON_PACKET) {
// convert to a delayed off packet and jump the high priority queue
p->type= ACC_OFF_PACKET;
p->packet[1] &= ~0x08; // set C to 0 (gate off)
p->startTime=millis()+p->delayOff;
highPriorityQueue->jumpQueue(p);
return true;
}
// Recycle packet just consumed
recycle(p);
return true;
}
// No packets found
return false;
}
// obtain and initialise slot for a PendingSlot.
PendingSlot* DCCQueue::getSlot(PendingType type, byte* packet, byte length, byte repeats,uint16_t loco) {
PendingSlot * p;
if (recycleList) {
p=recycleList;
recycleList=p->next;
}
else {
static int16_t created=0;
int16_t q1=0;
int16_t q2=0;
for (auto p=highPriorityQueue->head;p;p=p->next) q1++;
for (auto p=lowPriorityQueue->head;p;p=p->next) q2++;
bool leak=(q1+q2)!=created;
DIAG(F("New DCC queue slot type=%d length=%d loco=%d q1=%d q2=%d created=%d"),
type,length,loco,q1,q2, created);
if (leak) {
for (auto p=highPriorityQueue->head;p;p=p->next) DIAG(F("q1 %d %d"),p->type,p->locoId);
for (auto p=lowPriorityQueue->head;p;p=p->next) DIAG(F("q2 %d %d"),p->type,p->locoId);
}
p=new PendingSlot; // need a queue entry
created++;
}
p->next=nullptr;
p->type=type;
p->packetLength=length;
p->packetRepeat=repeats;
if (length>sizeof(p->packet)) {
DIAG(F("DCC bad packet length=%d"),length);
length=sizeof(p->packet); // limit to size of packet
}
p->startTime=0; // not used for loco packets
memcpy((void*)p->packet,packet,length);
p->locoId=loco;
return p;
}

83
DCCQueue.h Normal file
View File

@@ -0,0 +1,83 @@
/*
* © 2025 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef DCCQueue_h
#define DCCQueue_h
#include "Arduino.h"
#include "DCCWaveform.h"
enum PendingType:byte {NORMAL_PACKET,SPEED_PACKET,FUNCTION_PACKET,ACC_ON_PACKET,ACC_OFF_PACKET,DEAD_PACKET};
struct PendingSlot {
PendingSlot* next;
PendingType type;
byte packetLength;
byte packetRepeat;
byte packet[MAX_PACKET_SIZE];
union { // use depends on packet type
uint16_t locoId; // SPEED & FUNCTION packets
uint16_t delayOff; // ACC_ON_PACKET delay to apply between on/off
uint32_t startTime; // ACC_OFF_PACKET time (mS) to transmit
};
};
class DCCQueue {
public:
// Non-speed packets are queued in the main queue
static void scheduleDCCPacket(byte* packet, byte length, byte repeats, uint16_t loco=0);
// Speed packets are queued in the high priority queue
static void scheduleDCCSpeedPacket(byte* packet, byte length, byte repeats, uint16_t loco);
// ESTOP packets jump the high priority queue and discard any outstanding throttle packets for this loco
static void scheduleEstopPacket(byte* packet, byte length, byte repeats,uint16_t loco);
// Accessory gate-On Packet joins end of main queue as normal.
// When dequeued, packet is modified to gate-off and given the delayed start in the high priority queue.
// getNext will ignore this packet until the requested start time.
static void scheduleAccOnOffPacket(byte* packet, byte length, byte repeats,int16_t delayms);
// Schedules a main track packet from the queues.
static bool scheduleNext(bool force);
private:
bool scheduleNextInternal();
// statics to manage high and low priority queues and recycleing of PENDINGs
static PendingSlot* recycleList;
static DCCQueue* highPriorityQueue;
static DCCQueue* lowPriorityQueue;
static uint16_t lastSentPacketLocoId; // used to prevent two packets to the same loco in a row
DCCQueue();
PendingSlot* head;
PendingSlot * tail;
// obtain and initialise slot for a PendingSlot.
static PendingSlot* getSlot(PendingType type, byte* packet, byte length, byte repeats, uint16_t loco);
static void recycle(PendingSlot* p);
void addQueue(PendingSlot * p);
void jumpQueue(PendingSlot * p);
};
#endif

View File

@@ -17,6 +17,25 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* RMT has "channels" which us FIFO RAM where you place what you want to send
* or receive. Channels can be merged to get more words per channel.
*
* WROOM: 8 channels total of 512 words, 64 words per channel. We use currently
* channel 0+1 for 128 words for DCC MAIN and 2+3 for DCC PROG.
*
* S3: 8 channels total of 384 words. 4 channels dedicated for TX and 4 channels
* dedicated for RX. 48 words per channel. So for TX there are 4 channels and we
* could use them with 96 words for MAIN and PROG if DCC data does fit in there.
*
* C3: 4 channels total of 192 words. As we do not use RX we can use all for TX
* so the situation is the same as for the -S3
*
* C6, H2: 4 channels total of 192 words. 2 channels dedictaed for TX and
* 2 channels dedicated for RX. Half RMT capacity compared to the C3.
*
*/
#if defined(ARDUINO_ARCH_ESP32)
#include "defines.h"
#include "DIAG.h"

View File

@@ -44,6 +44,12 @@ class RMTChannel {
return true;
return dataReady;
};
inline void waitForDataCopy() {
while(1) { // do nothing and wait for interrupt clearing dataReady to happen
if (dataReady == false)
break;
}
};
inline uint32_t packetCount() { return packetCounter; };
private:

View File

@@ -1,5 +1,5 @@
/*
* © 2022-2023 Paul M. Antoine
* © 2022-2024 Paul M. Antoine
* © 2021 Mike S
* © 2021-2023 Harald Barth
* © 2021 Fred Decker
@@ -135,6 +135,8 @@ private:
#if defined (ARDUINO_ARCH_STM32)
// bit array of used pins (max 32)
static uint32_t usedpins;
static uint32_t * analogchans; // Array of channel numbers to be scanned
static ADC_TypeDef * * adcchans; // Array to capture which ADC is each input channel on
#else
// bit array of used pins (max 16)
static uint16_t usedpins;

View File

@@ -2,7 +2,7 @@
* © 2021 Mike S
* © 2021-2023 Harald Barth
* © 2021 Fred Decker
* © 2021 Chris Harlow
* © 2021-2025 Chris Harlow
* © 2021 David Cutting
* All rights reserved.
*
@@ -57,66 +57,59 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
TCCR1B = _BV(WGM13) | _BV(CS10); // Mode 8, clock select 1
TIMSK1 = _BV(TOIE1); // Enable Software interrupt
interrupts();
//diagnostic pinMode(4,OUTPUT);
}
void DCCTimer::startRailcomTimer(byte brakePin) {
(void) brakePin; // Ignored... works on pin 9 only
// diagnostic digitalWrite(4,HIGH);
/* The Railcom timer is started in such a way that it
- First triggers 28uS after the last TIMER1 tick.
- First triggers 58+29 uS after the previous 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.
- Sets the Railcom pin high at first tick and subsequent ticks
until its reset to setting pin 9 low at next tick.
- 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.
- Waveform code is responsible for resetting
any time between the first and second 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);
const int cycle=cutoutDuration/2;
// 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)
const byte RailcomFudge0=58+58+29;
// Set Timer2 to CTC mode with set on compare match
TCCR2A = (1 << WGM21) | (1 << COM2B0) | (1 << COM2B1);
// Prescaler of 32
TCCR2B = (1 << CS21) | (1 << CS20);
OCR2A = cycle-1; // Compare match value for 430 uS
// Enable Timer2 output on pin 9 (OC2B)
DDRB |= (1 << DDB1);
// TODO Fudge TCNT2 to sync with last tcnt1 tick + 28uS
// RailcomFudge2 is the expected time from idealised
// setup call (at previous DCC timer interrupt) to the cutout.
// This value should be reduced to reflect the Timer1 value
// measuring the time since the previous hardware interrupt
byte tcfudge=TCNT1/16;
TCNT2=cycle-RailcomFudge0/2+tcfudge/2;
// 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
// Change Timer2 to CTC mode with RESET pin 9 on next compare match
TCCR2A = (1 << WGM21) | (1 << COM2B1);
// diagnostic digitalWrite(4,LOW);
}
@@ -185,8 +178,10 @@ int DCCTimer::freeMemory() {
}
void DCCTimer::reset() {
wdt_enable( WDTO_15MS); // set Arduino watchdog timer for 15ms
delay(50); // wait for the prescaller time to expire
// 250ms chosen to circumwent bootloader bug which
// hangs at too short timepout (like 15ms)
wdt_enable( WDTO_250MS); // set Arduino watchdog timer for 250ms
delay(500); // wait for it to happen
}
@@ -195,6 +190,8 @@ void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
}
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
#if defined(ARDUINO_AVR_UNO)
(void)fbits;
(void) pin;
// 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.

View File

@@ -76,8 +76,19 @@ int DCCTimer::freeMemory() {
#endif
////////////////////////////////////////////////////////////////////////
#ifdef ARDUINO_ARCH_ESP32
#if __has_include("esp_idf_version.h")
#include "esp_idf_version.h"
#endif
#if ESP_IDF_VERSION_MAJOR == 4
// all well correct IDF version
#else
#error "DCC-EX does not support compiling with IDF version 5.0 or later. Downgrade your ESP32 library to a version that contains IDF version 4. Arduino ESP32 library 3.0.0 is too new. Downgrade to one of 2.0.9 to 2.0.17"
#endif
// protect all the rest of the code from IDF version 5
#if ESP_IDF_VERSION_MAJOR == 4
#include "DIAG.h"
#include <driver/adc.h>
#include <soc/sens_reg.h>
@@ -292,7 +303,12 @@ void DCCTimer::DCCEXInrushControlOn(uint8_t pin, int duty, bool inverted) {
int ADCee::init(uint8_t pin) {
pinMode(pin, ANALOG);
adc1_config_width(ADC_WIDTH_BIT_12);
// Espressif deprecated ADC_ATTEN_DB_11 somewhere between 2.0.9 and 2.0.17
#ifdef ADC_ATTEN_11db
adc1_config_channel_atten(pinToADC1Channel(pin),ADC_ATTEN_11db);
#else
adc1_config_channel_atten(pinToADC1Channel(pin),ADC_ATTEN_DB_11);
#endif
return adc1_get_raw(pinToADC1Channel(pin));
}
int16_t ADCee::ADCmax() {
@@ -312,6 +328,5 @@ void ADCee::scan() {
void ADCee::begin() {
}
#endif //IDF v4
#endif //ESP32

View File

@@ -1,6 +1,6 @@
/*
* © 2023 Neil McKechnie
* © 2022-2023 Paul M. Antoine
* © 2022-2024 Paul M. Antoine
* © 2021 Mike S
* © 2021, 2023 Harald Barth
* © 2021 Fred Decker
@@ -34,8 +34,22 @@
#include "TrackManager.h"
#endif
#include "DIAG.h"
#include <wiring_private.h>
#if defined(ARDUINO_NUCLEO_F401RE) || defined(ARDUINO_NUCLEO_F411RE)
#if defined(ARDUINO_NUCLEO_F401RE)
// Nucleo-64 boards don't have additional serial ports defined by default
// Serial1 is available on the F401RE, but not hugely convenient.
// Rx pin on PB7 is useful, but all the Tx pins map to Arduino digital pins, specifically:
// PA9 == D8
// PB6 == D10
// of which D8 is needed by the standard and EX8874 motor shields. D10 would be used if a second
// EX8874 is stacked. So only disable this if using a second motor shield.
HardwareSerial Serial1(PB7, PB6); // Rx=PB7, Tx=PB6 -- CN7 pin 17 and CN10 pin 17
// Serial2 is defined to use USART2 by default, but is in fact used as the diag console
// via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
// Let's define Serial6 as an additional serial port (the only other option for the F401RE)
HardwareSerial Serial6(PA12, PA11); // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14 - F401RE
#elif defined(ARDUINO_NUCLEO_F411RE)
// Nucleo-64 boards don't have additional serial ports defined by default
HardwareSerial Serial1(PB7, PA15); // Rx=PB7, Tx=PA15 -- CN7 pins 17 and 21 - F411RE
// Serial2 is defined to use USART2 by default, but is in fact used as the diag console
@@ -53,12 +67,12 @@ HardwareSerial Serial3(PC11, PC10); // Rx=PC11, Tx=PC10 -- USART3 - 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_F446ZE) || \
defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI)
defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)
// 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
HardwareSerial Serial2(PD6, PD5); // Rx=PD6, Tx=PD5 -- UART2
#if !defined(ARDUINO_NUCLEO_F412ZG) // F412ZG does not have UART5
HardwareSerial Serial5(PD2, PC12); // Rx=PD2, Tx=PC12 -- 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.
@@ -314,7 +328,7 @@ void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency
if (pin_timer[pin] != NULL)
{
pin_timer[pin]->setPWM(pin_channel[pin], pin, frequency, 0); // set frequency in Hertz, 0% dutycycle
DIAG(F("DCCEXanalogWriteFrequency::Pin %d on Timer %d, frequency %d"), pin, pin_channel[pin], frequency);
DIAG(F("DCCEXanalogWriteFrequency::Pin %d on Timer Channel %d, frequency %d"), pin, pin_channel[pin], frequency);
}
else
DIAG(F("DCCEXanalogWriteFrequency::failed to allocate HardwareTimer instance!"));
@@ -363,9 +377,9 @@ void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value, bool invert) {
uint32_t ADCee::usedpins = 0; // Max of 32 ADC input channels!
uint8_t ADCee::highestPin = 0; // Highest pin to scan
int * ADCee::analogvals = NULL; // Array of analog values last captured
uint32_t * analogchans = NULL; // Array of channel numbers to be scanned
uint32_t * ADCee::analogchans = NULL; // Array of channel numbers to be scanned
// bool adc1configured = false;
ADC_TypeDef * * adcchans = NULL; // Array to capture which ADC is each input channel on
ADC_TypeDef * * ADCee::adcchans = NULL; // Array to capture which ADC is each input channel on
int16_t ADCee::ADCmax()
{
@@ -383,9 +397,10 @@ int ADCee::init(uint8_t pin) {
uint32_t adcchan = STM_PIN_CHANNEL(pinmap_function(stmpin, PinMap_ADC)); // find ADC input channel
ADC_TypeDef *adc = (ADC_TypeDef *)pinmap_find_peripheral(stmpin, PinMap_ADC); // find which ADC this pin is on ADC1/2/3 etc.
int adcnum = 1;
// All variants have ADC1
if (adc == ADC1)
DIAG(F("ADCee::init(): found pin %d on ADC1"), pin);
// Checking for ADC2 and ADC3 being defined helps cater for more variants later
// Checking for ADC2 and ADC3 being defined helps cater for more variants
#if defined(ADC2)
else if (adc == ADC2)
{
@@ -432,6 +447,18 @@ int ADCee::init(uint8_t pin) {
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOFEN; //Power up PORTF
gpioBase = GPIOF;
break;
#endif
#if defined(GPIOG)
case 0x06:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOGEN; //Power up PORTG
gpioBase = GPIOG;
break;
#endif
#if defined(GPIOH)
case 0x07:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOHEN; //Power up PORTH
gpioBase = GPIOH;
break;
#endif
default:
return -1023; // some silly value as error

View File

@@ -24,14 +24,13 @@
#ifndef ARDUINO_ARCH_ESP32
// This code is replaced entirely on an ESP32
#include <Arduino.h>
#include "DCCWaveform.h"
#include "TrackManager.h"
#include "DCCTimer.h"
#include "DCCACK.h"
#include "DIAG.h"
bool DCCWaveform::cutoutNextTime=false;
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
@@ -71,9 +70,18 @@ void DCCWaveform::loop() {
#pragma GCC push_options
#pragma GCC optimize ("-O3")
void DCCWaveform::interruptHandler() {
// call the timer edge sensitive actions for progtrack and maintrack
// member functions would be cleaner but have more overhead
#if defined(HAS_ENOUGH_MEMORY)
if (cutoutNextTime) {
cutoutNextTime=false;
railcomSampleWindow=false; // about to cutout, stop reading railcom data.
railcomCutoutCounter++;
DCCTimer::startRailcomTimer(9);
}
#endif
byte sigMain=signalTransform[mainTrack.state];
byte sigProg=TrackManager::progTrackSyncMain? sigMain : signalTransform[progTrack.state];
@@ -115,19 +123,24 @@ DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
bytes_sent = 0;
bits_sent = 0;
}
bool DCCWaveform::railcomPossible=false; // High accuracy only
volatile bool DCCWaveform::railcomActive=false; // switched on by user
volatile bool DCCWaveform::railcomDebug=false; // switched on by user
volatile bool DCCWaveform::railcomSampleWindow=false; // true during packet transmit
volatile byte DCCWaveform::railcomCutoutCounter=0; // cyclic cutout
volatile byte DCCWaveform::railcomLastAddressHigh=0;
volatile byte DCCWaveform::railcomLastAddressLow=0;
bool DCCWaveform::setRailcom(bool on, bool debug) {
if (on) {
// TODO check possible
if (on && railcomPossible) {
railcomActive=true;
railcomDebug=debug;
}
else {
railcomActive=false;
railcomDebug=false;
railcomSampleWindow=false;
}
return railcomActive;
}
@@ -140,14 +153,37 @@ void DCCWaveform::interrupt2() {
// 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();
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<12 && remainingPreambles>1;
if (remainingPreambles==1)
promotePendingPacket();
#if defined(HAS_ENOUGH_MEMORY)
else if (isMainTrack && railcomActive) {
if (remainingPreambles==(requiredPreambles-1)) {
// First look if we need to start a railcom cutout on next interrupt
cutoutNextTime= true;
} else if (remainingPreambles==(requiredPreambles-12)) {
// cutout has ended so its now possible to poll the railcom detectors
// requiredPreambles is one higher that preamble length so
// if preamble length is 16 then this evaluates to 5
// Remember address bytes of last sent packet so that Railcom can
// work out where the channel2 data came from.
railcomLastAddressHigh=transmitPacket[0];
railcomLastAddressLow =transmitPacket[1];
railcomSampleWindow=true;
} else if (remainingPreambles==(requiredPreambles-3)) {
// cutout can be ended when read
// see above for requiredPreambles
DCCTimer::ackRailcomTimer();
}
}
#endif
// 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.
else DCCTimer::updateMinimumFreeMemoryISR(22);
@@ -171,13 +207,7 @@ void DCCWaveform::interrupt2() {
bytes_sent = 0;
// preamble for next packet will start...
remainingPreambles = requiredPreambles;
// 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);
}
}
}
}
#pragma GCC pop_options
@@ -212,7 +242,7 @@ void DCCWaveform::promotePendingPacket() {
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
@@ -230,7 +260,7 @@ void DCCWaveform::promotePendingPacket() {
// 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
// and only the presets will be ones, making it much
// easier to read on a logic analyser.
memcpy( transmitPacket, (isMainTrack && (!railcomDebug)) ? idlePacket : resetPacket, sizeof(idlePacket));
transmitLength = sizeof(idlePacket);
@@ -238,93 +268,3 @@ void DCCWaveform::promotePendingPacket() {
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
}
#endif
#ifdef ARDUINO_ARCH_ESP32
#include "DCCWaveform.h"
#include "DCCACK.h"
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
RMTChannel *DCCWaveform::rmtMainChannel = NULL;
RMTChannel *DCCWaveform::rmtProgChannel = NULL;
DCCWaveform::DCCWaveform(byte preambleBits, bool isMain) {
isMainTrack = isMain;
requiredPreambles = preambleBits;
}
void DCCWaveform::begin() {
for(const auto& md: TrackManager::getMainDrivers()) {
pinpair p = md->getSignalPin();
if(rmtMainChannel) {
//DIAG(F("added pins %d %d to MAIN channel"), p.pin, p.invpin);
rmtMainChannel->addPin(p); // add pin to existing main channel
} else {
//DIAG(F("new MAIN channel with pins %d %d"), p.pin, p.invpin);
rmtMainChannel = new RMTChannel(p, true); /* create new main channel */
}
}
MotorDriver *md = TrackManager::getProgDriver();
if (md) {
pinpair p = md->getSignalPin();
if (rmtProgChannel) {
//DIAG(F("added pins %d %d to PROG channel"), p.pin, p.invpin);
rmtProgChannel->addPin(p); // add pin to existing prog channel
} else {
//DIAG(F("new PROGchannel with pins %d %d"), p.pin, p.invpin);
rmtProgChannel = new RMTChannel(p, false);
}
}
}
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum
byte checksum = 0;
for (byte b = 0; b < byteCount; b++) {
checksum ^= buffer[b];
pendingPacket[b] = buffer[b];
}
// buffer is MAX_PACKET_SIZE but pendingPacket is one bigger
pendingPacket[byteCount] = checksum;
pendingLength = byteCount + 1;
pendingRepeats = repeats;
// DIAG repeated commands (accesories)
// if (pendingRepeats > 0)
// DIAG(F("Repeats=%d on %s track"), pendingRepeats, isMainTrack ? "MAIN" : "PROG");
// The resets will be zero not only now but as well repeats packets into the future
clearResets(repeats+1);
{
int ret = 0;
do {
if(isMainTrack) {
if (rmtMainChannel != NULL)
ret = rmtMainChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
} else {
if (rmtProgChannel != NULL)
ret = rmtProgChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
}
} while(ret > 0);
}
}
bool DCCWaveform::isReminderWindowOpen() {
if(isMainTrack) {
if (rmtMainChannel == NULL)
return false;
return !rmtMainChannel->busy();
} else {
if (rmtProgChannel == NULL)
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

@@ -3,7 +3,7 @@
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2024 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020-2025 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -23,11 +23,8 @@
*/
#ifndef DCCWaveform_h
#define DCCWaveform_h
#include "MotorDriver.h"
#ifdef ARDUINO_ARCH_ESP32
#include "DCCRMT.h"
#include "TrackManager.h"
#endif
@@ -86,8 +83,30 @@ class DCCWaveform {
bool isReminderWindowOpen();
void promotePendingPacket();
static bool setRailcom(bool on, bool debug);
static bool isRailcom() {return railcomActive;}
inline static bool isRailcom() {
return railcomActive;
};
inline static byte getRailcomCutoutCounter() {
return railcomCutoutCounter;
};
inline static bool isRailcomSampleWindow() {
return railcomSampleWindow;
};
inline static bool isRailcomPossible() {
return railcomPossible;
};
inline static void setRailcomPossible(bool yes) {
railcomPossible=yes;
if (!yes) setRailcom(false,false);
};
inline static uint16_t getRailcomLastLocoAddress() {
// first 2 bits 00=short loco, 11=long loco , 01/10 = accessory
byte addressType=railcomLastAddressHigh & 0xC0;
if (addressType==0xC0) return ((railcomLastAddressHigh & 0x3f)<<8) | railcomLastAddressLow;
if (addressType==0x00) return railcomLastAddressHigh & 0x3F;
return 0;
}
private:
#ifndef ARDUINO_ARCH_ESP32
volatile bool packetPending;
@@ -112,9 +131,13 @@ class DCCWaveform {
byte pendingPacket[MAX_PACKET_SIZE+1]; // +1 for checksum
byte pendingLength;
byte pendingRepeats;
static bool railcomPossible; // High accuracy mode only
static volatile bool railcomActive; // switched on by user
static volatile bool railcomDebug; // switched on by user
static volatile bool railcomSampleWindow; // when safe to sample
static volatile byte railcomCutoutCounter; // incremented for each cutout
static volatile byte railcomLastAddressHigh,railcomLastAddressLow;
static bool cutoutNextTime; // railcom
#ifdef ARDUINO_ARCH_ESP32
static RMTChannel *rmtMainChannel;
static RMTChannel *rmtProgChannel;

120
DCCWaveformRMT.cpp Normal file
View File

@@ -0,0 +1,120 @@
/*
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2022 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// This code is ESP32 ONLY.
#ifdef ARDUINO_ARCH_ESP32
#include "DCCWaveform.h"
#include "DCCACK.h"
#include "TrackManager.h"
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
RMTChannel *DCCWaveform::rmtMainChannel = NULL;
RMTChannel *DCCWaveform::rmtProgChannel = NULL;
bool DCCWaveform::railcomPossible=false; // High accuracy only
volatile bool DCCWaveform::railcomActive=false; // switched on by user
volatile bool DCCWaveform::railcomDebug=false; // switched on by user
volatile bool DCCWaveform::railcomSampleWindow=false; // true during packet transmit
volatile byte DCCWaveform::railcomCutoutCounter=0; // cyclic cutout
volatile byte DCCWaveform::railcomLastAddressHigh=0;
volatile byte DCCWaveform::railcomLastAddressLow=0;
DCCWaveform::DCCWaveform(byte preambleBits, bool isMain) {
isMainTrack = isMain;
requiredPreambles = preambleBits;
}
void DCCWaveform::begin() {
for(const auto& md: TrackManager::getMainDrivers()) {
pinpair p = md->getSignalPin();
if(rmtMainChannel) {
//DIAG(F("added pins %d %d to MAIN channel"), p.pin, p.invpin);
rmtMainChannel->addPin(p); // add pin to existing main channel
} else {
//DIAG(F("new MAIN channel with pins %d %d"), p.pin, p.invpin);
rmtMainChannel = new RMTChannel(p, true); /* create new main channel */
}
}
MotorDriver *md = TrackManager::getProgDriver();
if (md) {
pinpair p = md->getSignalPin();
if (rmtProgChannel) {
//DIAG(F("added pins %d %d to PROG channel"), p.pin, p.invpin);
rmtProgChannel->addPin(p); // add pin to existing prog channel
} else {
//DIAG(F("new PROGchannel with pins %d %d"), p.pin, p.invpin);
rmtProgChannel = new RMTChannel(p, false);
}
}
}
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum
RMTChannel *rmtchannel = (isMainTrack ? rmtMainChannel : rmtProgChannel);
if (rmtchannel == NULL)
return; // no idea to prepare packet if we can not send it anyway
rmtchannel->waitForDataCopy(); // blocking wait so we can write into buffer
byte checksum = 0;
for (byte b = 0; b < byteCount; b++) {
checksum ^= buffer[b];
pendingPacket[b] = buffer[b];
}
// buffer is MAX_PACKET_SIZE but pendingPacket is one bigger
pendingPacket[byteCount] = checksum;
pendingLength = byteCount + 1;
pendingRepeats = repeats;
// DIAG repeated commands (accesories)
// if (pendingRepeats > 0)
// DIAG(F("Repeats=%d on %s track"), pendingRepeats, isMainTrack ? "MAIN" : "PROG");
// The resets will be zero not only now but as well repeats packets into the future
clearResets(repeats+1);
{
int ret = 0;
do {
ret = rmtchannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
} while(ret > 0);
}
}
bool DCCWaveform::isReminderWindowOpen() {
if(isMainTrack) {
if (rmtMainChannel == NULL)
return false;
return !rmtMainChannel->busy();
} else {
if (rmtProgChannel == NULL)
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

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

View File

@@ -1,8 +1,10 @@
/*
* © 2024 Paul M. Antoine
* © 2021 Neil McKechnie
* © 2021-2023 Harald Barth
* © 2020-2023 Chris Harlow
* © 2020-2025 Chris Harlow
* © 2022-2023 Colin Murdoch
* © 2025 Morten Nielsen
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -54,6 +56,8 @@
#include "TrackManager.h"
#include "Turntables.h"
#include "IODevice.h"
#include "EXRAILSensor.h"
#include "Stash.h"
// One instance of RMFT clas is used for each "thread" in the automation.
@@ -71,6 +75,7 @@ RMFT2 * RMFT2::pausingTask=NULL; // Task causing a PAUSE.
byte RMFT2::flags[MAX_FLAGS];
Print * RMFT2::LCCSerial=0;
LookList * RMFT2::routeLookup=NULL;
LookList * RMFT2::signalLookup=NULL;
LookList * RMFT2::onThrowLookup=NULL;
LookList * RMFT2::onCloseLookup=NULL;
LookList * RMFT2::onActivateLookup=NULL;
@@ -84,10 +89,11 @@ LookList * RMFT2::onClockLookup=NULL;
LookList * RMFT2::onRotateLookup=NULL;
#endif
LookList * RMFT2::onOverloadLookup=NULL;
LookList * RMFT2::onBlockEnterLookup=NULL;
LookList * RMFT2::onBlockExitLookup=NULL;
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) {
@@ -128,11 +134,11 @@ int16_t LookList::find(int16_t value) {
void LookList::chain(LookList * chain) {
m_chain=chain;
}
void LookList::handleEvent(const FSH* reason,int16_t id) {
void LookList::handleEvent(const FSH* reason,int16_t id, int16_t loco) {
// 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]);
RMFT2::startNonRecursiveTask(reason,id,m_resultArray[i],loco);
}
@@ -176,7 +182,7 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
/* static */ void RMFT2::begin() {
DIAG(F("EXRAIL RoutCode at =%P"),RouteCode);
//DIAG(F("EXRAIL RoutCode at =%P"),RouteCode);
bool saved_diag=diag;
diag=true;
@@ -200,20 +206,39 @@ LookList* RMFT2::LookListLoader(OPCODE op1, OPCODE op2, OPCODE op3) {
onRotateLookup=LookListLoader(OPCODE_ONROTATE);
#endif
onOverloadLookup=LookListLoader(OPCODE_ONOVERLOAD);
if (compileFeatures & FEATURE_BLOCK) {
onBlockEnterLookup=LookListLoader(OPCODE_ONBLOCKENTER);
onBlockExitLookup=LookListLoader(OPCODE_ONBLOCKEXIT);
}
// 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);
}
}
if (compileFeatures & FEATURE_SIGNAL) {
onRedLookup=LookListLoader(OPCODE_ONRED);
onAmberLookup=LookListLoader(OPCODE_ONAMBER);
onGreenLookup=LookListLoader(OPCODE_ONGREEN);
// Load the signal lookup with slot numbers in the signal table
int signalCount=0;
for (int16_t slot=0;;slot++) {
SIGNAL_DEFINITION signal=getSignalSlot(slot);
DIAG(F("Signal s=%d id=%d t=%d"),slot,signal.id,signal.type);
if (signal.type==sigtypeNoMoreSignals) break;
if (signal.type==sigtypeContinuation) continue;
signalCount++;
}
signalLookup=new LookList(signalCount);
for (int16_t slot=0;;slot++) {
SIGNAL_DEFINITION signal=getSignalSlot(slot);
if (signal.type==sigtypeNoMoreSignals) break;
if (signal.type==sigtypeContinuation) continue;
signalLookup->add(signal.id,slot);
doSignal(signal.id, SIGNAL_RED);
}
}
int progCounter;
for (progCounter=0;; SKIPOP){
@@ -225,7 +250,6 @@ if (compileFeatures & FEATURE_SIGNAL) {
case OPCODE_AT:
case OPCODE_ATTIMEOUT2:
case OPCODE_AFTER:
case OPCODE_AFTEROVERLOAD:
case OPCODE_IF:
case OPCODE_IFNOT: {
int16_t pin = (int16_t)operand;
@@ -234,23 +258,31 @@ if (compileFeatures & FEATURE_SIGNAL) {
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:
case OPCODE_IFGTE:
case OPCODE_IFLT:
case OPCODE_IFBITMAP_ALL:
case OPCODE_IFBITMAP_ANY:
case OPCODE_DRIVE: {
DIAG(F("EXRAIL analog input VPIN %u"),(VPIN)operand);
IODevice::configureAnalogIn((VPIN)operand);
break;
}
case OPCODE_ONSENSOR:
if (compileFeatures & FEATURE_SENSOR)
new EXRAILSensor(operand,progCounter+3,true );
break;
case OPCODE_ONBITMAP:
if (compileFeatures & FEATURE_SENSOR)
new EXRAILSensor(operand,progCounter+3,true, true );
break;
case OPCODE_ONBUTTON:
if (compileFeatures & FEATURE_SENSOR)
new EXRAILSensor(operand,progCounter+3,false );
break;
case OPCODE_TURNOUT: {
VPIN id=operand;
int addr=getOperand(progCounter,1);
@@ -289,7 +321,7 @@ if (compileFeatures & FEATURE_SIGNAL) {
case OPCODE_EXTTTURNTABLE: {
VPIN id=operand;
VPIN pin=getOperand(progCounter,1);
int home=getOperand(progCounter,3);
int home=getOperand(progCounter,2);
setTurntableHiddenState(EXTTTurntable::create(id,pin));
Turntable *tto=Turntable::get(id);
tto->addPosition(0,0,home);
@@ -320,13 +352,7 @@ if (compileFeatures & FEATURE_SIGNAL) {
}
SKIPOP; // include ENDROUTES opcode
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);
DIAG(F("EXRAIL %db, fl=%d"),progCounter,MAX_FLAGS);
// Removed for 4.2.31 new RMFT2(0); // add the startup route
diag=saved_diag;
@@ -356,7 +382,7 @@ char RMFT2::getRouteType(int16_t id) {
}
RMFT2::RMFT2(int progCtr) {
RMFT2::RMFT2(int progCtr, int16_t _loco) {
progCounter=progCtr;
// get an unused task id from the flags table
@@ -369,11 +395,9 @@ RMFT2::RMFT2(int progCtr) {
}
}
delayTime=0;
loco=0;
speedo=0;
forward=true;
loco=_loco;
invert=false;
timeoutFlag=false;
blinkState=not_blink_task;
stackDepth=0;
onEventStartPosition=-1; // Not handling an ONxxx
@@ -389,7 +413,10 @@ RMFT2::RMFT2(int progCtr) {
RMFT2::~RMFT2() {
driveLoco(1); // ESTOP my loco if any
// estop my loco if this is not an ONevent
// (prevents DONE stopping loco at the end of an
// ONBLOCKENTER or ONBLOCKEXIT )
if (loco>0 && this->onEventStartPosition==-1) DCC::setThrottle(loco,1,DCC::getThrottleDirection(loco));
setFlag(taskId,0,TASK_FLAG); // we are no longer using this id
if (next==this)
loopTask=NULL;
@@ -405,23 +432,9 @@ RMFT2::~RMFT2() {
void RMFT2::createNewTask(int route, uint16_t cab) {
int pc=routeLookup->find(route);
if (pc<0) return;
RMFT2* task=new RMFT2(pc);
task->loco=cab;
new RMFT2(pc,cab);
}
void RMFT2::driveLoco(byte speed) {
if (loco<=0) return; // Prevent broadcast!
if (diag) DIAG(F("EXRAIL drive %d %d %d"),loco,speed,forward^invert);
/* TODO.....
power on appropriate track if DC or main if dcc
if (TrackManager::getMainPowerMode()==POWERMODE::OFF) {
TrackManager::setMainPower(POWERMODE::ON);
}
**********/
DCC::setThrottle(loco,speed, forward^invert);
speedo=speed;
}
bool RMFT2::readSensor(uint16_t sensorId) {
// Exrail operands are unsigned but we need the signed version as inserted by the macros.
@@ -468,6 +481,11 @@ bool RMFT2::skipIfBlock() {
/* static */ void RMFT2::readLocoCallback(int16_t cv) {
if (cv <= 0) {
DIAG(F("CV read error"));
progtrackLocoId = -1;
return;
}
if (cv & LONG_ADDR_MARKER) { // maker bit indicates long addr
progtrackLocoId = cv ^ LONG_ADDR_MARKER; // remove marker bit to get real long addr
if (progtrackLocoId <= HIGHEST_SHORT_ADDR ) { // out of range for long addr
@@ -479,7 +497,18 @@ bool RMFT2::skipIfBlock() {
}
}
void RMFT2::pause() {
if (loco)
pauseSpeed=DCC::getThrottleSpeedByte(loco);
}
void RMFT2::resume() {
if (loco)
DCC::setThrottle(loco,pauseSpeed & 0x7f, pauseSpeed & 0x80);
}
void RMFT2::loop() {
if (compileFeatures & FEATURE_SENSOR)
EXRAILSensor::checkAll();
// Round Robin call to a RMFT task each time
if (loopTask==NULL) return;
@@ -491,6 +520,23 @@ void RMFT2::loop() {
void RMFT2::loop2() {
if (delayTime!=0 && millis()-delayStart < delayTime) return;
// special stand alone blink task
if (compileFeatures & FEATURE_BLINK) {
if (blinkState==blink_low) {
IODevice::write(blinkPin,HIGH);
blinkState=blink_high;
delayMe(getOperand(1));
return;
}
if (blinkState==blink_high) {
IODevice::write(blinkPin,LOW);
blinkState=blink_low;
delayMe(getOperand(2));
return;
}
}
// Normal progstep following tasks continue here.
byte opcode = GET_OPCODE;
int16_t operand = getOperand(0);
@@ -511,6 +557,10 @@ void RMFT2::loop2() {
Turnout::setClosed(operand, true);
break;
case OPCODE_TOGGLE_TURNOUT:
Turnout::setClosed(operand, Turnout::isThrown(operand));
break;
#ifndef IO_NO_HAL
case OPCODE_ROTATE:
uint8_t activity;
@@ -520,18 +570,23 @@ void RMFT2::loop2() {
#endif
case OPCODE_REV:
forward = false;
driveLoco(operand);
if (loco) DCC::setThrottle(loco,operand,invert);
break;
case OPCODE_FWD:
forward = true;
driveLoco(operand);
break;
if (loco) DCC::setThrottle(loco,operand,!invert);
break;
case OPCODE_SPEED:
forward=DCC::getThrottleDirection(loco)^invert;
driveLoco(operand);
if (loco) DCC::setThrottle(loco,operand,DCC::getThrottleDirection(loco));
break;
case OPCODE_MOMENTUM:
DCC::setMomentum(loco,operand,getOperand(1));
break;
case OPCODE_ESTOPALL:
DCC::setThrottle(0,1,1); // all locos speed=1
break;
case OPCODE_FORGET:
@@ -543,12 +598,11 @@ void RMFT2::loop2() {
case OPCODE_INVERT_DIRECTION:
invert= !invert;
driveLoco(speedo);
break;
case OPCODE_RESERVE:
if (getFlag(operand,SECTION_FLAG)) {
driveLoco(0);
if (loco) DCC::setThrottle(loco,1,DCC::getThrottleDirection(loco));
delayMe(500);
return;
}
@@ -560,49 +614,51 @@ void RMFT2::loop2() {
break;
case OPCODE_AT:
timeoutFlag=false;
blinkState=not_blink_task;
if (readSensor(operand)) break;
delayMe(50);
return;
case OPCODE_ATGTE: // wait for analog sensor>= value
timeoutFlag=false;
blinkState=not_blink_task;
if (IODevice::readAnalogue(operand) >= (int)(getOperand(1))) break;
delayMe(50);
return;
case OPCODE_ATLT: // wait for analog sensor < value
timeoutFlag=false;
blinkState=not_blink_task;
if (IODevice::readAnalogue(operand) < (int)(getOperand(1))) break;
delayMe(50);
return;
case OPCODE_ATTIMEOUT1: // ATTIMEOUT(vpin,timeout) part 1
timeoutStart=millis();
timeoutFlag=false;
blinkState=not_blink_task;
break;
case OPCODE_ATTIMEOUT2:
if (readSensor(operand)) break; // success without timeout
if (millis()-timeoutStart > 100*getOperand(1)) {
timeoutFlag=true;
blinkState=at_timeout;
break; // and drop through
}
delayMe(50);
return;
case OPCODE_IFTIMEOUT: // do next operand if timeout flag set
skipIf=!timeoutFlag;
skipIf=blinkState!=at_timeout;
break;
case OPCODE_AFTER: // waits for sensor to hit and then remain off for 0.5 seconds. (must come after an AT operation)
case OPCODE_AFTER: // waits for sensor to hit and then remain off for x mS.
// Note, this must come after an AT operation, which is
// automatically inserted by the AFTER macro.
if (readSensor(operand)) {
// reset timer to half a second and keep waiting
// reset timer and keep waiting
waitAfter=millis();
delayMe(50);
return;
}
if (millis()-waitAfter < 500 ) return;
if (millis()-waitAfter < getOperand(1) ) return;
break;
case OPCODE_AFTEROVERLOAD: // waits for the power to be turned back on - either by power routine or button
@@ -624,15 +680,28 @@ void RMFT2::loop2() {
break;
case OPCODE_SET:
IODevice::write(operand,true);
break;
case OPCODE_RESET:
IODevice::write(operand,false);
{
auto count=getOperand(1);
for (uint16_t i=0;i<count;i++) {
killBlinkOnVpin(operand+i);
IODevice::write(operand+i,opcode==OPCODE_SET);
}
}
break;
case OPCODE_BLINK:
// Start a new task to blink this vpin
killBlinkOnVpin(operand);
{
auto newtask=new RMFT2(progCounter);
newtask->blinkPin=operand;
newtask->blinkState=blink_low; // will go high on first call
}
break;
case OPCODE_PAUSE:
DCC::setThrottle(0,1,true); // pause all locos on the track
DCC::estopAll(); // pause all locos on the track
pausingTask=this;
break;
@@ -640,6 +709,10 @@ void RMFT2::loop2() {
if (loco) DCC::writeCVByteMain(loco, operand, getOperand(1));
break;
case OPCODE_XPOM:
DCC::writeCVByteMain(operand, getOperand(1), getOperand(2));
break;
case OPCODE_POWEROFF:
TrackManager::setPower(POWERMODE::OFF);
TrackManager::setJoin(false);
@@ -671,47 +744,13 @@ void RMFT2::loop2() {
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;
}
DCC::setDCFreq(loco,operand);
break;
case OPCODE_RESUME:
pausingTask=NULL;
driveLoco(speedo);
for (RMFT2 * t=next; t!=this;t=t->next) if (t->loco >0) t->driveLoco(t->speedo);
resume();
for (RMFT2 * t=next; t!=this;t=t->next) t->resume();
break;
case OPCODE_IF: // do next operand if sensor set
@@ -729,6 +768,14 @@ void RMFT2::loop2() {
case OPCODE_IFLT: // do next operand if sensor< value
skipIf=IODevice::readAnalogue(operand)>=(int)(getOperand(1));
break;
case OPCODE_IFBITMAP_ALL: // do next operand if sensor & mask == mask
skipIf=(IODevice::readAnalogue(operand) & getOperand(1)) != getOperand(1);
break;
case OPCODE_IFBITMAP_ANY: // do next operand if sensor & mask !=0
skipIf=(IODevice::readAnalogue(operand) & getOperand(1)) == 0;
break;
case OPCODE_IFLOCO: // do if the loco is the active one
skipIf=loco!=(uint16_t)operand; // bad luck if someone enters negative loco numbers into EXRAIL
@@ -770,6 +817,10 @@ void RMFT2::loop2() {
case OPCODE_IFCLOSED:
skipIf=Turnout::isThrown(operand);
break;
case OPCODE_IFSTASH:
skipIf=Stash::get(operand)==0;
break;
#ifndef IO_NO_HAL
case OPCODE_IFTTPOSITION: // do block if turntable at this position
@@ -815,11 +866,14 @@ void RMFT2::loop2() {
case OPCODE_FOFF:
if (loco) DCC::setFn(loco,operand,false);
break;
case OPCODE_FTOGGLE:
if (loco) DCC::changeFn(loco,operand);
break;
case OPCODE_DRIVE:
{
byte analogSpeed=IODevice::readAnalogue(operand) *127 / 1024;
if (speedo!=analogSpeed) driveLoco(analogSpeed);
// Non functional but reserved
break;
}
@@ -830,7 +884,19 @@ void RMFT2::loop2() {
case OPCODE_XFOFF:
DCC::setFn(operand,getOperand(1),false);
break;
case OPCODE_XFTOGGLE:
DCC::changeFn(operand,getOperand(1));
break;
case OPCODE_XFWD:
DCC::setThrottle(operand,getOperand(1), true);
break;
case OPCODE_XREV:
DCC::setThrottle(operand,getOperand(1), false);
break;
case OPCODE_DCCACTIVATE: {
// operand is address<<3 | subaddr<<1 | active
int16_t addr=operand>>3;
@@ -882,8 +948,9 @@ void RMFT2::loop2() {
#ifndef DISABLE_PROG
case OPCODE_JOIN:
TrackManager::setPower(POWERMODE::ON);
TrackManager::setJoin(true);
TrackManager::setMainPower(POWERMODE::ON);
TrackManager::setProgPower(POWERMODE::ON);
break;
case OPCODE_UNJOIN:
@@ -900,14 +967,11 @@ void RMFT2::loop2() {
delayMe(100);
return; // still waiting for callback
}
if (progtrackLocoId<0) {
kill(F("No Loco Found"),progtrackLocoId);
return; // still waiting for callback
}
// At failed read will result in loco == -1
// which is intended so it can be checked
// from within EXRAIL
loco=progtrackLocoId;
speedo=0;
forward=true;
invert=false;
break;
#endif
@@ -929,16 +993,13 @@ void RMFT2::loop2() {
{
int newPc=routeLookup->find(getOperand(1));
if (newPc<0) break;
RMFT2* newtask=new RMFT2(newPc); // create new task
newtask->loco=operand;
new RMFT2(newPc,operand); // create new task
}
break;
case OPCODE_SETLOCO:
{
loco=operand;
speedo=0;
forward=true;
invert=false;
}
break;
@@ -947,6 +1008,14 @@ void RMFT2::loop2() {
if ((compileFeatures & FEATURE_LCC) && LCCSerial)
StringFormatter::send(LCCSerial,F("<L x%h>"),(uint16_t)operand);
break;
case OPCODE_ACON: // MERG adapter
case OPCODE_ACOF:
if ((compileFeatures & FEATURE_LCC) && LCCSerial)
StringFormatter::send(LCCSerial,F("<L x%c%h%h>"),
opcode==OPCODE_ACON?'0':'1',
(uint16_t)operand,getOperand(progCounter,1));
break;
case OPCODE_LCCX: // long form LCC
if ((compileFeatures & FEATURE_LCC) && LCCSerial)
@@ -968,8 +1037,18 @@ void RMFT2::loop2() {
return;
}
break;
#ifndef IO_NO_HAL
case OPCODE_NEOPIXEL:
// OPCODE_NEOPIXEL,V([-]vpin),OPCODE_PAD,V(colour_RG),OPCODE_PAD,V(colour_B),OPCODE_PAD,V(count)
{
VPIN vpin=operand>0?operand:-operand;
auto count=getOperand(3);
killBlinkOnVpin(vpin,count);
IODevice::writeAnalogueRange(vpin,getOperand(1),operand>0,getOperand(2),count);
}
break;
case OPCODE_WAITFORTT: // OPCODE_WAITFOR,V(turntable_id)
if (Turntable::ttMoving(operand)) {
delayMe(100);
@@ -995,39 +1074,61 @@ void RMFT2::loop2() {
break;
case OPCODE_STASH:
if (compileFeatures & FEATURE_STASH)
stashArray[operand] = invert? -loco : loco;
Stash::set(operand,invert? -loco : loco);
break;
case OPCODE_CLEAR_STASH:
if (compileFeatures & FEATURE_STASH)
stashArray[operand] = 0;
Stash::clear(operand);
break;
case OPCODE_CLEAR_ALL_STASH:
if (compileFeatures & FEATURE_STASH)
for (int i=0;i<=maxStashId;i++) stashArray[operand]=0;
Stash::clearAll();
break;
case OPCODE_CLEAR_ANY_STASH:
if (loco) Stash::clearAny(loco);
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;
{
auto x=Stash::get(operand);
if (x>=0) {
loco=x;
invert=false;
}
else {
loco=-x;
invert=true;
}
}
break;
case OPCODE_ROUTE:
case OPCODE_AUTOMATION:
case OPCODE_SEQUENCE:
if (diag) DIAG(F("EXRAIL begin(%d)"),operand);
//if (diag) DIAG(F("EXRAIL begin(%d)"),operand);
break;
case OPCODE_BITMAP_INC:
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand)+1);
break;
case OPCODE_BITMAP_DEC:
{ int newval=IODevice::readAnalogue(operand)-1;
if (newval<0) newval=0;
IODevice::writeAnalogue(operand,newval);
}
break;
case OPCODE_BITMAP_AND:
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) & getOperand(1));
break;
case OPCODE_BITMAP_OR:
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) | getOperand(1));
break;
case OPCODE_BITMAP_XOR:
IODevice::writeAnalogue(operand,IODevice::readAnalogue(operand) ^ getOperand(1));
break;
case OPCODE_AUTOSTART: // Handled only during begin process
case OPCODE_PAD: // Just a padding for previous opcode needing >1 operand byte.
case OPCODE_TURNOUT: // Turnout definition ignored at runtime
@@ -1035,6 +1136,8 @@ void RMFT2::loop2() {
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_ONACON: // MERG event catchers ignored here
case OPCODE_ONACOF: // MERG event catchers ignored here
case OPCODE_ONTHROW:
case OPCODE_ONACTIVATE: // Activate event catchers ignored here
case OPCODE_ONDEACTIVATE:
@@ -1043,6 +1146,9 @@ void RMFT2::loop2() {
case OPCODE_ONGREEN:
case OPCODE_ONCHANGE:
case OPCODE_ONTIME:
case OPCODE_ONBUTTON:
case OPCODE_ONSENSOR:
case OPCODE_ONBITMAP:
#ifndef IO_NO_HAL
case OPCODE_DCCTURNTABLE: // Turntable definition ignored at runtime
case OPCODE_EXTTTURNTABLE: // Turntable definition ignored at runtime
@@ -1050,7 +1156,8 @@ void RMFT2::loop2() {
case OPCODE_ONROTATE:
#endif
case OPCODE_ONOVERLOAD:
case OPCODE_ONBLOCKENTER:
case OPCODE_ONBLOCKEXIT:
break;
default:
@@ -1087,25 +1194,16 @@ void RMFT2::kill(const FSH * reason, int operand) {
delete this;
}
int16_t RMFT2::getSignalSlot(int16_t id) {
for (int sigslot=0;;sigslot++) {
int16_t sigid=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigslot*8);
if (sigid==0) { // end of signal list
DIAG(F("EXRAIL Signal %d not defined"), id);
return -1;
}
// sigid is the signal id used in RED/AMBER/GREEN macro
// for a LED signal it will be same as redpin
// but for a servo signal it will also have SERVO_SIGNAL_FLAG set.
if ((sigid & SIGNAL_ID_MASK)!= id) continue; // keep looking
return sigslot; // relative slot in signals table
}
SIGNAL_DEFINITION RMFT2::getSignalSlot(int16_t slot) {
SIGNAL_DEFINITION signal;
COPYHIGHFLASH(&signal,SignalDefinitions,slot*sizeof(SIGNAL_DEFINITION),sizeof(SIGNAL_DEFINITION));
return signal;
}
/* 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);
//if (diag) DIAG(F(" doSignal %d %x"),id,rag);
// Schedule any event handler for this signal change.
// This will work even without a signal definition.
@@ -1113,77 +1211,97 @@ int16_t RMFT2::getSignalSlot(int16_t id) {
else if (rag==SIGNAL_GREEN) onGreenLookup->handleEvent(F("GREEN"),id);
else onAmberLookup->handleEvent(F("AMBER"),id);
int16_t sigslot=getSignalSlot(id);
auto sigslot=signalLookup->find(id);
if (sigslot<0) return;
// keep track of signal state
setFlag(sigslot,rag,SIGNAL_MASK);
// Correct signal definition found, get the rag values
int16_t sigpos=sigslot*8;
VPIN sigid=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos);
VPIN redpin=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+2);
VPIN amberpin=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+4);
VPIN greenpin=GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+6);
if (diag) DIAG(F("signal %d %d %d %d %d"),sigid,id,redpin,amberpin,greenpin);
VPIN sigtype=sigid & ~SIGNAL_ID_MASK;
if (sigtype == SERVO_SIGNAL_FLAG) {
// A servo signal, the pin numbers are actually servo positions
// Note, setting a signal to a zero position has no effect.
int16_t servopos= rag==SIGNAL_RED? redpin: (rag==SIGNAL_GREEN? greenpin : amberpin);
if (diag) DIAG(F("sigA %d %d"),id,servopos);
auto signal=getSignalSlot(sigslot);
switch (signal.type) {
case sigtypeSERVO:
{
auto servopos = rag==SIGNAL_RED? signal.redpin: (rag==SIGNAL_GREEN? signal.greenpin : signal.amberpin);
//if (diag) DIAG(F("sigA %d %d"),id,servopos);
if (servopos!=0) IODevice::writeAnalogue(id,servopos,PCA9685::Bounce);
return;
}
}
if (sigtype== DCC_SIGNAL_FLAG) {
case sigtypeDCC:
{
// redpin,amberpin are the DCC addr,subaddr
DCC::setAccessory(redpin,amberpin, rag!=SIGNAL_RED);
DCC::setAccessory(signal.redpin,signal.amberpin, rag!=SIGNAL_RED);
return;
}
if (sigtype== DCCX_SIGNAL_FLAG) {
case sigtypeDCCX:
{
// 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);
auto value=signal.redpin;
if (rag==SIGNAL_AMBER) value=signal.amberpin;
if (rag==SIGNAL_GREEN) value=signal.greenpin;
DCC::setExtendedAccessory(id, value);
return;
}
case sigtypeNEOPIXEL:
{
// redpin,amberpin,greenpin are the 3 RG values but with no blue permitted. . (code limitation hack)
auto colour_RG=signal.redpin;
if (rag==SIGNAL_AMBER) colour_RG=signal.amberpin;
if (rag==SIGNAL_GREEN) colour_RG=signal.greenpin;
// blue channel is in followng signal slot (a continuation)
auto signal2=getSignalSlot(sigslot+1);
auto colour_B=signal2.redpin;
if (rag==SIGNAL_AMBER) colour_B=signal2.amberpin;
if (rag==SIGNAL_GREEN) colour_B=signal2.greenpin;
IODevice::writeAnalogue(id, colour_RG,true,colour_B);
return;
}
case sigtypeSIGNAL:
case sigtypeSIGNALH:
{
// 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;
if (rag==SIGNAL_AMBER && (amberpin==0)) rag=SIMAMBER; // special case this func only
if (rag==SIGNAL_AMBER && (signal.amberpin==0)) rag=SIMAMBER; // special case this func only
// Manage invert (HIGH on) pins
bool aHigh=sigid & ACTIVE_HIGH_SIGNAL_FLAG;
bool aHigh=signal.type==sigtypeSIGNALH;
// set the three pins
if (redpin) {
if (signal.redpin) {
bool redval=(rag==SIGNAL_RED || rag==SIMAMBER);
if (!aHigh) redval=!redval;
IODevice::write(redpin,redval);
killBlinkOnVpin(signal.redpin);
IODevice::write(signal.redpin,redval);
}
if (amberpin) {
if (signal.amberpin) {
bool amberval=(rag==SIGNAL_AMBER);
if (!aHigh) amberval=!amberval;
IODevice::write(amberpin,amberval);
killBlinkOnVpin(signal.amberpin);
IODevice::write(signal.amberpin,amberval);
}
if (greenpin) {
if (signal.greenpin) {
bool greenval=(rag==SIGNAL_GREEN || rag==SIMAMBER);
if (!aHigh) greenval=!greenval;
IODevice::write(greenpin,greenval);
killBlinkOnVpin(signal.greenpin);
IODevice::write(signal.greenpin,greenval);
}
}
case sigtypeVIRTUAL: break;
case sigtypeContinuation: break;
case sigtypeNoMoreSignals: break;
}
}
/* static */ bool RMFT2::isSignal(int16_t id,char rag) {
if (!(compileFeatures & FEATURE_SIGNAL)) return false;
int16_t sigslot=getSignalSlot(id);
int16_t sigslot=signalLookup->find(id);
if (sigslot<0) return false;
return (flags[sigslot] & SIGNAL_MASK) == rag;
}
@@ -1195,25 +1313,23 @@ int16_t RMFT2::getSignalSlot(int16_t id) {
// 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);
auto sigslot=signalLookup->find(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
auto signal=getSignalSlot(sigslot);
if (signal.type!=sigtypeDCCX) 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);
if (aspect==signal.redpin) {
doSignal(address,SIGNAL_RED);
return true;
}
if (aspect==GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+4)) {
doSignal(sigid,SIGNAL_AMBER);
if (aspect==signal.amberpin) {
doSignal(address,SIGNAL_AMBER);
return true;
}
if (aspect==GETHIGHFLASHW(RMFT2::SignalDefinitions,sigpos+6)) {
doSignal(sigid,SIGNAL_GREEN);
if (aspect==signal.greenpin) {
doSignal(address,SIGNAL_GREEN);
return true;
}
@@ -1233,6 +1349,14 @@ void RMFT2::activateEvent(int16_t addr, bool activate) {
else onDeactivateLookup->handleEvent(F("DEACTIVATE"),addr);
}
void RMFT2::blockEvent(int16_t block, int16_t loco, bool entering) {
if (compileFeatures & FEATURE_BLOCK) {
// Hunt for an ONBLOCKENTER/ONBLOCKEXIT for this accessory
if (entering) onBlockEnterLookup->handleEvent(F("BLOCKENTER"),block,loco);
else onBlockExitLookup->handleEvent(F("BLOCKEXIT"),block,loco);
}
}
void RMFT2::changeEvent(int16_t vpin, bool change) {
// Hunt for an ONCHANGE for this sensor
if (change) onChangeLookup->handleEvent(F("CHANGE"),vpin);
@@ -1248,7 +1372,7 @@ void RMFT2::rotateEvent(int16_t turntableId, bool change) {
void RMFT2::clockEvent(int16_t clocktime, bool change) {
// Hunt for an ONTIME for this time
if (Diag::CMD)
DIAG(F("Looking for clock event at : %d"), clocktime);
DIAG(F("clockEvent at : %d"), clocktime);
if (change) {
onClockLookup->handleEvent(F("CLOCK"),clocktime);
onClockLookup->handleEvent(F("CLOCK"),25*60+clocktime%60);
@@ -1258,17 +1382,41 @@ void RMFT2::clockEvent(int16_t clocktime, bool change) {
void RMFT2::powerEvent(int16_t track, bool overload) {
// Hunt for an ONOVERLOAD for this item
if (Diag::CMD)
DIAG(F("Looking for Power event on track : %c"), track);
DIAG(F("powerEvent : %c"), track + 'A');
if (overload) {
onOverloadLookup->handleEvent(F("POWER"),track);
}
}
void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc) {
// This function is used when setting pins so that a SET or RESET
// will cause any blink task on that pin to terminate.
// It will be compiled out of existence if no BLINK feature is used.
void RMFT2::killBlinkOnVpin(VPIN pin, uint16_t count) {
if (!(compileFeatures & FEATURE_BLINK)) return;
RMFT2 * stoptask=loopTask; // stop when we get back to here
RMFT2 * task=loopTask;
VPIN lastPin=pin+count-1;
while(task) {
auto nextTask=task->next;
if (
(task->blinkState==blink_high || task->blinkState==blink_low)
&& task->blinkPin>=pin
&& task->blinkPin<=lastPin
) {
if (diag) DIAG(F("kill blink %d"),task->blinkPin,lastPin);
task->kill();
}
task=nextTask;
if (task==stoptask) return;
}
}
void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc, uint16_t loco) {
// Check we dont already have a task running this handler
RMFT2 * task=loopTask;
while(task) {
if (task->onEventStartPosition==pc) {
if (task->onEventStartPosition==pc && task->loco==loco) {
DIAG(F("Recursive ON%S(%d)"),reason, id);
return;
}
@@ -1276,7 +1424,7 @@ void RMFT2::startNonRecursiveTask(const FSH* reason, int16_t id,int pc) {
if (task==loopTask) break;
}
task=new RMFT2(pc); // new task starts at this instruction
task=new RMFT2(pc,loco); // new task starts at this instruction
task->onEventStartPosition=pc; // flag for recursion detector
}

View File

@@ -1,8 +1,9 @@
/*
* © 2021 Neil McKechnie
* © 2020-2022 Chris Harlow
* © 2020-2025 Chris Harlow
* © 2022-2023 Colin Murdoch
* © 2023 Harald Barth
* © 2025 Morten Nielsen
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -33,17 +34,20 @@
// or more OPCODE_PAD instructions with the subsequent parameters. This wastes a byte but makes
// searching easier as a parameter can never be confused with an opcode.
//
enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,OPCODE_TOGGLE_TURNOUT,
OPCODE_FWD,OPCODE_REV,OPCODE_SPEED,OPCODE_INVERT_DIRECTION,
OPCODE_MOMENTUM,
OPCODE_RESERVE,OPCODE_FREE,
OPCODE_AT,OPCODE_AFTER,
OPCODE_AFTEROVERLOAD,OPCODE_AUTOSTART,
OPCODE_ATGTE,OPCODE_ATLT,
OPCODE_ATTIMEOUT1,OPCODE_ATTIMEOUT2,
OPCODE_LATCH,OPCODE_UNLATCH,OPCODE_SET,OPCODE_RESET,
OPCODE_BLINK,
OPCODE_ENDIF,OPCODE_ELSE,
OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_DELAYMS,OPCODE_RANDWAIT,
OPCODE_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF,
OPCODE_FTOGGLE,OPCODE_XFTOGGLE,OPCODE_XFWD,OPCODE_XREV,
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,OPCODE_DRIVE,
OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR,
OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN,
@@ -67,12 +71,19 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
OPCODE_TTADDPOSITION,OPCODE_DCCTURNTABLE,OPCODE_EXTTTURNTABLE,
OPCODE_ONROTATE,OPCODE_ROTATE,OPCODE_WAITFORTT,
OPCODE_LCC,OPCODE_LCCX,OPCODE_ONLCC,
OPCODE_ACON, OPCODE_ACOF,
OPCODE_ONACON, OPCODE_ONACOF,
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
OPCODE_CLEAR_ANY_STASH,
OPCODE_ONBUTTON,OPCODE_ONSENSOR,OPCODE_ONVP_SENSOR,
OPCODE_NEOPIXEL,
OPCODE_ONBLOCKENTER,OPCODE_ONBLOCKEXIT,
OPCODE_ESTOPALL,OPCODE_XPOM,
OPCODE_BITMAP_AND,OPCODE_BITMAP_OR,OPCODE_BITMAP_XOR,OPCODE_BITMAP_INC,OPCODE_BITMAP_DEC,OPCODE_ONBITMAP,
// OPcodes below this point are skip-nesting IF operations
// placed here so that they may be skipped as a group
// see skipIfBlock()
IF_TYPE_OPCODES, // do not move this...
@@ -84,7 +95,9 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
OPCODE_IFCLOSED,OPCODE_IFTHROWN,
OPCODE_IFRE,
OPCODE_IFLOCO,
OPCODE_IFTTPOSITION
OPCODE_IFTTPOSITION,
OPCODE_IFSTASH,
OPCODE_IFBITMAP_ALL,OPCODE_IFBITMAP_ANY,
};
// Ensure thrunge_lcd is put last as there may be more than one display,
@@ -98,12 +111,40 @@ enum thrunger: byte {
thrunge_lcd, // Must be last!!
};
enum BlinkState: byte {
not_blink_task,
blink_low, // blink task running with pin LOW
blink_high, // blink task running with pin high
at_timeout // ATTIMEOUT timed out flag
};
enum SignalType {
sigtypeVIRTUAL,
sigtypeSIGNAL,
sigtypeSIGNALH,
sigtypeDCC,
sigtypeDCCX,
sigtypeSERVO,
sigtypeNEOPIXEL,
sigtypeContinuation, // neopixels require a second line
sigtypeNoMoreSignals
};
struct SIGNAL_DEFINITION {
SignalType type;
VPIN id;
VPIN redpin,amberpin,greenpin;
};
// 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;
// spare = 0x08;
static const byte FEATURE_BLINK = 0x04;
static const byte FEATURE_SENSOR = 0x02;
static const byte FEATURE_BLOCK = 0x01;
// Flag bits for status of hardware and TPL
@@ -130,7 +171,7 @@ class LookList {
int16_t findPosition(int16_t value); // finds index
int16_t size();
void stream(Print * _stream);
void handleEvent(const FSH* reason,int16_t id);
void handleEvent(const FSH* reason,int16_t id, int16_t loco=0);
private:
int16_t m_size;
@@ -144,8 +185,7 @@ class LookList {
public:
static void begin();
static void loop();
RMFT2(int progCounter);
RMFT2(int route, uint16_t cab);
RMFT2(int progCounter, int16_t cab=0);
~RMFT2();
static void readLocoCallback(int16_t cv);
static void createNewTask(int route, uint16_t cab);
@@ -155,13 +195,9 @@ 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 void blockEvent(int16_t block, int16_t loco, bool entering);
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
// Throttle Info Access functions built by exrail macros
static const byte rosterNameCount;
static const int16_t HIGHFLASH routeIdList[];
static const int16_t HIGHFLASH automationIdList[];
@@ -173,8 +209,11 @@ 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);
static void startNonRecursiveTask(const FSH* reason, int16_t id,int pc, uint16_t loco=0);
static bool readSensor(uint16_t sensorId);
static bool isSignal(int16_t id,char rag);
static SIGNAL_DEFINITION getSignalSlot(int16_t slotno);
private:
static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
static bool parseSlash(Print * stream, byte & paramCount, int16_t p[]) ;
@@ -183,8 +222,6 @@ private:
static bool getFlag(VPIN id,byte mask);
static int16_t progtrackLocoId;
static void doSignal(int16_t id,char rag);
static bool isSignal(int16_t id,char rag);
static int16_t getSignalSlot(int16_t id);
static void setTurnoutHiddenState(Turnout * t);
#ifndef IO_NO_HAL
static void setTurntableHiddenState(Turntable * tto);
@@ -192,11 +229,10 @@ private:
static LookList* LookListLoader(OPCODE op1,
OPCODE op2=OPCODE_ENDEXRAIL,OPCODE op3=OPCODE_ENDEXRAIL);
static uint16_t getOperand(int progCounter,byte n);
static void killBlinkOnVpin(VPIN pin,uint16_t count=1);
static RMFT2 * loopTask;
static RMFT2 * pausingTask;
void delayMe(long millisecs);
void driveLoco(byte speedo);
bool readSensor(uint16_t sensorId);
bool skipIfBlock();
bool readLoco();
void loop2();
@@ -205,13 +241,16 @@ private:
void printMessage2(const FSH * msg);
void thrungeString(uint32_t strfar, thrunger mode, byte id=0);
uint16_t getOperand(byte n);
void pause();
void resume();
static bool diag;
static const HIGHFLASH3 byte RouteCode[];
static const HIGHFLASH int16_t SignalDefinitions[];
static const HIGHFLASH SIGNAL_DEFINITION SignalDefinitions[];
static byte flags[MAX_FLAGS];
static Print * LCCSerial;
static LookList * routeLookup;
static LookList * signalLookup;
static LookList * onThrowLookup;
static LookList * onCloseLookup;
static LookList * onActivateLookup;
@@ -225,6 +264,9 @@ private:
static LookList * onRotateLookup;
#endif
static LookList * onOverloadLookup;
static LookList * onBlockEnterLookup;
static LookList * onBlockExitLookup;
static const int countLCCLookup;
static int onLCCLookup[];
@@ -244,14 +286,13 @@ private:
union {
unsigned long waitAfter; // Used by OPCODE_AFTER
unsigned long timeoutStart; // Used by OPCODE_ATTIMEOUT
VPIN blinkPin; // Used by blink tasks
};
bool timeoutFlag;
byte taskId;
BlinkState blinkState; // includes AT_TIMEOUT flag.
uint16_t loco;
bool forward;
bool invert;
byte speedo;
byte pauseSpeed;
int onEventStartPosition;
byte stackDepth;
int callStack[MAX_STACK_DEPTH];

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
/*
* © 2021 Neil McKechnie
* © 2021-2023 Harald Barth
* © 2020-2023 Chris Harlow
* © 2020-2025 Chris Harlow
* © 2022-2023 Colin Murdoch
* All rights reserved.
*
@@ -36,7 +36,7 @@
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':
@@ -47,8 +47,7 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
break;
case '/': // New EXRAIL command
reject=!parseSlash(stream,paramCount,p);
opcode=0;
if (parseSlash(stream,paramCount,p)) opcode=0;
break;
case 'A': // <A address aspect>
@@ -62,53 +61,93 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
case 'L':
// This entire code block is compiled out if LLC macros not used
if (!(compileFeatures & FEATURE_LCC)) return;
static int lccProgCounter=0;
static int lccEventIndex=0;
if (paramCount==0) { //<L> LCC adapter introducing self
LCCSerial=stream; // now we know where to send events we raise
opcode=0; // flag command as intercepted
// 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"),
// loop through all possible sent/waited events
for (int progCounter=lccProgCounter;; SKIPOP) {
byte exrailOpcode=GET_OPCODE;
switch (exrailOpcode) {
case OPCODE_ENDEXRAIL:
stream->print(F("<LR>\n")); // ready to roll
lccProgCounter=0; // allow a second pass
lccEventIndex=0;
return;
case OPCODE_LCC:
StringFormatter::send(stream,F("<LS x%h>\n"),getOperand(progCounter,0));
SKIPOP;
lccProgCounter=progCounter;
return;
case 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)
);
}}
);
SKIPOP;SKIPOP;SKIPOP;SKIPOP;
lccProgCounter=progCounter;
return;
case OPCODE_ACON: // CBUS ACON
case OPCODE_ACOF: // CBUS ACOF
StringFormatter::send(stream,F("<LS x%c%h%h>\n"),
exrailOpcode==OPCODE_ACOF?'1':'0',
getOperand(progCounter,0),getOperand(progCounter,1));
SKIPOP;SKIPOP;
lccProgCounter=progCounter;
return;
// 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...
case OPCODE_ONLCC:
StringFormatter::send(stream,F("<LL %d x%h%h%h:%h>\n"),
eventIndex,
lccEventIndex,
getOperand(progCounter,1),
getOperand(progCounter,2),
getOperand(progCounter,3),
getOperand(progCounter,0)
);
eventIndex++;
}
SKIPOP;SKIPOP;SKIPOP;SKIPOP;
// start on handler at next
onLCCLookup[lccEventIndex]=progCounter;
lccEventIndex++;
lccProgCounter=progCounter;
return;
case OPCODE_ONACON:
case OPCODE_ONACOF:
StringFormatter::send(stream,F("<LL %d x%c%h%h>\n"),
lccEventIndex,
exrailOpcode==OPCODE_ONACOF?'1':'0',
getOperand(progCounter,0),getOperand(progCounter,1)
);
SKIPOP;SKIPOP;
// start on handler at next
onLCCLookup[lccEventIndex]=progCounter;
lccEventIndex++;
lccProgCounter=progCounter;
return;
default:
break;
}
}
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;
bool reject = eventid<0 || eventid>=countLCCLookup;
if (!reject) {
startNonRecursiveTask(F("LCC"),eventid,onLCCLookup[eventid]);
opcode=0;
}
}
break;
@@ -142,39 +181,20 @@ void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16
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;
}
case 'K': // <K blockid loco> Block enter
case 'k': // <k blockid loco> Block exit
if (paramCount!=2) break;
blockEvent(p[0],p[1],opcode=='K');
opcode=0;
break;
default: // other commands pass through
break;
}
}
}
bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
@@ -182,12 +202,18 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
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'
if ((compileFeatures & FEATURE_BLINK)
&& (task->blinkState==blink_high || task->blinkState==blink_low)) {
StringFormatter::send(stream,F("\nID=%d,PC=%d,BLINK=%d"),
(int)(task->taskId),task->progCounter,task->blinkPin
);
}
else {
StringFormatter::send(stream,F("\nID=%d,PC=%d,LOCO=%d %c"),
(int)(task->taskId),task->progCounter,task->loco,
task->invert?'I':' '
);
}
task=task->next;
if (task==loopTask) break;
}
@@ -205,42 +231,42 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
// 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
SIGNAL_DEFINITION slot=getSignalSlot(sigslot);
if (slot.type==sigtypeNoMoreSignals) break; // end of signal list
if (slot.type==sigtypeContinuation) continue; // continueation of previous line
byte flag=flags[sigslot] & SIGNAL_MASK; // obtain signal flags for this ids
StringFormatter::send(stream,F("\n%S[%d]"),
(flag == SIGNAL_RED)? F("RED") : (flag==SIGNAL_GREEN) ? F("GREEN") : F("AMBER"),
sigid & SIGNAL_ID_MASK);
(flag == SIGNAL_RED)? F("RED") : (flag==SIGNAL_GREEN) ? F("GREEN") : F("AMBER"),
slot.id);
}
}
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
{ // pause all tasks
RMFT2 * task=loopTask;
while(task) {
task->pause();
task=task->next;
if (task==loopTask) break;
}
}
DCC::estopAll(); // 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;
{
{ // resume all tasks
RMFT2 * task=loopTask;
while(task) {
if (task->loco) task->driveLoco(task->speedo);
task=task->next;
if (task==loopTask) break;
task->resume();
task=task->next;
if (task==loopTask) break;
}
}
return true;
@@ -253,8 +279,7 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
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;
new RMFT2(pc,cab);
}
return true;
@@ -315,4 +340,3 @@ bool RMFT2::parseSlash(Print * stream, byte & paramCount, int16_t p[]) {
return false;
}
}

165
EXRAILAsserts.h Normal file
View File

@@ -0,0 +1,165 @@
/*
* © 2020-2025 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// This file checks the myAutomation for errors by generating a list of compile time asserts.
// Assert Pass 1 Collect sequence numbers.
#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 number duplication
constexpr int16_t seqCount(const int16_t value, const int16_t pos=0, const int16_t count=0 ) {
return pos>=stuffSize? count :
seqCount(value,pos+1,count+((compileTimeSequenceList[pos]==value)?1:0));
}
// Build a compile time blacklist of pin numbers.
// Includes those defined in defaults.h for the cpu (PIN_BLACKLIST)
// and cheats in the motor shield pins from config.h (MOTOR_SHIELD_TYPE)
// for reference the MotorDriver constructor is:
// MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin, byte current_pin,
// float senseFactor, unsigned int tripMilliamps, byte faultPin);
// create capture macros to reinterpret MOTOR_SHIELD_TYPE from configuration
#define new
#define MotorDriver(power_pin,signal_pin,signal_pin2, \
brake_pin,current_pin,senseFactor,tripMilliamps,faultPin) \
abs(power_pin),abs(signal_pin),abs(signal_pin2),abs(brake_pin),abs(current_pin),abs(faultPin)
#ifndef PIN_BLACKLIST
#define PIN_BLACKLIST UNUSED_PIN
#endif
#define MDFURKLE(stuff) MDFURKLE2(stuff)
#define MDFURKLE2(description,...) REMOVE_TRAILING_COMMA(__VA_ARGS__)
#define REMOVE_TRAILING_COMMA(...) __VA_ARGS__
constexpr int16_t compileTimePinBlackList[]={
PIN_BLACKLIST, MDFURKLE(MOTOR_SHIELD_TYPE)
};
constexpr int16_t pbSize=sizeof(compileTimePinBlackList)/sizeof(int16_t) - 1;
// remove capture macros
#undef new
#undef MotorDriver
// Compile time function to check for dangerous pins.
constexpr bool unsafePin(const int16_t value, const uint16_t pos=0 ) {
return pos>=pbSize? false :
compileTimePinBlackList[pos]==value
|| unsafePin(value,pos+1);
}
//pass 2 apply static asserts:
// check call and follows etc for existing sequence numbers
// check sequence numbers for duplicates
// check range on LATCH/UNLATCH
// check range on RESERVE/FREE
// check range on SPEED/FWD/REV
// check range on SET/RESET (pins that are not safe to use in EXRAIL)
//
// 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");
// check references to sequences/routes/automations
#undef CALL
#define CALL(id) static_assert(seqCount(id)>0,"Sequence not found");
#undef FOLLOW
#define FOLLOW(id) static_assert(seqCount(id)>0,"Sequence not found");
#undef START
#define START(id) static_assert(seqCount(id)>0,"Sequence not found");
#undef SENDLOCO
#define SENDLOCO(cab,id) static_assert(seqCount(id)>0,"Sequence not found");
#undef ROUTE_ACTIVE
#define ROUTE_ACTIVE(id) static_assert(seqCount(id)>0,"Route not found");
#undef ROUTE_INACTIVE
#define ROUTE_INACTIVE(id) static_assert(seqCount(id)>0,"Route not found");
#undef ROUTE_HIDDEN
#define ROUTE_HIDDEN(id) static_assert(seqCount(id)>0,"Route not found");
#undef ROUTE_DISABLED
#define ROUTE_DISABLED(id) static_assert(seqCount(id)>0,"Route not found");
#undef ROUTE_CAPTION
#define ROUTE_CAPTION(id,caption) static_assert(seqCount(id)>0,"Route 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 IFRESERVE
#define IFRESERVE(id) static_assert(id>=0 && id<MAX_FLAGS,"Id out of valid range 0-255" );
//check speeds
#undef SPEED
#define SPEED(speed) static_assert(speed>=0 && speed<128,"\n\nUSER ERROR: Speed out of valid range 0-127\n");
#undef FWD
#define FWD(speed) static_assert(speed>=0 && speed<128,"\n\nUSER ERROR: Speed out of valid range 0-127\n");
#undef REV
#define REV(speed) static_assert(speed>=0 && speed<128,"\n\nUSER ERROR: Speed out of valid range 0-127\n");
// check duplicate sequences
#undef SEQUENCE
#define SEQUENCE(id) static_assert(seqCount(id)==1,"\n\nUSER ERROR: Duplicate ROUTE/AUTOMATION/SEQUENCE(" #id ")\n");
#undef AUTOMATION
#define AUTOMATION(id,description) static_assert(seqCount(id)==1,"\n\nUSER ERROR: Duplicate ROUTE/AUTOMATION/SEQUENCE(" #id ")\n");
#undef ROUTE
#define ROUTE(id,description) static_assert(seqCount(id)==1,"\n\nUSER ERROR: Duplicate ROUTE/AUTOMATION/SEQUENCE(" #id ")\n");
// check dangerous pins
#define _PIN_RESERVED_ "\n\nUSER ERROR: Pin is used by Motor Shield or other critical function.\n"
#undef SET
#define SET(vpin, ...) static_assert(!unsafePin(vpin),"SET(" #vpin ")" _PIN_RESERVED_);
#undef RESET
#define RESET(vpin,...) static_assert(!unsafePin(vpin),"RESET(" #vpin ")" _PIN_RESERVED_);
#undef BLINK
#define BLINK(vpin,onDuty,offDuty) static_assert(!unsafePin(vpin),"BLINK(" #vpin ")" _PIN_RESERVED_);
#undef SIGNAL
#define SIGNAL(redpin,amberpin,greenpin) \
static_assert(!unsafePin(redpin),"Red pin " #redpin _PIN_RESERVED_); \
static_assert(amberpin==0 ||!unsafePin(amberpin),"Amber pin " #amberpin _PIN_RESERVED_); \
static_assert(!unsafePin(greenpin),"Green pin " #greenpin _PIN_RESERVED_);
#undef SIGNALH
#define SIGNALH(redpin,amberpin,greenpin) \
static_assert(!unsafePin(redpin),"Red pin " #redpin _PIN_RESERVED_); \
static_assert(amberpin==0 ||!unsafePin(amberpin),"Amber pin " #amberpin _PIN_RESERVED_); \
static_assert(!unsafePin(greenpin),"Green pin " #greenpin _PIN_RESERVED_);
// and run the assert pass.
#include "myAutomation.h"

View File

@@ -1,8 +1,9 @@
/*
* © 2021 Neil McKechnie
* © 2020-2022 Chris Harlow
* © 2020-2025 Chris Harlow
* © 2022-2023 Colin Murdoch
* © 2023 Harald Barth
* © 2025 Morten Nielsen
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -23,6 +24,7 @@
#ifndef EXRAILMacros_H
#define EXRAILMacros_H
#include "IODeviceList.h"
// remove normal code LCD & SERIAL macros (will be restored later)
#undef LCD
@@ -63,6 +65,10 @@
// playing sounds with IO_I2CDFPlayer
#define PLAYSOUND ANOUT
// SEG7 is a helper to create ANOUT from a 7-segment request
#define SEG7(vpin,value,format) \
ANOUT(vpin,(value & 0xFFFF),TM1638::DF_##format,((uint32_t)value)>>16)
// helper macro to strip leading zeros off time inputs
// (10#mins)%100)
#define STRIP_ZERO(value) 10##value%100
@@ -71,78 +77,22 @@
//const byte TRACK_POWER_0=0, TRACK_POWER_OFF=0;
//const byte TRACK_POWER_1=1, TRACK_POWER_ON=1;
// NEOPIXEL RG generator for NEOPIXEL_SIGNAL
#define NeoRGB(red,green,blue) (((uint32_t)(red & 0xff)<<16) | ((uint32_t)(green & 0xff)<<8) | (uint32_t)(blue & 0xff))
// Pass 1 Implements aliases
#include "EXRAIL2MacroReset.h"
#undef ALIAS
#define ALIAS(name,value...) const int name= 1##value##0 ==10 ? -__COUNTER__ : value##0/10;
#define ALIAS(name,value...) const int name= #value[0] ? value+0: -__COUNTER__ ;
#include "myAutomation.h"
// Pass 1d Detect sequence duplicates.
// This pass generates no runtime data or code
// Perform compile time asserts to check the script for errors
#include "EXRAILAsserts.h"
// Pass 1g Implants STEALTH_GLOBAL in correct place
#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");
#undef STEALTH_GLOBAL
#define STEALTH_GLOBAL(code...) code
#include "myAutomation.h"
// Pass 1h Implements HAL macro by creating exrailHalSetup function
@@ -174,6 +124,8 @@ bool exrailHalSetup() {
#define DCC_SIGNAL(id,addr,subaddr) | FEATURE_SIGNAL
#undef DCCX_SIGNAL
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) | FEATURE_SIGNAL
#undef NEOPIXEL_SIGNAL
#define NEOPIXEL_SIGNAL(sigid,redcolour,ambercolour,greencolour) | FEATURE_SIGNAL
#undef VIRTUAL_SIGNAL
#define VIRTUAL_SIGNAL(id) | FEATURE_SIGNAL
@@ -183,6 +135,14 @@ bool exrailHalSetup() {
#define LCCX(senderid,eventid) | FEATURE_LCC
#undef ONLCC
#define ONLCC(senderid,eventid) | FEATURE_LCC
#undef ACON
#define ACON(eventid) | FEATURE_LCC
#undef ACOF
#define ACOF(eventid) | FEATURE_LCC
#undef ONACON
#define ONACON(eventid) | FEATURE_LCC
#undef ONACOF
#define ONACOF(eventid) | FEATURE_LCC
#undef ROUTE_ACTIVE
#define ROUTE_ACTIVE(id) | FEATURE_ROUTESTATE
#undef ROUTE_INACTIVE
@@ -194,14 +154,18 @@ bool exrailHalSetup() {
#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
#undef BLINK
#define BLINK(vpin,onDuty,offDuty) | FEATURE_BLINK
#undef ONBUTTON
#define ONBUTTON(vpin) | FEATURE_SENSOR
#undef ONSENSOR
#define ONSENSOR(vpin) | FEATURE_SENSOR
#undef ONBITMAP
#define ONBITMAP(vpin) | FEATURE_SENSOR
#undef ONBLOCKENTER
#define ONBLOCKENTER(blockid) | FEATURE_BLOCK
#undef ONBLOCKEXIT
#define ONBLOCKEXIT(blockid) | FEATURE_BLOCK
const byte RMFT2::compileFeatures = 0
#include "myAutomation.h"
@@ -401,26 +365,35 @@ const FSH * RMFT2::getRosterFunctions(int16_t id) {
// Pass 8 Signal definitions
#include "EXRAIL2MacroReset.h"
#undef SIGNAL
#define SIGNAL(redpin,amberpin,greenpin) redpin,redpin,amberpin,greenpin,
#define SIGNAL(redpin,amberpin,greenpin) {sigtypeSIGNAL,redpin,redpin,amberpin,greenpin},
#undef SIGNALH
#define SIGNALH(redpin,amberpin,greenpin) redpin | RMFT2::ACTIVE_HIGH_SIGNAL_FLAG,redpin,amberpin,greenpin,
#define SIGNALH(redpin,amberpin,greenpin) {sigtypeSIGNALH,redpin,redpin,amberpin,greenpin},
#undef SERVO_SIGNAL
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) vpin | RMFT2::SERVO_SIGNAL_FLAG,redval,amberval,greenval,
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) {sigtypeSERVO,vpin,redval,amberval,greenval},
#undef DCC_SIGNAL
#define DCC_SIGNAL(id,addr,subaddr) id | RMFT2::DCC_SIGNAL_FLAG,addr,subaddr,0,
#define DCC_SIGNAL(id,addr,subaddr) {sigtypeDCC,id,addr,subaddr,0},
#undef DCCX_SIGNAL
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) id | RMFT2::DCCX_SIGNAL_FLAG,redAspect,amberAspect,greenAspect,
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) {sigtypeDCCX,id,redAspect,amberAspect,greenAspect},
#undef NEOPIXEL_SIGNAL
#define NEOPIXEL_SIGNAL(id,redRGB,amberRGB,greenRGB) \
{sigtypeNEOPIXEL,id,((VPIN)((redRGB)>>8)), ((VPIN)((amberRGB)>>8)), ((VPIN)((greenRGB)>>8))},\
{sigtypeContinuation,id,((VPIN)((redRGB) & 0xff)), ((VPIN)((amberRGB) & 0xFF)), ((VPIN)((greenRGB) & 0xFF))},
#undef VIRTUAL_SIGNAL
#define VIRTUAL_SIGNAL(id) id,0,0,0,
#define VIRTUAL_SIGNAL(id) {sigtypeVIRTUAL,id,0,0,0},
const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
const HIGHFLASH SIGNAL_DEFINITION RMFT2::SignalDefinitions[] = {
#include "myAutomation.h"
0,0,0,0 };
{sigtypeNoMoreSignals,0,0,0,0}
};
// Pass 9 ONLCC counter and lookup array
// Pass 9 ONLCC/ ONMERG counter and lookup array
#include "EXRAIL2MacroReset.h"
#undef ONLCC
#define ONLCC(sender,event) +1
#undef ONACON
#define ONACON(event) +1
#undef ONACOF
#define ONACOF(event) +1
const int RMFT2::countLCCLookup=0
#include "myAutomation.h"
@@ -439,7 +412,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define ACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1 | 1),
#define ACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1 | 1),
#define AFTER(sensor_id) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id),
#define AFTER(sensor_id,timer...) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id),OPCODE_PAD,V(#timer[0]?timer+0:500),
#define AFTEROVERLOAD(track_id) OPCODE_AFTEROVERLOAD,V(TRACK_NUMBER_##track_id),
#define ALIAS(name,value...)
#define AMBER(signal_id) OPCODE_AMBER,V(signal_id),
@@ -451,10 +424,12 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define ATTIMEOUT(sensor_id,timeout) OPCODE_ATTIMEOUT1,0,0,OPCODE_ATTIMEOUT2,V(sensor_id),OPCODE_PAD,V(timeout/100L),
#define AUTOMATION(id, description) OPCODE_AUTOMATION, V(id),
#define AUTOSTART OPCODE_AUTOSTART,0,0,
#define BLINK(vpin,onDuty,offDuty) OPCODE_BLINK,V(vpin),OPCODE_PAD,V(onDuty),OPCODE_PAD,V(offDuty),
#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 CLEAR_ANY_STASH OPCODE_CLEAR_ANY_STASH,V(0),
#define CLOSE(id) OPCODE_CLOSE,V(id),
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile)
#ifndef IO_NO_HAL
@@ -474,16 +449,18 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define ENDIF OPCODE_ENDIF,0,0,
#define ENDTASK OPCODE_ENDTASK,0,0,
#define ESTOP OPCODE_SPEED,V(1),
#define ESTOPALL OPCODE_ESTOPALL,0,0,
#define EXRAIL
#ifndef IO_NO_HAL
#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 FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V((int16_t)PCA9685::ProfileType::UseDuration|(int16_t)PCA9685::ProfileType::NoPowerOff),OPCODE_PAD,V(ms/100L),
#define FOFF(func) OPCODE_FOFF,V(func),
#define FOLLOW(route) OPCODE_FOLLOW,V(route),
#define FON(func) OPCODE_FON,V(func),
#define FORGET OPCODE_FORGET,0,0,
#define FREE(blockid) OPCODE_FREE,V(blockid),
#define FTOGGLE(func) OPCODE_FTOGGLE,V(func),
#define FWD(speed) OPCODE_FWD,V(speed),
#define GREEN(signal_id) OPCODE_GREEN,V(signal_id),
#define HAL(haltype,params...)
@@ -499,12 +476,15 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define IFRANDOM(percent) OPCODE_IFRANDOM,V(percent),
#define IFRED(signal_id) OPCODE_IFRED,V(signal_id),
#define IFRESERVE(block) OPCODE_IFRESERVE,V(block),
#define IFSTASH(stash_id) OPCODE_IFSTASH,V(stash_id),
#define IFTHROWN(turnout_id) OPCODE_IFTHROWN,V(turnout_id),
#define IFTIMEOUT OPCODE_IFTIMEOUT,0,0,
#ifndef IO_NO_HAL
#define IFTTPOSITION(id,position) OPCODE_IFTTPOSITION,V(id),OPCODE_PAD,V(position),
#endif
#define IFRE(sensor_id,value) OPCODE_IFRE,V(sensor_id),OPCODE_PAD,V(value),
#define IFBITMAP_ALL(vpin,mask) OPCODE_IFBITMAP_ALL,V(vpin),OPCODE_PAD,V(mask),
#define IFBITMAP_ANY(vpin,mask) OPCODE_IFBITMAP_ANY,V(vpin),OPCODE_PAD,V(mask),
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0,
#define JMRI_SENSOR(vpin,count...)
#define JOIN OPCODE_JOIN,0,0,
@@ -515,15 +495,29 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
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 ACON(eventid) OPCODE_ACON,V(((uint32_t)eventid >>16) & 0xFFFF),OPCODE_PAD,V(eventid & 0xFFFF),
#define ACOF(eventid) OPCODE_ACOF,V(((uint32_t)eventid >>16) & 0xFFFF),OPCODE_PAD,V(eventid & 0xFFFF),
#define ONACON(eventid) OPCODE_ONACON,V((uint32_t)(eventid) >>16),OPCODE_PAD,V(eventid & 0xFFFF),
#define ONACOF(eventid) OPCODE_ONACOF,V((uint32_t)(eventid) >>16),OPCODE_PAD,V(eventid & 0xFFFF),
#define LCD(id,msg) PRINT(msg)
#define SCREEN(display,id,msg) PRINT(msg)
#define STEALTH(code...) PRINT(dummy)
#define STEALTH_GLOBAL(code...)
#define LCN(msg) PRINT(msg)
#define MESSAGE(msg) PRINT(msg)
#define MOMENTUM(accel,decel...) OPCODE_MOMENTUM,V(accel),OPCODE_PAD,V(#decel[0]?decel+0:accel),
#define MOVETT(id,steps,activity) OPCODE_SERVO,V(id),OPCODE_PAD,V(steps),OPCODE_PAD,V(EXTurntable::activity),OPCODE_PAD,V(0),
#define NEOPIXEL(id,r,g,b,count...) OPCODE_NEOPIXEL,V(id),\
OPCODE_PAD,V(((r & 0xff)<<8) | (g & 0xff)),\
OPCODE_PAD,V((b & 0xff)),\
OPCODE_PAD,V(#count[0]?(count+0):1),
#define NEOPIXEL_SIGNAL(sigid,redcolour,ambercolour,greencolour)
#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 ONBLOCKENTER(block_id) OPCODE_ONBLOCKENTER,V(block_id),
#define ONBLOCKEXIT(block_id) OPCODE_ONBLOCKEXIT,V(block_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),\
@@ -542,12 +536,13 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#endif
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
#define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id),
#define ONSENSOR(sensor_id) OPCODE_ONSENSOR,V(sensor_id),
#define ONBITMAP(sensor_id) OPCODE_ONBITMAP,V(sensor_id),
#define ONBUTTON(sensor_id) OPCODE_ONBUTTON,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),
#endif
#define POWEROFF OPCODE_POWEROFF,0,0,
#define POWERON OPCODE_POWERON,0,0,
#define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2),
@@ -555,7 +550,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define READ_LOCO OPCODE_READ_LOCO1,0,0,OPCODE_READ_LOCO2,0,0,
#define RED(signal_id) OPCODE_RED,V(signal_id),
#define RESERVE(blockid) OPCODE_RESERVE,V(blockid),
#define RESET(pin) OPCODE_RESET,V(pin),
#define RESET(pin,count...) OPCODE_RESET,V(pin),OPCODE_PAD,V(#count[0] ? count+0: 1),
#define RESUME OPCODE_RESUME,0,0,
#define RETURN OPCODE_RETURN,0,0,
#define REV(speed) OPCODE_REV,V(speed),
@@ -583,11 +578,11 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define SERVO2(id,position,ms) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::Instant),OPCODE_PAD,V(ms/100L),
#define SERVO_SIGNAL(vpin,redpos,amberpos,greenpos)
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile),
#define SET(pin) OPCODE_SET,V(pin),
#define SET(pin,count...) OPCODE_SET,V(pin),OPCODE_PAD,V(#count[0] ? count+0: 1),
#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 SETFREQ(freq) OPCODE_SETFREQ,V(freq),
#define SIGNAL(redpin,amberpin,greenpin)
#define SIGNALH(redpin,amberpin,greenpin)
#define SPEED(speed) OPCODE_SPEED,V(speed),
@@ -595,6 +590,7 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define STASH(id) OPCODE_STASH,V(id),
#define STOP OPCODE_SPEED,V(0),
#define THROW(id) OPCODE_THROW,V(id),
#define TOGGLE_TURNOUT(id) OPCODE_TOGGLE_TURNOUT,V(id),
#ifndef IO_NO_HAL
#define TT_ADDPOSITION(id,position,value,angle,description...) OPCODE_TTADDPOSITION,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(value),OPCODE_PAD,V(angle),
#endif
@@ -604,6 +600,11 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id),
#define VIRTUAL_SIGNAL(id)
#define VIRTUAL_TURNOUT(id,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(0),
#define BITMAP_AND(vpin,mask) OPCODE_BITMAP_AND,V(vpin),OPCODE_PAD,V(mask),
#define BITMAP_INC(vpin) OPCODE_BITMAP_INC,V(vpin),
#define BITMAP_DEC(vpin) OPCODE_BITMAP_DEC,V(vpin),
#define BITMAP_OR(vpin,mask) OPCODE_BITMAP_OR,V(vpin),OPCODE_PAD,V(mask),
#define BITMAP_XOR(vpin,mask) OPCODE_BITMAP_XOR,V(vpin),OPCODE_PAD,V(mask),
#define WITHROTTLE(msg) PRINT(msg)
#define WAITFOR(pin) OPCODE_WAITFOR,V(pin),
#ifndef IO_NO_HAL
@@ -611,6 +612,10 @@ int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
#endif
#define XFOFF(cab,func) OPCODE_XFOFF,V(cab),OPCODE_PAD,V(func),
#define XFON(cab,func) OPCODE_XFON,V(cab),OPCODE_PAD,V(func),
#define XFTOGGLE(cab,func) OPCODE_XFTOGGLE,V(cab),OPCODE_PAD,V(func),
#define XFWD(cab,speed) OPCODE_XFWD,V(cab),OPCODE_PAD,V(speed),
#define XREV(cab,speed) OPCODE_XREV,V(cab),OPCODE_PAD,V(speed),
#define XPOM(cab,cv,value) OPCODE_XPOM,V(cab),OPCODE_PAD,V(cv),OPCODE_PAD,V(value),
// Build RouteCode
const int StringMacroTracker2=__COUNTER__;

104
EXRAILSensor.cpp Normal file
View File

@@ -0,0 +1,104 @@
/*
* © 2024 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/>.
*/
/**********************************************************************
EXRAILSensor represents a sensor that should be monitored in order
to call an exrail ONBUTTON or ONCHANGE handler.
These are created at EXRAIL startup and thus need no delete or listing
capability.
The basic logic is similar to that found in the Sensor class
except that on the relevant change an EXRAIL thread is started.
**********************************************************************/
#include "EXRAILSensor.h"
#include "EXRAIL2.h"
void EXRAILSensor::checkAll() {
if (firstSensor == NULL) return; // No sensors to be scanned
if (readingSensor == NULL) {
// Not currently scanning sensor list
unsigned long thisTime = micros();
if (thisTime - lastReadCycle < cycleInterval) return;
// Required time has elapsed since last read cycle started,
// so initiate new scan through the sensor list
readingSensor = firstSensor;
lastReadCycle = thisTime;
}
// Loop until either end of list is encountered or we pause for some reason
byte sensorCount = 0;
while (readingSensor != NULL) {
bool pause=readingSensor->check();
// Move to next sensor in list.
readingSensor = readingSensor->nextSensor;
// Currently process max of 16 sensors per entry.
// Performance measurements taken during development indicate that, with 128 sensors configured
// on 8x 16-pin MCP23017 GPIO expanders with polling (no change notification), all inputs can be read from the devices
// within 1.4ms (400Mhz I2C bus speed), and a full cycle of checking 128 sensors for changes takes under a millisecond.
if (pause || (++sensorCount)>=16) return;
}
}
bool EXRAILSensor::check() {
// check for debounced change in this sensor
inputState = useAnalog?IODevice::readAnalogue(pin):RMFT2::readSensor(pin);
// Check if changed since last time, and process changes.
if (inputState == active) {// no change
latchDelay = minReadCount; // Reset counter
return false; // no change
}
// Change detected ... has it stayed changed for long enough
if (latchDelay > 0) {
latchDelay--;
return false;
}
// change validated, act on it.
active = inputState;
latchDelay = minReadCount; // Reset debounce counter
if (onChange || active) {
new RMFT2(progCounter);
return true; // Don't check any more sensors on this entry
}
return false;
}
EXRAILSensor::EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange, bool _useAnalog) {
nextSensor = firstSensor;
firstSensor = this;
pin=_pin;
progCounter=_progCounter;
onChange=_onChange;
useAnalog=_useAnalog;
IODevice::configureInput(pin, true);
active = useAnalog?IODevice::readAnalogue(pin): IODevice::read(pin);
inputState = active;
latchDelay = minReadCount;
}
EXRAILSensor *EXRAILSensor::firstSensor=NULL;
EXRAILSensor *EXRAILSensor::readingSensor=NULL;
unsigned long EXRAILSensor::lastReadCycle=0;

52
EXRAILSensor.h Normal file
View File

@@ -0,0 +1,52 @@
/*
* © 2024 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef EXRAILSensor_h
#define EXRAILSensor_h
#include "IODevice.h"
class EXRAILSensor {
static EXRAILSensor * firstSensor;
static EXRAILSensor * readingSensor;
static unsigned long lastReadCycle;
public:
static void checkAll();
EXRAILSensor(VPIN _pin, int _progCounter, bool _onChange, bool _useAnalog=false);
bool check();
private:
static const unsigned int cycleInterval = 10000; // min time between consecutive reads of each sensor in microsecs.
// should not be less than device scan cycle time.
static const byte minReadCount = 4; // number of additional scans before acting on change
// E.g. 1 means that a change is ignored for one scan and actioned on the next.
// Max value is 63
EXRAILSensor* nextSensor;
VPIN pin;
int progCounter;
uint16_t active;
uint16_t inputState;
bool onChange;
bool useAnalog;
byte latchDelay;
};
#endif

200
EXmDNS.cpp Normal file
View File

@@ -0,0 +1,200 @@
/*
* © 2024 Harald Barth
* © 2024 Paul M. Antoine
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "EthernetInterface.h"
#ifdef DO_MDNS
#include "EXmDNS.h"
// fixed values for mDNS
static IPAddress mdnsMulticastIPAddr = IPAddress(224, 0, 0, 251);
#define MDNS_SERVER_PORT 5353
// dotToLen()
// converts stings of form ".foo.barbar.x" to a string with the
// dots replaced with lenght. So string above would result in
// "\x03foo\x06barbar\x01x" in C notation. If not NULL, *substr
// will point to the beginning of the last component, in this
// example that would be "\x01x".
//
static void dotToLen(char *str, char **substr) {
char *dotplace = NULL;
char *s;
byte charcount = 0;
for (s = str;/*see break*/ ; s++) {
if (*s == '.' || *s == '\0') {
// take care of accumulated
if (dotplace != NULL && charcount != 0) {
*dotplace = charcount;
}
if (*s == '\0')
break;
if (substr && *s == '.')
*substr = s;
// set new values
dotplace = s;
charcount = 0;
} else {
charcount++;
}
}
}
MDNS::MDNS(EthernetUDP& udp) {
_udp = &udp;
}
MDNS::~MDNS() {
_udp->stop();
if (_name) free(_name);
if (_serviceName) free(_serviceName);
if (_serviceProto) free(_serviceProto);
}
int MDNS::begin(const IPAddress& ip, char* name) {
// if we were called very soon after the board was booted, we need to give the
// EthernetShield (WIZnet) some time to come up. Hence, we delay until millis() is at
// least 3000. This is necessary, so that if we need to add a service record directly
// after begin, the announce packet does not get lost in the bowels of the WIZnet chip.
//while (millis() < 3000)
// delay(100);
_ipAddress = ip;
_name = (char *)malloc(strlen(name)+2);
byte n;
for(n = 0; n<strlen(name); n++)
_name[n+1] = name[n];
_name[n+1] = '\0';
_name[0] = '.';
dotToLen(_name, NULL);
return _udp->beginMulticast(mdnsMulticastIPAddr, MDNS_SERVER_PORT);
}
int MDNS::addServiceRecord(const char* name, uint16_t port, MDNSServiceProtocol_t proto) {
// we ignore proto, assume TCP
(void)proto;
_serviceName = (char *)malloc(strlen(name) + 2);
byte n;
for(n = 0; n<strlen(name); n++)
_serviceName[n+1] = name[n];
_serviceName[n+1] = '\0';
_serviceName[0] = '.';
_serviceProto = NULL; //to be filled in
dotToLen(_serviceName, &_serviceProto);
_servicePort = port;
return 1;
}
static char dns_rr_services[] = "\x09_services\x07_dns-sd\x04_udp\x05local";
static char dns_rr_tcplocal[] = "\x04_tcp\x05local";
static char *dns_rr_local = dns_rr_tcplocal + dns_rr_tcplocal[0] + 1;
typedef struct _DNSHeader_t
{
uint16_t xid;
uint16_t flags; // flags condensed
uint16_t queryCount;
uint16_t answerCount;
uint16_t authorityCount;
uint16_t additionalCount;
} __attribute__((__packed__)) DNSHeader_t;
//
// MDNS::run()
// This broadcasts whatever we got evey BROADCASTTIME seconds.
// Why? Too much brokenness i all mDNS implementations available
//
void MDNS::run() {
static long int lastrun = BROADCASTTIME * 1000UL;
unsigned long int now = millis();
if (!(now - lastrun > BROADCASTTIME * 1000UL)) {
return;
}
lastrun = now;
DNSHeader_t dnsHeader = {0, 0, 0, 0, 0, 0};
// DNSHeader_t dnsHeader = { 0 };
_udp->beginPacket(mdnsMulticastIPAddr, MDNS_SERVER_PORT);
// dns header
dnsHeader.flags = HTONS((uint16_t)0x8400); // Response, authorative
dnsHeader.answerCount = HTONS(4 /*5 if TXT but we do not do that */);
_udp->write((uint8_t*)&dnsHeader, sizeof(DNSHeader_t));
// rr #1, the PTR record from generic _services.x.local to service.x.local
_udp->write((uint8_t*)dns_rr_services, sizeof(dns_rr_services));
byte buf[10];
buf[0] = 0x00;
buf[1] = 0x0c; //PTR
buf[2] = 0x00;
buf[3] = 0x01; //IN
*((uint32_t*)(buf+4)) = HTONL(120); //TTL in sec
*((uint16_t*)(buf+8)) = HTONS( _serviceProto[0] + 1 + strlen(dns_rr_tcplocal) + 1);
_udp->write(buf, 10);
_udp->write(_serviceProto,_serviceProto[0]+1);
_udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);
// rr #2, the PTR record from proto.x to name.proto.x
_udp->write(_serviceProto,_serviceProto[0]+1);
_udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);
*((uint16_t*)(buf+8)) = HTONS(strlen(_serviceName) + strlen(dns_rr_tcplocal) + 1); // recycle most of buf
_udp->write(buf, 10);
_udp->write(_serviceName, strlen(_serviceName));
_udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);
// rr #3, the SRV record for the service that points to local name
_udp->write(_serviceName, strlen(_serviceName));
_udp->write(dns_rr_tcplocal, strlen(dns_rr_tcplocal)+1);
buf[1] = 0x21; // recycle most of buf but here SRV
buf[2] = 0x80; // cache flush
*((uint16_t*)(buf+8)) = HTONS(strlen(_name) + strlen(dns_rr_local) + 1 + 6);
_udp->write(buf, 10);
byte srv[6];
// priority and weight
srv[0] = srv[1] = srv[2] = srv[3] = 0;
// port
*((uint16_t*)(srv+4)) = HTONS(_servicePort);
_udp->write(srv, 6);
// target
_udp->write(_name, _name[0]+1);
_udp->write(dns_rr_local, strlen(dns_rr_local)+1);
// rr #4, the A record for the name.local
_udp->write(_name, _name[0]+1);
_udp->write(dns_rr_local, strlen(dns_rr_local)+1);
buf[1] = 0x01; // recycle most of buf but here A
*((uint16_t*)(buf+8)) = HTONS(4);
_udp->write(buf, 10);
byte ip[4];
ip[0] = _ipAddress[0];
ip[1] = _ipAddress[1];
ip[2] = _ipAddress[2];
ip[3] = _ipAddress[3];
_udp->write(ip, 4);
_udp->endPacket();
_udp->flush();
//
}
#endif //DO_MDNS

50
EXmDNS.h Normal file
View File

@@ -0,0 +1,50 @@
/*
* © 2024 Harald Barth
* © 2024 Paul M. Antoine
* 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/>.
*/
#ifdef DO_MDNS
#define BROADCASTTIME 15 //seconds
// We do this ourselves because every library is different and/or broken...
#define HTONS(x) ((uint16_t)(((x) << 8) | (((x) >> 8) & 0xFF)))
#define HTONL(x) ( ((uint32_t)(x) << 24) | (((uint32_t)(x) << 8) & 0xFF0000) | \
(((uint32_t)(x) >> 8) & 0xFF00) | ((uint32_t)(x) >> 24) )
typedef enum _MDNSServiceProtocol_t
{
MDNSServiceTCP,
MDNSServiceUDP
} MDNSServiceProtocol_t;
class MDNS {
public:
MDNS(EthernetUDP& udp);
~MDNS();
int begin(const IPAddress& ip, char* name);
int addServiceRecord(const char* name, uint16_t port, MDNSServiceProtocol_t proto);
void run();
private:
EthernetUDP *_udp;
IPAddress _ipAddress;
char* _name;
char* _serviceName;
char* _serviceProto;
int _servicePort;
};
#endif //DO_MDNS

View File

@@ -1,8 +1,10 @@
/*
* © 2024 Morten "Doc" Nielsen
* © 2023-2024 Paul M. Antoine
* © 2022 Bruno Sanches
* © 2021 Fred Decker
* © 2020-2022 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020-2024 Chris Harlow
* © 2020 Gregor Baues
* All rights reserved.
*
@@ -30,75 +32,139 @@
#include "WiThrottle.h"
#include "DCCTimer.h"
EthernetInterface * EthernetInterface::singleton=NULL;
#ifdef DO_MDNS
#include "EXmDNS.h"
EthernetUDP udp;
MDNS mdns(udp);
#endif
//extern void looptimer(unsigned long timeout, const FSH* message);
#define looptimer(a,b)
bool EthernetInterface::connected=false;
EthernetServer * EthernetInterface::server= nullptr;
EthernetClient EthernetInterface::clients[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield
bool EthernetInterface::inUse[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield
uint8_t EthernetInterface::buffer[MAX_ETH_BUFFER+1]; // buffer used by TCP for the recv
RingStream * EthernetInterface::outboundRing = nullptr;
/**
* @brief Setup Ethernet Connection
*
*/
void EthernetInterface::setup()
void EthernetInterface::setup()
{
if (singleton!=NULL) {
DIAG(F("Prog Error!"));
return;
}
if ((singleton=new EthernetInterface()))
return;
DIAG(F("Ethernet not initialized"));
};
DIAG(F("Ethernet starting"
#ifdef DO_MDNS
" (with mDNS)"
#endif
" Please be patient, especially if no cable is connected!"
));
#ifdef STM32_ETHERNET
// Set a HOSTNAME for the DHCP request - a nice to have, but hard it seems on LWIP for STM32
// The default is "lwip", which is **always** set in STM32Ethernet/src/utility/ethernetif.cpp
// for some reason. One can edit it to instead read:
// #if LWIP_NETIF_HOSTNAME
// /* Initialize interface hostname */
// if (netif->hostname == NULL)
// netif->hostname = "lwip";
// #endif /* LWIP_NETIF_HOSTNAME */
// Which seems more useful! We should propose the patch... so the following line actually works!
netif_set_hostname(&gnetif, WIFI_HOSTNAME); // Should probably be passed in the contructor...
#endif
#ifdef IP_ADDRESS
static IPAddress myIP(IP_ADDRESS);
#endif
/**
* @brief Aquire IP Address from DHCP and start server
*
* @return true
* @return false
*/
EthernetInterface::EthernetInterface()
{
byte mac[6];
DCCTimer::getSimulatedMacAddress(mac);
connected=false;
#ifdef IP_ADDRESS
Ethernet.begin(mac, myIP);
#else
if (Ethernet.begin(mac) == 0)
{
DIAG(F("Ethernet.begin FAILED"));
return;
}
#endif
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
DIAG(F("Ethernet shield not found or W5100"));
}
unsigned long startmilli = millis();
while ((millis() - startmilli) < 5500) { // Loop to give time to check for cable connection
if (Ethernet.linkStatus() == LinkON)
break;
DIAG(F("Ethernet waiting for link (1sec) "));
delay(1000);
}
// now we either do have link of we have a W5100
// where we do not know if we have link. That's
// the reason to now run checkLink.
// CheckLinks sets up outboundRing if it does
// not exist yet as well.
checkLink();
#ifdef IP_ADDRESS
static IPAddress myIP(IP_ADDRESS);
Ethernet.begin(mac,myIP);
#else
if (Ethernet.begin(mac)==0)
{
LCD(4,F("IP: No DHCP"));
return;
}
#endif
auto ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static)
if (!ip) {
LCD(4,F("IP: None"));
return;
}
server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT
server->begin();
// Arrange display of IP address and port
#ifdef LCD_DRIVER
const byte lcdData[]={LCD_DRIVER};
const bool wideDisplay=lcdData[1]>=24; // data[1] is cols.
#else
const bool wideDisplay=true;
#endif
if (wideDisplay) {
// OLEDS or just usb diag is ok on one line.
LCD(4,F("IP %d.%d.%d.%d:%d"), ip[0], ip[1], ip[2], ip[3], IP_PORT);
}
else { // LCDs generally too narrow, so take 2 lines
LCD(4,F("IP %d.%d.%d.%d"), ip[0], ip[1], ip[2], ip[3]);
LCD(5,F("Port %d"), IP_PORT);
}
outboundRing=new RingStream(OUTBOUND_RING_SIZE);
#ifdef DO_MDNS
if (!mdns.begin(Ethernet.localIP(), (char *)WIFI_HOSTNAME))
DIAG(F("mdns.begin fail")); // hostname
mdns.addServiceRecord(WIFI_HOSTNAME "._withrottle", IP_PORT, MDNSServiceTCP);
mdns.run(); // run it right away to get out info ASAP
#endif
connected=true;
}
/**
* @brief Cleanup any resources
*
* @return none
*/
EthernetInterface::~EthernetInterface() {
delete server;
delete outboundRing;
#if defined (STM32_ETHERNET)
void EthernetInterface::acceptClient() { // STM32 version
auto client=server->available();
if (!client) return;
// check for existing client
for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)
if (inUse[socket] && client == clients[socket]) return;
// new client
for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)
{
if (!inUse[socket])
{
clients[socket] = client;
inUse[socket]=true;
if (Diag::ETHERNET)
DIAG(F("Ethernet: New client socket %d"), socket);
return;
}
}
// reached here only if more than MAX_SOCK_NUM clients want to connect
DIAG(F("Ethernet more than %d clients, not accepting new connection"), MAX_SOCK_NUM);
client.stop();
}
#else
void EthernetInterface::acceptClient() { // non-STM32 version
auto client=server->accept();
if (!client) return;
auto socket=client.getSocketNumber();
clients[socket]=client;
inUse[socket]=true;
if (Diag::ETHERNET)
DIAG(F("Ethernet: New client socket %d"), socket);
}
#endif
void EthernetInterface::dropClient(byte socket)
{
clients[socket].stop();
inUse[socket]=false;
CommandDistributor::forget(socket);
if (Diag::ETHERNET) DIAG(F("Ethernet: Disconnect %d "), socket);
}
/**
@@ -107,134 +173,109 @@ EthernetInterface::~EthernetInterface() {
*/
void EthernetInterface::loop()
{
if (!singleton || (!singleton->checkLink()))
return;
if (!connected) return;
looptimer(5000, F("E.loop"));
static bool warnedAboutLink=false;
if (Ethernet.linkStatus() == LinkOFF){
if (warnedAboutLink) return;
DIAG(F("Ethernet link OFF"));
warnedAboutLink=true;
return;
}
looptimer(5000, F("E.loop warn"));
// link status must be ok here
if (warnedAboutLink) {
DIAG(F("Ethernet link RESTORED"));
warnedAboutLink=false;
}
#ifdef DO_MDNS
// Always do this because we don't want traffic to intefere with being found!
mdns.run();
looptimer(5000, F("E.mdns"));
#endif
//
switch (Ethernet.maintain()) {
case 1:
//renewed fail
DIAG(F("Ethernet Error: renewed fail"));
singleton=NULL;
connected=false;
return;
case 3:
//rebind fail
DIAG(F("Ethernet Error: rebind fail"));
singleton=NULL;
connected=false;
return;
default:
//nothing happened
//DIAG(F("maintained"));
break;
}
singleton->loop2();
}
/**
* @brief Checks ethernet link cable status and detects when it connects / disconnects
*
* @return true when cable is connected, false otherwise
*/
bool EthernetInterface::checkLink() {
if (Ethernet.linkStatus() != LinkOFF) { // check for not linkOFF instead of linkON as the W5100 does return LinkUnknown
//if we are not connected yet, setup a new server
if(!connected) {
DIAG(F("Ethernet cable connected"));
connected=true;
#ifdef IP_ADDRESS
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
server->begin();
LCD(4,F("IP: %d.%d.%d.%d"), ip[0], ip[1], ip[2], ip[3]);
LCD(5,F("Port:%d"), IP_PORT);
// only create a outboundRing it none exists, this may happen if the cable
// gets disconnected and connected again
if(!outboundRing)
outboundRing=new RingStream(OUTBOUND_RING_SIZE);
}
return true;
} else { // connected
DIAG(F("Ethernet cable disconnected"));
connected=false;
//clean up any client
for (byte socket = 0; socket < MAX_SOCK_NUM; socket++) {
if(clients[socket].connected())
clients[socket].stop();
}
// tear down server
delete server;
server = nullptr;
LCD(4,F("IP: None"));
}
return false;
}
void EthernetInterface::loop2() {
if (!outboundRing) { // no idea to call loop2() if we can't handle outgoing data in it
if (Diag::ETHERNET) DIAG(F("No outboundRing"));
return;
}
looptimer(5000, F("E.maintain"));
// get client from the server
EthernetClient client = server->accept();
// check for new client
if (client)
acceptClient();
// handle disconnected sockets because STM32 library doesnt
// do the read==0 response.
for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)
{
if (Diag::ETHERNET) DIAG(F("Ethernet: New client "));
byte socket;
for (socket = 0; socket < MAX_SOCK_NUM; socket++)
{
if (!clients[socket])
{
// On accept() the EthernetServer doesn't track the client anymore
// so we store it in our client array
if (Diag::ETHERNET) DIAG(F("Socket %d"),socket);
clients[socket] = client;
break;
}
}
if (socket==MAX_SOCK_NUM) DIAG(F("new Ethernet OVERFLOW"));
}
if (inUse[socket] && !clients[socket].connected()) dropClient(socket);
}
// check for incoming data from all possible clients
for (byte socket = 0; socket < MAX_SOCK_NUM; socket++)
{
if (clients[socket]) {
int available=clients[socket].available();
if (available > 0) {
if (Diag::ETHERNET) DIAG(F("Ethernet: available socket=%d,avail=%d"), socket, available);
// read bytes from a client
int count = clients[socket].read(buffer, MAX_ETH_BUFFER);
buffer[count] = '\0'; // terminate the string properly
if (Diag::ETHERNET) DIAG(F(",count=%d:%e"), socket,buffer);
// execute with data going directly back
CommandDistributor::parse(socket,buffer,outboundRing);
return; // limit the amount of processing that takes place within 1 loop() cycle.
}
}
if (!inUse[socket]) continue; // socket is not in use
// read any bytes from this client
auto count = clients[socket].read(buffer, MAX_ETH_BUFFER);
if (count<0) continue; // -1 indicates nothing to read
if (count > 0) { // we have incoming data
buffer[count] = '\0'; // terminate the string properly
if (Diag::ETHERNET) DIAG(F("Ethernet s=%d, c=%d b=:%e"), socket, count, buffer);
// execute with data going directly back
CommandDistributor::parse(socket,buffer,outboundRing);
//looptimer(5000, F("Ethloop2 parse"));
return; // limit the amount of processing that takes place within 1 loop() cycle.
}
// count=0 The client has disconnected
dropClient(socket);
}
// stop any clients which disconnect
for (int socket = 0; socket<MAX_SOCK_NUM; socket++) {
if (clients[socket] && !clients[socket].connected()) {
clients[socket].stop();
CommandDistributor::forget(socket);
if (Diag::ETHERNET) DIAG(F("Ethernet: disconnect %d "), socket);
}
}
WiThrottle::loop(outboundRing);
// handle at most 1 outbound transmission
int socketOut=outboundRing->read();
auto socketOut=outboundRing->read();
if (socketOut<0) return; // no outbound pending
if (socketOut >= MAX_SOCK_NUM) {
DIAG(F("Ethernet outboundRing socket=%d error"), socketOut);
} else if (socketOut >= 0) {
int count=outboundRing->count();
if (Diag::ETHERNET) DIAG(F("Ethernet reply socket=%d, count=:%d"), socketOut,count);
for(;count>0;count--) clients[socketOut].write(outboundRing->read());
clients[socketOut].flush(); //maybe
// This is a catastrophic code failure and unrecoverable.
DIAG(F("Ethernet outboundRing s=%d error"), socketOut);
connected=false;
return;
}
auto count=outboundRing->count();
{
char tmpbuf[count+1]; // one extra for '\0'
for(int i=0;i<count;i++) {
tmpbuf[i] = outboundRing->read();
}
tmpbuf[count]=0;
if (inUse[socketOut]) {
if (Diag::ETHERNET) DIAG(F("Ethernet reply s=%d, c=%d, b:%e"),
socketOut,count,tmpbuf);
clients[socketOut].write(tmpbuf,count);
}
}
}
#endif

View File

@@ -1,8 +1,10 @@
/*
* © 2023-2024 Paul M. Antoine
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Chris Harlow
* © 2020-2024 Harald Barth
* © 2020-2024 Chris Harlow
* © 2020 Gregor Baues
* All rights reserved.
*
@@ -29,15 +31,32 @@
#define EthernetInterface_h
#include "defines.h"
#if ETHERNET_ON == true
#include "DCCEXParser.h"
#include <Arduino.h>
//#include <avr/pgmspace.h>
#if defined (ARDUINO_TEENSY41)
#include <NativeEthernet.h> //TEENSY Ethernet Treiber
#include <NativeEthernetUdp.h>
#ifndef MAX_SOCK_NUM
#define MAX_SOCK_NUM 4
#endif
// can't use our MDNS because of a namespace clash with Teensy's NativeEthernet library!
// #define DO_MDNS
#elif defined (ARDUINO_NUCLEO_F429ZI) || defined (ARDUINO_NUCLEO_F439ZI) || defined (ARDUINO_NUCLEO_F4X9ZI)
#include <LwIP.h>
#include <STM32Ethernet.h>
#include <lwip/netif.h>
extern "C" struct netif gnetif;
#define STM32_ETHERNET
#define MAX_SOCK_NUM MAX_NUM_TCP_CLIENTS
#define DO_MDNS
#else
#include "Ethernet.h"
#define DO_MDNS
#endif
#include "RingStream.h"
/**
@@ -45,7 +64,7 @@
*
*/
#define MAX_ETH_BUFFER 512
#define MAX_ETH_BUFFER 128
#define OUTBOUND_RING_SIZE 2048
class EthernetInterface {
@@ -56,16 +75,15 @@ class EthernetInterface {
static void loop();
private:
static EthernetInterface * singleton;
bool connected;
EthernetInterface();
~EthernetInterface();
void loop2();
bool checkLink();
EthernetServer * server = NULL;
EthernetClient clients[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield
uint8_t buffer[MAX_ETH_BUFFER+1]; // buffer used by TCP for the recv
RingStream * outboundRing = NULL;
static bool connected;
static EthernetServer * server;
static EthernetClient clients[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield
static bool inUse[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield
static uint8_t buffer[MAX_ETH_BUFFER+1]; // buffer used by TCP for the recv
static RingStream * outboundRing;
static void acceptClient();
static void dropClient(byte socketnum);
};
#endif // ETHERNET_ON
#endif

8
FSH.h
View File

@@ -52,6 +52,7 @@ typedef __FlashStringHelper FSH;
#define STRNCPY_P strncpy_P
#define STRNCMP_P strncmp_P
#define STRLEN_P strlen_P
#define STRCHR_P strchr_P
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
// AVR_MEGA memory deliberately placed at end of link may need _far functions
@@ -60,6 +61,8 @@ typedef __FlashStringHelper FSH;
#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)
#define COPYHIGHFLASH(target,base,offset,length) \
memcpy_PF(target,GETFARPTR(base) + offset,length)
#else
// AVR_UNO/NANO runtime does not support _far functions so just use _near equivalent
// as there is no progmem above 32kb anyway.
@@ -68,6 +71,8 @@ typedef __FlashStringHelper FSH;
#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))
#define COPYHIGHFLASH(target,base,offset,length) \
memcpy_P(target,(byte *)base + offset,length)
#endif
#else
@@ -87,10 +92,13 @@ typedef char FSH;
#define GETFLASH(addr) (*(const byte *)(addr))
#define GETHIGHFLASH(data,offset) (*(const byte *)(GETFARPTR(data)+offset))
#define GETHIGHFLASHW(data,offset) (*(const uint16_t *)(GETFARPTR(data)+offset))
#define COPYHIGHFLASH(target,base,offset,length) \
memcpy(target,(byte *)&base + offset,length)
#define STRCPY_P strcpy
#define STRCMP_P strcmp
#define STRNCPY_P strncpy
#define STRNCMP_P strncmp
#define STRLEN_P strlen
#define STRCHR_P strchr
#endif
#endif

View File

@@ -1 +1 @@
#define GITHUB_SHA "devel-202404061747Z"
#define GITHUB_SHA "devel-202503250850Z"

View File

@@ -46,27 +46,39 @@
// Helper function for listing device types
static const FSH * guessI2CDeviceType(uint8_t address) {
if (address >= 0x10 && address <= 0x17)
return F("EX-SensorCAM");
if (address == 0x1A)
// 0x09-0x18 selectable, but for now handle the default
return F("Piicodev 865/915MHz Transceiver");
if (address == 0x1C)
return F("QMC6310 Magnetometer");
if (address >= 0x20 && address <= 0x26)
return F("GPIO Expander");
else if (address == 0x27)
if (address == 0x27)
return F("GPIO Expander or LCD Display");
else if (address == 0x29)
if (address == 0x29)
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
if (address == 0x34)
return F("TCA8418 keypad scanner");
if (address >= 0x3c && address <= 0x3d)
// 0x3c can also be an HMC883L magnetometer
return F("OLED Display or HMC583L Magnetometer");
if (address >= 0x48 && address <= 0x57) // SC16IS752x UART detection
return F("SC16IS75x UART");
else if (address >= 0x48 && address <= 0x4f)
if (address >= 0x48 && address <= 0x4f)
return F("Analogue Inputs or PWM");
else if (address >= 0x40 && address <= 0x4f)
if (address >= 0x40 && address <= 0x4f)
return F("PWM");
else if (address >= 0x50 && address <= 0x5f)
if (address >= 0x50 && address <= 0x5f)
return F("EEPROM");
else if (address == 0x68)
if (address >= 0x60 && address <= 0x68)
return F("Adafruit NeoPixel Driver");
if (address == 0x68)
return F("Real-time clock");
else if (address >= 0x70 && address <= 0x77)
if (address >= 0x70 && address <= 0x77)
return F("I2C Mux");
else
// Unknown type
return F("?");
}

View File

@@ -384,4 +384,4 @@ void I2CManagerClass::handleInterrupt() {
}
}
#endif
#endif

View File

@@ -1,5 +1,5 @@
/*
* © 2022-23 Paul M Antoine
* © 2022-24 Paul M Antoine
* © 2023, Neil McKechnie
* All rights reserved.
*
@@ -38,8 +38,9 @@
*****************************************************************************/
#if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_ARCH_STM32)
#if defined(ARDUINO_NUCLEO_F401RE) || defined(ARDUINO_NUCLEO_F411RE) || defined(ARDUINO_NUCLEO_F446RE) \
|| defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) \
|| defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)
|| defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F446ZE) \
|| defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)
// Assume I2C1 for now - default I2C bus on Nucleo-F411RE and likely all Nucleo-64
// and Nucleo-144 variants
I2C_TypeDef *s = I2C1;
@@ -184,7 +185,7 @@ void I2CManagerClass::I2C_init()
GPIOB->OTYPER |= (1<<8) | (1<<9); // PB8 and PB9 set to open drain output capability
GPIOB->OSPEEDR |= (3<<(8*2)) | (3<<(9*2)); // PB8 and PB9 set to High Speed mode
GPIOB->PUPDR &= ~((3<<(8*2)) | (3<<(9*2))); // Clear all PUPDR bits for PB8 and PB9
GPIOB->PUPDR |= (1<<(8*2)) | (1<<(9*2)); // PB8 and PB9 set to pull-up capability
// GPIOB->PUPDR |= (1<<(8*2)) | (1<<(9*2)); // PB8 and PB9 set to pull-up capability
// Alt Function High register routing pins PB8 and PB9 for I2C1:
// Bits (3:2:1:0) = 0:1:0:0 --> AF4 for pin PB8
// Bits (7:6:5:4) = 0:1:0:0 --> AF4 for pin PB9

View File

@@ -231,4 +231,4 @@ void I2CManagerClass::queueRequest(I2CRB *req) {
***************************************************************************/
void I2CManagerClass::loop() {}
#endif
#endif

View File

@@ -251,6 +251,26 @@ void IODevice::write(VPIN vpin, int value) {
#endif
}
// Write value to count virtual pin(s).
// these may be within one driver or separated over several drivers
void IODevice::writeRange(VPIN vpin, int value, int count) {
while(count) {
auto dev = findDevice(vpin);
if (dev) {
auto vpinBefore=vpin;
// write to driver, driver will return next vpin it cant handle
vpin=dev->_writeRange(vpin, value,count);
count-= vpin-vpinBefore; // decrement by number of vpins changed
}
else {
// skip a vpin if no device handler
vpin++;
count--;
}
}
}
// Write analogue value to virtual pin(s). If multiple devices are allocated
// the same pin then only the first one found will be used.
//
@@ -270,6 +290,24 @@ void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t para
#endif
}
//
void IODevice::writeAnalogueRange(VPIN vpin, int value, uint8_t param1, uint16_t param2,int count) {
while(count) {
auto dev = findDevice(vpin);
if (dev) {
auto vpinBefore=vpin;
// write to driver, driver will return next vpin it cant handle
vpin=dev->_writeAnalogueRange(vpin, value, param1, param2,count);
count-= vpin-vpinBefore; // decrement by number of vpins changed
}
else {
// skip a vpin if no device handler
vpin++;
count--;
}
}
}
// isBusy, when called for a device pin is always a digital output or analogue output,
// returns input feedback state of the pin, i.e. whether the pin is busy performing
// an animation or fade over a period of time.
@@ -589,4 +627,3 @@ bool ArduinoPins::fastReadDigital(uint8_t pin) {
#endif
return result;
}

View File

@@ -38,6 +38,7 @@
#include "FSH.h"
#include "I2CManager.h"
#include "inttypes.h"
#include "TemplateForEnums.h"
typedef uint16_t VPIN;
// Limit VPIN number to max 32767. Above this number, printing often gives negative values.
@@ -128,9 +129,11 @@ public:
// write invokes the IODevice instance's _write method.
static void write(VPIN vpin, int value);
static void writeRange(VPIN vpin, int value,int count);
// write invokes the IODevice instance's _writeAnalogue method (not applicable for digital outputs)
static void writeAnalogue(VPIN vpin, int value, uint8_t profile=0, uint16_t duration=0);
static void writeAnalogueRange(VPIN vpin, int value, uint8_t profile, uint16_t duration, int count);
// isBusy returns true if the device is currently in an animation of some sort, e.g. is changing
// the output over a period of time.
@@ -177,11 +180,29 @@ public:
virtual void _write(VPIN vpin, int value) {
(void)vpin; (void)value;
};
// Method to write new state (optionally implemented within device class)
// This will, by default just write to one vpin and return whet to do next.
// the real power comes where a single driver can update many vpins in one call.
virtual VPIN _writeRange(VPIN vpin, int value, int count) {
(void)count;
_write(vpin,value);
return vpin+1; // try next vpin
};
// Method to write an 'analogue' value (optionally implemented within device class)
virtual void _writeAnalogue(VPIN vpin, int value, uint8_t param1=0, uint16_t param2=0) {
(void)vpin; (void)value; (void) param1; (void)param2;
};
// Method to write an 'analogue' value to a VPIN range (optionally implemented within device class)
// This will, by default just write to one vpin and return whet to do next.
// the real power comes where a single driver can update many vpins in one call.
virtual VPIN _writeAnalogueRange(VPIN vpin, int value, uint8_t param1, uint16_t param2, int count) {
(void) count;
_writeAnalogue(vpin, value, param1, param2);
return vpin+1;
};
// Method to read digital pin state (optionally implemented within device class)
virtual int _read(VPIN vpin) {
@@ -539,14 +560,6 @@ protected:
};
#include "IO_MCP23008.h"
#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"
//#include "IODeviceList.h"
#endif // iodevice_h

50
IODeviceList.h Normal file
View File

@@ -0,0 +1,50 @@
/*
* © 2024, Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
This is the list of HAL drivers automatically included by IODevice.h
It has been moved here to be easier to maintain than editing IODevice.h
*/
#include "IO_AnalogueInputs.h"
#include "IO_DFPlayer.h"
#include "IO_DS1307.h"
#include "IO_duinoNodes.h"
#include "IO_EncoderThrottle.h"
#include "IO_EXFastclock.h"
#include "IO_EXIOExpander.h"
#include "IO_EXSensorCAM.h"
#include "IO_HALDisplay.h"
#include "IO_HCSR04.h"
#include "IO_I2CDFPlayer.h"
#include "IO_I2CRailcom.h"
#include "IO_MCP23008.h"
#include "IO_MCP23017.h"
#include "IO_NeoPixel.h"
#include "IO_PCA9555.h"
#include "IO_PCA9685pwm.h"
#include "IO_PCF8574.h"
#include "IO_PCF8575.h"
#include "IO_RotaryEncoder.h"
#include "IO_Servo.h"
#include "IO_TCA8418.h"
#include "IO_TM1638.h"
#include "IO_TouchKeypad.h"
#include "IO_trainbrains.h"
#include "IO_Bitmap.h"
#include "IO_VL53L0X.h"

View File

@@ -166,4 +166,4 @@ private:
uint8_t _nextState;
};
#endif // io_analogueinputs_h
#endif // io_analogueinputs_h

89
IO_Bitmap.h Normal file
View File

@@ -0,0 +1,89 @@
/*
* © 2025, Chris Harlow. 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_Bitmap_h
#define IO_Bitmap_h
#include <Arduino.h>
#include "defines.h"
#include "IODevice.h"
/*
Bitmap provides a set of virtual pins with no hardware.
Bitmap pins are able to be output and input and may be set and tested
as digital or analogue values.
When writing a digital value, the analogue value is set to 0 or 1.
When reading a digital value, the return is LOW for value 0 or HIGH for any other value
or analogue.
Bitmap pins may be used for any purpose, this is easier to manage than LATCH in EXRAIL
as they can be explicitely set and tested without interfering with underlying hardware.
Bitmap pins may be set, reset and tested in the same way as any other pin.
They are not persistent across reboots, but are retained in the current session.
Bitmap pins may also be monitored by JMRI_SENSOR() and <S> as for any other pin.
*/
class Bitmap : public IODevice {
public:
static void create(VPIN firstVpin, int nPins) {
if (IODevice::checkNoOverlap(firstVpin,nPins))
new Bitmap( firstVpin, nPins);
}
Bitmap(VPIN firstVpin, int nPins) : IODevice(firstVpin, nPins) {
_pinValues=(int16_t *) calloc(nPins,sizeof(int16_t));
// Connect to HAL so my _write, _read and _loop will be called as required.
IODevice::addDevice(this);
}
// Called by HAL to start handling this device
void _begin() override {
_deviceState = DEVSTATE_NORMAL;
_display();
}
int _read(VPIN vpin) override {
int pin=vpin - _firstVpin;
return _pinValues[pin]?1:0;
}
void _write(VPIN vpin, int value) override {
int pin = vpin - _firstVpin;
_pinValues[pin]=value!=0; // this is digital write
}
int _readAnalogue(VPIN vpin) override {
int pin=vpin - _firstVpin;
return _pinValues[pin]; // this is analog read
}
void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override {
int pin=vpin - _firstVpin;
_pinValues[pin]=value; // this is analog write
}
void _display() override {
DIAG(F("Bitmap Configured on Vpins:%u-%u"),
(int)_firstVpin,
(int)_firstVpin+_nPins-1);
}
private:
int16_t* _pinValues;
};
#endif

View File

@@ -65,4 +65,3 @@ void DCCAccessoryDecoder::_display() {
DIAG(F("DCCAccessoryDecoder Configured on Vpins:%u-%u Addresses %d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1,
ADDRESS(_packedAddress), SUBADDRESS(_packedAddress), ADDRESS(endAddress), SUBADDRESS(endAddress));
}

143
IO_DS1307.cpp Normal file
View File

@@ -0,0 +1,143 @@
/*
* © 2024, 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/>.
*/
/*
* The IO_DS1307 device driver is used to interface a standalone realtime clock.
* The clock will announce every minute (which will trigger EXRAIL ONTIME events).
* Seconds, and Day/date info is ignored, except that the announced hhmm time
* will attempt to synchronize with the 0 seconds of the clock.
* An analog read in EXRAIL (IFGTE(vpin, value) etc will check against the hh*60+mm time.
* The clock can be easily set by an analog write to the vpin using 24 hr clock time
* with the command <z vpin hh mm ss>
*/
#include "IO_DS1307.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "CommandDistributor.h"
uint8_t d2b(uint8_t d) {
return (d >> 4)*10 + (d & 0x0F);
}
void DS1307::create(VPIN vpin, I2CAddress i2cAddress) {
if (checkNoOverlap(vpin, 1, i2cAddress)) new DS1307(vpin, i2cAddress);
}
// Constructor
DS1307::DS1307(VPIN vpin,I2CAddress i2cAddress){
_firstVpin = vpin;
_nPins = 1;
_I2CAddress = i2cAddress;
addDevice(this);
}
uint32_t DS1307::getTime() {
// Obtain ss,mm,hh buffers from device
uint8_t readBuffer[3];
const uint8_t writeBuffer[1]={0};
// address register 0 for read.
I2CManager.write(_I2CAddress, writeBuffer, 1);
if (I2CManager.read(_I2CAddress, readBuffer, 3) != I2C_STATUS_OK) {
_deviceState=DEVSTATE_FAILED;
return 0;
}
_deviceState=DEVSTATE_NORMAL;
if (debug) {
static const char hexchars[]="0123456789ABCDEF";
USB_SERIAL.print(F("<*RTC"));
for (int i=2;i>=0;i--) {
USB_SERIAL.write(' ');
USB_SERIAL.write(hexchars[readBuffer[i]>>4]);
USB_SERIAL.write(hexchars[readBuffer[i]& 0x0F ]);
}
StringFormatter::send(&USB_SERIAL,F(" %d *>\n"),_deviceState);
}
if (readBuffer[0] & 0x80) {
_deviceState=DEVSTATE_INITIALISING;
DIAG(F("DS1307 clock in standby"));
return 0; // clock is not running
}
// convert device format to seconds since midnight
uint8_t ss=d2b(readBuffer[0] & 0x7F);
uint8_t mm=d2b(readBuffer[1]);
uint8_t hh=d2b(readBuffer[2] & 0x3F);
return (hh*60ul +mm)*60ul +ss;
}
void DS1307::_begin() {
// Initialise device and sync loop() to zero seconds
I2CManager.begin();
auto tstamp=getTime();
if (_deviceState==DEVSTATE_NORMAL) {
byte seconds=tstamp%60;
delayUntil(micros() + ((60-seconds) * 1000000));
}
_display();
}
// Processing loop to obtain clock time.
// This self-synchronizes to the next minute tickover
void DS1307::_loop(unsigned long currentMicros) {
auto time=getTime();
if (_deviceState==DEVSTATE_NORMAL) {
byte ss=time%60;
CommandDistributor::setClockTime(time/60, 1, 1);
delayUntil(currentMicros + ((60-ss) * 1000000));
}
}
// Display device driver info.
void DS1307::_display() {
auto tstamp=getTime();
byte ss=tstamp%60;
tstamp/=60;
byte mm=tstamp%60;
byte hh=tstamp/60;
DIAG(F("DS1307 on I2C:%s vpin %d %d:%d:%d %S"),
_I2CAddress.toString(), _firstVpin,
hh,mm,ss,
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
// allow user to set the clock
void DS1307::_writeAnalogue(VPIN vpin, int hh, uint8_t mm, uint16_t ss) {
(void) vpin;
uint8_t writeBuffer[3];
writeBuffer[0]=1; // write mm,hh first
writeBuffer[1]=((mm/10)<<4) + (mm % 10);
writeBuffer[2]=((hh/10)<<4) + (hh % 10);
I2CManager.write(_I2CAddress, writeBuffer, 3);
writeBuffer[0]=0; // write ss
writeBuffer[1]=((ss/10)<<4) + (ss % 10);
I2CManager.write(_I2CAddress, writeBuffer, 2);
_loop(micros()); // resync with seconds rollover
}
// Method to read analogue hh*60+mm time
int DS1307::_readAnalogue(VPIN vpin) {
(void)vpin;
return getTime()/60;
};

54
IO_DS1307.h Normal file
View File

@@ -0,0 +1,54 @@
/*
* © 2024, 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/>.
*/
/*
* The IO_DS1307 device driver is used to interface a standalone realtime clock.
* The clock will announce every minute (which will trigger EXRAIL ONTIME events).
* Seconds, and Day/date info is ignored, except that the announced hhmm time
* will attempt to synchronize with the 0 seconds of the clock.
* An analog read in EXRAIL (IFGTE(vpin, value) etc will check against the hh*60+mm time.
* The clock can be easily set by an analog write to the vpin using 24 hr clock time
* with the command <z vpin hh mm ss>
*/
#ifndef IO_DS1307_h
#define IO_DS1307_h
#include "IODevice.h"
class DS1307 : public IODevice {
public:
static const bool debug=false;
static void create(VPIN vpin, I2CAddress i2cAddress);
private:
// Constructor
DS1307(VPIN vpin,I2CAddress i2cAddress);
uint32_t getTime();
void _begin() override;
void _display() override;
void _loop(unsigned long currentMicros) override;
int _readAnalogue(VPIN vpin) override;
void _writeAnalogue(VPIN vpin, int hh, uint8_t mm, uint16_t ss) override;
};
#endif

View File

@@ -51,6 +51,7 @@ static void create(I2CAddress i2cAddress) {
// Start by assuming we will find the clock
// Check if specified I2C address is responding (blocking operation)
// Returns I2C_STATUS_OK (0) if OK, or error code.
I2CManager.begin();
uint8_t _checkforclock = I2CManager.checkAddress(i2cAddress);
DIAG(F("Clock check result - %d"), _checkforclock);
// XXXX change thistosave2 bytes

425
IO_EXSensorCAM.h Normal file
View File

@@ -0,0 +1,425 @@
/* 2024/08/14
* © 2024, Barry Daniel ESP32-CAM revision
*
* This file is part of EX-CommandStation
*
* 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/>.
*/
#define driverVer 306
// v306 Pass vpin to regeister it in CamParser.
// Move base vpin to camparser.
// No more need for config.h settings.
// v305 less debug & alpha ordered switch
// v304 static oldb0; t(##[,%%];
// v303 zipped with CS 5.2.76 and uploaded to repo (with debug)
// v302 SEND=StringFormatter::send, remove Sp(), add 'q', memcpy( .8) -> .7);
// v301 improved 'f','p'&'q' code and driver version calc. Correct bsNo calc. for 'a'
// v300 stripped & revised without expander functionality. Needs sensorCAM.h v300 AND CamParser.cpp
// v222 uses '@'for EXIORDD read. handles <NB $> and <NN $ ##>
// v216 includes 'j' command and uses CamParser rather than myFilter.h Incompatible with v203 senorCAM
// v203 added pvtThreshold to 'i' output
// v201 deleted code for compatibility with CAM pre v171. Needs CAM ver201 with o06 only
// v200 rewrite reduces need for double reads of ESP32 slave CAM. Deleted ESP32CAP.
// Inompatible with pre-v170 sensorCAM, unless set S06 to 0 and S07 to 1 (o06 & l07 say)
/*
* The IO_EXSensorCAM.h device driver can integrate with the sensorCAM device.
* It is modelled on the IO_EXIOExpander.h device driver to include specific needs of the ESP32 sensorCAM
* This device driver will configure the device on startup, along with CamParser.cpp
* interacting with the sensorCAM device for all input/output duties.
*
* To create EX-SensorCAM devices,
* use HAL(EXSensorCAM, baseVpin, numpins, i2c_address) in myAutomation.h
* e.g.
* HAL(EXSensorCAM,700, 80, 0x11)
*
* or (deprecated) define them in myHal.cpp: with
* EXSensorCAM::create(baseVpin,num_vpins,i2c_address);
*
*/
#ifndef IO_EX_EXSENSORCAM_H
#define IO_EX_EXSENSORCAM_H
#define DIGITALREFRESH 20000UL // min uSec delay between digital reads of digitalInputStates
#define SEND StringFormatter::send
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "FSH.h"
#include "CamParser.h"
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for EX-SensorCAM.
*/
class EXSensorCAM : public IODevice {
public:
static void create(VPIN vpin, int nPins, I2CAddress i2cAddress) {
if (checkNoOverlap(vpin, nPins, i2cAddress))
new EXSensorCAM(vpin, nPins, i2cAddress);
}
private:
// Constructor
EXSensorCAM(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
_firstVpin = firstVpin;
// Number of pins cannot exceed 255 (1 byte) because of I2C message structure.
if (nPins > 80) nPins = 80;
_nPins = nPins;
_I2CAddress = i2cAddress;
addDevice(this);
CamParser::addVpin(firstVpin);
}
//*************************
void _begin() {
uint8_t status;
// Initialise EX-SensorCAM device
I2CManager.setClock(100000); // Set speed for I2C operations
I2CManager.begin();
if (!I2CManager.exists(_I2CAddress)) {
DIAG(F("EX-SensorCAM I2C:%s device not found"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
return;
}else {
uint8_t commandBuffer[4]={EXIOINIT,(uint8_t)_nPins,(uint8_t)(_firstVpin & 0xFF),(uint8_t)(_firstVpin>>8)};
status = I2CManager.read(_I2CAddress,_inputBuf,sizeof(_inputBuf),commandBuffer,sizeof(commandBuffer));
//EXIOINIT needed to trigger and send firstVpin to CAM
if (status == I2C_STATUS_OK) {
// Attempt to get version, non-blocking results in poor placement of response. Can be blocking here!
commandBuffer[0] = '^'; //new version code
status = I2CManager.read(_I2CAddress, _inputBuf, sizeof(_inputBuf), commandBuffer, 1);
// for ESP32 CAM, read again for good immediate response version data
status = I2CManager.read(_I2CAddress, _inputBuf, sizeof(_inputBuf), commandBuffer, 1);
if (status == I2C_STATUS_OK) {
_majorVer= _inputBuf[1]/10;
_minorVer= _inputBuf[1]%10;
_patchVer= _inputBuf[2];
DIAG(F("EX-SensorCAM device found, I2C:%s, Version v%d.%d.%d"),
_I2CAddress.toString(),_majorVer, _minorVer,_patchVer);
}
}
if (status != I2C_STATUS_OK)
reportError(status);
}
}
//*************************
// Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if requested.
// Configuration isn't done frequently so we can use blocking I2C calls here, and so buffers can
// be allocated from the stack to reduce RAM allocation.
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override {
(void)configType; (void)params; // unused
if(_verPrint) DIAG(F("_configure() driver IO_EXSensorCAM v0.%d.%d vpin: %d "), driverVer/100,driverVer%100,vpin);
_verPrint=false; //only give driver versions once
if (paramCount != 1) return false;
return true; //at least confirm that CAM is (always) configured (no vpin check!)
}
//*************************
// Analogue input pin configuration, used to enable an EX-IOExpander device.
int _configureAnalogIn(VPIN vpin) override {
DIAG(F("_configureAnalogIn() IO_EXSensorCAM vpin %d"),vpin);
return true; // NOTE: use of EXRAIL IFGTE() etc use "analog" reads.
}
//*************************
// Main loop, collect both digital and "analog" pin states continuously (faster sensor/input reads)
void _loop(unsigned long currentMicros) override {
if (_deviceState == DEVSTATE_FAILED) return;
// Request block is used for "analogue" (cmd. data) and digital reads from the sensorCAM, which
// are performed on a cyclic basis. Writes are performed synchronously as and when requested.
if (_readState != RDS_IDLE) { //expecting a return packet
if (_i2crb.isBusy()) return; // If I2C operation still in progress, return
uint8_t status = _i2crb.status;
if (status == I2C_STATUS_OK) { // If device request ok, read input data
//apparently the above checks do not guarantee a good packet! error rate about 1 pkt per 1000
//there should be a packet in _CAMresponseBuff[32]
if ((_CAMresponseBuff[0] & 0x60) >= 0x60) { //Buff[0] seems to have ascii cmd header (bit6 high) (o06)
int error = processIncomingPkt( _CAMresponseBuff, _CAMresponseBuff[0]); // '~' 'i' 'm' 'n' 't' etc
if (error>0) DIAG(F("CAM packet header(0x%x) not recognised"),_CAMresponseBuff[0]);
}else{ // Header not valid - typically replaced by bank 0 data! To avoid any bad responses set S06 to 0
// Versions of sensorCAM.h after v300 should return header for '@' of '`'(0x60) (not 0xE6)
// followed by digitalInputStates sensor state array
}
}else reportError(status, false); // report i2c eror but don't go offline.
_readState = RDS_IDLE;
}
// If we're not doing anything now, check to see if a new state table transfer, or for 't' repeat, is due.
if (_readState == RDS_IDLE) { //check if time for digitalRefresh
if ( currentMicros - _lastDigitalRead > _digitalRefresh) {
// Issue new read request for digital states.
_readCommandBuffer[0] = '@'; //start new read of digitalInputStates Table // non-blocking read
I2CManager.read(_I2CAddress,_CAMresponseBuff, 32,_readCommandBuffer, 1, &_i2crb);
_lastDigitalRead = currentMicros;
_readState = RDS_DIGITAL;
}else{ //slip in a repeat <NT n> if pending
if (currentMicros - _lasttStateRead > _tStateRefresh) // Delay for "analog" command repetitions
if (_savedCmd[2]>1) { //repeat a 't' command
for (int i=0;i<7;i++) _readCommandBuffer[i] =_savedCmd[i];
int errors = ioESP32(_I2CAddress, _CAMresponseBuff, 32, _readCommandBuffer, 7);
_lasttStateRead = currentMicros;
_savedCmd[2] -= 1; //decrement repeats
if (errors==0) return;
DIAG(F("ioESP32 error %d header 0x%x"),errors,_CAMresponseBuff[0]);
_readState = RDS_TSTATE; //this should stop further cmd requests until packet read (or timeout)
}
} //end repeat 't'
}
}
//*************************
// Obtain the bank of 8 sensors as an "analog" value
// can be used to track the position through a sequential sensor bank
int _readAnalogue(VPIN vpin) override {
if (_deviceState == DEVSTATE_FAILED) return 0;
return _digitalInputStates[(vpin - _firstVpin) / 8];
}
//*************************
// Obtain the correct digital sensor input value
int _read(VPIN vpin) override {
if (_deviceState == DEVSTATE_FAILED) return 0;
int pin = vpin - _firstVpin;
return bitRead(_digitalInputStates[pin / 8], pin % 8);
}
//*************************
// Write digital value.
void _write(VPIN vpin, int value) override {
DIAG(F("**_write() vpin %d = %d"),vpin,value);
return ;
}
//*************************
// i2cAddr of ESP32 CAM
// rBuf buffer for return packet
// inbytes number of bytes to request from CAM
// outBuff holds outbytes to be sent to CAM
int ioESP32(uint8_t i2cAddr,uint8_t *rBuf,int inbytes,uint8_t *outBuff,int outbytes) {
uint8_t status = _i2crb.status;
while( _i2crb.status != I2C_STATUS_OK){status = _i2crb.status;} //wait until bus free
status = I2CManager.read(i2cAddr, rBuf, inbytes, outBuff, outbytes);
if (status != I2C_STATUS_OK){
DIAG(F("EX-SensorCAM I2C:%s Error:%d %S"), _I2CAddress.toString(), status, I2CManager.getErrorMessage(status));
reportError(status); return status;
}
return 0; // 0 for no error != 0 for error number.
}
//*************************
//function to interpret packet from sensorCAM.ino
//i2cAddr to identify CAM# (if # >1)
//rBuf contains packet of up to 32 bytes usually with (ascii) cmd header in rBuf[0]
//sensorCmd command header byte from CAM (in rBuf[0]?)
int processIncomingPkt(uint8_t *rBuf,uint8_t sensorCmd) {
//static uint8_t oldb0; //for debug only
int k;
int b;
char str[] = "11111111";
// if (sensorCmd <= '~') DIAG(F("processIncomingPkt %c %d %d %d"),rBuf[0],rBuf[1],rBuf[2],rBuf[3]);
switch (sensorCmd){
case '`': //response to request for digitalInputStates[] table '@'=>'`'
memcpy(_digitalInputStates, rBuf+1, digitalBytesNeeded);
// if ( _digitalInputStates[0]!=oldb0) { oldb0=_digitalInputStates[0]; //debug
// for (k=0;k<5;k++) {Serial.print(" ");Serial.print(_digitalInputStates[k],HEX);}
// }
break;
case EXIORDY: //some commands give back acknowledgement only
break;
case CAMERR: //cmd format error code from CAM
DIAG(F("CAM cmd error 0xFE 0x%x"),rBuf[1]);
break;
case '~': //information from '^' version request <N v[er]>
DIAG(F("EX-SensorCAM device found, I2C:%s,CAM Version v%d.%d.%d vpins %u-%u"),
_I2CAddress.toString(), rBuf[1]/10, rBuf[1]%10, rBuf[2],(int) _firstVpin, (int) _firstVpin +_nPins-1);
DIAG(F("IO_EXSensorCAM driver v0.%d.%d vpin: %d "), driverVer/100,driverVer%100,_firstVpin);
break;
case 'f':
DIAG(F("(f %%%%) frame header 'f' for bsNo %d/%d - showing Quarter sample (1 row) only"), rBuf[1]/8,rBuf[1]%8);
SEND(&USB_SERIAL,F("<n row: %d Ref bytes: "),rBuf[2]);
for(k=3;k<15;k++)
SEND(&USB_SERIAL,F("%x%x%s"), rBuf[k]>>4, rBuf[k]&15, k%3==2 ? " " : " ");
Serial.print(" latest grab: ");
for(k=16;k<28;k++)
SEND(&USB_SERIAL,F("%x%x%s"), rBuf[k]>>4, rBuf[k]&15, (k%3==0) ? " " : " ");
Serial.print(" n>\n");
break;
case 'i': //information from i%%
k=256*rBuf[5]+rBuf[4];
DIAG(F("(i%%%%[,$$]) Info: Sensor 0%o(%d) enabled:%d status:%d row=%d x=%d Twin=0%o pvtThreshold=%d A~%d")
,rBuf[1],rBuf[1],rBuf[3],rBuf[2],rBuf[6],k,rBuf[7],rBuf[9],int(rBuf[8])*16);
break;
case 'm':
DIAG(F("(m$[,##]) Min/max: $ frames min2flip (trip) %d, maxSensors 0%o, minSensors 0%o, nLED %d,"
" threshold %d, TWOIMAGE_MAXBS 0%o"),rBuf[1],rBuf[3],rBuf[2],rBuf[4],rBuf[5],rBuf[6]);
break;
case 'n':
DIAG(F("(n$[,##]) Nominate: $ nLED %d, ## minSensors 0%o (maxSensors 0%o threshold %d)")
,rBuf[4],rBuf[2],rBuf[3],rBuf[5]);
break;
case 'p':
b=rBuf[1]-2;
if(b<4) { Serial.print("<n (p%%) Bank empty n>\n"); break; }
SEND(&USB_SERIAL,F("<n (p%%) Bank: %d "),(0x7F&rBuf[2])/8);
for (int j=2; j<b; j+=3)
SEND(&USB_SERIAL,F(" S[%d%d]: r=%d x=%d"),0x7F&rBuf[j]/8,0x7F&rBuf[j]%8,rBuf[j+1],rBuf[j+2]+2*(rBuf[j]&0x80));
Serial.print(" n>\n");
break;
case 'q':
for (int i =0; i<8; i++) str[i] = ((rBuf[2] << i) & 0x80 ? '1' : '0');
DIAG(F("(q $) Query bank %c ENABLED sensors(S%c7-%c0): %s "), rBuf[1], rBuf[1], rBuf[1], str);
break;
case 't': //threshold etc. from t## //bad pkt if 't' FF's
if(rBuf[1]==0xFF) {Serial.println("<n bad CAM 't' packet: 74 FF n>");_savedCmd[2] +=1; return 0;}
SEND(&USB_SERIAL,F("<n (t[##[,%%%%]]) Threshold:%d sensor S00:-%d"),rBuf[1],min(rBuf[2]&0x7F,99));
if(rBuf[2]>127) Serial.print("##* ");
else{
if(rBuf[2]>rBuf[1]) Serial.print("-?* ");
else Serial.print("--* ");
}
for(int i=3;i<31;i+=2){
uint8_t valu=rBuf[i]; //get bsn
if(valu==80) break; //80 = end flag
else{
SEND(&USB_SERIAL,F("%d%d:"), (valu&0x7F)/8,(valu&0x7F)%8);
if(valu>=128) Serial.print("?-");
else {if(rBuf[i+1]>=128) Serial.print("oo");else Serial.print("--");}
valu=rBuf[i+1];
SEND(&USB_SERIAL,F("%d%s"),min(valu&0x7F,99),(valu<128) ? "--* ":"##* ");
}
}
Serial.print(" >\n");
break;
default: //header not a recognised cmd character
DIAG(F("CAM packet header not valid (0x%x) (0x%x) (0x%x)"),rBuf[0],rBuf[1],rBuf[2]);
return 1;
}
return 0;
}
//*************************
// Write (analogue) 8bit (command) values. Write the parameters to the sensorCAM
void _writeAnalogue(VPIN vpin, int param1, uint8_t camop, uint16_t param3) override {
uint8_t outputBuffer[7];
int errors=0;
outputBuffer[0] = camop;
int pin = vpin - _firstVpin;
if(camop >= 0x80) { //case "a" (4p) also (3p) e.g. <N 713 210 310>
camop=param1; //put row (0-236) in expected place
param1=param3; //put column in expected place
outputBuffer[0] = 'A';
pin = (pin/8)*10 + pin%8; //restore bsNo. as integer
}
if (_deviceState == DEVSTATE_FAILED) return;
outputBuffer[1] = pin; //vpin => bsn
outputBuffer[2] = param1 & 0xFF;
outputBuffer[3] = param1 >> 8;
outputBuffer[4] = camop; //command code
outputBuffer[5] = param3 & 0xFF;
outputBuffer[6] = param3 >> 8;
int count=param1+1;
if(camop=='Q'){
if(param3<=10) {count=param3; camop='B';}
//if(param1<10) outputBuffer[2] = param1*10;
}
if(camop=='B'){ //then 'b'(b%) cmd - can totally deal with that here. (but can't do b%,# (brightSF))
if(param1>97) return;
if(param1>9) param1 = param1/10; //accept a bsNo
for(int bnk=param1;bnk<count;bnk++) {
uint8_t b=_digitalInputStates[bnk];
char str[] = "11111111";
for (int i=0;i<8;i++) if(((b<<i)&0x80) == 0) str[i]='0';
DIAG(F("(b $) Bank: %d activated byte: 0x%x%x (sensors S%d7->%d0) %s"), bnk,b>>4,b&15,bnk,bnk,str );
}
return;
}
if (outputBuffer[4]=='T') { //then 't' cmd
if(param1<31) { //repeated calls if param < 31
//for (int i=0;i<7;i++) _savedCmd[i]=outputBuffer[i];
memcpy( _savedCmd, outputBuffer, 7);
}else _savedCmd[2] = 0; //no repeats if ##>30
}else _savedCmd[2] = 0; //no repeats unless 't'
_lasttStateRead = micros(); //don't repeat until _tStateRefresh mSec
errors = ioESP32(_I2CAddress, _CAMresponseBuff, 32 , outputBuffer, 7); //send to esp32-CAM
if (errors==0) return;
else { // if (_CAMresponseBuff[0] != EXIORDY) //can't be sure what is inBuff[0] !
DIAG(F("ioESP32 i2c error %d header 0x%x"),errors,_CAMresponseBuff[0]);
}
}
//*************************
// Display device information and status.
void _display() override {
DIAG(F("EX-SensorCAM I2C:%s v%d.%d.%d Vpins %u-%u %S"),
_I2CAddress.toString(), _majorVer, _minorVer, _patchVer,
(int)_firstVpin, (int)_firstVpin+_nPins-1,
_deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F(""));
}
//*************************
// Helper function for error handling
void reportError(uint8_t status, bool fail=true) {
DIAG(F("EX-SensorCAM I2C:%s Error:%d (%S)"), _I2CAddress.toString(),
status, I2CManager.getErrorMessage(status));
if (fail) _deviceState = DEVSTATE_FAILED;
}
//*************************
uint8_t _numDigitalPins = 80;
size_t digitalBytesNeeded=10;
uint8_t _CAMresponseBuff[34];
uint8_t _majorVer = 0;
uint8_t _minorVer = 0;
uint8_t _patchVer = 0;
uint8_t _digitalInputStates[10];
I2CRB _i2crb;
uint8_t _inputBuf[12];
byte _outputBuffer[8];
bool _verPrint=true;
uint8_t _readCommandBuffer[8];
uint8_t _savedCmd[8]; //for repeat 't' command
//uint8_t _digitalPinBytes = 10; // Size of allocated memory buffer (may be longer than needed)
enum {RDS_IDLE, RDS_DIGITAL, RDS_TSTATE}; // Read operation states
uint8_t _readState = RDS_IDLE;
//uint8_t cmdBuffer[7]={0,0,0,0,0,0,0};
unsigned long _lastDigitalRead = 0;
unsigned long _lasttStateRead = 0;
unsigned long _digitalRefresh = DIGITALREFRESH; // Delay refreshing digital inputs for 10ms
const unsigned long _tStateRefresh = 120000UL; // Delay refreshing repeat "tState" inputs
enum {
EXIOINIT = 0xE0, // Flag to initialise setup procedure
EXIORDY = 0xE1, // Flag we have completed setup procedure, also for EX-IO to ACK setup
CAMERR = 0xFE
};
};
#endif

144
IO_EncoderThrottle.cpp Normal file
View File

@@ -0,0 +1,144 @@
/*
* © 2024, Chris Harlow. All rights reserved.
*
* This file is part of EX-CommandStation
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The IO_EncoderThrottle device driver uses a rotary encoder connected to vpins
* to drive a loco.
* Loco id is selected by writeAnalog.
*/
#include "IODevice.h"
#include "IO_EncoderThrottle.h"
#include "DIAG.h"
#include "DCC.h"
const byte _DIR_CW = 0x10; // Clockwise step
const byte _DIR_CCW = 0x20; // Counter-clockwise step
const byte transition_table[5][4]= {
{0,1,3,0}, // 0: 00
{1,1,1,2 | _DIR_CW}, // 1: 00->01
{2,2,0,2}, // 2: 00->01->11
{3,3,3,4 | _DIR_CCW}, // 3: 00->10
{4,0,4,4} // 4: 00->10->11
};
const byte _STATE_MASK = 0x07;
const byte _DIR_MASK = 0x30;
void EncoderThrottle::create(VPIN firstVpin, int dtPin, int clkPin, int clickPin, byte notch) {
if (checkNoOverlap(firstVpin)) new EncoderThrottle(firstVpin, dtPin,clkPin,clickPin,notch);
}
// Constructor
EncoderThrottle::EncoderThrottle(VPIN firstVpin, int dtPin, int clkPin, int clickPin, byte notch){
_firstVpin = firstVpin;
_nPins = 1;
_I2CAddress = 0;
_dtPin=dtPin;
_clkPin=clkPin;
_clickPin=clickPin;
_notch=notch;
_locoid=0;
_stopState=xrSTOP;
_rocoState=0;
_prevpinstate=4; // not 01..11
IODevice::configureInput(dtPin,true);
IODevice::configureInput(clkPin,true);
IODevice::configureInput(clickPin,true);
addDevice(this);
_display();
}
void EncoderThrottle::_loop(unsigned long currentMicros) {
if (_locoid==0) return; // not in use
// Clicking down on the roco, stops the loco and sets the direction as unknown.
if (IODevice::read(_clickPin)) {
if (_stopState==xrSTOP) return; // debounced multiple stops
DCC::setThrottle(_locoid,1,DCC::getThrottleDirection(_locoid));
_stopState=xrSTOP;
DIAG(F("DRIVE %d STOP"),_locoid);
return;
}
// read roco pins and detect state change
byte pinstate = (IODevice::read(_dtPin) << 1) | IODevice::read(_clkPin);
if (pinstate==_prevpinstate) return;
_prevpinstate=pinstate;
_rocoState = transition_table[_rocoState & _STATE_MASK][pinstate];
if ((_rocoState & _DIR_MASK) == 0) return; // no value change
int change=(_rocoState & _DIR_CW)?+1:-1;
// handle roco change -1 or +1 (clockwise)
if (_stopState==xrSTOP) {
// first move after button press sets the direction. (clockwise=fwd)
_stopState=change>0?xrFWD:xrREV;
}
// when going fwd, clockwise increases speed.
// but when reversing, anticlockwise increases speed.
// This is similar to a center-zero pot control but with
// the added safety that you cant panic-spin into the other
// direction.
if (_stopState==xrREV) change=-change;
// manage limits
int oldspeed=DCC::getThrottleSpeed(_locoid);
if (oldspeed==1)oldspeed=0; // break out of estop
int newspeed=change>0 ? (min((oldspeed+_notch),126)) : (max(0,(oldspeed-_notch)));
if (newspeed==1) newspeed=0; // normal decelereated stop.
if (oldspeed!=newspeed) {
DIAG(F("DRIVE %d notch %S %d %S"),_locoid,
change>0?F("UP"):F("DOWN"),_notch,
_stopState==xrFWD?F("FWD"):F("REV"));
DCC::setThrottle(_locoid,newspeed,_stopState==xrFWD);
}
}
// Selocoid as analog value to start drive
// use <z vpin locoid [notch]>
void EncoderThrottle::_writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) {
(void) param2;
_locoid=value;
if (param1>0) _notch=param1;
_rocoState=0;
// If loco is moving, we inherit direction from it.
_stopState=xrSTOP;
if (_locoid>0) {
auto speedbyte=DCC::getThrottleSpeedByte(_locoid);
if ((speedbyte & 0x7f) >1) {
// loco is moving
_stopState= (speedbyte & 0x80)?xrFWD:xrREV;
}
}
_display();
}
void EncoderThrottle::_display() {
DIAG(F("DRIVE vpin %d loco %d notch %d"),_firstVpin,_locoid,_notch);
}

53
IO_EncoderThrottle.h Normal file
View File

@@ -0,0 +1,53 @@
/*
* © 2024, Chris Harlow. All rights reserved.
*
* This file is part of EX-CommandStation
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The IO_EncoderThrottle device driver uses a rotary encoder connected to vpins
* to drive a loco.
* Loco id is selected by writeAnalog.
*/
#ifndef IO_EncoderThrottle_H
#define IO_EncoderThrottle_H
#include "IODevice.h"
class EncoderThrottle : public IODevice {
public:
static void create(VPIN firstVpin, int dtPin, int clkPin, int clickPin, byte notch=10);
private:
int _dtPin,_clkPin,_clickPin, _locoid, _notch,_prevpinstate;
enum {xrSTOP,xrFWD,xrREV} _stopState;
byte _rocoState;
// Constructor
EncoderThrottle(VPIN firstVpin, int dtPin, int clkPin, int clickPin, byte notch);
void _loop(unsigned long currentMicros) override ;
// Selocoid as analog value to start drive
// use <z vpin locoid [notch]>
void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override;
void _display() override ;
};
#endif

View File

@@ -162,4 +162,4 @@ protected:
};
#endif // IO_EXAMPLESERIAL_H
#endif // IO_EXAMPLESERIAL_H

View File

@@ -1,7 +1,9 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
* © 2024, Paul Antoine
* © 2023, Neil McKechnie
* All rights reserved.
*
* This file is part of DCC++EX API
* 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
@@ -112,13 +114,14 @@ protected:
// Fill buffer with spaces
memset(_buffer, ' ', _numCols*_numRows);
_displayDriver->clearNative();
// Add device to list of HAL devices (not necessary but allows
// status to be displayed using <D HAL SHOW> and device to be
// reinitialised using <D HAL RESET>).
IODevice::addDevice(this);
// Moved after addDevice() to ensure I2CManager.begin() has been called fisrt
_displayDriver->clearNative();
// Also add this display to list of display handlers
DisplayInterface::addDisplay(displayNo);
@@ -259,4 +262,4 @@ public:
};
#endif // IO_HALDisplay_H
#endif // IO_HALDisplay_H

View File

@@ -511,6 +511,7 @@ public:
if (pin == 0) { // Do nothing if not vPin 0
return _playing;
}
return _playing; // fix for compile error: "control reaches end of non-void function [-Wreturn-type]"
}
void _display() override {
@@ -549,8 +550,8 @@ private:
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[0] = REG_THR << 3 | _UART_CH << 1; //TX FIFO and UART Channel
for ( uint8_t i = 1; i < sizeof(out)+1 ; i++){
_outbuffer[i] = out[i-1];
}
@@ -616,6 +617,14 @@ private:
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);
// Extra delay when using low frequency xtal after soft reset
// Test when using 1.8432 Mhz xtal
if(_sc16is752_xtal_freq == SC16IS752_XTAL_FREQ_LOW){
_timeoutTime = micros() + 10000UL; // 10mS timeout
_awaitingResponse = true;
}
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

121
IO_I2CRailcom.cpp Normal file
View File

@@ -0,0 +1,121 @@
/*
* © 2024, Henk Kruisbrink & Chris Harlow. All rights reserved.
* © 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/>.
*/
/*
*
* Dec 2023, Added NXP SC16IS752 I2C Dual UART
* 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:
*
* I2CRailcom::create(1st vPin, vPins, I2C address);
*
* myAutomation configuration
* HAL(I2CRailcom, 1st vPin, vPins, I2C address)
* 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 (to prevent overlaps)
* I2C Address : I2C address of the serial controller, in 0x format
*/
#include "IODevice.h"
#include "IO_I2CRailcom.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "DCC.h"
#include "DCCWaveform.h"
#include "Railcom.h"
I2CRailcom::I2CRailcom(VPIN firstVpin, int nPins, I2CAddress i2cAddress){
_firstVpin = firstVpin;
_nPins = nPins;
_I2CAddress = i2cAddress;
addDevice(this);
}
void I2CRailcom::create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
if (checkNoOverlap(firstVpin, nPins, i2cAddress))
new I2CRailcom(firstVpin,nPins,i2cAddress);
}
void I2CRailcom::_begin() {
I2CManager.setClock(1000000); // TODO do we need this?
I2CManager.begin();
auto exists=I2CManager.exists(_I2CAddress);
DIAG(F("I2CRailcom: %s RailcomCollector %S detected"),
_I2CAddress.toString(), exists?F(""):F(" NOT"));
if (!exists) return;
_deviceState=DEVSTATE_NORMAL;
_display();
}
void I2CRailcom::_loop(unsigned long currentMicros) {
// Read responses from device
if (_deviceState!=DEVSTATE_NORMAL) return;
// have we read this cutout already?
// basically we only poll once per packet when railcom cutout is working
auto cut=DCCWaveform::getRailcomCutoutCounter();
if (cutoutCounter==cut) return;
cutoutCounter=cut;
Railcom::loop(); // in case a csv read has timed out
// Obtain data length from the collector
byte inbuf[1];
byte queryLength[]={'?'};
auto state=I2CManager.read(_I2CAddress, inbuf, 1,queryLength,sizeof(queryLength));
if (state) {
DIAG(F("RC ? state=%d"),state);
return;
}
auto length=inbuf[0];
if (length==0) return; // nothing to report
// Build a buffer and import the data from the collector
byte inbuf2[length];
byte queryData[]={'>'};
state=I2CManager.read(_I2CAddress, inbuf2, length,queryData,sizeof(queryData));
if (state) {
DIAG(F("RC > %d state=%d"),length,state);
return;
}
// process incoming data buffer
Railcom::process(_firstVpin,inbuf2,length);
}
void I2CRailcom::_display() {
DIAG(F("I2CRailcom: %s blocks %d-%d %S"), _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1,
(_deviceState!=DEVSTATE_NORMAL) ? F("OFFLINE") : F(""));
}

58
IO_I2CRailcom.h Normal file
View File

@@ -0,0 +1,58 @@
/*
* © 2024, Henk Kruisbrink & Chris Harlow. All rights reserved.
* © 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/>.
*/
/*
* This polls the RailcomCollecter device once per dcc packet
* and obtains an abbreviated list of block occupancy changes which
* are fortunately very rare compared with Railcom raw data.
*
* myAutomation configuration
* HAL(I2CRailcom, 1st vPin, vPins, I2C address)
* 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
* I2C Address : I2C address of the Railcom Collector, in 0x format
*/
#ifndef IO_I2CRailcom_h
#define IO_I2CRailcom_h
#include "Arduino.h"
#include "IODevice.h"
class I2CRailcom : public IODevice {
private:
byte cutoutCounter;
public:
// Constructor
I2CRailcom(VPIN firstVpin, int nPins, I2CAddress i2cAddress);
static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) ;
void _begin() ;
void _loop(unsigned long currentMicros) override ;
void _display() override ;
private:
};
#endif // IO_I2CRailcom_h

View File

@@ -98,4 +98,4 @@ private:
};
#endif
#endif

View File

@@ -108,4 +108,4 @@ private:
};
#endif
#endif

334
IO_NeoPixel.h Normal file
View File

@@ -0,0 +1,334 @@
/*
* © 2024, Chris Harlow. All rights reserved.
*
* This file is part of EX-CommandStation
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The IO_NEOPIXEL.h device driver integrates with one or more Adafruit neopixel drivers.
* This device driver will configure the device on startup, along with
* interacting with the device for all input/output duties.
*
* To create NEOPIXEL devices, these are defined in myAutomation.h:
* (Note the device driver is included by default)
*
* HAL(NEOPIXEL,first vpin, number of pixels,mode, i2c address)
* e.g. HAL(NEOPIXEL,1000,64,NEO_RGB,0x60)
* This gives each pixel in the chain an individual vpin
* The number of pixels must match the physical pixels in the chain.
*
* This driver maintains a colour (rgb value in 5,5,5 bits only) plus an ON bit.
* This can be written/read with an analog write/read call.
* The ON bit can be set on and off with a digital write. This allows for
* a pixel to be preset a colour and then turned on and off like any other light.
*/
#ifndef IO_EX_NeoPixel_H
#define IO_EX_NeoPixel_H
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "FSH.h"
// The following macros to define the Neopixel String type
// have been copied from the Adafruit Seesaw Library under the
// terms of the GPL.
// Credit to: https://github.com/adafruit/Adafruit_Seesaw
// The order of primary colors in the NeoPixel data stream can vary
// among device types, manufacturers and even different revisions of
// the same item. The third parameter to the seesaw_NeoPixel
// constructor encodes the per-pixel byte offsets of the red, green
// and blue primaries (plus white, if present) in the data stream --
// the following #defines provide an easier-to-use named version for
// each permutation. e.g. NEO_GRB indicates a NeoPixel-compatible
// device expecting three bytes per pixel, with the first byte
// containing the green value, second containing red and third
// containing blue. The in-memory representation of a chain of
// NeoPixels is the same as the data-stream order; no re-ordering of
// bytes is required when issuing data to the chain.
// Bits 5,4 of this value are the offset (0-3) from the first byte of
// a pixel to the location of the red color byte. Bits 3,2 are the
// green offset and 1,0 are the blue offset. If it is an RGBW-type
// device (supporting a white primary in addition to R,G,B), bits 7,6
// are the offset to the white byte...otherwise, bits 7,6 are set to
// the same value as 5,4 (red) to indicate an RGB (not RGBW) device.
// i.e. binary representation:
// 0bWWRRGGBB for RGBW devices
// 0bRRRRGGBB for RGB
// RGB NeoPixel permutations; white and red offsets are always same
// Offset: W R G B
#define NEO_RGB ((0 << 6) | (0 << 4) | (1 << 2) | (2))
#define NEO_RBG ((0 << 6) | (0 << 4) | (2 << 2) | (1))
#define NEO_GRB ((1 << 6) | (1 << 4) | (0 << 2) | (2))
#define NEO_GBR ((2 << 6) | (2 << 4) | (0 << 2) | (1))
#define NEO_BRG ((1 << 6) | (1 << 4) | (2 << 2) | (0))
#define NEO_BGR ((2 << 6) | (2 << 4) | (1 << 2) | (0))
// RGBW NeoPixel permutations; all 4 offsets are distinct
// Offset: W R G B
#define NEO_WRGB ((0 << 6) | (1 << 4) | (2 << 2) | (3))
#define NEO_WRBG ((0 << 6) | (1 << 4) | (3 << 2) | (2))
#define NEO_WGRB ((0 << 6) | (2 << 4) | (1 << 2) | (3))
#define NEO_WGBR ((0 << 6) | (3 << 4) | (1 << 2) | (2))
#define NEO_WBRG ((0 << 6) | (2 << 4) | (3 << 2) | (1))
#define NEO_WBGR ((0 << 6) | (3 << 4) | (2 << 2) | (1))
#define NEO_RWGB ((1 << 6) | (0 << 4) | (2 << 2) | (3))
#define NEO_RWBG ((1 << 6) | (0 << 4) | (3 << 2) | (2))
#define NEO_RGWB ((2 << 6) | (0 << 4) | (1 << 2) | (3))
#define NEO_RGBW ((3 << 6) | (0 << 4) | (1 << 2) | (2))
#define NEO_RBWG ((2 << 6) | (0 << 4) | (3 << 2) | (1))
#define NEO_RBGW ((3 << 6) | (0 << 4) | (2 << 2) | (1))
#define NEO_GWRB ((1 << 6) | (2 << 4) | (0 << 2) | (3))
#define NEO_GWBR ((1 << 6) | (3 << 4) | (0 << 2) | (2))
#define NEO_GRWB ((2 << 6) | (1 << 4) | (0 << 2) | (3))
#define NEO_GRBW ((3 << 6) | (1 << 4) | (0 << 2) | (2))
#define NEO_GBWR ((2 << 6) | (3 << 4) | (0 << 2) | (1))
#define NEO_GBRW ((3 << 6) | (2 << 4) | (0 << 2) | (1))
#define NEO_BWRG ((1 << 6) | (2 << 4) | (3 << 2) | (0))
#define NEO_BWGR ((1 << 6) | (3 << 4) | (2 << 2) | (0))
#define NEO_BRWG ((2 << 6) | (1 << 4) | (3 << 2) | (0))
#define NEO_BRGW ((3 << 6) | (1 << 4) | (2 << 2) | (0))
#define NEO_BGWR ((2 << 6) | (3 << 4) | (1 << 2) | (0))
#define NEO_BGRW ((3 << 6) | (2 << 4) | (1 << 2) | (0))
// If 400 KHz support is enabled, the third parameter to the constructor
// requires a 16-bit value (in order to select 400 vs 800 KHz speed).
// If only 800 KHz is enabled (as is default on ATtiny), an 8-bit value
// is sufficient to encode pixel color order, saving some space.
#define NEO_KHZ800 0x0000 // 800 KHz datastream
#define NEO_KHZ400 0x0100 // 400 KHz datastream
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for NeoPixel.
*/
class NeoPixel : public IODevice {
public:
static void create(VPIN vpin, int nPins, uint16_t mode=(NEO_GRB | NEO_KHZ800), I2CAddress i2cAddress=0x60) {
if (checkNoOverlap(vpin, nPins, i2cAddress)) new NeoPixel(vpin, nPins, mode, i2cAddress);
}
private:
static const byte SEESAW_NEOPIXEL_BASE=0x0E;
static const byte SEESAW_NEOPIXEL_STATUS = 0x00;
static const byte SEESAW_NEOPIXEL_PIN = 0x01;
static const byte SEESAW_NEOPIXEL_SPEED = 0x02;
static const byte SEESAW_NEOPIXEL_BUF_LENGTH = 0x03;
static const byte SEESAW_NEOPIXEL_BUF=0x04;
static const byte SEESAW_NEOPIXEL_SHOW=0x05;
// all adafruit examples say this pin. Presumably its hard wired
// in the adapter anyway.
static const byte SEESAW_PIN15 = 15;
// Constructor
NeoPixel(VPIN firstVpin, int nPins, uint16_t mode, I2CAddress i2cAddress) {
_firstVpin = firstVpin;
_nPins=nPins;
_I2CAddress = i2cAddress;
// calculate the offsets into the seesaw buffer for each colour depending
// on the pixel strip type passed in mode.
_redOffset=4+(mode >> 4 & 0x03);
_greenOffset=4+(mode >> 2 & 0x03);
_blueOffset=4+(mode & 0x03);
if (4+(mode >>6 & 0x03) == _redOffset) _bytesPerPixel=3;
else _bytesPerPixel=4; // string has a white byte.
_kHz800=(mode & NEO_KHZ400)==0;
_showPendimg=false;
// Each pixel requires 3 bytes RGB memory.
// Although the driver device can remember this, it cant do off/on without
// forgetting what the on colour was!
pixelBuffer=(RGB *) malloc(_nPins*sizeof(RGB));
stateBuffer=(byte *) calloc((_nPins+7)/8,sizeof(byte)); // all pixels off
if (pixelBuffer==nullptr || stateBuffer==nullptr) {
DIAG(F("NeoPixel I2C:%s not enough RAM"), _I2CAddress.toString());
return;
}
// preset all pins to white so a digital on/off will do something even if no colour set.
memset(pixelBuffer,0xFF,_nPins*sizeof(RGB));
addDevice(this);
}
void _begin() {
// Initialise Neopixel device
I2CManager.begin();
if (!I2CManager.exists(_I2CAddress)) {
DIAG(F("NeoPixel I2C:%s device not found"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
return;
}
byte speedBuffer[]={SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_SPEED,_kHz800};
I2CManager.write(_I2CAddress, speedBuffer, sizeof(speedBuffer));
// In the driver there are 3 of 4 byts per pixel
auto numBytes=_bytesPerPixel * _nPins;
byte setbuffer[] = {SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_BUF_LENGTH,
(byte)(numBytes >> 8), (byte)(numBytes & 0xFF)};
I2CManager.write(_I2CAddress, setbuffer, sizeof(setbuffer));
const byte pinbuffer[] = {SEESAW_NEOPIXEL_BASE, SEESAW_NEOPIXEL_PIN,SEESAW_PIN15};
I2CManager.write(_I2CAddress, pinbuffer, sizeof(pinbuffer));
for (auto pin=0;pin<_nPins;pin++) transmit(pin);
_display();
}
// loop called by HAL supervisor
void _loop(unsigned long currentMicros) override {
(void)currentMicros;
if (!_showPendimg) return;
byte showBuffer[]={SEESAW_NEOPIXEL_BASE,SEESAW_NEOPIXEL_SHOW};
I2CManager.write(_I2CAddress,showBuffer,sizeof(showBuffer));
_showPendimg=false;
}
// read back pixel on/off
int _read(VPIN vpin) override {
if (_deviceState == DEVSTATE_FAILED) return 0;
return isPixelOn(vpin-_firstVpin);
}
// Write digital value. Sets pixel on or off
void _write(VPIN vpin, int value) override {
if (_deviceState == DEVSTATE_FAILED) return;
auto pixel=vpin-_firstVpin;
if (value) {
if (isPixelOn(pixel)) return;
setPixelOn(pixel);
}
else { // set off
if (!isPixelOn(pixel)) return;
setPixelOff(pixel);
}
transmit(pixel);
}
VPIN _writeRange(VPIN vpin,int value, int count) {
// using write range cuts out the constant vpin to driver lookup so
// we can update multiple pixels much faster.
VPIN nextVpin=vpin + (count>_nPins ? _nPins : count);
if (_deviceState != DEVSTATE_FAILED) while(vpin<nextVpin) {
_write(vpin,value);
vpin++;
}
return nextVpin; // next pin we cant
}
// Write analogue value.
// The convoluted parameter mashing here is to allow passing the RGB and on/off
// information through the generic HAL _writeAnalog interface which was originally
// designed for servos and short integers
void _writeAnalogue(VPIN vpin, int colour_RG, uint8_t onoff, uint16_t colour_B) override {
if (_deviceState == DEVSTATE_FAILED) return;
RGB newColour={(byte)((colour_RG>>8) & 0xFF), (byte)(colour_RG & 0xFF), (byte)(colour_B & 0xFF)};
auto pixel=vpin-_firstVpin;
if (pixelBuffer[pixel]==newColour && isPixelOn(pixel)==(bool)onoff) return; // no change
if (onoff) setPixelOn(pixel); else setPixelOff(pixel);
pixelBuffer[pixel]=newColour;
transmit(pixel);
}
VPIN _writeAnalogueRange(VPIN vpin, int colour_RG, uint8_t onoff, uint16_t colour_B, int count) override {
// using write range cuts out the constant vpin to driver lookup so
VPIN nextVpin=vpin + (count>_nPins ? _nPins : count);
if (_deviceState != DEVSTATE_FAILED) while(vpin<nextVpin) {
_writeAnalogue(vpin,colour_RG, onoff,colour_B);
vpin++;
}
return nextVpin; // next pin we cant
}
// Display device information and status.
void _display() override {
DIAG(F("NeoPixel I2C:%s Vpins %u-%u %S"),
_I2CAddress.toString(),
(int)_firstVpin, (int)_firstVpin+_nPins-1,
_deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F(""));
}
bool isPixelOn(int16_t pixel) {return stateBuffer[pixel/8] & (0x80>>(pixel%8));}
void setPixelOn(int16_t pixel) {stateBuffer[pixel/8] |= (0x80>>(pixel%8));}
void setPixelOff(int16_t pixel) {stateBuffer[pixel/8] &= ~(0x80>>(pixel%8));}
// Helper function for error handling
void reportError(uint8_t status, bool fail=true) {
DIAG(F("NeoPixel I2C:%s Error:%d (%S)"), _I2CAddress.toString(),
status, I2CManager.getErrorMessage(status));
if (fail)
_deviceState = DEVSTATE_FAILED;
}
void transmit(uint16_t pixel) {
byte buffer[]={SEESAW_NEOPIXEL_BASE,SEESAW_NEOPIXEL_BUF,0x00,0x00,0x00,0x00,0x00};
uint16_t offset= pixel * _bytesPerPixel;
buffer[2]=(byte)(offset>>8);
buffer[3]=(byte)(offset & 0xFF);
if (isPixelOn(pixel)) {
auto colour=pixelBuffer[pixel];
buffer[_redOffset]=colour.red;
buffer[_greenOffset]=colour.green;
buffer[_blueOffset]=colour.blue;
} // else leave buffer black (in buffer preset to zeros above)
// Transmit pixel to driver
I2CManager.write(_I2CAddress,buffer,4 +_bytesPerPixel);
_showPendimg=true;
}
struct RGB {
byte red;
byte green;
byte blue;
bool operator==(const RGB& other) const {
return red == other.red && green == other.green && blue == other.blue;
}
};
RGB* pixelBuffer = nullptr;
byte* stateBuffer = nullptr; // 1 bit per pixel
bool _showPendimg;
// mapping of RGB onto pixel buffer for seesaw.
byte _bytesPerPixel;
byte _redOffset;
byte _greenOffset;
byte _blueOffset;
bool _kHz800;
};
#endif

View File

@@ -167,4 +167,4 @@ private:
};
#endif
#endif

View File

@@ -1,4 +1,5 @@
/*
* © 2025 Herb Morton
* © 2022 Paul M Antoine
* © 2021, Neil McKechnie. All rights reserved.
*
@@ -43,15 +44,21 @@
class PCF8574 : public GPIOBase<uint8_t> {
public:
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin);
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1) {
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin, initPortState);
}
private:
PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1, int initPortState=-1)
: GPIOBase<uint8_t>((FSH *)F("PCF8574"), firstVpin, nPins, i2cAddress, interruptPin)
{
requestBlock.setReadParams(_I2CAddress, inputBuffer, 1);
if (initPortState>=0) {
_portMode = 255; // set all pins to output mode
_portInUse = 255; // 8 ports in use
_portOutputState = initPortState; // initialize pins low-high 0-255
I2CManager.write(_I2CAddress, 1, initPortState);
}
}
// The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.
@@ -101,4 +108,4 @@ private:
uint8_t inputBuffer[1];
};
#endif
#endif

View File

@@ -106,4 +106,4 @@ private:
uint8_t inputBuffer[2];
};
#endif
#endif

View File

@@ -30,4 +30,3 @@
//
const uint8_t FLASH Servo::_bounceProfile[30] =
{0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100};

View File

@@ -295,4 +295,4 @@ private:
}
};
#endif
#endif

371
IO_TCA8418.h Normal file
View File

@@ -0,0 +1,371 @@
/*
* © 2023-2024, Paul M. Antoine
* © 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_tca8418_h
#define io_tca8418_h
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "FSH.h"
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for TCA8418 80-key keypad encoder, which we'll treat as 80 available VPINs where
* key down == 1 and key up == 0 by configuring just as an 8x10 keyboard matrix. Users can opt to use
* up to all 80 of the available VPINs for now, allowing memory to be saved if not all events are required.
*
* The datasheet says:
*
* The TCA8418 can be configured to support many different configurations of keypad setups.
* All 18 GPIOs for the rows and columns can be used to support up to 80 keys in an 8x10 key pad
* array. Another option is that all 18 GPIOs be used for GPIs to read 18 buttons which are
* not connected in an array. Any combination in between is also acceptable (for example, a
* 3x4 keypad matrix and using the remaining 11 GPIOs as a combination of inputs and outputs).
*
* With an 8x10 key event matrix, the events are numbered as such:
*
* C0 C1 C2 C3 C4 C5 C6 C7 C8 C9
* ========================================
* R0| 0 1 2 3 4 5 6 7 8 9
* R1| 10 11 12 13 14 15 16 17 18 19
* R2| 20 21 22 23 24 25 26 27 28 29
* R3| 30 31 32 33 34 35 36 37 38 39
* R4| 40 41 42 43 44 45 46 47 48 49
* R5| 50 51 52 53 54 55 56 57 58 59
* R6| 60 61 62 63 64 65 66 67 68 69
* R7| 70 71 72 73 74 75 76 77 78 79
*
* So if you start with VPIN 300, R0/C0 will be 300, and R7/C9 will be 379.
*
* HAL declaration for myAutomation.h is:
* HAL(TCA8418, firstVpin, numPins, I2CAddress, interruptPin)
*
* Where numPins can be 1-80, and interruptPin can be any spare Arduino pin.
*
* Configure using the following on the main I2C bus:
* HAL(TCA8418, 300, 80, 0x34)
*
* Use something like this on a multiplexor, and with up to 8 of the 8-way multiplexors you could have 64 different TCA8418 boards:
* HAL(TCA8418, 400, 80, {SubBus_1, 0x34})
*
* And if needing an Interrupt pin to speed up operations:
* HAL(TCA8418, 300, 80, 0x34, D21)
*
* Note that using an interrupt pin speeds up button press acquisition considerably (less than a millisecond vs 10-100),
* but even with interrupts enabled the code presently checks every 100ms in case the interrupt pin becomes disconnected.
* Use any available Arduino pin for interrupt monitoring.
*/
class TCA8418 : public IODevice {
public:
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
if (checkNoOverlap(firstVpin, nPins, i2cAddress))
new TCA8418(firstVpin, (nPins = (nPins > 80) ? 80 : nPins), i2cAddress, interruptPin);
}
private:
uint8_t* _digitalInputStates = NULL; // Array of pin states
uint8_t _digitalPinBytes = 0; // Number of bytes in pin state array
uint8_t _numKeyEvents = 0; // Number of outsanding key events waiting for us
unsigned long _lastEventRead = 0;
unsigned long _eventRefresh = 10000UL; // Delay refreshing events for 10ms
const unsigned long _eventRefreshSlow = 100000UL; // Delay refreshing events for 100ms
bool _gpioInterruptsEnabled = false;
uint8_t _inputBuffer[1];
uint8_t _commandBuffer[1];
I2CRB _i2crb;
enum {RDS_IDLE, RDS_EVENT, RDS_KEYCODE}; // Read operation states
uint8_t _readState = RDS_IDLE;
// Constructor
TCA8418(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
if (nPins > 0)
{
_firstVpin = firstVpin;
_nPins = nPins;
_I2CAddress = i2cAddress;
_gpioInterruptPin = interruptPin;
addDevice(this);
}
}
void _begin() {
I2CManager.begin();
if (I2CManager.exists(_I2CAddress)) {
// Default all GPIO pins to INPUT
I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR_1, 0x00);
I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR_2, 0x00);
I2CManager.write(_I2CAddress, 2, REG_GPIO_DIR_3, 0x00);
// Remove all GPIO pins from events
I2CManager.write(_I2CAddress, 2, REG_GPI_EM_1, 0x00);
I2CManager.write(_I2CAddress, 2, REG_GPI_EM_2, 0x00);
I2CManager.write(_I2CAddress, 2, REG_GPI_EM_3, 0x00);
// Set all pins to FALLING interrupts
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_LVL_1, 0x00);
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_LVL_2, 0x00);
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_LVL_3, 0x00);
// Remove all GPIO pins from interrupts
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN_1, 0x00);
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN_2, 0x00);
I2CManager.write(_I2CAddress, 2, REG_GPIO_INT_EN_3, 0x00);
// Set up an 8 x 10 matrix by writing 0xFF to all the row and column configs
// Row config is maximum of 8, and in REG_KP_GPIO_1
I2CManager.write(_I2CAddress, 2, REG_KP_GPIO_1, 0xFF);
// Column config is maximum of 10, lower 8 bits in REG_KP_GPIO_2, upper in REG_KP_GPIO_3
// Set first 8 columns
I2CManager.write(_I2CAddress, 2, REG_KP_GPIO_2, 0xFF);
// Turn on cols 9/10
I2CManager.write(_I2CAddress, 2, REG_KP_GPIO_3, 0x03);
// // Set all pins to Enable Debounce
I2CManager.write(_I2CAddress, 2, REG_DEBOUNCE_DIS_1, 0x00);
I2CManager.write(_I2CAddress, 2, REG_DEBOUNCE_DIS_2, 0x00);
I2CManager.write(_I2CAddress, 2, REG_DEBOUNCE_DIS_3, 0x00);
// Let's assume an 8x10 matrix for now, and configure
_digitalPinBytes = (_nPins + 7) / 8;
if ((_digitalInputStates = (byte *)calloc(_digitalPinBytes, 1)) == NULL) {
DIAG(F("TCA8418 I2C: Unable to alloc %d bytes"), _digitalPinBytes);
return;
}
// Configure pin used for GPIO extender notification of change (if allocated)
// and configure TCA8418 to produce key event interrupts
if (_gpioInterruptPin >= 0) {
DIAG(F("TCA8418 I2C: interrupt pin configured on %d"), _gpioInterruptPin);
_gpioInterruptsEnabled = true;
_eventRefresh = _eventRefreshSlow; // Switch to slower manual refreshes in case the INT pin isn't connected!
pinMode(_gpioInterruptPin, INPUT_PULLUP);
I2CManager.write(_I2CAddress, 2, REG_CFG, REG_CFG_KE_IEN);
// Clear any pending interrupts
I2CManager.write(_I2CAddress, 2, REG_INT_STAT, REG_STAT_K_INT);
}
#ifdef DIAG_IO
_display();
#endif
}
}
int _read(VPIN vpin) override {
if (_deviceState == DEVSTATE_FAILED)
return 0;
int pin = vpin - _firstVpin;
bool result = _digitalInputStates[pin / 8] & (1 << (pin % 8));
return result;
}
// Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads)
void _loop(unsigned long currentMicros) override {
if (_deviceState == DEVSTATE_FAILED) return; // If device failed, return
// Request block is used for key event reads from the TCA8418, which are performed
// on a cyclic basis.
if (_readState != RDS_IDLE) {
if (_i2crb.isBusy()) return; // If I2C operation still in progress, return
uint8_t status = _i2crb.status;
if (status == I2C_STATUS_OK) { // If device request ok, read input data
// First check if we have any key events waiting
if (_readState == RDS_EVENT) {
if ((_numKeyEvents = (_inputBuffer[0] & 0x0F)) != 0) {
// We could read each key event waiting in a synchronous loop, which may prove preferable
// but for now, schedule an async read of the first key event in the queue
_commandBuffer[0] = REG_KEY_EVENT_A;
I2CManager.read(_I2CAddress, _inputBuffer, 1, _commandBuffer, 1, &_i2crb); // non-blocking read
_readState = RDS_KEYCODE; // Shift to reading key events!
}
else // We found no key events waiting, return to IDLE
_readState = RDS_IDLE;
}
else {
// RDS_KEYCODE
uint8_t key = _inputBuffer[0] & 0x7F;
bool keyDown = _inputBuffer[0] & 0x80;
// Check for just keypad events
key--; // R0/C0 is key #1, so subtract 1 to create an array offset
// We only want to record key events we're configured for, as we have calloc'd an
// appropriately sized _digitalInputStates array!
if (key < _nPins) {
if (keyDown)
_digitalInputStates[key / 8] |= (1 << (key % 8));
else
_digitalInputStates[key / 8] &= ~(1 << (key % 8));
}
else
DIAG(F("TCA8418 I2C: key event %d discarded, outside Vpin range"), key);
_numKeyEvents--; // One less key event to get
if (_numKeyEvents != 0)
{
// DIAG(F("TCA8418 I2C: more keys in read event queue, # waiting is: %x"), _numKeyEvents);
// We could read each key event waiting in a synchronous loop, which may prove preferable
// but for now, schedule an async read of the first key event in the queue
_commandBuffer[0] = REG_KEY_EVENT_A;
I2CManager.read(_I2CAddress, _inputBuffer, 1, _commandBuffer, 1, &_i2crb); // non-blocking read
}
else {
// DIAG(F("TCA8418 I2C: no more keys in read event queue"));
// Clear any pending interrupts
I2CManager.write(_I2CAddress, 2, REG_INT_STAT, REG_STAT_K_INT);
_readState = RDS_IDLE; // Shift to IDLE
return;
}
}
} else
reportError(status, false); // report eror but don't go offline.
}
// If we're not doing anything now, check to see if we have an interrupt pin configured and it is low,
// or if our timer has elapsed and we should check anyway in case the interrupt pin is disconnected.
if (_readState == RDS_IDLE) {
if ((_gpioInterruptsEnabled && !digitalRead(_gpioInterruptPin)) ||
((currentMicros - _lastEventRead) > _eventRefresh))
{
_commandBuffer[0] = REG_KEY_LCK_EC;
I2CManager.read(_I2CAddress, _inputBuffer, 1, _commandBuffer, 1, &_i2crb); // non-blocking read
_lastEventRead = currentMicros;
_readState = RDS_EVENT; // Shift to looking for key events!
}
}
}
// Display device information and status
void _display() override {
DIAG(F("TCA8418 I2C:%s Vpins %u-%u%S"),
_I2CAddress.toString(),
_firstVpin, (_firstVpin+_nPins-1),
_deviceState == DEVSTATE_FAILED ? F(" OFFLINE") : F(""));
if (_gpioInterruptsEnabled)
DIAG(F("TCA8418 I2C:Interrupt on pin %d"), _gpioInterruptPin);
}
// Helper function for error handling
void reportError(uint8_t status, bool fail=true) {
DIAG(F("TCA8418 I2C:%s Error:%d (%S)"), _I2CAddress.toString(),
status, I2CManager.getErrorMessage(status));
if (fail)
_deviceState = DEVSTATE_FAILED;
}
enum tca8418_registers
{
// REG_RESERVED = 0x00
REG_CFG = 0x01, // Configuration register
REG_INT_STAT = 0x02, // Interrupt status
REG_KEY_LCK_EC = 0x03, // Key lock and event counter
REG_KEY_EVENT_A = 0x04, // Key event register A
REG_KEY_EVENT_B = 0x05, // Key event register B
REG_KEY_EVENT_C = 0x06, // Key event register C
REG_KEY_EVENT_D = 0x07, // Key event register D
REG_KEY_EVENT_E = 0x08, // Key event register E
REG_KEY_EVENT_F = 0x09, // Key event register F
REG_KEY_EVENT_G = 0x0A, // Key event register G
REG_KEY_EVENT_H = 0x0B, // Key event register H
REG_KEY_EVENT_I = 0x0C, // Key event register I
REG_KEY_EVENT_J = 0x0D, // Key event register J
REG_KP_LCK_TIMER = 0x0E, // Keypad lock1 to lock2 timer
REG_UNLOCK_1 = 0x0F, // Unlock register 1
REG_UNLOCK_2 = 0x10, // Unlock register 2
REG_GPIO_INT_STAT_1 = 0x11, // GPIO interrupt status 1
REG_GPIO_INT_STAT_2 = 0x12, // GPIO interrupt status 2
REG_GPIO_INT_STAT_3 = 0x13, // GPIO interrupt status 3
REG_GPIO_DAT_STAT_1 = 0x14, // GPIO data status 1
REG_GPIO_DAT_STAT_2 = 0x15, // GPIO data status 2
REG_GPIO_DAT_STAT_3 = 0x16, // GPIO data status 3
REG_GPIO_DAT_OUT_1 = 0x17, // GPIO data out 1
REG_GPIO_DAT_OUT_2 = 0x18, // GPIO data out 2
REG_GPIO_DAT_OUT_3 = 0x19, // GPIO data out 3
REG_GPIO_INT_EN_1 = 0x1A, // GPIO interrupt enable 1
REG_GPIO_INT_EN_2 = 0x1B, // GPIO interrupt enable 2
REG_GPIO_INT_EN_3 = 0x1C, // GPIO interrupt enable 3
REG_KP_GPIO_1 = 0x1D, // Keypad/GPIO select 1
REG_KP_GPIO_2 = 0x1E, // Keypad/GPIO select 2
REG_KP_GPIO_3 = 0x1F, // Keypad/GPIO select 3
REG_GPI_EM_1 = 0x20, // GPI event mode 1
REG_GPI_EM_2 = 0x21, // GPI event mode 2
REG_GPI_EM_3 = 0x22, // GPI event mode 3
REG_GPIO_DIR_1 = 0x23, // GPIO data direction 1
REG_GPIO_DIR_2 = 0x24, // GPIO data direction 2
REG_GPIO_DIR_3 = 0x25, // GPIO data direction 3
REG_GPIO_INT_LVL_1 = 0x26, // GPIO edge/level detect 1
REG_GPIO_INT_LVL_2 = 0x27, // GPIO edge/level detect 2
REG_GPIO_INT_LVL_3 = 0x28, // GPIO edge/level detect 3
REG_DEBOUNCE_DIS_1 = 0x29, // Debounce disable 1
REG_DEBOUNCE_DIS_2 = 0x2A, // Debounce disable 2
REG_DEBOUNCE_DIS_3 = 0x2B, // Debounce disable 3
REG_GPIO_PULL_1 = 0x2C, // GPIO pull-up disable 1
REG_GPIO_PULL_2 = 0x2D, // GPIO pull-up disable 2
REG_GPIO_PULL_3 = 0x2E, // GPIO pull-up disable 3
// REG_RESERVED = 0x2F
};
enum tca8418_config_reg_fields
{
// Config Register #1 fields
REG_CFG_AI = 0x80, // Auto-increment for read/write
REG_CFG_GPI_E_CGF = 0x40, // Event mode config
REG_CFG_OVR_FLOW_M = 0x20, // Overflow mode enable
REG_CFG_INT_CFG = 0x10, // Interrupt config
REG_CFG_OVR_FLOW_IEN = 0x08, // Overflow interrupt enable
REG_CFG_K_LCK_IEN = 0x04, // Keypad lock interrupt enable
REG_CFG_GPI_IEN = 0x02, // GPI interrupt enable
REG_CFG_KE_IEN = 0x01, // Key events interrupt enable
};
enum tca8418_int_status_fields
{
// Interrupt Status Register #2 fields
REG_STAT_CAD_INT = 0x10, // Ctrl-alt-del seq status
REG_STAT_OVR_FLOW_INT = 0x08, // Overflow interrupt status
REG_STAT_K_LCK_INT = 0x04, // Key lock interrupt status
REG_STAT_GPI_INT = 0x02, // GPI interrupt status
REG_STAT_K_INT = 0x01, // Key events interrupt status
};
enum tca8418_lock_ec_fields
{
// Key Lock Event Count Register #3
REG_LCK_EC_K_LCK_EN = 0x40, // Key lock enable
REG_LCK_EC_LCK_2 = 0x20, // Keypad lock status 2
REG_LCK_EC_LCK_1 = 0x10, // Keypad lock status 1
REG_LCK_EC_KLEC_3 = 0x08, // Key event count bit 3
REG_LCK_EC_KLEC_2 = 0x04, // Key event count bit 2
REG_LCK_EC_KLEC_1 = 0x02, // Key event count bit 1
REG_LCK_EC_KLEC_0 = 0x01, // Key event count bit 0
};
};
#endif

216
IO_TM1638.cpp Normal file
View File

@@ -0,0 +1,216 @@
/*
* © 2024, Chris Harlow. 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/>.
*/
/* Credit to https://github.com/dvarrel/TM1638 for the basic formulae.*/
#include <Arduino.h>
#include "IODevice.h"
#include "IO_TM1638.h"
#include "DIAG.h"
const uint8_t HIGHFLASH _digits[16]={
0b00111111,0b00000110,0b01011011,0b01001111,
0b01100110,0b01101101,0b01111101,0b00000111,
0b01111111,0b01101111,0b01110111,0b01111100,
0b00111001,0b01011110,0b01111001,0b01110001
};
// Constructor
TM1638::TM1638(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin){
_firstVpin = firstVpin;
_nPins = 8;
_clk_pin = clk_pin;
_stb_pin = stb_pin;
_dio_pin = dio_pin;
pinMode(clk_pin,OUTPUT);
pinMode(stb_pin,OUTPUT);
pinMode(dio_pin,OUTPUT);
_pulse = PULSE1_16;
_buttons=0;
_leds=0;
_lastLoop=micros();
addDevice(this);
}
void TM1638::create(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin) {
if (checkNoOverlap(firstVpin,8))
new TM1638(firstVpin, clk_pin,dio_pin,stb_pin);
}
void TM1638::_begin() {
displayClear();
test();
_display();
}
void TM1638::_loop(unsigned long currentMicros) {
if (currentMicros - _lastLoop > (1000000UL/LoopHz)) {
_buttons=getButtons();// Read the buttons
_lastLoop=currentMicros;
}
}
void TM1638::_display() {
DIAG(F("TM1638 Configured on Vpins:%u-%u"), _firstVpin, _firstVpin+_nPins-1);
}
// digital read gets button state
int TM1638::_read(VPIN vpin) {
byte pin=vpin - _firstVpin;
bool result=bitRead(_buttons,pin);
// DIAG(F("TM1638 read (%d) buttons %x = %d"),pin,_buttons,result);
return result;
}
// digital write sets led state
void TM1638::_write(VPIN vpin, int value) {
// TODO.. skip if no state change
writeLed(vpin - _firstVpin + 1,value!=0);
}
// Analog write sets digit displays
void TM1638::_writeAnalogue(VPIN vpin, int lowBytes, uint8_t mode, uint16_t highBytes) {
// mode is in DataFormat defined above.
byte formatLength=mode & 0x0F; // last 4 bits
byte formatType=mode & 0xF0; //
int8_t leftDigit=vpin-_firstVpin; // 0..7 from left
int8_t rightDigit=leftDigit+formatLength-1; // 0..7 from left
// loading is done right to left startDigit first
int8_t startDigit=7-rightDigit; // reverse as 7 on left
int8_t lastDigit=7-leftDigit; // reverse as 7 on left
uint32_t value=highBytes;
value<<=16;
value |= (uint16_t)lowBytes;
//DIAG(F("TM1638 fl=%d ft=%x sd=%d ld=%d v=%l vx=%X"),
// formatLength,formatType,startDigit,lastDigit,value,value);
while(startDigit<=lastDigit) {
switch (formatType) {
case _DF_DECIMAL:// decimal (leading zeros)
displayDig(startDigit,GETHIGHFLASH(_digits,(value%10)));
value=value/10;
break;
case _DF_HEX:// HEX (leading zeros)
displayDig(startDigit,GETHIGHFLASH(_digits,(value & 0x0F)));
value>>=4;
break;
case _DF_RAW:// Raw 7-segment pattern
displayDig(startDigit,value & 0xFF);
value>>=8;
break;
default:
DIAG(F("TM1368 invalid mode 0x%x"),mode);
return;
}
startDigit++;
}
}
uint8_t TM1638::getButtons(){
ArduinoPins::fastWriteDigital(_stb_pin, LOW);
writeData(INSTRUCTION_READ_KEY);
pinMode(_dio_pin, INPUT);
ArduinoPins::fastWriteDigital(_clk_pin, LOW);
uint8_t buttons=0;
for (uint8_t eachByte=0; eachByte<4;eachByte++) {
uint8_t value = 0;
for (uint8_t eachBit = 0; eachBit < 8; eachBit++) {
ArduinoPins::fastWriteDigital(_clk_pin, HIGH);
value |= ArduinoPins::fastReadDigital(_dio_pin) << eachBit;
ArduinoPins::fastWriteDigital(_clk_pin, LOW);
}
buttons |= value << eachByte;
delayMicroseconds(1);
}
pinMode(_dio_pin, OUTPUT);
ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
return buttons;
}
void TM1638::displayDig(uint8_t digitId, uint8_t pgfedcba){
if (digitId>7) return;
setDataInstruction(DISPLAY_TURN_ON | _pulse);
setDataInstruction(INSTRUCTION_WRITE_DATA| INSTRUCTION_ADDRESS_FIXED);
writeDataAt(FIRST_DISPLAY_ADDRESS+14-(digitId*2), pgfedcba);
}
void TM1638::displayClear(){
setDataInstruction(DISPLAY_TURN_ON | _pulse);
setDataInstruction(INSTRUCTION_WRITE_DATA | INSTRUCTION_ADDRESS_FIXED);
for (uint8_t i=0;i<15;i+=2){
writeDataAt(FIRST_DISPLAY_ADDRESS+i,0x00);
}
}
void TM1638::writeLed(uint8_t num,bool state){
if ((num<1) | (num>8)) return;
setDataInstruction(DISPLAY_TURN_ON | _pulse);
setDataInstruction(INSTRUCTION_WRITE_DATA | INSTRUCTION_ADDRESS_FIXED);
writeDataAt(FIRST_DISPLAY_ADDRESS + (num*2-1), state);
}
void TM1638::writeData(uint8_t data){
for (uint8_t i = 0; i < 8; i++) {
ArduinoPins::fastWriteDigital(_dio_pin, data & 1);
data >>= 1;
ArduinoPins::fastWriteDigital(_clk_pin, HIGH);
ArduinoPins::fastWriteDigital(_clk_pin, LOW);
}
}
void TM1638::writeDataAt(uint8_t displayAddress, uint8_t data){
ArduinoPins::fastWriteDigital(_stb_pin, LOW);
writeData(displayAddress);
writeData(data);
ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
delayMicroseconds(1);
}
void TM1638::setDataInstruction(uint8_t dataInstruction){
ArduinoPins::fastWriteDigital(_stb_pin, LOW);
writeData(dataInstruction);
ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
delayMicroseconds(1);
}
void TM1638::test(){
DIAG(F("TM1638 test"));
uint8_t val=0;
for(uint8_t i=0;i<5;i++){
setDataInstruction(DISPLAY_TURN_ON | _pulse);
setDataInstruction(INSTRUCTION_WRITE_DATA| INSTRUCTION_ADDRESS_AUTO);
ArduinoPins::fastWriteDigital(_stb_pin, LOW);
writeData(FIRST_DISPLAY_ADDRESS);
for(uint8_t i=0;i<16;i++)
writeData(val);
ArduinoPins::fastWriteDigital(_stb_pin, HIGH);
delay(1000);
val = ~val;
}
}

134
IO_TM1638.h Normal file
View File

@@ -0,0 +1,134 @@
/*
* © 2024, Chris Harlow. 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_TM1638_h
#define IO_TM1638_h
#include <Arduino.h>
#include "IODevice.h"
#include "DIAG.h"
class TM1638 : public IODevice {
private:
uint8_t _buttons;
uint8_t _leds;
unsigned long _lastLoop;
static const int LoopHz=20;
static const byte
INSTRUCTION_WRITE_DATA=0x40,
INSTRUCTION_READ_KEY=0x42,
INSTRUCTION_ADDRESS_AUTO=0x40,
INSTRUCTION_ADDRESS_FIXED=0x44,
INSTRUCTION_NORMAL_MODE=0x40,
INSTRUCTION_TEST_MODE=0x48,
FIRST_DISPLAY_ADDRESS=0xC0,
DISPLAY_TURN_OFF=0x80,
DISPLAY_TURN_ON=0x88;
uint8_t _clk_pin;
uint8_t _stb_pin;
uint8_t _dio_pin;
uint8_t _pulse;
bool _isOn;
// Constructor
TM1638(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin);
public:
enum DigitFormat : byte {
// last 4 bits are length.
// DF_1.. DF_8 decimal
DF_1=0x01,DF_2=0x02,DF_3=0x03,DF_4=0x04,
DF_5=0x05,DF_6=0x06,DF_7=0x07,DF_8=0x08,
// DF_1X.. DF_8X HEX
DF_1X=0x11,DF_2X=0x12,DF_3X=0x13,DF_4X=0x14,
DF_5X=0x15,DF_6X=0x16,DF_7X=0x17,DF_8X=0x18,
// DF_1R .. DF_4R raw 7 segmnent data
// only 4 because HAL analogWrite only passes 4 bytes
DF_1R=0x21,DF_2R=0x22,DF_3R=0x23,DF_4R=0x24,
// bits of data conversion type (ored with length)
_DF_DECIMAL=0x00,// right adjusted decimal unsigned leading zeros
_DF_HEX=0x10, // right adjusted hex leading zeros
_DF_RAW=0x20 // bytes are raw 7-segment pattern (max length 4)
};
static void create(VPIN firstVpin, byte clk_pin,byte dio_pin,byte stb_pin);
// Functions overridden in IODevice
void _begin();
void _loop(unsigned long currentMicros) override ;
void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override;
void _display() override ;
int _read(VPIN pin) override;
void _write(VPIN pin,int value) override;
// Device driving functions
private:
enum pulse_t {
PULSE1_16,
PULSE2_16,
PULSE4_16,
PULSE10_16,
PULSE11_16,
PULSE12_16,
PULSE13_16,
PULSE14_16
};
/**
* @fn getButtons
* @return state of 8 buttons
*/
uint8_t getButtons();
/**
* @fn writeLed
* @brief put led ON or OFF
* @param num num of led(1-8)
* @param state (true or false)
*/
void writeLed(uint8_t num, bool state);
/**
* @fn displayDig
* @brief set 7 segment display + dot
* @param digitId num of digit(0-7)
* @param val value 8 bits
*/
void displayDig(uint8_t digitId, uint8_t pgfedcba);
/**
* @fn displayClear
* @brief switch off all leds and segment display
*/
void displayClear();
void test();
void writeData(uint8_t data);
void writeDataAt(uint8_t displayAddress, uint8_t data);
void setDisplayMode(uint8_t displayMode);
void setDataInstruction(uint8_t dataInstruction);
};
#endif

View File

@@ -131,4 +131,4 @@ protected:
};
#endif // IO_TOUCHKEYPAD_H
#endif // IO_TOUCHKEYPAD_H

View File

@@ -170,4 +170,4 @@ public:
}
};
#endif
#endif

View File

@@ -95,4 +95,4 @@ private:
};
#endif
#endif

View File

@@ -26,7 +26,7 @@
Thus "MAIN"_hk generates exactly the same run time vakue
as const int16_t HASH_KEYWORD_MAIN=11339
*/
#ifndef KeywordHAsher_h
#ifndef KeywordHasher_h
#define KeywordHasher_h
#include <Arduino.h>
@@ -54,4 +54,43 @@ 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
// Compile time converter from "abcd"_s7 to the 7 segment nearest equivalent
constexpr uint8_t seg7Digits[]={
0b00111111,0b00000110,0b01011011,0b01001111, // 0..3
0b01100110,0b01101101,0b01111101,0b00000111, // 4..7
0b01111111,0b01101111 // 8..9
};
constexpr uint8_t seg7Letters[]={
0b01110111,0b01111100,0b00111001,0b01011110, // ABCD
0b01111001,0b01110001,0b00111101,0b01110110, // EFGH
0b00000100,0b00011110,0b01110010,0b00111000, //IJKL
0b01010101,0b01010100,0b01011100,0b01110011, // MNOP
0b10111111,0b01010000,0b01101101,0b01111000, // QRST
0b00111110,0b00011100,0b01101010,0b01001001, //UVWX
0b01100110,0b01011011 //YZ
};
constexpr uint8_t seg7Space=0b00000000;
constexpr uint8_t seg7Minus=0b01000000;
constexpr uint8_t seg7Equals=0b01001000;
constexpr uint32_t CompiletimeSeg7(const char * sv, uint32_t running, size_t rlen) {
return (*sv==0 || rlen==0) ? running << (8*rlen) : CompiletimeSeg7(sv+1,
(*sv >= '0' && *sv <= '9') ? (running<<8) | seg7Digits[*sv-'0'] :
(*sv >= 'A' && *sv <= 'Z') ? (running<<8) | seg7Letters[*sv-'A'] :
(*sv >= 'a' && *sv <= 'z') ? (running<<8) | seg7Letters[*sv-'a'] :
(*sv == '-') ? (running<<8) | seg7Minus :
(*sv == '=') ? (running<<8) | seg7Equals :
(running<<8) | seg7Space,
rlen-1
); //
}
constexpr uint32_t operator""_s7(const char * keyword, size_t len)
{
return CompiletimeSeg7(keyword,0*len,4);
}
#endif

View File

@@ -221,4 +221,4 @@ void LiquidCrystal_I2C::expanderWrite(uint8_t value) {
rb.wait();
outputBuffer[0] = value | _backlightval;
I2CManager.write(_Addr, outputBuffer, 1, &rb); // Write command asynchronously
}
}

View File

@@ -1,5 +1,6 @@
/*
* © 2022-2023 Paul M Antoine
* © 2022-2024 Paul M Antoine
* © 2024 Herb Morton
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2023 Harald Barth
@@ -38,6 +39,8 @@ volatile portreg_t shadowPORTC;
volatile portreg_t shadowPORTD;
volatile portreg_t shadowPORTE;
volatile portreg_t shadowPORTF;
volatile portreg_t shadowPORTG;
volatile portreg_t shadowPORTH;
#endif
MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, int16_t brake_pin,
@@ -88,6 +91,16 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTF;
}
if (HAVE_PORTG(fastSignalPin.inout == &PORTG)) {
DIAG(F("Found PORTG pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTG;
}
if (HAVE_PORTH(fastSignalPin.inout == &PORTH)) {
DIAG(F("Found PORTH pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTH;
}
signalPin2=signal_pin2;
if (signalPin2!=UNUSED_PIN) {
@@ -126,6 +139,16 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTF;
}
if (HAVE_PORTG(fastSignalPin2.inout == &PORTG)) {
DIAG(F("Found PORTG pin %d"),signalPin2);
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTG;
}
if (HAVE_PORTH(fastSignalPin2.inout == &PORTH)) {
DIAG(F("Found PORTH pin %d"),signalPin2);
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTH;
}
}
else dualSignal=false;
@@ -393,6 +416,18 @@ void MotorDriver::setDCSignal(byte speedcode, uint8_t frequency /*default =0*/)
setSignal(tDir);
HAVE_PORTF(PORTF=shadowPORTF);
interrupts();
} else if (HAVE_PORTG(fastSignalPin.shadowinout == &PORTG)) {
noInterrupts();
HAVE_PORTG(shadowPORTG=PORTG);
setSignal(tDir);
HAVE_PORTG(PORTG=shadowPORTG);
interrupts();
} else if (HAVE_PORTH(fastSignalPin.shadowinout == &PORTH)) {
noInterrupts();
HAVE_PORTH(shadowPORTH=PORTH);
setSignal(tDir);
HAVE_PORTH(PORTH=shadowPORTH);
interrupts();
} else {
noInterrupts();
setSignal(tDir);
@@ -541,7 +576,7 @@ 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))){
if ((trackMode & TRACK_MODIFIER_AUTO) && (trackMode & (TRACK_MODE_MAIN|TRACK_MODE_EXT|TRACK_MODE_BOOST))){
DIAG(F("TRACK %c INVERT"), trackno + 'A');
invertOutput();
}
@@ -604,6 +639,10 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
}
throttleInrush(false);
setPower(POWERMODE::ON);
break;
}
if (goodtime > POWER_SAMPLE_ALERT_GOOD/2) {
throttleInrush(false);
}
break;
}

View File

@@ -1,5 +1,5 @@
/*
* © 2022-2023 Paul M. Antoine
* © 2022-2024 Paul M. Antoine
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020 Chris Harlow
@@ -26,23 +26,33 @@
#include "FSH.h"
#include "IODevice.h"
#include "DCCTimer.h"
#include <wiring_private.h>
#include "TemplateForEnums.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_EXT = 16,
// For example TRACK_MODE_DC_INV is (TRACK_MODE_DC|TRACK_MODIFIER_INV)
enum TRACK_MODE : byte {
// main modes
TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PROG = 4,
TRACK_MODE_DC = 8, TRACK_MODE_EXT = 16,
// modifiers
TRACK_MODIFIER_INV = 64, TRACK_MODIFIER_AUTO = 128,
#ifdef ARDUINO_ARCH_ESP32
TRACK_MODE_BOOST = 32,
TRACK_MODE_BOOST = 32,
TRACK_MODE_BOOST_INV = TRACK_MODE_BOOST|TRACK_MODIFIER_INV,
TRACK_MODE_BOOST_AUTO = TRACK_MODE_BOOST|TRACK_MODIFIER_AUTO,
#else
TRACK_MODE_BOOST = 0,
TRACK_MODE_BOOST = 0,
TRACK_MODE_BOOST_INV = 0,
TRACK_MODE_BOOST_AUTO = 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};
// derived modes; TRACK_ALL is calles that so it does not match TRACK_MODE_*
TRACK_ALL = TRACK_MODE_MAIN|TRACK_MODE_PROG|TRACK_MODE_DC|TRACK_MODE_EXT|TRACK_MODE_BOOST,
TRACK_MODE_MAIN_INV = TRACK_MODE_MAIN|TRACK_MODIFIER_INV,
TRACK_MODE_MAIN_AUTO = TRACK_MODE_MAIN|TRACK_MODIFIER_AUTO,
TRACK_MODE_DC_INV = TRACK_MODE_DC|TRACK_MODIFIER_INV,
TRACK_MODE_DCX = TRACK_MODE_DC_INV // DCX is other name for historical reasons
};
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
@@ -83,6 +93,14 @@ enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PRO
#define PORTF GPIOF->ODR
#define HAVE_PORTF(X) X
#endif
#if defined(GPIOG)
#define PORTG GPIOG->ODR
#define HAVE_PORTG(X) X
#endif
#if defined(GPIOH)
#define PORTH GPIOH->ODR
#define HAVE_PORTH(X) X
#endif
#endif
// if macros not defined as pass-through we define
@@ -106,6 +124,12 @@ enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PRO
#ifndef HAVE_PORTF
#define HAVE_PORTF(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
#ifndef HAVE_PORTG
#define HAVE_PORTG(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
#ifndef HAVE_PORTH
#define HAVE_PORTH(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
// Virtualised Motor shield 1-track hardware Interface
@@ -145,6 +169,8 @@ extern volatile portreg_t shadowPORTC;
extern volatile portreg_t shadowPORTD;
extern volatile portreg_t shadowPORTE;
extern volatile portreg_t shadowPORTF;
extern volatile portreg_t shadowPORTG;
extern volatile portreg_t shadowPORTH;
enum class POWERMODE : byte { OFF, ON, OVERLOAD, ALERT };
@@ -256,7 +282,7 @@ class MotorDriver {
#endif
inline void setMode(TRACK_MODE m) {
trackMode = m;
invertOutput(trackMode & TRACK_MODE_INV);
invertOutput(trackMode & TRACK_MODIFIER_INV);
};
inline void invertOutput() { // toggles output inversion
invertPhase = !invertPhase;

View File

@@ -75,11 +75,19 @@
#define SAMD_STANDARD_MOTOR_SHIELD STANDARD_MOTOR_SHIELD
#define STM32_STANDARD_MOTOR_SHIELD STANDARD_MOTOR_SHIELD
#if defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F439ZI) || defined(ARDUINO_NUCLEO_F4X9ZI)
// EX 8874 based shield connected to a 3V3 system with 12-bit (4096) ADC
// The Ethernet capable STM32 models cannot use Channel B BRAKE on D8, and must use the ALT pin of D6,
// AND cannot use Channel B PWN on D11, but must use the ALT pin of D5
#define EX8874_SHIELD F("EX8874"), \
new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 1.27, 5000, A4), \
new MotorDriver( 5, 13, UNUSED_PIN, 6, A1, 1.27, 5000, A5)
#else
// EX 8874 based shield connected to a 3V3 system with 12-bit (4096) ADC
#define EX8874_SHIELD F("EX8874"), \
new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 1.27, 5000, A4), \
new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 1.27, 5000, A5)
#endif
#elif defined(ARDUINO_ARCH_ESP32)
// STANDARD shield on an ESPDUINO-32 (ESP32 in Uno form factor). The shield must be eiter the
@@ -97,6 +105,18 @@
new MotorDriver(25/* 3*/, 19/*12*/, UNUSED_PIN, 13/*9*/, 35/*A2*/, 1.27, 5000, 36 /*A4*/), \
new MotorDriver(23/*11*/, 18/*13*/, UNUSED_PIN, 12/*8*/, 34/*A3*/, 1.27, 5000, 39 /*A5*/)
// EX-CSB1 with integrated motor driver definition
#define EXCSB1 F("EXCSB1"),\
new MotorDriver(25, 0, UNUSED_PIN, -14, 34, 2.23, 5000, 19), \
new MotorDriver(27, 15, UNUSED_PIN, -2, 35, 2.23, 5000, 23)
// EX-CSB1 with EX-8874 stacked on top for 4 outputs
#define EXCSB1_WITH_EX8874 F("EXCSB1_WITH_EX8874"),\
new MotorDriver(25, 0, UNUSED_PIN, -14, 34, 2.23, 5000, 19), \
new MotorDriver(27, 15, UNUSED_PIN, -2, 35, 2.23, 5000, 23), \
new MotorDriver(26, 5, UNUSED_PIN, 13, 36, 1.52, 5000, 18), \
new MotorDriver(16, 4, UNUSED_PIN, 12, 39, 1.52, 5000, 17)
#else
// STANDARD shield on any Arduino Uno or Mega compatible with the original specification.
#define STANDARD_MOTOR_SHIELD F("STANDARD_MOTOR_SHIELD"), \

View File

@@ -1,77 +1,39 @@
# What is DCC++ EX?
DCC++ EX is the organization maintaining several codebases that together represent a fully open source DCC system. Currently, this includes the following:
# What is DCC-EX?
DCC-EX is a team of dedicated enthusiasts producing open source DCC & DC solutions for you to run your complete model railroad layout. Our easy to use, do-it-yourself, and free open source products run on off-the-shelf Arduino technology and are supported by numerous third party hardware and apps like JMRI, Engine Driver, wiThrottle, Rocrail and more.
* [CommandStation-EX](https://github.com/DCC-EX/CommandStation-EX/releases) - the latest take on the DCC++ command station for controlling your trains. Runs on an Arduino board, and includes advanced features such as a WiThrottle server implementation, turnout operation, general purpose inputs and outputs (I/O), and JMRI integration.
* [exWebThrottle](https://github.com/DCC-EX/exWebThrottle) - a simple web based controller for your DCC++ command station.
* [BaseStation-installer](https://github.com/DCC-EX/BaseStation-Installer) - an installer executable that takes care of downloading and installing DCC++ firmware onto your hardware setup.
* [BaseStation-Classic](https://github.com/DCC-EX/BaseStation-Classic) - the original DCC++ software, packaged in a stable release. No active development, bug fixes only.
Currently, our products include the following:
A basic DCC++ EX hardware setup can use easy to find, widely avalable Arduino boards that you can assemble yourself.
Both CommandStation-EX and BaseStation-Classic support much of the NMRA Digital Command Control (DCC) [standards](http://www.nmra.org/dcc-working-group "NMRA DCC Working Group"), including:
* simultaneous control of multiple locomotives
* 2-byte and 4-byte locomotive addressing
* 28 or 128-step speed throttling
* Activate/de-activate all accessory function addresses 0-2048
* Control of all cab functions F0-F28 and F29-F68
* Main Track: Write configuration variable bytes and set/clear specific configuration variable (CV) bits (aka Programming on Main or POM)
* Programming Track: Same as the main track with the addition of reading configuration variable bytes
* And many more custom features. see [What's new in CommandStation-EX?](#whats-new-in-commandstation-ex)
* [EX-CommandStation](https://github.com/DCC-EX/CommandStation-EX/releases)
* [EX-WebThrottle](https://github.com/DCC-EX/exWebThrottle)
* [EX-Installer](https://github.com/DCC-EX/EX-Installer)
* [EX-MotoShield8874](https://dcc-ex.com/reference/hardware/motorboards/ex-motor-shield-8874.html#gsc.tab=0)
* [EX-DCCInspector](https://github.com/DCC-EX/DCCInspector-EX)
* [EX-Toolbox](https://github.com/DCC-EX/EX-Toolbox)
* [EX-Turntable](https://github.com/DCC-EX/EX-Turntable)
* [EX-IOExpander](https://github.com/DCC-EX/EX-IOExpander)
* [EX-FastClock](https://github.com/DCC-EX/EX-FastClock)
* [DCCEXProtocol](https://github.com/DCC-EX/DCCEXProtocol)
Details of these projects can be found on [our web site](https://dcc-ex.com/).
# Whats in this Repository?
This repository, CommandStation-EX, contains a complete DCC++ EX Commmand Station sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
This repository, CommandStation-EX, contains a complete DCC-EX *EX-CommmandStation* sketch designed for compiling and uploading into an Arduino Uno, Mega, or Nano.
To utilize this sketch, you can use the following:
1. (beginner) our [automated installer](https://github.com/DCC-EX/BaseStation-Installer)
1. (recommended for all levels of user) our [automated installer](https://github.com/DCC-EX/EX-Installer)
2. (intermediate) download the latest version from the [releases page](https://github.com/DCC-EX/CommandStation-EX/releases)
3. (advanced) use git clone on this repository
Not using the installer? Open the file "CommandStation-EX.ino" in the
Arduino IDE. Please do not rename the folder containing the sketch
code, nor add any files in that folder. The Arduino IDE relies on the
structure and name of the folder to properly display and compile the
code. Rename or copy config.example.h to config.h. If you do not have
the standard setup, you must edit config.h according to the help texts
in config.h.
Refer to [our web site](https://https://dcc-ex.com/ex-commandstation/get-started/index.html#/) for the hardware required for this project.
## What's new in CommandStation-EX?
**We seriously recommend using the EX-Installer**, however if you choose not to use the installer...
* WiThrottle server built in. Connect Engine Driver or WiThrottle clients directly to your Command Station (or through JMRI as before)
* WiFi and Ethernet shield support
* No more jumpers or soldering!
* Direct support for all the most popular motor control boards including single pin (Arduino) or dual pin (IBT_2) type PWM inputs without the need for an adapter circuit
* I2C Display support (LCD and OLED)
* Improved short circuit detection and automatic reset from an overload
* Current reading, sensing and ACK detection settings in milliAmps instead of just pin readings
* Improved adherence to the NMRA DCC specification
* Complete support for all the old commands and front ends like JMRI
* Railcom cutout (beta)
* Simpler, modular, faster code with an API Library for developers for easy expansion
* New features and functions in JMRI
* Ability to join MAIN and PROG tracks into one MAIN track to run your locos
* "Drive-Away" feature - Throttles with support, like Engine Driver, can allow a loco to be programmed on a usable, electrically isolated programming track and then drive off onto the main track
* Diagnostic commands to test decoders that aren't reading or writing correctly
* Support for Uno, Nano, Mega, Nano Every and Teensy microcontrollers
* User Functions: Filter regular commands (like a turnout or output command) and pass it to your own function or accessory
* Support for LCN (layout control nodes)
* mySetup.h file that acts like an Autoexec.Bat command to send startup commands to the CS
* High Accuracty Waveform option for rock steady DCC signals
* New current response outputs current in mA, overlimit current, and maximum board capable current. Support for new current meter in JMRI
* USB Browser based EX-WebThrottle
* New, simpler, function control command
* Number of locos discovery command `<#>`
* Emergency stop command <!>
* Release cabs from memory command <-> all cabs, <- CAB> for just one loco address
* Automatic slot (register) management
* Automation (coming soon)
NOTE: DCC-EX is a major rewrite to the code. We started over and rebuilt it from the ground up! For what that means, you can read [HERE](https://dcc-ex.com/about/rewrite.html).
* Open the file ``CommandStation-EX.ino`` in the Arduino IDE or Visual Studio Code (VSC). Please do not rename the folder containing the sketch code, nor add any files in that folder. The Arduino IDE relies on the structure and name of the folder to properly display and compile the code.
* Rename or copy ``config.example.h`` to ``config.h``.
* You must edit ``config.h`` according to the help texts in ``config.h``.
# More information
You can learn more at the [DCC++ EX website](https://dcc-ex.com/)
You can learn more at the [DCC-EX website](https://dcc-ex.com/)
- November 14, 2020

82
Railcom.cpp Normal file
View File

@@ -0,0 +1,82 @@
/*
* © 2025 Chris Harlow
* All rights reserved.
*
* This file is part of DCC-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Railcom.h"
#include "DCC.h"
#include "DCCWaveform.h"
uint16_t Railcom::expectLoco=0;
uint16_t Railcom::expectCV=0;
unsigned long Railcom::expectWait=0;
ACK_CALLBACK Railcom::expectCallback=0;
// anticipate is used when waiting for a CV read from a railcom loco
void Railcom::anticipate(uint16_t loco, uint16_t cv, ACK_CALLBACK callback) {
expectLoco=loco;
expectCV=cv;
expectWait=millis(); // start of timeout
expectCallback=callback;
}
// process is called to handle data buffer sent by collector
void Railcom::process(int16_t firstVpin,byte * buffer, byte length) {
// block,locohi,locolow
// block|0x80,data pom read cv
byte i=0;
while (i<length) {
byte block=buffer[i] & 0x3f;
byte type=buffer[i]>>6;
switch (type) {
// a type=0 record has block,locohi,locolow
case 0: {
uint16_t locoid= ((uint16_t)buffer[i+1])<<8 | ((uint16_t)buffer[i+2]);
DIAG(F("RC3 b=%d l=%d"),block,locoid);
if (locoid==0) DCC::clearBlock(firstVpin+block);
else DCC::setLocoInBlock(locoid,firstVpin+block,true);
i+=3;
}
break;
case 2: { // csv value from POM read
byte value=buffer[i+1];
if (expectCV && DCCWaveform::getRailcomLastLocoAddress()==expectLoco) {
DCC::setLocoInBlock(expectLoco,firstVpin+block,false);
if (expectCallback) expectCallback(value);
expectCV=0;
}
i+=2;
}
break;
default:
DIAG(F("Unknown RC Collector code %d"),type);
return;
}
}
}
// loop() is called to detect timeouts waiting for a POM read result
void Railcom::loop() {
if (expectCV && (millis()-expectWait)> POM_READ_TIMEOUT) { // still waiting
expectCallback(-1);
expectCV=0;
}
}

40
Railcom.h Normal file
View File

@@ -0,0 +1,40 @@
/*
* © 202 5Chris Harlow
* All rights reserved.
*
* This file is part of DCC-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 Railcom_h
#define Railcom_h
#include "Arduino.h"
typedef void (*ACK_CALLBACK)(int16_t result);
class Railcom {
public:
static void anticipate(uint16_t loco, uint16_t cv, ACK_CALLBACK callback);
static void process(int16_t firstVpin,byte * buffer, byte length );
static void loop();
private:
static const unsigned long POM_READ_TIMEOUT=500; // as per spec
static uint16_t expectCV,expectLoco;
static unsigned long expectWait;
static ACK_CALLBACK expectCallback;
static const byte MAX_WAIT_FOR_GLITCH=20; // number of dead or empty packets before assuming loco=0
};
#endif

View File

@@ -0,0 +1,119 @@
// 5.2.49
Which is a more efficient than the AT/AFTER/IF methods
of handling buttons and switches, especially on MIMIC panels.
ONBUTTON(vpin)
handles debounce and starts a task if a button is used to
short a pin to ground.
for example:
ONBUTTON(30) TOGGLE_TURNOUT(30) DONE
ONSENSOR(vpin)
handles debounce and starts a task if the pin changes.
You may want to check the pin state with an IF ...
Note the ONBUTTON and ONSENSOR are not generally useful
for track sensors and running trains, because you dont know which
train triggered the sensor.
// 5.2.47
BLINK(vpin, onMs,offMs)
which will start a vpin blinking until such time as it is SET, RESET or set by a signal operation such as RED, AMBER, GREEN.
BLINK returns immediately, the blinking is autonomous.
This means a signal that always blinks amber could be done like this:
SIGNAL(30,31,32)
ONAMBER(30) BLINK(31,500,500) DONE
The RED or GREEN calls will turn off the amber blink automatically.
Alternatively a signal that has normal AMBER and flashing AMBER could be like this:
#define FLASHAMBER(signal) \
AMBER(signal) \
BLINK(signal+1,500,500)
(Caution: this assumes that the amber pin is redpin+1)
==
FTOGGLE(function)
Toggles the current loco function (see FON and FOFF)
XFTOGGLE(loco,function)
Toggles the function on given loco. (See XFON, XFOFF)
TOGGLE_TURNOUT(id)
Toggles the turnout (see CLOSE, THROW)
STEALTH_GLOBAL(code)
ADVANCED C++ users only.
Inserts code such as static variables and functions that
may be utilised by multiple STEALTH operations.
// 5.2.34 - <A address aspect> Command fopr DCC Extended Accessories.
This command sends an extended accessory packet to the track, Normally used to set
a signal aspect. Aspect numbers are undefined as sdtandards except for 0 which is
always considered a stop.
// - Exrail ASPECT(address,aspect) for above.
The ASPECT command sents an aspect to a DCC accessory using the same logic as
<A aspect address>.
// - EXRAIL DCCX_SIGNAL(Address,redAspect,amberAspect,greenAspect)
This defines a signal (with id same as dcc address) that can be operated
by the RED/AMBER/GREEN commands. In each case the command uses the signal
address to refer to the signal and the aspect chosen depends on the use of the RED
AMBER or GREEN command sent. Other aspects may be sent but will require the
direct use of the ASPECT command.
The IFRED/IFAMBER/IFGREEN and ONRED/ONAMBER/ONGREEN commands contunue to operate
as for any other signal type. It is important to be aware that use of the ASPECT
or <A> commands will correctly set the IF flags and call the ON handlers if ASPECT
is used to set one of the three aspects defined in the DCCX_SIGNAL command.
Direct use of other aspects does not affect the signal flags.
ASPECT and <A> can be used withput defining any signal if tyhe flag management or
ON event handlers are not required.
// 5.2.33 - Exrail CONFIGURE_SERVO(vpin,pos1,pos2,profile)
This macro offsers a more convenient way of performing the HAL call in halSetup.h
In halSetup.h --- IODevice::configureServo(101,300,400,PCA9685::slow);
In myAutomation.h --- CONFIGURE_SERVO(101,300,400,slow)
// 5.2.32 - Railcom Cutout (Initial trial Mega2560 only)
This cutout will only work on a Mega2560 with a single EX8874 motor shield
configured in the normal way with the main track brake pin on pin 9.
<C RAILCOM ON> Turns on the cutout mechanism.
<C RAILCOM OFF> Tirns off the cutout. (This is the default)
<C RAILCOM DEBUG> ONLY to be used by developers used for waveform diagnostics.
(In DEBUG mode the main track idle packets are replaced with reset packets, This
makes it far easier to see the preambles and cutouts on a logic analyser or scope.)
// 5.2.31 - Exrail JMRI_SENSOR(vpin [,count]) creates <S> types.
This Macro causes the creation of JMRI <S> type sensors in a way that is
simpler than repeating lines of <S> commands.
JMRI_SENSOR(100) is equenvelant to <S 100 100 1>
JMRI_SENSOR(100,16) will create <S> type sensors for vpins 100-115.
// 5.2.26 - Silently ignore overridden HAL defaults
// - include HAL_IGNORE_DEFAULTS macro in EXRAIL
The HAL_IGNORE_DEFAULTS command, anywhere in myAutomation.h will
prevent the startup code from trying the default I2C sensors/servos.
// 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
Causes compiler time messages for EXRAIL issues that would normally
only be discovered by things going wrong at run time.
// 5.2.13 - EXRAIL STEALTH
Permits a certain level of C++ code to be embedded as a single step in
an exrail sequence. Serious engineers only.
// 5.2.9 - EXRAIL STASH feature
// - Added ROUTE_DISABLED macro in EXRAIL

View File

@@ -0,0 +1,58 @@
Virtual Bitmap device pins.
a Bitmap device pin is a software representation of a virtual hardware device that has the ability to store a 16bit value.
This this is easier to manage than LATCH in EXRAIL as they can be explicitely set and tested without interfering with underlying hardware or breaching the 255 limit.
Virtual pins may be set, reset and tested in the same way as any other pin. Unlike sensors and leds, these device pins are both INPUT and OUTPUT These can be used in many ways:
As a simple digital flag to assist in inter-thread communication.
A flag or value that can be set from commands and tested in EXRAIL.(e.g. to stop a sequence)
As a counter for looping or occupancy counts such as trains passing over a multi track road crossing.
As a collection of 16 digital bits that can be set, reset, toggled, masked and tested.
Existing <> and exrail commands for vpins work on these pins.
Virtual pin creation:
HAL(Bitmap,firstpin,npins)
creates 1 or more virtual pins in software. (RAM requirement approximately 2 bytes per pin)
e.g. HAL(Bitmap,1000,20) creates pins 1000..1019
Simple use as flags:
This uses the traditional digital pin commands
SET(1013) RESET(1013) sets value 1 or 0
SET(1000,20) RESET(1000,20) sets/resets a range of pins
IF(1000) tests if pin value!=0
Commands can set 1/0 values using <z 1010> <z -1010> as for any digital output.
BLINK can be used to set them on/off on a time pattern.
In addition, Exrail sensor comands work as if these pins were sensors
ONBUTTON(1013) triggers when value changes from 0 to something.
ONSENSOR(1013) triggers when value changes to or from 0.
<S 1013 1013 1> and JMRI_SENSOR(1013) report <Q/q responses when changing to or from 0.
Use as analog values:
Analog values may be set into the virtual pins and tested using the existing analog value commands and exrail macros.
<z vpin value> <D ANIN vpin> etc.
Use as counters:
For loop counting, counters can be incremented by BITMAP_INC(1013) and decremented by BITMAP_DEC(1013) and tested with IF/IFNOT/IFGTE etc.
Counters be used to automate a multi track crossing where each train entering increments the counter and decrements it on clearing the crossing. Crossing gate automation can be started when the value changes from 0, and be stopped when the counter returns to 0. Detecting the first increment from 0 to 1 can be done with ONBUTTON(1013) and the automation can use IF(1013) or IFNOT(1013) to detect when it needs to reopen the road gates.
Use as binary flag groups:
Virtual pins (and others that respond to an analog read in order to provide bitmapped digital data, such as SensorCam) can be set and tested with new special EXRAIL commands
IFBITMAP_ALL(vpin,mask) Bitwise ANDs the the vpin value with the mask value and is true if ALL the 1 bits in the mask are also 1 bits in the value.
e.g. IFBITMAP_ALL(1013,0x0f) would be true if ALL the last 4 bits of the value are 1s.
IFBITMAP_ANY(1013,0x0f) would be true if ANY of the last 4 bits are 1s.
Modifying bitmap values:
BITMAP_AND(vpin,mask) performs a bitwise AND operation.
BITMAP_OR(vpin,mask) performa a bitwise OR operation
BITMAP_XOR(vpin,mask) performs a bitwise EXCLUSIVE OR (which is basically a toggle)

77
Release_Notes/NeoPixel.md Normal file
View File

@@ -0,0 +1,77 @@
NeoPixel support
The IO_NeoPixel.h driver supports the adafruit neopixel seesaw board. It turns each pixel into an individual VPIN which can be given a colour and turned on or off using the new <o> command or the NEOPIXEL Exrail macro. Exrail SIGNALS can also drive a single pixel signal or multiple separate pixels.
1. Defining the hardware driver:
Add a driver definition in myAutomation.h for each adafruit I2C driver.
HAL(neoPixel, firstVpin, numberOfPixels [, mode [, i2caddress])
Where mode is selected from the various pixel string types which have varying
colour order or refresh frequency. For MOST strings this mode will be NEO_GRB but for others refer to the comments in IO_NeoPixel.h
If omitted the node and i2caddress default to NEO_GRB, 0x60.
HAL(NeoPixel,1000,20)
This is a NeoPixel driver defaulting to I2C aqddress 0x60 for a GRB pixel string. Pixels are given vpin numbers from 1000 to 1019.
HAL(NeoPixel,1020,20,NEO_GRB,0x61)
This is a NeoPixel driver on i2c address 0x61
2. Setting pixels from the < > commands.
By default, each pixel in the string is created as white but switched off.
Each pixel has a vpin starting from the first vpin in the HAL definitions.
<o vpin> switches pixel on (same as <z vpin>) e.g. <o 1005>
<o -vpin> switches pixel off (same as <z -vpin>) e.g. <o -1003>
(the z commands work on pixels the same as other gpio pins.)
<o [-]vpin count> switches on/off count pixels starting at vpin. e.g <o 1000 5>
Note: it IS acceptable to switch across 2 strings of pixels if they are contiguous vpin ranges. It is also interesting that this command doesnt care if the vpins are NeoPixel or any other type, so it can be used to switch a range of other pin types.
<o [-]vpin red green blue [count]> sets the colour and on/off status of a pin or pins. Each colour is 0..255 e.g. <o 1005 255 255 0> sets pin 1005 to bright yellow and ON, <0 -1006 0 0 255 10> sets pins 1006 to 1015 (10 pins) to bright blue but OFF.
Note: If you set a pin to a colour, you can turn it on and off without having to reset the colour every time. This is something the adafruit seesaw library can't do and is just one of several reasons why we dont use it.
3. Setting pixels from EXRAIL
The new NEOPIXEL macro provides the same functionality as the <o [-]vpin red green blue [count]> command above.
NEOPIXEL([-]vpin, red, green, blue [,count])
Setting pixels on or off (without colour change) can be done with SET/RESET [currently there is no set range facility but that may be added as a general exrail thing... watch this space]
Because the pixels obey set/reset, the BLINK command can also be used to control blinking a pixel.
4. EXRAIL pixel signals.
There are two types possible, a mast with separate fixed colour pixels for each aspect, or a mast with one multiple colour pixel for all aspects.
For separate pixels, the colours should be established at startup and a normal SIGNALH macro used.
AUTOSTART
SIGNALH(1010,1011,1012)
NEOPIXEL(1010,255,0,0)
NEOPIXEL(1011,128,128,0)
NEOPIXEL(1012,0,255,0)
RED(1010) // force signal state otherwise all 3 lights will be on
DONE
For signals with 1 pixel, the NEOPIXEL_SIGNAL macro will create a signal
NEOPIXEL_SIGNAL(vpin,redfx,amberfx,greenfx)
** Changed... ****
The fx values above can be created by the NeoRGB macro so a bright red would be NeoRGB(255,0,0) bright green NeoRGB(0,255,0) and amber something like NeoRGB(255,100,0)
NeoRGB creates a single int32_t value so it can be used in several ways as convenient.
// create 1-lamp signal with NeoRGB colours
NEOPIXEL_SIGNAL(1000,NeoRGB(255,0,0),NeoRGB(255,100,0),NeoRGB(0,255,0))
// Create 1-lamp signal with named colours.
// This is better if you have multiple signals.
// (Note: ALIAS is not suitable due to word length defaults)
#define REDLAMP NeoRGB(255,0,0)
#define AMBERLAMP NeoRGB(255,100,0)
#define GREENLAMP NeoRGB(0,255,0)
NEOPIXEL_SIGNAL(1001,REDLAMP,AMBERLAMP,GREENLAMP)
// Create 1-lamp signal with web type RGB colours
// (Using blue for the amber signal , just testing)
NEOPIXEL_SIGNAL(1002,0xFF0000,0x0000FF,0x00FF00)

71
Release_Notes/Railcom.md Normal file
View File

@@ -0,0 +1,71 @@
Railcom implementation notes, Chris Harlow Oct 2024
Railcom support is in 3 parts
1. Generation of the DCC waveform with a Railcom cutout.
2. Accessing the railcom feedback from a loco using hardware detectors
3. Utilising the feedback to do something useful.
DCC Waveform Railcom cutout depends on using suitable motor shields (EX8874 primarily) as the standard Arduino shield is not suitable. (Too high resistance during cutout)
The choice of track management also depends on wiring all the MAIN tracks to use the same signal and brake pins. This allows separate track power management but prevents switching a single track from MAIN to PROG or DC...
Some CPUs require very specific choice of brake pins etc to match their internal timer register architecture.
- MEGA.. The default shield setting for an EX8874 is suitable for Railcom on Channel A (MAIN)
- ESP32 .. not yet supported.
- Nucleo ... TBA
Enabling the Railcom Cutout requires a `<C RAILCOM ON>` command. This can be added to myAutomation using `PARSE("<C RAILCOM ON>")`
Code to calculate the cutout position and provide synchronization for the sampling is in `DCCWaveform.cpp` (not ESP32)
and in general a global search for "railcom" will show all code changes that have been made to support this.
Code to actually implement the timing of the cutout is highly cpu dependent and can be found in the various implementations of `DCCTimer.h`. At this time only `DCCTimerAVR.cpp`has implemented this.
Reading Railcom data:
A new HAL handler (`IO_I2CRailcom.h`) has been added to process input from a 32-block railcom collecter which operates over I2C. The collector and its readers sit between the CS and the track and collect railcom data from locos during the cutout.
The Collector device removes 99.9% of the railcom traffic and returns just a summary of what has changed since the last cutout.
After the cutout the HAL driver reads the Collector summary over I2C and passes the raw data to the CS logic (`Railcom.cpp`) for analysis.
Each 32-block reader is described in myAutomation like `HAL(I2CRailcom,10000,32,0x08)` which will assign 32 blocks on i2c address 0x08 with vpin numbers 10000 and 10031. If you only use fewer channel in the collector, you can assign fewer pins here.
(Implementation notes.. you may have multiple collectors, each one will requite a HAL line to define its i2c address and vpins to represent block numbers.)
Making use of Railcom data
Exrail has two additional event handlers which can capture locos entering and exiting blocks. These handlers are started with the loco information already set, so for example:
```
ONBLOCKENTER(10000)
// a loco has entered block 10000
FON(0) // turn the light on
FON(1) // make a lot of noise
SPEED(20) // slow down
DONE
ONBLOCKEXIT(10000)
// a loco has left block 10000
FOFF(0) // turn the light off
FOFF(1) // stop the noise
SPEED(50) // speed up again
DONE
```
Note that the Railcom interpretation code is capable of detecting multiple locos in the same block at the same time and will create separate exrail tasks for each one.
There is however one minor loophole in the block exit logic...
If THREE or more locos are in the same block and ONE of them leaves, then ONBLOCKEXIT will not fire until
EITHER - The leaving loco enters another railcom block
OR - only ONE loco remains in the block just left.
To further support block management in railcom, two additional serial commands are available
`<K block loco >` to simulate a loco entering a block, and trigger any ONBLOCKENTER
`<k block loco >` to simulate a loco leaving a block, and trigger and ONBLOCKEXIT
Reading CV values on MAIN.
Railcom allows for the facility to read loco cv values while on the main track. This is considerably faster than PROG track access but depends on the loco being in a Railcom monitored block.
To read from PROG Track we use `<R cv>` response is `<r value>`
To read from MAIN track use `<r loco cv>`
response is `<r loco cv value>`

21
Release_Notes/Stash.md Normal file
View File

@@ -0,0 +1,21 @@
# The STASH feature of exrail.
STASH is used for scenarios where it is helpful to relate a loco id to where it is parked. For example a fiddle yard may have 10 tracks and it's much easier for the operator to select a train to depart by using the track number, or pressing a button relating to that track, rather than by knowing the loco id which may be difficult to see.
Automated yard parking can use the stash to determine which tracks are empty without the need for block occupancy detectors.
Note that a negative locoid may be stashed to indicate that the loco will operate with inverted direction. For example a loco facing backwards, with the INVERT_DRECTION state may be stashed by exrail and the invert state will be restored along with the loco id when using the PICKUP_STASH. CLEAR_ANY_STASH will clear all references to the loco regardless of direction.
The following Stash commands are available:
| EXRAIL command | Serial protocol | function |
| -------------- | --------------- | -------- |
| STASH(s) | `<JM s locoid>` | Save the current loco id in the stash array element s. |
| CLEAR_STASH(s) | `<JM s 0>` | Sets stash array element s to zero. |
| CLEAR_ALL_STASH | `<JM CLEAR ALL>` | sets all stash entries to zero |
| CLEAR_ANY_STASH | `<JM CLEAR ANY locoid>` | removes current loco from all stash elements |
| PICKUP_STASH(s) | N/A | sets current loco to stash element s |
| IFSTASH(s) | N/A | True if stash element s is not zero |
| N/A | `<JM>` | query all stashes (returns `<jM s loco>` where loco is not zero)
| N/A | `<JM stash>` | Query loco in stash (returns `<jM s loco>`)

44
Release_Notes/TCA8418.md Normal file
View File

@@ -0,0 +1,44 @@
## TCA8418 ##
The TCA8418 IC from Texas Instruments is a low cost and very capable GPIO and keyboard scanner. Used as a keyboard scanner, it has 8 rows of 10 columns of IO pins which allow encoding of up to 80 buttons. The IC is available on an Adafruit board with Qwiic I2C interconnect called the "Adafruit TCA8418 Keypad Matrix and GPIO Expander Breakout" and available here for the modest sum of $US6 or so: https://www.adafruit.com/product/4918
The great advantage of this IC is that the keyboard scanning is done continuously, and it has a 10-element event queue, so even if you don't get to the interrupt immediately, keypress and release events will be held for you. Since it's I2C its very easy to use with any DCC-EX command station.
The TCA8418 driver presently configures the IC in the full 8x10 keyboard scanning mode, and then maps each key down/key up event to the state of a single vpin for extremely easy use from within EX-RAIL and JMRI as each key looks like an individual sensor.
This is ideal for mimic panels where you may need a lot of buttons, but with this board you can use just 18 wires to handle as many as 80 buttons.
By adding a simple HAL statement to myAutomation.h it creates between 1 and 80 buttons it will report back.
`HAL(TCA8418, firstVpin, numPins, I2CAddress, interruptPin)`
For example:
`HAL(TCA8418, 300, 80, 0x34)`
Creates VPINs 300-379 which you can monitor with EX-RAIL, JMRI sensors etc.
With an 8x10 key event matrix, the events are numbered using the Rn row pins and Cn column pins as such:
C0 C1 C2 C3 C4 C5 C6 C7 C8 C9
========================================
R0| 0 1 2 3 4 5 6 7 8 9
R1| 10 11 12 13 14 15 16 17 18 19
R2| 20 21 22 23 24 25 26 27 28 29
R3| 30 31 32 33 34 35 36 37 38 39
R4| 40 41 42 43 44 45 46 47 48 49
R5| 50 51 52 53 54 55 56 57 58 59
R6| 60 61 62 63 64 65 66 67 68 69
R7| 70 71 72 73 74 75 76 77 78 79
So if you start with the first pin definition being VPIN 300, R0/C0 will be 300 + 0, and R7/C9 will be 300+79 or 379.
Use something like this on a multiplexor, and with up to 8 of the 8-way multiplexors you could have 64 different TCA8418 boards:
`HAL(TCA8418, 400, 80, {SubBus_1, 0x34})`
And if needing an Interrupt pin to speed up operations:
`HAL(TCA8418, 300, 80, 0x34, 21)`
Note that using an interrupt pin speeds up button press acquisition considerably (less than a millisecond vs 10-100), but even with interrupts enabled the code presently checks every 100ms in case the interrupt pin becomes disconnected. Use any available Arduino pin for interrupt monitoring.

84
Release_Notes/TM1638.md Normal file
View File

@@ -0,0 +1,84 @@
## TM1638 ##
The TM1638 board provides a very cheap way of implementing 8 buttons, 8 leds and an 8 digit 7segment display in a package requiring just 5 Dupont wires (vcc, gnd + 3 GPIO pins) from the command station without soldering.
This is ideal for prototyping and testing, simulating sensors and signals, displaying states etc. For a built layout, this could provide a control for things that are not particularly suited to throttle 'route' buttons, perhaps lineside automations or fiddle yard lane selection.
By adding a simple HAL statement to myAutomation.h it creates 8 buttons/sensors and 8 leds.
`HAL(TM1638,500,29,31,33)`
Creates VPINs 500-507 And desscribes the GPIO pins used to connect the clk,dio,stb pins on the TM1638 board.
Setting each of the VPINs will control the associated LED (using for example SET, RESET or BLINK in Exrail or `<z 500> <z -501> from a command).
Unlike most pins, you can also read the same pin number and get the button state, using Exrail IF/AT/ONBUTTON etc.
For example:
`
HAL(TM1638,500,29,31,33)
`
All the folowing examples assume you are using VPIN 500 as the first, leftmost, led/button on the TM1638 board.
`ONBUTTON(500)
SET(500) // light the first led
BLINK(501,500,500) // blink the second led
SETLOCO(3) FWD(50) // set a loco going
AT(501) STOP // press second button to stop
RESET(500) RESET(501) // turn leds off
DONE
`
Buttons behave like any other sensor, so using `<S 500 500 1>` will cause the command station to issue `<Q 500>` and `<q 500>` messages when the first button is pressed or released.
Exrail `JMRI_SENSOR(500,8)` will create `<S` commands for all 8 buttons.
## Using the 7 Segment display ##
The 8 digit display can be treated as 8 separate digits (left most being the same VPIN as the leftmost button and led) or be written to in sections of any length. Writing uses the existing analogue interface to the common HAL but is awkward to use directly. To make this easier from Exrail, a SEG7 macro provides a remapping to the ANOUT facility that makes more sense.
SEG7(vpin,value,format)
The vpin determins which digit to start writing at.
The value can be a 32bit unsigned integer but is interpreted differentlky according to the format.
Format values:
1..8 give the length (number of display digits) to fill, and defaults to decimal number with leading zeros.
1X..8X give the length but display in hex.
1R..4R treats each byte of the value as raw 7-segment patterns so that it can write letters and symbols using any compination of the 7segments and deciml point.
There is a useful description here:
https://jetpackacademy.com/wp-content/uploads/2018/06/TM1638_cheat_sheet_download.pdf
e.g. SEG7(500,3,4)
writes 0003 to first 4 digits of the display
SEG7(504,0xcafe,4X)
writes CAFE to the last 4 digits
SEG7(500,0xdeadbeef,8X)
writes dEAdbEEF to all 8 digits.
Writing raw segment patters requires knowledge of the bit pattern to segment relationship:
` 0
== 0 ==
5| | 1
== 6 ==
4 | | 2
== 3 ==
7=decimal point
Thus Letter A is segments 6 5 4 2 1 0, in bits that is (0 bit on right)
0b01110111 or 0x77
This is not easy to do my hand and thus a new string type suffix has been introduced to make simple text messages. Note that the HAL interface only has width for 32 bits which is only 4 symbols so writing 8 digits requires two calls.
e.g. SEG7(500,"Hell"_s7,4R) SEG7(504,"o"_s7,4R)
DELAY(1000)
SEG7(500,"Worl"_s7,4R) SEG7(504,"d"_s7,4R)
Note that some letters like k,m,v,x do not have particularly readable 7-segment representations.
Credit to https://github.com/dvarrel/TM1638 for the basic formulae.

29
Release_Notes/momentum.md Normal file
View File

@@ -0,0 +1,29 @@
New Momentum feature notes:
The command station can apply momentum to throttle movements in the same way that a standards compliant DCC decoder can be set to do. This momentum can be defaulted system wide and overridden on individual locos. It does not use or alter the loco CV values and so it also works when driving DC locos.
The momentum is applied regardless of the throttle type used (or even EXRAIL).
Momentum is specified in mS / throttle_step.
There is a new command `<m cabid accelerating [brake]>`
where the brake value defaults to the accelerating value.
For example:
`<m 3 0>` sets loco 3 to no momentum.
`<m 3 21>` sets loco 3 to 21 mS/step.
`<m 3 21 42>` sets loco 3 to 21 mS/step accelerating and 42 mS/step when decelerating.
`<m 0 21>` sets the default momentum to 21mS/Step for all current and future locos that have not been specifically set.
`<m 3 -1>` sets loco 3 to track the default momentum value.
EXRAIL
A new macro `MOMENTUM(accel [, decel])` sets the momentum value of the current tasks loco ot the global default if loco=0.
Note: Setting Momentum 7,14,21 etc is similar in effect to setting a decoder CV03/CV04 to 1,2,3.
As an additional option, the momentum calculation is based on the
difference in throttle setting and actual speed. For example, the time taken to reach speed 50 from a standing start would be less if the throttle were set to speed 100, thus increasing the acceleration.
`<m LINEAR>` - acceleration is uniform up to selected throttle speed.
`<m POWER>` - acceleration depends on difference between loco speed and selected throttle speed.

View File

@@ -0,0 +1,41 @@
<html>
<!-- Minimalist test page for the DCCEX websocket API.-->
<head>
<script>
let socket = new WebSocket("ws://192.168.1.242:2560","DCCEX");
// send message from the form
var sender = function() {
var msg=document.getElementById('message').value;
socket.send(msg);
}
// message received - show the message in div#messages
socket.onmessage = function(event) {
let message = event.data;
let messageElem = document.createElement('div');
messageElem.textContent = message;
document.getElementById('messages').prepend(messageElem);
}
socket.onerror = function(event) {
let message = event.data;
let messageElem = document.createElement('div');
messageElem.textContent = message;
document.getElementById('messages').prepend(messageElem);
}
</script>
</head>
<body>
This is a minimalist test page for the DCCEX websocket API.
It demonstrates the Websocket connection and how to send
or receive websocket traffic.
The connection string must be edited to address your command station
correctly.<p>
<!-- message form -->
<input type="text" id="message">
<input type="button" value="Send" onclick="sender();">
<!-- div with messages -->
<div id="messages"></div>
</body>
</html>

101
STM32lwipopts.h.copyme Normal file
View File

@@ -0,0 +1,101 @@
/*
* © 2024 Harald Barth
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
//
// Rewrite of the STM32lwipopts.h file from STM
// To be copied into where lwipopts_default.h resides
// typically into STM32Ethernet/src/STM32lwipopts.h
// or STM32Ethernet\src\STM32lwipopts.h
// search for `lwipopts_default.h` and copy this file into the
// same directory but name it STM32lwipopts.h
//
#ifndef __STM32LWIPOPTS_H__
#define __STM32LWIPOPTS_H__
// include this here and then override things we do differnet
#include "lwipopts_default.h"
// we can not include our "defines.h" here
// so we need to duplicate that define
#define MAX_NUM_TCP_CLIENTS_HERE 9
#ifdef MAX_NUM_TCP_CLIENTS
#if MAX_NUM_TCP_CLIENTS != MAX_NUM_TCP_CLIENTS_HERE
#error MAX_NUM_TCP_CLIENTS and MAX_NUM_TCP_CLIENTS_HERE must be same
#endif
#else
#define MAX_NUM_TCP_CLIENTS MAX_NUM_TCP_CLIENTS_HERE
#endif
// increase ARP cache
#undef MEMP_NUM_APR_QUEUE
#define MEMP_NUM_ARP_QUEUE MAX_NUM_TCP_CLIENTS+3 // one for each client (all on different HW) and a few extra
// Example for debug
//#define LWIP_DEBUG 1
//#define TCP_DEBUG LWIP_DBG_ON
// NOT STRICT NECESSARY ANY MORE BUT CAN BE USED TO SAVE RAM
#undef MEM_LIBC_MALLOC
#define MEM_LIBC_MALLOC 1 // use the same malloc as for everything else
#undef MEMP_MEM_MALLOC
#define MEMP_MEM_MALLOC 1 // uses malloc which means no pools which means slower but not mean 32KB up front
#undef MEMP_NUM_TCP_PCB
#define MEMP_NUM_TCP_PCB MAX_NUM_TCP_CLIENTS+1 // one extra so we can reject number N+1 from our code
#define MEMP_NUM_TCP_PCB_LISTEN 6
#undef MEMP_NUM_TCP_SEG
#define MEMP_NUM_TCP_SEG MAX_NUM_TCP_CLIENTS
#undef MEMP_NUM_SYS_TIMEOUT
#define MEMP_NUM_SYS_TIMEOUT MAX_NUM_TCP_CLIENTS+2
#undef PBUF_POOL_SIZE
#define PBUF_POOL_SIZE MAX_NUM_TCP_CLIENTS
#undef LWIO_ICMP
#define LWIP_ICMP 1
#undef LWIP_RAW
#define LWIP_RAW 1 /* PING changed to 1 */
#undef DEFAULT_RAW_RECVMBOX_SIZE
#define DEFAULT_RAW_RECVMBOX_SIZE 3 /* for ICMP PING */
#undef LWIP_DHCP
#define LWIP_DHCP 1
#undef LWIP_UDP
#define LWIP_UDP 1
/*
The STM32F4x7 allows computing and verifying the IP, UDP, TCP and ICMP checksums by hardware:
- To use this feature let the following define uncommented.
- To disable it and process by CPU comment the the checksum.
*/
#if CHECKSUM_GEN_TCP == 1
#error On STM32 TCP checksum should be in HW
#endif
#undef LWIP_IGMP
#define LWIP_IGMP 1
//#define SO_REUSE 1
//#define SO_REUSE_RXTOALL 1
#endif /* __STM32LWIPOPTS_H__ */

View File

@@ -1,7 +1,7 @@
/*
* © 2022 Paul M. Antoine
* © 2021 Chris Harlow
* © 2022 Harald Barth
* © 2022 2024 Harald Barth
* All rights reserved.
*
* This file is part of DCC++EX
@@ -23,6 +23,7 @@
#include "SerialManager.h"
#include "DCCEXParser.h"
#include "StringFormatter.h"
#include "DIAG.h"
#ifdef ARDUINO_ARCH_ESP32
#ifdef SERIAL_BT_COMMANDS
@@ -36,6 +37,10 @@ BluetoothSerial SerialBT;
#endif //COMMANDS
#endif //ESP32
static const byte PAYLOAD_FALSE = 0;
static const byte PAYLOAD_NORMAL = 1;
static const byte PAYLOAD_STRING = 2;
SerialManager * SerialManager::first=NULL;
SerialManager::SerialManager(Stream * myserial) {
@@ -43,7 +48,7 @@ SerialManager::SerialManager(Stream * myserial) {
next=first;
first=this;
bufferLength=0;
inCommandPayload=false;
inCommandPayload=PAYLOAD_FALSE;
}
void SerialManager::init() {
@@ -68,7 +73,11 @@ void SerialManager::init() {
new SerialManager(&Serial3);
#endif
#ifdef SERIAL2_COMMANDS
#ifdef ARDUINO_ARCH_ESP32
Serial2.begin(115200, SERIAL_8N1, 16, 17); // GPIO 16 RXD2; GPIO 17 TXD2 on ESP32
#else // not ESP32
Serial2.begin(115200);
#endif // ESP32
new SerialManager(&Serial2);
#endif
#ifdef SERIAL1_COMMANDS
@@ -88,7 +97,11 @@ void SerialManager::init() {
}
#endif
#ifdef SABERTOOTH
#ifdef ARDUINO_ARCH_ESP32
Serial2.begin(9600, SERIAL_8N1, 16, 17); // GPIO 16 RXD2; GPIO 17 TXD2 on ESP32
#else
Serial2.begin(9600);
#endif
#endif
}
@@ -104,23 +117,43 @@ void SerialManager::loop() {
}
void SerialManager::loop2() {
while (serial->available()) {
char ch = serial->read();
if (ch == '<') {
inCommandPayload = true;
bufferLength = 0;
buffer[0] = '\0';
}
else if (inCommandPayload) {
if (bufferLength < (COMMAND_BUFFER_SIZE-1))
buffer[bufferLength++] = ch;
while (serial->available()) {
char ch = serial->read();
if (!inCommandPayload) {
if (ch == '<') {
inCommandPayload = PAYLOAD_NORMAL;
bufferLength = 0;
buffer[0] = '\0';
}
} else { // if (inCommandPayload)
if (bufferLength < (COMMAND_BUFFER_SIZE-1)) {
buffer[bufferLength++] = ch; // advance bufferLength
if (inCommandPayload > PAYLOAD_NORMAL) {
if (inCommandPayload > 32 + 2) { // String way too long
ch = '>'; // we end this nonsense
inCommandPayload = PAYLOAD_NORMAL;
DIAG(F("Parse error: Unbalanced string"));
// fall through to ending parsing below
} else if (ch == '"') { // String end
inCommandPayload = PAYLOAD_NORMAL;
continue; // do not fall through
} else
inCommandPayload++;
}
if (inCommandPayload == PAYLOAD_NORMAL) {
if (ch == '>') {
buffer[bufferLength] = '\0';
DCCEXParser::parse(serial, buffer, NULL);
inCommandPayload = false;
buffer[bufferLength] = '\0'; // This \0 is after the '>'
DCCEXParser::parse(serial, buffer, NULL); // buffer parsed with trailing '>'
inCommandPayload = PAYLOAD_FALSE;
break;
} else if (ch == '"') {
inCommandPayload = PAYLOAD_STRING;
}
}
}
} else {
DIAG(F("Parse error: input buffer overflow"));
inCommandPayload = PAYLOAD_FALSE;
}
}
}
}

View File

@@ -44,6 +44,6 @@ private:
SerialManager * next;
byte bufferLength;
byte buffer[COMMAND_BUFFER_SIZE];
bool inCommandPayload;
byte inCommandPayload;
};
#endif

89
Stash.cpp Normal file
View File

@@ -0,0 +1,89 @@
/*
* © 2024 Chris Harlow
* All rights reserved.
*
* This file is part of DCC-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "Stash.h"
#include "StringFormatter.h"
Stash::Stash(int16_t stash_id, int16_t loco_id) {
this->stashId = stash_id;
this->locoId = loco_id;
this->next = first;
first = this;
}
void Stash::clearAll() {
for (auto s=first;s;s=s->next) {
s->locoId = 0;
s->stashId =0;
}
}
void Stash::clearAny(int16_t loco_id) {
auto lid=abs(loco_id);
for (auto s=first;s;s=s->next)
if (abs(s->locoId) == lid) {
s->locoId = 0;
s->stashId =0;
}
}
void Stash::clear(int16_t stash_id) {
set(stash_id,0);
}
int16_t Stash::get(int16_t stash_id) {
for (auto s=first;s;s=s->next)
if (s->stashId == stash_id) return s->locoId;
return 0;
}
void Stash::set(int16_t stash_id, int16_t loco_id) {
// replace any existing stash
for (auto s=first;s;s=s->next)
if (s->stashId == stash_id) {
s->locoId=loco_id;
if (loco_id==0) s->stashId=0; // recycle
return;
}
if (loco_id==0) return; // no need to create a zero entry.
// replace any empty stash
for (auto s=first;s;s=s->next)
if (s->locoId == 0) {
s->locoId=loco_id;
s->stashId=stash_id;
return;
}
// create a new stash
new Stash(stash_id, loco_id);
}
void Stash::list(Print * stream, int16_t stash_id) {
bool sent=false;
for (auto s=first;s;s=s->next)
if ((s->locoId) && (stash_id==0 || s->stashId==stash_id)) {
StringFormatter::send(stream,F("<jM %d %d>\n"),
s->stashId,s->locoId);
sent=true;
}
if (!sent) StringFormatter::send(stream,F("<jM %d 0>\n"),
stash_id);
}
Stash* Stash::first=nullptr;

39
Stash.h Normal file
View File

@@ -0,0 +1,39 @@
/*
* © 2024 Chris Harlow
* All rights reserved.
*
* This file is part of DCC-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 Stash_h
#define Stash_h
#include <Arduino.h>
class Stash {
public:
static void clear(int16_t stash_id);
static void clearAll();
static void clearAny(int16_t loco_id);
static int16_t get(int16_t stash_id);
static void set(int16_t stash_id, int16_t loco_id);
static void list(Print * stream, int16_t stash_id=0); // id0 = LIST ALL
private:
Stash(int16_t stash_id, int16_t loco_id);
static Stash* first;
Stash* next;
int16_t stashId;
int16_t locoId;
};
#endif

View File

@@ -41,5 +41,3 @@ size_t StringBuffer::write(uint8_t b) {
_buffer[_pos_write]='\0';
return 1;
}

View File

@@ -35,4 +35,4 @@ class StringBuffer : public Print {
char _buffer[buffer_max+2];
};
#endif
#endif

View File

@@ -1,5 +1,5 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2020=2025, Chris Harlow. All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -27,6 +27,9 @@ bool Diag::WIFI=false;
bool Diag::WITHROTTLE=false;
bool Diag::ETHERNET=false;
bool Diag::LCN=false;
bool Diag::RAILCOM=false;
bool Diag::WEBSOCKET=false;
void StringFormatter::diag( const FSH* input...) {
@@ -139,6 +142,7 @@ void StringFormatter::send2(Print * stream,const FSH* format, va_list args) {
case 'd': printPadded(stream,va_arg(args, int), formatWidth, formatLeft); break;
case 'u': printPadded(stream,va_arg(args, unsigned int), formatWidth, formatLeft); break;
case 'l': printPadded(stream,va_arg(args, long), formatWidth, formatLeft); break;
case 'L': stream->print(va_arg(args, unsigned long), DEC); break;
case 'b': stream->print(va_arg(args, int), BIN); break;
case 'o': stream->print(va_arg(args, int), OCT); break;
case 'x': stream->print((unsigned int)va_arg(args, unsigned int), HEX); break;

View File

@@ -1,5 +1,5 @@
/*
* © 2020, Chris Harlow. All rights reserved.
* © 2020-2025, Chris Harlow. All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -30,6 +30,8 @@ class Diag {
static bool WITHROTTLE;
static bool ETHERNET;
static bool LCN;
static bool RAILCOM;
static bool WEBSOCKET;
};

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