Returning data resived from i2c slave (M260) to calling macro
-
I am trying to design and implement a system on my 3D printer that would make the process of changing the nozzle and subsequent printer setup as easy as possible. As a result, I came to the design of a swinging double extruder with easily replaceable thermal blocks (assembly of a heater and a heat sink). For quick setup after the change, I planned to store parameters in i2c eeprom inside replaceable block. When it came to the control code, I was very disappointed. It turned out that M260 command can return data only in text form in reply. I tried to google the forum and didn't find a solution. Asked a question in the meta commands section and got no response. It remained only to delve into the firmware itself. It seemed to me that the normal option would be to return data through a local variable defined in the calling code. The name of this variable could be reported to the M260 command through an additional string parameter. For example:
var i2cReadBuffer = vector(10, 0) M260 A80 B0 R10 S"i2cReadBuffer" echo "buffer:", var.i2cReadBuffer
I changed the procedure of M260 gcode so that if S parameter is missing, the behavior remains unchanged. If S parameter is set and there are no errors, the received data is stored in a buffer variable. If an error is found, the result of the gcode is changed to ERROR and the reason is reported in the reply. Possible reasons:
- total transfer length exceeds MaxI2cBytes
- variable not found
- does not match the type of the variable (an array of integers)
- insufficient array length.
It works at first glance. I've done a few checks with BMP280 and eeprom 24c04:
I do not understand all the details of the functioning of the firmware. I looked at how similar tasks are solved in other places in the firmware code and tried to do the same. I ask the RepRap firmware developers to look at my code and tell me if I missed something important. (The text of the code has been updated and does not match the previous test output)
// Handle M260 - send and possibly receive via I2C GCodeResult GCodes::SendI2c(GCodeBuffer& gb, const StringRef &reply) { #if defined(I2C_IFACE) if (gb.Seen('A')) { const uint32_t address = gb.GetUIValue(); uint32_t numToReceive = 0; GCodeResult Result = GCodeResult::ok; bool seenR; gb.TryGetUIValue('R', numToReceive, seenR); int32_t values[MaxI2cBytes]; size_t numToSend; if (gb.Seen('B')) { numToSend = MaxI2cBytes; gb.GetIntArray(values, numToSend, false); //TODO allow hex values } else { numToSend = 0; } if (numToSend + numToReceive != 0) { if (numToSend + numToReceive > MaxI2cBytes) { numToReceive = MaxI2cBytes - numToSend; Result = GCodeResult::error; } uint8_t bValues[MaxI2cBytes]; for (size_t i = 0; i < numToSend; ++i) { bValues[i] = (uint8_t)values[i]; } I2C::Init(); const size_t bytesTransferred = I2C::Transfer(address, bValues, numToSend, numToReceive); if (bytesTransferred < numToSend) { reply.copy("I2C transmission error"); return GCodeResult::error; } else if (numToReceive != 0) { // At this moment transfer completed successfully, may be truncated due to insufficient i2c buffer. // We check if S parameter exists. If not, the received bytes are printed in hex format to replay. // If yes, then we try to store received bytes in specified variable // checking possible errors: // - total transfer length exceeds MaxI2cBytes // - unknown read buffer variable // - wrong type of read buffer (requires an array of integers) // - insufficient read buffer length // In case of error, the result of gcode is set to ERROR, the reason is reported in reply. if (!gb.Seen('S')) { reply.copy("Received"); if (bytesTransferred == numToSend) { reply.cat(" nothing"); } else { for (size_t i = numToSend; i < bytesTransferred; ++i) { reply.catf(" %02x", bValues[i]); } } } else { if (Result != GCodeResult::ok) { reply.cat(" Transmission length exceeds MaxI2cBytes"); } else { String<MaxVariableNameLength> varName; gb.GetQuotedString(varName.GetRef(), true); WriteLockedPointer<VariableSet> vset = WriteLockedPointer<VariableSet>(nullptr, &gb.GetVariables()); Variable *const var = vset->Lookup(varName.c_str()); if (var == nullptr) { Result = GCodeResult::error; reply.catf(" Unknown read buffer variable '%s'.", varName.c_str()); } else if ((*var).GetValue().GetType() != TypeCode::HeapArray) { Result = GCodeResult::error; reply.catf(" Incorrect read buffer type %i (requires an array of integers).", (int) (*var).GetValue().GetType()); } if (Result == GCodeResult::ok) { ReadLocker lock(Heap::heapLock); if ((*var).GetValue().ahVal.GetNumElements() < numToReceive) { Result = GCodeResult::error; reply.cat(" Insufficient read buffer array length."); } else for (size_t i = 0; i < numToReceive; ++i) { if ((*var).GetValue().ahVal.GetElementType(i) != TypeCode::Int32) { Result = GCodeResult::error; reply.catf(" Incorrect read buffer array element type %i (requires int).", (int) (*var).GetValue().ahVal.GetElementType(i)); break; } } lock.Release(); if (Result == GCodeResult::ok) { ExpressionValue ev; for (size_t i = 0; i < numToReceive; ++i) { ev.SetInt(bValues[numToSend + i]); (*var).GetValue().ahVal.AssignElement(i, ev); } } } } } } return Result; } } return GCodeResult::badOrMissingParameter; #else reply.copy("I2C not available"); return GCodeResult::error; #endif }
-
-
-
@T3P3Tony @dc42 I have been trying to figure out the best way to make M260 also be able to be used to send and receive bytes over UART to enable expanding spindle control capabilities. I think there should be a separate parameter set that would tell it that it is going to use a uart channel instead of an i2c channel, and then would it be best to use B for both?
-
@timothyz I don't think it's worth putting it all in one gcode command. Anyway, this would be a significant architectural change and need to be descussed. I would like to have in meta script commands for arbitrary data exchange both via uart and can. But this is a philosophical question - how far low-level tools to provide to the user. As for me, such commands would greatly simplify the addition of custom peripheral to the system and that is in line with the philosophy of RepRap project. The existing CanFD based interconnect channel is great, but the cost of microcontroller with it onboard plus transceiver make a barrier. А channel needed that's slower, cheaper, but flexible enough to connect multiple devices on the same bus. UART is not convenient for this, in my opinion. OneWire is not widely supported by hardware in microcontrollers. I2c is not formally designed for interunit communication, but at low speed and in low noise conditions it works.
-
@T3P3Tony I'm waiting for some words from @dc42. I'm not sure about my code. For example, during the checks, I found out that the types of arrays and their elements behave differently than I imagined. There was no time to dig deeper yet. My code is very short and simple, it is not difficult to check it for someone who is in the know. If it helps, I can make a pull request.
-
@arkayu It will be a while before he will be able to look at this.
-
The internal behavior of variables is as I imagined at the beginning. This was my error in the test macro. Here is a run of the corrected test.
-
If this form of gcode parameter is generally adopted, it would give a wider range of uses. For example, you could pass a structure that includes a set of parameters and return values. As to me, this form is quite natural and corresponds to the usual way of passing a variable by reference. If I'm not mistaken, the current gcode interface and implementation of meta variables allow this without difficulty.