CircuitPython_AT24MAC_EEPROM/at24mac_eeprom.py
2024-12-04 16:58:34 -06:00

198 lines
6.8 KiB
Python

# Written by FACTS Engineering
# Copyright (c) 2023 FACTS Engineering, LLC
# Licensed under the MIT license.
"""
`AT24MAC_EEPROM`
================================================================================
AT24MACx02 EEPROM Library
Provides an interface to read and write from EEPROM memory
as well as retrieve the unique MAC Address and serial number
Compatible with AT24MAC402 and AT24MAC602 devices using I2C
* Author(s): Adam Cummick, Tristan Warder
"""
import time
from adafruit_bus_device.i2c_device import I2CDevice
from micropython import const
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/facts-engineering/CircuitPython_AT24MAC_EEPROM.git"
_EEPROM_LENGTH = const(256) # bytes
_PAGE_SIZE = const(16)
_WRITE_TIME = 0.005
_EUI_LOCATION = b"\x9A"
_MAC_ADDR_LENGTH = const(6)
# Uncomment for AT24MAC602 with EUI64 8 byte MAC Address
# _EUI_LOCATION = b'\x98'
# _MAC_ADDR_LENGTH = const(8)
_SERIAL_NUMBER_LOCATION = b"\x80"
_SERIAL_NUMBER_LENGTH = const(16)
class AT24MAC:
"""Class to interface with EEPROM device"""
def __init__(self, i2c, address_pins=0b100):
self.eeprom_device = I2CDevice(i2c, 0x50 | address_pins, True)
self.eui_device = I2CDevice(i2c, 0x58 | address_pins, True)
self.mac = self.read_mac_address()
self.serial_number = self.read_serial_number()
def read_serial_number(self):
"""Reads the unique serial number for the Device"""
serial_number = bytearray(_SERIAL_NUMBER_LENGTH)
with self.eui_device as device:
device.write_then_readinto(_SERIAL_NUMBER_LOCATION, serial_number)
serial_number = int.from_bytes(serial_number, "big")
return serial_number
def read_mac_address(self):
"""Reads the 6 byte mac address from the
AT24MAC402 device
"""
mac_address = bytearray(_MAC_ADDR_LENGTH)
with self.eui_device as device:
device.write_then_readinto(_EUI_LOCATION, mac_address)
return mac_address
def __getitem__(self, address):
if isinstance(address, int):
return self._read(address)
if isinstance(address, slice):
if address.start is not None:
start = address.start
if (start < 0) or (start >= _EEPROM_LENGTH):
raise ValueError("Start Address goes beyond memory limit")
else:
start = 0
if address.stop is not None:
stop = address.stop
if (stop < 0) or (stop >= _EEPROM_LENGTH):
raise ValueError("Stop Address goes beyond memory limit")
else:
stop = _EEPROM_LENGTH
length = stop - start
return self._read(start, length)
raise ValueError("Item must be integer or slice")
def __setitem__(self, address, data):
if isinstance(address, int):
self._write(address, data)
elif isinstance(address, slice):
if address.start is not None:
start = address.start
if (start < 0) or (start >= _EEPROM_LENGTH):
raise ValueError("Start Address goes beyond memory limit")
else:
start = 0
if address.stop is not None:
stop = address.stop
if (stop < 0) or (stop >= _EEPROM_LENGTH):
raise ValueError("Stop Address goes beyond memory limit")
else:
stop = _EEPROM_LENGTH
self._write(start, data)
else:
raise ValueError("Item must be integer or slice")
def __len__(self):
return _EEPROM_LENGTH
def _read(self, data_address, length=1):
"""Reads data back from EEPROM at specified address
length defaults to 1, but can be specified
data is returned as bytearray
"""
if (length + data_address) > _EEPROM_LENGTH:
raise ValueError(
f"Data address {data_address} and length {length} go beyond memory limit"
)
data_bytes = bytearray(length)
data_address = data_address.to_bytes(1, "big")
with self.eeprom_device as device:
device.write_then_readinto(data_address, data_bytes)
if length > 1:
return data_bytes
return data_bytes[0]
def _write(self, data_address, data):
"""writes a byte or bytearray to device at specified address
data length is determined by bytearray passed in. Data is compared to
the current page before writing to preserve EEPROM life.
When writing larger than 1 page, memory is only checked page by page
"""
if not isinstance(data, (bytearray, bytes)):
try:
if 0x00 <= data <= 0xFF:
data = data.to_bytes(1, "big")
except TypeError:
data = bytearray(data)
if (len(data) + data_address) > _EEPROM_LENGTH:
raise ValueError(
f"Data address {data_address} and length {len(data)} go beyond memory limit"
)
if len(data) > 1:
total_pages = int(len(data) / _PAGE_SIZE)
remaining_bytes = len(data) % _PAGE_SIZE
offset = _PAGE_SIZE - (data_address % _PAGE_SIZE)
if offset == 16:
offset = 0
remaining_bytes -= offset
else:
offset = 0
total_pages = 0
remaining_bytes = 1
current_address = data_address
if offset > 0:
self._write_page(data_address, data[0:offset])
current_address += offset
if total_pages > 0:
for i in range(offset, total_pages * _PAGE_SIZE, _PAGE_SIZE):
this_page = data[i : i + _PAGE_SIZE]
self._write_page(current_address, this_page)
current_address += _PAGE_SIZE
if remaining_bytes > 0:
self._write_page(current_address, data[offset + total_pages * _PAGE_SIZE :])
def _write_page(self, data_address, data):
"""Writes a single page to eeprom. Data is compared to current data
before writing to extend EEPROM life. Delay cycle is included for write
latency
"""
if not self._does_data_match(data_address, data):
data_address = data_address.to_bytes(1, "big")
data_message = data_address + data
with self.eeprom_device as device:
device.write(data_message)
time.sleep(_WRITE_TIME)
def _does_data_match(self, data_address, data):
"""Compares data passed in with the data at the specified address"""
current_values = self._read(data_address, len(data))
if len(data) == 1:
current_values = bytes([current_values])
return current_values == data