What Carlos said, here’s the version I use for our line counters (an Uno doing the counting, a bigger board doing the network). Send binary, fixed size, in one struct, and use onReceive for commands.
Shared struct, in both sketches:
struct Reading {
int16_t moisture; // raw ADC
int16_t temp_x10; // 21.5 C -> 215
int16_t flow_x10; // L/min x10
};
Uno (slave):
volatile Reading r;
volatile bool openValve = false;
void requestEvent() { Wire.write((const uint8_t*)&r, sizeof(r)); } // one write
void receiveEvent(int n) { if (Wire.available()) openValve = Wire.read() == 1; }
void setup() { Wire.begin(8); Wire.onRequest(requestEvent); Wire.onReceive(receiveEvent); }
void loop() {
Reading tmp = { readMoisture(), (int16_t)(readTemp() * 10), (int16_t)(readFlow() * 10) };
noInterrupts(); r = tmp; interrupts();
digitalWrite(VALVE_PIN, openValve);
}
Mega (master):
Reading r;
if (Wire.requestFrom(8, (int)sizeof(r)) == sizeof(r)) Wire.readBytes((uint8_t*)&r, sizeof(r));
float tempC = r.temp_x10 / 10.0;
Wire.beginTransmission(8); Wire.write(1); Wire.endTransmission(); // open valve
Both are AVR, so byte order and int sizes match and the struct goes across as-is. The ÿ you saw is 0xFF, what the master reads when the slave has nothing left to send. Asking for exactly sizeof(r) fixes that too.

