Created
May 14, 2020 05:04
-
-
Save gandy92/a7eef12009045f7b3fc01d778c3b79a7 to your computer and use it in GitHub Desktop.
Python3 code to demonstrate how the data on the NFC tag on an Ultimaker material spool is organised
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- 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