imrishabh18/rp2040-motor-controller
This circuit module provides a microcontroller-based NEMA17 stepper motor driver with integrated USB-C Power Delivery, temperature sensing, and safety interlocks.
- Version
- 1.0.16
- License
- unset
- Stars
- 0
firmware/test_thermal.py
import unittest
from tmp102 import TMP102, CONFIG, encode_temperature, decode_temperature
from thermal_policy import ThermalPolicy
class FakeI2C:
def __init__(self):
self.registers = {0: b'\x19\x00', 1: b'\x60\xa0', 2: b'\x4b\x00', 3: b'\x50\x00'}
self.failed = False
def writeto_mem(self, address, register, data):
if self.failed:
raise OSError("bus failure")
assert address == 0x48
self.registers[register] = data
def readfrom_mem(self, address, register, size):
if self.failed:
raise OSError("bus failure")
assert address == 0x48 and size == 2
return self.registers[register]
class ProtectionTests(unittest.TestCase):
def test_sensor_register_byte_order_and_signed_temperature(self):
for data, c in ((b'\x19\x00', 25), (b'\x4b\x00', 75), (b'\x3c\x00', 60), (b'\xf5\xe0', -10.125)):
self.assertEqual(decode_temperature(data), c)
self.assertEqual(encode_temperature(c), data)
bus = FakeI2C()
sensor = TMP102(bus)
sensor.configure()
self.assertEqual(bus.registers[1], b'\x60\xc0')
self.assertEqual(bus.registers[2], b'\x3c\x00')
self.assertEqual(bus.registers[3], b'\x4b\x00')
self.assertEqual(sensor.temperature(), 25)
def test_sensor_reset_mode_corruption_and_missing_sensor(self):
for register, data in ((1, b'\x60\xa0'), (1, b'\x64\xc0'), (1, b'\x60\xd0'),
(1, b'\x61\xc0'), (2, b'\x4b\x00'), (3, b'\x50\x00')):
bus = FakeI2C()
sensor = TMP102(bus)
sensor.configure()
bus.registers[register] = data
with self.assertRaises(OSError):
sensor.temperature()
bus.failed = True
with self.assertRaises(OSError):
sensor.configure()
def test_status_bits_are_read_only_and_do_not_mask_configuration_errors(self):
bus = FakeI2C()
sensor = TMP102(bus)
sensor.configure()
bus.registers[1] = b'\xe0\xe0' # OS and AL may vary
self.assertEqual(sensor.temperature(), 25)
def test_startup_requires_valid_sensor_and_explicit_arm(self):
p = ThermalPolicy()
self.assertFalse(p.arm())
p.sample(25)
self.assertFalse(p.enabled)
self.assertTrue(p.arm())
def test_warning_trip_and_strict_cooldown_with_manual_rearm(self):
p = ThermalPolicy()
p.sample(25)
p.arm()
self.assertIn("WARNING", p.sample(65))
self.assertTrue(p.enabled)
p.sample(75)
self.assertFalse(p.enabled)
p.sample(60)
self.assertFalse(p.arm())
p.sample(59.9375)
self.assertFalse(p.enabled)
self.assertTrue(p.latched)
self.assertTrue(p.arm())
def test_faults_stop_motor_and_never_auto_resume(self):
for kwargs in ({'sensor_ok': False}, {'alert_ok': False}, {'driver_fault': True}):
p = ThermalPolicy()
p.sample(25)
p.arm()
p.sample(25, **kwargs)
self.assertFalse(p.enabled)
p.sample(25)
self.assertFalse(p.enabled)
self.assertTrue(p.latched)
for temp in (None, float('nan'), -41, 126):
p.sample(temp)
self.assertFalse(p.arm())
def test_bad_temperature_format_is_rejected(self):
for data in (b'\x00', b'\x19\x01', b'\x7f\xf0'):
with self.assertRaises(ValueError):
decode_temperature(data)
# Exercise the actual MicroPython adapter with emulated pins/I2C; no motors run.
class AdapterTests(unittest.TestCase):
def setUp(self):
import sys
import types
import importlib
from unittest.mock import patch
self.bus = FakeI2C()
self.pins = {}
pins = self.pins
class FakePin:
IN, OUT, IRQ_FALLING = 0, 1, 2
def __new__(cls, number, *args, **kwargs):
if number in pins:
return pins[number]
obj = super().__new__(cls)
pins[number] = obj
obj.level = 1
obj.callback = None
return obj
def __init__(self, number, mode=None, value=None):
if value is not None:
self.level = value
def value(self, value=None):
if value is not None:
self.level = value
return self.level
def irq(self, handler=None, **kwargs):
self.callback = handler
machine = types.SimpleNamespace(Pin=FakePin, I2C=lambda *a, **k: self.bus,
disable_irq=lambda: 0, enable_irq=lambda _: None)
clock = types.SimpleNamespace(sleep_ms=lambda _: None, ticks_ms=lambda: 1000,
ticks_diff=lambda a, b: a - b)
with patch.dict(sys.modules, {'machine': machine, 'time': clock}):
sys.modules.pop('thermal_guard', None)
self.module = importlib.import_module('thermal_guard')
self.guard = self.module.ThermalGuard()
def test_boot_and_sensor_failure_hold_enable_low(self):
self.assertEqual(self.pins[22].level, 0)
self.assertTrue(self.guard.arm())
self.bus.failed = True
self.guard.poll()
self.assertEqual(self.pins[22].level, 0)
self.assertFalse(self.guard.arm())
def test_interrupt_latches_even_after_alert_has_recovered(self):
self.guard.arm()
self.pins[24].level = 0
self.pins[24].callback(None)
self.assertEqual(self.pins[22].level, 0)
self.pins[24].level = 1
self.guard.poll()
self.assertFalse(self.guard.policy.enabled)
self.assertTrue(self.guard.arm()) # explicit user action after cool check
def test_arm_uses_fresh_temperature_and_checks_driver_fault(self):
self.bus.registers[0] = b'\x4b\x00'
self.assertFalse(self.guard.arm())
self.bus.registers[0] = b'\x19\x00'
self.pins[23].level = 0
self.assertFalse(self.guard.arm())
self.assertEqual(self.pins[22].level, 0)
def test_exit_and_stop_clear_enable_and_coil_inputs(self):
self.guard.arm()
for n in (18, 19, 20, 21):
self.pins[n].value(1)
self.guard.stop('user stop')
self.assertEqual([self.pins[n].level for n in (18, 19, 20, 21, 22)], [0] * 5)
if __name__ == '__main__':
unittest.main()