Post-merge fixes
Fixed M122 so that diagnostics are sent to the web interface as well as USB Fixed M220 command to respond to speed change requests faster Count the number of active extruders and heaters, and only send their values to the web interface, subject to a minimum of 2 heaters and 1 extruder
This commit is contained in:
parent
74a7616107
commit
3053359b30
12 changed files with 67 additions and 30 deletions
|
@ -24,7 +24,7 @@ Licence: GPL
|
||||||
#define CONFIGURATION_H
|
#define CONFIGURATION_H
|
||||||
|
|
||||||
#define NAME "RepRapFirmware"
|
#define NAME "RepRapFirmware"
|
||||||
#define VERSION "0.78-dc42"
|
#define VERSION "0.78a-dc42"
|
||||||
#define DATE "2014-07-06"
|
#define DATE "2014-07-06"
|
||||||
#define LAST_AUTHOR "reprappro, dc42. zpl"
|
#define LAST_AUTHOR "reprappro, dc42. zpl"
|
||||||
|
|
||||||
|
|
40
GCodes.cpp
40
GCodes.cpp
|
@ -91,6 +91,7 @@ void GCodes::Reset()
|
||||||
cannedCycleMoveCount = 0;
|
cannedCycleMoveCount = 0;
|
||||||
cannedCycleMoveQueued = false;
|
cannedCycleMoveQueued = false;
|
||||||
speedFactor = 1.0/60.0; // default is just to convert from mm/minute to mm/second
|
speedFactor = 1.0/60.0; // default is just to convert from mm/minute to mm/second
|
||||||
|
speedFactorChange = 1.0;
|
||||||
for (size_t i = 0; i < DRIVES - AXES; ++i)
|
for (size_t i = 0; i < DRIVES - AXES; ++i)
|
||||||
{
|
{
|
||||||
extrusionFactors[i] = 1.0;
|
extrusionFactors[i] = 1.0;
|
||||||
|
@ -234,7 +235,7 @@ void GCodes::Spin()
|
||||||
|
|
||||||
void GCodes::Diagnostics()
|
void GCodes::Diagnostics()
|
||||||
{
|
{
|
||||||
platform->Message(BOTH_MESSAGE, "GCodes Diagnostics:\n");
|
platform->AppendMessage(BOTH_MESSAGE, "GCodes Diagnostics:\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// The wait till everything's done function. If you need the machine to
|
// The wait till everything's done function. If you need the machine to
|
||||||
|
@ -367,12 +368,12 @@ bool GCodes::LoadMoveBufferFromGCode(GCodeBuffer *gb, bool doingG92, bool applyL
|
||||||
}
|
}
|
||||||
else if(drivesRelative)
|
else if(drivesRelative)
|
||||||
{
|
{
|
||||||
moveBuffer[drive + AXES] = moveArg * extrusionFactors[eDrive];
|
moveBuffer[drive + AXES] = moveArg * extrusionFactors[drive];
|
||||||
lastPos[drive] += moveArg;
|
lastPos[drive] += moveArg;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
moveBuffer[drive + AXES] = (moveArg - lastPos[drive]) * extrusionFactors[eDrive];
|
moveBuffer[drive + AXES] = (moveArg - lastPos[drive]) * extrusionFactors[drive];
|
||||||
lastPos[drive] = moveArg;
|
lastPos[drive] = moveArg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -430,10 +431,13 @@ int GCodes::SetUpMove(GCodeBuffer *gb)
|
||||||
if (moveAvailable)
|
if (moveAvailable)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
// Load the last position into moveBuffer; If Move can't accept more, return false
|
// Load the last position and feed rate into moveBuffer; If Move can't accept more, return false
|
||||||
if (!reprap.GetMove()->GetCurrentUserPosition(moveBuffer))
|
if (!reprap.GetMove()->GetCurrentUserPosition(moveBuffer))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
moveBuffer[DRIVES] *= speedFactorChange; // account for any change in the speed factor since the last move
|
||||||
|
speedFactorChange = 1.0;
|
||||||
|
|
||||||
// Check to see if the move is a 'homing' move that endstops are checked on.
|
// Check to see if the move is a 'homing' move that endstops are checked on.
|
||||||
checkEndStops = false;
|
checkEndStops = false;
|
||||||
if (gb->Seen('S'))
|
if (gb->Seen('S'))
|
||||||
|
@ -1367,21 +1371,22 @@ void GCodes::SetMACAddress(GCodeBuffer *gb)
|
||||||
|
|
||||||
void GCodes::HandleReply(bool error, bool fromLine, const char* reply, char gMOrT, int code, bool resend)
|
void GCodes::HandleReply(bool error, bool fromLine, const char* reply, char gMOrT, int code, bool resend)
|
||||||
{
|
{
|
||||||
if (gMOrT != 'M' || code != 111) // web server reply for M111 is handled before we get here
|
if (gMOrT != 'M' || (code != 111 && code != 122)) // web server reply for M111 and M122 is handled before we get here
|
||||||
{
|
{
|
||||||
if(error)
|
platform->Message((error) ? WEB_ERROR_MESSAGE : WEB_MESSAGE, reply);
|
||||||
platform->Message(WEB_ERROR_MESSAGE, reply);
|
|
||||||
else
|
|
||||||
platform->Message(WEB_MESSAGE, reply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Compatibility c = platform->Emulating();
|
Compatibility c = platform->Emulating();
|
||||||
if (!fromLine)
|
if (!fromLine)
|
||||||
|
{
|
||||||
c = me;
|
c = me;
|
||||||
|
}
|
||||||
|
|
||||||
const char* response = "ok";
|
const char* response = "ok";
|
||||||
if (resend)
|
if (resend)
|
||||||
|
{
|
||||||
response = "rs ";
|
response = "rs ";
|
||||||
|
}
|
||||||
|
|
||||||
const char* s = 0;
|
const char* s = 0;
|
||||||
|
|
||||||
|
@ -1392,13 +1397,14 @@ void GCodes::HandleReply(bool error, bool fromLine, const char* reply, char gMOr
|
||||||
if (!reply[0])
|
if (!reply[0])
|
||||||
return;
|
return;
|
||||||
if (error)
|
if (error)
|
||||||
|
{
|
||||||
platform->GetLine()->Write("Error: ");
|
platform->GetLine()->Write("Error: ");
|
||||||
|
}
|
||||||
platform->GetLine()->Write(reply);
|
platform->GetLine()->Write(reply);
|
||||||
platform->GetLine()->Write("\n");
|
platform->GetLine()->Write("\n");
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case marlin:
|
case marlin:
|
||||||
|
|
||||||
if (gMOrT == 'M' && code == 20)
|
if (gMOrT == 'M' && code == 20)
|
||||||
{
|
{
|
||||||
platform->GetLine()->Write("Begin file list\n");
|
platform->GetLine()->Write("Begin file list\n");
|
||||||
|
@ -2273,7 +2279,7 @@ bool GCodes::HandleMcode(GCodeBuffer* gb)
|
||||||
float newSpeedFactor = gb->GetFValue()/(60 * 100.0); // include the conversion from mm/minute to mm/second
|
float newSpeedFactor = gb->GetFValue()/(60 * 100.0); // include the conversion from mm/minute to mm/second
|
||||||
if (newSpeedFactor > 0)
|
if (newSpeedFactor > 0)
|
||||||
{
|
{
|
||||||
moveBuffer[DRIVES] *= newSpeedFactor/speedFactor;
|
speedFactorChange *= newSpeedFactor/speedFactor;
|
||||||
speedFactor = newSpeedFactor;
|
speedFactor = newSpeedFactor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2283,18 +2289,18 @@ bool GCodes::HandleMcode(GCodeBuffer* gb)
|
||||||
if (gb->Seen('S')) // S parameter sets the override percentage
|
if (gb->Seen('S')) // S parameter sets the override percentage
|
||||||
{
|
{
|
||||||
float extrusionFactor = gb->GetFValue()/100.0;
|
float extrusionFactor = gb->GetFValue()/100.0;
|
||||||
int head;
|
int drive;
|
||||||
if (gb->Seen('P')) // P parameter (if present) selects the head
|
if (gb->Seen('D')) // D parameter (if present) selects the extruder drive number
|
||||||
{
|
{
|
||||||
head = gb->GetIValue();
|
drive = gb->GetIValue();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
head = 1; // default to head 1 if not specified
|
drive = 0; // default to drive 0 if not specified
|
||||||
}
|
}
|
||||||
if (head >= 1 && head < DRIVES - AXES + 1 && extrusionFactor >= 0)
|
if (drive >= 0 && drive < DRIVES - AXES && extrusionFactor >= 0)
|
||||||
{
|
{
|
||||||
extrusionFactors[head - 1] = extrusionFactor;
|
extrusionFactors[drive] = extrusionFactor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
1
GCodes.h
1
GCodes.h
|
@ -191,6 +191,7 @@ class GCodes
|
||||||
bool waitingForMoveToComplete;
|
bool waitingForMoveToComplete;
|
||||||
bool coolingInverted;
|
bool coolingInverted;
|
||||||
float speedFactor; // speed factor, including the conversion from mm/min to mm/sec, normally 1/60
|
float speedFactor; // speed factor, including the conversion from mm/min to mm/sec, normally 1/60
|
||||||
|
float speedFactorChange; // factor by which we changed the speed factor since the last move
|
||||||
float extrusionFactors[DRIVES - AXES]; // extrusion factors (normally 1.0)
|
float extrusionFactors[DRIVES - AXES]; // extrusion factors (normally 1.0)
|
||||||
int8_t toolChangeSequence; // Steps through the tool change procedure
|
int8_t toolChangeSequence; // Steps through the tool change procedure
|
||||||
};
|
};
|
||||||
|
|
2
Heat.cpp
2
Heat.cpp
|
@ -62,7 +62,7 @@ void Heat::Spin()
|
||||||
|
|
||||||
void Heat::Diagnostics()
|
void Heat::Diagnostics()
|
||||||
{
|
{
|
||||||
platform->Message(HOST_MESSAGE, "Heat Diagnostics:\n");
|
platform->AppendMessage(BOTH_MESSAGE, "Heat Diagnostics:\n");
|
||||||
// TODO - Put something useful in here
|
// TODO - Put something useful in here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
Move.cpp
2
Move.cpp
|
@ -327,7 +327,7 @@ void Move::SetFeedrate(float feedRate)
|
||||||
|
|
||||||
void Move::Diagnostics()
|
void Move::Diagnostics()
|
||||||
{
|
{
|
||||||
platform->Message(HOST_MESSAGE, "Move Diagnostics:\n");
|
platform->AppendMessage(BOTH_MESSAGE, "Move Diagnostics:\n");
|
||||||
|
|
||||||
/* if(active)
|
/* if(active)
|
||||||
platform->Message(HOST_MESSAGE, " active\n");
|
platform->Message(HOST_MESSAGE, " active\n");
|
||||||
|
|
|
@ -714,14 +714,15 @@ void Platform::Tick()
|
||||||
|
|
||||||
//*************************************************************************************************
|
//*************************************************************************************************
|
||||||
|
|
||||||
|
// This diagnostics function is the first to be called, so it calls Message to start with.
|
||||||
|
// All other messages generated by this and other diagnostics functions must call AppendMessage.
|
||||||
void Platform::Diagnostics()
|
void Platform::Diagnostics()
|
||||||
{
|
{
|
||||||
Message(HOST_MESSAGE, "Platform Diagnostics:\n");
|
Message(BOTH_MESSAGE, "Platform Diagnostics:\n");
|
||||||
|
|
||||||
// Print memory stats and error codes to USB and copy them to the current webserver reply
|
// Print memory stats and error codes to USB and copy them to the current webserver reply
|
||||||
const char *ramstart = (char *) 0x20070000;
|
const char *ramstart = (char *) 0x20070000;
|
||||||
const struct mallinfo mi = mallinfo();
|
const struct mallinfo mi = mallinfo();
|
||||||
Message(BOTH_MESSAGE, "\n");
|
|
||||||
AppendMessage(BOTH_MESSAGE, "Memory usage:\n\n");
|
AppendMessage(BOTH_MESSAGE, "Memory usage:\n\n");
|
||||||
snprintf(scratchString, STRING_LENGTH, "Program static ram used: %d\n", &_end - ramstart);
|
snprintf(scratchString, STRING_LENGTH, "Program static ram used: %d\n", &_end - ramstart);
|
||||||
AppendMessage(BOTH_MESSAGE, scratchString);
|
AppendMessage(BOTH_MESSAGE, scratchString);
|
||||||
|
@ -777,7 +778,7 @@ void Platform::Diagnostics()
|
||||||
reprap.Timing();
|
reprap.Timing();
|
||||||
|
|
||||||
#if LWIP_STATS
|
#if LWIP_STATS
|
||||||
// Print LWIP stats to USB
|
// Print LWIP stats to USB only
|
||||||
stats_display();
|
stats_display();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
|
@ -177,6 +177,8 @@ RepRap::RepRap() : active(false), debug(false), stopped(false), spinState(0), ti
|
||||||
void RepRap::Init()
|
void RepRap::Init()
|
||||||
{
|
{
|
||||||
debug = false;
|
debug = false;
|
||||||
|
activeExtruders = 1; // we always report at least 1 extruder to the web interface
|
||||||
|
activeHeaters = 2; // we always report the bed heater + 1 extruder heater to the web interface
|
||||||
|
|
||||||
// All of the following init functions must execute reasonably quickly before the watchdog times us out
|
// All of the following init functions must execute reasonably quickly before the watchdog times us out
|
||||||
platform->Init();
|
platform->Init();
|
||||||
|
@ -302,12 +304,11 @@ void RepRap::Timing()
|
||||||
|
|
||||||
void RepRap::Diagnostics()
|
void RepRap::Diagnostics()
|
||||||
{
|
{
|
||||||
platform->Diagnostics();
|
platform->Diagnostics(); // this includes a call to our Timing() function
|
||||||
move->Diagnostics();
|
move->Diagnostics();
|
||||||
heat->Diagnostics();
|
heat->Diagnostics();
|
||||||
gCodes->Diagnostics();
|
gCodes->Diagnostics();
|
||||||
webserver->Diagnostics();
|
webserver->Diagnostics();
|
||||||
Timing();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn off the heaters, disable the motors, and
|
// Turn off the heaters, disable the motors, and
|
||||||
|
@ -366,6 +367,7 @@ void RepRap::AddTool(Tool* tool)
|
||||||
}
|
}
|
||||||
|
|
||||||
toolList->AddTool(tool);
|
toolList->AddTool(tool);
|
||||||
|
tool->UpdateExtrudersAndHeaters(activeExtruders, activeHeaters);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RepRap::SelectTool(int toolNumber)
|
void RepRap::SelectTool(int toolNumber)
|
||||||
|
|
6
Reprap.h
6
Reprap.h
|
@ -50,6 +50,8 @@ class RepRap
|
||||||
void Tick();
|
void Tick();
|
||||||
bool IsStopped() const;
|
bool IsStopped() const;
|
||||||
uint16_t GetTicksInSpinState() const;
|
uint16_t GetTicksInSpinState() const;
|
||||||
|
uint16_t GetExtrudersInUse() const;
|
||||||
|
uint16_t GetHeatersInUse() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
@ -69,6 +71,8 @@ class RepRap
|
||||||
bool stopped;
|
bool stopped;
|
||||||
bool active;
|
bool active;
|
||||||
bool resetting;
|
bool resetting;
|
||||||
|
uint16_t activeExtruders;
|
||||||
|
uint16_t activeHeaters;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline Platform* RepRap::GetPlatform() const { return platform; }
|
inline Platform* RepRap::GetPlatform() const { return platform; }
|
||||||
|
@ -79,6 +83,8 @@ inline Network* RepRap::GetNetwork() const { return network; }
|
||||||
inline Webserver* RepRap::GetWebserver() const { return webserver; }
|
inline Webserver* RepRap::GetWebserver() const { return webserver; }
|
||||||
inline bool RepRap::Debug() const { return debug; }
|
inline bool RepRap::Debug() const { return debug; }
|
||||||
inline Tool* RepRap::GetCurrentTool() { return currentTool; }
|
inline Tool* RepRap::GetCurrentTool() { return currentTool; }
|
||||||
|
inline uint16_t RepRap::GetExtrudersInUse() const { return activeExtruders; }
|
||||||
|
inline uint16_t RepRap::GetHeatersInUse() const { return activeHeaters; }
|
||||||
|
|
||||||
inline void RepRap::SetDebug(bool d)
|
inline void RepRap::SetDebug(bool d)
|
||||||
{
|
{
|
||||||
|
|
19
Tool.cpp
19
Tool.cpp
|
@ -173,4 +173,23 @@ void Tool::GetVariables(float* standby, float* active)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the number of active drives and extruders in use to reflect what this tool uses
|
||||||
|
void Tool::UpdateExtrudersAndHeaters(uint16_t &numExtruders, uint16_t &numHeaters)
|
||||||
|
{
|
||||||
|
for(int8_t drive = 0; drive < driveCount; drive++)
|
||||||
|
{
|
||||||
|
if (drives[drive] >= numExtruders)
|
||||||
|
{
|
||||||
|
numExtruders = drives[drive] + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int8_t heater = 0; heater < heaterCount; heater++)
|
||||||
|
{
|
||||||
|
if (heaters[heater] >= numHeaters)
|
||||||
|
{
|
||||||
|
numHeaters = heaters[heater] + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
1
Tool.h
1
Tool.h
|
@ -50,6 +50,7 @@ protected:
|
||||||
void Activate(Tool* currentlyActive);
|
void Activate(Tool* currentlyActive);
|
||||||
void Standby();
|
void Standby();
|
||||||
void AddTool(Tool* t);
|
void AddTool(Tool* t);
|
||||||
|
void UpdateExtrudersAndHeaters(uint16_t &extruders, uint16_t &heaters);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
|
|
@ -273,7 +273,7 @@ void Webserver::Exit()
|
||||||
|
|
||||||
void Webserver::Diagnostics()
|
void Webserver::Diagnostics()
|
||||||
{
|
{
|
||||||
platform->Message(HOST_MESSAGE, "Webserver Diagnostics:\n");
|
platform->AppendMessage(BOTH_MESSAGE, "Webserver Diagnostics:\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Webserver::SetPassword(const char* pw)
|
void Webserver::SetPassword(const char* pw)
|
||||||
|
@ -970,7 +970,7 @@ void Webserver::HttpInterpreter::GetStatusResponse(uint8_t type)
|
||||||
|
|
||||||
// Send the heater temperatures
|
// Send the heater temperatures
|
||||||
ch = '[';
|
ch = '[';
|
||||||
for (int8_t heater = 0; heater < HEATERS; heater++)
|
for (int8_t heater = 0; heater < reprap.GetHeatersInUse(); heater++)
|
||||||
{
|
{
|
||||||
sncatf(jsonResponse, ARRAY_UPB(jsonResponse), "%c\%.1f", ch, reprap.GetHeat()->GetTemperature(heater));
|
sncatf(jsonResponse, ARRAY_UPB(jsonResponse), "%c\%.1f", ch, reprap.GetHeat()->GetTemperature(heater));
|
||||||
ch = ',';
|
ch = ',';
|
||||||
|
@ -988,9 +988,9 @@ void Webserver::HttpInterpreter::GetStatusResponse(uint8_t type)
|
||||||
}
|
}
|
||||||
sncatf(jsonResponse, ARRAY_UPB(jsonResponse), "],\"extr\":"); // announce the extruder positions
|
sncatf(jsonResponse, ARRAY_UPB(jsonResponse), "],\"extr\":"); // announce the extruder positions
|
||||||
ch = '[';
|
ch = '[';
|
||||||
for (int8_t drive = AXES; drive < DRIVES; drive++) // loop through extruders
|
for (int8_t drive = 0; drive < reprap.GetExtrudersInUse(); drive++) // loop through extruders
|
||||||
{
|
{
|
||||||
sncatf(jsonResponse, ARRAY_UPB(jsonResponse), "%c%.3f", ch, gc->GetExtruderPosition(drive - AXES));
|
sncatf(jsonResponse, ARRAY_UPB(jsonResponse), "%c%.3f", ch, gc->GetExtruderPosition(drive));
|
||||||
ch = ',';
|
ch = ',';
|
||||||
}
|
}
|
||||||
strncat(jsonResponse, "]", ARRAY_UPB(jsonResponse));
|
strncat(jsonResponse, "]", ARRAY_UPB(jsonResponse));
|
||||||
|
@ -998,7 +998,7 @@ void Webserver::HttpInterpreter::GetStatusResponse(uint8_t type)
|
||||||
// Send the speed and extruder override factors
|
// Send the speed and extruder override factors
|
||||||
sncatf(jsonResponse, ARRAY_UPB(jsonResponse), ",\"sfactor\":%.2f,\"efactor:\":", gc->GetSpeedFactor() * 100.0);
|
sncatf(jsonResponse, ARRAY_UPB(jsonResponse), ",\"sfactor\":%.2f,\"efactor:\":", gc->GetSpeedFactor() * 100.0);
|
||||||
const float *extrusionFactors = gc->GetExtrusionFactors();
|
const float *extrusionFactors = gc->GetExtrusionFactors();
|
||||||
for (unsigned int i = 0; i < DRIVES - AXES; ++i)
|
for (unsigned int i = 0; i < reprap.GetExtrudersInUse(); ++i)
|
||||||
{
|
{
|
||||||
sncatf(jsonResponse, ARRAY_UPB(jsonResponse), "%c%.2f", (i == 0) ? '[' : ',', extrusionFactors[i] * 100.0);
|
sncatf(jsonResponse, ARRAY_UPB(jsonResponse), "%c%.2f", (i == 0) ? '[' : ',', extrusionFactors[i] * 100.0);
|
||||||
}
|
}
|
||||||
|
@ -1009,6 +1009,7 @@ void Webserver::HttpInterpreter::GetStatusResponse(uint8_t type)
|
||||||
// The old (deprecated) poll response lists the status, then all the heater temperatures, then the XYZ positions, then all the extruder positions.
|
// The old (deprecated) poll response lists the status, then all the heater temperatures, then the XYZ positions, then all the extruder positions.
|
||||||
// These are all returned in a single vector called "poll".
|
// These are all returned in a single vector called "poll".
|
||||||
// This is a poor choice of format because we can't easily tell which is which unless we already know the number of heaters and extruders.
|
// This is a poor choice of format because we can't easily tell which is which unless we already know the number of heaters and extruders.
|
||||||
|
// RRP reversed the order at version 0.65 to send the positions before the heaters, but we haven't yet done that.
|
||||||
char c = (gc->PrintingAFile()) ? 'P' : 'I';
|
char c = (gc->PrintingAFile()) ? 'P' : 'I';
|
||||||
snprintf(jsonResponse, ARRAY_UPB(jsonResponse), "{\"poll\":[\"%c\",", c); // Printing
|
snprintf(jsonResponse, ARRAY_UPB(jsonResponse), "{\"poll\":[\"%c\",", c); // Printing
|
||||||
for (int8_t heater = 0; heater < HEATERS; heater++)
|
for (int8_t heater = 0; heater < HEATERS; heater++)
|
||||||
|
|
Reference in a new issue