From f352a02b9f2ae273d5df0b588bf305d68358ac12 Mon Sep 17 00:00:00 2001 From: Dark98 Date: Thu, 22 Jan 2026 07:42:14 +0000 Subject: [PATCH] Initial ElegooLink Support (Only Centauri Carbon Tested) --- app/build.gradle | 1 + .../components/bed_menu/SliceMenu.java | 44 +- .../slicebeam/config/ConfigObject.java | 4 + .../fragment/PrinterConfigFragment.java | 31 ++ .../fragment/ProfileListFragment.java | 21 +- .../print_host/ElegooLinkClient.java | 441 ++++++++++++++++++ .../slicebeam/slic3r/Slic3rConfigWrapper.java | 3 +- app/src/main/jni/libslic3r/GCode.cpp | 23 + app/src/main/jni/libslic3r/Preset.cpp | 8 +- app/src/main/jni/libslic3r/Print.cpp | 3 + app/src/main/jni/libslic3r/PrintBase.cpp | 2 + app/src/main/jni/libslic3r/PrintConfig.cpp | 35 +- app/src/main/jni/libslic3r/PrintConfig.hpp | 9 +- 13 files changed, 613 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/ru/ytkab0bp/slicebeam/print_host/ElegooLinkClient.java diff --git a/app/build.gradle b/app/build.gradle index b14a7f9..586698b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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() { diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/SliceMenu.java b/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/SliceMenu.java index 27a8fd7..0143ce5 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/SliceMenu.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/components/bed_menu/SliceMenu.java @@ -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 SUPPORTED_SEND = Collections.singletonList("octoprint"); + private final static List 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; + } } } diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/config/ConfigObject.java b/app/src/main/java/ru/ytkab0bp/slicebeam/config/ConfigObject.java index d121ba9..e761902 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/config/ConfigObject.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/config/ConfigObject.java @@ -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"); diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/PrinterConfigFragment.java b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/PrinterConfigFragment.java index fcdb43a..32804f8 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/PrinterConfigFragment.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/PrinterConfigFragment.java @@ -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(); + } } diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/ProfileListFragment.java b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/ProfileListFragment.java index d7a2620..1792e80 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/ProfileListFragment.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/fragment/ProfileListFragment.java @@ -519,6 +519,7 @@ public abstract class ProfileListFragment extends Fragment { @SuppressLint("NotifyDataSetChanged") protected void setConfigItems(List items) { + categoryElements.clear(); List 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 expanded = new ArrayList<>(); + int categoryIndex = 0; + for (OptionWrapper w : list) { + expanded.add(w); + if (w.categoryIndex == categoryIndex && unfolded.get(categoryIndex)) { + List 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; diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/print_host/ElegooLinkClient.java b/app/src/main/java/ru/ytkab0bp/slicebeam/print_host/ElegooLinkClient.java new file mode 100644 index 0000000..ebb995f --- /dev/null +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/print_host/ElegooLinkClient.java @@ -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 buildWebSocketCandidates(String host) { + String h = sanitizeWebSocketHost(host); + List 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 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); + } + } + +} diff --git a/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/Slic3rConfigWrapper.java b/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/Slic3rConfigWrapper.java index 4369a6c..4d5f2d7 100644 --- a/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/Slic3rConfigWrapper.java +++ b/app/src/main/java/ru/ytkab0bp/slicebeam/slic3r/Slic3rConfigWrapper.java @@ -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 PHYSICAL_PRINTER_CONFIG_KEYS = Arrays.asList( "preset_name", // temporary option to compatibility with older Slicer diff --git a/app/src/main/jni/libslic3r/GCode.cpp b/app/src/main/jni/libslic3r/GCode.cpp index 734a402..2e92c4f 100644 --- a/app/src/main/jni/libslic3r/GCode.cpp +++ b/app/src/main/jni/libslic3r/GCode.cpp @@ -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(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); diff --git a/app/src/main/jni/libslic3r/Preset.cpp b/app/src/main/jni/libslic3r/Preset.cpp index 1df3b34..6e8e0d4 100644 --- a/app/src/main/jni/libslic3r/Preset.cpp +++ b/app/src/main/jni/libslic3r/Preset.cpp @@ -525,7 +525,8 @@ static std::vector 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 s_Preset_sla_print_options { @@ -1702,7 +1703,10 @@ static std::vector 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& PhysicalPrinter::printer_options() diff --git a/app/src/main/jni/libslic3r/Print.cpp b/app/src/main/jni/libslic3r/Print.cpp index edfb171..9bd0ca9 100644 --- a/app/src/main/jni/libslic3r/Print.cpp +++ b/app/src/main/jni/libslic3r/Print.cpp @@ -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; diff --git a/app/src/main/jni/libslic3r/PrintBase.cpp b/app/src/main/jni/libslic3r/PrintBase.cpp index ca13365..0db9512 100644 --- a/app/src/main/jni/libslic3r/PrintBase.cpp +++ b/app/src/main/jni/libslic3r/PrintBase.cpp @@ -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() ? diff --git a/app/src/main/jni/libslic3r/PrintConfig.cpp b/app/src/main/jni/libslic3r/PrintConfig.cpp index 1f27028..ab1f64a 100644 --- a/app/src/main/jni/libslic3r/PrintConfig.cpp +++ b/app/src/main/jni/libslic3r/PrintConfig.cpp @@ -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(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({ + { "pte", L("Side A") }, + { "pc", L("Side B") } + }); + def->mode = comAdvanced; + def->cli = ConfigOptionDef::nocli; + def->set_default_value(new ConfigOptionEnum(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 " diff --git a/app/src/main/jni/libslic3r/PrintConfig.hpp b/app/src/main/jni/libslic3r/PrintConfig.hpp index ebd6371..c6cb9cc 100644 --- a/app/src/main/jni/libslic3r/PrintConfig.hpp +++ b/app/src/main/jni/libslic3r/PrintConfig.hpp @@ -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, elegoolink_bed_type)) ((ConfigOptionIntsNullable, idle_temperature)) ((ConfigOptionInts, full_fan_speed_layer)) ((ConfigOptionFloat, infill_acceleration))