Open morgan-wild opened 2 months ago
Hey @morgan-wild! It has been a minute since I have touched this codebase, so I'm sorry for any inaccuracies.
There's actually a .data
property that you can access on the instantiated class which always has the most up to date data. If you want to continuously update things, as you mentioned, you can use the client#on("printer:dataUpdate")
event, and then every time it's called you can access client#data
for mc_percent
.
A much prettier solution however, is to use the client#on("job: ...
events. These events give you a Job
object, which contains the percentDone
property, which is the alias of mc_percent
.
I do not know how your LED light works, but a potential way to do this would be to:
job:start
, job:pause
, job:unpause
, job:finish
, job:update
events.So in summary, there's no need for you to call PushAllData
manually. The whole purpose of the client is to abstract the MQTT layer and provide an almost SDK level access to your printer.
In the meanwhile you're implementing this, I'll look into the edge case. However I do think that refactoring your code as I described above will fix your issue.
Also in regard to the second smaller issue, can't you just retry by calling client#connect
on the same instance?
If that's not the case then as of right now, all I can recommend is to try in a setInterval
every 15 seconds, and set a let
outside of the interval to the client once a connection has been successfully made.
The client is also automatically set up to retry connecting to your printer every second, but if what you described is true, then that only works if the client was able to connect in the first place.
I have figured out the cause of the edge case error.
The library treats both the PREPARE
and SLICING
statuses as IDLE
, as they do not indicate printing technically.
https://github.com/THE-SIMPLE-MARK/bambu-node/blob/30602798d20a3cb427aa99e71b47f780b8603de1/src/BambuClient.ts#L301-L303
The problem is that in the if else if statements below, which handle the status changes, I failed to account for FINISH
changing to IDLE
, even though this is normal behaviour because PREPARE
and SLICING
is before a job start.
I will push a fix to just quitely ignore FINISH
| FAILED
=> IDLE
changes as a minor update within the hour.
I have pushed the fix under semver 3.22.21
. As this was the primary issue, I will close this issue. Don't hesitate to reopen it or open another issue if you have found another bug.
As for the "connection retry on first connection", it is technically a feature request, so feel free to open another issue and a PR. If everything looks good in the implementation, I don't see any issues with implementing it.
Hi!
Thank you so much for your fast response.
add event listeners for the job:start, job:pause, job:unpause, job:finish, job:update events. on job start, you can reset the LED or read job.percentDone (would have the same effect since percentDone is 0 on job start) on job update, you can read job.percentDone on job pause, you can turn the entire strip orange (just an idea) on job unpause, you can read job.percentDone on job finish, you can make it blink green if outcome (second argument in callback) is "SUCCESS", red if it is "FAILED" or "UNEXPECTED" (just an idea)
Yes! That is exactly that I planned to do!
But I tried something like before to use the PushAllData command :
_client = new BambuClient(_myCredentials);
_client.on('job:start', job => {
// Never fired
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:start` + job);
});
_client.on('job:pause', job => {
// Never fired
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:pause` + job);
});
_client.on('job:unpause', job => {
// Never fired
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:unpause` + job);
});
_client.on('job:update', job => {
// Never fired
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:update` + job);
});
_client.on('job:finish', job => {
// Never fired
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:finish` + job);
});
_client.on('message', (topic, key, data) => {
// Is fired
});
_client.on('printer:statusUpdate', (oldStatus, newStatus) => {
// Is fired
});
I updated to 3.22.21 and it is still true.
Still using the 3.22.21 I tried again to reuse a client instance after a connection failure:
initializeClient(); // Create a new instance of client with listeners
connect();
async function connect() {
console.log('Connecting...');
if (_client.connected) {
return;
}
try {
await _client.connect();
console.log('Connected');
}
catch(exception) {
console.log('Failed to connect');
setTimeout(connect, 10000);
}
}
$ node . <- printer is off
Connecting...
Failed to connect <- long
Connecting... <- printer turned on
Failed to connect <- instant
Connecting...
Failed to connect <- instant
Connecting...
Failed to connect <- instant
Connecting...
Failed to connect <- instant
Connecting...
Failed to connect <- instant
...
<program restarted, printer online>
Connecting...
Connected <- OK
But:
connect();
async function connect() {
if (_client?.connected) {
return;
}
console.log('Connecting...');
try {
initializeClient(); // Create a new instance of client with listeners
await _client.connect();
console.log('Connected');
}
catch(exception) {
console.log('Failed to connect');
setTimeout(connect, 10000);
}
}
$ node . <- printer is off
Connecting...
Failed to connect <- long
Connecting... <- printer turned on
Connected
I will try to understand :)
Hi!
Thank you so much for your fast response.
add event listeners for the job:start, job:pause, job:unpause, job:finish, job:update events. on job start, you can reset the LED or read job.percentDone (would have the same effect since percentDone is 0 on job start) on job update, you can read job.percentDone on job pause, you can turn the entire strip orange (just an idea) on job unpause, you can read job.percentDone on job finish, you can make it blink green if outcome (second argument in callback) is "SUCCESS", red if it is "FAILED" or "UNEXPECTED" (just an idea)
Yes! That is exactly that I planned to do!
But I tried something like before to use the PushAllData command :
_client = new BambuClient(_myCredentials); _client.on('job:start', job => { // Never fired console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:start` + job); }); _client.on('job:pause', job => { // Never fired console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:pause` + job); }); _client.on('job:unpause', job => { // Never fired console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:unpause` + job); }); _client.on('job:update', job => { // Never fired console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:update` + job); }); _client.on('job:finish', job => { // Never fired console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:finish` + job); }); _client.on('message', (topic, key, data) => { // Is fired }); _client.on('printer:statusUpdate', (oldStatus, newStatus) => { // Is fired });
I updated to 3.22.21 and it is still true.
Still using the 3.22.21 I tried again to reuse a client instance after a connection failure:
initializeClient(); // Create a new instance of client with listeners connect(); async function connect() { console.log('Connecting...'); if (_client.connected) { return; } try { await _client.connect(); console.log('Connected'); } catch(exception) { console.log('Failed to connect'); setTimeout(connect, 10000); } }
$ node . <- printer is off Connecting... Failed to connect <- long Connecting... <- printer turned on Failed to connect <- instant Connecting... Failed to connect <- instant Connecting... Failed to connect <- instant Connecting... Failed to connect <- instant Connecting... Failed to connect <- instant ... <program restarted, printer online> Connecting... Connected <- OK
But:
connect(); async function connect() { if (_client?.connected) { return; } console.log('Connecting...'); try { initializeClient(); // Create a new instance of client with listeners await _client.connect(); console.log('Connected'); } catch(exception) { console.log('Failed to connect'); setTimeout(connect, 10000); } }
$ node . <- printer is off Connecting... Failed to connect <- long Connecting... <- printer turned on Connected
I will try to understand :)
Oh, that's interesting. I haven't done anything about the connection retry, but those events should fire. I seriously don't know why they don't.
The code you have for retrying the connection is what I had in mind. I do not think I'll implement something like that right into the library, so that's fine in my opinion.
Which printer model do you have?
Also, I should note that the events you said never fired are only actually fired when there's a print-related event. A job is created when the printer starts it's work, finish is called when it finishes, and so on.
It would be also really helpful if you could share what message
and printer:statusUpdate
logs. That might have the secret as to why those events never fire.
Also, I should note that the events you said never fired are only actually fired when there's a print-related event. A job is created when the printer starts it's work, finish is called when it finishes, and so on.
Yes for sure!
My printer is a P1S with the latest firmware.
It is currently printing, so:
// Example of message without a push all command:
New print message! {
nozzle_temper: 220.125,
bed_temper: 54.96875,
wifi_signal: '-27dBm',
mc_print_line_number: '69627',
command: 'push_status',
msg: 1,
sequence_id: '1151'
}
// Example of message after a pushAll command:
New print message! {
upgrade_state: {
sequence_id: 0,
progress: '',
status: '',
consistency_request: false,
dis_state: 0,
err_code: 0,
force_upgrade: false,
message: '0%, 0B/s',
module: '',
new_version_state: 0,
cur_state_code: 1,
new_ver_list: []
},
ipcam: {
ipcam_dev: '1',
ipcam_record: 'enable',
timelapse: 'disable',
resolution: '',
tutk_server: 'disable',
mode_bits: 3
},
upload: { status: 'idle', progress: 0, message: '' },
nozzle_temper: 219.96875,
nozzle_target_temper: 220,
bed_temper: 55,
bed_target_temper: 55,
chamber_temper: 5,
mc_print_stage: '2',
heatbreak_fan_speed: '15',
cooling_fan_speed: '15',
big_fan1_speed: '11',
big_fan2_speed: '8',
mc_percent: 72,
mc_remaining_time: 15,
ams_status: 768,
ams_rfid_status: 2,
hw_switch_state: 1,
spd_mag: 100,
spd_lvl: 2,
print_error: 0,
lifecycle: 'product',
wifi_signal: '-30dBm',
gcode_state: 'RUNNING',
gcode_file_prepare_percent: '0',
queue_number: 0,
queue_total: 0,
queue_est: 0,
queue_sts: 0,
project_id: '0',
profile_id: '0',
task_id: '0',
subtask_id: '0',
subtask_name: 'hook-30',
gcode_file: 'hook-30.gcode.3mf',
stg: [ 2, 14, 1 ],
stg_cur: 0,
print_type: 'local',
home_flag: 23086383,
mc_print_line_number: '66213',
mc_print_sub_stage: 0,
sdcard: true,
force_upgrade: false,
mess_production_state: 'active',
layer_num: 54,
total_layer_num: 109,
s_obj: [],
filam_bak: [],
fan_gear: 9876223,
nozzle_diameter: '0.4',
nozzle_type: 'stainless_steel',
cali_version: 0,
hms: [],
online: { ahb: false, rfid: false, version: 1900360653 },
ams: {
ams: [ [Object] ],
ams_exist_bits: '1',
tray_exist_bits: '7',
tray_is_bbl_bits: '7',
tray_tar: '0',
tray_now: '0',
tray_pre: '0',
tray_read_done_bits: '7',
tray_reading_bits: '0',
version: 3,
insert_flag: true,
power_on_flag: false
},
vt_tray: {
id: '254',
tag_uid: '0000000000000000',
tray_id_name: '',
tray_info_idx: 'GFU01',
tray_type: 'TPU',
tray_sub_brands: '',
tray_color: 'E05028FF',
tray_weight: '0',
tray_diameter: '0.00',
tray_temp: '0',
tray_time: '0',
bed_temp_type: '0',
bed_temp: '0',
nozzle_temp_max: '250',
nozzle_temp_min: '200',
xcam_info: '000000000000000000000000',
tray_uuid: '00000000000000000000000000000000',
remain: 0,
k: 0.019999999552965164,
n: 1,
cali_idx: -1
},
lights_report: [ { node: 'chamber_light', mode: 'on' } ],
command: 'push_status',
msg: 0,
sequence_id: '1115'
}
I can't give you a printer:statusUpdate because it is currently running.
Also, I should note that the events you said never fired are only actually fired when there's a print-related event. A job is created when the printer starts it's work, finish is called when it finishes, and so on.
Yes for sure!
My printer is a P1S with the latest firmware.
It is currently printing, so:
// Example of message without a push all command: New print message! { nozzle_temper: 220.125, bed_temper: 54.96875, wifi_signal: '-27dBm', mc_print_line_number: '69627', command: 'push_status', msg: 1, sequence_id: '1151' } // Example of message after a pushAll command: New print message! { upgrade_state: { sequence_id: 0, progress: '', status: '', consistency_request: false, dis_state: 0, err_code: 0, force_upgrade: false, message: '0%, 0B/s', module: '', new_version_state: 0, cur_state_code: 1, new_ver_list: [] }, ipcam: { ipcam_dev: '1', ipcam_record: 'enable', timelapse: 'disable', resolution: '', tutk_server: 'disable', mode_bits: 3 }, upload: { status: 'idle', progress: 0, message: '' }, nozzle_temper: 219.96875, nozzle_target_temper: 220, bed_temper: 55, bed_target_temper: 55, chamber_temper: 5, mc_print_stage: '2', heatbreak_fan_speed: '15', cooling_fan_speed: '15', big_fan1_speed: '11', big_fan2_speed: '8', mc_percent: 72, mc_remaining_time: 15, ams_status: 768, ams_rfid_status: 2, hw_switch_state: 1, spd_mag: 100, spd_lvl: 2, print_error: 0, lifecycle: 'product', wifi_signal: '-30dBm', gcode_state: 'RUNNING', gcode_file_prepare_percent: '0', queue_number: 0, queue_total: 0, queue_est: 0, queue_sts: 0, project_id: '0', profile_id: '0', task_id: '0', subtask_id: '0', subtask_name: 'hook-30', gcode_file: 'hook-30.gcode.3mf', stg: [ 2, 14, 1 ], stg_cur: 0, print_type: 'local', home_flag: 23086383, mc_print_line_number: '66213', mc_print_sub_stage: 0, sdcard: true, force_upgrade: false, mess_production_state: 'active', layer_num: 54, total_layer_num: 109, s_obj: [], filam_bak: [], fan_gear: 9876223, nozzle_diameter: '0.4', nozzle_type: 'stainless_steel', cali_version: 0, hms: [], online: { ahb: false, rfid: false, version: 1900360653 }, ams: { ams: [ [Object] ], ams_exist_bits: '1', tray_exist_bits: '7', tray_is_bbl_bits: '7', tray_tar: '0', tray_now: '0', tray_pre: '0', tray_read_done_bits: '7', tray_reading_bits: '0', version: 3, insert_flag: true, power_on_flag: false }, vt_tray: { id: '254', tag_uid: '0000000000000000', tray_id_name: '', tray_info_idx: 'GFU01', tray_type: 'TPU', tray_sub_brands: '', tray_color: 'E05028FF', tray_weight: '0', tray_diameter: '0.00', tray_temp: '0', tray_time: '0', bed_temp_type: '0', bed_temp: '0', nozzle_temp_max: '250', nozzle_temp_min: '200', xcam_info: '000000000000000000000000', tray_uuid: '00000000000000000000000000000000', remain: 0, k: 0.019999999552965164, n: 1, cali_idx: -1 }, lights_report: [ { node: 'chamber_light', mode: 'on' } ], command: 'push_status', msg: 0, sequence_id: '1115' }
I can't give you a printer:statusUpdate because it is currently running.
Thanks! What I can conclude from this is that the code is fine. Please make sure that the client is running when the printer starts working, as the job events are not fired if the application didn't see a IDLE | FINISH | FAILED => RUNNING
change before.
You can verify if this is the case by checking if client#currentJob
is set to null
.
Yes!
So I started a new small print with less traces but checking the client#currentJob value:
$ node .
Connecting...
New info message!
New print message!
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 100% <- Was the previous print
The printer's status has changed from OFFLINE to FINISH!
Connected
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 100% SENT!
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 100% SENT!
cilent.currentJob: null
New print message!
cilent.currentJob: null
New print message!
New info message!
cilent.currentJob: null
New print message!
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 100%
The printer's status has changed from FINISH to PREPARE!
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 0% SENT!
cilent.currentJob: null
New print message!
The printer's status has changed from PREPARE to RUNNING!
New print message!
cilent.currentJob: null
Sending pushAll command
New print message!
cilent.currentJob: null
New print message!
Sending pushAll command
New print message!
cilent.currentJob: null
New print message!
Sending pushAll command
New print message!
New print message!
cilent.currentJob: null
New print message!
Sending pushAll command
New print message!
New print message!
cilent.currentJob: null
New print message!
Sending pushAll command
New print message!
New print message!
cilent.currentJob: null
New print message!
Sending pushAll command
New print message!
New print message!
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11%
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11% SENT!
cilent.currentJob: null
New print message!
Sending pushAll command
New print message!
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11%
New print message!
cilent.currentJob: null
New print message!
Sending pushAll command
New print message!
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11%
New print message!
cilent.currentJob: null
Sending pushAll command
New print message!
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11%
New print message!
cilent.currentJob: null
Sending pushAll command
New print message!
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11%
cilent.currentJob: null
New print message!
Sending pushAll command
New print message!
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11%
cilent.currentJob: null
Sending pushAll command
New print message!
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11%
New print message!
cilent.currentJob: null
New print message!
And it is still null, that the reason why we haven't events about it.
Maybe it is time to give you my entire poc ;)
import { BambuClient, PushAllCommand } from 'bambu-node'; // https://www.npmjs.com/package/bambu-node
let _client = null;
let _pushAllTimer = null;
let _percent = 0;
function initializeClient() {
disconnect();
_client = new BambuClient(_credentials);
_client.on('job:start', job => {
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:start` + job);
});
_client.on('job:pause', job => {
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:pause` + job);
});
_client.on('job:unpause', job => {
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:unpause` + job);
});
_client.on('job:update', job => {
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:update` + job);
});
_client.on('job:finish', job => {
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX job:finish` + job);
});
// more about the available events below
_client.on('message', (topic, key, data) => {
console.log(`New ${key} message!` /*, data*/);
if (!data.mc_percent) {
return;
}
const percent = parseInt(data.mc_percent);
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ${percent}%`);
if (_percent === percent) {
return;
}
sendProgress(percent);
_percent = percent;
})
_client.on('printer:statusUpdate', (oldStatus, newStatus) => {
console.log(`The printer's status has changed from ${oldStatus} to ${newStatus}!`);
switch(newStatus) {
case 'FINISH':
sendProgress(100, '00FF00');
break;
case 'PREPARE':
sendProgress(0);
break;
}
newStatus === 'RUNNING' ? startPushAllCommandTimer() : stopPushAllCommandTimer();
});
_client.on('client:disconnect', () => {
connect();
});
}
// initializeClient();
connect();
async function disconnect() {
stopPushAllCommandTimer();
if (_client) {
_client.disconnect(true).catch();
}
}
async function connect() {
if (_client?.connected) {
return;
}
console.log('Connecting...');
try {
// A client cannot be reused after a connection failure :s
initializeClient(); // Create a new instance of client with listeners
await _client.connect();
console.log('Connected');
}
catch(exception) {
console.log('Failed to connect');
setTimeout(connect, 10000);
}
}
function stopPushAllCommandTimer() {
clearInterval(_pushAllTimer);
_pushAllTimer = null;
}
function startPushAllCommandTimer() {
stopPushAllCommandTimer();
// Required to get all parameters, including the 'mc_percent'
_pushAllTimer = setInterval(async () => {
console.log('Sending pushAll command');
await _client.executeCommand(new PushAllCommand()).catch();
}, 5000);
}
// Check client#currentJob periodically
setInterval(() => {
console.log(`cilent.currentJob: ${_client?.currentJob}`);
}, 5000);
// Send the progress to the leds stripe
async function sendProgress(percent, color='FFFFFF') {
await fetch(`${_ledsStripProgressEndpoint}?percent=${percent}&color=${color}`, { method: 'PUT', signal: AbortSignal.timeout(10000) })
.catch(exception => {
console.error('Send progress failed');
});
console.log(`XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ${percent}% SENT!`);
}
Hi! Any news?
Hi! Any news?
Hey!
Sorry for not getting back to you sooner. Unfortunately, I do not think I have the bandwidth to work on this.
Feel free to make a PR and if it's good quality I'll merge it for sure :)
Hi! Thank you so much for your library!
I'm encountering an exception I'm not able to catch each times a print finish and another starts (so that is not an edge case ^^).
The printer's status has changed from FINISH to PREPARE! file:///.../node_modules/bambu-node/dist/index.js:333
Error: Edge case detected while updating printer status! at D.onMessage (file:///.../node_modules/bambu-node/dist/index.js:333:14924) at MqttClient.listener (file:///.../node_modules/bambu-node/dist/index.js:333:12437) at MqttClient.emit (node:events:529:35) at handlePublish (...\node_modules\mqtt\build\lib\handlers\publish.js:97:20) at handle (...\node_modules\mqtt\build\lib\handlers\index.js:28:35) at work (...\node_modules\mqtt\build\lib\client.js:227:40) at writable._write (...\node_modules\mqtt\build\lib\client.js:252:13) at writeOrBuffer (...\node_modules\readable-stream\lib\internal\streams\writable.js: 334:12) at _write (...\node_modules\readable-stream\lib\internal\streams\writable.js:283:10) at Writable.write (...\node_modules\readable-stream\lib\internal\streams\writable.js :286:10)
More context:
Node.js v18.19.1
When the status of my printer changes for RUNNING, my app starts to send the command PushAllCommand each 5s to obtain the current progression (mc_percent).
If the status changes for any other value, it stops to send this command.
Maybe it is the cause of the crash? Maybe there is an easier way to get the progress of the print?
Another small thing. If you instanciate a client and the connection fails (because the printer is off for example), there is no way to reuse it to retry. You have to create a new instance of the client each time.
Sorry for my bad english and thank you again for your work, now I have a stripe of leds displaying the progress of my prints from another room on the top of my ultra wide desk (3 meters).