interesting discussion, ideas and experiences!
I don't have much to add, the wired and wireless modes of communication have their advantages and disadvantages.
The Arduino and port expander via I2C works great, but you need to build the code depending on your preferences.
Further communication is via USB.
I was dealing with a PCF8575 (16x I/O port expander).
An Arduino with 4xPCF8575 (total = 64 inputs or outputs) works great.
What I would recommend is to use the interrupt function (from PCF8575 pdf: An interrupt is generated by any rising or falling edge of the port inputs in the input mode.).
The alternative to this is to do polling, but this causes constant activity on the USB and additional CPU load. On the Raspberry Pi 3B, depending on the frequency of polling, the CPU was about 20-35% increased, which is not small.
In my opinion, it is better to use interrupt and the option to call port readings as needed (see trigger below).
With this, in a static situation, there is no USB traffic in practice and therefore no processing.
The PCF8575 I/O operates at 5V and as such can directly control the relays or relays modules.
Care should be taken here to choose the "Trigger LOW" module.
Selecting such a relay module avoids flickering when booting arduino & PCF8575.
Also choosing module with optocouplers avoids the need for additional driver chips as is ULN2803.
Of course, choose the 5V relay module (with this module the trigger is 5V of course).
If you choose a 12V relay module, you will need to redesign the 5V trigger module.
I needed to develop a wire type I/O module, and choosing the Arduino (Nano) and PCF8575 was a winning combination for me.
My working principle (Arduino code) is extremly simple and ideal for Node-RED.
Eg. to set a port to a specified state. Send command: < A10 >. (without space; can't post like that).
In this case:
A = first group of 8 ports (first bank of PCF8575)
1 = port number,
0 = port state.
That is all for turn the relay ON or OFF!
To read the port status, it is easiest to install node-red-contrib-PCF8575-extractor (link is in the documentation).
After configuration (just choose which group of inputs to analyze), and on the outputs we have a state for each individual port.
The output states are 1, 0, or E.
1 = status is 1
0 = status is 0,
E = Error (PCF8575 did not respond).
That is all, connect/disconnect port to GND and node output will be changed!
Each change on any port sends the state of the entire group (8xport).
It can also be posiible to send a trigger message (manual or at intervals) according to the USB.
The command to send is < T > (without space again).
This command checks all ports and returns statuses, including errors on PCF8575 (if any).
Full documentation is at https://www.tindie.com/products/picotouch/gpios-fromto-serial-usb-module-16-48-ports/
I hope it helps in which direction to go and what logic is used.
P.S. The module described on the link can work as an input or output module (selected by jumper).