diff --git a/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-digitalRead.py b/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-digitalRead.py new file mode 100644 index 0000000..6176363 --- /dev/null +++ b/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-digitalRead.py @@ -0,0 +1,25 @@ +# FILE: pcal6416a-digitalRead.py +# AUTHOR: Fran Fodor @ Soldered +# BRIEF: Read the state of a pin with an internal pull-up using the PCAL6416A IO expander. +# Connect a pushbutton between pin A0 and GND — pressing it pulls the pin LOW. +# WORKS WITH: PCAL6416A IO Expander breakout: www.solde.red/333351 +# LAST UPDATED: 2026-04-23 + +import time +from machine import Pin, I2C +from pcal6416a import PCAL6416A, PCAL6416A_A0, INPUT_PULLUP + +# If you aren't using the Qwiic connector, manually enter your I2C pins: +# i2c = I2C(0, scl=Pin(22), sda=Pin(21)) +# expander = PCAL6416A(i2c) + +# Initialize expander over Qwiic +expander = PCAL6416A() + +# Set pin A0 as input with internal pull-up (pin reads HIGH when button is open) +expander.pinMode(PCAL6416A_A0, INPUT_PULLUP) + +while True: + state = expander.digitalRead(PCAL6416A_A0) + print("State of pin A0 is: {}".format("HIGH" if state else "LOW")) + time.sleep(1) diff --git a/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-digitalWrite.py b/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-digitalWrite.py new file mode 100644 index 0000000..1e5a8b6 --- /dev/null +++ b/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-digitalWrite.py @@ -0,0 +1,26 @@ +# FILE: pcal6416a-digitalWrite.py +# AUTHOR: Fran Fodor @ Soldered +# BRIEF: Blink an output pin on the PCAL6416A IO expander. +# Connect an LED (with series resistor) between pin A0 and GND. +# WORKS WITH: PCAL6416A IO Expander breakout: www.solde.red/333351 +# LAST UPDATED: 2026-04-23 + +import time +from machine import Pin, I2C +from pcal6416a import PCAL6416A, PCAL6416A_A0, OUTPUT + +# If you aren't using the Qwiic connector, manually enter your I2C pins: +# i2c = I2C(0, scl=Pin(22), sda=Pin(21)) +# expander = PCAL6416A(i2c) + +# Initialize expander over Qwiic +expander = PCAL6416A() + +# Set pin A0 as output +expander.pinMode(PCAL6416A_A0, OUTPUT) + +while True: + expander.digitalWrite(PCAL6416A_A0, 1) # Set HIGH + time.sleep(1) + expander.digitalWrite(PCAL6416A_A0, 0) # Set LOW + time.sleep(1) diff --git a/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-driveStrength.py b/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-driveStrength.py new file mode 100644 index 0000000..f5ab84d --- /dev/null +++ b/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-driveStrength.py @@ -0,0 +1,32 @@ +# FILE: pcal6416a-driveStrength.py +# AUTHOR: Fran Fodor @ Soldered +# BRIEF: Cycle through all four drive-strength levels on pin A0 and set port B to +# open-drain mode. Drive strength controls how many output transistor pairs +# drive the pin — higher values allow more current to be sourced or sunk. +# WORKS WITH: PCAL6416A IO Expander breakout: www.solde.red/333351 +# LAST UPDATED: 2026-04-23 + +import time +from machine import Pin, I2C +from pcal6416a import PCAL6416A, PCAL6416A_A0, OUTPUT + +# If you aren't using the Qwiic connector, manually enter your I2C pins: +# i2c = I2C(0, scl=Pin(22), sda=Pin(21)) +# expander = PCAL6416A(i2c) + +# Initialize expander over Qwiic +expander = PCAL6416A() + +expander.pinMode(PCAL6416A_A0, OUTPUT) + +# Set port B (port 1) to open-drain — pin floats HIGH, pulls LOW when driven low +expander.openDrainPort(1, True) + +expander.digitalWrite(PCAL6416A_A0, 1) # Hold A0 HIGH throughout + +while True: + for strength in range(4): + # 0 = weakest (fewest transistor pairs), 3 = strongest (most current) + expander.driveStrength(PCAL6416A_A0, strength) + print("Drive strength set to {}".format(strength)) + time.sleep(1) diff --git a/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-interrupts.py b/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-interrupts.py new file mode 100644 index 0000000..db5cf8c --- /dev/null +++ b/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-interrupts.py @@ -0,0 +1,51 @@ +# FILE: pcal6416a-interrupts.py +# AUTHOR: Fran Fodor @ Soldered +# BRIEF: Detect pin-change interrupts from the PCAL6416A using the INT output. +# Connect pushbuttons between pins A0/A1 and GND. Connect the INT pin of +# the expander to pin D2 on your board. +# WORKS WITH: PCAL6416A IO Expander breakout: www.solde.red/333351 +# LAST UPDATED: 2026-04-23 + +from machine import Pin, I2C +from pcal6416a import PCAL6416A, PCAL6416A_A0, PCAL6416A_A1, INPUT_PULLUP + +# If you aren't using the Qwiic connector, manually enter your I2C pins: +# i2c = I2C(0, scl=Pin(22), sda=Pin(21)) +# expander = PCAL6416A(i2c) + +# Initialize expander over Qwiic +expander = PCAL6416A() + +expander.pinMode(PCAL6416A_A0, INPUT_PULLUP) +expander.pinMode(PCAL6416A_A1, INPUT_PULLUP) + +# Enable change-detection interrupt on both pins +expander.setInterrupt(PCAL6416A_A0, True) +expander.setInterrupt(PCAL6416A_A1, True) + +int_flag = False + + +def isr(pin): + global int_flag + int_flag = True + + +# D2 on most boards — adjust if needed +int_pin = Pin(2, Pin.IN, Pin.PULL_UP) +int_pin.irq(trigger=Pin.IRQ_FALLING, handler=isr) + +print("Waiting for interrupts on A0 and A1...") + +while True: + if int_flag: + int_flag = False + int_reg = expander.getInterrupts() # Reading clears the status register + + if int_reg & (1 << PCAL6416A_A0): + state = expander.digitalRead(PCAL6416A_A0) + print("Interrupt on A0 — pin is now {}".format("HIGH" if state else "LOW")) + + if int_reg & (1 << PCAL6416A_A1): + state = expander.digitalRead(PCAL6416A_A1) + print("Interrupt on A1 — pin is now {}".format("HIGH" if state else "LOW")) diff --git a/Communication/PCAL6416A/PCAL6416A/pcal6416a.py b/Communication/PCAL6416A/PCAL6416A/pcal6416a.py new file mode 100644 index 0000000..ae621f5 --- /dev/null +++ b/Communication/PCAL6416A/PCAL6416A/pcal6416a.py @@ -0,0 +1,288 @@ +# FILE: pcal6416a.py +# AUTHOR: Fran Fodor @ Soldered +# BRIEF: MicroPython library for the PCAL6416A 16-bit IO expander +# LAST UPDATED: 2026-04-23 + +from machine import I2C, Pin +from os import uname + +# I2C address (ADDR pin low = 0x20, ADDR pin high = 0x21) +PCAL6416A_I2C_ADDR = 0x20 + +# Register addresses +PCAL6416A_INPORT0 = 0x00 # Input port 0 +PCAL6416A_INPORT1 = 0x01 # Input port 1 +PCAL6416A_OUTPORT0 = 0x02 # Output port 0 +PCAL6416A_OUTPORT1 = 0x03 # Output port 1 +PCAL6416A_POLINVPORT0 = 0x04 # Polarity inversion port 0 +PCAL6416A_POLINVPORT1 = 0x05 # Polarity inversion port 1 +PCAL6416A_CFGPORT0 = 0x06 # Configuration port 0 (1=input, 0=output) +PCAL6416A_CFGPORT1 = 0x07 # Configuration port 1 +PCAL6416A_OUTDRVST_REG00 = 0x40 # Output drive strength port A, pins 0-3 (2 bits/pin) +PCAL6416A_OUTDRVST_REG01 = 0x41 # Output drive strength port A, pins 4-7 +PCAL6416A_OUTDRVST_REG10 = 0x42 # Output drive strength port B, pins 0-3 +PCAL6416A_OUTDRVST_REG11 = 0x43 # Output drive strength port B, pins 4-7 +PCAL6416A_INLAT_REG0 = 0x44 # Input latch register 0 +PCAL6416A_INLAT_REG1 = 0x45 # Input latch register 1 +PCAL6416A_PUPDEN_REG0 = 0x46 # Pull-up/pull-down enable register 0 +PCAL6416A_PUPDEN_REG1 = 0x47 # Pull-up/pull-down enable register 1 +PCAL6416A_PUPDSEL_REG0 = 0x48 # Pull-up/pull-down selection register 0 (1=pull-up) +PCAL6416A_PUPDSEL_REG1 = 0x49 # Pull-up/pull-down selection register 1 +PCAL6416A_INTMSK_REG0 = 0x4A # Interrupt mask register 0 (0=enabled, 1=masked) +PCAL6416A_INTMSK_REG1 = 0x4B # Interrupt mask register 1 +PCAL6416A_INTSTAT_REG0 = 0x4C # Interrupt status register 0 (cleared on read) +PCAL6416A_INTSTAT_REG1 = 0x4D # Interrupt status register 1 +PCAL6416A_OUTPORT_CONF = ( + 0x4F # Output port configuration (0=push-pull, 1=open-drain per port) +) + +# Pin name aliases — port A = pins 0-7, port B = pins 8-15 +PCAL6416A_A0 = 0 +PCAL6416A_A1 = 1 +PCAL6416A_A2 = 2 +PCAL6416A_A3 = 3 +PCAL6416A_A4 = 4 +PCAL6416A_A5 = 5 +PCAL6416A_A6 = 6 +PCAL6416A_A7 = 7 +PCAL6416A_B0 = 8 +PCAL6416A_B1 = 9 +PCAL6416A_B2 = 10 +PCAL6416A_B3 = 11 +PCAL6416A_B4 = 12 +PCAL6416A_B5 = 13 +PCAL6416A_B6 = 14 +PCAL6416A_B7 = 15 + +# Pin modes +INPUT = 0 +OUTPUT = 1 +INPUT_PULLUP = 2 +INPUT_PULLDOWN = 3 + + +class PCAL6416A: + """ + MicroPython class for the PCAL6416A 16-bit GPIO expander. + Supports 16 GPIO pins across two 8-bit ports (A0-A7, B0-B7). + """ + + def __init__(self, i2c=None, address=PCAL6416A_I2C_ADDR): + """ + Initialize the PCAL6416A expander. + + :param i2c: Initialized I2C object (optional, auto-detected on known boards) + :param address: I2C address of the device (default 0x20) + """ + if i2c is not None: + self.i2c = i2c + else: + if uname().sysname in ("esp32", "esp8266", "Soldered Dasduino CONNECTPLUS"): + self.i2c = I2C(0, scl=Pin(22), sda=Pin(21)) + else: + raise Exception("Board not recognized, enter I2C pins manually") + + self.address = address + + # Shadow registers mirror the chip state to avoid unnecessary reads + # Power-on reset defaults from PCAL6416A datasheet + self._cfg = [0xFF, 0xFF] # Configuration: all inputs + self._out = [0xFF, 0xFF] # Output: all high + self._pol = [0x00, 0x00] # Polarity: no inversion + self._pupden = [0x00, 0x00] # Pull enable: all disabled + self._pupdsel = [0xFF, 0xFF] # Pull selection: all pull-up + self._drvst = [0xFF, 0xFF, 0xFF, 0xFF] # Drive strength: strongest + self._intmsk = [0xFF, 0xFF] # Interrupt mask: all masked + self._inlat = [0x00, 0x00] # Input latch: disabled + self._outconf = 0x00 # Output config: push-pull + + def _readByte(self, reg): + try: + data = self.i2c.readfrom_mem(self.address, reg, 1) + return True, data[0] + except: + return False, 0 + + def _writeByte(self, reg, val): + try: + self.i2c.writeto_mem(self.address, reg, bytes([val])) + return True + except: + return False + + def pinMode(self, pin, mode): + """ + Configure a pin's direction and optional pull resistor. + + :param pin: Pin number 0-15 (use PCAL6416A_A0..A7 or PCAL6416A_B0..B7 aliases) + :param mode: INPUT, OUTPUT, INPUT_PULLUP, or INPUT_PULLDOWN + """ + if pin > 15: + return + port = pin // 8 + bit = pin % 8 + + if mode == INPUT: + self._cfg[port] |= 1 << bit + self._writeByte(PCAL6416A_CFGPORT0 + port, self._cfg[port]) + + elif mode == OUTPUT: + self._cfg[port] &= ~(1 << bit) + self._out[port] &= ~(1 << bit) + self._writeByte(PCAL6416A_OUTPORT0 + port, self._out[port]) + self._writeByte(PCAL6416A_CFGPORT0 + port, self._cfg[port]) + + elif mode == INPUT_PULLUP: + self._cfg[port] |= 1 << bit + self._pupden[port] |= 1 << bit + self._pupdsel[port] |= 1 << bit + self._writeByte(PCAL6416A_CFGPORT0 + port, self._cfg[port]) + self._writeByte(PCAL6416A_PUPDEN_REG0 + port, self._pupden[port]) + self._writeByte(PCAL6416A_PUPDSEL_REG0 + port, self._pupdsel[port]) + + elif mode == INPUT_PULLDOWN: + self._cfg[port] |= 1 << bit + self._pupden[port] |= 1 << bit + self._pupdsel[port] &= ~(1 << bit) + self._writeByte(PCAL6416A_CFGPORT0 + port, self._cfg[port]) + self._writeByte(PCAL6416A_PUPDEN_REG0 + port, self._pupden[port]) + self._writeByte(PCAL6416A_PUPDSEL_REG0 + port, self._pupdsel[port]) + + def digitalWrite(self, pin, state): + """ + Set the output level on a pin configured as OUTPUT. + + :param pin: Pin number 0-15 + :param state: 0 (LOW) or 1 (HIGH) + """ + if pin > 15: + return + state &= 1 + port = pin // 8 + bit = pin % 8 + + if state: + self._out[port] |= 1 << bit + else: + self._out[port] &= ~(1 << bit) + self._writeByte(PCAL6416A_OUTPORT0 + port, self._out[port]) + + def digitalRead(self, pin): + """ + Read the current logic level of a pin. + + :param pin: Pin number 0-15 + :return: 0 (LOW) or 1 (HIGH), or -1 for invalid pin or I2C error + """ + if pin > 15: + return -1 + port = pin // 8 + bit = pin % 8 + + ok, val = self._readByte(PCAL6416A_INPORT0 + port) + if not ok: + return -1 + return (val >> bit) & 1 + + def pinPolarity(self, pin, state): + """ + Invert the polarity of an input pin (reads as opposite of actual level). + + :param pin: Pin number 0-15 + :param state: 1 to invert, 0 for normal polarity + """ + if pin > 15: + return + state &= 1 + port = pin // 8 + bit = pin % 8 + + if state: + self._pol[port] |= 1 << bit + else: + self._pol[port] &= ~(1 << bit) + self._writeByte(PCAL6416A_POLINVPORT0 + port, self._pol[port]) + + def driveStrength(self, pin, strength): + """ + Set the output drive strength for a pin. + + Each pin has a 2-bit drive strength field: 0 = weakest, 3 = strongest. + + :param pin: Pin number 0-15 + :param strength: 0 to 3 + """ + if pin > 15: + return + reg = pin // 4 # which of the four drive-strength registers + bit_pos = (pin % 4) * 2 # 2 bits per pin within that register + strength &= 3 + + self._drvst[reg] &= ~(3 << bit_pos) + self._drvst[reg] |= strength << bit_pos + self._writeByte(PCAL6416A_OUTDRVST_REG00 + reg, self._drvst[reg]) + + def setInterrupt(self, pin, enable): + """ + Enable or disable change-detection interrupt for a pin. + + The INT pin goes low when an unmasked input changes state. + + :param pin: Pin number 0-15 + :param enable: True to enable interrupt, False to mask it + """ + if pin > 15: + return + port = pin // 8 + bit = pin % 8 + + # Interrupt mask register: 0 = interrupt enabled, 1 = masked + if enable: + self._intmsk[port] &= ~(1 << bit) + else: + self._intmsk[port] |= 1 << bit + self._writeByte(PCAL6416A_INTMSK_REG0 + port, self._intmsk[port]) + + def getInterrupts(self): + """ + Read the interrupt status flags. The register clears automatically on read. + + :return: 16-bit value — bit N is set if pin N triggered an interrupt + """ + ok0, low = self._readByte(PCAL6416A_INTSTAT_REG0) + ok1, high = self._readByte(PCAL6416A_INTSTAT_REG1) + if not ok0 or not ok1: + return 0 + return (high << 8) | low + + def inputLatching(self, pin, enable): + """ + Enable input latching on a pin so that brief pulses are held until read. + + :param pin: Pin number 0-15 + :param enable: True to enable latching, False to disable + """ + if pin > 15: + return + port = pin // 8 + bit = pin % 8 + + if enable: + self._inlat[port] |= 1 << bit + else: + self._inlat[port] &= ~(1 << bit) + self._writeByte(PCAL6416A_INLAT_REG0 + port, self._inlat[port]) + + def openDrainPort(self, port, enable): + """ + Configure an entire port as open-drain (requires external pull-up) or push-pull. + + :param port: 0 for port A, 1 for port B + :param enable: True for open-drain, False for push-pull + """ + port &= 1 + if enable: + self._outconf |= 1 << port + else: + self._outconf &= ~(1 << port) + self._writeByte(PCAL6416A_OUTPORT_CONF, self._outconf) diff --git a/Communication/PCAL6416A/package.json b/Communication/PCAL6416A/package.json new file mode 100644 index 0000000..716a85d --- /dev/null +++ b/Communication/PCAL6416A/package.json @@ -0,0 +1,26 @@ +{ + "urls": [ + [ + "pcal6416a.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Communication/PCAL6416A/PCAL6416A/pcal6416a.py" + ], + [ + "Examples/pcal6416a-digitalRead.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-digitalRead.py" + ], + [ + "Examples/pcal6416a-digitalWrite.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-digitalWrite.py" + ], + [ + "Examples/pcal6416a-driveStrength.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-driveStrength.py" + ], + [ + "Examples/pcal6416a-interrupts.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Communication/PCAL6416A/PCAL6416A/Examples/pcal6416a-interrupts.py" + ] + ], + "deps": [], + "version": "1.0" +} diff --git a/README.md b/README.md index 9f8ad95..859ad51 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Each module in the library is designed to be lightweight, readable, and compatib ### Sensors - [AD8495](Sensors/AD8495/) +- [AK09918 (3-Axis Digital Compass)](Sensors/AK09918/) - [APDS9960](Sensors/APDS9960/) - [BHI385](Sensors/BHI385/) - [BME280](Sensors/BME280/) @@ -28,7 +29,9 @@ Each module in the library is designed to be lightweight, readable, and compatib - [BMP180](Sensors/BMP180/) - [BMP280](Sensors/BMP280/) - [BMP388](Sensors/BMP388/) +- [DE2120](Sensors/DE2120/) - [HallEffect](Sensors/HallEffect/) +- [IIS2DULPX](Sensors/IIS2DULPX/) - [LaserDistanceSensor](Sensors/LaserDistanceSensor/) - [LTR507](Sensors/LTR507/) - [ObstacleSensor](Sensors/ObstacleSensor/) @@ -45,6 +48,7 @@ Each module in the library is designed to be lightweight, readable, and compatib - [WS2812](Actuators/WS2812/) ### Communication +- [PCAL6416A](Communication/PCAL6416A/) - [RFID](Communication/RFID/) ### Displays diff --git a/Sensors/AK09918/AK09918/Examples/ak09918-compassHeading.py b/Sensors/AK09918/AK09918/Examples/ak09918-compassHeading.py new file mode 100644 index 0000000..4dd76b1 --- /dev/null +++ b/Sensors/AK09918/AK09918/Examples/ak09918-compassHeading.py @@ -0,0 +1,69 @@ +# FILE: ak09918-compassHeading.py +# AUTHOR: Josip Šimun Kuči @ Soldered +# BRIEF: Calculate a compass heading from AK09918 X and Y readings and print +# the heading in degrees along with the nearest cardinal direction +# WORKS WITH: 3-Axis Digital Compass AK09918 breakout: www.solde.red/333392 +# LAST UPDATED: 2026-04-23 + +# The heading is computed with atan2(Y, X), which gives the angle of the +# horizontal magnetic field vector relative to the sensor's X axis. +# IMPORTANT: keep the board flat (horizontal) while testing — tilting +# introduces error because the vertical component of Earth's field leaks +# into X and Y. Full tilt compensation requires a separate accelerometer. + +from machine import Pin, I2C +from ak09918 import AK09918 +from ak09918_constants import AK09918_CONT_MEASURE_MODE1, AK09918_ERR_OK +import math +import time + + +def headingLabel(deg): + """Return the nearest cardinal/intercardinal label for a 0-360 degree heading.""" + if deg < 22.5 or deg >= 337.5: + return "N" + if deg < 67.5: + return "NE" + if deg < 112.5: + return "E" + if deg < 157.5: + return "SE" + if deg < 202.5: + return "S" + if deg < 247.5: + return "SW" + if deg < 292.5: + return "W" + return "NW" + + +# If you aren't using the Qwiic connector, manually enter your I2C pins +# i2c = I2C(0, scl=Pin(22), sda=Pin(21)) +# compass = AK09918(i2c, mode=AK09918_CONT_MEASURE_MODE1) + +# Initialize sensor over Qwiic in 10 Hz continuous measurement mode +compass = AK09918(mode=AK09918_CONT_MEASURE_MODE1) + +print("AK09918 compass heading - keep the board flat!") +print("Heading\t\tDirection\tX\t\tY\t\tZ") + +# Infinite loop +while True: + if compass.isDataReady() != AK09918_ERR_OK: + continue + + err, x, y, z = compass.getData() + if err != AK09918_ERR_OK: + print("Read error: {}".format(compass.strError(err))) + continue + + # atan2 returns radians in range -pi to +pi; convert to 0-360 degrees + heading = math.atan2(y, x) * 180.0 / math.pi + if heading < 0.0: + heading += 360.0 + + print( + "{:.1f}\t\t{}\t\t{}\t\t{}\t\t{}".format(heading, headingLabel(heading), x, y, z) + ) + + time.sleep_ms(200) diff --git a/Sensors/AK09918/AK09918/Examples/ak09918-readValues.py b/Sensors/AK09918/AK09918/Examples/ak09918-readValues.py new file mode 100644 index 0000000..22e6bd8 --- /dev/null +++ b/Sensors/AK09918/AK09918/Examples/ak09918-readValues.py @@ -0,0 +1,34 @@ +# FILE: ak09918-readValues.py +# AUTHOR: Josip Šimun Kuči @ Soldered +# BRIEF: Read X, Y, Z magnetic field values from the AK09918 in continuous +# measurement mode and print them to the console +# WORKS WITH: 3-Axis Digital Compass AK09918 breakout: www.solde.red/333392 +# LAST UPDATED: 2026-04-23 + +from machine import Pin, I2C +from ak09918 import AK09918 +from ak09918_constants import AK09918_CONT_MEASURE_MODE1, AK09918_ERR_OK + +# If you aren't using the Qwiic connector, manually enter your I2C pins +# i2c = I2C(0, scl=Pin(22), sda=Pin(21)) +# compass = AK09918(i2c, mode=AK09918_CONT_MEASURE_MODE1) + +# Initialize sensor over Qwiic in 10 Hz continuous measurement mode +compass = AK09918(mode=AK09918_CONT_MEASURE_MODE1) + +print("AK09918 ready - reading at 10 Hz") +print("X (uT*10)\tY (uT*10)\tZ (uT*10)") + +# Infinite loop +while True: + # Wait until a new measurement is available + if compass.isDataReady() != AK09918_ERR_OK: + continue + + err, x, y, z = compass.getData() + if err != AK09918_ERR_OK: + print("Read error: {}".format(compass.strError(err))) + continue + + # Print the X, Y, Z values + print("{}\t\t{}\t\t{}".format(x, y, z)) diff --git a/Sensors/AK09918/AK09918/Examples/ak09918-selfTest.py b/Sensors/AK09918/AK09918/Examples/ak09918-selfTest.py new file mode 100644 index 0000000..c5340e5 --- /dev/null +++ b/Sensors/AK09918/AK09918/Examples/ak09918-selfTest.py @@ -0,0 +1,37 @@ +# FILE: ak09918-selfTest.py +# AUTHOR: Josip Šimun Kuči @ Soldered +# BRIEF: Run the AK09918's built-in hardware self-test to verify the sensor +# is working correctly +# WORKS WITH: 3-Axis Digital Compass AK09918 breakout: www.solde.red/333392 +# LAST UPDATED: 2026-04-23 + +# The self-test energises an internal coil that applies a known magnetic field +# to the sensing elements. The driver checks that the resulting X, Y, Z outputs +# fall within the limits defined in the datasheet: +# X: -200 to +200 counts +# Y: -200 to +200 counts +# Z: -1000 to -150 counts +# Run this once after assembly to confirm the sensor is functional. + +from machine import Pin, I2C +from ak09918 import AK09918 +from ak09918_constants import AK09918_ERR_OK + +# If you aren't using the Qwiic connector, manually enter your I2C pins +# i2c = I2C(0, scl=Pin(22), sda=Pin(21)) +# compass = AK09918(i2c) + +# Initialize sensor in power-down; selfTest() manages mode transitions itself +compass = AK09918() + +device_id = compass.getDeviceID() +print("Device ID: 0x{:04X}".format(device_id)) +# Expected: 0x480A (company ID 0x48, device ID 0x0A) + +print("Running self-test...") +err = compass.selfTest() + +if err == AK09918_ERR_OK: + print("Self-test PASSED") +else: + print("Self-test FAILED: {}".format(compass.strError(err))) diff --git a/Sensors/AK09918/AK09918/Examples/ak09918-singleMeasurement.py b/Sensors/AK09918/AK09918/Examples/ak09918-singleMeasurement.py new file mode 100644 index 0000000..565a231 --- /dev/null +++ b/Sensors/AK09918/AK09918/Examples/ak09918-singleMeasurement.py @@ -0,0 +1,48 @@ +# FILE: ak09918-singleMeasurement.py +# AUTHOR: Josip Šimun Kuči @ Soldered +# BRIEF: Take one magnetic field measurement at a time using AK09918 +# single-measurement (NORMAL) mode +# WORKS WITH: 3-Axis Digital Compass AK09918 breakout: www.solde.red/333392 +# LAST UPDATED: 2026-04-23 + +# In single-measurement mode the sensor powers itself down after each reading. +# getData() triggers the measurement and blocks until it is complete, so no +# polling loop is needed. This mode is ideal for battery-powered applications. + +from machine import Pin, I2C +from ak09918 import AK09918 +from ak09918_constants import ( + AK09918_NORMAL, + AK09918_ERR_OK, + AK09918_ERR_TIMEOUT, + AK09918_ERR_OVERFLOW, +) +import time + +# If you aren't using the Qwiic connector, manually enter your I2C pins +# i2c = I2C(0, scl=Pin(22), sda=Pin(21)) +# compass = AK09918(i2c, mode=AK09918_NORMAL) + +# Initialize sensor in single-measurement mode +compass = AK09918(mode=AK09918_NORMAL) + +print("AK09918 ready - single-measurement mode") +print("One reading per second") +print("X (uT*10)\tY (uT*10)\tZ (uT*10)") + +# Infinite loop +while True: + # getData() triggers the measurement and blocks (~1 ms) until done + err, x, y, z = compass.getData() + + if err == AK09918_ERR_TIMEOUT: + print("Timeout - sensor did not respond") + elif err == AK09918_ERR_OVERFLOW: + print("Overflow - field too strong to measure") + elif err != AK09918_ERR_OK: + print("Error: {}".format(compass.strError(err))) + else: + # Print the X, Y, Z values + print("{}\t\t{}\t\t{}".format(x, y, z)) + + time.sleep(1) diff --git a/Sensors/AK09918/AK09918/ak09918.py b/Sensors/AK09918/AK09918/ak09918.py new file mode 100644 index 0000000..a467f0a --- /dev/null +++ b/Sensors/AK09918/AK09918/ak09918.py @@ -0,0 +1,285 @@ +# FILE: ak09918.py +# AUTHOR: Josip Šimun Kuči @ Soldered +# BRIEF: MicroPython library for the AK09918 3-axis digital compass sensor +# LAST UPDATED: 2026-04-23 + +import time +from machine import I2C, Pin +from os import uname +from ak09918_constants import ( + AK09918_I2C_ADDR, + AK09918_WIA1, + AK09918_ST1, + AK09918_HXL, + AK09918_CNTL2, + AK09918_CNTL3, + AK09918_DRDY_BIT, + AK09918_DOR_BIT, + AK09918_HOFL_BIT, + AK09918_SRST_BIT, + AK09918_ERR_OK, + AK09918_ERR_DOR, + AK09918_ERR_NOT_RDY, + AK09918_ERR_TIMEOUT, + AK09918_ERR_SELFTEST_FAILED, + AK09918_ERR_OVERFLOW, + AK09918_ERR_WRITE_FAILED, + AK09918_ERR_READ_FAILED, + AK09918_POWER_DOWN, + AK09918_NORMAL, + AK09918_SELF_TEST, +) + + +class AK09918: + """ + MicroPython class for the AK09918 3-axis digital compass sensor. + Supports single-measurement, continuous, and self-test modes. + """ + + def __init__(self, i2c=None, address=AK09918_I2C_ADDR, mode=AK09918_POWER_DOWN): + """ + Initialize the AK09918 sensor. + + :param i2c: Initialized I2C object + :param address: I2C address of the sensor (default 0x0C) + :param mode: Initial operating mode (default AK09918_POWER_DOWN) + """ + if i2c != None: + self.i2c = i2c + else: + if uname().sysname in ("esp32", "esp8266", "Soldered Dasduino CONNECTPLUS"): + self.i2c = I2C(0, scl=Pin(22), sda=Pin(21)) + else: + raise Exception("Board not recognized, enter I2C pins manually") + + self.address = address + self._mode = AK09918_POWER_DOWN + + if mode == AK09918_SELF_TEST: + mode = AK09918_POWER_DOWN + + if mode != AK09918_POWER_DOWN: + self.switchMode(mode) + + # Read a single byte from a register, returns (success, value) + def _readByte(self, reg): + try: + data = self.i2c.readfrom_mem(self.address, reg, 1) + return True, data[0] + except: + return False, 0 + + # Read multiple bytes starting from a register, returns (success, bytes) + def _readBytes(self, reg, count): + try: + data = self.i2c.readfrom_mem(self.address, reg, count) + return True, data + except: + return False, bytes(count) + + # Write a single byte to a register, returns success bool + def _writeByte(self, reg, val): + try: + self.i2c.writeto_mem(self.address, reg, bytes([val])) + return True + except: + return False + + # Read the current raw mode value from CNTL2 + def _getRawMode(self): + ok, val = self._readByte(AK09918_CNTL2) + if not ok: + return 0xFF + return val + + def isDataReady(self): + """ + Check whether new measurement data is available. + + :return: AK09918_ERR_OK if data is ready, AK09918_ERR_NOT_RDY if not, + or AK09918_ERR_READ_FAILED on a bus error + """ + ok, val = self._readByte(AK09918_ST1) + if not ok: + return AK09918_ERR_READ_FAILED + return AK09918_ERR_OK if (val & AK09918_DRDY_BIT) else AK09918_ERR_NOT_RDY + + def isDataSkip(self): + """ + Check whether any measurement was overwritten before being read. + + :return: AK09918_ERR_DOR if data was skipped, AK09918_ERR_OK if not, + or AK09918_ERR_READ_FAILED on a bus error + """ + ok, val = self._readByte(AK09918_ST1) + if not ok: + return AK09918_ERR_READ_FAILED + return AK09918_ERR_DOR if (val & AK09918_DOR_BIT) else AK09918_ERR_OK + + def getRawData(self): + """ + Read raw ADC values directly from the output registers. + + For AK09918_NORMAL mode, triggers a single measurement and blocks until complete. + For continuous modes, reads the latest available sample immediately. + + :return: (err, x, y, z) raw ADC counts as signed integers + """ + if self._mode == AK09918_NORMAL: + self.switchMode(AK09918_NORMAL) + retries = 0 + while self._getRawMode() != 0x00: + if retries >= 15: + return AK09918_ERR_TIMEOUT, 0, 0, 0 + retries += 1 + time.sleep_ms(1) + + ok, buf = self._readBytes(AK09918_HXL, 8) + if not ok: + return AK09918_ERR_READ_FAILED, 0, 0, 0 + + x = (buf[1] << 8) | buf[0] + y = (buf[3] << 8) | buf[2] + z = (buf[5] << 8) | buf[4] + + # Convert to signed 16-bit + if x > 32767: + x -= 65536 + if y > 32767: + y -= 65536 + if z > 32767: + z -= 65536 + + if buf[7] & AK09918_HOFL_BIT: + return AK09918_ERR_OVERFLOW, x, y, z + + return AK09918_ERR_OK, x, y, z + + def getData(self): + """ + Read calibrated magnetic field values. + + Each raw ADC count is multiplied by 0.15 (sensor sensitivity in µT/LSB). + The result is an integer where 1 unit ≈ 0.15 µT. + + For AK09918_NORMAL mode, triggers a fresh single measurement. + For continuous modes, reads the latest available sample. + + :return: (err, x, y, z) calibrated values + """ + err, x, y, z = self.getRawData() + x = x * 15 // 100 + y = y * 15 // 100 + z = z * 15 // 100 + return err, x, y, z + + def getMode(self): + """ + Return the currently active operating mode. + + :return: The mode constant last written to CNTL2 + """ + return self._mode + + def switchMode(self, mode): + """ + Change the sensor operating mode. + + AK09918_SELF_TEST cannot be set through this function; use selfTest() instead. + + :param mode: New mode to apply + :return: AK09918_ERR_OK on success, AK09918_ERR_WRITE_FAILED on failure + """ + if mode == AK09918_SELF_TEST: + return AK09918_ERR_WRITE_FAILED + self._mode = mode + if not self._writeByte(AK09918_CNTL2, mode): + return AK09918_ERR_WRITE_FAILED + return AK09918_ERR_OK + + def selfTest(self): + """ + Execute the built-in hardware self-test. + + Powers the sensor down, triggers the self-test sequence, and verifies that + the output falls within datasheet limits (X/Y: ±200 counts, Z: −1000 to −150). + + :return: AK09918_ERR_OK if the test passes, AK09918_ERR_SELFTEST_FAILED if + output is out of range, or an I2C error code on communication failure + """ + if not self._writeByte(AK09918_CNTL2, AK09918_POWER_DOWN): + return AK09918_ERR_WRITE_FAILED + time.sleep_ms(1) + if not self._writeByte(AK09918_CNTL2, AK09918_SELF_TEST): + return AK09918_ERR_WRITE_FAILED + + while True: + err = self.isDataReady() + if err == AK09918_ERR_READ_FAILED: + return AK09918_ERR_READ_FAILED + if err == AK09918_ERR_OK: + break + time.sleep_ms(1) + + ok, buf = self._readBytes(AK09918_HXL, 8) + if not ok: + return AK09918_ERR_READ_FAILED + + x = (buf[1] << 8) | buf[0] + y = (buf[3] << 8) | buf[2] + z = (buf[5] << 8) | buf[4] + + if x > 32767: + x -= 65536 + if y > 32767: + y -= 65536 + if z > 32767: + z -= 65536 + + if -200 <= x <= 200 and -200 <= y <= 200 and -1000 <= z <= -150: + return AK09918_ERR_OK + return AK09918_ERR_SELFTEST_FAILED + + def reset(self): + """ + Perform a software reset. All registers return to power-on defaults. + + :return: AK09918_ERR_OK on success, AK09918_ERR_WRITE_FAILED on failure + """ + if not self._writeByte(AK09918_CNTL3, AK09918_SRST_BIT): + return AK09918_ERR_WRITE_FAILED + return AK09918_ERR_OK + + def strError(self, err): + """ + Convert an error code into a human-readable string. + + :param err: Error code to describe + :return: String describing the error + """ + errors = { + AK09918_ERR_OK: "OK", + AK09918_ERR_DOR: "Data overrun: measurement was overwritten before being read", + AK09918_ERR_NOT_RDY: "Not ready: measurement still in progress", + AK09918_ERR_TIMEOUT: "Timeout: sensor did not complete measurement in time", + AK09918_ERR_SELFTEST_FAILED: "Self-test failed: output out of specified range", + AK09918_ERR_OVERFLOW: "Overflow: magnetic field exceeded measurable range", + AK09918_ERR_WRITE_FAILED: "Write failed: I2C write error", + AK09918_ERR_READ_FAILED: "Read failed: I2C read error", + } + return errors.get(err, "Unknown error") + + def getDeviceID(self): + """ + Read the device identification registers. + + WIA1 contains the company ID (0x48) and WIA2 contains the device ID (0x0A). + The expected combined value is 0x480A. + + :return: 16-bit value with WIA1 in the high byte and WIA2 in the low byte + """ + ok, buf = self._readBytes(AK09918_WIA1, 2) + if not ok: + return 0 + return (buf[0] << 8) | buf[1] diff --git a/Sensors/AK09918/AK09918/ak09918_constants.py b/Sensors/AK09918/AK09918/ak09918_constants.py new file mode 100644 index 0000000..37f788c --- /dev/null +++ b/Sensors/AK09918/AK09918/ak09918_constants.py @@ -0,0 +1,47 @@ +# FILE: ak09918_constants.py +# AUTHOR: Josip Šimun Kuči @ Soldered +# BRIEF: Register definitions, error codes, and operating modes for the AK09918 sensor +# LAST UPDATED: 2026-04-23 + +# I2C address +AK09918_I2C_ADDR = 0x0C + +# Register addresses +AK09918_WIA1 = 0x00 # Company ID register +AK09918_WIA2 = 0x01 # Device ID register +AK09918_ST1 = 0x10 # Status register 1 (data ready / overrun) +AK09918_HXL = 0x11 # X-axis measurement low byte +AK09918_HXH = 0x12 # X-axis measurement high byte +AK09918_HYL = 0x13 # Y-axis measurement low byte +AK09918_HYH = 0x14 # Y-axis measurement high byte +AK09918_HZL = 0x15 # Z-axis measurement low byte +AK09918_HZH = 0x16 # Z-axis measurement high byte +AK09918_TMPS = 0x17 # Dummy register (must be read as part of 8-byte block) +AK09918_ST2 = 0x18 # Status register 2 (overflow flag) +AK09918_CNTL2 = 0x31 # Control register 2 (operating mode) +AK09918_CNTL3 = 0x32 # Control register 3 (soft reset) + +# Status register bits +AK09918_DRDY_BIT = 0x01 # Data ready +AK09918_DOR_BIT = 0x02 # Data overrun +AK09918_HOFL_BIT = 0x08 # Hall sensor overflow +AK09918_SRST_BIT = 0x01 # Soft reset + +# Error codes +AK09918_ERR_OK = 0 +AK09918_ERR_DOR = 1 +AK09918_ERR_NOT_RDY = 2 +AK09918_ERR_TIMEOUT = 3 +AK09918_ERR_SELFTEST_FAILED = 4 +AK09918_ERR_OVERFLOW = 5 +AK09918_ERR_WRITE_FAILED = 6 +AK09918_ERR_READ_FAILED = 7 + +# Operating modes +AK09918_POWER_DOWN = 0x00 # Power-down mode +AK09918_NORMAL = 0x01 # Single measurement mode (auto power-down after each reading) +AK09918_CONT_MEASURE_MODE1 = 0x02 # Continuous measurement at 10 Hz +AK09918_CONT_MEASURE_MODE2 = 0x04 # Continuous measurement at 20 Hz +AK09918_CONT_MEASURE_MODE3 = 0x06 # Continuous measurement at 50 Hz +AK09918_CONT_MEASURE_MODE4 = 0x08 # Continuous measurement at 100 Hz +AK09918_SELF_TEST = 0x10 # Self-test mode diff --git a/Sensors/AK09918/package.json b/Sensors/AK09918/package.json new file mode 100644 index 0000000..969c967 --- /dev/null +++ b/Sensors/AK09918/package.json @@ -0,0 +1,30 @@ +{ + "urls": [ + [ + "ak09918.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Sensors/AK09918/AK09918/ak09918.py" + ], + [ + "ak09918_constants.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Sensors/AK09918/AK09918/ak09918_constants.py" + ], + [ + "Examples/ak09918-readValues.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Sensors/AK09918/AK09918/Examples/ak09918-readValues.py" + ], + [ + "Examples/ak09918-compassHeading.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Sensors/AK09918/AK09918/Examples/ak09918-compassHeading.py" + ], + [ + "Examples/ak09918-singleMeasurement.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Sensors/AK09918/AK09918/Examples/ak09918-singleMeasurement.py" + ], + [ + "Examples/ak09918-selfTest.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Sensors/AK09918/AK09918/Examples/ak09918-selfTest.py" + ] + ], + "deps": [], + "version": "1.0" +} diff --git a/Sensors/DE2120/DE2120/Examples/de2120-continuousRead.py b/Sensors/DE2120/DE2120/Examples/de2120-continuousRead.py new file mode 100644 index 0000000..b85f413 --- /dev/null +++ b/Sensors/DE2120/DE2120/Examples/de2120-continuousRead.py @@ -0,0 +1,28 @@ +# FILE: de2120-continuousRead.py +# AUTHOR: Fran Fodor @ Soldered +# BRIEF: Example for reading barcodes in continuous mode — the scanner reads +# automatically without needing startScan() calls +# WORKS WITH: Barcode scanner DE2120 breakout: www.solde.red/333182 +# LAST UPDATED: 2026-04-23 + +from machine import UART +from de2120 import DE2120 +import time + +# Adjust tx and rx to match the pins you have wired on your board. +scanner = DE2120(tx=5, rx=4) + +if not scanner.begin(): + print("DE2120 not detected. Check wiring.") + raise SystemExit + +# Enable continuous read with a 0.5 s repeat interval for the same barcode +scanner.enableContinuousRead(repeat_interval=2) + +print("DE2120 in continuous read mode. Point at a barcode.") + +while True: + barcode = scanner.readBarcode() + if barcode: + print("Scanned:", barcode) + time.sleep_ms(50) diff --git a/Sensors/DE2120/DE2120/Examples/de2120-readBarcode.py b/Sensors/DE2120/DE2120/Examples/de2120-readBarcode.py new file mode 100644 index 0000000..ff6a5a0 --- /dev/null +++ b/Sensors/DE2120/DE2120/Examples/de2120-readBarcode.py @@ -0,0 +1,27 @@ +# FILE: de2120-readBarcode.py +# AUTHOR: Fran Fodor @ Soldered +# BRIEF: Read barcodes with the DE2120 scanner in manual trigger mode. +# Press the button on the scanner breakout to trigger a scan. +# WORKS WITH: Barcode scanner DE2120 breakout: www.solde.red/333182 +# LAST UPDATED: 2026-04-23 + +from machine import UART +from de2120 import DE2120 +import time + +# Adjust tx and rx to match the pins you have wired on your board. +scanner = DE2120(tx=5, rx=4) + +if not scanner.begin(): + print("DE2120 not detected. Check wiring.") + raise SystemExit + +print("DE2120 ready. Press the button on the scanner to read a barcode.") + +while True: + # The scanner sends data when its physical button is pressed — + # just poll readBarcode() and print whatever arrives. + barcode = scanner.readBarcode() + if barcode: + print("Scanned:", barcode) + time.sleep_ms(20) diff --git a/Sensors/DE2120/DE2120/Examples/de2120-sendCommand.py b/Sensors/DE2120/DE2120/Examples/de2120-sendCommand.py new file mode 100644 index 0000000..12a8e2a --- /dev/null +++ b/Sensors/DE2120/DE2120/Examples/de2120-sendCommand.py @@ -0,0 +1,62 @@ +# FILE: de2120-sendCommand.py +# AUTHOR: Fran Fodor @ Soldered +# BRIEF: Toggle Code ID prefix transmission on the DE2120 scanner. +# When enabled, the scanner prepends a symbology identifier to every +# scan (e.g. 'A' for Code 128, 'Q' for QR Code). +# Type 'y' or 'n' in the REPL to enable or disable it. +# Barcodes scanned while waiting are also printed. +# WORKS WITH: Barcode scanner DE2120 breakout: www.solde.red/333182 +# LAST UPDATED: 2026-04-23 + +import sys +import uselect +from de2120 import DE2120 +import time + +scanner = DE2120(tx=5, rx=4) + +if not scanner.begin(): + print("Scanner did not respond. Check wiring. Did you scan the POR232 barcode?") + raise SystemExit + +print("Scanner online!") + +# Set up non-blocking stdin so we can poll for barcodes while waiting for input +poll = uselect.poll() +poll.register(sys.stdin, uselect.POLLIN) + + +def stdin_available(): + return bool(poll.poll(0)) + + +def flush_stdin(): + while stdin_available(): + sys.stdin.read(1) + + +while True: + flush_stdin() + + print() + print("Transmit Code ID with barcode? (y/n)") + print("-------------------------------------") + print("Type 'y' or 'n', or scan a barcode:") + + while not stdin_available(): + barcode = scanner.readBarcode() + if barcode: + print("...") + print("Code found:", barcode) + time.sleep_ms(200) + + cmd = sys.stdin.read(1) + + if cmd == "y": + print("Code ID will be displayed on scan") + scanner.sendCommand("CIDENA", "1") + elif cmd == "n": + print("Code ID will NOT be displayed on scan") + scanner.sendCommand("CIDENA", "0") + else: + print("Command not recognized") diff --git a/Sensors/DE2120/DE2120/Examples/de2120-serialSettings.py b/Sensors/DE2120/DE2120/Examples/de2120-serialSettings.py new file mode 100644 index 0000000..30e6bed --- /dev/null +++ b/Sensors/DE2120/DE2120/Examples/de2120-serialSettings.py @@ -0,0 +1,194 @@ +# FILE: de2120-serialSettings.py +# AUTHOR: Fran Fodor @ Soldered +# BRIEF: Interactive configuration menu for the DE2120 scanner. +# Control scanning, flashlight, reticle, reading area, reading mode, +# and symbologies from the REPL. Barcodes scanned while the menu is +# displayed are also printed. +# WORKS WITH: Barcode scanner DE2120 breakout: www.solde.red/333182 +# LAST UPDATED: 2026-04-23 + +import sys +import uselect +from de2120 import DE2120 +import time + +scanner = DE2120(tx=5, rx=4) + +if not scanner.begin(): + print("Scanner did not respond. Check wiring. Did you scan the POR232 barcode?") + raise SystemExit + +print("Scanner online!") + +poll = uselect.poll() +poll.register(sys.stdin, uselect.POLLIN) + + +def stdin_available(): + return bool(poll.poll(0)) + + +def flush_stdin(): + while stdin_available(): + sys.stdin.read(1) + + +def wait_for_key(): + """Block until a key is typed, printing any scanned barcodes in the meantime.""" + while not stdin_available(): + barcode = scanner.readBarcode() + if barcode: + print("...") + print("Code found:", barcode) + time.sleep_ms(200) + return sys.stdin.read(1) + + +def menu_flashlight(): + flush_stdin() + print() + print("-------------------------------------") + print("1) Enable Flashlight") + print("2) Disable Flashlight") + print("-------------------------------------") + print("Select an option number:") + + cmd = wait_for_key() + if cmd == "1": + print("White scan light on") + scanner.lightOn() + elif cmd == "2": + print("White scan light off") + scanner.lightOff() + else: + print("Command not recognized") + + +def menu_reticle(): + flush_stdin() + print() + print("-------------------------------------") + print("1) Enable Reticle") + print("2) Disable Reticle") + print("-------------------------------------") + print("Select an option number:") + + cmd = wait_for_key() + if cmd == "1": + print("Red scan reticle on") + scanner.reticleOn() + elif cmd == "2": + print("Red scan reticle off") + scanner.reticleOff() + else: + print("Command not recognized") + + +def menu_reading_area(): + flush_stdin() + print() + print("-------------------------------------") + print("1) Full Width (Default)") + print("2) Center 80%") + print("3) Center 60%") + print("4) Center 40%") + print("5) Center 20%") + print("-------------------------------------") + print("Select an option number:") + + cmd = wait_for_key() + areas = {"1": 100, "2": 80, "3": 60, "4": 40, "5": 20} + if cmd in areas: + pct = areas[cmd] + print("Scanning {}% of frame".format(pct)) + scanner.changeReadingArea(pct) + else: + print("Command not recognized") + + +def menu_reading_mode(): + flush_stdin() + print() + print("-------------------------------------") + print("1) Manual Read Mode (Default)") + print("2) Continuous Read Mode") + print("3) Motion Sensor Mode") + print("-------------------------------------") + print("Select an option number:") + + cmd = wait_for_key() + if cmd == "1": + print("Manual Trigger Mode enabled") + scanner.disableMotionSense() + elif cmd == "2": + print("Continuous Read Mode enabled") + scanner.enableContinuousRead() + elif cmd == "3": + print("Motion Trigger Mode enabled") + scanner.enableMotionSense() + else: + print("Command not recognized") + + +def menu_symbologies(): + flush_stdin() + print() + print("-------------------------------------") + print("1) Enable All 1D Symbologies") + print("2) Disable All 1D Symbologies") + print("3) Enable All 2D Symbologies") + print("4) Disable All 2D Symbologies") + print("-------------------------------------") + print("Select an option number:") + + cmd = wait_for_key() + if cmd == "1": + print("1D Symbologies enabled") + scanner.enableAll1D() + elif cmd == "2": + print("1D Symbologies disabled") + scanner.disableAll1D() + elif cmd == "3": + print("2D Symbologies enabled") + scanner.enableAll2D() + elif cmd == "4": + print("2D Symbologies disabled") + scanner.disableAll2D() + else: + print("Command not recognized") + + +while True: + flush_stdin() + + print() + print("DE2120 Barcode Scanner") + print("-------------------------------------") + print("1) Start Scan") + print("2) Stop Scan") + print("3) Enable/Disable Flashlight") + print("4) Enable/Disable Aiming Reticle") + print("5) Set Reading Area") + print("6) Set Reading Mode") + print("7) Enable/Disable Symbologies") + print("-------------------------------------") + print("Select an option number:") + + cmd = wait_for_key() + + if cmd == "1": + scanner.startScan() + elif cmd == "2": + scanner.stopScan() + elif cmd == "3": + menu_flashlight() + elif cmd == "4": + menu_reticle() + elif cmd == "5": + menu_reading_area() + elif cmd == "6": + menu_reading_mode() + elif cmd == "7": + menu_symbologies() + else: + print("Command not recognized") diff --git a/Sensors/DE2120/DE2120/de2120.py b/Sensors/DE2120/DE2120/de2120.py new file mode 100644 index 0000000..a0c1ef2 --- /dev/null +++ b/Sensors/DE2120/DE2120/de2120.py @@ -0,0 +1,276 @@ +# FILE: de2120.py +# AUTHOR: Fran Fodor @ Soldered +# BRIEF: MicroPython library for the DE2120 2D Barcode Scanner Engine +# LAST UPDATED: 2026-04-23 + +from machine import UART +import time + +# ACK / NACK bytes returned by the module after a command +DE2120_COMMAND_ACK = 0x06 +DE2120_COMMAND_NACK = 0x15 + +# Commands (sent as ^_^.) +COMMAND_START_SCAN = "SCAN" +COMMAND_STOP_SCAN = "SLEEP" +COMMAND_SET_DEFAULTS = "DEFALT" +COMMAND_GET_VERSION = "DSPYFW" + +# Configurable properties +PROPERTY_BUZZER_FREQ = "BEPPWM" +# BEPPWM0 - Active Drive +# BEPPWM1 - Passive Low Freq +# BEPPWM2 - Passive Med Freq (default) +# BEPPWM3 - Passive Hi Freq + +PROPERTY_DECODE_BEEP = "BEPSUC" +# BEPSUC1 - ON (default) BEPSUC0 - OFF + +PROPERTY_BOOT_BEEP = "BEPPWR" +# BEPPWR1 - ON (default) BEPPWR0 - OFF + +PROPERTY_FLASH_LIGHT = "LAMENA" +# LAMENA1 - ON (default) LAMENA0 - OFF + +PROPERTY_AIM_LIGHT = "AIMENA" +# AIMENA1 - ON (default) AIMENA0 - OFF + +PROPERTY_READING_AREA = "IMGREG" +# IMGREG0 - Full (default) IMGREG1 - 80% IMGREG2 - 60% +# IMGREG3 - 40% IMGREG4 - 20% + +PROPERTY_MIRROR_FLIP = "MIRLRE" +# MIRLRE1 - ON MIRLRE0 - OFF (default) + +PROPERTY_USB_DATA_FORMAT = "UTFEAN" +PROPERTY_SERIAL_DATA_FORMAT = "232UTF" +PROPERTY_INVOICE_MODE = "SPCINV" +PROPERTY_VIRTUAL_KEYBOARD = "KBDVIR" + +PROPERTY_COMM_MODE = "POR" +# PORKBD - USB-KBW PORHID - USB-HID PORVIC - USB-COM POR232 - TTL/RS232 + +PROPERTY_BAUD_RATE = "232BAD" +# 232BAD2=1200 232BAD3=2400 232BAD4=4800 232BAD5=9600 +# 232BAD6=19200 232BAD7=38400 232BAD8=57600 232BAD9=115200 (default) + +PROPERTY_READING_MODE = "SCM" +# SCMMAN - Manual (default) SCMCNT - Continuous SCMMDH - Motion + +PROPERTY_CONTINUOUS_MODE_INTERVAL = "CNTALW" +# CNTALW0 - Once CNTALW1 - No interval +# CNTALW2 - 0.5 s interval CNTALW3 - 1 s interval + +PROPERTY_MOTION_SENSITIVITY = "MDTTHR" +# MDTTHR15 - Highest MDTTHR20 - High (default) +# MDTTHR30 MDTTHR50 MDTTHR100 - Lowest + +PROPERTY_TRANSFER_CODE_ID = "CIDENA" +PROPERTY_KBD_CASE_CONVERSION = "KBDCNV" + +PROPERTY_ENABLE_ALL_1D = "ODCENA" +PROPERTY_DISABLE_ALL_1D = "ODCDIS" +PROPERTY_ENABLE_ALL_2D = "AQRENA" +PROPERTY_DISABLE_ALL_2D = "AQRDIS" + +_BAUD_MAP = { + 1200: "2", + 2400: "3", + 4800: "4", + 9600: "5", + 19200: "6", + 38400: "7", + 57600: "8", + 115200: "9", +} +_AREA_MAP = {100: "0", 80: "1", 60: "2", 40: "3", 20: "4"} +_MOTION_VALID = frozenset({15, 20, 30, 50, 100}) +_MAX_BARCODE_LEN = 256 + + +class DE2120: + """Driver for the DE2120 2D barcode scanner engine over UART.""" + + def __init__(self, uart=None, tx=None, rx=None, uart_id=1): + """ + Initialize with either a pre-built UART object or tx/rx pin numbers. + If tx/rx are given, a UART(uart_id) is created internally at 9600 baud. + """ + if uart is not None: + self._uart = uart + elif tx is not None and rx is not None: + self._uart = UART(uart_id, baudrate=9600, tx=tx, rx=rx) + else: + raise ValueError("Provide a UART object or tx and rx pin numbers") + self._rx_buf = bytearray() + + def begin(self): + """Verify communication with the scanner. Returns True on success.""" + if not self._isConnected(): + return False + while self._uart.any(): + self._uart.read(1) + return True + + def _isConnected(self): + self._uart.init(baudrate=9600) + if self._sendCommand(COMMAND_GET_VERSION, "", 800): + return True + + self._uart.init(baudrate=115200) + time.sleep_ms(10) + self._sendCommand(PROPERTY_BAUD_RATE, "5", 500) + + self._uart.init(baudrate=9600) + time.sleep_ms(10) + return self._sendCommand(COMMAND_GET_VERSION, "", 800) + + def factoryDefault(self): + """Reset scanner to all factory default settings.""" + return self._sendCommand(COMMAND_SET_DEFAULTS) + + def available(self): + """Return True if bytes are waiting in the UART receive buffer.""" + return self._uart.any() > 0 + + def read(self): + """Read one raw byte from the UART buffer, or -1 if none available.""" + b = self._uart.read(1) + return b[0] if b else -1 + + def _sendCommand(self, cmd, arg="", max_wait_ms=3000): + self._uart.write(("^_^" + cmd + arg + ".").encode()) + deadline = time.ticks_add(time.ticks_ms(), max_wait_ms) + while time.ticks_diff(deadline, time.ticks_ms()) > 0: + while self._uart.any(): + b = self._uart.read(1) + if b: + if b[0] == DE2120_COMMAND_ACK: + return True + if b[0] == DE2120_COMMAND_NACK: + return False + time.sleep_ms(1) + return False + + def sendCommand(self, cmd, arg="", max_wait_ms=3000): + """Send an arbitrary command string to the scanner. Returns True on ACK.""" + return self._sendCommand(cmd, arg, max_wait_ms) + + def readBarcode(self): + """ + Read a barcode from the scanner. Call repeatedly in a loop. + Returns the barcode string when a complete CR-terminated scan arrives, + or None if no complete scan is available yet. + """ + if not self._uart.any() and not self._rx_buf: + return None + while self._uart.any(): + b = self._uart.read(1) + if not b: + break + byte = b[0] + if byte == 0x0D: # carriage return marks end of scan + result = "".join(chr(b) for b in self._rx_buf) + self._rx_buf = bytearray() + return result + if len(self._rx_buf) < _MAX_BARCODE_LEN: + self._rx_buf.append(byte) + return None + + def changeBaudRate(self, baud): + """Change module baud rate. Valid values: 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200.""" + arg = _BAUD_MAP.get(baud) + return self._sendCommand(PROPERTY_BAUD_RATE, arg) if arg else False + + def changeBuzzerTone(self, tone): + """Set buzzer frequency: 1=low, 2=medium, 3=high.""" + if 1 <= tone <= 3: + return self._sendCommand(PROPERTY_BUZZER_FREQ, str(tone)) + return False + + def enableDecodeBeep(self): + return self._sendCommand(PROPERTY_DECODE_BEEP, "1") + + def disableDecodeBeep(self): + return self._sendCommand(PROPERTY_DECODE_BEEP, "0") + + def enableBootBeep(self): + return self._sendCommand(PROPERTY_BOOT_BEEP, "1") + + def disableBootBeep(self): + return self._sendCommand(PROPERTY_BOOT_BEEP, "0") + + def lightOn(self): + """Enable the white illumination LED.""" + return self._sendCommand(PROPERTY_FLASH_LIGHT, "1") + + def lightOff(self): + return self._sendCommand(PROPERTY_FLASH_LIGHT, "0") + + def reticleOn(self): + """Enable the red aim/reticle laser.""" + return self._sendCommand(PROPERTY_AIM_LIGHT, "1") + + def reticleOff(self): + return self._sendCommand(PROPERTY_AIM_LIGHT, "0") + + def changeReadingArea(self, percent): + """Set scanning area as a percentage of the frame: 100, 80, 60, 40, or 20.""" + arg = _AREA_MAP.get(percent) + return ( + self._sendCommand(PROPERTY_READING_AREA, arg) if arg is not None else False + ) + + def enableImageFlipping(self): + return self._sendCommand(PROPERTY_MIRROR_FLIP, "1") + + def disableImageFlipping(self): + return self._sendCommand(PROPERTY_MIRROR_FLIP, "0") + + def enableContinuousRead(self, repeat_interval=2): + """ + Enable continuous read mode. + repeat_interval: 0=output once, 1=no delay, 2=0.5 s delay, 3=1 s delay. + """ + if 0 <= repeat_interval <= 3: + self._sendCommand(PROPERTY_READING_MODE, "CNT") + return self._sendCommand( + PROPERTY_CONTINUOUS_MODE_INTERVAL, str(repeat_interval) + ) + return False + + def disableContinuousRead(self): + return self._sendCommand(PROPERTY_READING_MODE, "MAN") + + def enableMotionSense(self, sensitivity=50): + """ + Enable motion-triggered scanning. + sensitivity: 15=highest, 20=high (default), 30, 50, 100=lowest. + """ + if sensitivity in _MOTION_VALID: + self._sendCommand(PROPERTY_READING_MODE, "MDH") + return self._sendCommand(PROPERTY_MOTION_SENSITIVITY, str(sensitivity)) + return False + + def disableMotionSense(self): + return self._sendCommand(PROPERTY_READING_MODE, "MAN") + + def enableAll1D(self): + return self._sendCommand(PROPERTY_ENABLE_ALL_1D) + + def disableAll1D(self): + return self._sendCommand(PROPERTY_DISABLE_ALL_1D) + + def enableAll2D(self): + return self._sendCommand(PROPERTY_ENABLE_ALL_2D) + + def disableAll2D(self): + return self._sendCommand(PROPERTY_DISABLE_ALL_2D) + + def startScan(self): + """Trigger a single scan in manual (trigger) mode.""" + return self._sendCommand(COMMAND_START_SCAN) + + def stopScan(self): + """Stop an in-progress scan.""" + return self._sendCommand(COMMAND_STOP_SCAN) diff --git a/Sensors/DE2120/package.json b/Sensors/DE2120/package.json new file mode 100644 index 0000000..07b5832 --- /dev/null +++ b/Sensors/DE2120/package.json @@ -0,0 +1,26 @@ +{ + "urls": [ + [ + "de2120.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/DE2120/DE2120/de2120.py" + ], + [ + "Examples/de2120-continuousRead.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/DE2120/DE2120/Examples/de2120-continuousRead.py" + ], + [ + "Examples/de2120-readBarcode.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/DE2120/DE2120/Examples/de2120-readBarcode.py" + ], + [ + "Examples/de2120-sendCommand.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/DE2120/DE2120/Examples/de2120-sendCommand.py" + ], + [ + "Examples/de2120-serialSettings.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/DE2120/DE2120/Examples/de2120-serialSettings.py" + ] + ], + "deps": [], + "version": "1.0" +} \ No newline at end of file diff --git a/Sensors/IIS2DULPX/IIS2DULPX/Examples/iis2dulpx-interruptWakeup.py b/Sensors/IIS2DULPX/IIS2DULPX/Examples/iis2dulpx-interruptWakeup.py new file mode 100644 index 0000000..2cf9d5d --- /dev/null +++ b/Sensors/IIS2DULPX/IIS2DULPX/Examples/iis2dulpx-interruptWakeup.py @@ -0,0 +1,55 @@ +# FILE: iis2dulpx-interruptWakeup.py +# AUTHOR: Josip Simun Kuci @ Soldered +# BRIEF: Use IIS2DULPX wake-up interrupt to detect movement +# WORKS WITH: IIS2DULPX Accelerometer breakout: www.solde.red/333363 +# LAST UPDATED: 2026-04-24 + +from machine import Pin, I2C +from iis2dulpx import IIS2DULPX, IIS2DULPX_OK, IIS2DULPX_INT1_PIN +import time + +# Hardware setup: +# - Connect breakout INT1 pin to INT1_PIN below +# - Optional: connect a buzzer or LED to ALERT_PIN +INT1_PIN = 5 +ALERT_PIN = 23 + +mems_event = False + + +def int1_callback(pin): + global mems_event + mems_event = True + + +# If you are not using the Qwiic connector, manually enter your I2C pins +# i2c = I2C(0, scl=Pin(22), sda=Pin(21)) +# sensor = IIS2DULPX(i2c) + +sensor = IIS2DULPX() +alert = Pin(ALERT_PIN, Pin.OUT) +int1 = Pin(INT1_PIN, Pin.IN) +int1.irq(trigger=Pin.IRQ_RISING, handler=int1_callback) + +if sensor.begin() != IIS2DULPX_OK: + raise Exception("Failed to initialize IIS2DULPX sensor") + +if sensor.Enable_X() != IIS2DULPX_OK: + raise Exception("Failed to enable accelerometer") + +if sensor.Enable_Wake_Up_Detection(IIS2DULPX_INT1_PIN) != IIS2DULPX_OK: + raise Exception("Failed to enable wake-up detection") + +print("Wake-up detection enabled") + +while True: + if mems_event: + mems_event = False + status = sensor.Get_X_Event_Status() + if status["WakeUpStatus"]: + print("Wake up Detected!") + alert.on() + time.sleep_ms(200) + alert.off() + + time.sleep_ms(10) diff --git a/Sensors/IIS2DULPX/IIS2DULPX/Examples/iis2dulpx-serialPlotter.py b/Sensors/IIS2DULPX/IIS2DULPX/Examples/iis2dulpx-serialPlotter.py new file mode 100644 index 0000000..817dcd8 --- /dev/null +++ b/Sensors/IIS2DULPX/IIS2DULPX/Examples/iis2dulpx-serialPlotter.py @@ -0,0 +1,28 @@ +# FILE: iis2dulpx-serialPlotter.py +# AUTHOR: Josip Simun Kuci @ Soldered +# BRIEF: Poll IIS2DULPX acceleration values for serial plotting +# WORKS WITH: IIS2DULPX Accelerometer breakout: www.solde.red/333363 +# LAST UPDATED: 2026-04-24 + +from machine import I2C, Pin +from iis2dulpx import IIS2DULPX, IIS2DULPX_OK +import time + +# If you are not using the Qwiic connector, manually enter your I2C pins +# i2c = I2C(0, scl=Pin(22), sda=Pin(21)) +# sensor = IIS2DULPX(i2c) + +sensor = IIS2DULPX() + +if sensor.begin() != IIS2DULPX_OK: + raise Exception("Failed to initialize IIS2DULPX sensor") + +if sensor.Enable_X() != IIS2DULPX_OK: + raise Exception("Failed to enable accelerometer") + +print("IIS2DULPX sensor initialized and accelerometer enabled.") + +while True: + x, y, z = sensor.Get_X_Axes() + print("Accel-X [mg]:{},Accel-Y[mg]:{},Accel-Z[mg]:{}".format(x, y, z)) + time.sleep_ms(50) diff --git a/Sensors/IIS2DULPX/IIS2DULPX/iis2dulpx.py b/Sensors/IIS2DULPX/IIS2DULPX/iis2dulpx.py new file mode 100644 index 0000000..e24adda --- /dev/null +++ b/Sensors/IIS2DULPX/IIS2DULPX/iis2dulpx.py @@ -0,0 +1,403 @@ +# FILE: iis2dulpx.py +# AUTHOR: Josip Simun Kuci @ Soldered +# BRIEF: MicroPython library for the IIS2DULPX 3-axis accelerometer +# LAST UPDATED: 2026-04-24 + +import time +from machine import I2C, Pin +from os import uname + +IIS2DULPX_OK = 0 +IIS2DULPX_ERROR = -1 + +IIS2DULPX_INT1_PIN = 0 +IIS2DULPX_INT2_PIN = 1 + +IIS2DULPX_ULTRA_LOW_POWER = 0 +IIS2DULPX_LOW_POWER = 1 +IIS2DULPX_HIGH_PERFORMANCE = 2 + +_IIS2DULPX_I2C_ADDR_L = 0x18 +_IIS2DULPX_I2C_ADDR_H = 0x19 +_IIS2DULPX_ID = 0x47 + +_REG_WAKE_UP_DUR_EXT = 0x0E +_REG_WHO_AM_I = 0x0F +_REG_CTRL1 = 0x10 +_REG_CTRL3 = 0x12 +_REG_CTRL4 = 0x13 +_REG_CTRL5 = 0x14 +_REG_I3C_IF_CTRL = 0x33 +_REG_FUNC_CFG_ACCESS = 0x3F +_REG_INTERRUPT_CFG = 0x17 +_REG_WAKE_UP_THS = 0x1C +_REG_WAKE_UP_DUR = 0x1D +_REG_MD1_CFG = 0x1F +_REG_MD2_CFG = 0x20 +_REG_ALL_INT_SRC = 0x24 +_REG_STATUS = 0x25 +_REG_OUT_X_L = 0x28 + +_ACC_SENS_MG_LSB = { + 2: 0.061, + 4: 0.122, + 8: 0.244, + 16: 0.488, +} + +_FS_TO_BITS = { + 2: 0b00, + 4: 0b01, + 8: 0b10, + 16: 0b11, +} + +_BITS_TO_FS = { + 0b00: 2, + 0b01: 4, + 0b10: 8, + 0b11: 16, +} + +# CTRL5 ODR field (matches iis2dulpx_mode_get in ST driver), not sequential 0..N +_BITS_TO_ODR = { + 0x0: 0.0, + 0x1: 1.6, + 0x2: 3.0, + 0x3: 25.0, + 0x4: 6.0, + 0x5: 12.5, + 0x6: 25.0, + 0x7: 50.0, + 0x8: 100.0, + 0x9: 200.0, + 0xA: 400.0, + 0xB: 800.0, +} + + +class IIS2DULPX: + def __init__(self, i2c=None, address=_IIS2DULPX_I2C_ADDR_H): + if i2c is not None: + self.i2c = i2c + else: + if uname().sysname in ("esp32", "esp8266", "Soldered Dasduino CONNECTPLUS"): + self.i2c = I2C(0, scl=Pin(22), sda=Pin(21)) + else: + raise Exception("Board not recognized, enter I2C pins manually") + + self.address = address + self._acc_is_enabled = False + self._is_initialized = False + self._acc_odr = 100.0 + self._power_mode = IIS2DULPX_HIGH_PERFORMANCE + + def begin(self): + if self.ReadID() != _IIS2DULPX_ID: + return IIS2DULPX_ERROR + + # Boot / interface wake-up (matches STM IIS2DULPXSensor::begin()) + time.sleep_ms(25) + + # Prefer I2C behaviour on the serial bus (ASF / I3C ctrl — same as Arduino driver) + i3c = self._read_reg(_REG_I3C_IF_CTRL) + self._write_reg(_REG_I3C_IF_CTRL, i3c | (1 << 5)) + + # Accelerometer data lives in the main register bank; embedded bank reads as zeros here + self._write_reg( + _REG_FUNC_CFG_ACCESS, self._read_reg(_REG_FUNC_CFG_ACCESS) & 0x7F + ) + + # BDU + no embedded funcs + address auto-increment (iis2dulpx_init_set SENSOR_ONLY_ON) + ctrl4 = self._read_reg(_REG_CTRL4) + ctrl4 &= ~(1 << 4) + ctrl4 |= 1 << 5 + self._write_reg(_REG_CTRL4, ctrl4) + + ctrl1 = self._read_reg(_REG_CTRL1) + self._write_reg(_REG_CTRL1, ctrl1 | (1 << 4)) + + # Default: powered down, FS = 2g + self._set_ctrl5(fs=2, odr=0.0) + self._is_initialized = True + self._acc_is_enabled = False + return IIS2DULPX_OK + + def end(self): + self.Disable_X() + self._is_initialized = False + self._acc_odr = 0.0 + self._power_mode = IIS2DULPX_LOW_POWER + return IIS2DULPX_OK + + def ReadID(self): + return self._read_reg(_REG_WHO_AM_I) + + def Enable_X(self): + if self._acc_is_enabled: + return IIS2DULPX_OK + status = self.Set_X_OutputDataRate_With_Mode(self._acc_odr, self._power_mode) + if status != IIS2DULPX_OK: + return status + # Must apply ODR here: Set_X_OutputDataRate_With_Mode skips HW while disabled + self._set_ctrl5(odr=self._acc_odr) + self._acc_is_enabled = True + return IIS2DULPX_OK + + def Disable_X(self): + self._set_ctrl5(odr=0.0) + self._acc_is_enabled = False + return IIS2DULPX_OK + + def Get_X_Sensitivity(self): + full_scale = self.Get_X_FullScale() + return _ACC_SENS_MG_LSB.get(full_scale, -1.0) + + def Get_X_OutputDataRate(self): + ctrl5 = self._read_reg(_REG_CTRL5) + odr_bits = (ctrl5 >> 4) & 0x0F + return _BITS_TO_ODR.get(odr_bits, -1.0) + + def Set_X_OutputDataRate(self, odr): + return self.Set_X_OutputDataRate_With_Mode(odr, IIS2DULPX_HIGH_PERFORMANCE) + + def Set_X_OutputDataRate_With_Mode(self, odr, power_mode): + selected_odr = self._select_odr(odr, power_mode) + if selected_odr is None: + return IIS2DULPX_ERROR + + self._power_mode = power_mode + self._acc_odr = selected_odr + if self._acc_is_enabled: + self._set_ctrl5(odr=selected_odr) + return IIS2DULPX_OK + + def Get_X_FullScale(self): + ctrl5 = self._read_reg(_REG_CTRL5) + fs_bits = ctrl5 & 0x03 + return _BITS_TO_FS.get(fs_bits, -1) + + def Set_X_FullScale(self, full_scale): + if full_scale <= 2: + fs = 2 + elif full_scale <= 4: + fs = 4 + elif full_scale <= 8: + fs = 8 + else: + fs = 16 + self._set_ctrl5(fs=fs) + return IIS2DULPX_OK + + def Get_X_AxesRaw(self): + data = self.i2c.readfrom_mem(self.address, _REG_OUT_X_L, 6) + x = self._to_int16(data[0], data[1]) + y = self._to_int16(data[2], data[3]) + z = self._to_int16(data[4], data[5]) + return x, y, z + + def Get_X_Axes(self): + x_raw, y_raw, z_raw = self.Get_X_AxesRaw() + sens = self.Get_X_Sensitivity() + if sens <= 0: + return 0, 0, 0 + x = int(x_raw * sens) + y = int(y_raw * sens) + z = int(z_raw * sens) + return x, y, z + + def Read_Reg(self, reg): + return self._read_reg(reg) + + def Write_Reg(self, reg, data): + self._write_reg(reg, data) + return IIS2DULPX_OK + + def Get_X_DRDY_Status(self): + status = self._read_reg(_REG_STATUS) + return 1 if (status & 0x01) else 0 + + def Get_X_Init_Status(self): + return 1 if self._is_initialized else 0 + + def Get_X_Event_Status(self): + all_int_src = self._read_reg(_REG_ALL_INT_SRC) + md1_cfg = self._read_reg(_REG_MD1_CFG) + md2_cfg = self._read_reg(_REG_MD2_CFG) + + wakeup_enabled = ((md1_cfg >> 5) & 0x01) or ((md2_cfg >> 5) & 0x01) + sixd_enabled = ((md1_cfg >> 2) & 0x01) or ((md2_cfg >> 2) & 0x01) + + return { + "FreeFallStatus": 0, + "TapStatus": 0, + "DoubleTapStatus": 0, + "WakeUpStatus": 1 + if (wakeup_enabled and ((all_int_src >> 1) & 0x01)) + else 0, + "StepStatus": 0, + "TiltStatus": 0, + "D6DOrientationStatus": 1 + if (sixd_enabled and ((all_int_src >> 5) & 0x01)) + else 0, + "SleepStatus": 0, + } + + def Enable_Wake_Up_Detection(self, int_pin=IIS2DULPX_INT1_PIN): + if self.Set_X_OutputDataRate(200.0) != IIS2DULPX_OK: + return IIS2DULPX_ERROR + if self.Set_X_FullScale(2) != IIS2DULPX_OK: + return IIS2DULPX_ERROR + if self.Set_Wake_Up_Threshold(63) != IIS2DULPX_OK: + return IIS2DULPX_ERROR + if self.Set_Wake_Up_Duration(0) != IIS2DULPX_OK: + return IIS2DULPX_ERROR + + ctrl1 = self._read_reg(_REG_CTRL1) + ctrl1 |= 0x07 # Enable wake-up on X, Y, Z + self._write_reg(_REG_CTRL1, ctrl1) + + if int_pin == IIS2DULPX_INT1_PIN: + md1 = self._read_reg(_REG_MD1_CFG) + self._write_reg(_REG_MD1_CFG, md1 | (1 << 5)) + elif int_pin == IIS2DULPX_INT2_PIN: + md2 = self._read_reg(_REG_MD2_CFG) + self._write_reg(_REG_MD2_CFG, md2 | (1 << 5)) + else: + return IIS2DULPX_ERROR + + interrupt_cfg = self._read_reg(_REG_INTERRUPT_CFG) + self._write_reg(_REG_INTERRUPT_CFG, interrupt_cfg | 0x01) + return IIS2DULPX_OK + + def Disable_Wake_Up_Detection(self): + md1 = self._read_reg(_REG_MD1_CFG) + md2 = self._read_reg(_REG_MD2_CFG) + ctrl1 = self._read_reg(_REG_CTRL1) + + self._write_reg(_REG_MD1_CFG, md1 & ~(1 << 5)) + self._write_reg(_REG_MD2_CFG, md2 & ~(1 << 5)) + self._write_reg(_REG_CTRL1, ctrl1 & ~0x07) + self.Set_Wake_Up_Threshold(0) + self.Set_Wake_Up_Duration(0) + return IIS2DULPX_OK + + def Set_Wake_Up_Threshold(self, threshold_mg): + fs = self.Get_X_FullScale() + if fs not in (2, 4, 8, 16): + return IIS2DULPX_ERROR + + if fs == 2: + small_step, large_step = 7.8125, 31.25 + elif fs == 4: + small_step, large_step = 15.625, 62.5 + elif fs == 8: + small_step, large_step = 31.25, 125.0 + else: + small_step, large_step = 62.5, 250.0 + + interrupt_cfg = self._read_reg(_REG_INTERRUPT_CFG) + if threshold_mg < small_step * 63.0: + interrupt_cfg |= 1 << 5 + wk_ths = int(threshold_mg / small_step) + elif threshold_mg < large_step * 63.0: + interrupt_cfg &= ~(1 << 5) + wk_ths = int(threshold_mg / large_step) + else: + interrupt_cfg &= ~(1 << 5) + wk_ths = 63 + + self._write_reg(_REG_INTERRUPT_CFG, interrupt_cfg) + + wake_up_ths = self._read_reg(_REG_WAKE_UP_THS) + wake_up_ths = (wake_up_ths & 0xC0) | (wk_ths & 0x3F) + self._write_reg(_REG_WAKE_UP_THS, wake_up_ths) + return IIS2DULPX_OK + + def Set_Wake_Up_Duration(self, duration): + if duration not in (0, 1, 2, 3, 7, 11, 15): + return IIS2DULPX_ERROR + + wake_up_dur = self._read_reg(_REG_WAKE_UP_DUR) + dur_ext = self._read_reg(_REG_WAKE_UP_DUR_EXT) + + if duration in (0, 1, 2): + dur_ext &= ~(1 << 4) + wake_dur_bits = duration + else: + dur_ext |= 1 << 4 + wake_dur_bits = {3: 0, 7: 1, 11: 2, 15: 3}[duration] + + wake_up_dur = (wake_up_dur & ~(0b11 << 5)) | ((wake_dur_bits & 0b11) << 5) + self._write_reg(_REG_WAKE_UP_DUR, wake_up_dur) + self._write_reg(_REG_WAKE_UP_DUR_EXT, dur_ext) + return IIS2DULPX_OK + + def _select_odr(self, odr, power_mode): + if power_mode == IIS2DULPX_ULTRA_LOW_POWER: + if odr <= 1.6: + return 1.6 + if odr <= 3.0: + return 3.0 + return 25.0 + if power_mode in (IIS2DULPX_LOW_POWER, IIS2DULPX_HIGH_PERFORMANCE): + for odr_val in (6.0, 12.5, 25.0, 50.0, 100.0, 200.0, 400.0, 800.0): + if odr <= odr_val: + return odr_val + return 800.0 + return None + + def _odr_float_to_nibble(self, rate, power_mode): + if power_mode == IIS2DULPX_ULTRA_LOW_POWER: + if rate <= 1.6: + return 0x1 + if rate <= 3.0: + return 0x2 + return 0x3 + if rate <= 6.0: + return 0x4 + if rate <= 12.5: + return 0x5 + if rate <= 25.0: + return 0x6 + if rate <= 50.0: + return 0x7 + if rate <= 100.0: + return 0x8 + if rate <= 200.0: + return 0x9 + if rate <= 400.0: + return 0xA + return 0xB + + def _set_ctrl5(self, fs=None, odr=None): + ctrl5 = self._read_reg(_REG_CTRL5) + ctrl3 = self._read_reg(_REG_CTRL3) + if fs is not None: + ctrl5 = (ctrl5 & 0xFC) | _FS_TO_BITS[fs] + if odr is not None: + if odr == 0.0: + ctrl5 = ctrl5 & 0x0F + ctrl3 &= ~(1 << 2) + else: + nib = self._odr_float_to_nibble(odr, self._power_mode) + ctrl5 = (ctrl5 & 0x0F) | ((nib & 0x0F) << 4) + if self._power_mode == IIS2DULPX_HIGH_PERFORMANCE: + ctrl3 |= 1 << 2 + else: + ctrl3 &= ~(1 << 2) + self._write_reg(_REG_CTRL3, ctrl3) + self._write_reg(_REG_CTRL5, ctrl5) + + def _read_reg(self, reg): + return self.i2c.readfrom_mem(self.address, reg, 1)[0] + + def _write_reg(self, reg, value): + self.i2c.writeto_mem(self.address, reg, bytes([value & 0xFF])) + + @staticmethod + def _to_int16(lsb, msb): + value = (msb << 8) | lsb + if value & 0x8000: + value -= 0x10000 + return value diff --git a/Sensors/IIS2DULPX/package.json b/Sensors/IIS2DULPX/package.json new file mode 100644 index 0000000..f7b0b3b --- /dev/null +++ b/Sensors/IIS2DULPX/package.json @@ -0,0 +1,18 @@ +{ + "urls": [ + [ + "iis2dulpx.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Sensors/IIS2DULPX/IIS2DULPX/iis2dulpx.py" + ], + [ + "Examples/iis2dulpx-serialPlotter.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Sensors/IIS2DULPX/IIS2DULPX/Examples/iis2dulpx-serialPlotter.py" + ], + [ + "Examples/iis2dulpx-interruptWakeup.py", + "github:SolderedElectronics/Soldered-MicroPython-Modules/Sensors/IIS2DULPX/IIS2DULPX/Examples/iis2dulpx-interruptWakeup.py" + ] + ], + "deps": [], + "version": "1.0" +}