Skip to content

Instantly share code, notes, and snippets.

@gandy92

gandy92/spool.py

Created May 14, 2020
Embed
What would you like to do?
Python3 code to demonstrate how the data on the NFC tag on an Ultimaker material spool is organised
# -*- coding: utf-8 -*-
#
# required packages: ndeflib, crc8
#
from crc8 import crc8
import ndef
import uuid
class UltimakerMaterialRecord(ndef.record.GlobalRecord):
_type = 'urn:nfc:ext:ultimaker.nl:material'
_name = '1'
NO_MATERIAL = uuid.UUID('00000000-0000-0000-0000-000000000000')
def __init__(self, material_id=None, serial="", version=0, compat_version=0,
manufacturing_ts=0, station_id=0, batch_code=""):
self._version = version
self._compatibility_version = compat_version
self._serial_number = serial
self._manufacturing_timestamp = manufacturing_ts
self._material_id = material_id if material_id is not None else self.NO_MATERIAL
self._programming_station_id = station_id
self._batch_code = batch_code
def _encode_payload(self):
data = self._encode_struct('>BB', self._version, self._compatibility_version)
serial = self._serial_number.encode("utf-8") + b'\x00'*14
data += serial[:14]
data += self._encode_struct('>Q', self._manufacturing_timestamp)
data += self._material_id.bytes
data += self._encode_struct(">H", self._programming_station_id)
data += self._batch_code.encode("utf-8")
data += b'\x00' * 108
return data[0:108]
@classmethod
def _decode_payload(cls, octets, errors):
version, compat_version = cls._decode_struct('>BB', octets[0:2])
serial_number = octets[2:16].decode("utf-8").split("\x00")[0]
manufacturing_timestamp = cls._decode_struct(">Q", octets[16:24])
material_id = uuid.UUID(bytes=octets[24:40])
programming_station_id = cls._decode_struct(">H", octets[40:42])
batch_code = octets[42:106].decode("utf-8").split("\x00")[0]
return cls(material_id, serial_number, version, compat_version,
manufacturing_ts=manufacturing_timestamp, station_id=programming_station_id, batch_code=batch_code)
class UltimakerStatRecord(ndef.record.GlobalRecord):
_type = 'urn:nfc:ext:ultimaker.nl:stat'
_name = '2'
MATERIAL_UNIT_UNUSED = 0
MATERIAL_QUANTITY_LENGTH_MM = 1
MATERIAL_QUANTITY_MASS_GR = 2
MATERIAL_QUANTITY_VOLUME_CM3 = 3
def __init__(self, material_total=0, material_unit=None, material_remaining=None, total_usage_duration=0,
version=0, compat_version=0):
self._version = version
self._compatibility_version = compat_version
self._material_unit = material_unit if material_unit is not None else self.MATERIAL_UNIT_UNUSED
self._material_total = material_total
self._material_remaining = material_remaining if material_remaining is not None else material_total
self._total_usage_duration = total_usage_duration
self._unit = ["N/A", "mm", "mg", "cm³"][self._material_unit]
def _encode_payload(self):
data = self._encode_struct('>BBBLLQ', self._version, self._compatibility_version, int(self._material_unit),
self._material_total, self._material_remaining, self._total_usage_duration)
data = data[:19] + crc8(data[:19]).digest()
return data[0:20]
@classmethod
def _decode_payload(cls, octets, errors):
version, compat_version, material_unit, material_total, material_remaining, total_usage_duration = \
cls._decode_struct('>BBBLLQ', octets)
crc = crc8(octets[:19]).digest()[0]
if octets[19] != crc:
print(" **** crc mismatch: tag={} self={}".format(octets[19], crc))
return cls(material_total=material_total, material_unit=material_unit, material_remaining=material_remaining,
total_usage_duration=total_usage_duration, version=version, compat_version=compat_version)
class SigRecord(ndef.record.GlobalRecord):
_type = 'urn:nfc:wkt:Sig'
_decode_min_payload_length = 2
def __init__(self, sig):
self._sig = sig
def _encode_payload(self):
return self._encode_struct('>H', self._sig)
@classmethod
def _decode_payload(cls, octets, errors):
sig = cls._decode_struct('>H', octets)
return cls(sig)
ndef.Record.register_type(UltimakerMaterialRecord)
ndef.Record.register_type(UltimakerStatRecord)
ndef.Record.register_type(SigRecord)
def decode(octets):
records = ndef.message_decoder(octets, errors='relax')
for record in ndef.message_decoder(octets, errors='relax'):
print(record, "length is", len(record.data))
# print(" ", "\n ".join([record.data[i:i+4].hex() for i in range(0, len(record.data), 4)]))
if type(record) is UltimakerMaterialRecord:
print(" GUID:", record._material_id)
print(" version:", record._version)
print(" compatibility_version:", record._compatibility_version)
print(" serial_number:", record._serial_number)
print(" manufacturing_timestamp:", record._manufacturing_timestamp)
print(" programming_station_id:", record._programming_station_id)
print(" batch_code:", record._batch_code)
if type(record) is UltimakerStatRecord:
print(" version:", record._version)
print(" compatibility_version:", record._compatibility_version)
print(" material_unit:", record._material_unit, "({})".format(record._unit))
print(" material_total:", record._material_total, record._unit)
print(" material_remaining:", record._material_remaining, record._unit)
print(" total_usage_duration:", int(record._total_usage_duration/3600), "h")
class MyFilamentSpool:
def __init__(self, guid, weight, serial):
self.material = UltimakerMaterialRecord(material_id=guid,
serial=serial,
batch_code="123456789AB",
station_id=0xaffe)
self.status = UltimakerStatRecord(material_unit=2, material_total=weight)
def data(self) -> bytes:
encoder = ndef.message_encoder()
results = list()
encoder.send(None)
encoder.send(self.material)
results.append(encoder.send(SigRecord(0x2000)))
results.append(encoder.send(self.status))
results.append(encoder.send(self.status))
results.append(encoder.send(None))
result = b''.join(results)
if len(result) % 4 != 0:
print("Padding data with {} bytes to full page size.".format(len(result) % 4))
result += b'\x00' * (4-len(result) % 4)
print(" Size is now {} bytes, that is {} pages with {} excess.".format(len(result), len(result)//4, len(result) % 4))
return result
proto = MyFilamentSpool(uuid.UUID("ec8321a4-a798-4983-bc4f-72aef80daf9f"), 500000, "01020304050607")
carbon = MyFilamentSpool(uuid.UUID("4debf055-6eed-4795-b4c4-4643fba8ea53"), 800000, "01020304050607")
if __name__ == '__main__':
print("***** Carbon: ******")
decode(carbon.data())
print()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment