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

Compare commits

...

590 Commits

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

FIrst use of compile-out of unused features.
2023-10-13 13:59:06 +01:00
peteGSX
5726844c83 Merge pull request #360 from DCC-EX:fix-ifttposition
Fixed
2023-10-13 04:46:05 +10:00
peteGSX
0214a55b23 Fixed 2023-10-13 04:37:38 +10:00
Asbelos
7db4a9575a Merge branch 'master' into devel 2023-10-12 11:07:39 +01:00
Asbelos
8b8e9e4919 clean result from invalid <JR n> 2023-10-12 11:07:05 +01:00
peteGSX
ce84974967 Missed one i 2023-10-12 13:42:14 +10:00
peteGSX
034c441c34 Merge pull request #359 from DCC-EX:turntable-broadcast-I
Change broadcast
2023-10-12 13:35:55 +10:00
peteGSX
d5978b1578 Change broadcast 2023-10-12 13:28:39 +10:00
Colin Murdoch
ea4f90d5fc Merged in Power changes
Merge in power changes and EXRAIL command & update to version.h
2023-10-11 17:06:56 +01:00
Colin Murdoch
1181fd855d Merge branch 'devel-power-chm' into devel 2023-10-11 16:58:17 +01:00
Colin Murdoch
a092e06a6f Update .gitignore
added UserAddin.txt to gitignore
2023-10-10 12:11:49 +01:00
Colin Murdoch
68fd56e7fc Added returnDCAddr
Added function to return DC address
2023-10-10 11:52:46 +01:00
Asbelos
370dae0ab8 Merge branch 'master' into devel 2023-10-09 18:15:36 +01:00
Asbelos
bef4b2ec35 fix <JR> default roster 2023-10-09 18:09:48 +01:00
Colin Murdoch
fe618d0b85 Add getModeName()
Add facility to get the name of the track mode
2023-10-06 19:11:11 +01:00
pmantoine
2ff1619ad1 STM32 reinstate 100% duty cycle PWM 2023-10-04 14:54:06 +08:00
pmantoine
7afd4443d6 STM32 revised I2C clock setup 2023-10-02 12:04:47 +08:00
Colin Murdoch
52cfc18754 Remove Diags not needed
Tidy up Diags and responses - use HASH_KEYWORD in place of 'A'
2023-09-28 15:02:30 +01:00
pmantoine
ed0cfee091 STM32 DCCEXanalogWrite for TrackManager PWM 2023-09-28 17:43:22 +08:00
Colin Murdoch
25bbfa4c68 Fix <1 JOIN>
Fixed <1 JOIN> issue in TrackManager
2023-09-27 14:46:48 +01:00
Colin Murdoch
2a46b96083 Updates to power
Updates to powere routines and EXRAIL
2023-09-26 18:02:39 +01:00
Colin Murdoch
17c004aecf Code corrections
code corrections
2023-09-25 14:32:54 +01:00
Colin Murdoch
9e3ae21bb8 Change to EXRAIL Set_Power
Change to EXRAIL SET_Power
2023-09-25 09:59:17 +01:00
Harald Barth
624656ebc9 date tag 2023-09-24 20:56:27 +02:00
Harald Barth
5a7f278b1e correct return when requesting D RAM 2023-09-24 20:55:16 +02:00
Harald Barth
9333beda49 correct return when requesting D RAM 2023-09-24 20:54:17 +02:00
Colin Murdoch
aacb980dc8 Power control plus EXRAIL
Power Control <0 A> etc plus EXRAIL SET_POWER
Not yet fully tested.
2023-09-24 15:40:42 +01:00
kempe63
11b9fd4ef5 Fixed IO_PCA9555.h to work with PCA9548 mux 2023-09-23 20:25:10 +01:00
kempe63
d07718be8c Revert "Update version.h to 5.1.7"
This reverts commit d57b5ba537.
2023-09-23 20:18:59 +01:00
kempe63
d57b5ba537 Update version.h to 5.1.7 2023-09-23 20:09:01 +01:00
kempe63
dab02ec659 Update IO_PCA9555.h
Now compiles and works on PCA9548 Mux. Tested with MCP23017 and PCA9555 on the same Subbus
2023-09-23 16:10:16 +01:00
kempe63
6ad5326f1d PCA9555 compiles and tested on PCA9548 mux
IO_PCA9555.h added changes that had been applied to IO_MCP23017 for support on PCA9548 Mux. Constructor now also private and type casting of variables made the same for IO_PCA9555. Tested with MCP23017 and PCA9555 simultaneous on the same Mux Subbus
2023-09-23 16:06:14 +01:00
kempe63
39e1363ce0 Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2023-09-23 16:04:26 +01:00
kempe63
c9d4f5e94d Update IO_PCA9555.h 2023-09-23 15:56:09 +01:00
Colin Murdoch
8052090e0f Added Single Track Power On/Off
Added power On/Off <> commands
2023-09-22 17:03:40 +01:00
pmantoine
dfe3e9d42c STM32 ADCee extensions 2023-09-21 16:09:04 +08:00
pmantoine
5d810a620b STM32 variant support extension and tidy 2023-09-21 16:05:35 +08:00
pmantoine
550ad58c4d STM32 ADCee extended to support ADC2 and ADC3 2023-09-21 14:39:25 +08:00
peteGSX
7a305e179c Merge pull request #353 from DCC-EX:turntable-fix
Updated broadcasts
2023-09-15 06:32:30 +10:00
peteGSX
8437b0e7aa Updated broadcasts 2023-09-15 06:26:29 +10:00
Harald Barth
46289fa78c Check bad AT firmware version 2023-09-14 09:05:23 +02:00
pmantoine
ebbeea5fbb STM32F4xx native I2C driver merge 2023-09-13 16:46:36 +08:00
pmantoine
a8321fff42 Merge pull request #352 from DCC-EX/STM32_I2C_PMA_NEIL
Stm32 i2 c pma neil
2023-09-13 16:43:59 +08:00
peteGSX
a16790f585 Merge pull request #349 from DCC-EX:add-turntable-object
Add-turntable-object
2023-09-11 05:02:20 +10:00
peteGSX
da6a3c442f Remove redundant line 2023-09-10 18:41:14 +10:00
peteGSX
4fcd81a118 Update version 2023-09-10 07:18:54 +10:00
peteGSX
eb450dbd89 Merge branch 'devel' into add-turntable-object 2023-09-10 07:13:16 +10:00
peteGSX
a0562fdf5c Update defines to match changes in devel 2023-09-10 07:06:27 +10:00
peteGSX
7ee2c29a52 Include HAL create with EXTT_TURNTABLE 2023-09-10 05:30:48 +10:00
peteGSX
dba5d35aa2 Add response to create 2023-09-09 09:23:10 +10:00
peteGSX
be10be5a1a Added angles 2023-09-09 07:22:10 +10:00
Colin Murdoch
dca023ffd7 Update version.h
Added ONOVERLOAD and AFTEROVERLOAD as 5.1.4
2023-09-07 20:27:25 +01:00
Colin Murdoch
4eef9581fe Merge branch 'devel-plus-onoverload' into devel 2023-09-07 20:23:17 +01:00
peteGSX
bd02d1c15b WAITFORTT ready for testing 2023-09-07 07:58:19 +10:00
peteGSX
004d7b6631 JO and JP working 2023-09-07 07:32:54 +10:00
peteGSX
21ce87eb3e Descriptions available 2023-09-07 05:33:26 +10:00
Colin Murdoch
ab393047c1 Update MotorDriver.cpp
Replace duplicate call to EXRAIL with single in overload.
2023-09-06 11:20:23 +01:00
peteGSX
1f5f7754c1 Start on position description 2023-09-06 15:16:46 +10:00
peteGSX
6adff43f4b Update add position 2023-09-06 07:59:43 +10:00
Colin Murdoch
1ac104704e Update TrackManager.cpp
Reverse logic in TM::IisPowerOn()
2023-09-05 20:52:18 +01:00
Colin Murdoch
2f8e915b1e Added AFTEROVERLOAD
Added the AFTEROVERLOAD option - as yet untested.
2023-09-05 12:21:09 +01:00
peteGSX
152f9850bb Working 2023-09-05 19:05:18 +10:00
peteGSX
3094c52bf8 Ready to test 2023-09-05 08:38:37 +10:00
peteGSX
86f4567556 Revisiting logic 2023-09-04 18:46:28 +10:00
peteGSX
dd890e65bf Add move check 2023-09-04 07:38:26 +10:00
peteGSX
1e48c59cd8 Capture progress 2023-09-03 18:54:56 +10:00
peteGSX
004d10ee58 Fix build errors 2023-09-02 18:45:59 +10:00
peteGSX
e734661d1b EXRAIL ready for testing 2023-09-02 08:29:49 +10:00
peteGSX
bcb250bacf Broadcasts working 2023-09-01 18:30:02 +10:00
peteGSX
798241927f Really fix build errors 2023-09-01 13:28:24 +10:00
peteGSX
df2f09f4d2 Fix build errors 2023-09-01 09:04:48 +10:00
peteGSX
f40d57d8bd Add DCC type, EXTT broadcast from driver 2023-09-01 08:44:32 +10:00
peteGSX
9fa213e198 Undo callback 2023-08-31 13:51:25 +10:00
Harald Barth
44d8154223 version number update 2023-08-30 23:57:49 +02:00
Harald Barth
01919b33df Make parser more fool proof 2023-08-30 23:55:39 +02:00
Harald Barth
b3cafd126e sample file corrections 2023-08-30 23:26:20 +02:00
peteGSX
a0c1ad182c Start on callback 2023-08-30 19:48:30 +10:00
peteGSX
dbf053858b Undo max params 2023-08-30 13:40:09 +10:00
peteGSX
232ac993ec Separate add from create 2023-08-30 08:45:11 +10:00
Harald Barth
26ddd27ecf let user disable <D > command in favour for HAL on the Uno platform 2023-08-29 14:27:38 +02:00
peteGSX
6cad794411 Working with 15 positions 2023-08-29 19:04:45 +10:00
peteGSX
b0d8510127 Working but limited 2023-08-29 13:38:52 +10:00
peteGSX
3bfdd16288 Start on JO 2023-08-28 13:11:37 +10:00
peteGSX
df4a501e8a Writing to driver 2023-08-28 08:36:09 +10:00
peteGSX
2202cb0c5e Minor progress 2023-08-27 19:30:56 +10:00
peteGSX
1425da20b5 Correct order 2023-08-26 19:41:17 +10:00
peteGSX
b823a647ac Some progress 2023-08-26 10:26:01 +10:00
Harald Barth
2c64f10da8 Merge branch 'devel' of https://github.com/DCC-EX/CommandStation-EX into devel 2023-08-25 19:14:25 +02:00
Harald Barth
25426d076d version number update 2023-08-25 19:14:03 +02:00
Harald Barth
3453da0671 Bugfix: ESP32 30ms off time 2023-08-25 19:12:47 +02:00
Harald Barth
c55fa9f9d2 version number update 2023-08-25 19:08:58 +02:00
Harald Barth
210d96a3e3 Bugfix: ESP32 30ms off time 2023-08-25 19:07:57 +02:00
Colin Murdoch
fb226311e5 Update myHal.cpp_example.txt
Added missing ::create to LiquidCrystal HAL definition
2023-08-24 14:54:33 +01:00
Colin Murdoch
6392c74ead Update myHal.cpp_example.txt
Added missing ::create to LiquidCrystal
2023-08-24 14:53:01 +01:00
Harald Barth
25f8852af6 call devel for 5.1.X version number update 2023-08-24 10:09:38 +02:00
Harald Barth
9842ea8a42 Bugfix: execute 30ms off time before rejoin 2023-08-24 10:07:25 +02:00
peteGSX
fa0aa27d46 Add OPCODE list to DCCEXParser.cpp 2023-08-24 10:07:15 +02:00
Harald Barth
42f3c7c128 version number update 2023-08-24 10:05:31 +02:00
Harald Barth
6cd7002e91 Bugfix: execute 30ms off time before rejoin 2023-08-24 10:03:29 +02:00
peteGSX
57d4655d54 Fix Uno/Nano build errors 2023-08-24 07:22:37 +10:00
peteGSX
ff9c558b61 Not much progress 2023-08-23 19:08:04 +10:00
peteGSX
b277d204f0 Progress! 2023-08-22 19:30:22 +10:00
peteGSX
c4febd1d0f More parser 2023-08-21 19:33:45 +10:00
peteGSX
98f8022268 Fix device driver, disable objects, start parser 2023-08-21 06:43:06 +10:00
peteGSX
1491da4813 Starting, very broken 2023-08-20 19:26:04 +10:00
kempe63
4b2c0702a4 Update version.h
Update version.h for IO_PCA9555 changes
2023-08-18 11:40:34 +01:00
kempe63
e27cceeb74 Update PCA9555.h inconsistencies to IO_MCP23017.h causing IO_PCA9555.h compile error when configure for Mux
Updated Class PCA9555 definition reflecting changes in IO_MCP23017.h to support PCA9548 mux. Checked with PCA9555 base board, compiles and run EXRAIL script with  output driver
2023-08-18 11:30:37 +01:00
peteGSX
085762e800 Add OPCODE list to DCCEXParser.cpp 2023-08-18 18:52:34 +10:00
Colin Murdoch
247763ac00 Code Corrections
Code corrections
2023-08-12 19:10:35 +01:00
Colin Murdoch
e327e0ae8d Added ONOVERLOAD
Added code changes to create ONOVERLOAD command in EXRAIL
2023-08-12 18:40:48 +01:00
Harald Barth
9f38dae8ba Check bad AT firmware version 2023-08-11 00:07:02 +02:00
pmantoine
e51f8e9c0a STM32 I2C Clock selection for 100/400KHz 2023-04-11 15:48:35 +08:00
Neil McKechnie
4f43a413b5 Update I2CManager_STM32.h
Remove debug code (writing to pin D2).  Update comments.  Restructure.
2023-03-30 18:30:38 +01:00
Neil McKechnie
4f56837d28 Fixes to timeout handling (due to STM32 micros() difference). 2023-03-28 18:07:52 +01:00
Neil McKechnie
cc2846d932 STM32 Native I2C first working version
Working for reads and writes, needs more testing and perhaps a polish.
2023-03-27 00:20:59 +01:00
pmantoine
83325ebf78 Initial I2C native driver 2023-03-23 08:44:25 +11:00
160 changed files with 19072 additions and 2130 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.

5
.gitignore vendored
View File

@@ -13,3 +13,8 @@ myFilter.cpp
my*.h
!my*.example.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
@@ -105,6 +128,7 @@ void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream
void CommandDistributor::forget(byte clientId) {
if (clients[clientId]==WITHROTTLE_TYPE) WiThrottle::forget(clientId);
clients[clientId]=NONE_TYPE;
if (virtualLCDClient==clientId) virtualLCDClient=RingStream::NO_CLIENT;
}
#endif
@@ -130,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();
@@ -161,6 +185,10 @@ void CommandDistributor::broadcastTurnout(int16_t id, bool isClosed ) {
#endif
}
void CommandDistributor::broadcastTurntable(int16_t id, uint8_t position, bool moving) {
broadcastReply(COMMAND_TYPE, F("<I %d %d %d>\n"), id, position, moving);
}
void CommandDistributor::broadcastClockTime(int16_t time, int8_t rate) {
// The JMRI clock command is of the form : PFT65871<;>4
// The CS broadcast is of the form "<jC mmmm nn" where mmmm is time minutes and dd speed
@@ -180,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;
@@ -202,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;
@@ -243,28 +280,132 @@ 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++)
if (TrackManager::getPower(t, pstr))
broadcastReply(COMMAND_TYPE, F("<p%s>\n"),pstr);
byte trackcount=0;
byte oncount=0;
byte offcount=0;
for(byte t=0; t<TrackManager::MAX_TRACKS; t++) {
if (TrackManager::isActive(t)) {
trackcount++;
// do not call getPower(t) unless isActive(t)!
if (TrackManager::getPower(t) == POWERMODE::ON)
oncount++;
else
offcount++;
}
}
//DIAG(F("t=%d on=%d off=%d"), trackcount, oncount, offcount);
char state='2';
if (oncount==0 || offcount == trackcount)
state = '0';
else if (oncount == trackcount) {
state = '1';
}
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;
bool join=TrackManager::isJoined();
//DIAG(F("m=%d p=%d j=%d"), main, prog, join);
const FSH * reason=F("");
char state='1';
if (main && prog && join) reason=F(" JOIN");
else if (main && prog);
else if (main) reason=F(" MAIN");
else if (prog) reason=F(" PROG");
else state='0';
broadcastReply(COMMAND_TYPE, F("<p%c%S>\n"),state,reason);
if (join) {
reason = F(" JOIN"); // with space at start so we can append without space
broadcastReply(COMMAND_TYPE, F("<p1%S>\n"),reason);
} else {
if (main) {
//reason = F("MAIN");
broadcastReply(COMMAND_TYPE, F("<p1 MAIN>\n"));
}
if (prog) {
//reason = F("PROG");
broadcastReply(COMMAND_TYPE, F("<p1 PROG>\n"));
}
}
#ifdef CD_HANDLE_RING
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1':'0');
// send '1' if all main are on, otherwise global state (which in that case is '0' or '2')
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1': state);
#endif
LCD(2,F("Power %S%S"),state=='1'?F("On"):F("Off"),reason);
LCD(2,F("Power %S%S"),state=='1'?F("On"): ( state=='0'? F("Off") : F("SC") ),reason);
}
void CommandDistributor::broadcastRaw(clientType type, char * msg) {
broadcastReply(type, F("%s"),msg);
}
void CommandDistributor::broadcastTrackState(const FSH* format,byte trackLetter,int16_t dcAddr) {
broadcastReply(COMMAND_TYPE, format,trackLetter,dcAddr);
void CommandDistributor::broadcastMessage(char * message) {
broadcastReply(COMMAND_TYPE, F("<m \"%s\">\n"),message);
broadcastReply(WITHROTTLE_TYPE, F("Hm%s\n"),message);
}
void CommandDistributor::broadcastTrackState(const FSH* format, byte trackLetter, const FSH *modename, int16_t dcAddr) {
broadcastReply(COMMAND_TYPE, format, trackLetter, modename, dcAddr);
}
void CommandDistributor::broadcastRouteState(int16_t routeId, byte state ) {
broadcastReply(COMMAND_TYPE, F("<jB %d %d>\n"),routeId,state);
}
void CommandDistributor::broadcastRouteCaption(int16_t routeId, const FSH* caption ) {
broadcastReply(COMMAND_TYPE, F("<jB %d \"%S\">\n"),routeId,caption);
}
Print * CommandDistributor::getVirtualLCDSerial(byte screen, byte row) {
Print * stream=virtualLCDSerial;
#ifdef CD_HANDLE_RING
rememberVLCDClient=RingStream::NO_CLIENT;
if (!stream && virtualLCDClient!=RingStream::NO_CLIENT) {
// If we are broadcasting from a wifi/eth process we need to complete its output
// before merging broadcasts in the ring, then reinstate it in case
// the process continues to output to its client.
if ((rememberVLCDClient = ring->peekTargetMark()) != RingStream::NO_CLIENT) {
ring->commit();
}
ring->mark(virtualLCDClient);
stream=ring;
}
#endif
if (stream) StringFormatter::send(stream,F("<@ %d %d \""), screen,row);
return stream;
}
void CommandDistributor::commitVirtualLCDSerial() {
#ifdef CD_HANDLE_RING
if (virtualLCDClient!=RingStream::NO_CLIENT) {
StringFormatter::send(ring,F("\">\n"));
ring->commit();
if (rememberVLCDClient!=RingStream::NO_CLIENT) ring->mark(rememberVLCDClient);
return;
}
#endif
StringFormatter::send(virtualLCDSerial,F("\">\n"));
}
void CommandDistributor::setVirtualLCDSerial(Print * stream) {
#ifdef CD_HANDLE_RING
virtualLCDClient=RingStream::NO_CLIENT;
if (stream && stream->availableForWrite()==RingStream::THIS_IS_A_RINGSTREAM) {
virtualLCDClient=((RingStream *) stream)->peekTargetMark();
virtualLCDSerial=nullptr;
return;
}
#endif
virtualLCDSerial=stream;
}
Print* CommandDistributor::virtualLCDSerial=&USB_SERIAL;
byte CommandDistributor::virtualLCDClient=0xFF;
byte CommandDistributor::rememberVLCDClient=0;

View File

@@ -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,28 +37,41 @@
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);
static void broadcastClockTime(int16_t time, int8_t rate);
static void setClockTime(int16_t time, int8_t rate, byte opt);
static int16_t retClockTime();
static void broadcastPower();
static void broadcastRaw(clientType type,char * msg);
static void broadcastTrackState(const FSH* format,byte trackLetter,int16_t dcAddr);
static void broadcastTrackState(const FSH* format,byte trackLetter, const FSH* modename, int16_t dcAddr);
template<typename... Targs> static void broadcastReply(clientType type, Targs... msg);
static void forget(byte clientId);
static void broadcastRouteState(int16_t routeId,byte state);
static void broadcastRouteCaption(int16_t routeId,const FSH * caption);
static void broadcastMessage(char * message);
// Handling code for virtual LCD receiver.
static Print * getVirtualLCDSerial(byte screen, byte row);
static void commitVirtualLCDSerial();
static void setVirtualLCDSerial(Print * stream);
private:
static Print * virtualLCDSerial;
static byte virtualLCDClient;
static byte rememberVLCDClient;
};
#endif

View File

@@ -31,6 +31,7 @@
* © 2020-2021 Chris Harlow, Harald Barth, David Cutting,
* Fred Decker, Gregor Baues, Anthony W - Dayton
* © 2023 Nathan Kellenicki
* © 2025 Herb Morton
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -51,6 +52,12 @@
#include "DCCEX.h"
#include "Display_Implementation.h"
#ifdef ARDUINO_ARCH_ESP32
#include "Sniffer.h"
#include "DCCDecoder.h"
Sniffer *dccSniffer = NULL;
bool DCCDecoder::active = false;
#endif // ARDUINO_ARCH_ESP32
#ifdef CPU_TYPE_ERROR
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN defines.h
@@ -65,6 +72,9 @@
#ifdef EXRAIL_WARNING
#warning You have myAutomation.h but your hardware has not enough memory to do that, so EX-RAIL DISABLED
#endif
// compile time check, passwords 1 to 7 chars do not work, so do not try to compile with them at all
// remember trailing '\0', sizeof("") == 1.
#define PASSWDCHECK(S) static_assert(sizeof(S) == 1 || sizeof(S) > 8, "Password shorter than 8 chars")
void setup()
{
@@ -76,6 +86,12 @@ void setup()
DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com"));
// If user has defined a startup delay, delay here before starting IO
#if defined(STARTUP_DELAY)
DIAG(F("Delaying startup for %dms"), STARTUP_DELAY);
delay(STARTUP_DELAY);
#endif
// Initialise HAL layer before reading EEprom or setting up MotorDrivers
IODevice::begin();
@@ -87,7 +103,7 @@ void setup()
DISPLAY_START (
// This block is still executed for DIAGS if display not in use
LCD(0,F("DCC-EX v%S"),F(VERSION));
LCD(0,F("DCC-EX v" VERSION));
LCD(1,F("Lic GPLv3"));
);
@@ -96,11 +112,14 @@ void setup()
// Start Ethernet if it exists
#ifndef ARDUINO_ARCH_ESP32
#if WIFI_ON
PASSWDCHECK(WIFI_PASSWORD); // compile time check
WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
#endif // WIFI_ON
#else
// ESP32 needs wifi on always
#if WIFI_ON
PASSWDCHECK(WIFI_PASSWORD); // compile time check
WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL, WIFI_FORCE_AP);
#endif // WIFI_ON
#endif // ARDUINO_ARCH_ESP32
#if ETHERNET_ON
@@ -113,6 +132,11 @@ void setup()
// Start RMFT aka EX-RAIL (ignored if no automnation)
RMFT::begin();
#ifdef ARDUINO_ARCH_ESP32
#ifdef BOOSTER_INPUT
dccSniffer = new Sniffer(BOOSTER_INPUT);
#endif // BOOSTER_INPUT
#endif // ARDUINO_ARCH_ESP32
// Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
// This can be used to create turnouts, outputs, sensors etc. through the normal text commands.
@@ -132,24 +156,47 @@ void setup()
void loop()
{
#ifdef ARDUINO_ARCH_ESP32
#ifdef BOOSTER_INPUT
static bool oldactive = false;
if (dccSniffer) {
bool newactive = dccSniffer->inputActive();
if (oldactive != newactive) {
RMFT2::railsyncEvent(newactive);
oldactive = newactive;
}
DCCPacket p = dccSniffer->fetchPacket();
if (p.len() != 0) {
if (DCCDecoder::parse(p)) {
if (Diag::SNIFFER)
p.print();
}
}
}
#endif // BOOSTER_INPUT
#endif // ARDUINO_ARCH_ESP32
// The main sketch has responsibilities during 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
#if WIFI_ON
#ifndef WIFI_TASK_ON_CORE0
WifiESP::loop();
#endif
#endif //WIFI_ON
#endif //ARDUINO_ARCH_ESP32
#if ETHERNET_ON
EthernetInterface::loop();

705
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) {
void DCC::setFunctionInternal(int cab, byte group, byte byte1, byte byte2) {
// DIAG(F("setFunctionInternal %d %x %x"),cab,byte1,byte2);
byte b[4];
byte nB = 0;
@@ -132,33 +172,50 @@ void DCC::setFunctionInternal(int cab, byte byte1, byte byte2) {
b[nB++] = lowByte(cab);
if (byte1!=0) b[nB++] = byte1;
b[nB++] = byte2;
DCCWaveform::mainTrack.schedulePacket(b, nB, 0);
DCCQueue::scheduleDCCFunctionPacket(b, nB, cab,group);
}
// 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
uint8_t DCC::getThrottleFrequency(int cab) {
#if defined(ARDUINO_AVR_UNO)
(void)cab;
return 0;
#else
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)(slot->functions >>29);
//DIAG(F("Speed table %d functions %l shifted %d"), reg, slot->functions, res);
return res;
#endif
}
// 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
@@ -182,47 +239,50 @@ 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);
return true;
DCCQueue::scheduleDCCPacket(b, nB, 4,cab);
}
int reg = lookupSpeedTable(cab);
if (reg<0) return false;
// We use the reminder table up to 28 for normal functions.
// We use 29 to 31 for DC frequency as well so up to 28
// are "real" functions and 29 to 31 are frequency bits
// controlled by function buttons
if (functionNumber > 31)
return true;
LOCO * slot = lookupSpeedTable(cab);
// Take care of functions:
// Set state of function
unsigned long previous=speedTable[reg].functions;
unsigned long funcmask = (1UL<<functionNumber);
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) {
updateGroupflags(speedTable[reg].groupFlags, functionNumber);
CommandDistributor::broadcastLoco(reg);
if (slot->functions != previous) {
if (functionNumber <= 28)
updateGroupflags(slot->groupFlags, functionNumber);
CommandDistributor::broadcastLoco(slot);
}
return true;
}
// Flip function state
// Flip function state (used from withrottle protocol)
void DCC::changeFn( int cab, int16_t functionNumber) {
if (cab<=0 || functionNumber>28) return;
int reg = lookupSpeedTable(cab);
if (reg<0) return;
unsigned long funcmask = (1UL<<functionNumber);
speedTable[reg].functions ^= funcmask;
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);
}
int DCC::getFn( int cab, int16_t functionNumber) {
if (cab<=0 || functionNumber>28) return -1; // unknown
int reg = lookupSpeedTable(cab);
if (reg<0) return -1;
// Report function state (used from withrottle protocol)
// returns 0 false, 1 true or -1 for do not know
int8_t DCC::getFn( int cab, int16_t functionNumber) {
if (cab<=0 || functionNumber>31)
return -1; // unknown
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.
@@ -239,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*/) {
@@ -253,7 +327,7 @@ void DCC::setAccessory(int address, byte port, bool gate, byte onoff /*= 2*/) {
// the initial decoders were orgnized and that influenced how the DCC
// standard was made.
#ifdef DIAG_IO
DIAG(F("DCC::setAccessory(%d,%d,%d)"), address, port, gate);
DIAG(F("DCC::setAccessory(%d,%d,%d,%d)"), address, port, gate, onoff);
#endif
// use masks to detect wrong values and do nothing
if(address != (address & 511))
@@ -266,16 +340,68 @@ 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) {
/* From https://www.nmra.org/sites/default/files/s-9.2.1_2012_07.pdf
The Extended Accessory Decoder Control Packet is included for the purpose of transmitting aspect control to signal
decoders or data bytes to more complex accessory decoders. Each signal head can display one aspect at a time.
{preamble} 0 10AAAAAA 0 0AAA0AA1 0 000XXXXX 0 EEEEEEEE 1
XXXXX is for a single head. A value of 00000 for XXXXX indicates the absolute stop aspect. All other aspects
represented by the values for XXXXX are determined by the signaling system used and the prototype being
modeled.
From https://normen.railcommunity.de/RCN-213.pdf:
More information is in RCN-213 about how the address bits are organized.
preamble -0- 1 0 A7 A6 A5 A4 A3 A2 -0- 0 ^A10 ^A9 ^A8 0 A1 A0 1 -0- ....
Thus in byte packet form the format is 10AAAAAA, 0AAA0AA1, 000XXXXX
Die Adresse f<>r den ersten erweiterten Zubeh<65>rdecoder ist wie bei den einfachen
Zubeh<EFBFBD>rdecodern die Adresse 4 = 1000-0001 0111-0001 . Diese Adresse wird in
Anwenderdialogen als Adresse 1 dargestellt.
This means that the first address shown to the user as "1" is mapped
to internal address 4.
Note that the Basic accessory format mentions "By convention these
bits (bits 4-6 of the second data byte) are in ones complement" but
this note is absent from the advanced packet description. The
english translation does not mention that the address format for
the advanced packet follows the one for the basic packet but
according to the RCN-213 this is the case.
We allow for addresses from -3 to 2047-3 as that allows to address the
whole range of the 11 bits sent to track.
*/
if ((address > 2044) || (address < -3)) return false; // 2047-3, 11 bits but offset 3
if (value != (value & 0x1F)) return false; // 5 bits
address+=3; // +3 offset according to RCN-213
byte b[3];
b[0]= 0x80 // bits always on
| ((address>>2) & 0x3F); // shift out 2, mask out used bits
b[1]= 0x01 // bits always on
| (((~(address>>8)) & 0x07)<<4) // shift out 8, invert, mask 3 bits, shift up 4
| ((address & 0x03)<<1); // mask 2 bits, shift up 1
b[2]=value;
DCCQueue::scheduleDCCPacket(b, sizeof(b), repeats);
return true;
}
//
@@ -293,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);
}
//
@@ -314,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() {
@@ -421,6 +604,37 @@ const ackOp FLASH READ_CV_PROG[] = {
const ackOp FLASH LOCO_ID_PROG[] = {
BASELINE,
// first check cv20 for extended addressing
SETCV, (ackOp)20, // CV 19 is extended
SETBYTE, (ackOp)0,
VB, WACK, ITSKIP, // skip past extended section if cv20 is zero
// read cv20 and 19 and merge
STARTMERGE, // Setup to read cv 20
V0, WACK, MERGE,
V0, WACK, MERGE,
V0, WACK, MERGE,
V0, WACK, MERGE,
V0, WACK, MERGE,
V0, WACK, MERGE,
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
V0, WACK, MERGE,
V0, WACK, MERGE,
V0, WACK, MERGE,
V0, WACK, MERGE,
V0, WACK, MERGE,
V0, WACK, MERGE,
V0, WACK, MERGE,
V0, WACK, MERGE,
VB, WACK, NAKFAIL, // cant recover if cv 19 unreadable
COMBINE1920, // Combile byte with stash and callback
// end of advanced 20,19 check
SKIPTARGET,
SETCV, (ackOp)19, // CV 19 is consist setting
SETBYTE, (ackOp)0,
VB, WACK, ITSKIP, // ignore consist if cv19 is zero (no consist)
@@ -487,6 +701,10 @@ const ackOp FLASH LOCO_ID_PROG[] = {
const ackOp FLASH SHORT_LOCO_ID_PROG[] = {
BASELINE,
// Clear consist CV 19,20
SETCV,(ackOp)20,
SETBYTE, (ackOp)0,
WB,WACK, // ignore dedcoder without cv20 support
SETCV,(ackOp)19,
SETBYTE, (ackOp)0,
WB,WACK, // ignore dedcoder without cv19 support
@@ -502,9 +720,27 @@ 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
// Clear consist CV 19,20
SETCV,(ackOp)20,
SETBYTE, (ackOp)0,
WB,WACK, // ignore dedcoder without cv20 support
SETCV,(ackOp)19,
SETBYTE, (ackOp)0,
WB,WACK, // ignore decoder without cv19 support
@@ -573,79 +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.getPacketPending()) 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)
setFunctionInternal(loco,0, 128 | ((functions>>1)& 0x0F) | ((functions & 0x01)<<4)); // 100D DDDD
if (flags & FN_GROUP_1) {
setFunctionInternal(loco,1,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)
setFunctionInternal(loco,0, 176 | ((functions>>5)& 0x0F)); // 1011 DDDD
case 3: // remind function group 2 F5-F8
if (flags & FN_GROUP_2) {
setFunctionInternal(loco,2,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)
setFunctionInternal(loco,0, 160 | ((functions>>9)& 0x0F)); // 1010 DDDD
case 5: // remind function group 3 F9-F12
if (flags & FN_GROUP_3) {
setFunctionInternal(loco,3,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));
flags&= ~FN_GROUP_4; // dont send them again
case 7: // remind function group 4 F13-F20
if (flags & FN_GROUP_4) {
setFunctionInternal(loco,4,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));
flags&= ~FN_GROUP_5; // dont send them again
case 9: // remind function group 5 F21-F28
if (flags & FN_GROUP_5) {
setFunctionInternal(loco,5,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
}
@@ -662,70 +995,132 @@ 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) {
// avoid unused warnings when EXRAIL not active
(void)loco; (void)blockid; (void)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) {
(void)blockid; // avoid unused warning when EXRAIL not active
// clear block occupied by loco, 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
}

43
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,18 +59,25 @@ 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 int getFn(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);
static bool writeTextPacket(byte *b, int nBytes);
// ACKable progtrack calls bitresults callback 0,0 or -1, cv returns value or -1
@@ -80,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
@@ -98,21 +107,32 @@ public:
int loco;
byte speedCode;
byte groupFlags;
unsigned long functions;
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);
static bool issueReminder(int reg);
static int lastLocoReminder;
static int highestUsedReg;
static void setFunctionInternal(int cab, byte group, byte fByte, byte eByte);
static bool issueReminder(LOCO * slot);
static LOCO* nextLocoReminder;
static FSH *shieldName;
static byte globalSpeedsteps;
@@ -123,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.
@@ -314,6 +322,14 @@ void DCCACK::loop() {
callback( LONG_ADDR_MARKER | ( ackManagerByte + ((ackManagerStash - 192) << 8)));
return;
case COMBINE1920:
// ackManagerStash is cv20, ackManagerByte is CV 19
// This will not be called if cv20==0
ackManagerByte &= 0x7F; // ignore direction marker
ackManagerByte %=100; // take last 2 decimal digits
callback( ackManagerStash*100+ackManagerByte);
return;
case ITSKIP:
if (!ackReceived) break;
// SKIP opcodes until SKIPTARGET found
@@ -322,6 +338,29 @@ void DCCACK::loop() {
opcode=GETFLASH(ackManagerProg);
}
break;
case NAKSKIP:
if (ackReceived) break;
// SKIP opcodes until SKIPTARGET found
while (opcode!=SKIPTARGET) {
ackManagerProg++;
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:
@@ -351,7 +390,7 @@ void DCCACK::callback(int value) {
switch (callbackState) {
case AFTER_READ:
if (ackManagerRejoin && autoPowerOff) {
if (ackManagerRejoin && !autoPowerOff) {
progDriver->setPower(POWERMODE::OFF);
callbackStart=millis();
callbackState=WAITING_30;
@@ -466,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

@@ -56,6 +56,10 @@ enum ackOp : byte
STASHLOCOID, // keeps current byte value for later
COMBINELOCOID, // combines current value with stashed value and returns it
ITSKIP, // skip to SKIPTARGET if ack true
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
};
@@ -77,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;
}
@@ -124,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;

178
DCCDecoder.cpp Normal file
View File

@@ -0,0 +1,178 @@
/*
* © 2025 Harald Barth
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifdef ARDUINO_ARCH_ESP32
#include "DCCDecoder.h"
#include "LocoTable.h"
#include "DCCEXParser.h"
#include "DIAG.h"
#include "DCC.h"
bool DCCDecoder::parse(DCCPacket &p) {
if (!active)
return false;
const byte DECODER_MOBILE = 1;
const byte DECODER_ACCESSORY = 2;
byte decoderType = 0; // use 0 as none
byte *d = p.data();
byte *instr = 0; // will be set to point to the instruction part of the DCC packet (instr[0] to instr[n])
uint16_t addr; // will be set to decoder addr (long/shor mobile or accessory)
bool locoInfoChanged = false;
if (d[0] == 0B11111111) { // Idle packet
return false;
}
// CRC verification here
byte checksum = 0;
for (byte n = 0; n < p.len(); n++)
checksum ^= d[n];
if (checksum) { // Result should be zero, if not it's an error!
if (Diag::SNIFFER) {
DIAG(F("Checksum error:"));
p.print();
}
return false;
}
/*
Serial.print("< ");
for(int n=0; n<8; n++) {
Serial.print(d[0]&(1<<n)?"1":"0");
}
Serial.println(" >");
*/
if (bitRead(d[0],7) == 0) { // bit7 == 0 => loco short addr
decoderType = DECODER_MOBILE;
instr = d+1;
addr = d[0];
} else {
if (bitRead(d[0],6) == 1) { // bit7 == 1 and bit6 == 1 => loco long addr
decoderType = DECODER_MOBILE;
instr = d+2;
addr = 256 * (d[0] & 0B00111111) + d[1];
} else { // bit7 == 1 and bit 6 == 0
decoderType = DECODER_ACCESSORY;
instr = d+1;
addr = d[0] & 0B00111111;
}
}
if (decoderType == DECODER_MOBILE) {
switch (instr[0] & 0xE0) {
case 0x20: // 001x-xxxx Extended commands
if (instr[0] == 0B00111111) { // 128 speed steps
if ((locoInfoChanged = LocoTable::updateLoco(addr, instr[1])) == true) {
byte speed = instr[1] & 0B01111111;
byte direction = instr[1] & 0B10000000;
DCC::setThrottle(addr, speed, direction);
//DIAG(F("UPDATE"));
// send speed change to DCCEX here
}
}
break;
case 0x40: // 010x-xxxx 28 (or 14 step) speed we assume 28
case 0x60: // 011x-xxxx
if ((locoInfoChanged = LocoTable::updateLoco(addr, instr[0] & 0B00111111)) == true) {
byte speed = instr[0] & 0B00001111; // first only look at 4 bits
if (speed > 1) { // neither stop nor emergency stop, recalculate speed
speed = ((instr[0] & 0B00001111) << 1) + bitRead(instr[0], 4); // reshuffle bits
speed = (speed - 3) * 9/2;
}
byte direction = instr[0] & 0B00100000;
DCC::setThrottle(addr, speed, direction);
}
break;
case 0x80: // 100x-xxxx Function group 1
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 1)) == true) {
byte normalized = (instr[0] << 1 & 0x1e) | (instr[0] >> 4 & 0x01);
DCCEXParser::funcmap(addr, normalized, 0, 4);
}
break;
case 0xA0: // 101x-xxxx Function group 3 and 2
{
byte low, high;
if (bitRead(instr[0], 4)) {
low = 5;
high = 8;
} else {
low = 9;
high = 12;
}
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], low)) == true) {
DCCEXParser::funcmap(addr, instr[0], low, high);
}
}
break;
case 0xC0: // 110x-xxxx Extended (here are functions F13 and up
switch (instr[0] & 0B00011111) {
case 0B00011110: // F13-F20 Function Control
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 13)) == true) {
DCCEXParser::funcmap(addr, instr[1], 13, 20);
}
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[0], 17)) == true) {
DCCEXParser::funcmap(addr, instr[1], 13, 20);
}
break;
case 0B00011111: // F21-F28 Function Control
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[1], 21)) == true) {
DCCEXParser::funcmap(addr, instr[1], 21, 28);
} // updateFunc handles only the 4 low bits as that is the most common case
if ((locoInfoChanged = LocoTable::updateFunc(addr, instr[1]>>4, 25)) == true) {
DCCEXParser::funcmap(addr, instr[1], 21, 28);
}
break;
/* do that later
case 0B00011000: // F29-F36 Function Control
break;
case 0B00011001: // F37-F44 Function Control
break;
case 0B00011010: // F45-F52 Function Control
break;
case 0B00011011: // F53-F60 Function Control
break;
case 0B00011100: // F61-F68 Function Control
break;
*/
}
break;
case 0xE0: // 111x-xxxx Config vars
break;
}
return locoInfoChanged;
}
if (decoderType == DECODER_ACCESSORY) {
if (instr[0] & 0B10000000) { // Basic Accessory
addr = (((~instr[0]) & 0B01110000) << 2) + addr;
byte port = (instr[0] & 0B00000110) >> 1;
byte activate = (instr[0] & 0B00001000) >> 3;
byte coil = (instr[0] & 0B00000001);
locoInfoChanged = true;
//(void)addr; (void)port; (void)coil; (void)activate;
//DIAG(F("HL=%d LL=%d C=%d A=%d"), addr, port, coil, activate);
DCC::setAccessory(addr, port, coil, activate);
} else { // Accessory Extended NMRA spec, do we need to decode this?
/*
addr = (addr << 5) +
((instr[0] & 0B01110000) >> 2) +
((instr[0] & 0B00000110) >> 1);
*/
}
return locoInfoChanged;
}
return false;
}
#endif // ARDUINO_ARCH_ESP32

30
DCCDecoder.h Normal file
View File

@@ -0,0 +1,30 @@
/*
* © 2025 Harald Barth
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifdef ARDUINO_ARCH_ESP32
#include <Arduino.h>
#include "DCCPacket.h"
class DCCDecoder {
public:
static bool parse(DCCPacket &p);
static inline void onoff(bool on) {active = on;};
private:
static bool active;
};
#endif // ARDUINO_ARCH_ESP32

View File

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

File diff suppressed because it is too large Load Diff

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
@@ -24,6 +24,7 @@
#include <Arduino.h>
#include "FSH.h"
#include "RingStream.h"
#include "defines.h"
typedef void (*FILTER_CALLBACK)(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
typedef void (*AT_COMMAND_CALLBACK)(HardwareSerial * stream,const byte * command);
@@ -36,22 +37,29 @@ 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
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
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[]);
static bool parseS(Print * stream, int16_t params, int16_t p[]);
static bool parsef(Print * stream, int16_t params, int16_t p[]);
static bool parseD(Print * stream, int16_t params, int16_t p[]);
static bool parseZ(Print * stream, int16_t params, int16_t p[]);
static bool parseS(Print * stream, int16_t params, int16_t p[]);
static bool parsef(Print * stream, int16_t params, int16_t p[]);
static bool parseC(Print * stream, int16_t params, int16_t p[]);
static bool parseD(Print * stream, int16_t params, int16_t p[]);
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
static Print * getAsyncReplyStream();
static void commitAsyncReplyStream();
static Print * getAsyncReplyStream();
static void commitAsyncReplyStream();
static bool stashBusy;
static byte stashTarget;
@@ -63,15 +71,17 @@ 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[]);
};

83
DCCPacket.h Normal file
View File

@@ -0,0 +1,83 @@
/*
* © 2025 Harald Barth
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#ifndef DCCPacket_h
#define DCCPacket_h
#include <strings.h>
#include "defines.h"
class DCCPacket {
public:
DCCPacket() {
_len = 0;
_data = NULL;
};
DCCPacket(byte *d, byte l) {
_len = l;
_data = new byte[_len];
for (byte n = 0; n<_len; n++)
_data[n] = d[n];
};
DCCPacket(const DCCPacket &old) {
_len = old._len;
_data = new byte[_len];
for (byte n = 0; n<_len; n++)
_data[n] = old._data[n];
};
DCCPacket &operator=(const DCCPacket &rhs) {
if (this == &rhs)
return *this;
delete[]_data;
_len = rhs._len;
_data = new byte[_len];
for (byte n = 0; n<_len; n++)
_data[n] = rhs._data[n];
return *this;
};
~DCCPacket() {
if (_len) {
delete[]_data;
_len = 0;
_data = NULL;
}
};
inline bool operator==(const DCCPacket &right) {
if (_len != right._len)
return false;
if (_len == 0)
return true;
return (bcmp(_data, right._data, _len) == 0);
};
void print() {
static const char hexchars[]="0123456789ABCDEF";
USB_SERIAL.print(F("<* DCCPACKET "));
for (byte n = 0; n< _len; n++) {
USB_SERIAL.print(hexchars[_data[n]>>4]);
USB_SERIAL.print(hexchars[_data[n] & 0x0f]);
USB_SERIAL.print(' ');
}
USB_SERIAL.print(F("*>\n"));
};
inline byte len() {return _len;};
inline byte *data() {return _data;};
private:
byte _len = 0;
byte *_data = NULL;
};
#endif

257
DCCQueue.cpp Normal file
View File

@@ -0,0 +1,257 @@
/*
* © 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;
}
void DCCQueue::remove(PendingSlot* premove) {
PendingSlot* previous=nullptr;
for (auto p=head;p;previous=p,p=p->next) {
if (p==premove) {
// remove this slot from the queue
if (previous) previous->next=p->next;
else head=p->next;
if (p==tail) tail=previous; // if last packet, update tail
return;
}
}
DIAG(F("DCCQueue::remove slot not found"));
}
// 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
memcpy(p->packet,packet,length);
p->packetLength=length;
p->packetRepeat=repeats;
return;
}
}
highPriorityQueue->addQueue(getSlot(SPEED_PACKET,packet,length,repeats,loco));
}
// Packet replaces existing loco function packet or joins end of high priority queue.
void DCCQueue::scheduleDCCFunctionPacket(byte* packet, byte length, uint16_t loco, byte group) {
PendingType type=DEAD_PACKET;
switch(group) {
case 1: type=FUNCTION1_PACKET; break;
case 2: type=FUNCTION2_PACKET; break;
case 3: type=FUNCTION3_PACKET; break;
case 4: type=FUNCTION4_PACKET; break;
case 5: type=FUNCTION5_PACKET; break;
default:
DIAG(F("DCCQueue::scheduleDCCFunctionPacket invalid group %d"),group);
return; // invalid group
}
for (auto p=lowPriorityQueue->head;p;p=p->next) {
if (p->locoId==loco && p->type==type) {
// replace existing packet for same loco and function group
memcpy(p->packet,packet,length);
p->packetLength=length;
p->packetRepeat=0;
return;
}
}
lowPriorityQueue->addQueue(getSlot(type,packet,length,0,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 * pNext=nullptr;
for (auto p=highPriorityQueue->head;p;p=pNext) {
pNext=p->next; // save next packet in case we recycle this one
if (p->type!=ACC_OFF_PACKET && (loco==0 || p->locoId==loco)) {
// remove this slot from the queue or it will interfere with our ESTOP
highPriorityQueue->remove(p);
recycle(p); // recycle this slot
}
}
// add the estop packet to the start of the queue
highPriorityQueue->jumpQueue(getSlot(SPEED_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() {
for (auto p=head;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
remove(p);
// 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);
}
else 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"),
(int16_t)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;
}

88
DCCQueue.h Normal file
View File

@@ -0,0 +1,88 @@
/*
* © 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,
FUNCTION1_PACKET, FUNCTION2_PACKET, FUNCTION3_PACKET, FUNCTION4_PACKET, FUNCTION5_PACKET,
SPEED_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);
// Function group packets are queued in the low priority queue
static void scheduleDCCFunctionPacket(byte* packet, byte length, uint16_t loco, byte group);
// 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);
void remove(PendingSlot * p);
};
#endif // DCCQueue_h

View File

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

View File

@@ -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
@@ -62,8 +62,14 @@ class DCCTimer {
static bool isPWMPin(byte pin);
static void setPWM(byte pin, bool high);
static void clearPWM();
static void startRailcomTimer(byte brakePin);
static void ackRailcomTimer();
static void DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency);
static void DCCEXanalogWrite(uint8_t pin, int value);
static void DCCEXanalogWrite(uint8_t pin, int value, bool invert);
static void DCCEXledcDetachPin(uint8_t pin);
static void DCCEXanalogCopyChannel(int8_t frompin, int8_t topin);
static void DCCEXInrushControlOn(uint8_t pin, int duty, bool invert);
static void DCCEXledcAttachPin(uint8_t pin, int8_t channel, bool inverted);
// Update low ram level. Allow for extra bytes to be specified
// by estimation or inspection, that may be used by other
@@ -85,6 +91,7 @@ class DCCTimer {
static void reset();
private:
static void DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency);
static int freeMemory();
static volatile int minimum_free_memory;
static const int DCC_SIGNAL_TIME=58; // this is the 58uS DCC 1-bit waveform half-cycle
@@ -125,8 +132,15 @@ private:
// On platforms that scan, it is called from waveform ISR
// only on a regular basis.
static void scan();
#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;
#endif
static uint8_t highestPin;
// cached analog values (malloc:ed to actual number of ADC channels)
static int *analogvals;

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.
*
@@ -29,6 +29,7 @@
#include <avr/boot.h>
#include <avr/wdt.h>
#include "DCCTimer.h"
#include "DIAG.h"
#ifdef DEBUG_ADC
#include "TrackManager.h"
#endif
@@ -39,6 +40,9 @@ INTERRUPT_CALLBACK interruptHandler=0;
#define TIMER1_A_PIN 11
#define TIMER1_B_PIN 12
#define TIMER1_C_PIN 13
#define TIMER2_A_PIN 10
#define TIMER2_B_PIN 9
#else
#define TIMER1_A_PIN 9
#define TIMER1_B_PIN 10
@@ -53,8 +57,62 @@ 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 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 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 resetting
any time between the first and second tick.
(there will be 7 DCC timer1 ticks in which to do this.)
*/
const int cutoutDuration = 430; // Desired interval in microseconds
const int cycle=cutoutDuration/2;
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);
// 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.
}
void DCCTimer::ackRailcomTimer() {
// Change Timer2 to CTC mode with RESET pin 9 on next compare match
TCCR2A = (1 << WGM21) | (1 << COM2B1);
// diagnostic digitalWrite(4,LOW);
}
// ISR called by timer interrupt every 58uS
ISR(TIMER1_OVF_vect){ interruptHandler(); }
@@ -120,11 +178,90 @@ 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
}
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
}
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t fbits) {
#if defined(ARDUINO_AVR_UNO)
(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.
// We are most likely not on pin 3 or 11 as no known motor shield has that as brake.
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
// Speed mapping is done like this:
// No functions buttons: 000 0 -> low 131Hz
// Only F29 pressed 001 1 -> mid 490Hz
// F30 with or w/o F29 01x 2-3 -> high 3400Hz
// F31 with or w/o F29/30 1xx 4-7 -> supersonic 62500Hz
uint8_t abits;
uint8_t bbits;
if (pin == 9 || pin == 10) { // timer 2 is different
if (fbits >= 4)
abits = B00000011;
else
abits = B00000001;
if (fbits >= 4)
bbits = B0001;
else if (fbits >= 2)
bbits = B0010;
else if (fbits == 1)
bbits = B0100;
else // fbits == 0
bbits = B0110;
TCCR2A = (TCCR2A & B11111100) | abits; // set WGM0 and WGM1
TCCR2B = (TCCR2B & B11110000) | bbits; // set WGM2 and 3 bits of prescaler
DIAG(F("Timer 2 A=%x B=%x"), TCCR2A, TCCR2B);
} else { // not timer 9 or 10
abits = B01;
if (fbits >= 4)
bbits = B1001;
else if (fbits >= 2)
bbits = B0010;
else if (fbits == 1)
bbits = B0011;
else
bbits = B0100;
switch (pin) {
// case 9 and 10 taken care of above by if()
case 6:
case 7:
case 8:
// Timer4
TCCR4A = (TCCR4A & B11111100) | abits; // set WGM0 and WGM1
TCCR4B = (TCCR4B & B11100000) | bbits; // set WGM2 and WGM3 and divisor
//DIAG(F("Timer 4 A=%x B=%x"), TCCR4A, TCCR4B);
break;
case 46:
case 45:
case 44:
// Timer5
TCCR5A = (TCCR5A & B11111100) | abits; // set WGM0 and WGM1
TCCR5B = (TCCR5B & B11100000) | bbits; // set WGM2 and WGM3 and divisor
//DIAG(F("Timer 5 A=%x B=%x"), TCCR5A, TCCR5B);
break;
default:
break;
}
}
#endif
}
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
#define NUM_ADC_INPUTS 16
#else

View File

@@ -76,8 +76,20 @@ 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>
#include <soc/sens_struct.h>
@@ -151,10 +163,28 @@ void DCCTimer::reset() {
ESP.restart();
}
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
if (f >= 16)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
/*
else if (f == 7) // not used on ESP32
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 62500);
*/
else if (f >= 4)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 32000);
else if (f >= 3)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 16000);
else if (f >= 2)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3400);
else if (f == 1)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);
else
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 131);
}
#include "esp32-hal.h"
#include "soc/soc_caps.h"
#ifdef SOC_LEDC_SUPPORT_HS_MODE
#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM<<1)
#else
@@ -164,7 +194,7 @@ void DCCTimer::reset() {
static int8_t pin_to_channel[SOC_GPIO_PIN_COUNT] = { 0 };
static int cnt_channel = LEDC_CHANNELS;
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency) {
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency) {
if (pin < SOC_GPIO_PIN_COUNT) {
if (pin_to_channel[pin] != 0) {
ledcSetup(pin_to_channel[pin], frequency, 8);
@@ -172,27 +202,115 @@ void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency) {
}
}
void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value) {
void DCCTimer::DCCEXledcDetachPin(uint8_t pin) {
#ifdef DIAG_IO
DIAG(F("Clear pin %d from ledc channel"), pin);
#endif
pin_to_channel[pin] = 0;
pinMatrixOutDetach(pin, false, false);
}
static byte LEDCToMux[] = {
LEDC_HS_SIG_OUT0_IDX,
LEDC_HS_SIG_OUT1_IDX,
LEDC_HS_SIG_OUT2_IDX,
LEDC_HS_SIG_OUT3_IDX,
LEDC_HS_SIG_OUT4_IDX,
LEDC_HS_SIG_OUT5_IDX,
LEDC_HS_SIG_OUT6_IDX,
LEDC_HS_SIG_OUT7_IDX,
LEDC_LS_SIG_OUT0_IDX,
LEDC_LS_SIG_OUT1_IDX,
LEDC_LS_SIG_OUT2_IDX,
LEDC_LS_SIG_OUT3_IDX,
LEDC_LS_SIG_OUT4_IDX,
LEDC_LS_SIG_OUT5_IDX,
LEDC_LS_SIG_OUT6_IDX,
LEDC_LS_SIG_OUT7_IDX,
};
void DCCTimer::DCCEXledcAttachPin(uint8_t pin, int8_t channel, bool inverted) {
DIAG(F("Attaching pin %d to channel %d %c"), pin, channel, inverted ? 'I' : ' ');
ledcAttachPin(pin, channel);
if (inverted) // we attach again but with inversion
gpio_matrix_out(pin, LEDCToMux[channel], inverted, 0);
}
void DCCTimer::DCCEXanalogCopyChannel(int8_t frompin, int8_t topin) {
// arguments are signed depending on inversion of pins
DIAG(F("Pin %d copied to %d"), frompin, topin);
bool inverted = false;
if (frompin<0)
frompin = -frompin;
if (topin<0) {
inverted = true;
topin = -topin;
}
int channel = pin_to_channel[frompin]; // after abs(frompin)
pin_to_channel[topin] = channel;
DCCTimer::DCCEXledcAttachPin(topin, channel, inverted);
}
void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value, bool invert) {
// This allocates channels 15, 13, 11, ....
// so each channel gets its own timer.
if (pin < SOC_GPIO_PIN_COUNT) {
if (pin_to_channel[pin] == 0) {
int search_channel;
int n;
if (!cnt_channel) {
log_e("No more PWM channels available! All %u already used", LEDC_CHANNELS);
return;
}
pin_to_channel[pin] = --cnt_channel;
ledcSetup(cnt_channel, 1000, 8);
ledcAttachPin(pin, cnt_channel);
// search for free channels top down
for (search_channel=LEDC_CHANNELS-1; search_channel >=cnt_channel; search_channel -= 2) {
bool chanused = false;
for (n=0; n < SOC_GPIO_PIN_COUNT; n++) {
if (pin_to_channel[n] == search_channel) { // current search_channel used
chanused = true;
break;
}
}
if (chanused)
continue;
if (n == SOC_GPIO_PIN_COUNT) // current search_channel unused
break;
}
if (search_channel >= cnt_channel) {
pin_to_channel[pin] = search_channel;
DIAG(F("Pin %d assigned to search channel %d"), pin, search_channel);
} else {
pin_to_channel[pin] = --cnt_channel; // This sets 15, 13, ...
DIAG(F("Pin %d assigned to new channel %d"), pin, cnt_channel);
--cnt_channel; // Now we are at 14, 12, ...
}
ledcSetup(pin_to_channel[pin], 1000, 8);
DCCEXledcAttachPin(pin, pin_to_channel[pin], invert);
} else {
ledcAttachPin(pin, pin_to_channel[pin]);
// This else is only here so we can enable diag
// Pin should be already attached to channel
// DIAG(F("Pin %d assigned to old channel %d"), pin, pin_to_channel[pin]);
}
ledcWrite(pin_to_channel[pin], value);
}
}
void DCCTimer::DCCEXInrushControlOn(uint8_t pin, int duty, bool inverted) {
// this uses hardcoded channel 0
ledcSetup(0, 62500, 8);
DCCEXledcAttachPin(pin, 0, inverted);
ledcWrite(0, duty);
}
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() {
@@ -212,6 +330,5 @@ void ADCee::scan() {
void ADCee::begin() {
}
#endif //IDF v4
#endif //ESP32

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/*
* © 2023 Neil McKechnie
* © 2022-23 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
@@ -50,11 +64,16 @@ HardwareSerial Serial6(PA12, PA11); // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14
// via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
// On the F446RE, Serial3 and Serial5 are easy to use:
HardwareSerial Serial3(PC11, PC10); // Rx=PC11, Tx=PC10 -- USART3 - F446RE
HardwareSerial Serial5(PD2, PC12); // Rx=PC7, Tx=PC6 -- UART5 - F446RE
HardwareSerial Serial5(PD2, PC12); // Rx=PD2, Tx=PC12 -- UART5 - F446RE
// On the F446RE, Serial4 and Serial6 also use pins we can't readily map while using the Arduino pins
#elif defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)|| defined(ARDUINO_NUCLEO_F412ZG)
#elif defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F413ZH) || defined(ARDUINO_NUCLEO_F446ZE) || \
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 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.
#else
@@ -154,13 +173,28 @@ HardwareSerial Serial6(PG9, PG14); // Rx=PG9, Tx=PG14 -- USART6
///////////////////////////////////////////////////////////////////////////////////////////////
INTERRUPT_CALLBACK interruptHandler=0;
// Let's use STM32's timer #11 until disabused of this notion
// Timer #11 is used for "servo" library, but as DCC-EX is not using
// this libary, we should be free and clear.
HardwareTimer timer(TIM11);
// On STM32F4xx models that have them, Timers 6 and 7 have no PWM output capability,
// so are good choices for general timer duties - they are used for tone and servo
// in stm32duino so we shall usurp those as DCC-EX doesn't use tone or servo libs.
// NB: the F401, F410 and F411 do **not** have Timer 6 or 7, so we use Timer 11
#ifndef DCC_EX_TIMER
#if defined(TIM6)
#define DCC_EX_TIMER TIM6
#elif defined(TIM7)
#define DCC_EX_TIMER TIM7
#elif defined(TIM11)
#define DCC_EX_TIMER TIM11
#else
#warning This STM32F4XX variant does not have Timers 6,7 or 11!!
#endif
#endif // ifndef DCC_EX_TIMER
HardwareTimer dcctimer(DCC_EX_TIMER);
void DCCTimer_Handler() __attribute__((interrupt));
// Timer IRQ handler
void Timer11_Handler() {
void DCCTimer_Handler() {
interruptHandler();
}
@@ -168,22 +202,33 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
noInterrupts();
// adc_set_sample_rate(ADC_SAMPLETIME_480CYCLES);
timer.pause();
timer.setPrescaleFactor(1);
dcctimer.pause();
dcctimer.setPrescaleFactor(1);
// timer.setOverflow(CLOCK_CYCLES * 2);
timer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);
timer.attachInterrupt(Timer11_Handler);
timer.refresh();
timer.resume();
dcctimer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);
// dcctimer.attachInterrupt(Timer11_Handler);
dcctimer.attachInterrupt(DCCTimer_Handler);
dcctimer.setInterruptPriority(1, 0); // Set second highest preemptive priority!
dcctimer.refresh();
dcctimer.resume();
interrupts();
}
void DCCTimer::startRailcomTimer(byte brakePin) {
// TODO: for intended operation see DCCTimerAVR.cpp
(void) brakePin;
}
void DCCTimer::ackRailcomTimer() {
// TODO: for intended operation see DCCTimerAVR.cpp
}
bool DCCTimer::isPWMPin(byte pin) {
//TODO: SAMD whilst this call to digitalPinHasPWM will reveal which pins can do PWM,
//TODO: STM32 whilst this call to digitalPinHasPWM will reveal which pins can do PWM,
// there's no support yet for High Accuracy, so for now return false
// return digitalPinHasPWM(pin);
(void) pin;
return false;
}
@@ -198,9 +243,9 @@ void DCCTimer::clearPWM() {
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
volatile uint32_t *serno1 = (volatile uint32_t *)0x1FFF7A10;
volatile uint32_t *serno2 = (volatile uint32_t *)0x1FFF7A14;
// volatile uint32_t *serno3 = (volatile uint32_t *)0x1FFF7A18;
volatile uint32_t *serno1 = (volatile uint32_t *)UID_BASE;
volatile uint32_t *serno2 = (volatile uint32_t *)UID_BASE+4;
// volatile uint32_t *serno3 = (volatile uint32_t *)UID_BASE+8;
volatile uint32_t m1 = *serno1;
volatile uint32_t m2 = *serno2;
@@ -235,22 +280,110 @@ void DCCTimer::reset() {
while(true) {};
}
// TODO: may need to use uint32_t on STMF4xx variants with > 16 analog inputs!
#if defined(ARDUINO_NUCLEO_F446RE) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)
#warning STM32 board selected not fully supported - only use ADC1 inputs 0-15 for current sensing!
#endif
// For now, define the max of 16 ports - some variants have more, but this not **yet** supported
#define NUM_ADC_INPUTS 16
// #define NUM_ADC_INPUTS NUM_ANALOG_INPUTS
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t f) {
if (f >= 16)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, f);
else if (f == 7)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 62500);
else if (f >= 4)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 32000);
else if (f >= 3)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 16000);
else if (f >= 2)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 3400);
else if (f == 1)
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 480);
else
DCCTimer::DCCEXanalogWriteFrequencyInternal(pin, 131);
}
uint16_t ADCee::usedpins = 0;
uint8_t ADCee::highestPin = 0;
int * ADCee::analogvals = NULL;
uint32_t * analogchans = NULL;
bool adc1configured = false;
// TODO: rationalise the size of these... could really use sparse arrays etc.
static HardwareTimer * pin_timer[100] = {0};
static uint32_t channel_frequency[100] = {0};
static uint32_t pin_channel[100] = {0};
int16_t ADCee::ADCmax() {
return 4095;
// Using the HardwareTimer library API included in stm32duino core to handle PWM duties
// TODO: in order to use the HA code above which Neil kindly wrote, we may have to do something more
// sophisticated about detecting any clash between the timer we'd like to use for PWM and the ones
// currently used for HA so they don't interfere with one another. For now we'll just make PWM
// work well... then work backwards to integrate with HA mode if we can.
void DCCTimer::DCCEXanalogWriteFrequencyInternal(uint8_t pin, uint32_t frequency)
{
if (pin_timer[pin] == NULL) {
// Automatically retrieve TIM instance and channel associated to pin
// This is used to be compatible with all STM32 series automatically.
TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(digitalPinToPinName(pin), PinMap_PWM);
if (Instance == NULL) {
// We shouldn't get here (famous last words) as it ought to have been caught by brakeCanPWM()!
DIAG(F("DCCEXanalogWriteFrequency::Pin %d has no PWM function!"), pin);
return;
}
pin_channel[pin] = STM_PIN_CHANNEL(pinmap_function(digitalPinToPinName(pin), PinMap_PWM));
// Instantiate HardwareTimer object. Thanks to 'new' instantiation,
// HardwareTimer is not destructed when setup function is finished.
pin_timer[pin] = new HardwareTimer(Instance);
// Configure and start PWM
// MyTim->setPWM(channel, pin, 5, 10, NULL, NULL); // No callback required, we can simplify the function call
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 Channel %d, frequency %d"), pin, pin_channel[pin], frequency);
}
else
DIAG(F("DCCEXanalogWriteFrequency::failed to allocate HardwareTimer instance!"));
}
else
{
// Frequency change request
if (frequency != channel_frequency[pin])
{
pinmap_pinout(digitalPinToPinName(pin), PinMap_TIM); // ensure the pin has been configured!
pin_timer[pin]->setOverflow(frequency, HERTZ_FORMAT); // Just change the frequency if it's already running!
DIAG(F("DCCEXanalogWriteFrequency::setting frequency to %d"), frequency);
}
}
channel_frequency[pin] = frequency;
return;
}
void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value, bool invert) {
if (invert)
value = 255-value;
// Calculate percentage duty cycle from value given
uint32_t duty_cycle = (value * 100 / 256) + 1;
if (pin_timer[pin] != NULL) {
// if (duty_cycle == 100)
// {
// pin_timer[pin]->pauseChannel(pin_channel[pin]);
// DIAG(F("DCCEXanalogWrite::Pausing timer channel on pin %d"), pin);
// }
// else
// {
pinmap_pinout(digitalPinToPinName(pin), PinMap_TIM); // ensure the pin has been configured!
// pin_timer[pin]->resumeChannel(pin_channel[pin]);
pin_timer[pin]->setCaptureCompare(pin_channel[pin], duty_cycle, PERCENT_COMPARE_FORMAT); // DCC_EX_PWM_FREQ Hertz, duty_cycle% dutycycle
DIAG(F("DCCEXanalogWrite::Pin %d, value %d, duty cycle %d"), pin, value, duty_cycle);
// }
}
else
DIAG(F("DCCEXanalogWrite::Pin %d is not configured for PWM!"), pin);
}
// Now we can handle more ADCs, maybe this works!
#define NUM_ADC_INPUTS NUM_ANALOG_INPUTS
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 * ADCee::analogchans = NULL; // Array of channel numbers to be scanned
// bool adc1configured = false;
ADC_TypeDef * * ADCee::adcchans = NULL; // Array to capture which ADC is each input channel on
int16_t ADCee::ADCmax()
{
return 4095;
}
int ADCee::init(uint8_t pin) {
@@ -261,11 +394,34 @@ int ADCee::init(uint8_t pin) {
return -1024; // some silly value as error
uint32_t stmgpio = STM_PORT(stmpin); // converts to the GPIO port (16-bits per port group on STM32)
uint32_t adcchan = STM_PIN_CHANNEL(pinmap_function(stmpin, PinMap_ADC)); // find ADC channel (only valid for ADC1!)
GPIO_TypeDef * gpioBase;
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
#if defined(ADC2)
else if (adc == ADC2)
{
DIAG(F("ADCee::init(): found pin %d on ADC2"), pin);
adcnum = 2;
}
#endif
#if defined(ADC3)
else if (adc == ADC3)
{
DIAG(F("ADCee::init(): found pin %d on ADC3"), pin);
adcnum = 3;
}
#endif
else DIAG(F("ADCee::init(): found pin %d on unknown ADC!"), pin);
// Port config - find which port we're on and power it up
switch(stmgpio) {
// Port config - find which port we're on and power it up
GPIO_TypeDef *gpioBase;
switch (stmgpio)
{
case 0x00:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; //Power up PORTA
gpioBase = GPIOA;
@@ -278,6 +434,32 @@ int ADCee::init(uint8_t pin) {
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; //Power up PORTC
gpioBase = GPIOC;
break;
case 0x03:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; //Power up PORTD
gpioBase = GPIOD;
break;
case 0x04:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN; //Power up PORTE
gpioBase = GPIOE;
break;
#if defined(GPIOF)
case 0x05:
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
}
@@ -293,31 +475,33 @@ int ADCee::init(uint8_t pin) {
if (adcchan > 18)
return -1022; // silly value as error
if (adcchan < 10)
ADC1->SMPR2 |= (0b111 << (adcchan * 3)); // Channel sampling rate 480 cycles
adc->SMPR2 |= (0b111 << (adcchan * 3)); // Channel sampling rate 480 cycles
else
ADC1->SMPR1 |= (0b111 << ((adcchan - 10) * 3)); // Channel sampling rate 480 cycles
adc->SMPR1 |= (0b111 << ((adcchan - 10) * 3)); // Channel sampling rate 480 cycles
// Read the inital ADC value for this analog input
ADC1->SQR3 = adcchan; // 1st conversion in regular sequence
ADC1->CR2 |= (1 << 30); // Start 1st conversion SWSTART
while(!(ADC1->SR & (1 << 1))); // Wait until conversion is complete
value = ADC1->DR; // Read value from register
adc->SQR3 = adcchan; // 1st conversion in regular sequence
adc->CR2 |= ADC_CR2_SWSTART; //(1 << 30); // Start 1st conversion SWSTART
while(!(adc->SR & (1 << 1))); // Wait until conversion is complete
value = adc->DR; // Read value from register
uint8_t id = pin - PNUM_ANALOG_BASE;
if (id > 15) { // today we have not enough bits in the mask to support more
return -1021;
}
// if (id > 15) { // today we have not enough bits in the mask to support more
// return -1021;
// }
if (analogvals == NULL) { // allocate analogvals and analogchans if this is the first invocation of init.
if (analogvals == NULL) { // allocate analogvals, analogchans and adcchans if this is the first invocation of init
analogvals = (int *)calloc(NUM_ADC_INPUTS+1, sizeof(int));
analogchans = (uint32_t *)calloc(NUM_ADC_INPUTS+1, sizeof(uint32_t));
adcchans = (ADC_TypeDef **)calloc(NUM_ADC_INPUTS+1, sizeof(ADC_TypeDef));
}
analogvals[id] = value; // Store sampled value
analogchans[id] = adcchan; // Keep track of which ADC channel is used for reading this pin
usedpins |= (1 << id); // This pin is now ready
adcchans[id] = adc; // Keep track of which ADC this channel is on
usedpins |= (1 << id); // This pin is now ready
if (id > highestPin) highestPin = id; // Store our highest pin in use
DIAG(F("ADCee::init(): value=%d, channel=%d, id=%d"), value, adcchan, id);
DIAG(F("ADCee::init(): value=%d, ADC%d: channel=%d, id=%d"), value, adcnum, adcchan, id);
return value;
}
@@ -344,13 +528,16 @@ void ADCee::scan() {
static uint8_t id = 0; // id and mask are the same thing but it is faster to
static uint16_t mask = 1; // increment and shift instead to calculate mask from id
static bool waiting = false;
static ADC_TypeDef *adc;
if (waiting) {
adc = adcchans[id];
if (waiting)
{
// look if we have a result
if (!(ADC1->SR & (1 << 1)))
if (!(adc->SR & (1 << 1)))
return; // no result, continue to wait
// found value
analogvals[id] = ADC1->DR;
analogvals[id] = adc->DR;
// advance at least one track
#ifdef DEBUG_ADC
if (id == 1) TrackManager::track[1]->setBrake(0);
@@ -369,9 +556,10 @@ void ADCee::scan() {
// look for a valid track to sample or until we are around
while (true) {
if (mask & usedpins) {
// start new ADC aquire on id
ADC1->SQR3 = analogchans[id]; //1st conversion in regular sequence
ADC1->CR2 |= (1 << 30); //Start 1st conversion SWSTART
// start new ADC aquire on id
adc = adcchans[id];
adc->SQR3 = analogchans[id]; // 1st conversion in regular sequence
adc->CR2 |= (1 << 30); // Start 1st conversion SWSTART
#ifdef DEBUG_ADC
if (id == 1) TrackManager::track[1]->setBrake(1);
#endif
@@ -392,19 +580,83 @@ void ADCee::scan() {
void ADCee::begin() {
noInterrupts();
//ADC1 config sequence
// TODO: currently defaults to ADC1, may need more to handle other members of STM32F4xx family
RCC->APB2ENR |= (1 << 8); //Enable ADC1 clock (Bit8)
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; // Enable ADC1 clock
// Set ADC prescaler - DIV8 ~ 40ms, DIV6 ~ 30ms, DIV4 ~ 20ms, DIV2 ~ 11ms
ADC->CCR = (0 << 16); // Set prescaler 0=DIV2, 1=DIV4, 2=DIV6, 3=DIV8
ADC1->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)
ADC1->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)
ADC1->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion
// Disable the DMA controller for ADC1
ADC1->CR2 &= ~ADC_CR2_DMA;
ADC1->CR2 &= ~(1 << 1); //Single conversion
ADC1->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0
ADC1->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
ADC1->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
ADC1->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
ADC1->CR2 |= (1 << 0); // Switch on ADC1
// Wait for ADC1 to become ready (calibration complete)
while (!(ADC1->CR2 & ADC_CR2_ADON)) {
}
#if defined(ADC2)
// Enable the ADC2 clock
RCC->APB2ENR |= RCC_APB2ENR_ADC2EN;
// Initialize ADC2
ADC2->CR1 = 0; // Disable all channels
ADC2->CR2 = 0; // Clear CR2 register
ADC2->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)
ADC2->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)
ADC2->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion
ADC2->CR2 &= ~ADC_CR2_DMA; // Disable the DMA controller for ADC3
ADC2->CR2 &= ~(1 << 1); //Single conversion
ADC2->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0
ADC2->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
ADC2->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
ADC2->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
// Enable the ADC
ADC2->CR2 |= ADC_CR2_ADON;
// Wait for ADC2 to become ready (calibration complete)
while (!(ADC2->CR2 & ADC_CR2_ADON)) {
}
// Perform ADC3 calibration (optional)
// ADC3->CR2 |= ADC_CR2_CAL;
// while (ADC3->CR2 & ADC_CR2_CAL) {
// }
#endif
#if defined(ADC3)
// Enable the ADC3 clock
RCC->APB2ENR |= RCC_APB2ENR_ADC3EN;
// Initialize ADC3
ADC3->CR1 = 0; // Disable all channels
ADC3->CR2 = 0; // Clear CR2 register
ADC3->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)
ADC3->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)
ADC3->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion
ADC3->CR2 &= ~ADC_CR2_DMA; // Disable the DMA controller for ADC3
ADC3->CR2 &= ~(1 << 1); //Single conversion
ADC3->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0
ADC3->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
ADC3->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
ADC3->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
// Enable the ADC
ADC3->CR2 |= ADC_CR2_ADON;
// Wait for ADC3 to become ready (calibration complete)
while (!(ADC3->CR2 & ADC_CR2_ADON)) {
}
// Perform ADC3 calibration (optional)
// ADC3->CR2 |= ADC_CR2_CAL;
// while (ADC3->CR2 & ADC_CR2_CAL) {
// }
#endif
interrupts();
}
#endif

View File

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

View File

@@ -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];
@@ -106,6 +114,7 @@ void DCCWaveform::interruptHandler() {
DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
isMainTrack = isMain;
packetPending = false;
reminderWindowOpen = false;
memcpy(transmitPacket, idlePacket, sizeof(idlePacket));
state = WAVE_START;
// The +1 below is to allow the preamble generator to create the stop bit
@@ -115,7 +124,26 @@ DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
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 && railcomPossible) {
railcomActive=true;
railcomDebug=debug;
}
else {
railcomActive=false;
railcomDebug=false;
railcomSampleWindow=false;
}
return railcomActive;
}
#pragma GCC push_options
#pragma GCC optimize ("-O3")
@@ -123,13 +151,42 @@ void DCCWaveform::interrupt2() {
// calculate the next bit to be sent:
// set state WAVE_MID_1 for a 1=bit
// or WAVE_HIGH_0 for a 0 bit.
if (remainingPreambles > 0 ) {
state=WAVE_MID_1; // switch state to trigger LOW on next interrupt
remainingPreambles--;
// As we get to the end of the preambles, open the reminder window.
// This delays any reminder insertion until the last moment so
// that the reminder doesn't block a more urgent packet.
reminderWindowOpen=transmitRepeats==0 && remainingPreambles<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.
DCCTimer::updateMinimumFreeMemoryISR(22);
else DCCTimer::updateMinimumFreeMemoryISR(22);
return;
}
@@ -148,29 +205,8 @@ void DCCWaveform::interrupt2() {
if (bytes_sent >= transmitLength) {
// end of transmission buffer... repeat or switch to next message
bytes_sent = 0;
// preamble for next packet will start...
remainingPreambles = requiredPreambles;
if (transmitRepeats > 0) {
transmitRepeats--;
}
else if (packetPending) {
// Copy pending packet to transmit packet
// a fixed length memcpy is faster than a variable length loop for these small lengths
// for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];
memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));
transmitLength = pendingLength;
transmitRepeats = pendingRepeats;
packetPending = false;
clearResets();
}
else {
// Fortunately reset and idle packets are the same length
memcpy( transmitPacket, isMainTrack ? idlePacket : resetPacket, sizeof(idlePacket));
transmitLength = sizeof(idlePacket);
transmitRepeats = 0;
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
}
}
}
}
@@ -193,91 +229,42 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
packetPending = true;
clearResets();
}
bool DCCWaveform::getPacketPending() {
return packetPending;
}
#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);
}
}
bool DCCWaveform::isReminderWindowOpen() {
return reminderWindowOpen && ! packetPending;
}
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;
do {
if(isMainTrack) {
if (rmtMainChannel != NULL)
ret = rmtMainChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
} else {
if (rmtProgChannel != NULL)
ret = rmtProgChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
void DCCWaveform::promotePendingPacket() {
// fill the transmission packet from the pending packet
// Just keep going if repeating
if (transmitRepeats > 0) {
transmitRepeats--;
return;
}
} while(ret > 0);
}
}
bool DCCWaveform::getPacketPending() {
if(isMainTrack) {
if (rmtMainChannel == NULL)
return true;
return rmtMainChannel->busy();
} else {
if (rmtProgChannel == NULL)
return true;
return rmtProgChannel->busy();
}
}
void IRAM_ATTR DCCWaveform::loop() {
DCCACK::checkAck(progTrack.getResets());
if (packetPending) {
// Copy pending packet to transmit packet
// a fixed length memcpy is faster than a variable length loop for these small lengths
// for (int b = 0; b < pendingLength; b++) transmitPacket[b] = pendingPacket[b];
memcpy( transmitPacket, pendingPacket, sizeof(pendingPacket));
transmitLength = pendingLength;
transmitRepeats = pendingRepeats;
packetPending = false;
clearResets();
return;
}
// nothing to do, just send idles or resets
// Fortunately reset and idle packets are the same length
// Note: If railcomDebug is on, then we send resets to the main
// track instead of idles. This means that all data will be zeros
// and only the 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);
transmitRepeats = 0;
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
}
#endif

View File

@@ -2,8 +2,8 @@
* © 2021 M Steve Todd
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020-2024 Harald Barth
* © 2020-2025 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -23,24 +23,28 @@
*/
#ifndef DCCWaveform_h
#define DCCWaveform_h
#include "MotorDriver.h"
#ifdef ARDUINO_ARCH_ESP32
#include "DCCRMT.h"
#include "TrackManager.h"
#endif
// Number of preamble bits.
const int PREAMBLE_BITS_MAIN = 16;
const int PREAMBLE_BITS_PROG = 22;
const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum.
const byte PREAMBLE_BITS_MAIN = 16;
const byte PREAMBLE_BITS_PROG = 22;
const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum.
// The WAVE_STATE enum is deliberately numbered because a change of order would be catastrophic
// to the transform array.
enum WAVE_STATE : byte {WAVE_START=0,WAVE_MID_1=1,WAVE_HIGH_0=2,WAVE_MID_0=3,WAVE_LOW_0=4,WAVE_PENDING=5};
enum WAVE_STATE : byte {
WAVE_START=0, // wave going high at start of bit
WAVE_MID_1=1, // middle of 1 bit
WAVE_HIGH_0=2, // first part of 0 bit high
WAVE_MID_0=3, // middle of 0 bit
WAVE_LOW_0=4, // first part of 0 bit low
WAVE_PENDING=5 // next bit not yet known
};
// NOTE: static functions are used for the overall controller, then
// one instance is created for each track.
@@ -76,11 +80,37 @@ class DCCWaveform {
};
#endif
void schedulePacket(const byte buffer[], byte byteCount, byte repeats);
bool getPacketPending();
bool isReminderWindowOpen();
void promotePendingPacket();
static bool setRailcom(bool on, bool debug);
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;
volatile bool reminderWindowOpen;
volatile byte sentResetsSincePacket;
#else
volatile uint32_t resetPacketBase;
@@ -101,6 +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

@@ -188,6 +188,7 @@ Display *Display::loop2(bool force) {
#endif
noMoreRowsToDisplay = false;
slot = 0;
_deviceDriver->setRowNative(slot); // Set position for display
lastScrollTime = currentMillis;
return NULL;
}

View File

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

View File

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

View File

@@ -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

File diff suppressed because it is too large Load Diff

185
EXRAIL2.h
View File

@@ -1,8 +1,9 @@
/*
* © 2021 Neil McKechnie
* © 2020-2022 Chris Harlow
* © 2022 Colin Murdoch
* © 2020-2025 Chris Harlow
* © 2022-2023 Colin Murdoch
* © 2023 Harald Barth
* © 2025 Morten Nielsen
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -25,6 +26,7 @@
#include "FSH.h"
#include "IODevice.h"
#include "Turnouts.h"
#include "Turntables.h"
// The following are the operation codes (or instructions) for a kind of virtual machine.
// Each instruction is normally 3 bytes long with an operation code followed by a parameter.
@@ -32,16 +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_AUTOSTART,
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,
@@ -49,21 +55,36 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2,
#endif
OPCODE_POM,
OPCODE_START,OPCODE_SETLOCO,OPCODE_SENDLOCO,OPCODE_FORGET,
OPCODE_START,OPCODE_SETLOCO,OPCODE_SETFREQ,OPCODE_SENDLOCO,OPCODE_FORGET,
OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,OPCODE_POWERON,
OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT,
OPCODE_PRINT,OPCODE_DCCACTIVATE,
OPCODE_PRINT,OPCODE_DCCACTIVATE,OPCODE_ASPECT,
OPCODE_ONACTIVATE,OPCODE_ONDEACTIVATE,
OPCODE_ROSTER,OPCODE_KILLALL,
OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,
OPCODE_ENDTASK,OPCODE_ENDEXRAIL,
OPCODE_SET_TRACK,
OPCODE_SET_TRACK,OPCODE_SET_POWER,
OPCODE_ONRED,OPCODE_ONAMBER,OPCODE_ONGREEN,
OPCODE_ONCHANGE,
OPCODE_ONCLOCKTIME,
OPCODE_ONTIME,
// OPcodes below this point are skip-nesting IF operations
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_ONRAILSYNCON,OPCODE_ONRAILSYNCOFF,
OPCODE_ROUTE_ACTIVE,OPCODE_ROUTE_INACTIVE,OPCODE_ROUTE_HIDDEN,
OPCODE_ROUTE_DISABLED,
OPCODE_STASH,OPCODE_CLEAR_STASH,OPCODE_CLEAR_ALL_STASH,OPCODE_PICKUP_STASH,
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...
@@ -74,7 +95,10 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
OPCODE_IFRANDOM,OPCODE_IFRESERVE,
OPCODE_IFCLOSED,OPCODE_IFTHROWN,
OPCODE_IFRE,
OPCODE_IFLOCO
OPCODE_IFLOCO,
OPCODE_IFTTPOSITION,
OPCODE_IFSTASH,
OPCODE_IFBITMAP_ALL,OPCODE_IFBITMAP_ANY,
};
// Ensure thrunge_lcd is put last as there may be more than one display,
@@ -84,11 +108,45 @@ enum thrunger: byte {
thrunge_serial,thrunge_parse,
thrunge_serial1, thrunge_serial2, thrunge_serial3,
thrunge_serial4, thrunge_serial5, thrunge_serial6,
thrunge_lcn,
thrunge_lcn,thrunge_message,
thrunge_lcd, // Must be last!!
};
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;
// 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
static const byte SECTION_FLAG = 0x80;
@@ -108,21 +166,27 @@ enum thrunger: byte {
class LookList {
public:
LookList(int16_t size);
void chain(LookList* chainTo);
void add(int16_t lookup, int16_t result);
int16_t find(int16_t value);
int16_t find(int16_t value); // finds result value
int16_t findPosition(int16_t value); // finds index
int16_t size();
void stream(Print * _stream);
void handleEvent(const FSH* reason,int16_t id, int16_t loco=0);
private:
int16_t m_size;
int16_t m_loaded;
int16_t * m_lookupArray;
int16_t * m_resultArray;
int16_t * m_resultArray;
LookList* m_chain;
};
class RMFT2 {
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);
@@ -130,11 +194,14 @@ class LookList {
static void activateEvent(int16_t addr, bool active);
static void changeEvent(int16_t id, bool change);
static void clockEvent(int16_t clocktime, bool change);
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 SIGNAL_ID_MASK=0x0FFF;
// Throttle Info Access functions built by exrail macros
static void rotateEvent(int16_t id, bool change);
static void powerEvent(int16_t track, bool overload);
#ifdef BOOSTER_INPUT
static void railsyncEvent(bool on);
#endif
static void blockEvent(int16_t block, int16_t loco, bool entering);
static bool signalAspectEvent(int16_t address, byte aspect );
// Throttle Info Access functions built by exrail macros
static const byte rosterNameCount;
static const int16_t HIGHFLASH routeIdList[];
static const int16_t HIGHFLASH automationIdList[];
@@ -144,7 +211,13 @@ class LookList {
static const FSH * getTurnoutDescription(int16_t id);
static const FSH * getRosterName(int16_t id);
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, 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[]) ;
@@ -153,18 +226,17 @@ 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);
#endif
static LookList* LookListLoader(OPCODE op1,
OPCODE op2=OPCODE_ENDEXRAIL,OPCODE op3=OPCODE_ENDEXRAIL);
static void handleEvent(const FSH* reason,LookList* handlers, int16_t id);
static uint16_t getOperand(int progCounter,byte n);
static 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();
@@ -173,12 +245,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 HIGHFLASH byte RouteCode[];
static const HIGHFLASH int16_t SignalDefinitions[];
static const HIGHFLASH3 byte RouteCode[];
static const HIGHFLASH SIGNAL_DEFINITION SignalDefinitions[];
static byte flags[MAX_FLAGS];
static LookList * sequenceLookup;
static Print * LCCSerial;
static LookList * routeLookup;
static LookList * signalLookup;
static LookList * onThrowLookup;
static LookList * onCloseLookup;
static LookList * onActivateLookup;
@@ -188,6 +264,27 @@ private:
static LookList * onGreenLookup;
static LookList * onChangeLookup;
static LookList * onClockLookup;
#ifndef IO_NO_HAL
static LookList * onRotateLookup;
#endif
static LookList * onOverloadLookup;
static LookList * onBlockEnterLookup;
static LookList * onBlockExitLookup;
#ifdef BOOSTER_INPUT
static LookList * onRailSyncOnLookup;
static LookList * onRailSyncOffLookup;
#endif
static const int countLCCLookup;
static int onLCCLookup[];
static const byte compileFeatures;
static void manageRouteState(int16_t id, byte state);
static void manageRouteCaption(int16_t id, const FSH* caption);
static byte * routeStateArray;
static const FSH ** routeCaptionArray;
static int16_t * stashArray;
static int16_t maxStashId;
// Local variables - exist for each instance/task
RMFT2 *next; // loop chain
@@ -197,16 +294,38 @@ 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];
};
#define GET_OPCODE GETHIGHFLASH(RMFT2::RouteCode,progCounter)
#define SKIPOP progCounter+=3
// IO_I2CDFPlayer commands and values
enum : uint8_t{
DF_PLAY = 0x0F,
DF_VOL = 0x06,
DF_FOLDER = 0x2B, // Not a DFPlayer command, used to set folder nr where audio file is
DF_REPEATPLAY = 0x08,
DF_STOPPLAY = 0x16,
DF_EQ = 0x07, // Set equaliser, require parameter NORMAL, POP, ROCK, JAZZ, CLASSIC or BASS
DF_RESET = 0x0C,
DF_DACON = 0x1A,
DF_SETAM = 0x2A, // Set audio mixer 1 or 2 for this DFPLayer
DF_NORMAL = 0x00, // Equalizer parameters
DF_POP = 0x01,
DF_ROCK = 0x02,
DF_JAZZ = 0x03,
DF_CLASSIC = 0x04,
DF_BASS = 0x05,
};
#endif

File diff suppressed because it is too large Load Diff

363
EXRAIL2Parser.cpp Normal file
View File

@@ -0,0 +1,363 @@
/*
* © 2021 Neil McKechnie
* © 2021-2023 Harald Barth
* © 2020-2025 Chris Harlow
* © 2022-2023 Colin Murdoch
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// THIS file is an extension of the RMFT2 class
// normally found in EXRAIL2.cpp
#include <Arduino.h>
#include "defines.h"
#include "EXRAIL2.h"
#include "DCC.h"
#include "KeywordHasher.h"
// This filter intercepts <> commands to do the following:
// - Implement RMFT specific commands/diagnostics
// - Reject/modify JMRI commands that would interfere with RMFT processing
void RMFT2::ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]) {
(void)stream; // avoid compiler warning if we don't access this parameter
switch(opcode) {
case 'D':
if (p[0]=="EXRAIL"_hk) { // <D EXRAIL ON/OFF>
diag = paramCount==2 && (p[1]=="ON"_hk || p[1]==1);
opcode=0;
}
break;
case '/': // New EXRAIL command
if (parseSlash(stream,paramCount,p)) opcode=0;
break;
case 'A': // <A address aspect>
if (paramCount!=2) break;
// Ask exrail if this is just changing the aspect on a
// predefined DCCX_SIGNAL. Because this will handle all
// the IFRED and ONRED type issues at the same time.
if (signalAspectEvent(p[0],p[1])) opcode=0; // all done
break;
case 'L':
// This entire code block is compiled out if LLC macros not used
if (!(compileFeatures & FEATURE_LCC)) return;
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/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.
case OPCODE_ONLCC:
StringFormatter::send(stream,F("<LL %d x%h%h%h:%h>\n"),
lccEventIndex,
getOperand(progCounter,1),
getOperand(progCounter,2),
getOperand(progCounter,3),
getOperand(progCounter,0)
);
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;
}
}
}
if (paramCount==1) { // <L eventid> LCC event arrived from adapter
int16_t eventid=p[0];
bool reject = eventid<0 || eventid>=countLCCLookup;
if (!reject) {
startNonRecursiveTask(F("LCC"),eventid,onLCCLookup[eventid]);
opcode=0;
}
}
break;
case 'J': // throttle info commands
if (paramCount<1) return;
switch(p[0]) {
case "A"_hk: // <JA> returns automations/routes
if (paramCount==1) {// <JA>
StringFormatter::send(stream, F("<jA"));
routeLookup->stream(stream);
StringFormatter::send(stream, F(">\n"));
opcode=0;
return;
}
if (paramCount==2) { // <JA id>
int16_t id=p[1];
StringFormatter::send(stream,F("<jA %d %c \"%S\">\n"),
id, getRouteType(id), getRouteDescription(id));
if (compileFeatures & FEATURE_ROUTESTATE) {
// Send any non-default button states or captions
int16_t statePos=routeLookup->findPosition(id);
if (statePos>=0) {
if (routeStateArray[statePos])
StringFormatter::send(stream,F("<jB %d %d>\n"), id, routeStateArray[statePos]);
if (routeCaptionArray[statePos])
StringFormatter::send(stream,F("<jB %d \"%S\">\n"), id,routeCaptionArray[statePos]);
}
}
opcode=0;
return;
}
break;
case '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[]) {
if (paramCount==0) { // STATUS
StringFormatter::send(stream, F("<* EXRAIL STATUS"));
RMFT2 * task=loopTask;
while(task) {
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':' '
);
auto progCounter=task->progCounter; // name to satisfy macros below
auto operand=task->getOperand(progCounter,0);
switch(GET_OPCODE) {
case OPCODE_RESERVE:
StringFormatter::send(stream,F(" WAIT RESERVE %d"),operand);
break;
case OPCODE_AT:
case OPCODE_ATTIMEOUT2:
case OPCODE_AFTER:
case OPCODE_ATGTE:
case OPCODE_ATLT:
StringFormatter::send(stream,F(" WAIT AT/AFTER %d"),operand);
break;
case OPCODE_DELAY:
case OPCODE_DELAYMINS:
case OPCODE_DELAYMS:
case OPCODE_RANDWAIT:
StringFormatter::send(stream,F(" WAIT DELAY"));
break;
default: break;
}
}
task=task->next;
if (task==loopTask) break;
}
// Now stream the flags
for (int id=0;id<MAX_FLAGS; id++) {
byte flag=flags[id];
if (flag & ~TASK_FLAG & ~SIGNAL_MASK) { // not interested in TASK_FLAG only. Already shown above
StringFormatter::send(stream,F("\nflags[%d] "),id);
if (flag & SECTION_FLAG) StringFormatter::send(stream,F(" RESERVED"));
if (flag & LATCH_FLAG) StringFormatter::send(stream,F(" LATCHED"));
}
}
if (compileFeatures & FEATURE_SIGNAL) {
// do the signals
// flags[n] represents the state of the nth signal in the table
for (int sigslot=0;;sigslot++) {
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"),
slot.id);
}
}
StringFormatter::send(stream,F(" *>\n"));
return true;
}
switch (p[0]) {
case "PAUSE"_hk: // </ PAUSE>
if (paramCount!=1) return false;
{ // 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) {
task->resume();
task=task->next;
if (task==loopTask) break;
}
}
return true;
case "START"_hk: // </ START [cab] route >
if (paramCount<2 || paramCount>3) return false;
{
int route=(paramCount==2) ? p[1] : p[2];
uint16_t cab=(paramCount==2)? 0 : p[1];
int pc=routeLookup->find(route);
if (pc<0) return false;
new RMFT2(pc,cab);
}
return true;
default:
break;
}
// check KILL ALL here, otherwise the next validation confuses ALL with a flag
if (p[0]=="KILL"_hk && p[1]=="ALL"_hk) {
while (loopTask) loopTask->kill(F("KILL ALL")); // destructor changes loopTask
return true;
}
// all other / commands take 1 parameter
if (paramCount!=2 ) return false;
switch (p[0]) {
case "KILL"_hk: // Kill taskid|ALL
{
if ( p[1]<0 || p[1]>=MAX_FLAGS) return false;
RMFT2 * task=loopTask;
while(task) {
if (task->taskId==p[1]) {
task->kill(F("KILL"));
return true;
}
task=task->next;
if (task==loopTask) break;
}
}
return false;
case "RESERVE"_hk: // force reserve a section
return setFlag(p[1],SECTION_FLAG);
case "FREE"_hk: // force free a section
return setFlag(p[1],0,SECTION_FLAG);
case "LATCH"_hk:
return setFlag(p[1], LATCH_FLAG);
case "UNLATCH"_hk:
return setFlag(p[1], 0, LATCH_FLAG);
case "RED"_hk:
doSignal(p[1],SIGNAL_RED);
return true;
case "AMBER"_hk:
doSignal(p[1],SIGNAL_AMBER);
return true;
case "GREEN"_hk:
doSignal(p[1],SIGNAL_GREEN);
return true;
default:
return false;
}
}

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
* © 2022 Colin Murdoch
* © 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
@@ -54,32 +56,159 @@
// helper macro for turnout descriptions, creates NULL for missing description
#define O_DESC(id, desc) case id: return ("" desc)[0]?F("" desc):NULL;
// helper macro for turntable descriptions, creates NULL for missing description
#define T_DESC(tid,pid,desc) if(turntableId==tid && positionId==pid) return ("" desc)[0]?F("" desc):NULL;
// helper macro for turnout description as HIDDEN
#define HIDDEN "\x01"
// PLAYSOUND is alias of ANOUT to make the user experience of a Conductor beter for
// playing sounds with IO_I2CDFPlayer
#define PLAYSOUND ANOUT
// 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
// These constants help EXRAIL macros convert Track Power e.g. SET_POWER(A ON|OFF).
//const byte TRACK_POWER_0=0, TRACK_POWER_OFF=0;
//const byte TRACK_POWER_1=1, TRACK_POWER_ON=1;
// 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 1h Implements HAL macro by creating exrailHalSetup function
// 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 STEALTH_GLOBAL
#define STEALTH_GLOBAL(code...) code
#include "myAutomation.h"
// Pass 1h Implements HAL macro by creating exrailHalSetup1 function
// Also allows creating EXTurntable object
#include "EXRAIL2MacroReset.h"
#undef HAL
#define HAL(haltype,params...) haltype::create(params);
void exrailHalSetup() {
#undef HAL_IGNORE_DEFAULTS
#define HAL_IGNORE_DEFAULTS ignore_defaults=true;
bool exrailHalSetup1() {
bool ignore_defaults=false;
#include "myAutomation.h"
return ignore_defaults;
}
// Pass 1s Implements servos by creating exrailHalSetup2
// TODO Turnout and turntable creation should be moved to here instead of
// the first pass from the opcode table.
#include "EXRAIL2MacroReset.h"
#undef JMRI_SENSOR
#define JMRI_SENSOR(vpin,count...) \
{ \
const int npins=#count[0]? count+0:1; \
static byte state_map[(npins+7)/8]; \
SensorGroup::doSensorGroup(vpin,npins,state_map,action,&USB_SERIAL,true); \
}
#undef JMRI_SENSOR_NOPULLUP
#define JMRI_SENSOR_NOPULLUP(vpin,count...) \
{ \
const int npins=#count[0]? count+0:1; \
static byte state_map[(npins+7)/8]; \
SensorGroup::doSensorGroup(vpin,npins,state_map,action,&USB_SERIAL,false); \
}
void SensorGroup::doExrailSensorGroup(GroupProcess action, Print * stream) {
(void) action; // suppress unused warnings if no groups
(void) stream;
#include "myAutomation.h"
}
// Pass 1s Implements servos by creating exrailHalSetup2
// TODO Turnout and turntable creation should be moved to here instead of
// the first pass from the opcode table.
#include "EXRAIL2MacroReset.h"
#undef CONFIGURE_SERVO
#define CONFIGURE_SERVO(vpin,pos1,pos2,profile) IODevice::configureServo(vpin,pos1,pos2,PCA9685::profile);
void exrailHalSetup2() {
#include "myAutomation.h"
// pullup any group sensors
SensorGroup::prepareAll();
}
// Pass 1c detect compile time featurtes
#include "EXRAIL2MacroReset.h"
#undef SIGNAL
#define SIGNAL(redpin,amberpin,greenpin) | FEATURE_SIGNAL
#undef SIGNALH
#define SIGNALH(redpin,amberpin,greenpin) | FEATURE_SIGNAL
#undef SERVO_SIGNAL
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) | FEATURE_SIGNAL
#undef DCC_SIGNAL
#define DCC_SIGNAL(id,addr,subaddr) | FEATURE_SIGNAL
#undef DCCX_SIGNAL
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect) | FEATURE_SIGNAL
#undef NEOPIXEL_SIGNAL
#define NEOPIXEL_SIGNAL(sigid,redcolour,ambercolour,greencolour) | FEATURE_SIGNAL
#undef VIRTUAL_SIGNAL
#define VIRTUAL_SIGNAL(id) | FEATURE_SIGNAL
#undef LCC
#define LCC(eventid) | FEATURE_LCC
#undef LCCX
#define LCCX(senderid,eventid) | FEATURE_LCC
#undef ONLCC
#define ONLCC(senderid,eventid) | FEATURE_LCC
#undef 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
#define ROUTE_INACTIVE(id) | FEATURE_ROUTESTATE
#undef ROUTE_HIDDEN
#define ROUTE_HIDDEN(id) | FEATURE_ROUTESTATE
#undef ROUTE_DISABLED
#define ROUTE_DISABLED(id) | FEATURE_ROUTESTATE
#undef ROUTE_CAPTION
#define ROUTE_CAPTION(id,caption) | FEATURE_ROUTESTATE
#undef 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"
;
// Pass 2 create throttle route list
#include "EXRAIL2MacroReset.h"
#undef ROUTE
#define ROUTE(id, description) id,
const int16_t HIGHFLASH RMFT2::routeIdList[]= {
const int16_t HIGHFLASH RMFT2::routeIdList[]= {
#include "myAutomation.h"
INT16_MAX};
// Pass 2a create throttle automation list
@@ -121,6 +250,15 @@ const int StringMacroTracker1=__COUNTER__;
#define PRINT(msg) THRUNGE(msg,thrunge_print)
#undef LCN
#define LCN(msg) THRUNGE(msg,thrunge_lcn)
#undef MESSAGE
#define MESSAGE(msg) THRUNGE(msg,thrunge_message)
#undef ROUTE_CAPTION
#define ROUTE_CAPTION(id,caption) \
case (__COUNTER__ - StringMacroTracker1) : {\
manageRouteCaption(id,F(caption));\
return;\
}
#undef SERIAL
#define SERIAL(msg) THRUNGE(msg,thrunge_serial)
#undef SERIAL1
@@ -153,6 +291,8 @@ const int StringMacroTracker1=__COUNTER__;
lcdid=id;\
break;\
}
#undef STEALTH
#define STEALTH(code...) case (__COUNTER__ - StringMacroTracker1) : {code} return;
#undef WITHROTTLE
#define WITHROTTLE(msg) THRUNGE(msg,thrunge_withrottle)
@@ -172,6 +312,8 @@ void RMFT2::printMessage(uint16_t id) {
#include "EXRAIL2MacroReset.h"
#undef TURNOUT
#define TURNOUT(id,addr,subaddr,description...) O_DESC(id,description)
#undef TURNOUTL
#define TURNOUTL(id,addr,description...) O_DESC(id,description)
#undef PIN_TURNOUT
#define PIN_TURNOUT(id,pin,description...) O_DESC(id,description)
#undef SERVO_TURNOUT
@@ -187,6 +329,33 @@ const FSH * RMFT2::getTurnoutDescription(int16_t turnoutid) {
return NULL;
}
// Pass to get turntable descriptions (optional)
#include "EXRAIL2MacroReset.h"
#undef DCC_TURNTABLE
#define DCC_TURNTABLE(id,home,description...) O_DESC(id,description)
#undef EXTT_TURNTABLE
#define EXTT_TURNTABLE(id,vpin,home,description...) O_DESC(id,description)
const FSH * RMFT2::getTurntableDescription(int16_t turntableId) {
switch (turntableId) {
#include "myAutomation.h"
default:break;
}
return NULL;
}
// Pass to get turntable position descriptions (optional)
#include "EXRAIL2MacroReset.h"
#undef TT_ADDPOSITION
#define TT_ADDPOSITION(turntable_id,position,value,home,description...) T_DESC(turntable_id,position,description)
const FSH * RMFT2::getTurntablePositionDescription(int16_t turntableId, uint8_t positionId) {
(void)turntableId;
(void)positionId;
#include "myAutomation.h"
return NULL;
}
// Pass 6: Roster IDs (count)
#include "EXRAIL2MacroReset.h"
#undef ROSTER
@@ -229,19 +398,40 @@ 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) {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/ 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"
;
int RMFT2::onLCCLookup[RMFT2::countLCCLookup];
// Last Pass : create main routes table
// Only undef the macros, not dummy them.
@@ -255,25 +445,36 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#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),
#define ANOUT(vpin,value,param1,param2) OPCODE_SERVO,V(vpin),OPCODE_PAD,V(value),OPCODE_PAD,V(param1),OPCODE_PAD,V(param2),
#define ASPECT(address,value) OPCODE_ASPECT,V((address<<5) | (value & 0x1F)),
#define AT(sensor_id) OPCODE_AT,V(sensor_id),
#define ATGTE(sensor_id,value) OPCODE_ATGTE,V(sensor_id),OPCODE_PAD,V(value),
#define ATLT(sensor_id,value) OPCODE_ATLT,V(sensor_id),OPCODE_PAD,V(value),
#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
#define DCC_TURNTABLE(id,home,description...) OPCODE_DCCTURNTABLE,V(id),OPCODE_PAD,V(home),
#endif
#define DEACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1),
#define DEACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1),
#define DELAY(ms) ms<30000?OPCODE_DELAYMS:OPCODE_DELAY,V(ms/(ms<30000?1L:100L)),
#define DELAYMINS(mindelay) OPCODE_DELAYMINS,V(mindelay),
#define DELAYRANDOM(mindelay,maxdelay) DELAY(mindelay) OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L),
#define DCC_SIGNAL(id,add,subaddr)
#define DCCX_SIGNAL(id,redAspect,amberAspect,greenAspect)
#define DONE OPCODE_ENDTASK,0,0,
#define DRIVE(analogpin) OPCODE_DRIVE,V(analogpin),
#define ELSE OPCODE_ELSE,0,0,
@@ -281,16 +482,22 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#define ENDIF OPCODE_ENDIF,0,0,
#define ENDTASK OPCODE_ENDTASK,0,0,
#define ESTOP OPCODE_SPEED,V(1),
#define EXRAIL
#define FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V(PCA9685::ProfileType::UseDuration|PCA9685::NoPowerOff),OPCODE_PAD,V(ms/100L),
#define 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((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...)
#define HAL_IGNORE_DEFAULTS
#define IF(sensor_id) OPCODE_IF,V(sensor_id),
#define IFAMBER(signal_id) OPCODE_IFAMBER,V(signal_id),
#define IFCLOSED(turnout_id) OPCODE_IFCLOSED,V(turnout_id),
@@ -302,35 +509,76 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#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 JMRI_SENSOR_NOPULLUP(vpin,count...)
#define JOIN OPCODE_JOIN,0,0,
#define KILLALL OPCODE_KILLALL,0,0,
#define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id),
#define LCC(eventid) OPCODE_LCC,V(eventid),
#define LCCX(sender,event) OPCODE_LCCX,V(event),\
OPCODE_PAD,V((((uint64_t)sender)>>32)&0xFFFF),\
OPCODE_PAD,V((((uint64_t)sender)>>16)&0xFFFF),\
OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF),
#define 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),\
OPCODE_PAD,V((((uint64_t)sender)>>16)&0xFFFF),\
OPCODE_PAD,V((((uint64_t)sender)>>0)&0xFFFF),
#define ONTIME(value) OPCODE_ONTIME,V(value),
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
#define ONOVERLOAD(track_id) OPCODE_ONOVERLOAD,V(TRACK_NUMBER_##track_id),
#define ONRAILSYNCON OPCODE_ONRAILSYNCON,0,0,
#define ONRAILSYNCOFF OPCODE_ONRAILSYNCOFF,0,0,
#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),
#define ONRED(signal_id) OPCODE_ONRED,V(signal_id),
#ifndef IO_NO_HAL
#define ONROTATE(id) OPCODE_ONROTATE,V(id),
#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),
@@ -338,12 +586,21 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#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),
#define ROSTER(cabid,name,funcmap...)
#ifndef IO_NO_HAL
#define ROTATE(id,position,activity) OPCODE_ROTATE,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(EXTurntable::activity),
#define ROTATE_DCC(id,position) OPCODE_ROTATE,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(0),
#endif
#define ROUTE(id, description) OPCODE_ROUTE, V(id),
#define ROUTE_ACTIVE(id) OPCODE_ROUTE_ACTIVE,V(id),
#define ROUTE_INACTIVE(id) OPCODE_ROUTE_INACTIVE,V(id),
#define ROUTE_HIDDEN(id) OPCODE_ROUTE_HIDDEN,V(id),
#define ROUTE_DISABLED(id) OPCODE_ROUTE_DISABLED,V(id),
#define ROUTE_CAPTION(id,caption) PRINT(caption)
#define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route),
#define SEQUENCE(id) OPCODE_SEQUENCE, V(id),
#define SERIAL(msg) PRINT(msg)
@@ -357,29 +614,48 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#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(freq) OPCODE_SETFREQ,V(freq),
#define SIGNAL(redpin,amberpin,greenpin)
#define SIGNALH(redpin,amberpin,greenpin)
#define SPEED(speed) OPCODE_SPEED,V(speed),
#define START(route) OPCODE_START,V(route),
#define START(route) OPCODE_START,V(route),
#define STASH(id) OPCODE_STASH,V(id),
#define STOP OPCODE_SPEED,V(0),
#define THROW(id) OPCODE_THROW,V(id),
#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
#define TURNOUT(id,addr,subaddr,description...) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr),
#define TURNOUTL(id,addr,description...) TURNOUT(id,(addr-1)/4+1,(addr-1)%4, description)
#define UNJOIN OPCODE_UNJOIN,0,0,
#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
#define WAITFORTT(turntable_id) OPCODE_WAITFORTT,V(turntable_id),
#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__;
const HIGHFLASH byte RMFT2::RouteCode[] = {
const HIGHFLASH3 byte RMFT2::RouteCode[] = {
#include "myAutomation.h"
OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 };

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,71 +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()
{
if (singleton!=NULL) {
DIAG(F("Prog Error!"));
return;
}
if ((singleton=new EthernetInterface()))
return;
DIAG(F("Ethernet not initialized"));
};
/**
* @brief Aquire IP Address from DHCP and start server
*
* @return true
* @return false
*/
EthernetInterface::EthernetInterface()
void EthernetInterface::setup()
{
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
byte mac[6];
DCCTimer::getSimulatedMacAddress(mac);
connected=false;
#ifdef IP_ADDRESS
Ethernet.begin(mac, IP_ADDRESS);
#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);
}
/**
@@ -103,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(IP_ADDRESS); // 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

11
FSH.h
View File

@@ -52,20 +52,27 @@ 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
#define HIGHFLASH __attribute__((section(".fini2")))
#define HIGHFLASH3 __attribute__((section(".fini3")))
#define GETFARPTR(data) pgm_get_far_address(data)
#define GETHIGHFLASH(data,offset) pgm_read_byte_far(GETFARPTR(data)+offset)
#define GETHIGHFLASHW(data,offset) pgm_read_word_far(GETFARPTR(data)+offset)
#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.
#define HIGHFLASH PROGMEM
#define HIGHFLASH3 PROGMEM
#define GETFARPTR(data) ((uint32_t)(data))
#define GETHIGHFLASH(data,offset) pgm_read_byte_near(GETFARPTR(data)+(offset))
#define GETHIGHFLASHW(data,offset) pgm_read_word_near(GETFARPTR(data)+(offset))
#define COPYHIGHFLASH(target,base,offset,length) \
memcpy_P(target,(byte *)base + offset,length)
#endif
#else
@@ -80,14 +87,18 @@ typedef __FlashStringHelper FSH;
typedef char FSH;
#define FLASH
#define HIGHFLASH
#define HIGHFLASH3
#define GETFARPTR(data) ((uint32_t)(data))
#define GETFLASH(addr) (*(const byte *)(addr))
#define GETHIGHFLASH(data,offset) (*(const byte *)(GETFARPTR(data)+offset))
#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 "3bddf4d"
#define GITHUB_SHA "devel-202507181124Z"

View File

@@ -46,25 +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 <= 0x4f)
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");
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("?");
}
@@ -92,7 +106,7 @@ void I2CManagerClass::begin(void) {
// Probe and list devices. Use standard mode
// (clock speed 100kHz) for best device compatibility.
_setClock(100000);
unsigned long originalTimeout = _timeout;
uint32_t originalTimeout = _timeout;
setTimeout(1000); // use 1ms timeout for probes
#if defined(I2C_EXTENDED_ADDRESS)
@@ -363,4 +377,4 @@ void I2CAddress::toHex(const uint8_t value, char *buffer) {
/* static */ bool I2CAddress::_addressWarningDone = false;
#endif
#endif

View File

@@ -485,7 +485,7 @@ private:
// When retries are enabled, the timeout applies to each
// try, and failure from timeout does not get retried.
// A value of 0 means disable timeout monitoring.
unsigned long _timeout = 100000UL;
uint32_t _timeout = 100000UL;
// Finish off request block by waiting for completion and posting status.
uint8_t finishRB(I2CRB *rb, uint8_t status);
@@ -532,14 +532,15 @@ private:
uint8_t bytesToSend = 0;
uint8_t bytesToReceive = 0;
uint8_t operation = 0;
unsigned long startTime = 0;
uint32_t startTime = 0;
uint8_t muxPhase = 0;
uint8_t muxAddress = 0;
uint8_t muxData[1];
uint8_t deviceAddress;
const uint8_t *sendBuffer;
uint8_t *receiveBuffer;
uint8_t transactionState = 0;
volatile uint32_t pendingClockSpeed = 0;
void startTransaction();

View File

@@ -37,21 +37,42 @@
// it can replace use of noInterrupts/interrupts in other parts of DCC-EX.
//
static inline uint8_t _deferInterrupts(void) {
#if defined(ARDUINO_ARCH_STM32)
NVIC_DisableIRQ(I2C1_EV_IRQn);
NVIC_DisableIRQ(I2C1_ER_IRQn);
#else
noInterrupts();
#endif
return 1;
}
static inline void _conditionalEnableInterrupts(bool *wasEnabled) {
#if defined(ARDUINO_ARCH_STM32)
(void)wasEnabled;
NVIC_EnableIRQ(I2C1_EV_IRQn);
NVIC_EnableIRQ(I2C1_ER_IRQn);
#else
if (*wasEnabled) interrupts();
#endif
}
#define ATOMIC_BLOCK(x) \
for (bool _int_saved __attribute__((__cleanup__(_conditionalEnableInterrupts))) \
=_getInterruptState(),_ToDo=_deferInterrupts(); _ToDo; _ToDo=0)
// The construct of
// "variable __attribute__((__cleanup__(func)))"
// calls the func with *variable when variable goes out of scope
#if defined(__AVR__) // Nano, Uno, Mega2580, NanoEvery, etc.
static inline bool _getInterruptState(void) {
return bitRead(SREG, SREG_I); // true if enabled, false if disabled
}
#elif defined(__arm__) // STM32, SAMD, Teensy
#elif defined(ARDUINO_ARCH_STM32)
static inline bool _getInterruptState( void ) {
// as we do ony mess with the I2C interrupts in the STM32 case,
// we do not care about their previous state
return true;
}
#elif defined(__arm__) // SAMD, Teensy
static inline bool _getInterruptState( void ) {
uint32_t reg;
__asm__ __volatile__ ("MRS %0, primask" : "=r" (reg) );
@@ -172,6 +193,10 @@ void I2CManagerClass::startTransaction() {
* Function to queue a request block and initiate operations.
***************************************************************************/
void I2CManagerClass::queueRequest(I2CRB *req) {
if (((req->operation & OPERATION_MASK) == OPERATION_READ) && req->readLen == 0)
return; // Ignore null read
req->status = I2C_STATUS_PENDING;
req->nextRequest = NULL;
ATOMIC_BLOCK() {
@@ -184,6 +209,7 @@ void I2CManagerClass::queueRequest(I2CRB *req) {
}
/***************************************************************************
* Initiate a write to an I2C device (non-blocking operation)
***************************************************************************/
@@ -240,8 +266,8 @@ void I2CManagerClass::checkForTimeout() {
I2CRB *t = queueHead;
if (state==I2C_STATE_ACTIVE && t!=0 && t==currentRequest && _timeout > 0) {
// Check for timeout
unsigned long elapsed = micros() - startTime;
if (elapsed > _timeout) {
int32_t elapsed = micros() - startTime;
if (elapsed > (int32_t)_timeout) {
#ifdef DIAG_IO
//DIAG(F("I2CManager Timeout on %s"), t->i2cAddress.toString());
#endif
@@ -300,12 +326,12 @@ void I2CManagerClass::handleInterrupt() {
// Check if current request has completed. If there's a current request
// and state isn't active then state contains the completion status of the request.
if (state == I2C_STATE_COMPLETED && currentRequest != NULL) {
if (state == I2C_STATE_COMPLETED && currentRequest != NULL && currentRequest == queueHead) {
// Operation has completed.
if (completionStatus == I2C_STATUS_OK || ++retryCounter > MAX_I2C_RETRIES
|| currentRequest->operation & OPERATION_NORETRY)
{
// Status is OK, or has failed and retry count exceeded, or retries disabled.
// Status is OK, or has failed and retry count exceeded, or failed and retries disabled.
#if defined(I2C_EXTENDED_ADDRESS)
if (muxPhase == MuxPhase_PROLOG ) {
overallStatus = completionStatus;
@@ -379,4 +405,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.
*
@@ -26,27 +26,45 @@
#include "I2CManager.h"
#include "I2CManager_NonBlocking.h" // to satisfy intellisense
//#include <avr/io.h>
//#include <avr/interrupt.h>
#include <wiring_private.h>
#include "stm32f4xx_hal_rcc.h"
/***************************************************************************
* Interrupt handler.
* IRQ handler for SERCOM3 which is the default I2C definition for Arduino Zero
* compatible variants such as the Sparkfun SAMD21 Dev Breakout etc.
* Later we may wish to allow use of an alternate I2C bus, or more than one I2C
* bus on the SAMD architecture
***************************************************************************/
/*****************************************************************************
* STM32F4xx I2C native driver support
*
* Nucleo-64 and Nucleo-144 boards all use I2C1 as the default I2C peripheral
* Later we may wish to support other STM32 boards, allow use of an alternate
* I2C bus, or more than one I2C bus on the STM32 architecture
*****************************************************************************/
#if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_ARCH_STM32)
void I2C1_IRQHandler() {
#if defined(ARDUINO_NUCLEO_F401RE) || defined(ARDUINO_NUCLEO_F411RE) || defined(ARDUINO_NUCLEO_F446RE) \
|| 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;
// In init we will ask the STM32 HAL layer for the configured APB1 clock frequency in Hz
uint32_t APB1clk1; // Peripheral Input Clock speed in Hz.
uint32_t i2c_MHz; // Peripheral Input Clock speed in MHz.
// IRQ handler for I2C1, replacing the weak definition in the STM32 HAL
extern "C" void I2C1_EV_IRQHandler(void) {
I2CManager.handleInterrupt();
}
extern "C" void I2C1_ER_IRQHandler(void) {
I2CManager.handleInterrupt();
}
#else
#warning STM32 board selected is not yet supported - so I2C1 peripheral is not defined
#endif
#endif
// Assume I2C1 for now - default I2C bus on Nucleo-F411RE and likely Nucleo-64 variants
I2C_TypeDef *s = I2C1;
#define I2C_IRQn I2C1_EV_IRQn
#define I2C_BUSFREQ 16
// Peripheral Input Clock speed in MHz.
// For STM32F446RE, the speed is 45MHz. Ideally, this should be determined
// at run-time from the APB1 clock, as it can vary from STM32 family to family.
// #define I2C_PERIPH_CLK 45
// I2C SR1 Status Register #1 bit definitions for convenience
// #define I2C_SR1_SMBALERT (1<<15) // SMBus alert
@@ -80,52 +98,65 @@ I2C_TypeDef *s = I2C1;
// #define I2C_CR1_SMBUS (1<<1) // SMBus mode, 1=SMBus, 0=I2C
// #define I2C_CR1_PE (1<<0) // I2C Peripheral enable
// States of the STM32 I2C driver state machine
enum {TS_IDLE,TS_START,TS_W_ADDR,TS_W_DATA,TS_W_STOP,TS_R_ADDR,TS_R_DATA,TS_R_STOP};
/***************************************************************************
* Set I2C clock speed register. This should only be called outside of
* a transmission. The I2CManagerClass::_setClock() function ensures
* that it is only called at the beginning of an I2C transaction.
***************************************************************************/
void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
// Calculate a rise time appropriate to the requested bus speed
// Use 10x the rise time spec to enable integer divide of 62.5ns clock period
// Use 10x the rise time spec to enable integer divide of 50ns clock period
uint16_t t_rise;
uint32_t ccr_freq;
if (i2cClockSpeed < 200000L) {
// i2cClockSpeed = 100000L;
t_rise = 0x11; // (1000ns /62.5ns) + 1;
}
else if (i2cClockSpeed < 800000L)
while (s->CR1 & I2C_CR1_STOP); // Prevents lockup by guarding further
// writes to CR1 while STOP is being executed!
// Disable the I2C device, as TRISE can only be programmed whilst disabled
s->CR1 &= ~(I2C_CR1_PE); // Disable I2C
s->CR1 |= I2C_CR1_SWRST; // reset the I2C
asm("nop"); // wait a bit... suggestion from online!
s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
if (i2cClockSpeed > 100000UL)
{
i2cClockSpeed = 400000L;
t_rise = 0x06; // (300ns / 62.5ns) + 1;
// } else if (i2cClockSpeed < 1200000L) {
// i2cClockSpeed = 1000000L;
// t_rise = 120;
// if (i2cClockSpeed > 400000L)
// i2cClockSpeed = 400000L;
t_rise = 300; // nanoseconds
}
else
{
i2cClockSpeed = 100000L;
t_rise = 0x11; // (1000ns /62.5ns) + 1;
// i2cClockSpeed = 100000L;
t_rise = 1000; // nanoseconds
}
// Enable the I2C master mode
s->CR1 &= ~(I2C_CR1_PE); // Enable I2C
// Software reset the I2C peripheral
// s->CR1 |= I2C_CR1_SWRST; // reset the I2C
// Release reset
// s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
// Calculate baudrate - using a rise time appropriate for the speed
ccr_freq = I2C_BUSFREQ * 1000000 / i2cClockSpeed / 2;
// Configure the rise time register - max allowed tRISE is 1000ns,
// so value = 1000ns * I2C_PERIPH_CLK MHz / 1000 + 1.
s->TRISE = (t_rise * i2c_MHz / 1000) + 1;
// Bit 15: I2C Master mode, 0=standard, 1=Fast Mode
// Bit 14: Duty, fast mode duty cycle
// Bit 11-0: FREQR = 16MHz => TPCLK1 = 62.5ns, so CCR divisor must be 0x50 (80 * 62.5ns = 5000ns)
s->CCR = (uint16_t)ccr_freq;
// Bit 14: Duty, fast mode duty cycle (use 2:1)
// Bit 11-0: FREQR
// if (i2cClockSpeed > 400000UL) {
// // In fast mode plus, I2C period is 3 * CCR * TPCLK1.
// // s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value
// s->CCR = APB1clk1 / 3 / i2cClockSpeed; // Set I2C clockspeed to start!
// s->CCR |= 0xC000; // We need Fast Mode AND DUTY bits set
// } else {
// In standard and fast mode, I2C period is 2 * CCR * TPCLK1
s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value
s->CCR |= (APB1clk1 / 2 / i2cClockSpeed); // Set I2C clockspeed to start!
// s->CCR |= (i2c_MHz * 500 / (i2cClockSpeed / 1000)); // Set I2C clockspeed to start!
// if (i2cClockSpeed > 100000UL)
// s->CCR |= 0xC000; // We need Fast Mode bits set as well
// }
// Configure the rise time register
s->TRISE = t_rise; // 1000 ns / 62.5 ns = 16 + 1
// DIAG(F("I2C_init() peripheral clock is now: %d, full reg is %x"), (s->CR2 & 0xFF), s->CR2);
// DIAG(F("I2C_init() peripheral CCR is now: %d"), s->CCR);
// DIAG(F("I2C_init() peripheral TRISE is now: %d"), s->TRISE);
// Enable the I2C master mode
s->CR1 |= I2C_CR1_PE; // Enable I2C
@@ -136,32 +167,64 @@ void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
***************************************************************************/
void I2CManagerClass::I2C_init()
{
//Setting up the clocks
RCC->APB1ENR |= (1<<21); // Enable I2C CLOCK
RCC->AHB1ENR |= (1<<1); // Enable GPIOB CLOCK for PB8/PB9
// Query the clockspeed from the STM32 HAL layer
APB1clk1 = HAL_RCC_GetPCLK1Freq();
i2c_MHz = APB1clk1 / 1000000UL;
// DIAG(F("I2C_init() peripheral clock speed is: %d"), i2c_MHz);
// Enable clocks
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;//(1 << 21); // Enable I2C CLOCK
// Reset the I2C1 peripheral to initial state
RCC->APB1RSTR |= RCC_APB1RSTR_I2C1RST;
RCC->APB1RSTR &= ~RCC_APB1RSTR_I2C1RST;
// Standard I2C pins are SCL on PB8 and SDA on PB9
RCC->AHB1ENR |= (1<<1); // Enable GPIOB CLOCK for PB8/PB9
// Bits (17:16)= 1:0 --> Alternate Function for Pin PB8;
// Bits (19:18)= 1:0 --> Alternate Function for Pin PB9
GPIOB->MODER &= ~((3<<(8*2)) | (3<<(9*2))); // Clear all MODER bits for PB8 and PB9
GPIOB->MODER |= (2<<(8*2)) | (2<<(9*2)); // PB8 and PB9 set to ALT function
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 |= (1<<(8*2)) | (1<<(9*2)); // PB8 and PB9 set to pull-up capability
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
// 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
GPIOB->AFR[1] &= ~((15<<0) | (15<<4)); // Clear all AFR bits for PB8 on low nibble, PB9 on next nibble up
GPIOB->AFR[1] |= (4<<0) | (4<<4); // PB8 on low nibble, PB9 on next nibble up
// Software reset the I2C peripheral
I2C1->CR1 &= ~I2C_CR1_PE; // Disable I2C1 peripheral
s->CR1 |= I2C_CR1_SWRST; // reset the I2C
s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
asm("nop"); // wait a bit... suggestion from online!
s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
// Program the peripheral input clock in CR2 Register in order to generate correct timings
s->CR2 |= I2C_BUSFREQ; // PCLK1 FREQUENCY in MHz
// Clear all bits in I2C CR2 register except reserved bits
s->CR2 &= 0xE000;
// Set I2C peripheral clock frequency
// s->CR2 |= I2C_PERIPH_CLK;
s->CR2 |= i2c_MHz;
// DIAG(F("I2C_init() peripheral clock is now: %d"), s->CR2);
// set own address to 00 - not used in master mode
I2C1->OAR1 = (1 << 14); // bit 14 should be kept at 1 according to the datasheet
#if defined(I2C_USE_INTERRUPTS)
// Setting NVIC
NVIC_SetPriority(I2C_IRQn, 1); // Match default priorities
NVIC_EnableIRQ(I2C_IRQn);
NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); // 4 means that we have all bits for preemptive grouping
// prio scheme:
// systick : 0
// waveform timer : 1
// i2c : 2
// one must call NVIC_EncodePriority() to bitshift the priorities
// according to the active priority grouping and then use that
// value as argument to NVIC_SetPriority().
NVIC_SetPriority(I2C1_EV_IRQn,
NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 2, 0));
NVIC_EnableIRQ(I2C1_EV_IRQn);
NVIC_SetPriority(I2C1_ER_IRQn,
NVIC_EncodePriority(NVIC_GetPriorityGrouping(), 2, 0));
NVIC_EnableIRQ(I2C1_ER_IRQn);
// CR2 Interrupt Settings
// Bit 15-13: reserved
@@ -172,23 +235,28 @@ void I2CManagerClass::I2C_init()
// Bit 8: ITERREN - Error interrupt enable
// Bit 7-6: reserved
// Bit 5-0: FREQ - Peripheral clock frequency (max 50MHz)
// s->CR2 |= 0x0700; // Enable Buffer, Event and Error interrupts
s->CR2 |= 0x0300; // Enable Event and Error interrupts
s->CR2 |= (I2C_CR2_ITBUFEN | I2C_CR2_ITEVTEN | I2C_CR2_ITERREN); // Enable Buffer, Event and Error interrupts
#endif
// DIAG(F("I2C_init() setting initial I2C clock to 100KHz"));
// Calculate baudrate and set default rate for now
// Configure the Clock Control Register for 100KHz SCL frequency
// Bit 15: I2C Master mode, 0=standard, 1=Fast Mode
// Bit 14: Duty, fast mode duty cycle
// Bit 11-0: FREQR = 16MHz => TPCLK1 = 62.5ns, so CCR divisor must be 0x50 (80 * 62.5ns = 5000ns)
s->CCR = 0x0050;
// Bit 11-0: so CCR divisor would be clk / 2 / 100000 (where clk is in Hz)
// s->CCR = I2C_PERIPH_CLK * 5;
s->CCR &= ~(0x3000); // Clear all bits except 12 and 13 which must remain per reset value
s->CCR |= (APB1clk1 / 2 / 100000UL); // Set a default of 100KHz I2C clockspeed to start!
// Configure the rise time register - max allowed in 1000ns
s->TRISE = 0x0011; // 1000 ns / 62.5 ns = 16 + 1
// Configure the rise time register - max allowed is 1000ns, so value = 1000ns * I2C_PERIPH_CLK MHz / 1000 + 1.
s->TRISE = (1000 * i2c_MHz / 1000) + 1;
// DIAG(F("I2C_init() peripheral clock is now: %d, full reg is %x"), (s->CR2 & 0xFF), s->CR2);
// DIAG(F("I2C_init() peripheral CCR is now: %d"), s->CCR);
// DIAG(F("I2C_init() peripheral TRISE is now: %d"), s->TRISE);
// Enable the I2C master mode
s->CR1 |= I2C_CR1_PE; // Enable I2C
// Setting bus idle mode and wait for sync
}
/***************************************************************************
@@ -198,49 +266,30 @@ void I2CManagerClass::I2C_sendStart() {
// Set counters here in case this is a retry.
rxCount = txCount = 0;
uint8_t temp;
// On a single-master I2C bus, the start bit won't be sent until the bus
// state goes to IDLE so we can request it without waiting. On a
// multi-master bus, the bus may be BUSY under control of another master,
// On a single-master I2C bus, the start bit won't be sent until the bus
// state goes to IDLE so we can request it without waiting. On a
// multi-master bus, the bus may be BUSY under control of another master,
// in which case we can avoid some arbitration failures by waiting until
// the bus state is IDLE. We don't do that here.
//while (s->SR2 & I2C_SR2_BUSY) {}
// If anything to send, initiate write. Otherwise initiate read.
if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend))
{
// Send start for read operation
s->CR1 |= I2C_CR1_ACK; // Enable the ACK
s->CR1 |= I2C_CR1_START; // Generate START
// Send address with read flag (1) or'd in
s->DR = (deviceAddress << 1) | 1; // send the address
while (!(s->SR1 && I2C_SR1_ADDR)); // wait for ADDR bit to set
// Special case for 1 byte reads!
if (bytesToReceive == 1)
{
s->CR1 &= ~I2C_CR1_ACK; // clear the ACK bit
temp = I2C1->SR1 | I2C1->SR2; // read SR1 and SR2 to clear the ADDR bit.... EV6 condition
s->CR1 |= I2C_CR1_STOP; // Stop I2C
}
else
temp = s->SR1 | s->SR2; // read SR1 and SR2 to clear the ADDR bit
}
else {
// Send start for write operation
s->CR1 |= I2C_CR1_ACK; // Enable the ACK
s->CR1 |= I2C_CR1_START; // Generate START
// Send address with write flag (0) or'd in
s->DR = (deviceAddress << 1) | 0; // send the address
while (!(s->SR1 && I2C_SR1_ADDR)); // wait for ADDR bit to set
temp = s->SR1 | s->SR2; // read SR1 and SR2 to clear the ADDR bit
}
// Check there's no STOP still in progress. If we OR the START bit into CR1
// and the STOP bit is already set, we could output multiple STOP conditions.
while (s->CR1 & I2C_CR1_STOP) {} // Wait for STOP bit to reset
s->CR2 |= (I2C_CR2_ITEVTEN | I2C_CR2_ITERREN); // Enable interrupts
s->CR2 &= ~I2C_CR2_ITBUFEN; // Don't enable buffer interupts yet.
s->CR1 &= ~I2C_CR1_POS; // Clear the POS bit
s->CR1 |= (I2C_CR1_ACK | I2C_CR1_START); // Enable the ACK and generate START
transactionState = TS_START;
}
/***************************************************************************
* Initiate a stop bit for transmission (does not interrupt)
***************************************************************************/
void I2CManagerClass::I2C_sendStop() {
s->CR1 |= I2C_CR1_STOP; // Stop I2C
s->CR1 |= I2C_CR1_STOP; // Stop I2C
}
/***************************************************************************
@@ -252,9 +301,11 @@ void I2CManagerClass::I2C_close() {
s->CR1 &= ~I2C_CR1_PE; // Disable I2C peripheral
// Should never happen, but wait for up to 500us only.
unsigned long startTime = micros();
while ((s->CR1 && I2C_CR1_PE) != 0) {
if (micros() - startTime >= 500UL) break;
while ((s->CR1 & I2C_CR1_PE) != 0) {
if ((int32_t)(micros() - startTime) >= 500) break;
}
NVIC_DisableIRQ(I2C1_EV_IRQn);
NVIC_DisableIRQ(I2C1_ER_IRQn);
}
/***************************************************************************
@@ -263,50 +314,218 @@ void I2CManagerClass::I2C_close() {
* (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).
***************************************************************************/
void I2CManagerClass::I2C_handleInterrupt() {
volatile uint16_t temp_sr1, temp_sr2;
(void) temp_sr2; // only used as target for reads
if (s->SR1 && I2C_SR1_ARLO) {
// Arbitration lost, restart
I2C_sendStart(); // Reinitiate request
} else if (s->SR1 && I2C_SR1_BERR) {
// Bus error
completionStatus = I2C_STATUS_BUS_ERROR;
state = I2C_STATE_COMPLETED;
} else if (s->SR1 && I2C_SR1_TXE) {
// Master write completed
if (s->SR1 && (1<<10)) {
// Nacked, send stop.
I2C_sendStop();
temp_sr1 = s->SR1;
// Check for errors first
if (temp_sr1 & (I2C_SR1_AF | I2C_SR1_ARLO | I2C_SR1_BERR)) {
// Check which error flag is set
if (temp_sr1 & I2C_SR1_AF)
{
s->SR1 &= ~(I2C_SR1_AF); // Clear AF
I2C_sendStop(); // Clear the bus
transactionState = TS_IDLE;
completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
state = I2C_STATE_COMPLETED;
} else if (bytesToSend) {
// Acked, so send next byte
s->DR = sendBuffer[txCount++];
bytesToSend--;
} else if (bytesToReceive) {
// Last sent byte acked and no more to send. Send repeated start, address and read bit.
// s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1) | 1;
} else {
// Check both TxE/BTF == 1 before generating stop
while (!(s->SR1 && I2C_SR1_TXE)); // Check TxE
while (!(s->SR1 && I2C_SR1_BTF)); // Check BTF
// No more data to send/receive. Initiate a STOP condition and finish
I2C_sendStop();
}
else if (temp_sr1 & I2C_SR1_ARLO)
{
// Arbitration lost, restart
s->SR1 &= ~(I2C_SR1_ARLO); // Clear ARLO
I2C_sendStart(); // Reinitiate request
transactionState = TS_START;
}
else if (temp_sr1 & I2C_SR1_BERR)
{
// Bus error
s->SR1 &= ~(I2C_SR1_BERR); // Clear BERR
I2C_sendStop(); // Clear the bus
transactionState = TS_IDLE;
completionStatus = I2C_STATUS_BUS_ERROR;
state = I2C_STATE_COMPLETED;
}
} else if (s->SR1 && I2C_SR1_RXNE) {
// Master read completed without errors
if (bytesToReceive == 1) {
// s->I2CM.CTRLB.bit.ACKACT = 1; // NAK final byte
I2C_sendStop(); // send stop
receiveBuffer[rxCount++] = s->DR; // Store received byte
bytesToReceive = 0;
state = I2C_STATE_COMPLETED;
} else if (bytesToReceive) {
// s->I2CM.CTRLB.bit.ACKACT = 0; // ACK all but final byte
receiveBuffer[rxCount++] = s->DR; // Store received byte
bytesToReceive--;
}
else {
// No error flags, so process event according to current state.
switch (transactionState) {
case TS_START:
if (temp_sr1 & I2C_SR1_SB) {
// Event EV5
// Start bit has been sent successfully and we have the bus.
// If anything to send, initiate write. Otherwise initiate read.
if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend)) {
// Send address with read flag (1) or'd in
s->DR = (deviceAddress << 1) | 1; // send the address
transactionState = TS_R_ADDR;
} else {
// Send address with write flag (0) or'd in
s->DR = (deviceAddress << 1) | 0; // send the address
transactionState = TS_W_ADDR;
}
}
// SB bit is cleared by writing to DR (already done).
break;
case TS_W_ADDR:
if (temp_sr1 & I2C_SR1_ADDR) {
temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit
// Event EV6
// Address sent successfully, device has ack'd in response.
if (!bytesToSend) {
I2C_sendStop();
transactionState = TS_IDLE;
completionStatus = I2C_STATUS_OK;
state = I2C_STATE_COMPLETED;
} else {
// Put one byte into DR to load shift register.
s->DR = sendBuffer[txCount++];
bytesToSend--;
if (bytesToSend) {
// Put another byte to load DR
s->DR = sendBuffer[txCount++];
bytesToSend--;
}
if (!bytesToSend) {
// No more bytes to send.
// The TXE interrupt occurs when the DR is empty, and the BTF interrupt
// occurs when the shift register is also empty (one character later).
// To avoid repeated TXE interrupts during this time, we disable TXE interrupt.
s->CR2 &= ~I2C_CR2_ITBUFEN; // Wait for BTF interrupt, disable TXE interrupt
transactionState = TS_W_STOP;
} else {
// More data remaining to send after this interrupt, enable TXE interrupt.
s->CR2 |= I2C_CR2_ITBUFEN;
transactionState = TS_W_DATA;
}
}
}
break;
case TS_W_DATA:
if (temp_sr1 & I2C_SR1_TXE) {
// Event EV8_1/EV8
// Transmitter empty, write a byte to it.
if (bytesToSend) {
s->DR = sendBuffer[txCount++];
bytesToSend--;
if (!bytesToSend) {
s->CR2 &= ~I2C_CR2_ITBUFEN; // Disable TXE interrupt
transactionState = TS_W_STOP;
}
}
}
break;
case TS_W_STOP:
if (temp_sr1 & I2C_SR1_BTF) {
// Event EV8_2
// Done, last character sent. Anything to receive?
if (bytesToReceive) {
I2C_sendStart();
// NOTE: Three redundant BTF interrupts take place between the
// first BTF interrupt and the START interrupt. I've tried all sorts
// of ways to eliminate them, and the only thing that worked for
// me was to loop until the BTF bit becomes reset. Either way,
// it's a waste of processor time. Anyone got a solution?
//while (s->SR1 && I2C_SR1_BTF) {}
transactionState = TS_START;
} else {
I2C_sendStop();
transactionState = TS_IDLE;
completionStatus = I2C_STATUS_OK;
state = I2C_STATE_COMPLETED;
}
s->SR1 &= I2C_SR1_BTF; // Clear BTF interrupt
}
break;
case TS_R_ADDR:
if (temp_sr1 & I2C_SR1_ADDR) {
// Event EV6
// Address sent for receive.
// The next bit is different depending on whether there are
// 1 byte, 2 bytes or >2 bytes to be received, in accordance with the
// Programmers Reference RM0390.
if (bytesToReceive == 1) {
// Receive 1 byte
s->CR1 &= ~I2C_CR1_ACK; // Disable ack
temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit
// Next step will occur after a RXNE interrupt, so enable it
s->CR2 |= I2C_CR2_ITBUFEN;
transactionState = TS_R_STOP;
} else if (bytesToReceive == 2) {
// Receive 2 bytes
s->CR1 &= ~I2C_CR1_ACK; // Disable ACK for final byte
s->CR1 |= I2C_CR1_POS; // set POS flag to delay effect of ACK flag
// Next step will occur after a BTF interrupt, so disable RXNE interrupt
s->CR2 &= ~I2C_CR2_ITBUFEN;
temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit
transactionState = TS_R_STOP;
} else {
// >2 bytes, just wait for bytes to come in and ack them for the time being
// (ack flag has already been set).
// Next step will occur after a BTF interrupt, so disable RXNE interrupt
s->CR2 &= ~I2C_CR2_ITBUFEN;
temp_sr2 = s->SR2; // read SR2 to complete clearing the ADDR bit
transactionState = TS_R_DATA;
}
}
break;
case TS_R_DATA:
// Event EV7/EV7_1
if (temp_sr1 & I2C_SR1_BTF) {
// Byte received in receiver - read next byte
if (bytesToReceive == 3) {
// Getting close to the last byte, so a specific sequence is recommended.
s->CR1 &= ~I2C_CR1_ACK; // Reset ack for next byte received.
transactionState = TS_R_STOP;
}
receiveBuffer[rxCount++] = s->DR; // Store received byte
bytesToReceive--;
}
break;
case TS_R_STOP:
if (temp_sr1 & I2C_SR1_BTF) {
// Event EV7 (last one)
// When we've got here, the receiver has got the last two bytes
// (or one byte, if only one byte is being received),
// and NAK has already been sent, so we need to read from the receiver.
if (bytesToReceive) {
if (bytesToReceive > 1)
I2C_sendStop();
while(bytesToReceive) {
receiveBuffer[rxCount++] = s->DR; // Store received byte(s)
bytesToReceive--;
}
// Finish.
transactionState = TS_IDLE;
completionStatus = I2C_STATUS_OK;
state = I2C_STATE_COMPLETED;
}
} else if (temp_sr1 & I2C_SR1_RXNE) {
if (bytesToReceive == 1) {
// One byte on a single-byte transfer. Ack has already been set.
I2C_sendStop();
receiveBuffer[rxCount++] = s->DR; // Store received byte
bytesToReceive--;
// Finish.
transactionState = TS_IDLE;
completionStatus = I2C_STATUS_OK;
state = I2C_STATE_COMPLETED;
} else
s->SR1 &= I2C_SR1_RXNE; // Acknowledge interrupt
}
break;
}
// If we've received an interrupt at any other time, we're not interested so clear it
// to prevent it recurring ad infinitum.
s->SR1 = 0;
}
}
#endif /* I2CMANAGER_STM32_H */

View File

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

View File

@@ -33,7 +33,9 @@
// Link to halSetup function. If not defined, the function reference will be NULL.
extern __attribute__((weak)) void halSetup();
extern __attribute__((weak)) void exrailHalSetup();
extern __attribute__((weak)) bool exrailHalSetup1();
extern __attribute__((weak)) bool exrailHalSetup2();
//==================================================================================================================
// Static methods
@@ -59,35 +61,46 @@ void IODevice::begin() {
if (halSetup)
halSetup();
// include any HAL devices defined in exrail.
if (exrailHalSetup)
exrailHalSetup();
// Include any HAL devices defined in exrail.
// The first pass call only creates HAL devices,
// the second pass will apply servo settings etc which can only be
// done after all devices (including the defaults) are created.
// If exrailHalSetup1 is not defined, then it will be NULL and the call
// will be ignored.
// If it returns true, then the default HAL devices will not be created.
// Predefine two PCA9685 modules 0x40-0x41 if no conflicts
// Allocates 32 pins 100-131
if (checkNoOverlap(100, 16, 0x40)) {
PCA9685::create(100, 16, 0x40);
} else {
DIAG(F("Default PCA9685 at I2C 0x40 disabled due to configured user device"));
}
if (checkNoOverlap(116, 16, 0x41)) {
PCA9685::create(116, 16, 0x41);
} else {
DIAG(F("Default PCA9685 at I2C 0x41 disabled due to configured user device"));
}
bool ignoreDefaults=false;
if (exrailHalSetup1)
ignoreDefaults=exrailHalSetup1();
// Predefine two MCP23017 module 0x20/0x21 if no conflicts
// Allocates 32 pins 164-195
if (checkNoOverlap(164, 16, 0x20)) {
MCP23017::create(164, 16, 0x20);
} else {
DIAG(F("Default MCP23017 at I2C 0x20 disabled due to configured user device"));
}
if (checkNoOverlap(180, 16, 0x21)) {
MCP23017::create(180, 16, 0x21);
} else {
DIAG(F("Default MCP23017 at I2C 0x21 disabled due to configured user device"));
if (!ignoreDefaults) {
// Predefine two PCA9685 modules 0x40-0x41 if no conflicts
// Allocates 32 pins 100-131
const bool silent=true; // no message if these conflict
if (checkNoOverlap(100, 16, 0x40, silent)) {
PCA9685::create(100, 16, 0x40);
}
if (checkNoOverlap(116, 16, 0x41, silent)) {
PCA9685::create(116, 16, 0x41);
}
// Predefine two MCP23017 module 0x20/0x21 if no conflicts
// Allocates 32 pins 164-195
if (checkNoOverlap(164, 16, 0x20, silent)) {
MCP23017::create(164, 16, 0x20);
}
if (checkNoOverlap(180, 16, 0x21, silent)) {
MCP23017::create(180, 16, 0x21);
}
}
// apply any second pass HAL setup from EXRAIL.
// This will typically set up servo profiles, or create turnouts.
if (exrailHalSetup2)
exrailHalSetup2();
}
// reset() function to reinitialise all devices
@@ -176,6 +189,13 @@ bool IODevice::exists(VPIN vpin) {
return findDevice(vpin) != NULL;
}
// Return the status of the device att vpin.
uint8_t IODevice::getStatus(VPIN vpin) {
IODevice *dev = findDevice(vpin);
if (!dev) return false;
return dev->_deviceState;
}
// check whether the pin supports notification. If so, then regular _read calls are not required.
bool IODevice::hasCallback(VPIN vpin) {
IODevice *dev = findDevice(vpin);
@@ -247,6 +267,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.
//
@@ -266,6 +306,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.
@@ -332,7 +390,10 @@ IODevice *IODevice::findDeviceFollowing(VPIN vpin) {
// returns true if pins DONT overlap with existing device
// TODO: Move the I2C address reservation and checks into the I2CManager code.
// That will enable non-HAL devices to reserve I2C addresses too.
bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddress) {
// Silent is used by the default setup so that there is no message if the default
// device has already been handled by the user setup.
bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins,
I2CAddress i2cAddress, bool silent) {
#ifdef DIAG_IO
DIAG(F("Check no overlap %u %u %s"), firstPin,nPins,i2cAddress.toString());
#endif
@@ -345,14 +406,14 @@ bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddres
VPIN lastDevPin=firstDevPin+dev->_nPins-1;
bool noOverlap= firstPin>lastDevPin || lastPin<firstDevPin;
if (!noOverlap) {
DIAG(F("WARNING HAL Overlap, redefinition of Vpins %u to %u ignored."),
if (!silent) DIAG(F("WARNING HAL Overlap, redefinition of Vpins %u to %u ignored."),
firstPin, lastPin);
return false;
}
}
// Check for overlapping I2C address
if (i2cAddress && dev->_I2CAddress==i2cAddress) {
DIAG(F("WARNING HAL Overlap. i2c Addr %s ignored."),i2cAddress.toString());
if (!silent) DIAG(F("WARNING HAL Overlap. i2c Addr %s ignored."),i2cAddress.toString());
return false;
}
}
@@ -582,4 +643,3 @@ bool ArduinoPins::fastReadDigital(uint8_t pin) {
#endif
return result;
}

View File

@@ -27,12 +27,6 @@
// Define symbol DIAG_LOOPTIMES to enable CS loop execution time to be reported
//#define DIAG_LOOPTIMES
// Define symbol IO_NO_HAL to reduce FLASH footprint when HAL features not required
// The HAL is disabled by default on Nano and Uno platforms, because of limited flash space.
#if defined(ARDUINO_AVR_NANO) || defined(ARDUINO_AVR_UNO)
#define IO_NO_HAL
#endif
// Define symbol IO_SWITCH_OFF_SERVO to set the PCA9685 output to 0 when an
// animation has completed. This switches off the servo motor, preventing
// the continuous buzz sometimes found on servos, and reducing the
@@ -44,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.
@@ -134,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.
@@ -160,6 +157,9 @@ public:
// exists checks whether there is a device owning the specified vpin
static bool exists(VPIN vpin);
// getStatus returns the state of the device at the specified vpin
static uint8_t getStatus(VPIN vpin);
// Enable shared interrupt on specified pin for GPIO extender modules. The extender module
// should pull down this pin when requesting a scan. The pin may be shared by multiple modules.
// Without the shared interrupt, input states are scanned periodically to detect changes on
@@ -169,7 +169,8 @@ public:
void setGPIOInterruptPin(int16_t pinNumber);
// Method to check if pins will overlap before creating new device.
static bool checkNoOverlap(VPIN firstPin, uint8_t nPins=1, I2CAddress i2cAddress=0);
static bool checkNoOverlap(VPIN firstPin, uint8_t nPins=1,
I2CAddress i2cAddress=0, bool silent=false);
// Method used by IODevice filters to locate slave pins that may be overlayed by their own
// pin range.
@@ -179,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) {
@@ -383,6 +402,7 @@ private:
uint8_t *_pinInUse;
};
#ifndef IO_NO_HAL
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for EX-Turntable.
@@ -411,10 +431,14 @@ private:
void _begin() override;
void _loop(unsigned long currentMicros) override;
int _read(VPIN vpin) override;
void _broadcastStatus (VPIN vpin, uint8_t status, uint8_t activity);
void _writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) override;
void _display() override;
uint8_t _stepperStatus;
uint8_t _previousStatus;
uint8_t _currentActivity;
};
#endif
/////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -536,12 +560,6 @@ protected:
};
#include "IO_MCP23008.h"
#include "IO_MCP23017.h"
#include "IO_PCF8574.h"
#include "IO_PCF8575.h"
#include "IO_duinoNodes.h"
#include "IO_EXIOExpander.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

91
IO_Bitmap.h Normal file
View File

@@ -0,0 +1,91 @@
/*
* © 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 {
(void)profile; // suppress warning, not used in this function
(void)duration; // suppress warning, not used in this function
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

View File

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

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

View File

@@ -20,20 +20,21 @@
/*
* The IO_EXTurntable device driver is used to control a turntable via an Arduino with a stepper motor over I2C.
*
* The EX-Turntable code lives in a separate repo (https://github.com/DCC-EX/Turntable-EX) and contains the stepper motor logic.
* The EX-Turntable code lives in a separate repo (https://github.com/DCC-EX/EX-Turntable) and contains the stepper motor logic.
*
* This device driver sends a step position to Turntable-EX to indicate the step position to move to using either of these commands:
* This device driver sends a step position to EX-Turntable to indicate the step position to move to using either of these commands:
* <D TT vpin steps activity> in the serial console
* MOVETT(vpin, steps, activity) in EX-RAIL
* Refer to the documentation for further information including the valid activities.
*/
#ifndef IO_EXTurntable_h
#define IO_EXTurntable_h
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "Turntables.h"
#include "CommandDistributor.h"
#ifndef IO_NO_HAL
void EXTurntable::create(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {
new EXTurntable(firstVpin, nPins, I2CAddress);
@@ -44,6 +45,8 @@ EXTurntable::EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {
_firstVpin = firstVpin;
_nPins = nPins;
_I2CAddress = I2CAddress;
_stepperStatus = 0;
_previousStatus = 0;
addDevice(this);
}
@@ -51,6 +54,7 @@ EXTurntable::EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {
void EXTurntable::_begin() {
I2CManager.begin();
if (I2CManager.exists(_I2CAddress)) {
DIAG(F("EX-Turntable device found, I2C:%s"), _I2CAddress.toString());
#ifdef DIAG_IO
_display();
#endif
@@ -67,15 +71,20 @@ void EXTurntable::_loop(unsigned long currentMicros) {
uint8_t readBuffer[1];
I2CManager.read(_I2CAddress, readBuffer, 1);
_stepperStatus = readBuffer[0];
// DIAG(F("Turntable-EX returned status: %d"), _stepperStatus);
delayUntil(currentMicros + 500000); // Wait 500ms before checking again, turntables turn slowly
if (_stepperStatus != _previousStatus && _stepperStatus == 0) { // Broadcast when a rotation finishes
if ( _currentActivity < 4) {
_broadcastStatus(_firstVpin, _stepperStatus, _currentActivity);
}
_previousStatus = _stepperStatus;
}
delayUntil(currentMicros + 100000); // Wait 100ms before checking again
}
// Read returns status as obtained in our loop.
// Return false if our status value is invalid.
int EXTurntable::_read(VPIN vpin) {
(void)vpin; // surpress warning
if (_deviceState == DEVSTATE_FAILED) return 0;
// DIAG(F("_read status: %d"), _stepperStatus);
if (_stepperStatus > 1) {
return false;
} else {
@@ -83,6 +92,17 @@ int EXTurntable::_read(VPIN vpin) {
}
}
// If a status change has occurred for a turntable object, broadcast it
void EXTurntable::_broadcastStatus (VPIN vpin, uint8_t status, uint8_t activity) {
Turntable *tto = Turntable::getByVpin(vpin);
if (tto) {
if (activity < 4) {
tto->setMoving(status);
CommandDistributor::broadcastTurntable(tto->getId(), tto->getPosition(), status);
}
}
}
// writeAnalogue to send the steps and activity to Turntable-EX.
// Sends 3 bytes containing the MSB and LSB of the step count, and activity.
// value contains the steps, bit shifted to MSB + LSB.
@@ -100,6 +120,7 @@ int EXTurntable::_read(VPIN vpin) {
// Acc_Off = 9 // Turn accessory pin off
void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) {
if (_deviceState == DEVSTATE_FAILED) return;
if (value < 0) return;
uint8_t stepsMSB = value >> 8;
uint8_t stepsLSB = value & 0xFF;
#ifdef DIAG_IO
@@ -107,8 +128,13 @@ void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_
vpin, value, activity, duration);
DIAG(F("I2CManager write I2C Address:%d stepsMSB:%d stepsLSB:%d activity:%d"),
_I2CAddress.toString(), stepsMSB, stepsLSB, activity);
#else
(void)duration;
#endif
_stepperStatus = 1; // Tell the device driver Turntable-EX is busy
if (activity < 4) _stepperStatus = 1; // Tell the device driver Turntable-EX is busy
_previousStatus = _stepperStatus;
_currentActivity = activity;
_broadcastStatus(vpin, _stepperStatus, activity); // Broadcast when the rotation starts
I2CManager.write(_I2CAddress, 3, stepsMSB, stepsLSB, activity);
}

147
IO_EncoderThrottle.cpp Normal file
View File

@@ -0,0 +1,147 @@
/*
* © 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) {
(void) currentMicros; // suppress warning
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);
}
}
// Set locoid 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)vpin; // not used, but needed to match IODevice interface
(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

818
IO_I2CDFPlayer.h Normal file
View File

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

122
IO_I2CRailcom.cpp Normal file
View File

@@ -0,0 +1,122 @@
/*
* © 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) {
(void)currentMicros; // not used, but needed to match IODevice interface
// 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

101
IO_PCA9554.h Normal file
View File

@@ -0,0 +1,101 @@
/*
* © 2025, 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_pca9554_h
#define io_pca9554_h
#include "IO_GPIOBase.h"
#include "FSH.h"
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for PCA9554/TCA9554 8-bit I/O expander (NXP & Texas Instruments).
*/
class PCA9554 : public GPIOBase<uint8_t> {
public:
static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
if (checkNoOverlap(vpin, nPins, i2cAddress)) new PCA9554(vpin,nPins, i2cAddress, interruptPin);
}
private:
// Constructor
PCA9554(VPIN vpin, uint8_t nPins, I2CAddress I2CAddress, int interruptPin=-1)
: GPIOBase<uint8_t>((FSH *)F("PCA9554"), vpin, nPins, I2CAddress, interruptPin)
{
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer));
outputBuffer[0] = REG_INPUT_P0;
}
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 2, REG_OUTPUT_P0, _portOutputState);
}
void _writePullups() override {
// Do nothing, pull-ups are always in place for input ports
// This function is here for HAL GPIOBase API compatibilitiy
}
void _writePortModes() override {
// Write 0 to REG_CONF_P0 for in-use pins that are outputs, 1 for others.
// PCA9554 & TCA9554, Interrupt is always enabled for raising and falling edge
uint8_t temp = ~(_portMode & _portInUse);
I2CManager.write(_I2CAddress, 2, REG_CONF_P0, temp);
}
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer[1];
I2CManager.read(_I2CAddress, buffer, 1, 1, REG_INPUT_P0);
_portInputState = buffer[0];
} else {
// Queue new request
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status == I2C_STATUS_OK)
_portInputState = inputBuffer[0];
else
_portInputState = 0xff;
}
void _setupDevice() override {
// HAL API calls
_writePortModes();
_writePullups();
_writeGpioPort();
}
uint8_t inputBuffer[1];
uint8_t outputBuffer[1];
enum {
REG_INPUT_P0 = 0x00,
REG_OUTPUT_P0 = 0x01,
REG_POL_INV_P0 = 0x02,
REG_CONF_P0 = 0x03,
};
};
#endif

View File

@@ -30,20 +30,19 @@
class PCA9555 : public GPIOBase<uint16_t> {
public:
static void create(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1) {
new PCA9555(vpin, min(nPins,16), I2CAddress, interruptPin);
static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
if (checkNoOverlap(vpin, nPins, i2cAddress)) new PCA9555(vpin,nPins, i2cAddress, interruptPin);
}
private:
// Constructor
PCA9555(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1)
PCA9555(VPIN vpin, uint8_t nPins, I2CAddress I2CAddress, int interruptPin=-1)
: GPIOBase<uint16_t>((FSH *)F("PCA9555"), vpin, nPins, I2CAddress, interruptPin)
{
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer));
outputBuffer[0] = REG_INPUT_P0;
}
private:
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 3, REG_OUTPUT_P0, _portOutputState, _portOutputState>>8);
}

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

@@ -42,9 +42,9 @@
* Defining in myAutomation.h requires the device driver to be included in addition to the HAL() statement. Examples:
*
* #include "IO_RotaryEncoder.h"
* HAL(RotaryEncoder, 700, 1, 0x70) // Define single Vpin, no feedback or position sent to rotary encoder software
* HAL(RotaryEncoder, 700, 2, 0x70) // Define two Vpins, feedback only sent to rotary encoder software
* HAL(RotaryEncoder, 700, 3, 0x70) // Define three Vpins, can send feedback and position update to rotary encoder software
* HAL(RotaryEncoder, 700, 1, 0x67) // Define single Vpin, no feedback or position sent to rotary encoder software
* HAL(RotaryEncoder, 700, 2, 0x67) // Define two Vpins, feedback only sent to rotary encoder software
* HAL(RotaryEncoder, 700, 3, 0x67) // Define three Vpins, can send feedback and position update to rotary encoder software
*
* Refer to the documentation for further information including the valid activities and examples.
*/
@@ -136,6 +136,7 @@ private:
// Return the position sent by the rotary encoder software
int _readAnalogue(VPIN vpin) override {
(void)vpin; // suppress warning, not used in this function
if (_deviceState == DEVSTATE_FAILED) return 0;
return _position;
}
@@ -153,6 +154,8 @@ private:
// To be valid, must be 0 to 255, and different to the current position
// If the current position is the same, it was initiated by the rotary encoder
void _writeAnalogue(VPIN vpin, int position, uint8_t profile, uint16_t duration) override {
(void)profile; // suppress warning, not used in this function
(void)duration; // suppress warning, not used in this function
if (vpin == _firstVpin + 2) {
if (position >= 0 && position <= 255 && position != _position) {
byte newPosition = position & 0xFF;

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

98
IO_trainbrains.h Normal file
View File

@@ -0,0 +1,98 @@
/*
* © 2023, Chris Harlow. All rights reserved.
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef io_trainbrains_h
#define io_trainbrains_h
#include "IO_GPIOBase.h"
#include "FSH.h"
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for trainbrains 3-block occupancy detector.
* For details see http://trainbrains.eu
*/
enum TrackUnoccupancy
{
TRACK_UNOCCUPANCY_UNKNOWN = 0,
TRACK_OCCUPIED = 1,
TRACK_UNOCCUPIED = 2
};
class Trainbrains02 : public GPIOBase<uint16_t> {
public:
static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress) {
if (checkNoOverlap(vpin, nPins, i2cAddress)) new Trainbrains02(vpin, nPins, i2cAddress);
}
private:
// Constructor
Trainbrains02(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
: GPIOBase<uint16_t>((FSH *)F("Trainbrains02"), vpin, nPins, i2cAddress, interruptPin)
{
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer));
outputBuffer[0] = (uint8_t)_I2CAddress; // strips away the mux part.
outputBuffer[1] =14;
outputBuffer[2] =1;
outputBuffer[3] =0; // This is the channel updated at each poling call
outputBuffer[4] =0;
outputBuffer[5] =0;
outputBuffer[6] =0;
outputBuffer[7] =0;
outputBuffer[8] =0;
outputBuffer[9] =0;
}
void _writeGpioPort() override {}
void _readGpioPort(bool immediate) override {
// cycle channel on device each time
outputBuffer[3]=channelInProgress+1; // 1-origin
channelInProgress++;
if(channelInProgress>=_nPins) channelInProgress=0;
if (immediate) {
_processCompletion(I2CManager.read(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer)));
} else {
// Queue new request
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status != I2C_STATUS_OK) inputBuffer[6]=TRACK_UNOCCUPANCY_UNKNOWN;
if (inputBuffer[6] == TRACK_UNOCCUPIED ) _portInputState |= 0x01 <<channelInProgress;
else _portInputState &= ~(0x01 <<channelInProgress);
}
uint8_t channelInProgress=0;
uint8_t outputBuffer[10];
uint8_t inputBuffer[10];
};
#endif

96
KeywordHasher.h Normal file
View File

@@ -0,0 +1,96 @@
/*
* © 2024 Vincent Hamp and Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/* Reader be aware:
This function implements the _hk data type so that a string keyword
is hashed to the same value as the DCCEXParser uses to hash incoming
keywords.
Thus "MAIN"_hk generates exactly the same run time vakue
as const int16_t HASH_KEYWORD_MAIN=11339
*/
#ifndef KeywordHasher_h
#define KeywordHasher_h
#include <Arduino.h>
constexpr uint16_t CompiletimeKeywordHasher(const char * sv, uint16_t running=0) {
return (*sv==0) ? running : CompiletimeKeywordHasher(sv+1,
(*sv >= '0' && *sv <= '9')
? (10*running+*sv-'0') // Numeric hash
: ((running << 5) + running) ^ *sv
); //
}
constexpr int16_t operator""_hk(const char * keyword, size_t len)
{
return (int16_t) CompiletimeKeywordHasher(keyword,len*0);
}
/* Some historical values for testing:
const int16_t HASH_KEYWORD_MAIN = 11339;
const int16_t HASH_KEYWORD_SLOW = -17209;
const int16_t HASH_KEYWORD_SPEED28 = -17064;
const int16_t HASH_KEYWORD_SPEED128 = 25816;
*/
static_assert("MAIN"_hk == 11339,"Keyword hasher error");
static_assert("SLOW"_hk == -17209,"Keyword hasher error");
static_assert("SPEED28"_hk == -17064,"Keyword hasher error");
static_assert("SPEED128"_hk == 25816,"Keyword hasher error");
// 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
}
}

130
LocoTable.cpp Normal file
View File

@@ -0,0 +1,130 @@
/* Copyright (c) 2023 Harald Barth
*
* This source is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This source 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 this software. If not, see
* <http://www.gnu.org/licenses/>.
*/
#include "LocoTable.h"
LocoTable::LOCO LocoTable::speedTable[MAX_LOCOS] = { {0,0,0,0,0,0} };
int LocoTable::highestUsedReg = 0;
int LocoTable::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;
}
// 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 (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;
}
// returns false only if loco existed but nothing was changed
bool LocoTable::updateLoco(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 true;
}
// determine speed reg for this loco
int reg=lookupSpeedTable(loco, false);
if (reg>=0) {
speedTable[reg].speedcounter++;
if (speedTable[reg].speedCode!=speedCode) {
speedTable[reg].speedCode = speedCode;
return true;
} else {
return false;
}
} else {
// new
reg=lookupSpeedTable(loco, true);
if(reg >=0) speedTable[reg].speedCode = speedCode;
return true;
}
}
bool LocoTable::updateFunc(int loco, byte func, int shift) {
unsigned long previous;
unsigned long newfunc;
bool retval = false; // nothing was touched
int reg = lookupSpeedTable(loco, false);
if (reg < 0) { // not found
retval = true;
reg = lookupSpeedTable(loco, true);
newfunc = previous = 0;
} else {
newfunc = previous = speedTable[reg].functions;
}
speedTable[reg].funccounter++;
if(shift == 1) { // special case for light
newfunc &= ~1UL;
newfunc |= ((func & 0B10000) >> 4);
}
newfunc &= ~(0B1111UL << shift);
newfunc |= ((func & 0B1111) << shift);
if (newfunc != previous) {
speedTable[reg].functions = newfunc;
retval = true;
}
return retval;
}
void LocoTable::dumpTable(Stream *output) {
output->print("\n-----------Table---------\n");
for (byte reg = 0; reg <= highestUsedReg; reg++) {
if (speedTable[reg].loco != 0) {
output->print(speedTable[reg].loco);
output->print(' ');
output->print(speedTable[reg].speedCode);
output->print(' ');
output->print(speedTable[reg].functions);
output->print(" #funcpacks:");
output->print(speedTable[reg].funccounter);
output->print(" #speedpacks:");
output->print(speedTable[reg].speedcounter);
speedTable[reg].funccounter = 0;
speedTable[reg].speedcounter = 0;
output->print('\n');
}
}
}

44
LocoTable.h Normal file
View File

@@ -0,0 +1,44 @@
/* Copyright (c) 2023 Harald Barth
*
* This source is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This source 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 this software. If not, see
* <http://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "DCC.h" // fetch MAX_LOCOS from there
class LocoTable {
public:
void forgetLoco(int cab) {
int reg=lookupSpeedTable(cab, false);
if (reg>=0) speedTable[reg].loco=0;
}
static int lookupSpeedTable(int locoId, bool autoCreate);
static bool updateLoco(int loco, byte speedCode);
static bool updateFunc(int loco, byte func, int shift);
static void dumpTable(Stream *output);
private:
struct LOCO
{
int loco;
byte speedCode;
byte groupFlags;
unsigned long functions;
unsigned int funccounter;
unsigned int speedcounter;
};
static LOCO speedTable[MAX_LOCOS];
static int highestUsedReg;
};

View File

@@ -1,9 +1,11 @@
/*
* © 2022-2023 Paul M Antoine
* © 2022-2024 Paul M Antoine
* © 2024 Herb Morton
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2023 Harald Barth
* © 2020-2021 Chris Harlow
* © 2023 Colin Murdoch
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -26,12 +28,20 @@
#include "DCCWaveform.h"
#include "DCCTimer.h"
#include "DIAG.h"
#include "EXRAIL2.h"
unsigned long MotorDriver::globalOverloadStart = 0;
volatile portreg_t shadowPORTA;
volatile portreg_t shadowPORTB;
volatile portreg_t shadowPORTC;
#if defined(ARDUINO_ARCH_STM32)
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,
byte current_pin, float sense_factor, unsigned int trip_milliamps, int16_t fault_pin) {
@@ -66,6 +76,31 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTC;
}
if (HAVE_PORTD(fastSignalPin.inout == &PORTD)) {
DIAG(F("Found PORTD pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTD;
}
if (HAVE_PORTE(fastSignalPin.inout == &PORTE)) {
DIAG(F("Found PORTE pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTE;
}
if (HAVE_PORTF(fastSignalPin.inout == &PORTF)) {
DIAG(F("Found PORTF pin %d"),signalPin);
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) {
@@ -89,6 +124,31 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTC;
}
if (HAVE_PORTD(fastSignalPin2.inout == &PORTD)) {
DIAG(F("Found PORTD pin %d"),signalPin2);
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTD;
}
if (HAVE_PORTE(fastSignalPin2.inout == &PORTE)) {
DIAG(F("Found PORTE pin %d"),signalPin2);
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTE;
}
if (HAVE_PORTF(fastSignalPin2.inout == &PORTF)) {
DIAG(F("Found PORTF pin %d"),signalPin2);
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;
@@ -167,7 +227,7 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
}
bool MotorDriver::isPWMCapable() {
return (!dualSignal) && DCCTimer::isPWMPin(signalPin);
return (!dualSignal) && DCCTimer::isPWMPin(signalPin);
}
@@ -182,7 +242,7 @@ void MotorDriver::setPower(POWERMODE mode) {
// when switching a track On, we need to check the crrentOffset with the pin OFF
if (powerMode==POWERMODE::OFF && currentPin!=UNUSED_PIN) {
senseOffset = ADCee::read(currentPin);
DIAG(F("Track %c sensOffset=%d"),trackLetter,senseOffset);
if (Diag::ACK) DIAG(F("Track %c sensOffset=%d"),trackLetter,senseOffset);
}
IODevice::write(powerPin,invertPower ? LOW : HIGH);
@@ -277,7 +337,7 @@ void MotorDriver::startCurrentFromHW() {
#pragma GCC pop_options
#endif //ANALOG_READ_INTERRUPT
#if defined(ARDUINO_ARCH_ESP32)
#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32)
#ifdef VARIABLE_TONES
uint16_t taurustones[28] = { 165, 175, 196, 220,
247, 262, 294, 330,
@@ -288,49 +348,21 @@ uint16_t taurustones[28] = { 165, 175, 196, 220,
220, 196, 175, 165 };
#endif
#endif
void MotorDriver::setDCSignal(byte speedcode) {
void MotorDriver::setDCSignal(byte speedcode, uint8_t frequency /*default =0*/) {
if (brakePin == UNUSED_PIN)
return;
switch(brakePin) {
#if defined(ARDUINO_AVR_UNO)
// Not worth doin something here as:
// If we are on pin 9 or 10 we are on Timer1 and we can not touch Timer1 as that is our DCC source.
// If we are on pin 5 or 6 we are on Timer 0 ad we can not touch Timer0 as that is millis() etc.
// We are most likely not on pin 3 or 11 as no known motor shield has that as brake.
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
case 9:
case 10:
// Timer2 (is differnet)
TCCR2A = (TCCR2A & B11111100) | B00000001; // set WGM1=0 and WGM0=1 phase correct PWM
TCCR2B = (TCCR2B & B11110000) | B00000110; // set WGM2=0 ; set divisor on timer 2 to 1/256 for 122.55Hz
//DIAG(F("2 A=%x B=%x"), TCCR2A, TCCR2B);
break;
case 6:
case 7:
case 8:
// Timer4
TCCR4A = (TCCR4A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for normal PWM 8-bit
TCCR4B = (TCCR4B & B11100000) | B00000100; // set WGM2=0 and WGM3=0 for normal PWM 8 bit and div 1/256 for 122.55Hz
break;
case 46:
case 45:
case 44:
// Timer5
TCCR5A = (TCCR5A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for normal PWM 8-bit
TCCR5B = (TCCR5B & B11100000) | B00000100; // set WGM2=0 and WGM3=0 for normal PWM 8 bit and div 1/256 for 122.55Hz
break;
#endif
default:
break;
}
// spedcoode is a dcc speed & direction
byte tSpeed=speedcode & 0x7F; // DCC Speed with 0,1 stop and speed steps 2 to 127
byte tDir=speedcode & 0x80;
byte brake;
#if defined(ARDUINO_ARCH_ESP32)
{
int f = 131;
if (tSpeed <= 1) brake = 255;
else if (tSpeed >= 127) brake = 0;
else brake = 2 * (128-tSpeed);
{ // new block because of variable f
#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_STM32)
int f = frequency;
#ifdef VARIABLE_TONES
if (tSpeed > 2) {
if (tSpeed <= 58) {
@@ -338,19 +370,15 @@ void MotorDriver::setDCSignal(byte speedcode) {
}
}
#endif
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency to 100Hz XXX May move to setup
//DIAG(F("Brake pin %d value %d freqency %d"), brakePin, brake, f);
DCCTimer::DCCEXanalogWrite(brakePin, brake, invertBrake);
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency
#else // all AVR here
DCCTimer::DCCEXanalogWriteFrequency(brakePin, frequency); // frequency steps
analogWrite(brakePin, invertBrake ? 255-brake : brake);
#endif
}
#endif
if (tSpeed <= 1) brake = 255;
else if (tSpeed >= 127) brake = 0;
else brake = 2 * (128-tSpeed);
if (invertBrake)
brake=255-brake;
#if defined(ARDUINO_ARCH_ESP32)
DCCTimer::DCCEXanalogWrite(brakePin,brake);
#else
analogWrite(brakePin,brake);
#endif
//DIAG(F("DCSignal %d"), speedcode);
if (HAVE_PORTA(fastSignalPin.shadowinout == &PORTA)) {
noInterrupts();
@@ -370,6 +398,36 @@ void MotorDriver::setDCSignal(byte speedcode) {
setSignal(tDir);
HAVE_PORTC(PORTC=shadowPORTC);
interrupts();
} else if (HAVE_PORTD(fastSignalPin.shadowinout == &PORTD)) {
noInterrupts();
HAVE_PORTD(shadowPORTD=PORTD);
setSignal(tDir);
HAVE_PORTD(PORTD=shadowPORTD);
interrupts();
} else if (HAVE_PORTE(fastSignalPin.shadowinout == &PORTE)) {
noInterrupts();
HAVE_PORTE(shadowPORTE=PORTE);
setSignal(tDir);
HAVE_PORTE(PORTE=shadowPORTE);
interrupts();
} else if (HAVE_PORTF(fastSignalPin.shadowinout == &PORTF)) {
noInterrupts();
HAVE_PORTF(shadowPORTF=PORTF);
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);
@@ -379,53 +437,28 @@ void MotorDriver::setDCSignal(byte speedcode) {
void MotorDriver::throttleInrush(bool on) {
if (brakePin == UNUSED_PIN)
return;
if ( !(trackMode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_EXT)))
if ( !(trackMode & (TRACK_MODE_MAIN | TRACK_MODE_PROG | TRACK_MODE_EXT | TRACK_MODE_BOOST)))
return;
byte duty = on ? 208 : 0;
if (invertBrake)
duty = 255-duty;
byte duty = on ? 207 : 0; // duty of 81% at 62500Hz this gives pauses of 3usec
#if defined(ARDUINO_ARCH_ESP32)
if(on) {
DCCTimer::DCCEXanalogWrite(brakePin,duty);
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 62500);
DCCTimer::DCCEXInrushControlOn(brakePin, duty, invertBrake);
} else {
ledcDetachPin(brakePin);
ledcDetachPin(brakePin); // not DCCTimer::DCCEXledcDetachPin() as we have not
// registered the pin in the pin to channel array
}
#else
#elif defined(ARDUINO_ARCH_STM32)
if(on) {
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max
DCCTimer::DCCEXanalogWrite(brakePin,duty,invertBrake);
} else {
pinMode(brakePin, OUTPUT);
}
#else // all AVR here
if (invertBrake)
duty = 255-duty;
if(on){
switch(brakePin) {
#if defined(ARDUINO_AVR_UNO)
// Not worth doin something here as:
// If we are on pin 9 or 10 we are on Timer1 and we can not touch Timer1 as that is our DCC source.
// If we are on pin 5 or 6 we are on Timer 0 ad we can not touch Timer0 as that is millis() etc.
// We are most likely not on pin 3 or 11 as no known motor shield has that as brake.
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
case 9:
case 10:
// Timer2 (is different)
TCCR2A = (TCCR2A & B11111100) | B00000011; // set WGM0=1 and WGM1=1 for fast PWM
TCCR2B = (TCCR2B & B11110000) | B00000001; // set WGM2=0 and prescaler div=1 (max)
DIAG(F("2 A=%x B=%x"), TCCR2A, TCCR2B);
break;
case 6:
case 7:
case 8:
// Timer4
TCCR4A = (TCCR4A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for fast PWM 8-bit
TCCR4B = (TCCR4B & B11100000) | B00001001; // set WGM2=1 and WGM3=0 for fast PWM 8 bit and div=1 (max)
break;
case 46:
case 45:
case 44:
// Timer5
TCCR5A = (TCCR5A & B11111100) | B00000001; // set WGM0=1 and WGM1=0 for fast PWM 8-bit
TCCR5B = (TCCR5B & B11100000) | B00001001; // set WGM2=1 and WGM3=0 for fast PWM 8 bit and div=1 (max)
break;
#endif
default:
break;
}
DCCTimer::DCCEXanalogWriteFrequency(brakePin, 7); // 7 means max
}
analogWrite(brakePin,duty);
#endif
@@ -543,6 +576,10 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
DIAG(F("TRACK %c ALERT FAULT"), trackno + 'A');
}
setPower(POWERMODE::ALERT);
if ((trackMode & TRACK_MODIFIER_AUTO) && (trackMode & (TRACK_MODE_MAIN|TRACK_MODE_EXT|TRACK_MODE_BOOST))){
DIAG(F("TRACK %c INVERT"), trackno + 'A');
invertOutput();
}
break;
}
// all well
@@ -602,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;
}
@@ -613,7 +654,11 @@ void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
// adjust next wait time
power_sample_overload_wait *= 2;
if (power_sample_overload_wait > POWER_SAMPLE_RETRY_MAX)
power_sample_overload_wait = POWER_SAMPLE_RETRY_MAX;
power_sample_overload_wait = POWER_SAMPLE_RETRY_MAX;
#ifdef EXRAIL_ACTIVE
DIAG(F("Calling EXRAIL"));
RMFT2::powerEvent(trackno, true); // Tell EXRAIL we have an overload
#endif
// power on test
DIAG(F("TRACK %c POWER RESTORE (after %4M)"), trackno + 'A', mslpc);
setPower(POWERMODE::ALERT);

View File

@@ -1,9 +1,9 @@
/*
* © 2022 Paul M Antoine
* © 2022-2024 Paul M. Antoine
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020 Chris Harlow
* © 2022 Harald Barth
* © 2022,2023 Harald Barth
* All rights reserved.
*
* This file is part of CommandStation-EX
@@ -26,10 +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.
enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PROG = 4,
TRACK_MODE_DC = 8, TRACK_MODE_DCX = 16, TRACK_MODE_EXT = 32};
// 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_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_INV = 0,
TRACK_MODE_BOOST_AUTO = 0,
#endif
// 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
@@ -60,6 +83,24 @@ enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PRO
#define HAVE_PORTB(X) X
#define PORTC GPIOC->ODR
#define HAVE_PORTC(X) X
#define PORTD GPIOD->ODR
#define HAVE_PORTD(X) X
#if defined(GPIOE)
#define PORTE GPIOE->ODR
#define HAVE_PORTE(X) X
#endif
#if defined(GPIOF)
#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
@@ -74,6 +115,21 @@ enum TRACK_MODE : byte {TRACK_MODE_NONE = 1, TRACK_MODE_MAIN = 2, TRACK_MODE_PRO
#ifndef HAVE_PORTC
#define HAVE_PORTC(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
#ifndef HAVE_PORTD
#define HAVE_PORTD(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
#ifndef HAVE_PORTE
#define HAVE_PORTE(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
#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
@@ -110,6 +166,11 @@ struct FASTPIN {
extern volatile portreg_t shadowPORTA;
extern volatile portreg_t shadowPORTB;
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 };
@@ -126,7 +187,11 @@ class MotorDriver {
// otherwise the call from interrupt context can undo whatever we do
// from outside interrupt
void setBrake( bool on, bool interruptContext=false);
__attribute__((always_inline)) inline void setSignal( bool high) {
__attribute__((always_inline)) inline void setSignal( bool high) {
#ifndef ARDUINO_ARCH_ESP32
if (invertPhase)
high = !high;
#endif
if (trackPWM) {
DCCTimer::setPWM(signalPin,high);
}
@@ -146,15 +211,22 @@ class MotorDriver {
pinMode(signalPin, OUTPUT);
else
pinMode(signalPin, INPUT);
if (signalPin2 != UNUSED_PIN) {
if (on)
pinMode(signalPin2, OUTPUT);
else
pinMode(signalPin2, INPUT);
}
};
inline pinpair getSignalPin() { return pinpair(signalPin,signalPin2); };
void setDCSignal(byte speedByte);
inline int8_t getBrakePinSigned() { return invertBrake ? -brakePin : brakePin; };
void setDCSignal(byte speedByte, uint8_t frequency=0);
void throttleInrush(bool on);
inline void detachDCSignal() {
#if defined(__arm__)
pinMode(brakePin, OUTPUT);
#elif defined(ARDUINO_ARCH_ESP32)
ledcDetachPin(brakePin);
DCCTimer::DCCEXledcDetachPin(brakePin);
#else
setDCSignal(128);
#endif
@@ -163,16 +235,16 @@ class MotorDriver {
unsigned int raw2mA( int raw);
unsigned int mA2raw( unsigned int mA);
inline bool brakeCanPWM() {
#if defined(ARDUINO_ARCH_ESP32) || defined(__arm__)
// TODO: on ARM we can use digitalPinHasPWM, and may wish/need to
return true;
#else
#ifdef digitalPinToTimer
#if defined(ARDUINO_ARCH_ESP32)
return (brakePin != UNUSED_PIN); // This was just (true) but we probably do need to check for UNUSED_PIN!
#elif defined(__arm__)
// On ARM we can use digitalPinHasPWM
return ((brakePin!=UNUSED_PIN) && (digitalPinHasPWM(brakePin)));
#elif defined(digitalPinToTimer)
return ((brakePin!=UNUSED_PIN) && (digitalPinToTimer(brakePin)));
#else
return (brakePin<14 && brakePin >1);
#endif //digitalPinToTimer
#endif //ESP32/ARM
#endif
}
inline int getRawCurrentTripValue() {
return rawCurrentTripValue;
@@ -210,6 +282,32 @@ class MotorDriver {
#endif
inline void setMode(TRACK_MODE m) {
trackMode = m;
invertOutput(trackMode & TRACK_MODIFIER_INV);
};
inline void invertOutput() { // toggles output inversion
invertPhase = !invertPhase;
invertOutput(invertPhase);
};
inline void invertOutput(bool b) { // sets output inverted or not
if (b)
invertPhase = 1;
else
invertPhase = 0;
#if defined(ARDUINO_ARCH_ESP32)
pinpair p = getSignalPin();
uint32_t *outreg = (uint32_t *)(GPIO_FUNC0_OUT_SEL_CFG_REG + 4*p.pin);
if (invertPhase) // set or clear the invert bit in the gpio out register
*outreg |= ((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
else
*outreg &= ~((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
if (p.invpin != UNUSED_PIN) {
outreg = (uint32_t *)(GPIO_FUNC0_OUT_SEL_CFG_REG + 4*p.invpin);
if (invertPhase) // clear or set the invert bit in the gpio out register
*outreg &= ~((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
else
*outreg |= ((uint32_t)0x1 << GPIO_FUNC0_OUT_INV_SEL_S);
}
#endif
};
inline TRACK_MODE getMode() {
return trackMode;
@@ -241,7 +339,7 @@ class MotorDriver {
bool invertBrake; // brake pin passed as negative means pin is inverted
bool invertPower; // power pin passed as negative means pin is inverted
bool invertFault; // fault pin passed as negative means pin is inverted
bool invertPhase = 0; // phase of out pin is inverted
// Raw to milliamp conversion factors avoiding float data types.
// Milliamps=rawADCreading * sensefactorInternal / senseScale
//

View File

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

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