mirror of
https://github.com/Dark98/SliceBeam.git
synced 2026-07-03 00:38:53 +00:00
Initial ElegooLink Support (Only Centauri Carbon Tested)
This commit is contained in:
@@ -110,6 +110,7 @@ dependencies {
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'com.loopj.android:android-async-http:1.4.11'
|
||||
implementation 'androidx.activity:activity:1.10.1'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
|
||||
}
|
||||
|
||||
def loadLocalProperties() {
|
||||
|
||||
@@ -56,6 +56,7 @@ import ru.ytkab0bp.slicebeam.config.ConfigObject;
|
||||
import ru.ytkab0bp.slicebeam.events.NeedDismissSnackbarEvent;
|
||||
import ru.ytkab0bp.slicebeam.events.NeedSnackbarEvent;
|
||||
import ru.ytkab0bp.slicebeam.fragment.BedFragment;
|
||||
import ru.ytkab0bp.slicebeam.print_host.ElegooLinkClient;
|
||||
import ru.ytkab0bp.slicebeam.recycler.SimpleRecyclerItem;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.GCodeProcessorResult;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.GCodeViewer;
|
||||
@@ -76,7 +77,7 @@ public class SliceMenu extends ListBedMenu {
|
||||
client.setMaxRetriesAndTimeout(0, 10000);
|
||||
}
|
||||
|
||||
private final static List<String> SUPPORTED_SEND = Collections.singletonList("octoprint");
|
||||
private final static List<String> SUPPORTED_SEND = Arrays.asList("octoprint", "elegoolink");
|
||||
private int lastUid;
|
||||
|
||||
@Override
|
||||
@@ -121,13 +122,14 @@ public class SliceMenu extends ListBedMenu {
|
||||
String apiKey = obj.get("printhost_apikey");
|
||||
if (SUPPORTED_SEND.contains(type) && !TextUtils.isEmpty(host)) {
|
||||
String finalType = type;
|
||||
items.add(new BedMenuItem(R.string.MenuSliceSendToPrinter, R.drawable.send_outline_28).onClick(v -> upload(finalType, host, apiKey, false)));
|
||||
items.add(new BedMenuItem(R.string.MenuSliceSendToPrinterAndPrint, R.drawable.send_28).onClick(v -> upload(finalType, host, apiKey, true)));
|
||||
ConfigObject finalObj = obj;
|
||||
items.add(new BedMenuItem(R.string.MenuSliceSendToPrinter, R.drawable.send_outline_28).onClick(v -> upload(finalType, host, apiKey, false, finalObj)));
|
||||
items.add(new BedMenuItem(R.string.MenuSliceSendToPrinterAndPrint, R.drawable.send_28).onClick(v -> upload(finalType, host, apiKey, true, finalObj)));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private void upload(String type, String host, String apiKey, boolean print) {
|
||||
private void upload(String type, String host, String apiKey, boolean print, ConfigObject config) {
|
||||
String name = fragment.getGlView().getRenderer().getGcodeResult().getRecommendedName();
|
||||
switch (type) {
|
||||
default:
|
||||
@@ -172,7 +174,41 @@ public class SliceMenu extends ListBedMenu {
|
||||
.show());
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "elegoolink": {
|
||||
if (!host.startsWith("http://") && !host.startsWith("https://")) {
|
||||
host = "http://" + host;
|
||||
}
|
||||
String elegooTag = UUID.randomUUID().toString();
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(SnackbarsLayout.Type.LOADING, R.string.MenuSliceSendToPrinterLoading).tag(elegooTag));
|
||||
String finalHost = host;
|
||||
final boolean timelapse = config != null && "1".equals(config.get("elegoolink_timelapse"));
|
||||
final boolean bedLeveling = config != null && "1".equals(config.get("elegoolink_bed_leveling"));
|
||||
int bedTypeVal = 0;
|
||||
if (config != null) {
|
||||
String bedTypeValue = config.get("elegoolink_bed_type");
|
||||
if ("1".equals(bedTypeValue) || "pc".equalsIgnoreCase(bedTypeValue)) {
|
||||
bedTypeVal = 1;
|
||||
}
|
||||
}
|
||||
final int bedType = bedTypeVal;
|
||||
new Thread(() -> {
|
||||
ElegooLinkClient.Result result = ElegooLinkClient.upload(BedFragment.getTempGCodePath(), finalHost, name, print, timelapse, bedLeveling, bedType);
|
||||
ViewUtils.postOnMainThread(() -> {
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedDismissSnackbarEvent(elegooTag));
|
||||
if (result.ok) {
|
||||
SliceBeam.EVENT_BUS.fireEvent(new NeedSnackbarEvent(print ? SnackbarsLayout.Type.INFO : SnackbarsLayout.Type.DONE, print ? R.string.MenuSliceSendToPrinterPrintStarted : R.string.MenuSliceSendToPrinterOK));
|
||||
} else {
|
||||
new BeamAlertDialogBuilder(fragment.getContext())
|
||||
.setTitle(R.string.MenuSliceSendToPrinterFailed)
|
||||
.setMessage(result.error)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}).start();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,10 @@ public class ConfigObject implements ProfileListFragment.ProfileListItem {
|
||||
custom.put("machine_min_extruding_rate", "0");
|
||||
custom.put("machine_min_travel_rate", "0");
|
||||
|
||||
custom.put("elegoolink_timelapse", "0");
|
||||
custom.put("elegoolink_bed_leveling", "0");
|
||||
custom.put("elegoolink_bed_type", "pte");
|
||||
|
||||
custom.put("start_gcode", "G90 ; use absolute coordinates\\nM83 ; extruder relative mode\\nM104 S{is_nil(idle_temperature[0]) ? 150 : idle_temperature[0]} ; set temporary nozzle temp to prevent oozing during homing\\nM140 S{first_layer_bed_temperature[0]} ; set final bed temp\\nG4 S30 ; allow partial nozzle warmup\\nG28 ; home all axis\\nG1 Z50 F240\\nG1 X2.0 Y10 F3000\\nM104 S{first_layer_temperature[0]} ; set final nozzle temp\\nM190 S{first_layer_bed_temperature[0]} ; wait for bed temp to stabilize\\nM109 S{first_layer_temperature[0]} ; wait for nozzle temp to stabilize\\nG1 Z0.28 F240\\nG92 E0\\nG1 X2.0 Y140 E10 F1500 ; prime the nozzle\\nG1 X2.3 Y140 F5000\\nG92 E0\\nG1 X2.3 Y10 E10 F1200 ; prime the nozzle\\nG92 E0");
|
||||
custom.put("end_gcode", "{if max_layer_z < max_print_height}G1 Z{z_offset+min(max_layer_z+2, max_print_height)} F600 ; Move print head up{endif}\\nG1 X5 Y{print_bed_max[1]*0.85} F{travel_speed*60} ; present print\\n{if max_layer_z < max_print_height-10}G1 Z{z_offset+min(max_layer_z+70, max_print_height-10)} F600 ; Move print head further up{endif}\\n{if max_layer_z < max_print_height*0.6}G1 Z{max_print_height*0.6} F600 ; Move print head further up{endif}\\nM140 S0 ; turn off heatbed\\nM104 S0 ; turn off temperature\\nM107 ; turn off fan\\nM84 X Y E ; disable motors");
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import ru.ytkab0bp.slicebeam.SliceBeam;
|
||||
import ru.ytkab0bp.slicebeam.config.ConfigObject;
|
||||
import ru.ytkab0bp.slicebeam.recycler.SpaceItem;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.PrintConfigDef;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.ConfigOptionDef;
|
||||
import ru.ytkab0bp.slicebeam.slic3r.Slic3rLocalization;
|
||||
import ru.ytkab0bp.slicebeam.utils.ViewUtils;
|
||||
|
||||
@@ -175,6 +176,22 @@ public class PrinterConfigFragment extends ProfileListFragment {
|
||||
new OptionElement(def.options.get("printhost_apikey"))
|
||||
));
|
||||
|
||||
String hostType = null;
|
||||
if (diffObject != null && diffObject.has("host_type")) {
|
||||
hostType = diffObject.get("host_type");
|
||||
}
|
||||
if (hostType == null) {
|
||||
hostType = currentConfig.get("host_type");
|
||||
}
|
||||
if ("elegoolink".equalsIgnoreCase(hostType)) {
|
||||
list.addAll(Arrays.asList(
|
||||
new OptionElement(new SubHeader("ElegooLink")),
|
||||
new OptionElement(def.options.get("elegoolink_timelapse")),
|
||||
new OptionElement(def.options.get("elegoolink_bed_leveling")),
|
||||
new OptionElement(def.options.get("elegoolink_bed_type"))
|
||||
));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -237,4 +254,18 @@ public class PrinterConfigFragment extends ProfileListFragment {
|
||||
// TODO: Reset print/filament profiles, maybe physical profiles?
|
||||
SliceBeam.saveConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateConfigField(ConfigOptionDef def, int i, String value) {
|
||||
super.updateConfigField(def, i, value);
|
||||
if ("host_type".equals(def.key)) {
|
||||
onUpdateConfigItems();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateConfigItems() {
|
||||
setConfigItems(getConfigItems());
|
||||
super.onUpdateConfigItems();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,6 +519,7 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
protected void setConfigItems(List<OptionElement> items) {
|
||||
categoryElements.clear();
|
||||
List<OptionWrapper> list = new ArrayList<>();
|
||||
int j = 0;
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
@@ -539,7 +540,21 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
categoryElements.get(j - 1).add(w);
|
||||
}
|
||||
}
|
||||
currentList = list;
|
||||
List<OptionWrapper> expanded = new ArrayList<>();
|
||||
int categoryIndex = 0;
|
||||
for (OptionWrapper w : list) {
|
||||
expanded.add(w);
|
||||
if (w.categoryIndex == categoryIndex && unfolded.get(categoryIndex)) {
|
||||
List<OptionWrapper> extra = categoryElements.get(categoryIndex);
|
||||
if (extra != null) {
|
||||
expanded.addAll(extra);
|
||||
}
|
||||
}
|
||||
if (w.categoryIndex == categoryIndex) {
|
||||
categoryIndex++;
|
||||
}
|
||||
}
|
||||
currentList = expanded;
|
||||
recyclerView.getAdapter().notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -676,8 +691,8 @@ public abstract class ProfileListFragment extends Fragment {
|
||||
String[] labels;
|
||||
String[] values;
|
||||
if (Objects.equals("host_type", def.key)) {
|
||||
labels = new String[]{"OctoPrint"};
|
||||
values = new String[]{"octoprint"};
|
||||
labels = new String[]{"OctoPrint", "ElegooLink"};
|
||||
values = new String[]{"octoprint", "elegoolink"};
|
||||
} else {
|
||||
labels = new String[def.enumLabels.length];
|
||||
values = def.enumValues;
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
package ru.ytkab0bp.slicebeam.print_host;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okio.BufferedSink;
|
||||
|
||||
public final class ElegooLinkClient {
|
||||
private static final int MAX_UPLOAD_PACKAGE_LENGTH = 1024 * 1024;
|
||||
private static final MediaType OCTET_STREAM = MediaType.parse("application/octet-stream");
|
||||
|
||||
private ElegooLinkClient() {}
|
||||
|
||||
public static Result upload(File gcode, String host, String uploadName, boolean startPrint, boolean timelapse, boolean bedLeveling, int bedType) {
|
||||
if (gcode == null || !gcode.exists()) {
|
||||
return Result.error("G-code file not found.");
|
||||
}
|
||||
String finalName = (uploadName == null || uploadName.isEmpty()) ? gcode.getName() : uploadName;
|
||||
String baseUrl = normalizeBaseUrl(host);
|
||||
String uploadUrl = baseUrl + "/uploadFile/upload";
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.callTimeout(60, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
String md5;
|
||||
try {
|
||||
md5 = md5File(gcode);
|
||||
} catch (Exception e) {
|
||||
return Result.error("Failed to compute MD5: " + e.getMessage());
|
||||
}
|
||||
long size = gcode.length();
|
||||
String uuid = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
int packageCount = (int) ((size + MAX_UPLOAD_PACKAGE_LENGTH - 1) / MAX_UPLOAD_PACKAGE_LENGTH);
|
||||
for (int i = 0; i < packageCount; i++) {
|
||||
long offset = (long) MAX_UPLOAD_PACKAGE_LENGTH * i;
|
||||
long length = Math.min(MAX_UPLOAD_PACKAGE_LENGTH, size - offset);
|
||||
Result partRes = uploadPart(client, uploadUrl, gcode, finalName, md5, uuid, size, offset, length);
|
||||
if (!partRes.ok) {
|
||||
return partRes;
|
||||
}
|
||||
}
|
||||
|
||||
if (!startPrint) {
|
||||
return Result.ok();
|
||||
}
|
||||
return startPrint(client, host, finalName, timelapse, bedLeveling, bedType);
|
||||
}
|
||||
|
||||
private static Result uploadPart(OkHttpClient client, String url, File file, String uploadName, String md5, String uuid, long totalSize, long offset, long length) {
|
||||
RequestBody fileBody = new FileSliceRequestBody(file, offset, length);
|
||||
MultipartBody requestBody = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("Check", "1")
|
||||
.addFormDataPart("S-File-MD5", md5)
|
||||
.addFormDataPart("Offset", String.valueOf(offset))
|
||||
.addFormDataPart("Uuid", uuid)
|
||||
.addFormDataPart("TotalSize", String.valueOf(totalSize))
|
||||
.addFormDataPart("File", uploadName, fileBody)
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
String body = response.body() != null ? response.body().string() : "";
|
||||
if (!response.isSuccessful()) {
|
||||
return Result.error("Upload failed: HTTP " + response.code());
|
||||
}
|
||||
if (!isElegooOk(body)) {
|
||||
return Result.error(parseElegooError(body));
|
||||
}
|
||||
return Result.ok();
|
||||
} catch (IOException e) {
|
||||
return Result.error("Upload failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Result startPrint(OkHttpClient client, String host, String filename, boolean timelapse, boolean bedLeveling, int bedType) {
|
||||
WebSocketSession session = null;
|
||||
String lastError = null;
|
||||
for (String wsUrl : buildWebSocketCandidates(host)) {
|
||||
WebSocketSession attempt = new WebSocketSession(client, wsUrl);
|
||||
if (attempt.awaitOpen(10, TimeUnit.SECONDS)) {
|
||||
session = attempt;
|
||||
break;
|
||||
}
|
||||
lastError = attempt.failureSummary();
|
||||
if (attempt.isTooManyClients()) {
|
||||
return Result.error("Printer reports too many connected clients. Close the ElegooLink web UI and other apps, then try again.");
|
||||
}
|
||||
attempt.close();
|
||||
}
|
||||
if (session == null) {
|
||||
if (lastError != null && !lastError.isEmpty()) {
|
||||
return Result.error("Failed to connect to ElegooLink websocket. " + lastError);
|
||||
}
|
||||
return Result.error("Failed to connect to ElegooLink websocket.");
|
||||
}
|
||||
|
||||
String requestId = UUID.randomUUID().toString().replace("-", "");
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String json = "{"
|
||||
+ "\"Id\":\"\","
|
||||
+ "\"Data\":{"
|
||||
+ "\"Cmd\":128,"
|
||||
+ "\"Data\":{"
|
||||
+ "\"Filename\":\"/local/" + filename + "\","
|
||||
+ "\"StartLayer\":0,"
|
||||
+ "\"Calibration_switch\":" + (bedLeveling ? 1 : 0) + ","
|
||||
+ "\"PrintPlatformType\":" + (bedType != 0 ? 1 : 0) + ","
|
||||
+ "\"Tlp_Switch\":" + (timelapse ? 1 : 0)
|
||||
+ "},"
|
||||
+ "\"RequestID\":\"" + requestId + "\","
|
||||
+ "\"MainboardID\":\"\","
|
||||
+ "\"TimeStamp\":" + timestamp + ","
|
||||
+ "\"From\":1"
|
||||
+ "}"
|
||||
+ "}";
|
||||
|
||||
session.sendText(json);
|
||||
String response = session.receiveText(30, TimeUnit.SECONDS);
|
||||
if (response == null) {
|
||||
session.close();
|
||||
return Result.error("Start print timeout.");
|
||||
}
|
||||
try {
|
||||
JSONObject root = new JSONObject(response);
|
||||
JSONObject data = root.optJSONObject("Data");
|
||||
if (data == null) {
|
||||
session.close();
|
||||
return Result.error("Invalid response from printer.");
|
||||
}
|
||||
int cmd = data.optInt("Cmd", -1);
|
||||
if (cmd != 128) {
|
||||
session.close();
|
||||
return Result.error("Unexpected response from printer.");
|
||||
}
|
||||
JSONObject ackData = data.optJSONObject("Data");
|
||||
int ack = ackData != null ? ackData.optInt("Ack", -1) : -1;
|
||||
if (ack == 0) {
|
||||
session.close();
|
||||
return Result.ok();
|
||||
}
|
||||
String error = mapAckError(ack);
|
||||
session.close();
|
||||
return Result.error(error);
|
||||
} catch (JSONException e) {
|
||||
session.close();
|
||||
return Result.error("Invalid response from printer.");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isElegooOk(String body) {
|
||||
try {
|
||||
JSONObject root = new JSONObject(body);
|
||||
String code = root.optString("code", "");
|
||||
return "000000".equals(code);
|
||||
} catch (JSONException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String parseElegooError(String body) {
|
||||
try {
|
||||
JSONObject root = new JSONObject(body);
|
||||
String code = root.optString("code", "unknown");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ErrorCode: ").append(code);
|
||||
JSONArray messages = root.optJSONArray("messages");
|
||||
if (messages != null) {
|
||||
for (int i = 0; i < messages.length(); i++) {
|
||||
JSONObject msg = messages.optJSONObject(i);
|
||||
if (msg != null) {
|
||||
sb.append("\n").append(msg.optString("field", ""))
|
||||
.append(":").append(msg.optString("message", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (JSONException e) {
|
||||
return "Upload failed.";
|
||||
}
|
||||
}
|
||||
|
||||
private static String mapAckError(int ack) {
|
||||
switch (ack) {
|
||||
case 1:
|
||||
return "The printer is busy.";
|
||||
case 2:
|
||||
return "The file is missing.";
|
||||
case 3:
|
||||
return "MD5 check failed.";
|
||||
case 4:
|
||||
return "File I/O error.";
|
||||
case 5:
|
||||
case 6:
|
||||
return "File format or resolution is invalid.";
|
||||
case 7:
|
||||
return "File does not match the printer.";
|
||||
default:
|
||||
return "Unknown error. Error code: " + ack;
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalizeBaseUrl(String host) {
|
||||
String value = host.trim();
|
||||
if (!value.contains("://")) {
|
||||
value = "http://" + value;
|
||||
}
|
||||
if (value.endsWith("/")) {
|
||||
value = value.substring(0, value.length() - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static List<String> buildWebSocketCandidates(String host) {
|
||||
String h = sanitizeWebSocketHost(host);
|
||||
List<String> urls = new ArrayList<>(1);
|
||||
urls.add(String.format(Locale.US, "ws://%s:3030/websocket", h));
|
||||
return urls;
|
||||
}
|
||||
|
||||
private static String sanitizeWebSocketHost(String host) {
|
||||
String base = normalizeBaseUrl(host);
|
||||
try {
|
||||
URI uri = new URI(base);
|
||||
String h = uri.getHost() != null ? uri.getHost() : uri.getAuthority();
|
||||
if (h != null && h.contains(":")) {
|
||||
h = h.substring(0, h.indexOf(':'));
|
||||
}
|
||||
if (h == null || h.isEmpty()) {
|
||||
h = host;
|
||||
}
|
||||
return wrapIpv6Host(h);
|
||||
} catch (URISyntaxException e) {
|
||||
String h = host;
|
||||
int schemeIdx = h.indexOf("://");
|
||||
if (schemeIdx != -1) {
|
||||
h = h.substring(schemeIdx + 3);
|
||||
}
|
||||
int slashIdx = h.indexOf('/');
|
||||
if (slashIdx != -1) {
|
||||
h = h.substring(0, slashIdx);
|
||||
}
|
||||
int portIdx = h.indexOf(':');
|
||||
if (portIdx != -1) {
|
||||
h = h.substring(0, portIdx);
|
||||
}
|
||||
return wrapIpv6Host(h);
|
||||
}
|
||||
}
|
||||
|
||||
private static String wrapIpv6Host(String host) {
|
||||
if (host == null || host.isEmpty()) {
|
||||
return host;
|
||||
}
|
||||
if (host.indexOf(':') != -1 && !host.startsWith("[") && !host.endsWith("]")) {
|
||||
return "[" + host + "]";
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
private static String md5File(File file) throws IOException, NoSuchAlgorithmException {
|
||||
MessageDigest digest = MessageDigest.getInstance("MD5");
|
||||
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : digest.digest()) {
|
||||
sb.append(String.format(Locale.US, "%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static final class FileSliceRequestBody extends RequestBody {
|
||||
private final File file;
|
||||
private final long offset;
|
||||
private final long length;
|
||||
|
||||
FileSliceRequestBody(File file, long offset, long length) {
|
||||
this.file = file;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaType contentType() {
|
||||
return OCTET_STREAM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(BufferedSink sink) throws IOException {
|
||||
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
|
||||
raf.seek(offset);
|
||||
byte[] buffer = new byte[8192];
|
||||
long remaining = length;
|
||||
while (remaining > 0) {
|
||||
int read = raf.read(buffer, 0, (int) Math.min(buffer.length, remaining));
|
||||
if (read == -1) {
|
||||
break;
|
||||
}
|
||||
sink.write(buffer, 0, read);
|
||||
remaining -= read;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class WebSocketSession extends okhttp3.WebSocketListener {
|
||||
private final java.util.concurrent.BlockingQueue<String> messages = new java.util.concurrent.LinkedBlockingQueue<>();
|
||||
private final okhttp3.WebSocket socket;
|
||||
private volatile boolean opened;
|
||||
private volatile String failureBody;
|
||||
private volatile okhttp3.Response failureResponse;
|
||||
private volatile Throwable failure;
|
||||
|
||||
WebSocketSession(OkHttpClient client, String url) {
|
||||
Request request = new Request.Builder().url(url).build();
|
||||
socket = client.newWebSocket(request, this);
|
||||
}
|
||||
|
||||
boolean awaitOpen(long timeout, TimeUnit unit) {
|
||||
try {
|
||||
long deadline = System.nanoTime() + unit.toNanos(timeout);
|
||||
while (!opened && System.nanoTime() < deadline) {
|
||||
Thread.sleep(50);
|
||||
}
|
||||
return opened;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void sendText(String message) {
|
||||
socket.send(message);
|
||||
}
|
||||
|
||||
String receiveText(long timeout, TimeUnit unit) {
|
||||
try {
|
||||
return messages.poll(timeout, unit);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
void close() {
|
||||
socket.close(1000, "done");
|
||||
}
|
||||
|
||||
String failureSummary() {
|
||||
if (failureResponse != null) {
|
||||
return "HTTP " + failureResponse.code();
|
||||
}
|
||||
if (failure != null) {
|
||||
return failure.getMessage();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
boolean isTooManyClients() {
|
||||
return failureBody != null && failureBody.toLowerCase(Locale.US).contains("too many client");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(okhttp3.WebSocket webSocket, okhttp3.Response response) {
|
||||
opened = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(okhttp3.WebSocket webSocket, String text) {
|
||||
messages.offer(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(okhttp3.WebSocket webSocket, Throwable t, okhttp3.Response response) {
|
||||
failure = t;
|
||||
failureResponse = response;
|
||||
if (response != null && response.body() != null) {
|
||||
try {
|
||||
failureBody = response.body().string();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Result {
|
||||
public final boolean ok;
|
||||
public final String error;
|
||||
|
||||
private Result(boolean ok, String error) {
|
||||
this.ok = ok;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public static Result ok() {
|
||||
return new Result(true, null);
|
||||
}
|
||||
|
||||
public static Result error(String error) {
|
||||
return new Result(false, error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -95,7 +95,8 @@ public class Slic3rConfigWrapper {
|
||||
"machine_max_acceleration_x", "machine_max_acceleration_y", "machine_max_acceleration_z", "machine_max_acceleration_e",
|
||||
"machine_max_feedrate_x", "machine_max_feedrate_y", "machine_max_feedrate_z", "machine_max_feedrate_e",
|
||||
"machine_min_extruding_rate", "machine_min_travel_rate",
|
||||
"machine_max_jerk_x", "machine_max_jerk_y", "machine_max_jerk_z", "machine_max_jerk_e"
|
||||
"machine_max_jerk_x", "machine_max_jerk_y", "machine_max_jerk_z", "machine_max_jerk_e",
|
||||
"elegoolink_timelapse", "elegoolink_bed_leveling", "elegoolink_bed_type"
|
||||
);
|
||||
public final static List<String> PHYSICAL_PRINTER_CONFIG_KEYS = Arrays.asList(
|
||||
"preset_name", // temporary option to compatibility with older Slicer
|
||||
|
||||
@@ -1173,6 +1173,29 @@ void GCodeGenerator::_do_export(Print& print, GCodeOutputStream &file, Thumbnail
|
||||
this->placeholder_parser().set("has_wipe_tower", has_wipe_tower);
|
||||
this->placeholder_parser().set("has_single_extruder_multi_material_priming", has_wipe_tower && print.config().single_extruder_multi_material_priming);
|
||||
this->placeholder_parser().set("total_toolchanges", tool_ordering.toolchanges_count());
|
||||
{
|
||||
const char *bed_type_label = (print.config().elegoolink_bed_type.value == ElegooBedType::PTE) ? "Side A" : "Side B";
|
||||
this->placeholder_parser().set("curr_bed_type", new ConfigOptionString(bed_type_label));
|
||||
}
|
||||
{
|
||||
this->placeholder_parser().set("printable_height", new ConfigOptionFloat(print.config().max_print_height.value));
|
||||
this->placeholder_parser().set("nozzle_temperature_initial_layer", new ConfigOptionInt(print.config().first_layer_temperature.get_at(initial_extruder_id)));
|
||||
this->placeholder_parser().set("bed_temperature_initial_layer_single", new ConfigOptionInt(print.config().first_layer_bed_temperature.get_at(initial_extruder_id)));
|
||||
this->placeholder_parser().set("initial_no_support_extruder", new ConfigOptionInt(int(initial_extruder_id)));
|
||||
this->placeholder_parser().set("outer_wall_acceleration", new ConfigOptionFloat(print.config().external_perimeter_acceleration.value));
|
||||
|
||||
if (const ConfigOption *opt = print.config().optptr("enable_pressure_advance"); opt != nullptr) {
|
||||
this->placeholder_parser().set("enable_pressure_advance", opt->clone());
|
||||
} else {
|
||||
this->placeholder_parser().set("enable_pressure_advance", new ConfigOptionBools(std::max<size_t>(1, print.config().nozzle_diameter.values.size()), false));
|
||||
}
|
||||
|
||||
if (const ConfigOption *opt = print.config().optptr("pressure_advance"); opt != nullptr) {
|
||||
this->placeholder_parser().set("pressure_advance", opt->clone());
|
||||
} else {
|
||||
this->placeholder_parser().set("pressure_advance", new ConfigOptionFloat(0.0));
|
||||
}
|
||||
}
|
||||
{
|
||||
BoundingBoxf bbox(print.config().bed_shape.values);
|
||||
assert(bbox.defined);
|
||||
|
||||
@@ -525,7 +525,8 @@ static std::vector<std::string> s_Preset_printer_options {
|
||||
"cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "multimaterial_purging",
|
||||
"max_print_height", "default_print_profile", "inherits",
|
||||
"remaining_times", "silent_mode",
|
||||
"machine_limits_usage", "thumbnails", "thumbnails_format"
|
||||
"machine_limits_usage", "thumbnails", "thumbnails_format",
|
||||
"elegoolink_timelapse", "elegoolink_bed_leveling", "elegoolink_bed_type"
|
||||
};
|
||||
|
||||
static std::vector<std::string> s_Preset_sla_print_options {
|
||||
@@ -1702,7 +1703,10 @@ static std::vector<std::string> s_PhysicalPrinter_opts {
|
||||
// HTTP digest authentization (RFC 2617)
|
||||
"printhost_user",
|
||||
"printhost_password",
|
||||
"printhost_ssl_ignore_revoke"
|
||||
"printhost_ssl_ignore_revoke",
|
||||
"elegoolink_timelapse",
|
||||
"elegoolink_bed_leveling",
|
||||
"elegoolink_bed_type"
|
||||
};
|
||||
|
||||
const std::vector<std::string>& PhysicalPrinter::printer_options()
|
||||
|
||||
@@ -1622,6 +1622,9 @@ std::string Print::output_filename(const std::string &filename_base) const
|
||||
DynamicConfig config = this->finished() ? this->print_statistics().config() : this->print_statistics().placeholders();
|
||||
config.set_key_value("num_extruders", new ConfigOptionInt((int)m_config.nozzle_diameter.size()));
|
||||
config.set_key_value("default_output_extension", new ConfigOptionString(".gcode"));
|
||||
config.set_key_value("nozzle_diameter", new ConfigOptionFloats(m_config.nozzle_diameter.values));
|
||||
config.set_key_value("filament_type", new ConfigOptionStrings(m_config.filament_type.values));
|
||||
config.set_key_value("layer_height", new ConfigOptionFloat(m_default_object_config.layer_height.value));
|
||||
|
||||
// Handle output_filename_format. There is a hack related to binary G-codes: gcode / bgcode substitution.
|
||||
std::string output_filename_format = m_config.output_filename_format.value;
|
||||
|
||||
@@ -56,6 +56,7 @@ void PrintBase::update_object_placeholders(DynamicConfig &config, const std::str
|
||||
const std::string input_filename_base = input_filename.substr(0, input_filename.find_last_of("."));
|
||||
// config.set_key_value("input_filename", new ConfigOptionString(input_filename_base + default_output_ext));
|
||||
config.set_key_value("input_filename_base", new ConfigOptionString(input_filename_base));
|
||||
config.set_key_value("_input_filename_base", new ConfigOptionString(input_filename_base));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +73,7 @@ std::string PrintBase::output_filename(const std::string &format, const std::str
|
||||
if (! filename_base.empty()) {
|
||||
// cfg.set_key_value("input_filename", new ConfigOptionString(filename_base + default_ext));
|
||||
cfg.set_key_value("input_filename_base", new ConfigOptionString(filename_base));
|
||||
cfg.set_key_value("_input_filename_base", new ConfigOptionString(filename_base));
|
||||
}
|
||||
try {
|
||||
boost::filesystem::path filename = format.empty() ?
|
||||
|
||||
@@ -104,6 +104,7 @@ static const t_config_enum_values s_keys_map_PrintHostType {
|
||||
{ "repetier", htRepetier },
|
||||
{ "mks", htMKS },
|
||||
{ "prusaconnectnew", htPrusaConnectNew },
|
||||
{ "elegoolink", htElegooLink },
|
||||
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintHostType)
|
||||
@@ -244,6 +245,12 @@ static const t_config_enum_values s_keys_map_GCodeThumbnailsFormat = {
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(GCodeThumbnailsFormat)
|
||||
|
||||
static const t_config_enum_values s_keys_map_ElegooBedType = {
|
||||
{ "pte", int(ElegooBedType::PTE) },
|
||||
{ "pc", int(ElegooBedType::PC) }
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ElegooBedType)
|
||||
|
||||
static const t_config_enum_values s_keys_map_ForwardCompatibilitySubstitutionRule = {
|
||||
{ "disable", ForwardCompatibilitySubstitutionRule::Disable },
|
||||
{ "enable", ForwardCompatibilitySubstitutionRule::Enable },
|
||||
@@ -2223,12 +2230,38 @@ void PrintConfigDef::init_fff_params()
|
||||
{ "flashair", "FlashAir" },
|
||||
{ "astrobox", "AstroBox" },
|
||||
{ "repetier", "Repetier" },
|
||||
{ "mks", "MKS" }
|
||||
{ "mks", "MKS" },
|
||||
{ "elegoolink", "ElegooLink" }
|
||||
});
|
||||
def->mode = comAdvanced;
|
||||
def->cli = ConfigOptionDef::nocli;
|
||||
def->set_default_value(new ConfigOptionEnum<PrintHostType>(htPrusaLink));
|
||||
|
||||
def = this->add("elegoolink_timelapse", coBool);
|
||||
def->label = L("ElegooLink timelapse");
|
||||
def->tooltip = L("Enable timelapse recording when starting a print via ElegooLink.");
|
||||
def->mode = comAdvanced;
|
||||
def->cli = ConfigOptionDef::nocli;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("elegoolink_bed_leveling", coBool);
|
||||
def->label = L("ElegooLink bed leveling");
|
||||
def->tooltip = L("Enable heated bed leveling when starting a print via ElegooLink.");
|
||||
def->mode = comAdvanced;
|
||||
def->cli = ConfigOptionDef::nocli;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("elegoolink_bed_type", coEnum);
|
||||
def->label = L("ElegooLink bed type");
|
||||
def->tooltip = L("Select bed type for ElegooLink printing.");
|
||||
def->set_enum<ElegooBedType>({
|
||||
{ "pte", L("Side A") },
|
||||
{ "pc", L("Side B") }
|
||||
});
|
||||
def->mode = comAdvanced;
|
||||
def->cli = ConfigOptionDef::nocli;
|
||||
def->set_default_value(new ConfigOptionEnum<ElegooBedType>(ElegooBedType::PTE));
|
||||
|
||||
def = this->add("only_retract_when_crossing_perimeters", coBool);
|
||||
def->label = L("Only retract when crossing perimeters");
|
||||
def->tooltip = L("Disables retraction when the travel path does not exceed the upper layer's perimeters "
|
||||
|
||||
@@ -65,7 +65,7 @@ enum class MachineLimitsUsage {
|
||||
};
|
||||
|
||||
enum PrintHostType {
|
||||
htPrusaLink, htPrusaConnect, htOctoPrint, htMoonraker, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htPrusaConnectNew
|
||||
htPrusaLink, htPrusaConnect, htOctoPrint, htMoonraker, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htPrusaConnectNew, htElegooLink
|
||||
};
|
||||
|
||||
enum AuthorizationType {
|
||||
@@ -78,6 +78,11 @@ enum class FuzzySkinType {
|
||||
All,
|
||||
};
|
||||
|
||||
enum class ElegooBedType {
|
||||
PTE = 0,
|
||||
PC = 1
|
||||
};
|
||||
|
||||
enum InfillPattern : int {
|
||||
ipRectilinear, ipMonotonic, ipMonotonicLines, ipAlignedRectilinear, ipGrid, ipTriangles, ipStars, ipCubic, ipLine, ipConcentric, ipHoneycomb, ip3DHoneycomb,
|
||||
ipGyroid, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral, ipAdaptiveCubic, ipSupportCubic, ipSupportBase,
|
||||
@@ -212,6 +217,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(MachineLimitsUsage)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrintHostType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AuthorizationType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ElegooBedType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType)
|
||||
@@ -883,6 +889,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
||||
((ConfigOptionFloatOrPercent, first_layer_height))
|
||||
((ConfigOptionFloatOrPercent, first_layer_speed))
|
||||
((ConfigOptionInts, first_layer_temperature))
|
||||
((ConfigOptionEnum<ElegooBedType>, elegoolink_bed_type))
|
||||
((ConfigOptionIntsNullable, idle_temperature))
|
||||
((ConfigOptionInts, full_fan_speed_layer))
|
||||
((ConfigOptionFloat, infill_acceleration))
|
||||
|
||||
Reference in New Issue
Block a user