fetchPrinterCalibrations.test.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. /**
  2. * `fetchPrinterCalibrations` asks the printer for its K-profile table.
  3. *
  4. * Two properties, both measured on real hardware rather than reasoned about:
  5. *
  6. * 1. It asks for EVERY standard nozzle size, not only the sizes currently
  7. * fitted. A K profile is stored on the printer per diameter and survives a
  8. * nozzle swap, so fetching only what is screwed in right now hides a 0.6
  9. * profile until a 0.6 is fitted, and stops a spool being prepared for a
  10. * nozzle that is about to be changed to.
  11. *
  12. * 2. It asks for them ONE AT A TIME. H2-series firmware answers only the first
  13. * one or two of a concurrent burst of `extrusion_cali_get` and silently
  14. * drops the rest; each dropped request then costs a 5-second timeout before
  15. * the retry. Measured on an H2C and an H2D: four parallel requests took 11s
  16. * and 23s, against roughly 1s sent in series. An X1C answers all four
  17. * concurrently, which is why this stayed hidden while only dual-diameter
  18. * printers ever sent more than one request.
  19. *
  20. * The second is the one worth a test: it is invisible in every unit-level
  21. * result (the same rows come back either way) and only shows up as a stall on
  22. * one brand of hardware.
  23. */
  24. import { describe, it, expect, vi, beforeEach } from 'vitest';
  25. const getKProfiles = vi.fn();
  26. vi.mock('../../../api/client', () => ({
  27. api: {
  28. get getKProfiles() {
  29. return getKProfiles;
  30. },
  31. },
  32. }));
  33. import { fetchPrinterCalibrations } from '../../../components/spool-form/utils';
  34. import { STANDARD_NOZZLE_DIAMETERS } from '../../../components/spool-form/constants';
  35. function profile(slotId: number, diameter: string) {
  36. return {
  37. slot_id: slotId,
  38. filament_id: 'GFL99',
  39. setting_id: 'GFSL99',
  40. name: `PLA ${diameter}`,
  41. k_value: '0.020',
  42. n_coef: '1.0',
  43. extruder_id: 0,
  44. nozzle_diameter: diameter,
  45. };
  46. }
  47. describe('fetchPrinterCalibrations', () => {
  48. beforeEach(() => {
  49. getKProfiles.mockReset();
  50. });
  51. it('asks for every standard nozzle size, not just the fitted one', async () => {
  52. getKProfiles.mockResolvedValue({ profiles: [] });
  53. await fetchPrinterCalibrations(1, { nozzles: [{ nozzle_diameter: '0.4' }] });
  54. const asked = getKProfiles.mock.calls.map(([, diameter]) => diameter);
  55. expect(asked).toEqual(expect.arrayContaining(STANDARD_NOZZLE_DIAMETERS));
  56. });
  57. it('includes an unusual fitted diameter alongside the standard sizes', async () => {
  58. getKProfiles.mockResolvedValue({ profiles: [] });
  59. await fetchPrinterCalibrations(1, { nozzles: [{ nozzle_diameter: '1.0' }] });
  60. const asked = getKProfiles.mock.calls.map(([, diameter]) => diameter);
  61. expect(asked).toContain('1.0');
  62. // Asked once each, no duplicate for a size that is both standard and fitted.
  63. expect(new Set(asked).size).toBe(asked.length);
  64. });
  65. it('never has two requests in flight at once', async () => {
  66. let inFlight = 0;
  67. let maxInFlight = 0;
  68. getKProfiles.mockImplementation(async () => {
  69. inFlight++;
  70. maxInFlight = Math.max(maxInFlight, inFlight);
  71. await new Promise(resolve => setTimeout(resolve, 0));
  72. inFlight--;
  73. return { profiles: [] };
  74. });
  75. await fetchPrinterCalibrations(1, { nozzles: [{ nozzle_diameter: '0.4' }] });
  76. expect(getKProfiles.mock.calls.length).toBeGreaterThan(1);
  77. expect(maxInFlight).toBe(1);
  78. });
  79. it('keeps the diameters that answered when one request fails', async () => {
  80. getKProfiles.mockImplementation(async (_id: number, diameter: string) => {
  81. if (diameter === '0.6') throw new Error('printer said no');
  82. return { profiles: [profile(1, diameter)] };
  83. });
  84. const rows = await fetchPrinterCalibrations(1, { nozzles: [{ nozzle_diameter: '0.4' }] });
  85. const diameters = rows.map(r => r.nozzle_diameter);
  86. expect(diameters).toContain('0.4');
  87. expect(diameters).not.toContain('0.6');
  88. });
  89. it('flattens every size into one list of calibrations', async () => {
  90. getKProfiles.mockImplementation(async (_id: number, diameter: string) => ({
  91. profiles: diameter === '0.8' ? [] : [profile(Number(diameter.replace('.', '')), diameter)],
  92. }));
  93. const rows = await fetchPrinterCalibrations(1, { nozzles: [{ nozzle_diameter: '0.4' }] });
  94. expect(rows.map(r => r.nozzle_diameter).sort()).toEqual(['0.2', '0.4', '0.6']);
  95. expect(rows[0].k_value).toBeCloseTo(0.02);
  96. });
  97. });