eufy e1 ink refilling

[adding more photos and such as i go] this is still a work in progress, we’re happy with the ink side, but the cleaning cart needs work. part one is here https://charliex2.wordpress.com/2026/03/06/eufy/ which is more about the software.

note: we’re still investigating the cleaning tank, we wondered why two holes and someone on reddit mentioned the moisturiser and cleaner which make sense.. so we need to double check which is which, thanks Haunting-Sun4086 who also sent this along https://www.youtube.com/watch?app=desktop&v=YY3D_2Uo2wk as another method

the full detector on the cleaning tank is a hall effect sensor in the top left of the cleaning tank pocket on the machine side.

don’t do any of this unless you’re sure you are comfortable with it, as we’re dealing with unknowns and it’s very early stages yet. we usually prefix these posts with a cautionary note that highlights the importance of being prudent; don’t do any of this unless you fully understand the risks involved, as it is all for informational purposes only. it’s crucial to take the time to analyse the situation, weigh the potential outcomes, and consider seeking other advice if necessary. being informed will help you navigate the complexities more effectively and reduce the likelihood of encountering unforeseen complications.

always proceed with caution and prioritise your and your printers safety and well-being above all else.

the DRM is still in play, so even refilling the tank of the cartrdiges will very likely still be ignored by the printer and it won’t use the refilled ink, we don’t know enough yet. we’re crazy, read some of the other posts on this blog, we’ll take anything apart the first day we get it.





We’ve been messing with the eufy e1 printer, first we did the security audit. tested the printer. all great. then we ordered almost $1,000 worth of ink and cleaner for their storefront, and got nothing from anker/eufymake in more than a month or so and still no indication of it appearing, shipping or existing, so we’re not using the printer as much because of the inkjet version of range anxiety…

We already know its an epson head of the xp600/700/800 variety (see previous post), the ink carts use a 5 pin which isn’t typical, there is a mag stirrer in the white which is a nice feature

anyway on to it…

another note to add is that if the printer indicates that the cartridge is empty, simply refilling it may not revive its functionality or restore it to working order. i haven’t tested this theory extensively, but Ii feel fairly confident in my assessment based on various reports i’ve encountered.

if you are considering experimenting with this, it’s advisable not to purchase a large quantity of ink, as it could lead to wasted resources if the theory holds true. Instead, opt for a single container of the smallest size available to conduct your tests and determine whether it resolves the issue. additionally, there is still a lot we don’t know about printer mechanisms and the nuances of ink quality, so proceeding with caution can save you both time and money in the long run.

there are three pumps, and three holes so it makes sense there is a cleaning fluid, moisturising fluid and a waste tank.. we will identify which hole of the two green circled ones is which. i’ve ordered some uv moisturising fluid for UV DTF printers

have ordered this https://www.amazon.com/dp/B0F6NX5D84

show me your secrets.. LA….

the nozzle from the printer and how it pushes back the spring load mechanism in the cartridge and still flows liquid.

first we removed the cleaning cartridge as felt like that was the best one to start with. after removing we noticed it had a clip on cover. so we removed that.

the fill hole that is seealed with a plastic thermal bonded sheet, puncture thru the plastic sheet, then the fill hole is not a sprung load like the others so it is easier to fill here but you can also use one of the sprung loaded ones the printer uses.

the waste tank is visible here, don’t puncture this sheet.

red is the waste, green cleaning fluid (we’re wondering if one of the green holes is moisturiser and the other is cleaner so still evaluating that one) https://www.dtflinko.com/printer-product/dtf-moisturizing-solution/ some of the cleaners are both cleaning fluid and moisturiser https://supergamut.com/product/uv-dtf-solvent-cleaner-and-moisturizer-1-liter/ etc

Ok confirmed its two tanks, looking from the back of the jet clean, right side solvent, left side mosituriser.

once will feel more slippery. since one is glycerol/proplene glycol and the other is a solvent. smell should be different too

another test, put the same amount on a sheet of paper, the one that spreads slower is the moisturising fluid. solvents in the cleaning should make it evaporate as well

doing a few tests to see which one has more solvent in it.

OK so the bottom port goes the left rear port thats circled in green, i think the left one is moisturising fluid and right one is cleaning fluid, but i want to be sure first, waiting on some stuff to be shipped for more testing.

so the green one under the red circle is solvent
the other green one is mouisturiser

after the cover is removed , there is this refill hole. its not sprung loaded , just push the luer into it. you can reseal the plastic if you want too.

load up on moisturising fluid

just insert it in, no spring loading, fill it slowly

(we are pretty sure this is moisturising refill port, not cleaning fluid, dont mix them), the deep clean cycle sucks from the other tank, so we are saying this is moisturiser after more tests

inserting a luer lock into the waste and letting it pour out also works, just make sure you collect and dispose of it properly

using a syringe instead to suck out the material

the printers side of the white cartridge connection. a nozzle to ge thte ink , a locator pin? and the mag stirrer, notice + shape thats like that for a good reason so that when it pushes the spring loaded side of the cartridge the ink flow isn’t blocked, consider that when refilling.

mag stirrer hole,locator and the pop out spring latch, where the ink comes out (the white part) this one is spring loaded just depress it with something and then fill it. take it slowly allow it time to settle, shake the ink first then let it settle for a few minutes before syringing it.

insert the syringe into the hole , we’re using a second spudger here to depress the mechanism inside the hole then let the syringe flow it in because if you use the syringe flat on at 90o it can block the flow (plan is to mod a luer to add a cutout or hole so it can flow ink and press down the spring loaded plunger, but this works too.

cleaning fluid we used

the notch we cut in the cleaning cartridge to access the refill port. you do not need to do this, but we wanted too.

looks like we are about to start a painting, or prep for tamper at defcon.

interesting looking antenna like thing in that black glossy strip…

you could easily mod this printer to make an epson tank style where it gravity feeds the cartridges from bottles. but we need to look into the cartridge mangement a bit more and get more data about what that looks like, W and G , maybe cleaning are the ones probably the best to modify to be tanks since they use the most commonly used for us anyway.

also as a top tip, do not lift the top cover on the printer as it’ll pause and its restart logic is terrible and might leave a large stripe.

todays ink use, interestingly unless i am reading it wrong this seems wrong to me. there is no way we’ve used the same amount of ink on all colours. one print went bonkers and printed out like solid white for at least 5mm thick in a 6cm square. so either it means something else or…

ahh derp i think realised what it means, its maintainance log only usage ink ? (flux always tints the pictures)

weighing the ink tanks is a good way to stop overfiling, but we need a baseline first.

Tapu sent over a trace and some images, which confirms what we suspected. It’s shared I2C bus. Each cartridge has its set if id’s.

however the potential bad news is the sequence to me looks like a challenge response sequence. i don’t think its simple eeprom. It’d be a device like ST/AD/Microchips small eeprom with a built in security layer. I want to do some more tracing but thats my always been what I expected it to be

those chips are cheap and easily available, its a WLCSP 1.8mm x 1.8mm

thanks Tapu!

will post some more pictures soon.. but for now (also anker as frequent tamper evident competition winners/teachers we were so disappointed with your tamperproof stickers ) but i have to say i really liked the one that was a larger void sticker on the top of the other one.

https://www.ftc.gov/news-events/news/press-releases/2024/07/ftc-warns-companies-stop-warranty-practices-harm-consumers-right-repair

it’s always interestng to see empty connectors, that are only a few pins and in different places.

so next step remove the shields. and see what processor we are dealling with, it’ll be some SOC like a MT or one of those larger capable chips that has all the features.

that tar file though….

eufy

Reverse Engineering eufy Studio: GPL Violations, Telemetry, Ink DRM, and Encryption

This is very much a work in progress. I’ve only had this printer for about a week and we’re still in the discovery phase, so expect mistakes and things that turn out to be wrong. I’ll update as i go.

Some findings are concrete, the GPL/AGPL licence violations for instance are pretty clear cut. Other parts are still conjecture or best guesses from reading decompiled code. The ink DRM behaviour is mostly inferred from code analysis, not verified by running out of ink. Things like avrdude are almost certainly carried over from the 3D printer side of the codebase (it’s in the other open source slicers this was forked from), not necessarily used by the UV printer, but i haven’t confirmed that either way yet.

I’ve redacted the keys (they’re easy to get) until i determine how risky it is for other users. I don’t want wallofsheep getting to private files or triggering deletions or such, i don’t know if that can happen yet, but i’d rather be sure. For the GPL stuff, to the best of my knowledge the source hasn’t been shared, i also haven’t asked for it. But there is a github for the version this is built from. They may be planning to release it at some point or waiting to be asked.

So far i have only looked at the software side. We security audit everything that goes on our network, especially cloud based products. There are always potential risks, so we isolate these machines from core networking.

Over the course of a weekend, i looked over eufyMake Studio, the desktop companion app for eufy’s (formerly AnkerMake) UV printer E1 and 3D printer product line. What i found was a tangled web of open-source licence violations, extensive telemetry, software defined ink DRM, encrypted firmware updates, and an API encryption layer.

If you were someone who stayed on the earlier bambu firmwares (1.8 and earlier if i recall correctly) you might want to hold off on future updates.

Platform tested: macOS (x86_64, runs under Rosetta 2 on Apple Silicon) and Windows (x86-64, NSIS installer). Printer: eufyMake UV Printer E1 (model V8260).

Table of Contents

  1. Fork Lineage: PrusaSlicer to eufyMake
  2. GPL/AGPL Licence Violations
  3. Source Code Availability Audit
  4. Telemetry and Analytics
  5. Ink Cartridge DRM
  6. API Encryption and Signing
  7. EMPF File Format and Decryption
  8. Firmware Update Security
  9. P2P Protocol and Network Architecture
  10. Hooking Techniques and Rosetta 2 Limitations
  11. Log File Encryption
  12. AI Hub and Relief Generation
  13. Conclusions

1. Fork Lineage: PrusaSlicer to eufyMake

eufyMake Studio is not built from scratch. It is a fork of a fork of a fork: (as riverside quoth it’s a spork)

PrusaSlicer 3.3.0
-> BambuStudio code merged
-> AnkerSlicer_P / AnkerMake Studio
-> eufyMake Studio

The evidence is overwhelming to say the least (allegedly) :

  • Build paths leaked in binaries: /Users/anker/Projects/3D_Projects/PrusaSlicer/deps/build_x86/
  • CI server paths: /Users/anker/codingci/tools/jenkins_home/workspace/build_x86_dir/AnkerSlicer_P/AnkerStudio/
  • wxWidgets built from: /Users/anker/Documents/Workspace/PrusaSlicer-3.3.0_for_x86/
  • Windows build path: D:\_build\eufyMakeScript\build_windows_20260106155741\AnkerSlicer_P\AnkerStudio\
  • 22,923 symbols in the Slic3r C++ namespace
  • BambuStudio 3MF importer: 157 symbols with _BBS_3MF_Importer
  • GCode comments: ; generated by Slic3r
  • About dialog HTML credits Alessandro Ranellucci (Slic3r author) 2011-2018
  • Internal name “AnkerMake_alpha” found in localisation strings

Incomplete Rebranding

The rebranding from PrusaSlicer/AnkerMake to eufyMake was done hastily. The localisation files still contain references to previous product names across all 8 supported languages:

LanguageSlic3r refsAnkerMake_alpha refsPrusa refs
en240
de371996
es580212
fr580214
it560209
ja55037
zh_CN5115412
zh_TW4604

Notable unrebranded strings include: “PrusaSlicer is based on Slic3r by Alessandro Ranellucci and the RepRap community.” (in Italian, French, and Spanish), and “Please download … from https://www.prusa3d.cz/prusaslicer/” (in Chinese and German).


2. GPL/AGPL Licence Violations

This is the most serious finding. eufyMake Studio ships zero licence files no COPYING, LICENSE, NOTICE, or credits files anywhere in either the macOS or Windows distributions. The app bundle contains over 50 open-source components, many under copyleft licences that require source code distribution and licence text inclusion.

AGPL-3.0 Violations (most severe)

The entire slicer engine is derived from PrusaSlicer, which is AGPL-3.0 licenced. The AGPL requires that the complete corresponding source code be made available to every user who interacts with the software.

ComponentSymbolsBinaryLicence
PrusaSlicer / Slic3r (core slicer engine + GUI)22,923eufyStudio (main binary)AGPL-3.0
BambuStudio 3MF importer code157eufyStudioAGPL-3.0

GPL-3.0 Violations

ComponentSymbolsBinaryLicence
libigl copyleft/cgal module (mesh booleans)221eufyStudioGPL-3.0
CGAL (computational geometry)1,443eufyStudioGPL-3.0+

GPL-2.0+ Violations

ComponentSymbolsBinaryLicence
avrdude (firmware flasher)75eufyStudioGPL-2.0+
x264 (H.264 encoder)2,626libAnkerNet.dylibGPL-2.0+
x265 (HEVC encoder, 8/10/12-bit)20,096libAnkerNet.dylibGPL-2.0+
XviD / libxvidcore44libAnkerNet.dylibGPL-2.0+
FFmpeg (libavcodec/format/util/filter)9,223libAnkerNet.dylibLGPL-2.1+ (GPL-tainted by x264/x265/xvid)

On macOS, all of these are statically linked into libAnkerNet.dylib (65 MB). On Windows, FFmpeg ships as separate DLLs (avcodec-59.dll at 75 MB, avformat-59.dll at 15 MB) specifically the gyan.dev “full” build compiled with -enable-gpl enable-version3, making it explicitly GPL-3.0+.

LGPL Violations (static linking problem)

LGPL requires that users be able to relink against a modified version of the library. Three LGPL-2.1 components are statically compiled into the main binary with no separate .dylib, making relinking impossible:

ComponentSymbolsLicence
OpenCASCADE (OCCT)3,567+LGPL-2.1
wxWidgets44,099+LGPL-2.0 + wxWindows exception
NLopt175LGPL-2.1+

Attribution-only components (also missing)

Even components with permissive licences (Apache-2.0, MIT, BSD) require copyright notices to be included. eufyMake ships zero notice files. Affected components include OpenCV, Intel TBB, Boost, Dear ImGui, CEF, libsentry, and over 20 others.


3. Source Code Availability Audit

To their credit, eufy does publish older source code on GitHub under the eufymake organisation. However, there are significant gaps.

What they publish

RepositoryLicenceLast pushedNotes
eufyMake-PrusaSlicer-ReleaseAGPL-3.02025-03-24Real source code, 4,298 files, buildable
eufyMakePrusaSlicer-settingsAGPL-3.0Fork of prusa3d/PrusaSlicer-settings
eufyMake-MarlinGPL-3.02023-10-27Firmware for M5 printer
eufyMake-Marlin-M5CGPL-3.02024-08-30Firmware for M5C printer
eufyMake-Marlin-for-CompetitionNONENo licence on GPL-derived code
eufyMake-linux-sdkGPL-3.02023-04-27Bootloader/kernel/buildroot (727 MB)

Compliance gaps

  • Periodic source dumps, not continuous history: commits appear as large “open source for version X.Y.Z” pushes, not continuous development history. This raises questions about whether the published source is the exact “corresponding source” required by the AGPL.
  • No source link on product website: neither eufymake.com nor ankermake.com has a dedicated GPL/open-source page. Both /opensource, /gpl, /source, and /license return 404.
  • No source offer in the application UI: the AGPL typically requires a link to source code in the about/help dialog.
  • Stale firmware repos: M5 firmware source is 2.5 years old; M5C is 1.5 years old. The linux-sdk hasn’t been updated in nearly 3 years.
  • Missing licence on competition repo: eufyMake-Marlin-for-Competition has no licence declaration despite being Marlin-derived (GPL-3.0).

4. Telemetry and Analytics

eufyMake Studio has an extensive custom telemetry system called “BuryPoint” (a direct translation of the Chinese term “mai dian”, meaning instrumented analytics events). It tracks nearly every user action.

Events tracked (20+ handler methods)

  • Print start, finish, failure, reprint
  • File uploads and downloads
  • Slicing results and calibration events
  • Ink estimation and levels
  • Height measurements and auto-measurements
  • UI interactions (layout snaps, scrolls, key presses)
  • Web-based generation events
  • Photo captures
  • Self-check results
  • Cloud storage encryption events
  • P2P connection status, MQTT status, gcode transfer speed

Events are cached locally in bury_point_cache.json and uploaded to Anker’s servers via /v1/slicer/logging/upload_events. A server-side toggle (burypoint_switch) can enable/disable collection.

Worth noting that some of this “telemetry” may just be a side effect of the architecture. The printer communicates via MQTT through Anker’s cloud servers, so all commands and status updates pass through their infrastructure by default. There does appear to be an offline/LAN mode but i haven’t dug into that yet. The overall setup is very similar to how BambuLab’s printers work.

Third-party analytics services

ServicePurposeRegion
SentryCrash reportingGlobal
Alibaba Cloud RUMReal user monitoringChina only
voc.aiAI customer support chatGlobal

Connectivity checks

The app uses google.com/generate_204 for international connectivity checks and baidu.com for China.


5. Ink Cartridge DRM

What the vendor says

“Each cartridge includes an identification chip that helps the printer recognise the ink type, model, and production date.”

eufymake.com/blogs/news/all-about-eufymake-uv-ink

The chip stores type, model, and production date

What was NOT found in binaries

Exhaustive search of all macOS and Windows binaries found zero matches for: NTAG, MIFARE, ISO 14443/15693, FeliCa, DS2431/DS18xx, ATECC/AT24Cxx, or any NFC/RFID/I2C/SPI authentication protocol strings.

Five software-defined enforcement mechanisms

  1. Cartridge type detection: physical or electrical identification. The printer checks whiteInkBoxType against whiteInkInWayType. Mismatches block printing with “current ink cartridge type: %d, ink way type: %d, need inject ink”.
  2. Expiry enforcement: date-based, tracked via MQTT state. The expireTime field is compared against the current date. Expired but physically full cartridges trigger a warning: “Your UV ink cartridge has expired.” This is gleaned from the code, not verified by actual usage. It’s also possible there’s a skip/override, but both are conjecture at the moment.
  3. Ink level estimation: computed by host software from print job raster data, not read from a chip. Cloud config flags control a 3-level indicator.
  4. Cloud consumable parts tracking: maintenance items managed via query_maker_part / reset_maker_part APIs with remaining work life, percentage, and reset capability.
  5. White ink cycle tracking: tracks circulation frequency for maintenance scheduling.

Third-party ink feasibility

The maintenance HTML page in the app bundle says: “Strictly prohibit the use of third-party inks!” a policy warning, not a hardware enforcement statement. If crypto auth were enforced, the firmware would simply refuse to print.

A third-party cartridge with a cloned or emulated chip (returning valid type + future date) would likely pass all checks. The 5-10x price premium over bulk UV ink (~$430/litre vs ~$40-80/litre for bulk equivalent) makes this commercially attractive.

Where the chip protocol lives

The identification chip is read by the printer’s embedded Linux firmware, not by eufyMake Studio. The Studio app only sees the result via MQTT. This is why no I2C/SPI transaction strings appear in the desktop binaries that layer is entirely in the printer’s own firmware (not in our possession).

Ink system overview

The E1 supports 6 ink channels (C/M/Y/K/White/Varnish) plus a cleaning cartridge. 100 ml per cartridge at approximately $43 USD each. Print modes include CMYK, White, Varnish, sandwiched layers (White_CMYK_GlossVarnish), emboss, texture painting, and sticker mode.

I’m reasonably sure its an Epson XP600/700/1/DX11, FA09121, F1080-A1 variant print head, so its likely epson chips as well for the DRM. so the ink is very likely epson compatible.

There are some slight differences in the mould


6. API Encryption and Signing

The entire API communication layer has been reverse-engineered from the webpack JS bundle served by the WebView2Loader.dll (Edge/Chromium WebView2) , DYLD hook captures, and MITM sessions.

Request signing: HMAC-SHA256

sign_string = timestamp + "+" + nonce + "+" + encrypted_body_base64
X-Signature = HMAC-SHA256(key=UTF8(key_hex), message=UTF8(sign_string))

Critical detail: the HMAC key is the hex string encoded as UTF-8 (32 bytes), NOT the raw binary bytes the hex represents (16 bytes). This was verified against 2 captured signatures from MITM both matched exactly.

Four hardcoded localKeys

The key exchange uses one of 4 hardcoded keys depending on server and environment:

makeitreal QA/CI: redacted
anker_make QA/CI: redacted
makeitreal PROD: redacted
anker_make PROD: redacted

ECDH P-256 key exchange

Session keys are established via ECDH on the P-256 curve:

  1. Client generates P-256 keypair, exports public key as 130-char hex string
  2. Public key is AES-CBC encrypted with the localKey (16 bytes from hex), random IV prepended
  3. Result is base64-encoded and sent as client_public_key
  4. Server responds with its encrypted public key in the same format
  5. ECDH shared secret is derived, converted to hex, padded to 64 chars
  6. shareKey = first 32 hex chars (used as 16 raw bytes for AES-CBC encryption, or 32 UTF-8 bytes for HMAC signing)

The mystery of the 160-byte encoded public keys is explained by: 16 bytes IV + 144 bytes AES-CBC(130-char hex string with PKCS7 padding) = 160.

Body encryption: AES-128-CBC

All API request and response bodies are: JSON.stringify -> AES-128-CBC encrypt with shareKey (random IV prepended) -> base64 encode. The base64 string replaces the HTTP body. PKCS7 padding is used.

Password encryption: separate ECDH

Login passwords are encrypted with a separate ECDH key exchange using a hardcoded server public key:

redacted
redacted

The password is AES-256-CBC encrypted with the full 32-byte shared secret as key and the first 16 bytes as IV. Only the ciphertext (no IV) is base64-encoded and sent.

Complete login flow

A full login involves 113 HTTP requests across 8 phases:

  1. Domain resolution: GET make-extend.ankermake.com/domain/{country} (plaintext, returns API endpoints)
  2. Key exchange #1: POST make-app.ankermake.com/v3/pc/oauth/key_exchange (ECDH with anker_make localKey)
  3. Login: POST make-app.ankermake.com/v2/passport/login (encrypted credentials, 9-field body including ECDH-encrypted password)
  4. Key exchange #2: POST aiot-api-us.ankermake.com/openapi/oauth/key/exchange (ECDH with makeitreal localKey)
  5. Quick login: POST aiot-api-us.ankermake.com/app/muses/passport/quick_login (token federation with gtoken = MD5(user_id))
  6. Repeat steps 4-5 for EU API
  7. Authenticated API calls using regional shareKeys

Session persistence: share_key.ini

Session keys are stored encrypted in ~/Library/Application Support/eufyMake Studio Profile/share_key.ini. The encryption key is derived from a combination of a hex prefix stored in the file and the machine’s IOPlatformUUID.

On Windows this uses HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid (changes if you reinstall Windows), on macOS it’s the IOPlatformUUID (hardware-tied). Full details in the share_key.ini decryption section below.

device_list.json extensive device info:

share_key.ini Decryption – Key Derivation Process

Storage Format

Each entry in share_key.ini is stored as:

US_TEXT = <identity><base64_blob>

Where:

  • identity – first 32 characters, a hex string identifying the ECDH key pair
  • base64_blob – remaining characters, base64-encoded binary data containing:
    • bytes 0-15: random AES IV (16 bytes)
    • bytes 16+: AES-128-CBC encrypted ciphertext (PKCS7 padded)

Key Derivation

The AES-128 decryption key is derived as follows:

SHA256( hardcoded_salt + identity + machine_id + \x00 ) -> take first 16 bytes

Component Sources

Component Value Source
──────────────── ──────────────────────── ─────────────────────────────────────────
hardcoded_salt redacted (32B, ASCII) Compiled into AnkerPlugin.dll at .rdata
offset 0xB2160. Initialised via strcpy
into heap std::string at qword_1800D4E98.
Matches GUID format with hyphens stripped.
identity redacted (32B, ASCII) First 32 chars of stored US_TEXT value.
Generated during ECDH P-256 key exchange.
machine_id redacted (36B, ASCII) Windows: HKLM\SOFTWARE\Microsoft\
Cryptography\MachineGuid
macOS: IOPlatformUUID
Fallback: "DEFAULT_MACHINE_ID"
\x00 null byte (1B) Bug: MachineGuid length includes null
terminator → 101 bytes input, not 100

Step by Step

  1. Read MachineGuid from HKLM\SOFTWARE\Microsoft\Cryptography (wide string, converted to UTF-8)
  2. Concatenate three strings plus the trailing null (total: 101 bytes)
  3. SHA-256 hash the concatenated input → 32-byte digest
  4. Hex-encode the full 32-byte digest → 64 hex characters
  5. Truncate to first 32 hex characters (representing the first 16 bytes of the digest)
  6. Hex-decode those 32 characters back to 16 raw bytes → this is the AES-128 key

Steps 4-6 are an artefact of the C++ implementation routing everything through std::ostringstream with std::hex formatting. The net result is simply: AES key = first 16 bytes of SHA-256 digest.

Decryption

base64_decode(stored_text[32:]) -> blob
IV = blob[0:16]
ciphertext = blob[16:]
key = SHA256(salt + identity + machine_id + \x00)[:16]
plaintext = AES-128-CBC-decrypt(key, IV, ciphertext)
plaintext = strip PKCS7 padding

What Gets Decrypted

The plaintext is itself a 32-character hex string – the ECDH shared secret derived from the initial key exchange:

Section Decrypted Key Used For
──────────────────────── ─────────────── ────────────────────────────────────
[HTTPS anker_make] redacted HMAC-SHA256 signing for anker_make
API endpoints
[HTTPS make_it_real] redacted HMAC-SHA256 signing for make_it_real
API endpoints

These shared keys are then used as AES keys to encrypt/decrypt API request and response bodies for all subsequent HTTPS communication with eufy/Anker cloud services.

Source File

The implementation lives in:

AnkerSlicer_P/AnkerStudio/build_temp/_deps/ankerplugin-src/
anker_plungin/Http/Security/HttpSecurityPersistence.cpp

Functions: EncryptKeyForStorage (line ~167) and DecryptKeyFromStorage (line ~224).

Verification Method

The key derivation was confirmed by:

  1. Static analysis of the decompiled AnkerPlugin.dll (IDA Pro)
  2. Dynamic verification using cdb.exe (WinDbg command-line debugger) with breakpoints on AnkerPlugin+0x2FCB0 (SHA256 key derivation) and AnkerPlugin+0x2DB40 (AES-CBC decrypt)
  3. Successful decryption of both share_key.ini entries using Python with the derived algorithm

7. EMPF File Format and Decryption

EMPF (eufyMake Project File) is the project format for 2D/UV printing workflows. It’s an AES-256-GCM encrypted ZIP with a custom header.

File structure

offset size description
0x00 8 magic: "eufyMake" (ASCII)
0x08 4 header_length (uint32, big-endian). content starts at this offset.
0x0C var TLV header entries
hdr_len 12 random nonce (AES-GCM IV)
hdr+12 var AES-256-GCM ciphertext
end-16 16 AES-GCM authentication tag

TLV entries are type (1 byte) + length (2 bytes big-endian) + value. Known types:

  • type 1: key version (1 byte, 0x01 for offline)
  • type 3: unknown flag (always 0x01)
  • type 4: user identifier (40 bytes, null-padded). "offline" for locally-saved files.
  • type 5: entry name (e.g. "file")
  • type 6: MIME type (e.g. "application/octet-stream")

Decrypted content is a ZIP archive containing:

Metadata/
project_info.json
Asset/
images/
thumbnail.png
project_file/
canvas_<hash>.json
font/
font_mapping.json

Same structure as the empf-generator third-party tool uses for the E1 printer, just without encryption.

Where the crypto actually lives

I wasted a lot of time setting breakpoints on native crypto. The eufyStudio.dll has a full EMPF decryption pipeline – HandleEufymakeFileDecryption at RVA 0xF9A5C0, encryption checks, a key lookup in a red-black tree, vtable dispatch into AnkerNet_2060004.dll. None of it fires for offline files.

The “Make It Real” design editor runs inside an embedded Chromium WebView. Locally saved projects are encrypted and decrypted entirely in JavaScript via the WebCrypto API (window.crypto.subtle). The native code path is only for cloud-transferred files between users.

The JS bundle at %APPDATA%\eufyMake Studio Profile\cache\offline\web_res\versions\1.13.02\web\assets\index-DQRbPRws.js (14MB minified) has two classes: FileEncryUtil for AES-256-GCM operations and FileEncryptionManager for header parsing and key selection.

Offline key

I should have realised the same as the other ones that the offline key is hardcoded in the JS as a double-base64-encoded string:

encoded = "[redacted-base64]"
atob(encoded) = "[redacted-base64-inner]"
atob(atob(encoded)) = "[redacted]"

32 ASCII characters used directly as a 256-bit AES key. It looks like it was meant to be a hex-encoded GUID but there’s an l (lowercase L) where a digit should be, so it’s not valid hex. Gets UTF-8 encoded and fed straight into importKey. No AAD.

Cloud keys

For files shared between users, keys are fetched from /web/editor/user/get_encrypt_key_list. The TLV version number selects which key. Cloud material files (downloaded from S3) use the same mechanism. The native code path in eufyStudio.dll has its own key lookup (the file_key_list red-black tree), used when cloud-transferred EMPF files are opened through the native handler rather than the WebView.

Decryption in Python

import struct
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
KEY = b'[redacted]'
def decrypt_empf(path):
with open(path, 'rb') as f:
data = f.read()
assert data[:8] == b'eufyMake'
hdr_len = struct.unpack('>I', data[8:12])[0]
content = data[hdr_len:]
nonce = content[:12]
ciphertext_and_tag = content[12:]
return AESGCM(KEY).decrypt(nonce, ciphertext_and_tag, None)

Returns a ZIP. Tested on 7 files from 74KB to 139MB, all decrypted fine.

Dead ends

For anyone going down a similar path:

  • Breakpoints on EVP_DecryptInit_ex, BCryptDecrypt, AnkerPlugin decrypt functions – none fire for offline EMPF. The native code is dead code for this path.
  • The key [redacted] shows up during EMPF opens but it’s the cloud thumbnail decryption key used by AnkerPlugin!AnkerDecryptContent.
  • The API secret_key from device_list.json with IV 3DPrintAnker is for API response encryption only.

8. Firmware Update Security

The firmware update mechanism has a concerning security posture. Entropy analysis of the V8260 firmware binary (353 MB) confirms it is fully encrypted from byte zero with no plaintext header or magic number.

Cloud OTA (primary path)

The app checks for updates via POST /v1/app/ota/get_app_version. The server response includes a download_url (dynamic, not hardcoded) and critically, a forceUpgrade flag that can mandate silent updates. MQTT can also push OTA state changes via MQTT_CMD_FIRMWARE_VERSION.

No firmware signature verification was found. No SHA256, MD5, HMAC, or digital signature verification strings exist in the download/flash code path. Combined with the forceUpgrade flag, a compromised API server or MITM on the HTTPS session could push arbitrary firmware to devices silently.

Firmware binary analysis

The firmware binary (ANKER_V8260_RELEASE_3.2.73_0001_20260209_15C97C_ENCRYPT.bin, 337 MB) was downloaded from the S3 URL provided by the MQTT commandType 1002 response. Entropy analysis:

entropy = 7.999999 bits/byte (max 8.0)
chi-square = 248.37 (60.51% - well within random)
mean = 127.5042 (random = 127.5)
monte carlo pi = 3.141266 (0.01% error)
serial correlation = 0.000072

The file is fully encrypted from byte zero with no recognisable magic, header, or metadata prefix. Not merely compressed (chi-square and serial correlation confirm encryption). The _ENCRYPT suffix in the filename confirms this is intentional. Without the decryption key (likely held only in the printer’s secure bootloader), the firmware contents remain opaque.

USB avrdude (fallback path)

A USB flashing path using the PrusaSlicer fork of avrdude (STK500v1/v2 protocol) is also available. This includes a custom Anker bootloader handshake (anker_init_external_flash()). No integrity checks were found here either.

Aspect Finding
────────────────────────── ──────────────────────────────────────
Update check transport HTTPS (TLS)
Download URL Dynamic (from server response)
Firmware signature check *** NOT FOUND ***
forceUpgrade flag Present (server can mandate silent update)
USB flash verification NOT FOUND
MQTT OTA trigger Present (server can push OTA)

9. P2P Protocol and Network Architecture

PPCS / CS2 P2P SDK

The app uses the CS2 P2P SDK (Cloud Smart Service, namespace cs2p2p, protocol name PPPP) for direct device communication. This is distinct from TUTK/Kalay. The Windows DLL (PPCS_API.dll) was built 2024-08-07.

Connection flow: LAN discovery via UDP broadcast (SSD@cs2-network.), then UDP hole punch attempt, falling back to TCP relay if needed. Relay server addresses are fetched at runtime from /v1/app/equipment/get_dsk_keys.

Two encryption layers

  1. PPPP protocol level: proprietary encrypt/decrypt with CRC on raw packets
  2. Application level: per-session ECDH key exchange (P-256), then AES-CBC with random IV per message

Video and file transfer are mutually exclusive

The P2P handshake is JSON-based: the app sends {"cmd":"handshake", "timeout":"10", "timestamp":"...", "taskId":"..."}, receives an ack from the printer, verifies the remote address by MAC (7c:e9:13:6b:d8:40) and IP, pings it (RTT ~1ms, TTL 64 confirming Linux), then sends a handshake_result. The entire handshake completes in ~150ms over LAN.

The P2P channel can carry either video or file data, but not both simultaneously. Log strings confirm: “p2p file is transfering, can’t open p2p video” and “video closed, ok to use p2p transfer file for print”.

Network architecture

The app hardcodes only a single bootstrap URL per region (/v1/slicer/get_net) which returns live hostnames for everything else (MQTT brokers, WebSocket, P2P relay servers). Production endpoints span *.ankermake.com (US/EU) and *.eufymake.com.cn (China). Staging/QA environments use *.mkitreal.com and *.eufylife.com.

MQTT brokers run on port 8789 (TLS, non-standard) at make-mqtt.ankermake.com (US) and make-mqtt-eu.ankermake.com (EU). The port was confirmed via netstat -ano during a live session; the commonly assumed 8883 is not used.

A full nmap scan (-sV -p-) of the printer at 192.168.1.118 (MAC 7C:E9:13:6B:D8:40) reveals only a single open TCP port: port 53 running dnsmasq 2.85. All other 65534 TCP ports are closed. This means all control traffic (P2P/PPCS) uses UDP, and MQTT is an outbound connection from the printer to the cloud broker. The dnsmasq instance likely serves the printer’s AP mode (direct WiFi connection).

MQTT protocol details

MQTT authentication was captured by hooking the Paho MQTT synchronous client library (paho-mqtt3cs.dll) via cdb.exe breakpoints on MQTTClient_create and MQTTClient_connect:

Field Value
──────────────── ──────────────────────────────────────────────────────
Broker ssl://make-mqtt.ankermake.com:8789
Username eufy_{user_id}
Password Account email (URL-decoded from login_info.json)
Client ID pc_windows_AnkerMakeStudio_direct_{user_id}_{hex}_{ts}
Keepalive 1 second
Connect options Paho MQTC v8, SSL options MQTS v1

All MQTT payloads are AES-256-GCM encrypted on the wire using the secret_key from device_list.json and the hardcoded IV 3DPrintAnker. Wire format: 4-byte big-endian length + 16-byte GCM tag + ciphertext.

Dual-channel architecture

Every command is sent on both MQTT and P2P simultaneously. Responses arrive on both channels. This provides redundancy: if the cloud broker is down, P2P over LAN still works, and vice versa. The app subscribes to five topic patterns on connect: /phone/{user_id}/{station_sn} (device to app), /device/{station_sn} (app to device), /server/{station_sn} (server to device), plus wildcards /{station_sn}/# and /phone/{user_id}/#.

Messages on both channels carry correlation IDs for matching requests to responses. MQTT uses a numeric sequence ID after the station serial number. P2P uses a compound format: {commandType}_{seqId}_{counter}, e.g. 1027_31436_1. The sequence IDs match between request and response on the same channel.

Captured command types

Cmd Dir Purpose
────── ──────── ──────────────────────────────────────────────────
1000 <= Printer status (0=idle, 7=moving, 8=ready)
1002 => / <= Firmware version check and OTA update info
1027 => / <= Device query
1064 => / <= Calibration (print_cail, zero_cail, ai_print_test)
1068 <= Print job history (per-job ink consumption, project IDs)
1100 => / <= Ink levels (leftInk in 1/100ths of %, e.g. 8833 = 88.33%)
1101 => / <= Ink status query (timeout: 25s)
1102 => / <= Bed scanning (timeout: 900s)
1104 => / <= Pre-print check (plate, humidity, device status)
1105 => / <= Snapshot/photo progress (height, plate dims, 0-100%)
1116 => / <= Focus capture/calibration (timeout: 1500s)
1118 <= Multi-status response (error codes, state updates)
1121 <= Print scaling/sizing response
1122 => / <= Print transformation (rotate/flip) (timeout: 1920s)
1128 <= Heartbeat config (interval 150s)
1131 => / <= Calibration file check (cail_file:1)
1132 => / <= Auto-measure height (timeout: 180s)
1133 => / <= UV light control (light:0/1, light_level:0-2)
1135 => / <= Bed scanning related
1136 => / <= Distance measurement (timeout: 1920s)
1139 => / <= Height measurement / Z-axis detect (timeout: 1920s)
1142 => Z-axis move
1143 => Head/carriage move
1144 <= Operation timeouts per command type
1152 <= Structure/state data (internal)
1153 <= Project metadata fetch
1154 => / <= Maintenance counter (times/totalTimes:700, warning)
1156 <= AP/WiFi status (AKNMT_CMD_AP_STATUS)
1158 <= Configuration data
1159 <= Feature flags (encryption, p2pencryption, aiPrintTest,
cameraCaliPause, assistedShot, customInkMode,
maintenanceLogData, lowPower)
1160 <= Network/DNS related
1163 <= Status validation
1165 <= Device capability response
1171 <= Unknown (value:0)
1172 => / <= Operation completion callback (timeout: 1500s)
1183 <= Data response (filtered by app)
100000 internal P2P file transfer progress (TakePhotoObserver)
100006 internal Message channel notify (connection events)
100007 internal Message channel notify (connection events)

Print job workflow

When starting a print, the app creates a tar archive containing camera calibration data and sends it to the printer via P2P. The tar is not encrypted and contains:

  • plate.jpg – camera snapshot of the print bed with fiducial markers for alignment
  • params.txt – stereo camera intrinsic calibration (focal length, distortion, field of view)
  • ex_param.txt – extrinsic parameters (rotation/translation between cameras)
  • large_patten_homography.yaml – OpenCV homography matrix for camera-to-printer coordinate transform

The printer stores this at /usr/data/p2p/tmp/autofill_{task_id}.tar, confirming the printer runs Linux. The actual print raster data is sent separately via the P2P channel with encryption:1. The print completion message includes the print height and the tar file path on the printer’s filesystem.

Snapshot and height measurement

CommandType 1105 serves double duty: it initiates a snapshot/photo capture and reports progress. The request includes encryption:1, net_mode:1, and a task_id. Progress responses stream back on both MQTT and P2P simultaneously with fields including plate_print_width:335, plate_print_height:420 (the print bed dimensions in mm), and a progress percentage (0-100). At step:1, the response includes a height field with the measured object height in mm (e.g. 0.546). The photo data itself is transferred via P2P file transfer (commandType 100000), which reports its own progress through TakePhotoObserver.

Standalone MQTT client

Using the captured authentication parameters, a standalone Python MQTT client was built that successfully connects to the broker, subscribes to device topics, and can send commands to the printer. This confirms the protocol is fully understood and replayable without the official app.

Ink cartridge MQTT data

CommandType 1100 returns detailed ink cartridge telemetry. The E1 uses a 6-ink system: Cyan, Magenta, Yellow, Black, White, and Gloss.

Ink System: 6 cartridges (C, M, Y, K, W, G)
Status: per-cartridge valid/expired flags
Levels: in 1/100ths of percent (e.g. 8833 = 88.33% remaining)
example: [8833, 8766, 8837, 8717, 7801, 7920]
Serial Numbers: per-cartridge SNs (format: AR48xxxx...)
Manufacture: unix timestamps per cartridge
Expiration: unix timestamps per cartridge
Dist. Expiry: days until expiration per cartridge (e.g. 173, 174, ...)
Waste Ink:
Tank count: 1
Level: in 1/100ths of percent (e.g. 3616 = 36.16% full)
Expired: 0/1 flag
Dist. Expiry: days until expiration (e.g. 443)
Print History (commandType 1068):
ink_quantity: per-job ink consumption [C, M, Y, K, W, G]
e.g. [0.0016, 0.0177, 0.0169, 0.0068, 0, 0]
jobSize: {width: 42, height: 70, unit: "mm"}
material: ink type code (6 = standard)

Each cartridge has its own serial number, manufacture date, and expiration timestamp. The app checks the valid and expired arrays before allowing prints. This is the ink DRM enforcement point over MQTT. cartridges that fail validation are rejected by the printer firmware.

Error codes

The pre-print check (commandType 1104) returns error codes as hex strings when conditions aren’t met:

Error Code Meaning
─────────────────── ──────────────────────────────────────────
0xFD01110007 Humidity out of range (requires 20%-85%)
The printer refuses to start if ambient
humidity is below 20% or above 85%.
Response format:
{"step":1, "status":1, "errorCode":["0xFD01110007"]}
Success format:
{"step":1, "status":0, "errorCode":[]}

The errorCode field is an array, suggesting multiple errors can be reported simultaneously. The 1104 request includes check_type_ext with individual flags for dev_status, plate_status, object_status, and dtg_platform_status.

State machines

The app uses named state machines tracked in the console log, keyed by printer serial number:

State Machine Events Observed
────────────────────── ────────────────────────────────────────
PRINT_TEST:{sn} E_PRTEST_DETECT_PLATE_PASS
ZERO_POINT:{sn} ZERO_DETECT_PLATE_PASS
ZERO_CALIBRATE_FILE_CHECK_PASS
Printer States (commandType 1000):
state=0 idle
state=7 moving/busy (axis move in progress)
state=8 ready to print (pre-check passed)

The ZERO_POINT machine handles the initial calibration/homing sequence. PRINT_TEST handles the print readiness checks. Both must pass before a print job can start. The ext.maintainable flag in status responses indicates whether maintenance operations (head cleaning, nozzle check) are available in the current state.

Stereo camera system

The E1 has a dual-camera stereo vision system used for object detection, height measurement, and plate alignment. Camera calibration data is sent to the printer as part of the autofill tar before each print.

Intrinsic parameters (params.txt)

Two cameras (index 0 and 1) form a stereo pair for depth/height measurement:

Camera Serial: redacted (format: AKCxxx...)
Camera 0 (primary):
Focal length: fx=1503.6 fy=1502.8 (pixels)
Principal point: cx=1588.5 cy=1232.1 (pixels)
Distortion: rational model (k1-k6, p1, p2)
k1=0.0655 k2=-0.0167 k3=-0.0039
k4=0.0905 k5=-0.0342 k6=-0.0057
p1=-0.0007 p2=0.0001
RMS error: 0.418 pixels
Max reproj error: 0.459 pixels
Field of view: 97.8° horizontal, 79.0° vertical
Camera 1 (secondary):
Focal length: fx=1496.2 fy=1495.0 (pixels)
Principal point: cx=1590.6 cy=1218.8 (pixels)
Distortion: simplified model (k4-k6 = 0)
RMS error: 0.373 pixels
Field of view: 98.0° horizontal, 79.2° vertical

The ~98° horizontal FoV and ~1500px focal length suggest a wide-angle lens on a ~3MP sensor (3200×2400 implied by principal point position). Camera 0 uses the full rational distortion model with 6 radial coefficients (k1-k6), while camera 1 uses a simpler 3-coefficient model (k4-k6 forced to zero), suggesting different lenses or that the secondary camera’s barrel distortion is simpler.

Extrinsic parameters (ex_param.txt)

Stereo baseline (camera 0 → camera 1):
Rotation: rx=-0.00982 ry=0.00220 rz=-0.00063 (radians)
Translation: tx=-0.121m ty=-0.029m tz=0.210m
The ~12cm horizontal and ~21cm depth offset defines the stereo
baseline. The small rotation angles confirm the cameras are
nearly parallel-mounted.
Note: rotMat field appears truncated/buggy (same value as tz)

Homography matrix (large_patten_homography.yaml)

OpenCV 3x3 homography (camera pixels → printer bed coordinates):
| 1.4019 0.0046 -339.62 |
| 0.0089 1.4033 -461.32 |
| 0.0000 0.0000 1.00 |
Scale factor: ~1.4x (camera pixels to printer units)
Translation: (-340, -461) accounts for camera offset from
print area origin
Near-zero off-diagonal: cameras are well-aligned with bed axes

The printer setup prints a calibration plate which has typical OpenCV fiducial checkerboard markers at the corners and a QR code on the bottom right, it then uses the camera to calibrate against this on initial setup (and you can do it again later).

During calibration, the plate.jpg snapshot captures these markers, enabling the printer to calculate exact positioning.

The object’s height is calculated on the snapshot command (reported as height in mm in the 1105 completion message). here i have an m2 macbook i am going to print on, I haven’t changed out the calibration sheet because i am using it for testing.

This is what is in the tar (f.lux altered my screenshot) that i captured.

Operation timeouts

The printer reports per-command timeouts via commandType 1144:

Command Timeout Purpose
───────── ────────── ────────────────────────
1105 600s Print job (10 min)
1132 180s Unknown (3 min)
1131 60s Calibration file check
1101 25s Unknown
1102 900s Unknown (15 min)
1116 1500s Unknown (25 min)
1136 1920s Unknown (32 min)
1139 1920s Unknown (32 min)
1122 1920s Unknown (32 min)
1104 185s Pre-print check (~3 min)
1172 1500s Unknown (25 min)
1064 sub-commands:
print_cail 960s Print calibration (16 min)
print_test 600s Print test (10 min)
zero_cail 120s Zero calibration (2 min)
ai_print_test 720s AI print test (12 min)

Maintenance counter

CommandType 1154 tracks head maintenance cycles. The response includes times (current count), totalTimes (threshold, 700 cycles), and isWarning. Once times approaches totalTimes, the app prompts for head cleaning. The progress field tracks ongoing maintenance operations (0-100%).

Feature flags

CommandType 1159 reports runtime feature flags: encryption:1 (payload encryption enabled), p2pencryption:1 (P2P channel encrypted), aiPrintTest:1 (AI-assisted print test available). These flags could potentially be toggled server-side to enable or disable features per device.


10. Hooking Techniques and Rosetta 2 Limitations

To capture API traffic in plaintext, I used DYLD_INSERT_LIBRARIES to inject a hook dylib that intercepts OpenSSL AES and ECDH functions. This was the only approach that worked. Five other approaches failed.

What worked: DYLD_INSERT_LIBRARIES + __interpose

Hooking AES_cbc_encrypt, AES_set_encrypt_key, AES_set_decrypt_key, and ECDH_compute_key from libcrypto.1.1.dylib via __interpose sections. This captures all API request/response JSON in plaintext, AES-128 session keys, and share_key.ini decryption operations. Typical session: 3000+ lines of captured data.

What failed and why

Approach Failure Reason
─────────────────────────── ──────────────────────────────────────────────
EVP hooks (GCM capture) Crashes CEF helper subprocesses - EVP called
by BoringSSL during helper startup
weak_import + __interpose dyld dereferences NULL during interpose table
setup when symbol absent in helper processes
Separate dylib via dlopen macOS dyld only processes __interpose sections
for images loaded at startup; dlopen'd ignored
Inline code patching Rosetta 2 AOT cache - vm_protect returns
(x86_64 absolute jump) KERN_PROTECTION_FAILURE on code pages; patched
x86_64 bytes invisible to ARM64 translation
GOT/PLT patching Not applicable - FileEncrypt functions are
internal to libAnkerPlugin.dylib, using direct
relative jumps

CEF helper complications

eufyMake Studio spawns 7+ CEF helper subprocesses (GPU, Renderer, etc.) that all inherit DYLD_INSERT_LIBRARIES. Each helper has its own .app bundle, so @executable_path resolves differently. Absolute paths must be used. Helpers don’t load libAnkerPlugin, so any hook referencing those symbols crashes them.

Windows: cdb.exe (WinDbg CLI) breakpoint hooking

On Windows, i used cdb.exe, the command-line version of WinDbg that ships with the Windows SDK/WinDbg Preview package.

C:\Program Files\WindowsApps\Microsoft.WinDbg_1.2601.12001.0_x64__8wekyb3d8bbwe\amd64\cdb.exe

The basic workflow is: write a script file with breakpoint commands, launch the app under cdb with -cf script.txt, and let it run. cdb breaks at process start, executes the script, then continues. The output is either written to a log file with -logo or captured via shell redirection. Every script follows the same pattern: wait for a DLL to load, set breakpoints on its functions, then let the app run normally.

The sxe ld: pattern for delay-loaded DLLs

The biggest problem with hooking eufyMake is that most of the interesting DLLs are delay-loaded. AnkerPlugin.dll, paho-mqtt3cs.dll, libcrypto-1_1-x64.dll, and eufyStudio.dll all load well after process startup, so you can’t just set breakpoints at the initial break. WinDbg’s bu (deferred breakpoint) command is supposed to handle this, but it was unreliable for several of these modules. The approach that actually worked was sxe ld:, which tells cdb to break when a specific module loads:

sxe ld:paho-mqtt3cs $$ break when paho DLL loads
g $$ continue until module load event fires
bp paho_mqtt3cs!MQTTClient_connect ".echo ===CONNECT===; da poi(@rdx+20); gc"
g $$ now continue with breakpoints active

When you need to hook functions in two different DLLs that load at different times, you chain the sxe ld: events. For the EMPF decryption trace, i needed breakpoints in both eufyStudio.dll and AnkerPlugin.dll:

sxe ld:eufyStudio
g
bp eufyStudio+0xF9A5C0 ".echo ===HANDLE_EUFYMAKE_DECRYPTION===; gc"
bp eufyStudio+0xE47430 ".echo ===ENCRYPT_CHECK===; gc"
sxe ld:AnkerPlugin
g
bp AnkerPlugin+0xBCD0 ".echo ===DECRYPT_CONTENT===; gc"
bp AnkerPlugin+0xB4A0 ".echo ===GCMDECRYPT===; gc"
g

From IDA decompilation to breakpoints

The whole process starts with IDA. I decompiled all three main binaries: eufymake studio.exe (218 MB of C output), eufyStudio.dll (217 MB), and AnkerPlugin.dll (3.7 MB, 131K lines). The smaller AnkerPlugin is where most of the crypto lives, so it’s the most useful.

IDA’s decompiler gives you function addresses in the form sub_18000XXXX, where 0x180000000 is the default image base for 64-bit DLLs. To get the RVA (relative virtual address) for a cdb breakpoint, you subtract the base: sub_18000BCD0 becomes AnkerPlugin+0xBCD0. cdb adds the module’s actual load address at runtime, so ASLR doesn’t matter.

The function signatures tell you which registers hold which arguments. For example, sub_18000BCD0 decompiles to:

// IDA decompilation:
char __fastcall sub_18000BCD0(_QWORD *a1, __int64 a2, _QWORD *a3)
// a1 = RCX = content (std::string*)
// a2 = RDX = output buffer
// a3 = R8 = key (std::string*)
// the log string inside confirms the name:
v7 = "FileEncrypt::AnkerDecryptContent";

So for this function, RCX has the ciphertext content, R8 has the decryption key, and both are MSVC std::string pointers. The embedded log strings are a gift from the developers. They tell you not only the function name but often the class hierarchy: FileEncrypt::AESGCMDecrypt, FileEncrypt::AnkerDecryptContent, FileEncrypt::AnkerDecryptFile. These form a clear call chain: file-level wrapper calls content parser calls low-level GCM.

Similarly, the share_key.ini decryption involved three functions: sub_180041DE0 loads the stored encrypted key, sub_18002FCB0 derives an AES key by SHA256-hashing the concatenation of salt + identity + MachineGuid, and sub_18002DB40 does the AES-CBC decryption. I found these by searching the decompiled output for string literals like “share_key” and “identity”, then tracing the call chain outward.

For functions without exported symbols, the module+RVA syntax is the only option. For DLLs with exports (like paho-mqtt3cs.dll), you can use the much friendlier module!FunctionName syntax: bp paho_mqtt3cs!MQTTClient_connect. No IDA needed for those.

Breakpoint actions: reading x64 arguments and MSVC strings

Each breakpoint has an inline action string that dumps register contents and continues (gc = go from conditional breakpoint). On x64, the first four arguments land in RCX, RDX, R8, R9, and the rest go on the stack. The tricky bit is reading MSVC’s std::string layout: if the capacity (+0x18) is less than 0x10, the string data is stored inline in the object (small string optimisation). Otherwise, the first qword is a pointer to heap-allocated data. Most of the breakpoint scripts have to handle both cases:

$$ read key from R8 (std::string*), handling small string optimisation
.if (poi(@r8+18) < 0x10) { da @r8 L!30 } .else { da poi(@r8) L!30 }

For the share_key.ini decryption, the breakpoints targeted three functions at known RVAs in AnkerPlugin.dll: the SHA256 key derivation at +0x2FCB0 (which concatenates salt, identity, and MachineGuid), the AES-CBC decrypt at +0x2DB40, and the storage key loader at +0x41DE0. The AES breakpoint dumps the key, IV, and ciphertext all in one hit:

bp AnkerPlugin+0x2db40 ".echo ===AES_CBC_DECRYPT===;
.echo key_r8:;
.if (poi(@r8+18) < 0x10) { da @r8 L!30 } .else { da poi(@r8) L!30 };
.echo iv_r9:;
.if (poi(@r9+18) < 0x10) { da @r9 L!30 } .else { da poi(@r9) L!30 };
.echo ct_data:;
db poi(@rdx) L(poi(@rdx+8)-poi(@rdx));
gc"

That L(poi(@rdx+8)-poi(@rdx)) is reading the length from a std::vector (end minus begin pointer). It dumps the exact number of ciphertext bytes so you can replay the decryption offline.

Capturing MQTT credentials

For the Paho MQTT library, the connect options are passed in a struct at RDX with a documented layout: the username pointer lives at +0x20 and the password pointer at +0x28. I also dumped the first 192 bytes of the struct raw, which revealed the header magic (MQTC), the version byte (8), the keepalive value, and the SSL options pointer. The struct format matches Paho’s MQTTClient_connectOptions v8 exactly:

bp paho_mqtt3cs!MQTTClient_connect ".echo ===MQTT_CONNECT===;
.echo --username--;
.if (poi(@rdx+20) != 0) { da poi(@rdx+20) } .else { .echo NULL };
.echo --password--;
.if (poi(@rdx+28) != 0) { da poi(@rdx+28) } .else { .echo NULL };
.echo --keepalive--;
dd @rdx+0c L1;
.echo --full_struct_first_192_bytes--;
db @rdx L0C0;
gc"

This gave us the MQTT username (eufy_{user_id}), password (the account email address), client ID format, keepalive of 1 second, and confirmed the SSL options struct. That was enough to write a standalone Python MQTT client that connects to the broker independently.

Capturing log encryption keys at runtime

For the encrypted log files, the AES-128-CBC key and IV are generated once per session and stored as globals in AnkerPlugin.dll at fixed offsets (+0xD5C90 for the key, +0xD5CA0 for the IV). The cleanest approach was to break on the first call to the per-record encrypt function (+0x21D00), dump the key and IV from the known global addresses, then remove all breakpoints and let the app run undisturbed:

bp AnkerPlugin+0x21D00 ".echo ===LOG_AES_KEY_CAPTURED===;
.echo key:; db AnkerPlugin+0xD5C90 L10;
.echo iv:; db AnkerPlugin+0xD5CA0 L10;
bc *; g"

The bc * clears all breakpoints after the first hit, so the app runs at full speed from that point. You grab the 16-byte key and IV from the output, feed them into a Python decrypt tool, and you can read the full session log. This is how i captured the 4500+ record log that contained all the MQTT traffic, ink telemetry, WebView bridge calls, and AI generation data.

As an alternative to the debugger, i also wrote a Python script that uses ReadProcessMemory via ctypes to grab the same 32 bytes from the running process, no debugger needed. It finds the eufyMake process by name, walks the module list with CreateToolhelp32Snapshot to find AnkerPlugin.dll’s base address, and reads the key material directly. Quicker if you just need the key and don’t care about other breakpoints.

The EMPF dead end that proved the answer

The most educational debugging session was the EMPF decryption hunt. I wrote progressively more elaborate cdb scripts trying to catch the EMPF file being decrypted. First i hooked every crypto API i could think of: BCryptDecrypt (Windows CNG), CryptDecrypt (legacy CryptoAPI), OpenSSL’s EVP_DecryptInit_ex, EVP_aes_256_gcm, EVP_aes_128_cbc, plus the known AnkerPlugin functions at their IDA-derived RVAs (+0xBCD0 for decrypt content, +0xB4A0 for AES-GCM, +0xCFF0 for file decrypt).

Then i opened a local EMPF file and… none of them fired. Not BCrypt, not CryptoAPI, not OpenSSL EVP, not the AnkerPlugin functions. Nothing. The file opened fine in the UI, but no native crypto code was involved. I went through five iterations of increasingly paranoid breakpoint scripts, checking if maybe i was missing a code path, or the file was cached, or the DLL was loaded under a different name. Each iteration was more comprehensive than the last, and each one came up empty.

That negative result was actually the key insight. If no native crypto API gets called when opening a local EMPF file, the decryption must be happening somewhere else entirely. Combined with the IDA decompilation showing that the native HandleEufymakeFileDecryption function was only called for cloud-downloaded files (it checks an is_encrypted flag in the EMPF header), it pointed directly at the WebView. The “Make It Real” editor runs in an embedded Chromium browser, and sure enough, the EMPF encryption turned out to use the WebCrypto API (window.crypto.subtle) in JavaScript, completely bypassing all native crypto libraries. No amount of native hooking would have caught it.

Two capture modes

One thing that caught me out: cdb’s -logo flag captures debugger output (breakpoint dumps, echo statements) to a file, but it does not capture the app’s own stdout. eufyMake Studio writes a lot of useful diagnostic output to the console, including all MQTT command types, P2P handshake data, and printer status updates in plaintext. To capture that, you need shell redirection instead:

$$ debugger output only (breakpoints):
cdb.exe -cf script.txt -logo breakpoints.log "eufymake studio.exe"
$$ everything including app console output:
cdb.exe -cf script.txt "eufymake studio.exe" > everything.log 2>&1

The second form is how i captured all the MQTT command types documented in the P2P section. The simplest possible cdb script for that is literally one line: g (go). Just run the app under a debugger with stdout redirected and it spills its guts.

cdb quirks and gotchas

  • bu (deferred breakpoints) did not resolve for some modules; sxe ld: + bp was more reliable
  • Module names replace hyphens with underscores (paho-mqtt3cs becomes paho_mqtt3cs in breakpoint expressions)
  • The -g flag (skip initial break) causes “No runnable debuggees” errors when combined with -cf scripts. Don’t use it. Let the script handle the initial break.
  • Breakpoint action strings need semicolons between commands and the whole thing has to be one line. The multiline examples above are for readability; the actual scripts are single-line monstrosities
  • Reading 5th+ function arguments on x64 requires pulling them off the stack: poi(@rsp+28) for the 5th arg, poi(@rsp+30) for the 6th, etc. The shadow space (32 bytes above RSP) is caller-allocated but may not be initialised yet at function entry
  • The .if conditional is essential for null-checking pointers before dereferencing. Without it, a single NULL pointer in any argument crashes the breakpoint action and halts the entire app
  • .symopt+0x10 at the top of a script enables SYMOPT_LOAD_LINES, which quiets some of the noisier symbol loading messages

Tracing the JavaScript layer

When the native cdb breakpoints on every crypto API failed to fire for EMPF decryption (as described above), attention shifted to the WebView. The “Make It Real” editor loads from files cached at %APPDATA%\eufyMake Studio Profile\cache\offline\web_res\versions\1.13.02\web\assets\. The main JavaScript bundle, index-DQRbPRws.js, is a ~14 MB Vite-bundled minified file containing the entire editor application.

Analysis was entirely static: grep through the minified source for keywords. Searching for “eufyMake” (the EMPF magic bytes) led to the FileEncryUtil class, which uses window.crypto.subtle.decrypt({name:"AES-GCM"}). Searching for “encrypt” and “decrypt” nearby revealed the key loading logic: a double-base64-encoded string that decodes to the offline key ab24ba760a896cd89eb9e15a9caec7fa. Searching for “Gtoken” and “md5” revealed the API auth scheme (MD5 of user_id). Searching for “ReliefType” mapped out the relief generation enum. Each discovery came from a targeted string search, not from trying to read the entire 14 MB file.

The minification actually helps in one way: Vite preserves string literals and object property names, so class names, API paths, error messages, and enum values are all searchable. Variable names are mangled to single letters (ur, sr, ar), but the surrounding context (string literals, property names, API URLs) makes it possible to reconstruct the logic. For the EMPF decryption, the relevant code was about 30 lines once extracted and reformatted. For the AI Hub API client, the endpoint list, header construction, and Gtoken generation were all found the same way.

Intercepting WebView2 traffic (HSTS bypass)

The WebView2 runtime uses the Edge/Chromium network stack, which means it has its own certificate store, HSTS preload list, and TLS verification independent of the Windows system settings. If you want to MITM the WebView’s traffic (e.g. to watch the AI Hub API calls live), you need to tell the Chromium engine to ignore certificate errors. WebView2 accepts Chromium command-line flags through the WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS environment variable:

set WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--ignore-certificate-errors --disable-features=RendererCodeIntegrity
"C:\Users\charlie\AppData\Local\eufyMake Studio\eufymake studio.exe"

The --ignore-certificate-errors flag disables all TLS verification including HSTS, certificate pinning, and CA validation, so a local proxy (mitmproxy, Fiddler, Burp) with its own CA certificate can intercept HTTPS traffic from the WebView. The --disable-features=RendererCodeIntegrity flag is sometimes needed on Windows to prevent the renderer from crashing when injected DLLs are present. You can also add --remote-debugging-port=9222 to get a full Chrome DevTools session on the embedded WebView, which lets you inspect the DOM, set JavaScript breakpoints, and watch network requests in real time.

Note that these flags only affect the WebView2 content. The native app’s own HTTPS calls (login, OTA checks, telemetry) go through Windows’ WinHTTP/Schannel stack and are not affected by Chromium flags. For those you’d need a system-level proxy with a trusted root CA installed.


11. Log File Encryption

The app generates encrypted diagnostic log files in %LOCALAPPDATA%\eufyMake Studio\Logs\. These are designed to be sent to Anker’s support team for remote debugging. The encryption scheme means users cannot read their own log files, but Anker can.

Encryption scheme

Each session generates a random AES-128-CBC key and IV using a Mersenne Twister PRNG (sub_180021F10 in AnkerPlugin.dll). The key and IV are then RSA-2048 encrypted with a hardcoded public key and written as a 264-byte header at the start of the log file. Only Anker holds the corresponding RSA private key.

The file format is straightforward:

file header (if present):
EF 5C 00 01 magic (4 bytes)
[260 bytes] RSA-2048 encrypted key block
per-record:
F1 C0 marker (2 bytes)
[2 bytes BE] flags
[4 bytes BE] encrypted payload length
[N bytes] AES-128-CBC ciphertext (PKCS7 padded)

Every log line is individually encrypted as its own AES-CBC record. The same key and IV are reused for every record in the session, which is a CBC-mode misuse (the IV should be unique per encryption operation), though in practice it just means identical log lines produce identical ciphertext.

Bypass attempt

There’s a bypass file check in eufyStudio.dll (sub_180AEE8D0): if the file C:\eufyMake_enclog_close exists, the RSA key header is skipped. However, AES encryption still runs with a randomly generated key that is never saved anywhere, making the logs unrecoverable. The bypass is essentially useless for actually reading logs.

Reading logs anyway

Since the AES key and IV are stored as globals in AnkerPlugin.dll at known offsets (+0xD5C90 for key, +0xD5CA0 for IV), they can be grabbed from process memory at runtime using either a cdb breakpoint or a standalone Python script with ReadProcessMemory (see the hooking section for details on both approaches).

This only works for the current session. Old log files from previous sessions have their keys locked inside the RSA-encrypted header, which only Anker’s server can decrypt. So the diagnostic logs are effectively write-only from the user’s perspective unless you capture the key at runtime.

RSA public key

The RSA-2048 public key used to encrypt the key block is embedded in AnkerPlugin.dll as six base64 strings stored at ascending RVAs but concatenated in reverse order by the code. Tracing the string concatenation chain in the decompiled code was needed to get the correct line ordering. The extracted key is a standard 2048-bit RSA public key in PKCS#8 PEM format.

What the logs contain

Decrypted logs are verbose diagnostic output with timestamps, thread IDs, source file names, and function names. They contain:

  • Full system info: OS version, CPU model, GPU model, RAM, MAC address, Windows MachineGuid
  • Auth tokens, user IDs, email addresses sent in plaintext to the embedded WebView via window.anker_msg.receiveMessageFromClient()
  • MQTT client configuration: broker URI, client ID format, certificate paths, Paho library version
  • API responses including the cloud EMPF encryption key list from /web/editor/user/get_encrypt_key_list
  • HTTP security shared keys with identity hashes for “US_AnkerMake” and “US_MakeItReal” environments
  • A Google Maps API key (geo_key) passed in login data, though testing shows it has no APIs enabled (all requests return REQUEST_DENIED)
  • Global configuration parameters: encryption flags, ink volume settings, maintenance firmware version, P2P settings
  • Digital signature verification results for the main executable

The encryption here protects Anker’s diagnostic data from users, not user data from attackers. If the logs were sent unencrypted, users could see exactly what telemetry the app collects. The RSA scheme ensures only Anker can read them.


12. AI Hub and Relief Generation

The “Make It Real” design editor runs entirely in a WebView (WebView2 on Windows, CEF on macOS), loading from https://makeitreal-beta.eufymake.com/. All AI features are server-side, with the WebView making API calls directly to aiot-api-us.ankermake.com (US) or aiot-api-eu.ankermake.com (EU). The native C++ app only injects HTTP auth headers into the WebView; the actual AI API calls never pass through the native code, so they don’t appear in the encrypted app logs.

AI Hub API endpoints

The JavaScript bundle (index-DQRbPRws.js) reveals a comprehensive AI feature set, all behind a credit system (/web/aicredit/get_ai_credit):

Feature Endpoints (base: aiot-api-us.ankermake.com)
────────────────── ──────────────────────────────────────────────────
Text to Image /web/aihub/text2image/create_task, get_task_status
Style Transfer /web/aihub/styletransfer/create_task, get_task_status
Style Transfer v2 /web/aihub/styletransfer2/create_task
Face Swap /web/aihub/faceswap/create_task, get_task_status
Pet Portrait /web/aihub/petportrait/create_task, get_task_status
Background Remove /web/aihub/removebg/create_task, get_task_status, stop_task
Upscale /web/aihub/upscale/create_task, get_task_status, stop_task
Training /web/aihub/train/create_task, get_task_status, get_upload_token
Common /web/aihub/common/get_history_list, get_task_status, repeat_task
Credits /web/aicredit/get_ai_credit, get_ai_credit_consume_info

All endpoints use POST requests with the user’s auth_token header plus a Gtoken header. The Gtoken is simply MD5(user_id), generated by the js-md5 library in the JS bundle. Without both headers, the API returns 401 “token error”. Standard headers also include App-Name: makeitreal, Model_type: PC, and Openudid. The task pattern is async: create_task returns a task ID, then the client polls get_task_status until completion. During a test session, the polling ran for approximately 4 minutes for a single relief generation.

Credit system

AI features run on a credit system. Each account gets a pool of credits (new accounts appear to get ~600), and each generation costs between 1 and 10 credits depending on the tool. Using a Python script to query the API directly, the per-tool costs break down as:

Cost Tool types (ai_tool_type IDs)
──── ────────────────────────────────────────────
0 1, 2, 3, 10000 (free tier / internal)
1 6, 7, 9, 10 (removebg, upscale, simple ops)
5 4, 5, 8, 11, 12, 16, 30, 33-37 (generation, style transfer, face swap)
10 50 (model training)

Generated results are stored on AWS CloudFront (d3qv8e0rx0ln72.cloudfront.net) with S3 pre-signed URLs that expire after 7 days. The history endpoint returns both full-resolution download URLs and thumbnail URLs. Task IDs encode the type: ag_ for AI generation, ti_ for text-to-image, us_ for upscale.

Relief and raised print system

The E1’s signature feature is raised/textured UV printing, where ink layers are built up to create physical height. The JS defines a ReliefType enum controlling the generation mode:

Value Name Description
───── ────────────────────────── ──────────────────────────
-1 print_relief_type_def Default (none)
0 print_relief_type_rel Basic relief
1 print_relief_type_texture Texture only
2 print_relief_type_textureRel Texture + relief combined
3 print_relief_type_brush_strokes Brush stroke effect
4 print_relief_type_poster Poster style
5 print_relief_type_stick Sticker
6 print_relief_type_gild Gilding effect
7 print_relief_raised Full raised print

The relief generation pipeline uses styletransfer/create_task or styletransfer2/create_task to generate the depth map server-side. The result is then processed client-side using WebGL shaders (shader_normalization_relief) to produce a grayscale height map. The depth map intensity is controlled by grayPercent, grayPercentMax, and grayPercentMin parameters, with optional contrast and invert adjustments. This grayscale map is then sent to the printer as part of the print data, where it controls how many ink layers are deposited at each pixel.

Other WebView features

The WebView also loads OpenCV (compiled to WASM via opencv.js.zip) for client-side image processing including camera calibration homography and image manipulation. The print data includes layer information (printLayerData) with layer types like PicColorInk, and the format supports split printing across multiple pieces. Print parameters include max_thickness (e.g. 0.6mm), gloss_thickness, ink mode (CMYK/Gloss/Gild), and quality settings.


13. Conclusions

App architecture summary

  • UI framework: native wxWidgets C++ application. macOS uses embedded CEF (Chromium) for web-based UI panels, Windows uses WebView2 (Edge/Chromium)
  • Main binary: ~300+ MB statically linked (macOS), 52 MB DLL (Windows uses Edge Embedded)
  • Networking: libAnkerNet.dylib (65 MB) handles video streaming (AV1, H.264/265, XviD, VP8/VP9), P2P, and MQTT
  • Encryption: libAnkerPlugin.dylib (2.9 MB) handles EMPF encryption and API security
  • Supports: FDM printing (AnkerMake M5/M5C), SLA printing, laser engraving, UV inkjet printing (eufyMake E1)
  • File types: STL, OBJ, AMF, 3MF, GCODE, EMPF
  • Custom URL scheme: eufystudio://
  • libpag.dll: PAG animation framework by Tencent (Windows)
  • Mesa fallback: ships Mesa 3D software renderer as a fallback for systems without GPU support
  • Installer: NSIS on Windows, standard .app bundle on macOS

Security concerns

  • Shared TLS key: every installation uses the same RSA-2048 private key for the local wss:// WebSocket (CN=localhost, valid 2025-2035). Any user could MITM another user’s local WebSocket on the same machine.
  • No firmware signature verification: combined with server-mandated forceUpgrade and MQTT triggers, this is a potential supply chain risk. (Firmware is encrypted)
  • Hardcoded encryption keys: all 4 localKeys and the password encryption server key are embedded in the JS bundle, making the entire API encryption layer transparent to anyone who reads the source.
  • Extensive telemetry with no opt-out UI: the BuryPoint system tracks nearly every user interaction, there might be an opt out but i didn’t see it.

Licence compliance summary

  • AGPL-3.0 violation: PrusaSlicer-derived slicer engine ships with no licence text. Source is published on GitHub but as periodic dumps, not continuous history.
  • GPL-2.0+/3.0+ violation: FFmpeg, x264, x265, XviD, CGAL, libigl, avrdude all ship with no licence text and no source code offer in the application.
  • LGPL violation: OpenCASCADE, wxWidgets, and NLopt are statically linked without providing object files for relinking.
  • Attribution failures: over 20 permissive-licence components ship with no copyright notices.
  • Zero licence files: both macOS and Windows distributions ship with no COPYING, LICENSE, or NOTICE files whatsoever.

Tools

All the tools written during this analysis are in the project’s tools/ directory:

  • empf_decrypt.py – decrypt and extract .empf files (offline key built in, supports custom keys for cloud files)
  • log_decrypt.py – decrypt encrypted app log files given the AES-128-CBC key and IV
  • dump_log_key.py – extract the current session’s log encryption key from process memory via ReadProcessMemory, no debugger needed. Can also decrypt the current log in one step with --decrypt
  • mqtt_client.py – standalone MQTT client with AES-256-GCM encrypt/decrypt. Connects to the broker, subscribes to device topics, and has command shortcuts (--ink, --status, --firmware, --light, etc.)
  • ai_hub_client.py – query the AI Hub API for credit balance, generation history, and credit costs. Uses the discovered Gtoken = MD5(user_id) authentication

The debug_scripts/ directory has ~25 cdb script files covering MQTT auth capture, EMPF decryption tracing, share_key.ini key derivation, log encryption key extraction, and the comprehensive crypto API sweep that ruled out native code for EMPF decryption. A PowerShell port scanner used for the nmap verification is also there.

What’s still unknown

  • Cloud EMPF key distribution – the offline key is hardcoded in JS, but cloud-transferred files use server-fetched keys via /web/editor/user/get_encrypt_key_list. The native code path in eufyStudio.dll may use different keys.
  • The protocol used by the printer firmware to communicate with ink cartridge identification chips. We don’t know if it’s I2C, SPI, or something else entirely. Waiting on an empty cartridge to investigate further.
  • Whether the Bytedance Starling endpoint is loaded dynamically at runtime in certain builds or regions (I’ve seen it in PiHole it might be the printer)

This analysis was conducted over a weekend using: binary string extraction and symbol demangling (328,078 symbol lines), MITM proxy captures (113 login requests), DYLD_INSERT_LIBRARIES hook injection on macOS (3,700+ lines of captured API traffic), cdb.exe breakpoint hooking on Windows (Paho MQTT auth capture, crypto API tracing), webpack JS bundle analysis (4.7 MB minified bundle from makeitreal-beta.eufymake.com), IDA Pro decompilation of eufymake studio.exe (54 MB) and eufyStudio.dll (52 MB), entropy analysis of firmware binary (337 MB), standalone MQTT client development and testing, Windows NSIS installer extraction, GitHub source code auditing, and process memory key extraction for encrypted log file decryption.

Methodology

There are things i’ve found that i’ve deliberately left out of this write-up. Some findings could make it too easy to do things i’d rather not enable, and others need more verification before i’m comfortable publishing them. If something seems like an obvious gap, it’s probably intentional.

Anyway, i’m going back to finishing building a gate to stop our escape artist dogs from trying to give me a heart attack, and smashing up concrete. See you at LayerOne conference on memorial day weekend in Pasadena ;).

“Main board” , motor drivers are under the heat sink, wifi maybe , power conditioning, head connection?

OMTECH Fibre Laser Welder unboxing

This is mostly just the PDI ( post delivery inspection )

I have been eyeing a fibre laser welder for a bit because as much fun as our combo plasma cutter/welder is its messy and you need some skills, laser welding has promises of being closer to soldering…

I shopped around for a bit and saw omtech had a 1500W maxphotonics source, our other laser is raycus who i soured a but with when i found out after a fault when it was new was a 30 day warranty. they are generally all chasing IPG anyway. I have an omtech red/black and i like it so i figured why not and i know they have drop ship close to us and have way built up their USA side since i bought that CO2.

Summer was great over at omtech ( their website needs some work,shopify pulls in all the trackers and brave/adblockers just have a field day with it check out the console logs!) but she helped out and was really responsive and helpful since i decide to just email direct.

Anyway the first thing to arrive via fedex was like 15kgs of304 stainless, then today the LTL guy showed up with a palette and a box larger than i was expecting, but isn’t it always the way. So far i have decrated it and checked it over as i figure out the easiest way of getting it off the crate into the garage. They have a nice diagram of how to decrate it, but its a bit wrong. The outside has a USA QC check but i am not sure, it looks chinese factory packed to me and theres cobwebs inside it, with all the bubble wrap. However it doesn’t have that usual china cling film smell so maybe it was unpacked in the US but the labels are Xhinese. Not that it matters to me as the Xhinese factories are excellent at packing though wrapping sensitive electronics in cling film i never understand even though its in a metal frame its not grounded but still its probably ok, so much ESD though during unwrap.

The where it was last packed is an interesting question since there were some QC issues.

The box, nicely packed.

Lots of cling film.

The first label i came across, 10/13/2025 ok thats a good time frame til now to get some spiders in there to make webs, idk much about what sort of spiders china has but its bad enough here with all the brown widows, but hey its not OZ so thats good.

Decrated and ready to get off the base…

A fun drawer

Gas struts to hold it up, bolts were loose no thread lock or such, no biggie

Other side ok

Some rust on the cylinder.

Ok pics of the insides

Back of the control panel

The seal around the lid was stuck to the top side, easy fix. jsut sitting a while i guess and cheap glue

the helmet , these are the basic types that just block the laser not really the auto dim etc type, the laser is still really bright so probably wont use this

it has the writing on it and everyone always says get the ones with the etched writing, as if fake laser protection cant be etched? get something with a traceable certificate chain like NoIR and thorlabs

Small QC issue the latch for the top of the units lid is meant to hold it down, but the mysterious ‘FC’ listed on the USA ‘QC’ didnt latch it properly and instead closed the lid with the latch on top of the tab instead of latched to it, so it doesn’t latch at all. another small issue but easy fix.

its hard to see but its bent out of shape so it cant latch and its honestly not a great design. i am not sure if the top metal rods are meant for anything other than looks but definitely dont try to lift it by the as the latch is poor if it was working, which on mine its not.

Luckily i have a fibre laser cnc cutter so i can remake it !

Said bars (also with some rust)

regulator, i am sure its fine but its something i would likely swap, my buddy doesnt care for high voltage, i dont care for high pressure

Wire feeder

Alarm buzzer for the chiller?

Second one of these i’ve spotted so far, please use a grommet/rubber protection and don’t let insulated wire rub up against a metal edge it will cut thru eventually, wear a rubber!

Industrially chilly

Apparently my camera phone doesnt like this text

Some random photos

Front Panel

Power!

Case fans

Cooler connections, this power cable rotates, will check to see if its meant too

Primary

Large axial

Second place and more important for that rubber grommet , this is the cable to the hand held laser part, so its going to get a lot of action up against that metal hole, it should be protected. The shroud will wear thru and also zipties…

Electrical insides

Not secured properly

Also not secured properly

the old hws50000 very common

If you have ever bought equipment from china, you have one of those yellow fishing tackle boxes, idk who makes them but they must be making a killing, i think i have 5 of them now

a box to hold the things that are in the bag

more eyewear

Stylish

This box has to be famous

a test

Eakins Camera XY Scanning

image

Scanner Project Update and Technical Details

(work in progress)

Recently, I’ve made the design and functionality more useable, automatic scans are working as expected and its neater.

A youtube short of it scanning

https://www.youtube.com/shorts/pULRlDODE48

OpenSeaDragon example scan PCB Test.

Script Repository

Access the scripts used in this project on GitHub.

The scanner’s recent changes include the integration of a controlled XY table with a TinyG board, interfaced with the camera via USB. This setup is based on ideas shared on my GitHub and in a previous blog post.

 

Image Rotation Correction

Due to alignment challenges, post-process image alignment and rotation correction are essential. Affinity Photo 2 works pretty well though lacks customisation.

FTP Daemon Configuration for Eakins Camera to download files after scan

/etc/inetd.conf

21 stream tcp nowait root ftpd ftpd -w /mnt/sdcard

After configuring, start the inetd service to enable FTP functionalities if you’ve added a USB ethernet.

Most of this is just simple scripts, the main scan script is  a busybox sh script. I process the resulting images on windows so my scripts are TCC (Take Command) usually, but most of the apps are available on other OS’s

Scripting and Processing Workflow

The backbone of this project is a series of scripts. The primary scanning script is developed in shell script (sh), optimized for Unix-like operating systems. My data processing is executed on Windows, where I employ TCC (Take Command) scripts, known for their robust command line capabilities and batch file compatibility. The scripts are on GitHub.

Basically they calculate a G0XnYnF100 command and echo it to the /dev/ttyUSB0 device which is a TinyG, that moves the microscope to that location, then it calls a second script which does the Z focus steps with the talk command. The scripts do need some optimisation on time and checking to see if the saved image appeared, it’d be nice if it renamed the file to the Z step on each row_col

Focus Stacking Solutions

For the task of focus stacking, Helicon Focus is a handy tool for combining multiple images at different focus points into a single, detailed composite. As an alternative, I also use a customized open-source tool from PetteriAimonen’s GitHub. My version adds a Visual Studio Solution (SLN) file and integrates vcpkg for seamless dependency management, alongside a feature to batch-process images from a directory, though i think the original already does what i added …. so i might have replicated that one…

Helicon can do the focus stacking but unless your inputs require no rotation its likely going to fail the stitch, so I use Affinity Photo 2 for that it works really well for me (though it failed during the test for the writeup) , it can focus stack too, Photo 2 uses a 3rd party library a few different packages use.

Image Processing with Affinity Photo 2

In scenarios where Helicon Focus might struggle, particularly with images that need rotation adjustments, Affinity Photo 2 works . It is able to do both Focus Stacking while correcting rotational misalignments, internally it utilises  a third-party library called AutoStitch that is common across several image processing tools. http://matthewalunbrown.com/autostitch/autostitch.html

Most stitching software is expecting the camera to rotate or a 360/spherical style and not where its just moving in one plane. PTGUI/Hugin should be able to hande these too. These types of stitchers are common in the medical field for processing microscope slides etc.

A tutorial for Hugin.

https://hugin.sourceforge.io/tutorials/scans/en.shtml

For more tailored image rotation needs, I have developed a specific batch rotation tool available at https://github.com/charlie-x/rotImage . This tool is just a simple OpenCV based batch rotation tool there are other much more comprehensive solutions like ImageMagick, which has a broad rage of image manipulation functionalities, including batch rotation.

Step-by-Step Guide to Executing a Focus Stack

To initiate a focus stack, first obtain the tool from GitHub. Build it yourself or download a pre-compiled release. Note that the command syntax differs if you’re using the original version.

After gathering all images in a local directory (e.g., o:\code\eakins\scan-5), create a designated output folder (e.g., out5\).


go.bat
for /A:D %i in (O:\code\eakins\scan-5\*.*) do call run %i out5\%@name[%i].jpg

run.bat
focus-stack.exe --global-align --output=%2 --input-folder=%1

This method will compile the images into a focus-stacked collection in the out5\ directory, achieving a level of detail similarly to the banner image of this post.

If you get something like

[ERROR:15@2.413] global ocl.cpp:3765 cv::ocl::Kernel::set OpenCL: Kernel(decompose_vertical)::set(arg_index=0, flags=258): can’t create cl_mem handle for passed UMat buffer (addr=000000B1986FF140)

just try re-running it , if it fails again check there aren’t images in the folder that can’t be stacked, wrong image location, poor contrast/lighting or try not using opencl

Stitch the Image

In Affinity Photo2 from File menu “New Stack” and add those files, select “Scale, rotate and translate”

image

This will take a while

Sometimes it just fails , I think this a lack of overlap and not enough details for the feature match, the golden ratio makes a visit. Make sure the lighting is good and there is enough overlap to properly stitch. correcting rotation can also help, but try it both ways first.

image

Some fun results.

image

Helicon Focus with the same data set made this, some issues here too

imageimage

Hmmm nulspacelibs ?

Some manual tweaking gets it closer, but affinity usually does a better job its just harder to tweak later.

image

Source Images

image

OpenSeadragon

If you want to use OpenSeaDragon process the stitched image with vips

vips dzsave pcb-test3.png pcb-test3

this will create a bunch of tiles in the pcb-test3 folder and a .dzi file

<Image xmlns=”http://schemas.microsoft.com/deepzoom/2008″
   Format=”jpeg”
   Overlap=”1″
   TileSize=”254″
   >
<Size
     Height=”12747″
     Width=”20219″
   />
</Image>


Add a script.js , make sure var name and tileSources match, set the Url to the location of the files you created with vips, change the TileSize and width/height to match the .dzi

  • var pcbtest3 = {
      Image: {
        xmlns: “http://schemas.microsoft.com/deepzoom/2008″,
        Url: “pcb-test3_files/”,
        Format: “jpeg”,
        Overlap: “1”,
        TileSize: “254”,
        Size: {
            Width: “20219”,
            Height: “12747”
        }
      }
    };
  • var viewer = OpenSeadragon({
      id: “seadragon-viewer”,
      prefixUrl: “//openseadragon.github.io/openseadragon/images/”,
      tileSources: pcbtest3
    });

Add a simple HTML file, include the scripts for OpenSeaDragon and the above script.js

  • <html>
      <head>
    <style>
          #seadragon-viewer {
            width: 100vw;   /* 100% of the viewport width */
            height: 100vh;  /* 100% of the viewport height */
          }
        </style>
      </head>
    <body style=”background-color: black;”>
            <div id=”seadragon-viewer”></div>
            //openseadragon.github.io/openseadragon/openseadragon.min.js
            http://script.js
  • </body>
    </html>

3D Depth

focus-stack can also generate depth maps and a simulated 3d output , not sure how much good it is is here. I have yet to try stitching the non focus stacked images, then stacking them, since that will make a depth map and 3d of the whole image, if we could output the translation matrix the stitcher used and then use these images over it, that might work instead. a photogrammetry tool like Reality Capture with images captured at different angles would do a better job here.

image

image

MiniWave ES15 screwdriver + KER toolkit 3d print.

Fusion 360 model

image

https://a360.co/3ZfWJPU “V18 – tested works” is the last tested printed version

image

image

image

this is V15 (Fusion 360 version), the third hole is a teensy bit off , pressure fit

image

image

Version 16+

Swap tray 1 to tray 8 (little bit of glue to over come) but you have to clip these sides a little bit to clear the magnetic locks. it is thin plastic so its easy to do.

image

Arrangement looks like this after the mod, so it removes the screwdriver+tweezers from the base KER only

image

Timelapse print

https://youtu.be/a2AgcULsrTs

image

image

The inner sketches for the holes are correct, i was using larger ones for test fits.

I moved the third hole in v17 to a 46mm from 46.3mm in v16

And just as I printed V16 which seems to work the best, Amazon delivered the PLA i wanted to use…. I guess V17 is getting a run then.

image

this is hatchbox yellow PLA, going to try microcenter in house brand inland gold pla as well, needs a little orange

image

“V18 – tested works”, is the current verified working version from the Fusion360 Link

This is the Inland Gold PLA at the bottommost  (warning inalnd uses cardboard reels)

image

The KER kit is sold as 

KER 128 in 1 Precision Screwdriver Set with Magnetic Driver Kit

I may make some small changes that should reflect in the A360.

Also its currently in slot 8, which is the extension and metal spatula, since slot 1 won’t fit. Will either make a new plate for slot 1 that lets the driver fit, or see if i can adjust so you can move slot 8 to Slot 1 instead

Bambu X1 carbon notes

not much yet as i only had the machine for a day, but keeping notes, as is the way.

Auditing these machines, since they can/are connected to a plain ip addresses in china, have unfettered local network access, no proxy settings, a closed ecosystem and could download and execute arbitrary code without permission and a *lot* of domains they can connect too

added more notes i have had in limbo since feb 2023.. time flies… 

FTP

the BambuLabs X1  Carbon currently uses FTP when the SD-CARD is installed, the username is bblc and the access code is  the password , which is stored in

C:\Users\%USERNAME%\AppData\Roaming\BambuStudio

BambuStudio.conf

It’s in the JSON file, under access_code

  • “access_code”: {
  • }

regular FTP port 21.

if you can’t find the access code, just wireshark with a tcp.port == 21 filter.

the access code appears other places too in the p1p, but i haven’t seen it on the x1c

BambuLabs say they are planning to remove the FTP connection, since they may want something either more secure or just go via their cloud (doesn’t seem to have happened )

RFID

the RFID is a mifare classic 1K and doesn’t use any of the std/extended keys, i guess dust off the proxmark time.  not susceptible to NACK

AMS RFID reader

https://www.fmsh.com/AjaxFile/DownLoadFile.aspx?FilePath=/UpLoadFile/20220804/FM17580_ps_web_2022.pdf&fileExt=file

https://github.com/ian688t6/FM175XXREADER/blob/master/mifare_card.c

3.3V signalling

image

The RFID boards are connected with a single  SPI connection with separate chip selects.

image

even after decrypt of the rfid to access the data in the rfid tag there is probably some more encryption/verification in the data stored on the card itself. (it might be pub/priv key signed , i wouldn’t put it past them they’re a very smart bunch at bambu, reached out to someone else to confirm if they’d seen any indications of it) it’d explain why it hasn’t been done yet, since there are lot of avenues to recover the keys. MITMing the SPI to emulate and add something wouldn’t work since the data is passed back as is. with current thinking of how the serial protocol works it’d be quite hard to do at that level too,and then the ams cpu would be out of sync… replacing the firmware in the ams cpu board would likely be required to add custom tags if this line of thought is valid. unless the private key is somehow recovered, if it is a private key.

note: CF 522 / CV520

AMS

24V top left of  connector , looking at rear of ams, cpu board + two rfid boards, and small power/data bus board for connection to hub and daisy chaining ( uart/rs485 3.3V signaling)

power board. 24V DC on right , bus on  left connects to the ams cpu board.

image

6 pin connector looking into the connector from rear of the AMS, pin1 top left

HUB connectors, top left is Pin1 (24V) longer pin at Pin2 is GND/VSS

image

24v,  gnd,       pair a –/+

pair a –/+, pair b –/+ pair b –/+

Pair B is just the state of magnetic sensor in the hub and is passed back to the AMS  as a multi node RS485 bus chain. It does not connect back to the printer ( the status is sent to the  AMS and the AMS can send likely it back to the X1 via pair A . The duty cycle of the signal is the position of the magnet basically.

image

Pair A is the primary half duplex communication line, its setup as a traditional multi node bus (or is it)

https://www.dfrobot.com/product-1492.html

https://www.lcsc.com/product-detail/Operational-Amplifier_TECH-PUBLIC-TPV358S8_C2923372.html

BUILDING THE SLICER

Building the app from github was a bit of a chore, build the deps first then the application (debug mode doesn’t seem to work with the networking plugin dll, will have to check into that a bit more, probably just an incompatibility with name mangle/that dll) the PrusaSlicer github page issues has a lot of notes on the build process.

openssl needs  a windows path / \ compatible perl to build. i just used one from vcpkg

Although the Slicer is open source (since its a fork of PrusaSlicer) the communications for the device  are in a separate library that does all the communications with the cloud service/machine. This is because this allows them to keep the network communications closed source,  the Camera works the same way.

NOTES

internal urls are :-

bambu:///machine?authkey=xxxxxxxxxx&passwd=xxxx&region=us

HARDWARE

back of the 4 way hub, i’ll add better photos.

I like the cute little VOID sticker, is it telling me it is void, it is a portal to another dimension perhaps?

Warranty void stickers are illegal in a lot of places (Definitely Germany and USA) magnusson moss act in the USA

https://www.ftc.gov/news-events/news/press-releases/2018/04/ftc-staff-warns-companies-it-illegal-condition-warranty-coverage-use-specified-parts-or-services

Rockchip RV1126

h5tq4g63efr 4Gb DDR3 SDRAM

KIOXIA  THGBMNG5D1LBAIL VD1216   2140KAE CHINA

https://www.digikey.com/en/products/detail/kioxia-america-inc/thgbmng5d1lbail/9841782

https://pdf1.alldatasheet.com/datasheet-pdf/view/1244282/TOSHIBA/THGBMNG5D1LBAIL.html

https://github.com/yunzhaoyu2050/rockchip_rv1126_rv1109_docs/blob/main/RV1126_RV1109/Rockchip_RV1126_RV1109_Quick_Start_Linux_EN.pdf

https://datasheet.lcsc.com/lcsc/2202131900_SK-HYNIX-H5TQ4G63EFR-RDC_C2803259.pdf

image

image

CPU’s used.

http://www.spintrol.com/index/index/product2/id/25.html

http://www.spintrol.com/index/index/product2/id/23.html

https://jlcpcb.com/partdetail/ZHONGKEWEI-AT8236/C2827823

https://www.alldatasheet.com/datasheet-pdf/pdf/1149566/CHIPSEA/CSU32P10.html

TI Version of the transceiver https://www.ti.com/product/SN75176B

https://www.arrow.com/en/products/tp75176e-sr/3peak

FM 1230 SC

2023-01-14 16_53_27-SharpCap (v4.0.9478, 64 bit) - USB CAMERA  - C__Users_charlie_Desktop_SharpCap C

2023-01-14 16_53_46-SharpCap (v4.0.9478, 64 bit) - USB CAMERA  - C__Users_charlie_Desktop_SharpCap C

3PEAK 75176 rs485 transceiver

https://datasheet.lcsc.com/lcsc/1811071710_3PEAK-TP75176E-SR_C94207.pdf

2023-01-14 16_55_14-SharpCap (v4.0.9478, 64 bit) - USB CAMERA  - C__Users_charlie_Desktop_SharpCap C

 2023-01-14 18_49_36-SharpCap (v4.0.9478, 64 bit) - USB CAMERA  - C__Users_charlie_Desktop_SharpCap C

F330G8BUH5468 Giga

https://www.gigadevice.com/products/microcontrollers/gd32/arm-cortex-m4/mainstream-line/gd32f303-series/

https://www.gigadevice.com/microcontroller/gd32f330g8u6/ 

2023-01-14 16_56_23-SharpCap (v4.0.9478, 64 bit) - USB CAMERA  - C__Users_charlie_Desktop_SharpCap C

the GD’s are popular because of supply chain issues, shorter lead times than the somewhat similar lines from ST Micro

CHIPSEA 32P

https://www.lcsc.com/product-detail/Microcontroller-Units-MCUs-MPUs-SOCs_CHIPSEA-CSU32P10-SOP8_C914988.html

2023-01-14 18_49_10-SharpCap (v4.0.9478, 64 bit) - USB CAMERA  - C__Users_charlie_Desktop_SharpCap C

hall effect

2023-01-14 16_53_46-SharpCap (v4.0.9478, 64 bit) - USB CAMERA  - C__Users_charlie_Desktop_SharpCap C

2023-01-14 18_50_17-SharpCap (v4.0.9478, 64 bit) - USB CAMERA  - C__Users_charlie_Desktop_SharpCap C

filament buffer ( ams hub replaces it)

image

image

image

image

filament displacement, this is spring loaded when the filament passes thru it changes the hall effect sensor state

image

image

image

image

the long magnets are setup to be horizontal , so N E S , so the sensor can determine which direction or no filament

Connectors

https://www.literature.molex.com/SQLImages/kelmscott/Molex/PDF_Images/987651-4661.pdf

Lidar

Haven’t confirmed it yet but the uploaded .3mf contains a 2d image of the first layer expectation this is probably used as the mask for the lidar/first layer inspection

image

April Tags  / ArUco

I’ll add code for the ArUco regen at some point

The codes the printer reads on the plates are 16H5’s 5mmx5mm :-

https://docs.opencv.org/4.x/d5/dae/tutorial_aruco_detection.html

detection   3:id (16x 5)-25  , hamming 2, margin   24.959 etc

1 border bit


examples

16h5a

BambuNetworkEngine.conf

Rijndael  json  with last machine, username , avatar link , userid/name and tokens.

OpenSSH

nlohman json

paho mqtt

spdlog

boost

curl

std::fmt

List of domains/ip’s

https://ipinfo.io/47.100.225.51

DevModel.bambu.com https://www.whois.com/whois/bambu.com

DevName.bambu.com

DevSignal.bambu.com

DevConnect.bambu.com

DevBind.bambu.com

bambu-lab.com https://www.whois.com/whois/bambu-lab.com

bambooolab.com https://www.whois.com/whois/bambooolab.com

portal.bambulab.com

cn.mqtt.bambulab.com

api.bambulab.com

us.mqtt.bambulab.com

pre.mqtt.bambu-lab.com

pre.us.bambu-lab.com

bambulab-demo.myshopify.com

portal-qa.bambulab.com

portal-dev.bambu-lab.com

portal-pre.bambu-lab.com

dev.mqtt.bambu-lab.com

pre_us.mqtt.bambu-lab.com

api-us-pre.bambu-lab.com

upgrade-file.bambulab.com

cdn.bambulab.com

BambuLabs X1 Carbon, P1P

EzCad .ezd parsing the pens

My notes on parsing the EZD Cad 2 format and accessing the pen information for regular and Q/MOPA types.

The key part is getBasePtr () + the offset

    1. base = (uint32_t*)(dataBuffer->data() + 0x160 );
    2. dataBuffer->data() + baseOffset + SPEED_MM_OFFSET

start of file + baseOffset + value you want to read/write

These are the offsets
this is a pointer to an offset where the pens/data so baseOffset+this
DATA_PENS_OFFSET ( 0x160 )

offset of the parameter name NAME_OFFSET ( 8UL )
this is the length of the first name of the .ezd i started with
NAME_LENGTH ( 0x10 )

USE_DEFAULT_OFFSET ( 0x0c + 8 ) USE_ON_OFF_OFFSET ( 0x0c ) LOOP_COUNT_OFFSET ( 0x14 ) SPEED_MM_OFFSET ( 0x1c ) POWER_OFFSET ( 0x28 ) FREQUENCY_OFFSET ( 0x34 )

unsigned long here Q_OFFSET ( 0x3c )

double here
Q_OFFSET_2 ( 0x3c+0xa0 ) START_TC_OFFSET ( 0x44 ) LASER_OFF_OFFSET ( 0xCC ) END_TC_OFFSET ( 0x4c ) POLYGON_TC_OFFSET ( 0x54 )

#pragma once

// so far, we no longer need this since it moves
// ezcad version 2.14.11
#define BASE_POINTER (0x27300)

// some test ezd files I found on the web
//#define BASE_POINTER (0x464)

enum {
	COL_PARAMETER = 1,
	COL_RGB = 2,
	COL_DEFAULT = 3,
	COL_ONOFF = 4,
	COL_LOOPCOUNT = 5,
	COL_SPEED_MM = 6,
	COL_POWER = 7,
	COL_FREQUENCY = 8,
	COL_Q = 9,
	COL_START_TC = 10,
	COL_LASER_OFF = 11,
	COL_END_TC = 12,
	COL_POLYGON_TC = 13,
	MAX_COLS = 14
};

#define MAX_PEN				( 256 )

#define	DATA_PENS_OFFSET		( 0x160 )	// this is a pointer to an offset where the pens/data is 
#define	NAME_OFFSET			( 8UL )		// offset of the parameter name
#define	NAME_LENGTH			( 0x10 )	// this is the length of the first name of the .ezd i started with
#define USE_DEFAULT_OFFSET		( 0x0c + 8 )
#define USE_ON_OFF_OFFSET		( 0x0c )
#define LOOP_COUNT_OFFSET		( 0x14 )
#define SPEED_MM_OFFSET			( 0x1c )
#define POWER_OFFSET			( 0x28 )
#define FREQUENCY_OFFSET		( 0x34 )
#define Q_OFFSET			( 0x3c )	// unsigned long here
#define Q_OFFSET_2			( 0x3c+0xa0 )	// double here
#define START_TC_OFFSET			( 0x44 )
#define LASER_OFF_OFFSET		( 0xCC )
#define END_TC_OFFSET			( 0x4c )
#define POLYGON_TC_OFFSET		( 0x54 )



class EZD {

public:
	uint16_t	penID;

	// start of pen data
	size_t	basePointer;

	// offset of data after the Parameter string
	uint64_t	baseOffset;

	std::string* dataBuffer = NULL;

	size_t parameter_length;

	~EZD() {
	}
		
	EZD() : baseOffset(0),basePointer(0), penID(0) ,parameter_length(0) {

	}

	EZD(size_t basePtr, uint16_t pen, std::string& data) : EZD() {

		/// whole EZD file
		dataBuffer = &data;

		// start of pen data
		basePointer = basePtr;

		// length of unicode parameter string in bytes
		parameter_length = dataBuffer->at(basePointer + 4);

		// rebases a pointer to after the unicode string .
		// 8 is the offset of the Unicode string
		baseOffset = 
			(size_t)(basePointer + NAME_OFFSET + parameter_length);
		penID = pen;
	}

	// returns the address of the last byte in the block
	size_t EndOfBlock()
	{
		// 0x27c is the size of a block with 0x10 length of the "default" string
		return basePointer + parameter_length + (0x27c - 0x10);
	}

	// these are before the unicode string so are only affected by the basePointer
	void R(uint8_t r) { dataBuffer->at(basePointer) = r; };
	void G(uint8_t g) { dataBuffer->at(basePointer + 1) = g; };
	void B(uint8_t b) { dataBuffer->at(basePointer + 2) = b; };

	uint8_t R() { return dataBuffer->at(basePointer); };
	uint8_t G() { return dataBuffer->at(basePointer + 1); };
	uint8_t B() { return dataBuffer->at(basePointer + 2); };

	uint32_t GetRGB() {
		uint32_t rgb;
		rgb = RGB(R(), G(), B());
		return rgb;

	}

	void SetRGB(uint8_t r, uint8_t g, uint8_t b) {

		R(r);
		G(g);
		B(b);
	}

	void SetRGB(COLORREF r) {
		uint32_t rgb;
		rgb = (uint32_t)r;

		R(GetRValue(rgb));
		G(GetGValue(rgb));
		B(GetBValue(rgb));
	}

	// these are before the unicode string so are only affected by the basePointer
	CString Parameter() {

		// ugly way to do a unicode string...
		std::wstring data;

		// check that we were able to actually read a string length
		ASSERT(parameter_length);
		if (parameter_length == 0) {
			return CString(_T("NA"));
		}

		// resize our string to match
		data.resize(parameter_length);

		// fetch ptr to it
		unsigned char* p = (unsigned char*)dataBuffer->data() + basePointer + 8;
		ASSERT(p);

		// check results
		if (p == 0) {
			return CString(_T("NA"));
		}

		char* d = (char*)data.data();
		ASSERT(d);

		// check results
		if (d == 0) return CString(_T("NA"));

		//copy over string data
		memcpy(
			d,
			p,
			parameter_length
		);

		return CString(data.c_str());
	}

	// these are before the unicode string so are only affected by the basePointer
	std::wstring Parameter(CString value) {

		// ugly way to do a unicode string...
		std::wstring data( value);

		// check that we were able to actually read a string length
		ASSERT(parameter_length);
		if (parameter_length == 0) {
			return std::wstring(_T(""));
		}

		// resize our string to match the length store, its a hassle to rewrite the whole file for this otherwise
		data.resize((parameter_length-1)/2);

		// fetch ptr to it
		unsigned char* p = (unsigned char*)dataBuffer->data() + basePointer + 8;
		ASSERT(p);

		// check results
		if (p == 0) {
			return std::wstring(_T(""));
		}

		char* d = (char*)data.data();
		ASSERT(d);

		// check results
		if (d == 0) return std::wstring(_T(""));

		//copy over string data
		memcpy(
			p,
			d,
			parameter_length
		);

		return data;
	}

	// these are afer the unicode string so are only affected by the basePointer and the length of the unicode string
	uint8_t UseDefault() {
		uint64_t offset = basePointer + USE_DEFAULT_OFFSET + parameter_length;
		uint8_t* b = (uint8_t*)(dataBuffer->data() + offset);
		return *b;
	}

	// only verifys its 0 or 1
	bool UseDefault(uint8_t value) {

		if ((value != 0) && (value != 1)) {
			return false;
		}

		uint64_t offset = basePointer + USE_DEFAULT_OFFSET + parameter_length;
		uint8_t* b = (uint8_t*)(dataBuffer->data() + offset);
		*b = value;

		return true;
	}

	// these are afer the unicode string so are only affected by the basePointer and the length of the unicode string
	uint8_t OnOff() {
		uint64_t offset = basePointer + USE_ON_OFF_OFFSET + parameter_length;
		uint8_t* b = (uint8_t*)(dataBuffer->data() + offset);
		return *b;
	}

	// only verifys its 0 or 1
	bool OnOff(uint8_t value) {

		if ((value != 0) && (value != 1)) {
			return false;
		}

		uint64_t offset = basePointer + USE_ON_OFF_OFFSET + parameter_length;
		uint8_t* b = (uint8_t*)(dataBuffer->data() + offset);
		*b = value;

		return true;
	}

	uint32_t getBasePtr() {
		uint32_t* base;
		base = (uint32_t*)(dataBuffer->data() + 0x160 );
		return *base;

	}

	uint32_t LoopCount() {
		uint32_t* loop;
		loop = (uint32_t*)(dataBuffer->data() + baseOffset + LOOP_COUNT_OFFSET);
		return (uint32_t)*loop;
	}
	
	bool LoopCount(uint32_t value) {

		if (value < ezdConfig.LoopCountMin || value > ezdConfig.LoopCountMax) {
			return false;
		}

		uint32_t* loop;
		loop = (uint32_t*)(dataBuffer->data() + baseOffset + LOOP_COUNT_OFFSET);
		*loop = value;

		return true;
	}

	double SpeedMM() {
		double* speed;
		speed = (double*)(dataBuffer->data() + baseOffset + SPEED_MM_OFFSET);
		return *speed;
	}

	bool SpeedMM(double val) {
		//reject OOB
		if (val < ezdConfig.SpeedMMMin || val > ezdConfig.SpeedMMMax) {
			return false;
		}

		double* speed;
		speed = (double*)(dataBuffer->data() + baseOffset + SPEED_MM_OFFSET);
		*speed = val;
		return true;
	}

	double Power() {
		double* power;
		power = (double*)(dataBuffer->data() + baseOffset + POWER_OFFSET);
		return *power;
	}

	bool Power(double val) {
		double* power;

		//reject OOB
		if (val <ezdConfig.PowerMin|| val > ezdConfig.PowerMax) {
			return false;
		}

		power = (double*)(dataBuffer->data() + baseOffset + POWER_OFFSET);
		*power = val;
		return true;
	}

	double Frequency() {
		uint32_t* frequency;
		frequency = (uint32_t*)(dataBuffer->data() + baseOffset + FREQUENCY_OFFSET);
		return (double)(*frequency) / 1000.0;
	}

	bool Frequency(double val) {

		//reject OOB
		if (val < ezdConfig.FrequencyMin || val > ezdConfig.FrequencyMax) {
			return false;
		}

		uint32_t* frequency;
		frequency = (uint32_t*)(dataBuffer->data() + baseOffset + FREQUENCY_OFFSET);
		uint32_t t = (uint32_t)(val * 1000.0);
		(*frequency) = t;

		return true;
	}

	int32_t StartTC() {
		int32_t* temp;
		temp = (int32_t*)(dataBuffer->data() + baseOffset + START_TC_OFFSET);

		return *temp;
	}

	bool StartTC(int32_t val) {
		//reject OOB
		if (val < ezdConfig.StartTCMin || val > ezdConfig.StartTCMax) {
			return false;
		}
		
		int32_t* temp;
		temp = (int32_t*)(dataBuffer->data() + baseOffset + START_TC_OFFSET);
		*temp = val;
		
		return true;
	}

	uint32_t LaserOff() {
		uint32_t* temp;
		temp = (uint32_t*)(dataBuffer->data() + baseOffset + LASER_OFF_OFFSET);

		return *temp;
	}

	bool LaserOff(uint32_t val) {
		//reject OOB
		if (val < ezdConfig.LaserOffMin || val > ezdConfig.LaserOffMax) {
			return false;
		}


		uint32_t* temp;
		temp = (uint32_t*)(dataBuffer->data() + baseOffset + LASER_OFF_OFFSET);
		*temp = val;

		return true;
	}

	uint32_t EndTC() {
		uint32_t* temp;
		temp = (uint32_t*)(dataBuffer->data() + baseOffset + END_TC_OFFSET);

		return *temp;
	}

	bool EndTC(uint32_t val) {
		//reject OOB
		if (val < ezdConfig.EndTCMin || val > ezdConfig.EndTCMax) {
			return false;
		}

		uint32_t* temp;
		temp = (uint32_t*)(dataBuffer->data() + baseOffset + END_TC_OFFSET);
		*temp = val;

		return true;

	}

	uint32_t PolygonTC() {
		uint32_t* temp;
		temp = (uint32_t*)(dataBuffer->data() + baseOffset + POLYGON_TC_OFFSET);

		return *temp;
	}

	bool PolygonTC(uint32_t val) {
		//reject OOB
		if (val < ezdConfig.PolygonTCMin|| val > ezdConfig.PolygonTCMax) {
			return false;
		}

		uint32_t* temp;
		temp = (uint32_t*)(dataBuffer->data() + baseOffset + POLYGON_TC_OFFSET);
		*temp = val;

		return true;
	}

	uint32_t Q() {
		uint32_t* temp;
		double* temp1;
		temp = (uint32_t*)(dataBuffer->data() + baseOffset + Q_OFFSET);
		temp1 = (double*)(dataBuffer->data() + baseOffset + Q_OFFSET_2);

		return *temp;
	}

	bool Q(uint32_t val) {

		//reject OOB
		if (ezdConfig.QMin < 0 || val > ezdConfig.QMax) {
			return false;
		}

		uint32_t* temp;
		double* temp1;

		temp = (uint32_t*)(dataBuffer->data() + baseOffset + Q_OFFSET);
		temp1 = (double*)(dataBuffer->data() + baseOffset + Q_OFFSET_2);

		*temp = val;
		*temp1 = double(val);

		return true;
	}
};

decode .upd files

just a very quick post on how to decode a chinese laser cutter firmware update file 644 etc

#include <iostream>
#include <fstream>
#include <vector>

uint8_t decode(uint8_t updByte)
{
     return (((uint8_t)(updByte – 1) ^ 0x88) << 7) |
         (( uint8_t)((updByte – 1) ^ 0x88) >> 7) |
         ((updByte – 1) ^ 0x88) & 0x7E;
}

int main(int argc, char* argv[])
{
     if (argc < 3) {
         return -1;
     }

    std::ifstream updFile(argv[1], std::ios::binary | std::ios::ate);
     std::streamsize size = updFile.tellg();
     updFile.seekg(0, std::ios::beg);

    std::vector<char> buffer(size);
     if (updFile.read(buffer.data(), size))
     {
         std::cout << “loaded ” << buffer.size() << std::endl;
         for (size_t i = 0; i < buffer.size(); i++ ) {
             buffer[i] = decode(buffer[i]);
         }

        auto outputFile = std::fstream(argv[2], std::ios::out | std::ios::binary);
         outputFile.write((char*)buffer.data(), buffer.size());
         outputFile.close();
     }
}

Super Mini Mini 2 – TC motor part 2

Finally done with all my trips over the seas. So had time to pull apart the SMM2 and get to the second motor and inspect it.

it came pretty dirty, and i guess i didn’t clean off that drywall as well as thought, and that stuff isn’t good so cleaned that up first

then remove all the bolts on the top, not the door ones on the side though,they’re obviously not part of the top though, the enclosure will still in place when the top is removed.

image

i am pretty sure the previous owners just hosed it down with the green stuff. But we can see the same motor OEM but this time it has sealed brushes, don’t know why they used sealed on this one and not the other!

image

reasons as why not to use this style of molex connector in a place it can get contaminated. chips inside the connector, coolant you name it, so wrapped it.

image

the umbrella door opening mech gets a lot of swarf under it too, so good place to put on a cleaning schedule

close up showing the internal part of the connector has been contaminated.image

under this wheel the chips were getting bunched up so i cleaned it out

image

this is where the cables for the motor and sensors enter the steel tube frame, the bulkhead protecting ring had fallen out and it just wouldn’t stay in place, i pressed it in here in this picture but it just fell out again. this is meant to stop the edge of the metal slowing eating away at the insulation on the wiring since it’ll move/vibrate so its worth improving it. the haas one just wouldn’t lock into place, either a sloppy fit or it wore out

image

removing the access panel on the  steel tube to get access to the connectors. neither of these were tight and the left one was a tad cross threaded and the cable was loose and moved so wasn’t really sealed , also no gasket/seal on the panel so the coolant can get inside, as you can see here. sloppy fit on the bulkhead again. not a huge deal but could be better.

image

just like the green from swamp thing, it gets everywhere.

image

rear of panel

image

motor type SPG LS series LS85E82K-A06. haas part no 32-1875

image

image

and apparently after this i didn’t take more pictures though i thought i had..

the brushes on this motor have o ring seals so they were fine inside, all we did was clean up excess coolant and chips from everywhere feasible and then put it back together, no real issues there, add a gasket to that access panel. cover up the molex connectors for the sensor with some self annealing tape etc.

two!

image

also finally managed to get an CSMD NGC from eBay for a decent amount less than the list price from Haas. i have no idea what goes on with pricing on eBay, but i regularly see these go for 2x or more the list price from Haas for used ones… ($1600) maybe there is some secret Haas charge but we did get a HFO quote for it at that price. so who knows, but apparently you can arbitrage CSMDs on eBay. More on that later (the csmd not the arbitrage )

image

haas super mini mill 2 ngc tool changer carousel motor

hello,

don’t use tool 1 during testing the carousel, use tool 2

the folks that had our SMM2 with only 300 hours before us didn’t do the best job of keeping it clean and used some wacky looking coolants.

we noticed that the tool change was a bit sloppy/noisy and looking around people just said that is just Haas, but eventually while I was in the UK it failed with a lot of serious looking warnings 9847 etc that if you looked up talked about firmware mismatches but looking into it, it was basically just saying that the motor was on, but didn’t seem to be drawing current.

oh dear.

image

so pulled apart the machine, removed the access panel to the changer , lifted out the motor and looked at the brushes. they were contaminated so cleaned it up, put it back and seemed to be better.

when i got back to the USA a week or so later we noticed the tool change was throwing a quick alarm during a tool change.

so today we pulled it apart again

this motor being disconnected, throws an 858 ATC carousel motor electrical fault

cover off

image

there are some interesting design choices in here.

removed wiring, tool #1 connector wire is marked as tool #1, t.c mark isn’t marked as t.c. mark.

image

removed the motor from the housing

image

very steely.

image

baldor motor,. spg assembly https://spgmotor.net/bs-series/#1561724885376-8af2aff5-cb26e609-82cd0c42-a4318461-c59ceb77-0fe8467c-de89

image

pepperl fuchs

NBB4-12M45-Z5-0.3MM sensors

image

yep


Get to the brushes!

remove this cover, flat blade should come out easily.

image

image

fun stuff, it is not sealed , nor is meant to be by the manufacturer even though this is a custom one for haas ( seems to be, different ratio and they run it at 160V)

20211002_161250

egads

image

brush is all gunked up

image


the new dune is looking good, but low budget sandworms

20211002_161506

i did order a couple of replacement brushes, but they’re not really right since its just the carbon side with no provision for the wire, not a big deal since the brush itself is the same length as the new one and looks fine.

cleaned up the brush, and put it back ( added some teflon tape on retainer)

lets take a look at the other brush

image

interesting, totally fine no damage,  no corrosion.

when the motor is mounted, the dirty brush is on the inside where the green rectangle is

image

most of the box this is all in is “sealed”, gasket on the front, sealed wiring… except there is a ruddy great slot and bearing as part of the tool changer mech right next to the brush, so no we know why its contaminated on that side only….  why not rotate the motor 90o so the brushes aren’t exposed to the grease and coolant, after thought?

a plate over this with an indent for the nut/bolt would have been great too, and would have kept chips and coolant etc out of this mechanism,

some photos of the offending part, you can see the grease, coolant and chips

image

the nasty brush is right next to the nut, and we cleaned up the inside of the cabinet a lot , it was a lot worse.

image

a temp fix while we decide what to do about it long term, the teflon tape will help and i wrapped the motor with some aluminum tape

image

so if you get tool change errors, or a sloppy tool change where it feels like it doesn’t engage well or the motor just sounds funky, check that motor and brush, , it might just need a good cleaning.

measured 21 ohms on the motor after cleaning. (haas specs 5 to 20 ohms)

note correct placement of wiring at the front of the motor versus behind it

image

next we are going to pull out the shuttle motor and check it, that does not look fun to remove.

also this is a fun fella.

image

image

hello onsemi/fairchild ?

cheers