Skip to content

Commit ed09395

Browse files
committed
test prusa link
1 parent 35b301a commit ed09395

8 files changed

Lines changed: 1075 additions & 0 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Build PrusaLink Firmware
2+
3+
on:
4+
push:
5+
paths:
6+
- 'firmware/prusalink/**'
7+
branches:
8+
- main
9+
tags:
10+
- 'v*'
11+
workflow_dispatch:
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
environment: [prusa_drucker_4, prusa_drucker_5]
20+
21+
steps:
22+
- name: Checkout Code
23+
uses: actions/checkout@v4
24+
25+
- name: Set up Python
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: '3.11'
29+
30+
- name: Install PlatformIO
31+
run: pip install -U platformio
32+
33+
- name: Create dummy secret.h if missing
34+
run: |
35+
mkdir -p firmware/prusalink/include
36+
touch firmware/prusalink/include/secret.h
37+
38+
- name: Run PlatformIO Build
39+
working-directory: firmware/prusalink
40+
run: pio run -e ${{ matrix.environment }}
41+
env:
42+
WIFI_SSID: ${{ secrets.WIFI_SSID }}
43+
WIFI_PASS: ${{ secrets.WIFI_PASS }}
44+
PRUSA_API_KEY_D4: ${{ secrets.PRUSA_API_KEY_D4 }}
45+
PRUSA_API_KEY_D5: ${{ secrets.PRUSA_API_KEY_D5 }}
46+
47+
- name: Upload Artifacts
48+
uses: actions/upload-artifact@v4
49+
with:
50+
name: firmware-${{ matrix.environment }}
51+
path: firmware/prusalink/.pio/build/${{ matrix.environment }}/firmware.bin
52+
53+
release:
54+
needs: build
55+
if: startsWith(github.ref, 'refs/tags/v')
56+
runs-on: ubuntu-latest
57+
permissions:
58+
contents: write
59+
steps:
60+
- name: Download all artifacts
61+
uses: actions/download-artifact@v4
62+
with:
63+
path: ./artifacts
64+
65+
- name: Prepare Release Assets
66+
run: |
67+
mv ./artifacts/firmware-prusa_drucker_4/firmware.bin ./prusa_drucker_4.bin
68+
mv ./artifacts/firmware-prusa_drucker_5/firmware.bin ./prusa_drucker_5.bin
69+
70+
- name: Create GitHub Release
71+
uses: softprops/action-gh-release@v2
72+
with:
73+
files: |
74+
./prusa_drucker_4.bin
75+
./prusa_drucker_5.bin
76+
body: "Automatischer Build der PrusaLink-Varianten für Version ${{ github.ref_name }}"
77+
env:
78+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
.DS_Store
33
Software/secrets/*
44
firmware/octoprint/include/secret.h
5+
firmware/prusalink/include/secret.h

firmware/prusalink/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.pio
2+
.vscode/.browse.c_cpp.db*
3+
.vscode/c_cpp_properties.json
4+
.vscode/launch.json
5+
.vscode/ipch
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
// See http://go.microsoft.com/fwlink/?LinkId=827846
3+
// for the documentation about the extensions.json format
4+
"recommendations": [
5+
"platformio.platformio-ide"
6+
],
7+
"unwantedRecommendations": [
8+
"ms-vscode.cpptools-extension-pack"
9+
]
10+
}
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/* ___ _ ____ _ _ _ ____ ___
2+
/ _ \ ___| |_ ___ | _ \ _ __(_)_ __ | |_ / \ | _ \_ _|
3+
| | | |/ __| __/ _ \| |_) | '__| | '_ \| __| / _ \ | |_) | |
4+
| |_| | (__| || (_) | __/| | | | | | | |_ / ___ \| __/| |
5+
\___/ \___|\__\___/|_| |_| |_|_| |_|\__/_/ \_\_| |___|
6+
.......By Stephen Ludgate https://www.chunkymedia.co.uk.......
7+
.......Redesigned for Prusa Link by Marius Tetard
8+
.......Updated for API Key Auth....... 08/2025....
9+
10+
*/
11+
12+
#include "PrusaLinkAPI.h"
13+
#include "Arduino.h"
14+
15+
// The Base64 helper function is no longer needed and has been removed.
16+
17+
PrusaLinkApi::PrusaLinkApi(void){
18+
if (_debug)
19+
Serial.println("Be sure to Call init to setup and start the PrusaLinkApi instance");
20+
}
21+
22+
PrusaLinkApi::PrusaLinkApi(Client &client, IPAddress prusaLinkIp, int prusaLinkPort, const char* apiKey) {
23+
init(client, prusaLinkIp, prusaLinkPort, apiKey);
24+
}
25+
26+
void PrusaLinkApi::init(Client &client, IPAddress prusaLinkIp, int prusaLinkPort, const char* apiKey) {
27+
_client = &client;
28+
_prusaLinkIp = prusaLinkIp;
29+
_prusaLinkPort = prusaLinkPort;
30+
_usingIpAddress = true;
31+
_apiKey = apiKey; // Store the API Key
32+
}
33+
34+
PrusaLinkApi::PrusaLinkApi(Client &client, char *prusaLinkUrl, int prusaLinkPort, const char* apiKey) {
35+
init(client, prusaLinkUrl, prusaLinkPort, apiKey);
36+
}
37+
38+
void PrusaLinkApi::init(Client &client, char *prusaLinkUrl, int prusaLinkPort, const char* apiKey) {
39+
_client = &client;
40+
_prusaLinkUrl = prusaLinkUrl;
41+
_prusaLinkPort = prusaLinkPort;
42+
_usingIpAddress = false;
43+
_apiKey = apiKey; // Store the API Key
44+
}
45+
46+
String PrusaLinkApi::sendRequestToPrusaLink(String type, String command, const char *data) {
47+
if (_debug)
48+
Serial.println("PrusaLinkApi::sendRequestToPrusaLink() CALLED");
49+
50+
if ((type != "GET") && (type != "POST") && (type != "DELETE")) {
51+
if (_debug)
52+
Serial.println("PrusaLinkApi::sendRequestToPrusaLink() Only GET, POST & DELETE are supported... exiting.");
53+
return "";
54+
}
55+
56+
String statusCode = "";
57+
String headers = "";
58+
String body = "";
59+
bool finishedStatusCode = false;
60+
bool finishedHeaders = false;
61+
bool currentLineIsBlank = true;
62+
int ch_count = 0;
63+
unsigned long now;
64+
65+
bool connected;
66+
67+
if (_usingIpAddress)
68+
connected = _client->connect(_prusaLinkIp, _prusaLinkPort);
69+
else
70+
connected = _client->connect(_prusaLinkUrl, _prusaLinkPort);
71+
72+
if (connected) {
73+
if (_debug)
74+
Serial.println(".... connected to server");
75+
76+
_client->println(type + " " + command + " HTTP/1.1");
77+
_client->print("Host: ");
78+
if (_usingIpAddress)
79+
_client->println(_prusaLinkIp);
80+
else
81+
_client->println(_prusaLinkUrl);
82+
83+
// Send the API Key in the X-Api-Key header
84+
_client->print("X-Api-Key: ");
85+
_client->println(_apiKey);
86+
87+
_client->println("User-Agent: " + String(USER_AGENT));
88+
_client->println("Connection: close");
89+
if (data != NULL) {
90+
_client->println("Content-Type: application/json");
91+
_client->print("Content-Length: ");
92+
_client->println(strlen(data));
93+
_client->println();
94+
_client->println(data);
95+
} else {
96+
_client->println();
97+
}
98+
99+
now = millis();
100+
while (millis() - now < PLAPI_TIMEOUT) {
101+
while (_client->available()) {
102+
char c = _client->read();
103+
104+
if (_debug)
105+
Serial.print(c);
106+
107+
if (!finishedStatusCode) {
108+
if (c == '\n')
109+
finishedStatusCode = true;
110+
else
111+
statusCode = statusCode + c;
112+
}
113+
114+
if (!finishedHeaders) {
115+
if (c == '\n') {
116+
if (currentLineIsBlank)
117+
finishedHeaders = true;
118+
}
119+
headers = headers + c;
120+
} else {
121+
body = body + c;
122+
}
123+
if (c == '\n')
124+
currentLineIsBlank = true;
125+
else if (c != '\r') {
126+
currentLineIsBlank = false;
127+
}
128+
}
129+
}
130+
} else {
131+
if (_debug) {
132+
Serial.println("connection failed");
133+
}
134+
}
135+
136+
closeClient();
137+
138+
httpStatusCode = extractHttpCode(statusCode, body);
139+
if (_debug) {
140+
Serial.print("\nhttpCode:");
141+
Serial.println(httpStatusCode);
142+
}
143+
if(httpStatusCode < 200 || httpStatusCode >= 300){httpErrorBody = body;}
144+
145+
return body;
146+
}
147+
148+
String PrusaLinkApi::sendGetToPrusaLink(String endpoint) {
149+
if (_debug)
150+
Serial.println("PrusaLinkApi::sendGetToPrusaLink() CALLED");
151+
152+
return sendRequestToPrusaLink("GET", endpoint, NULL);
153+
}
154+
155+
String PrusaLinkApi::sendPostToPrusaLink(String endpoint, const char *postData) {
156+
if (_debug)
157+
Serial.println("PrusaLinkApi::sendPostToPrusaLink() CALLED");
158+
return sendRequestToPrusaLink("POST", endpoint, postData);
159+
}
160+
161+
String PrusaLinkApi::sendDeleteToPrusaLink(String endpoint) {
162+
if (_debug)
163+
Serial.println("PrusaLinkApi::sendDeleteToPrusaLink() CALLED");
164+
return sendRequestToPrusaLink("DELETE", endpoint, NULL);
165+
}
166+
167+
168+
bool PrusaLinkApi::getPrinterStatus() {
169+
String response = sendGetToPrusaLink("/api/v1/status");
170+
171+
StaticJsonDocument<JSONDOCUMENT_SIZE> root;
172+
DeserializationError error = deserializeJson(root, response);
173+
174+
if (error) {
175+
if(_debug) Serial.println("Failed to parse printer status");
176+
return false;
177+
}
178+
179+
const char* stateStr = root["printer"]["state"] | "UNKNOWN";
180+
strncpy(printerStats.printerState, stateStr, sizeof(printerStats.printerState) - 1);
181+
printerStats.printerState[sizeof(printerStats.printerState) - 1] = '\0';
182+
183+
printerStats.printerStatePrinting = (strcmp(printerStats.printerState, "PRINTING") == 0);
184+
printerStats.printerStatePaused = (strcmp(printerStats.printerState, "PAUSED") == 0);
185+
printerStats.printerStateError = (strcmp(printerStats.printerState, "ERROR") == 0) || (strcmp(printerStats.printerState, "ATTENTION") == 0);
186+
printerStats.printerStateFinished = (strcmp(printerStats.printerState, "FINISHED") == 0);
187+
printerStats.printerStateReady = (strcmp(printerStats.printerState, "IDLE") == 0);
188+
printerStats.printerStateBusy = (strcmp(printerStats.printerState, "BUSY") == 0);
189+
190+
printerStats.printerBedTempActual = root["printer"]["temp_bed"];
191+
printerStats.printerBedTempTarget = root["printer"]["target_bed"];
192+
193+
printerStats.printerTool0TempActual = root["printer"]["temp_nozzle"];
194+
printerStats.printerTool0TempTarget = root["printer"]["target_nozzle"];
195+
196+
return true;
197+
}
198+
199+
bool PrusaLinkApi::getJobInfo() {
200+
String response = sendGetToPrusaLink("/api/v1/job");
201+
202+
StaticJsonDocument<JSONDOCUMENT_SIZE> root;
203+
DeserializationError error = deserializeJson(root, response);
204+
205+
if (error || !root.containsKey("progress")) {
206+
if(_debug && error) Serial.println("Failed to parse job info");
207+
if(_debug && !root.containsKey("progress")) Serial.println("No active job");
208+
return false;
209+
}
210+
211+
const char* filenameStr = root["file"]["display_name"] | "No file";
212+
strncpy(jobInfo.jobFileName, filenameStr, sizeof(jobInfo.jobFileName) - 1);
213+
jobInfo.jobFileName[sizeof(jobInfo.jobFileName) - 1] = '\0';
214+
215+
jobInfo.progressCompletion = root["progress"];
216+
jobInfo.progressPrintTime = root["time_printing"];
217+
jobInfo.progressPrintTimeLeft = root["time_remaining"];
218+
219+
return true;
220+
}
221+
222+
bool PrusaLinkApi::printerCommand(const char* gcodeCommand) {
223+
char postData[POSTDATA_GCODE_SIZE];
224+
snprintf(postData, POSTDATA_GCODE_SIZE, "{\"command\": \"%s\"}", gcodeCommand);
225+
sendPostToPrusaLink("/api/v1/printer/command", postData);
226+
return (httpStatusCode == 204);
227+
}
228+
229+
void PrusaLinkApi::closeClient() { _client->stop(); }
230+
231+
int PrusaLinkApi::extractHttpCode(String statusCode, String body) {
232+
if (_debug) {
233+
Serial.print("\nStatus code to extract: ");
234+
Serial.println(statusCode);
235+
}
236+
int firstSpace = statusCode.indexOf(" ");
237+
int lastSpace = statusCode.lastIndexOf(" ");
238+
if (firstSpace > -1 && lastSpace > -1 && firstSpace != lastSpace) {
239+
String statusCodeExtract = statusCode.substring(firstSpace + 1, lastSpace);
240+
int statusCodeInt = statusCodeExtract.toInt();
241+
if (_debug and statusCodeInt != 200 and statusCodeInt != 201 and statusCodeInt != 202 and statusCodeInt != 204) {
242+
Serial.print("\nSERVER RESPONSE CODE: " + statusCode);
243+
if (body != "")
244+
Serial.println(" - " + body);
245+
else
246+
Serial.println();
247+
}
248+
return statusCodeInt;
249+
} else
250+
return -1;
251+
}

0 commit comments

Comments
 (0)