@@ -41,6 +41,10 @@ void EventDispatcher::setCrossLinkSerial(HardwareSerial &reference) {
4141
4242void EventDispatcher::setDebug (bool enabled) { debugEnabled = enabled; }
4343
44+ void EventDispatcher::setNextSwitchBoard (byte boardId) {
45+ nextSwitchBoard = boardId;
46+ }
47+
4448void EventDispatcher::addListener (EventListener *eventListener) {
4549 addListener (eventListener, EVENT_SOURCE_ANY);
4650}
@@ -153,6 +157,10 @@ size_t EventDispatcher::getV2PayloadBytes(ppuc::v2::FrameType frameType) {
153157 case ppuc::v2::kFrameOutputState :
154158 return ppuc::v2::BitsToBytes (runtimeConfig.coilBits ) +
155159 ppuc::v2::BitsToBytes (runtimeConfig.lampBits );
160+ case ppuc::v2::kFrameSwitchState :
161+ return ppuc::v2::BitsToBytes (runtimeConfig.switchBits );
162+ case ppuc::v2::kFrameSwitchNoChange :
163+ return 0 ;
156164 case ppuc::v2::kFrameHeartbeat :
157165 case ppuc::v2::kFrameError :
158166 case ppuc::v2::kFrameReset :
@@ -226,11 +234,26 @@ bool EventDispatcher::processV2Frame(const byte* frame, size_t payloadBytes) {
226234 const size_t lampBytes = ppuc::v2::BitsToBytes (runtimeConfig.lampBits );
227235 applyOutputStates (&frame[4 ], coilBytes, &frame[4 + coilBytes], lampBytes);
228236 if (frame[2 ] == board) {
229- sendSwitchStateFrame ((byte)((board + 1 ) % ppuc::v2::kMaxBoards ));
237+ if (switchDirty) {
238+ sendSwitchStateFrame (nextSwitchBoard);
239+ switchDirty = false ;
240+ } else {
241+ sendSwitchNoChangeFrame (nextSwitchBoard);
242+ }
230243 }
231244 return true ;
232245 }
233246
247+ if (frameType == ppuc::v2::kFrameSwitchState ) {
248+ const size_t switchBytes = ppuc::v2::BitsToBytes (runtimeConfig.switchBits );
249+ applySwitchStates (&frame[4 ], switchBytes);
250+ return true ;
251+ }
252+
253+ if (frameType == ppuc::v2::kFrameSwitchNoChange ) {
254+ return true ;
255+ }
256+
234257 if (frameType == ppuc::v2::kFrameReset ) {
235258 dispatch (new Event (EVENT_RESET));
236259 return true ;
@@ -405,6 +428,9 @@ void EventDispatcher::updateSwitchBitmap(Event *event) {
405428
406429 ppuc::v2::SetBitmapBit (switchStates, (uint16_t )mappedIndex,
407430 event->value != 0 );
431+ if (!applyingRemoteSwitchState) {
432+ switchDirty = true ;
433+ }
408434}
409435
410436void EventDispatcher::applyOutputStates (const byte *coils, size_t coilBytes,
@@ -437,6 +463,24 @@ void EventDispatcher::applyOutputStates(const byte *coils, size_t coilBytes,
437463 memcpy (outputLamps, lamps, lampBytes);
438464}
439465
466+ void EventDispatcher::applySwitchStates (const byte* switches,
467+ size_t switchBytes) {
468+ // Global switch state is board-to-board on the RS485 bus. CPU/libppuc never
469+ // broadcasts switch states. Every board consumes incoming switch frames and
470+ // emits local switch events for fast-flip/effect listeners.
471+ applyingRemoteSwitchState = true ;
472+ for (uint16_t n = 0 ; n < runtimeConfig.switchBits ; ++n) {
473+ bool oldState = ppuc::v2::GetBitmapBit (switchStates, n);
474+ bool newState = ppuc::v2::GetBitmapBit (switches, n);
475+ if (oldState != newState) {
476+ dispatch (new Event (EVENT_SOURCE_SWITCH, switchIndexToNumber[n],
477+ newState ? 1 : 0 , true ));
478+ }
479+ }
480+ applyingRemoteSwitchState = false ;
481+ memcpy (switchStates, switches, switchBytes);
482+ }
483+
440484void EventDispatcher::sendSwitchStateFrame (byte nextBoard) {
441485 // Switch updates are transmitted as a compact V2 frame containing the full
442486 // dense switch bitmap. The CPU selects the responding board via token
@@ -472,6 +516,31 @@ void EventDispatcher::sendSwitchStateFrame(byte nextBoard) {
472516 lastPoll = millis ();
473517}
474518
519+ void EventDispatcher::sendSwitchNoChangeFrame (byte nextBoard) {
520+ byte* frame = v2DmaTxBuffer;
521+ frame[0 ] = ppuc::v2::kSyncByte ;
522+ frame[1 ] = ppuc::v2::ComposeTypeAndFlags (ppuc::v2::kFrameSwitchNoChange ,
523+ ppuc::v2::kFlagNone );
524+ frame[2 ] = nextBoard;
525+ frame[3 ] = txSequence++;
526+ uint16_t crc = ppuc::v2::Crc16Ccitt (frame, ppuc::v2::kHeaderBytes );
527+ frame[4 ] = highByte (crc);
528+ frame[5 ] = lowByte (crc);
529+
530+ if (!v2UartDmaActive || !sendV2FrameUartDma (frame, ppuc::v2::kResetFrameBytes )) {
531+ v2TxFallback++;
532+ digitalWrite (rs485Pin, HIGH); // Write.
533+ delayMicroseconds (RS485_MODE_SWITCH_DELAY);
534+ hwSerial->write (frame, ppuc::v2::kResetFrameBytes );
535+ hwSerial->flush ();
536+ digitalWrite (rs485Pin, LOW); // Read.
537+ delayMicroseconds (RS485_MODE_SWITCH_DELAY);
538+ }
539+
540+ v2SwitchNoChangeTx++;
541+ lastPoll = millis ();
542+ }
543+
475544bool EventDispatcher::handleV2Frame () {
476545 if (hwSerial->available () < (int )ppuc::v2::kHeaderBytes ) {
477546 return false ;
@@ -490,7 +559,8 @@ bool EventDispatcher::handleV2Frame() {
490559 if (frameType != ppuc::v2::kFrameHeartbeat &&
491560 frameType != ppuc::v2::kFrameError &&
492561 frameType != ppuc::v2::kFrameReset && payloadBytes == 0 &&
493- frameType != ppuc::v2::kFrameOutputState ) {
562+ frameType != ppuc::v2::kFrameOutputState &&
563+ frameType != ppuc::v2::kFrameSwitchNoChange ) {
494564 return false ;
495565 }
496566
@@ -572,6 +642,8 @@ void EventDispatcher::update() {
572642 Serial.print (v2RxDmaTimeouts);
573643 Serial.print (" tx=" );
574644 Serial.print (v2TxFrames);
645+ Serial.print (" tx_nochange=" );
646+ Serial.print (v2SwitchNoChangeTx);
575647 Serial.print (" tx_fallback=" );
576648 Serial.println (v2TxFallback);
577649 rp2040.resumeOtherCore ();
0 commit comments