Chris, my method of block occupancy is very different from most approaches, but I am very happy with it. Here is what I do:
I opted for isolated rail detection, rather than something like an opto sensor. Isolated rails are monitored by an Arduino (I'll explain how, below) as a means of keeping track of where all of the trains are on my layout. I use an isolated section at each end of every block, including, most importantly, at each end of every double-ended siding. When a train trips the *entry* block of a siding where it is scheduled to stop, an Arduino sends Legacy commands to begin slowing down the train (and start ringing the bell, make an announcement, etc.) The speed and momentum commands that are sent depend on several factors including the length of the siding, the speed at entry, and the individual locomotive characteristics. I try to time this so that I'm moving slowly, but not *too* slowly, by the time the train trips the exit sensor of the block. If it's a long siding, I don't start slowing down right away. When the exit sensor is tripped, I do an absolute stop of the locomotive.
So here is one challenge: there are many factors that contribute to the latency between the instant an occupancy sensor is tripped, and when the locomotive receives and responds to a command -- such as to stop immediately before overshooting the block and fouling the exit turnout. An optical sensor would allow me to move it farther into the block if I needed to allow for more latency, but that sensor also serves as an *entry* sensor if a train enters the block from the opposite direction -- and I want to know as soon as a train enters a block -- not a couple of feet into it. So I need a "long" section of isolated rail -- that would trip the instant a train enters from that end, but also maybe a foot or so before it reaches the end if traveling the other direction. I hope that makes sense ;-) But how long should it be? I won't know for sure what the latency will be until I have half a dozen trains running at the same time, with the Arduinos juggling lots of tasks etc. But I can't test it until I have all of those isolated sections in place. Catch 22.
My solution was to use a very thin insulating tape called Kapton tape, on top of the rails, followed by a layer of very thin adhesive copper foil tape. The Kapton tape is 1/2" and the copper tape is 1/4" -- so I have an insulated strip of copper tape on top of my rails (I use MTH ScaleTrax.) I solder a small wire anywhere along the outside edge of the copper strip, and that is my grounded/not-grounded indicator signal. I can easily add or remove tape to adjust the length of the isolated section at any time. I have to be a bit careful -- I can't clean my rails with a Brite Boy pad, or the tape would be destroyed. But it's been working great for many months of testing. And I can always convert these to "cut the rail" isolated sections at a later time, when I am confident that I know what length is ideal.
So the only thing left is how to get an Arduino to read that grounded/not-grounded wire, for over 50 isolated sections. My solution -- which I'm sure many think is ridiculous -- was to use time-delay-off relays that I buy on eBay for $5 each:
This is an expensive and space-consuming solution but I love it. I connect the isolated-rail wire directly to one side of the relay coil (the other connected to 12VAC "hot"). I connect one side of the contacts to ground and the other to an input pin of my shift register (a Centipede, in my case.) When a train is on an isolated section, the relay coil energizes, the Arduino sees it (via the shift register) as a simple contact closure -- and the contacts stay closed for several seconds *after* the train has left the scene (any amount of time can be set using the dial) to eliminate issues of chatter, dirty wheels, etc.
I've attached some photos of the work that's been completed. Feel free to ask questions!