From 1b4d60e19ae8278dc4fb8c89bec71afe4bc844ff Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 27 May 2026 12:39:50 +0800 Subject: [PATCH] first --- motors.py | 29 ++++ receiver.py | 406 +++++++++++++++++++++++++++++++++++++++++++++++++ servo.py | 74 +++++++++ transmitter.py | 323 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 832 insertions(+) create mode 100644 motors.py create mode 100644 receiver.py create mode 100644 servo.py create mode 100644 transmitter.py diff --git a/motors.py b/motors.py new file mode 100644 index 0000000..49f89dc --- /dev/null +++ b/motors.py @@ -0,0 +1,29 @@ +# motors.py +from machine import Pin, PWM + +class Motor: + def __init__(self, pin1=13, pin2=14, freq=20000): + # Two PWM objects, one per pin + self._pwm1 = PWM(Pin(pin1), freq=freq, duty=0) + self._pwm2 = PWM(Pin(pin2), freq=freq, duty=0) + + def move(self, value): + # Clamp input + if value > 1023: + value = 1023 + elif value < -1023: + value = -1023 + + if value == 0: + # Stop: both low + self._pwm1.duty(0) + self._pwm2.duty(0) + elif value > 0: + # Forward: pin1 PWM, pin2 low + self._pwm1.duty(value) + self._pwm2.duty(0) + else: + # Backward: pin2 PWM, pin1 low + self._pwm1.duty(0) + self._pwm2.duty(-value) + diff --git a/receiver.py b/receiver.py new file mode 100644 index 0000000..40a1667 --- /dev/null +++ b/receiver.py @@ -0,0 +1,406 @@ +from machine import Pin, PWM +import time, network, espnow +import motors + +BUZZ_PIN = 39 +PAIR_BEEP_INTERVAL_MS = 1400 + +# ---------------- ESP-NOW helpers (match transmitter.py) ---------------- +_espnow_sta = None +_espnow = None +_espnow_peers = [] +_espnow_broadcast = b'\xff\xff\xff\xff\xff\xff' + + +def _espnow_parse_mac(mac): + if isinstance(mac, bytes): + return mac + if isinstance(mac, bytearray): + return bytes(mac) + if isinstance(mac, str): + mac = mac.replace(':', '').replace('-', '').replace(' ', '') + if len(mac) != 12: + raise ValueError('MAC must be 12 hex chars') + return bytes(int(mac[i : i + 2], 16) for i in range(0, 12, 2)) + raise ValueError('MAC must be bytes or string') + + +def _espnow_init(channel=6, rate='RATE_LORA_250K'): + global _espnow_sta, _espnow + _espnow_sta = network.WLAN(network.STA_IF) + _espnow_sta.active(True) + _espnow_sta.disconnect() + _espnow_sta.config(channel=int(channel), protocol=network.WLAN.PROTOCOL_LR) + _espnow = espnow.ESPNow() + _espnow.active(True) + rate_val = getattr(espnow, str(rate), espnow.RATE_LORA_250K) + _espnow.config(rate=rate_val) + return _espnow + + +def _espnow_my_mac(): + if _espnow_sta is None: + _espnow_init() + return _espnow_sta.config('mac') + + +def _espnow_add_peer(mac): + global _espnow_peers + if _espnow is None: + _espnow_init() + peer = _espnow_parse_mac(mac) + if peer not in _espnow_peers: + _espnow.add_peer(peer) + _espnow_peers.append(peer) + return peer + + +def _espnow_pack(data): + if isinstance(data, (list, tuple)): + return ','.join(str(v) for v in data) + return str(data) + + +def _espnow_send_peer(mac, data): + if _espnow is None: + _espnow_init() + peer = _espnow_add_peer(mac) + payload = _espnow_pack(data) + _espnow.send(peer, payload) + + +def _espnow_try_num(s): + try: + return int(s) + except: + pass + try: + return float(s) + except: + return s + + +def _espnow_unpack(msg): + if msg is None: + return [] + if isinstance(msg, bytes): + msg = msg.decode('utf-8', 'ignore') + parts = str(msg).split(',') + out = [] + for p in parts: + p = p.strip() + if p == '': + continue + out.append(_espnow_try_num(p)) + return out + + +def _espnow_recv(timeout_ms=10): + if _espnow is None: + _espnow_init() + host, msg = _espnow.recv(timeout_ms) + return host, _espnow_unpack(msg) + + +def _mac_eq(a, b): + if a is None or b is None: + return False + return bytes(a) == bytes(b) + + +# ---------------- Pairing (receiver side) ---------------- +PAIR_PREFIX = 'PAIR_REQ:' +PAIR_ACK_PREFIX = 'PAIR_ACK:' +PEER_FILE = 'peer_mac.txt' +peer_mac = None + + +def load_peer(): + try: + with open(PEER_FILE, 'rb') as f: + mac = f.read() + if mac and len(mac) == 6: + return mac + except: + pass + return None + + +def save_peer(mac): + with open(PEER_FILE, 'wb') as f: + f.write(mac) + + +def buzz_pairing_chirp(): + """Very short tone while waiting to pair (passive piezo: PWM; active: GPIO pulse).""" + try: + pwm = PWM(Pin(BUZZ_PIN, Pin.OUT), freq=3800, duty_u16=24576) + time.sleep_ms(14) + pwm.deinit() + except: + b = Pin(BUZZ_PIN, Pin.OUT) + b.on() + time.sleep_ms(12) + b.off() + + +def try_pair_from_message(host, msg, my_hex): + """If msg is a PAIR_REQ from a transmitter, ACK and return True when paired.""" + global peer_mac + if not (msg and isinstance(msg, list) and len(msg) >= 2 and msg[0] == PAIR_PREFIX): + return False + tx_hex = str(msg[1]).strip().lower() + tx_mac = None + if len(tx_hex) == 12: + try: + tx_mac = _espnow_parse_mac(tx_hex) + except: + tx_mac = None + if tx_mac is None and host: + tx_mac = bytes(host) if not isinstance(host, bytes) else host + if not tx_mac: + return False + _espnow_add_peer(tx_mac) + _espnow_send_peer(tx_mac, [PAIR_ACK_PREFIX, my_hex]) + save_peer(tx_mac) + peer_mac = tx_mac + print('Paired with transmitter', tx_hex if len(tx_hex) == 12 else tx_mac.hex()) + return True + + +# Same channel and rate as transmitter.py +_espnow_init(1, 'RATE_LORA_500K') +peer_mac = load_peer() +if peer_mac: + _espnow_add_peer(peer_mac) + +my_mac_hex = _espnow_my_mac().hex() +last_pair_beep = time.ticks_ms() +print('Receiver ready. My MAC:', my_mac_hex, 'Peer:', peer_mac.hex() if peer_mac else '(searching)') + +leftMotor = motors.Motor(13, 14) +rightMotor = motors.Motor(15, 16) + +# ---------------- Main loop ---------------- +while True: + if not peer_mac: + host, msg = _espnow_recv(timeout_ms=120) + if try_pair_from_message(host, msg, my_mac_hex): + pass + else: + if time.ticks_diff(time.ticks_ms(), last_pair_beep) >= PAIR_BEEP_INTERVAL_MS: + buzz_pairing_chirp() + last_pair_beep = time.ticks_ms() + time.sleep_ms(10) + continue + + host, data = _espnow_recv(timeout_ms=50) + if host and data and _mac_eq(host, peer_mac): + # Transmitter payload: 4 axes, adc_1, adc_10, btn2,5,6,7,11,12 + print(data) + + time.sleep_ms(5) + +from machine import Pin, PWM +import time, network, espnow +import motors + +BUZZ_PIN = 39 +PAIR_BEEP_INTERVAL_MS = 1400 + +# ---------------- ESP-NOW helpers (match transmitter.py) ---------------- +_espnow_sta = None +_espnow = None +_espnow_peers = [] +_espnow_broadcast = b'\xff\xff\xff\xff\xff\xff' + + +def _espnow_parse_mac(mac): + if isinstance(mac, bytes): + return mac + if isinstance(mac, bytearray): + return bytes(mac) + if isinstance(mac, str): + mac = mac.replace(':', '').replace('-', '').replace(' ', '') + if len(mac) != 12: + raise ValueError('MAC must be 12 hex chars') + return bytes(int(mac[i : i + 2], 16) for i in range(0, 12, 2)) + raise ValueError('MAC must be bytes or string') + + +def _espnow_init(channel=6, rate='RATE_LORA_250K'): + global _espnow_sta, _espnow + _espnow_sta = network.WLAN(network.STA_IF) + _espnow_sta.active(True) + _espnow_sta.disconnect() + _espnow_sta.config(channel=int(channel), protocol=network.WLAN.PROTOCOL_LR) + _espnow = espnow.ESPNow() + _espnow.active(True) + rate_val = getattr(espnow, str(rate), espnow.RATE_LORA_250K) + _espnow.config(rate=rate_val) + return _espnow + + +def _espnow_my_mac(): + if _espnow_sta is None: + _espnow_init() + return _espnow_sta.config('mac') + + +def _espnow_add_peer(mac): + global _espnow_peers + if _espnow is None: + _espnow_init() + peer = _espnow_parse_mac(mac) + if peer not in _espnow_peers: + _espnow.add_peer(peer) + _espnow_peers.append(peer) + return peer + + +def _espnow_pack(data): + if isinstance(data, (list, tuple)): + return ','.join(str(v) for v in data) + return str(data) + + +def _espnow_send_peer(mac, data): + if _espnow is None: + _espnow_init() + peer = _espnow_add_peer(mac) + payload = _espnow_pack(data) + _espnow.send(peer, payload) + + +def _espnow_try_num(s): + try: + return int(s) + except: + pass + try: + return float(s) + except: + return s + + +def _espnow_unpack(msg): + if msg is None: + return [] + if isinstance(msg, bytes): + msg = msg.decode('utf-8', 'ignore') + parts = str(msg).split(',') + out = [] + for p in parts: + p = p.strip() + if p == '': + continue + out.append(_espnow_try_num(p)) + return out + + +def _espnow_recv(timeout_ms=10): + if _espnow is None: + _espnow_init() + host, msg = _espnow.recv(timeout_ms) + return host, _espnow_unpack(msg) + + +def _mac_eq(a, b): + if a is None or b is None: + return False + return bytes(a) == bytes(b) + + +# ---------------- Pairing (receiver side) ---------------- +PAIR_PREFIX = 'PAIR_REQ:' +PAIR_ACK_PREFIX = 'PAIR_ACK:' +PEER_FILE = 'peer_mac.txt' +peer_mac = None + + +def load_peer(): + try: + with open(PEER_FILE, 'rb') as f: + mac = f.read() + if mac and len(mac) == 6: + return mac + except: + pass + return None + + +def save_peer(mac): + with open(PEER_FILE, 'wb') as f: + f.write(mac) + + +def buzz_pairing_chirp(): + """Very short tone while waiting to pair (passive piezo: PWM; active: GPIO pulse).""" + try: + pwm = PWM(Pin(BUZZ_PIN, Pin.OUT), freq=3800, duty_u16=24576) + time.sleep_ms(14) + pwm.deinit() + except: + b = Pin(BUZZ_PIN, Pin.OUT) + b.on() + time.sleep_ms(12) + b.off() + + +def try_pair_from_message(host, msg, my_hex): + """If msg is a PAIR_REQ from a transmitter, ACK and return True when paired.""" + global peer_mac + if not (msg and isinstance(msg, list) and len(msg) >= 2 and msg[0] == PAIR_PREFIX): + return False + tx_hex = str(msg[1]).strip().lower() + tx_mac = None + if len(tx_hex) == 12: + try: + tx_mac = _espnow_parse_mac(tx_hex) + except: + tx_mac = None + if tx_mac is None and host: + tx_mac = bytes(host) if not isinstance(host, bytes) else host + if not tx_mac: + return False + _espnow_add_peer(tx_mac) + _espnow_send_peer(tx_mac, [PAIR_ACK_PREFIX, my_hex]) + save_peer(tx_mac) + peer_mac = tx_mac + print('Paired with transmitter', tx_hex if len(tx_hex) == 12 else tx_mac.hex()) + return True + + +# Same channel and rate as transmitter.py +_espnow_init(1, 'RATE_LORA_500K') +peer_mac = load_peer() +if peer_mac: + _espnow_add_peer(peer_mac) + +my_mac_hex = _espnow_my_mac().hex() +last_pair_beep = time.ticks_ms() +print('Receiver ready. My MAC:', my_mac_hex, 'Peer:', peer_mac.hex() if peer_mac else '(searching)') + +leftMotor = motors.Motor(13, 14) +rightMotor = motors.Motor(15, 16) + +# ---------------- Main loop ---------------- +while True: + if not peer_mac: + host, msg = _espnow_recv(timeout_ms=120) + if try_pair_from_message(host, msg, my_mac_hex): + pass + else: + if time.ticks_diff(time.ticks_ms(), last_pair_beep) >= PAIR_BEEP_INTERVAL_MS: + buzz_pairing_chirp() + last_pair_beep = time.ticks_ms() + time.sleep_ms(10) + continue + + host, data = _espnow_recv(timeout_ms=50) + if host and data and _mac_eq(host, peer_mac): + # Transmitter payload: 4 axes, adc_1, adc_10, btn2,5,6,7,11,12 + print(data) + + time.sleep_ms(5) + diff --git a/servo.py b/servo.py new file mode 100644 index 0000000..b96a5fb --- /dev/null +++ b/servo.py @@ -0,0 +1,74 @@ +# servo.py — MG995 (and other analog hobby servos), 50 Hz PWM +from machine import Pin, PWM + +# MG995: treat as standard 1.0–2.0 ms for ~0–180°; many units reach full travel closer to 0.5–2.5 ms. +_DEFAULT_MIN_US = 1000 +_DEFAULT_MAX_US = 2000 +_WIDE_MIN_US = 500 +_WIDE_MAX_US = 2500 + + +class MG995: + def __init__( + self, + pin, + freq=50, + min_us=_DEFAULT_MIN_US, + max_us=_DEFAULT_MAX_US, + angle_max=180, + wide_range=False, + ): + """ + Drive one MG995 on `pin` (GPIO number or Pin). + + `wide_range=True` uses ~500–2500 µs pulse limits (often needed for full + mechanical sweep); default 1000–2000 µs is gentler on the gearbox. + + `angle_max` is the upper limit passed to write_angle (default 180). + """ + if wide_range: + min_us, max_us = _WIDE_MIN_US, _WIDE_MAX_US + self._pin_id = pin + self._freq = int(freq) + self._min_us = int(min_us) + self._max_us = int(max_us) + self._angle_max = float(angle_max) + self._pwm = PWM(Pin(pin), freq=self._freq, duty_u16=0) + self._use_ns = hasattr(self._pwm, 'duty_ns') + self._period_us = 1_000_000 // self._freq if self._freq > 0 else 20_000 + + def write_microseconds(self, us): + """Set pulse width in microseconds (clamped to configured min/max).""" + us = max(self._min_us, min(self._max_us, int(us))) + if self._use_ns: + self._pwm.duty_ns(us * 1000) + else: + # Fraction of period → duty_u16 (ESP32-style full-scale mapping) + self._pwm.duty_u16(max(0, min(65535, int(us * 65535 / self._period_us))))) + + def write_angle(self, degrees): + """Map `degrees` (0 … angle_max) to pulse between min_us and max_us.""" + a = max(0.0, min(self._angle_max, float(degrees))) + span = self._max_us - self._min_us + us = self._min_us + int(round(span * (a / self._angle_max))) + self.write_microseconds(us) + + def center(self): + """Mid pulse (~center position for default symmetric limits).""" + self.write_microseconds((self._min_us + self._max_us) // 2) + + def off(self): + """Stop PWM output (no holding torque from the signal).""" + try: + self._pwm.duty_u16(0) + except Exception: + pass + try: + if self._use_ns: + self._pwm.duty_ns(0) + except Exception: + pass + + def deinit(self): + self.off() + self._pwm.deinit() diff --git a/transmitter.py b/transmitter.py new file mode 100644 index 0000000..71e828b --- /dev/null +++ b/transmitter.py @@ -0,0 +1,323 @@ +from machine import Pin, ADC +import time, network, espnow + +# ---------------- ESP-NOW helpers ---------------- +_espnow_sta = None +_espnow = None +_espnow_peers = [] +_espnow_broadcast = b'\xff\xff\xff\xff\xff\xff' + +led = Pin(15, Pin.OUT); +led.value(0) +btn2 = Pin(2, Pin.IN, Pin.PULL_DOWN) +btn5 = Pin(5, Pin.IN, Pin.PULL_DOWN) +btn6 = Pin(6, Pin.IN, Pin.PULL_DOWN) +btn7 = Pin(7, Pin.IN, Pin.PULL_DOWN) +btn11 = Pin(11, Pin.IN, Pin.PULL_DOWN) +btn12 = Pin(12, Pin.IN, Pin.PULL_DOWN) +time.sleep_ms(20) +PAIRMODE_ON_START = btn12.value() +CALIBRATE_ON_START = btn5.value() +print(PAIRMODE_ON_START) +adc_3 = ADC(Pin(3)) +adc_4 = ADC(Pin(4)) +adc_8 = ADC(Pin(8)) +adc_9 = ADC(Pin(9)) +adc_1 = ADC(Pin(1)) +adc_10 = ADC(Pin(10)) + +def _espnow_parse_mac(mac): + if isinstance(mac, bytes): + return mac + if isinstance(mac, bytearray): + return bytes(mac) + if isinstance(mac, str): + mac = mac.replace(':', '').replace('-', '').replace(' ', '') + if len(mac) != 12: + raise ValueError('MAC must be 12 hex chars') + return bytes(int(mac[i:i+2], 16) for i in range(0, 12, 2)) + raise ValueError('MAC must be bytes or string') + +def _espnow_init(channel=6, rate='RATE_LORA_250K'): + global _espnow_sta, _espnow + _espnow_sta = network.WLAN(network.STA_IF) + _espnow_sta.active(True) + _espnow_sta.disconnect() + _espnow_sta.config(channel=int(channel), protocol=network.WLAN.PROTOCOL_LR) + _espnow = espnow.ESPNow() + _espnow.active(True) + rate_val = getattr(espnow, str(rate), espnow.RATE_LORA_250K) + _espnow.config(rate=rate_val) + return _espnow + +def _espnow_my_mac(): + if _espnow_sta is None: + _espnow_init() + return _espnow_sta.config('mac') + +def _espnow_add_peer(mac): + global _espnow_peers + if _espnow is None: + _espnow_init() + peer = _espnow_parse_mac(mac) + if peer not in _espnow_peers: + _espnow.add_peer(peer) + _espnow_peers.append(peer) + return peer + +def _espnow_pack(data): + if isinstance(data, (list, tuple)): + return ','.join(str(v) for v in data) + return str(data) + +def _espnow_send_peer(mac, data): + if _espnow is None: + _espnow_init() + peer = _espnow_add_peer(mac) + payload = _espnow_pack(data) + _espnow.send(peer, payload) + +def _espnow_send_all(data): + if _espnow is None: + _espnow_init() + payload = _espnow_pack(data) + try: + _espnow.add_peer(_espnow_broadcast) + except: + pass + _espnow.send(_espnow_broadcast, payload) + +def _espnow_try_num(s): + try: + return int(s) + except: + pass + try: + return float(s) + except: + return s + +def _espnow_unpack(msg): + if msg is None: + return [] + if isinstance(msg, bytes): + msg = msg.decode('utf-8', 'ignore') + parts = str(msg).split(',') + out = [] + for p in parts: + p = p.strip() + if p == '': + continue + out.append(_espnow_try_num(p)) + return out + +def _espnow_recv(timeout_ms=10): + if _espnow is None: + _espnow_init() + host, msg = _espnow.recv(timeout_ms) + return host, _espnow_unpack(msg) + +# ---------------- Pairing logic ---------------- +PAIR_PREFIX = "PAIR_REQ:" +PAIR_ACK_PREFIX = "PAIR_ACK:" +PEER_FILE = "peer_mac.txt" +peer_mac = None +pairing_mode = False + +def load_peer(): + try: + with open(PEER_FILE, "rb") as f: + return f.read() + except: + return None + +def save_peer(mac): + with open(PEER_FILE, "wb") as f: + f.write(mac) + +def enter_pairing(): + global pairing_mode, peer_mac + pairing_mode = True + my_mac = _espnow_my_mac() + print("Pairing mode started, broadcasting...") + while pairing_mode: + # Broadcast as two fields + _espnow_send_all([PAIR_PREFIX, my_mac.hex()]) + print("Sent:", [PAIR_PREFIX, my_mac.hex()]) + led.value(not led.value()) + # Non-blocking receive + host, msg = _espnow_recv(timeout_ms=50) + if msg and isinstance(msg, list) and len(msg) > 1: + if msg[0] == PAIR_ACK_PREFIX: + partner_hex = str(msg[1]).strip() + if len(partner_hex) == 12: # sanity check + partner = _espnow_parse_mac(partner_hex) + save_peer(partner) + peer_mac = partner + print("Paired with", partner_hex) + pairing_mode = False + time.sleep_ms(500) + +def calibrate(): + print("CALIBRATING") + # ADCs + axes = [adc_3, adc_4, adc_8, adc_9] + + # --- Centering phase --- + centers = [0]*len(axes) + samples = 0 + start = time.ticks_ms() + next_flash = start + while time.ticks_diff(time.ticks_ms(), start) < 3000: + vals = [a.read() for a in axes] + centers = [c+v for c,v in zip(centers, vals)] + samples += 1 + # LED flash 6 Hz + if time.ticks_diff(time.ticks_ms(), next_flash) >= 0: + led.value(not led.value()) + next_flash = time.ticks_add(next_flash, 83) # ~83ms + time.sleep_ms(5) + centers = [int(c/samples) for c in centers] + print("Centers:", centers) + + # --- Min/Max phase --- + mins = [65535]*len(axes) + maxs = [0]*len(axes) + start = time.ticks_ms() + next_flash = start + while time.ticks_diff(time.ticks_ms(), start) < 5000: + vals = [a.read() for a in axes] + mins = [min(m,v) for m,v in zip(mins, vals)] + maxs = [max(m,v) for m,v in zip(maxs, vals)] + # LED flash 2 Hz + if time.ticks_diff(time.ticks_ms(), next_flash) >= 0: + led.value(not led.value()) + next_flash = time.ticks_add(next_flash, 250) # 250ms + time.sleep_ms(5) + print("Min/Max:", list(zip(mins, maxs))) + + # Return calibration data + return centers, mins, maxs + + +def apply_deadzone(norm_val, deadzone=0.05): + if abs(norm_val) < deadzone: + return 0.0 + if norm_val > 0: + return (norm_val - deadzone) / (1.0 - deadzone) + else: + return (norm_val + deadzone) / (1.0 - deadzone) + +def normalize_axis(raw, center, min_val, max_val): + if raw >= center: + span = max_val - center + if span <= 0: + return 0 + norm = (raw - center) / span + else: + span = center - min_val + if span <= 0: + return 0 + norm = (raw - center) / span # negative + # Clamp to [-1, 1] + if norm > 1: + norm = 1 + if norm < -1: + norm = -1 + # Apply deadzone (still in −1…+1) + norm = apply_deadzone(norm) + # Scale to −255…+255 and return integer + return int(norm * 255) + + +CAL_FILE = "calibration.txt" + +def save_calibration(centers, mins, maxs): + with open(CAL_FILE, "w") as f: + # Write as comma-separated values + f.write(",".join(str(v) for v in centers) + "\n") + f.write(",".join(str(v) for v in mins) + "\n") + f.write(",".join(str(v) for v in maxs) + "\n") + +def load_calibration(): + try: + with open(CAL_FILE, "r") as f: + lines = f.readlines() + centers = [int(v) for v in lines[0].strip().split(",")] + mins = [int(v) for v in lines[1].strip().split(",")] + maxs = [int(v) for v in lines[2].strip().split(",")] + return centers, mins, maxs + except: + return None, None, None + +# Globals to hold calibration +cal_centers, cal_mins, cal_maxs = load_calibration() +if cal_centers: + print("Loaded calibration:", cal_centers, cal_mins, cal_maxs) +else: + print("No calibration stored, will use raw values until calibrated") + + + +# ---------------- Setup ---------------- +_espnow_init(1, 'RATE_LORA_500K') +peer_mac = load_peer() +if not peer_mac: + enter_pairing() +else: + _espnow_add_peer(peer_mac) + + + + +# ---------------- Main loop ---------------- +while True: + if btn12.value() == 1 and PAIRMODE_ON_START == 1: + print("PAIRING") + start = time.ticks_ms() + while btn12.value() == 1: + if time.ticks_diff(time.ticks_ms(), start) > 3000: + enter_pairing() + if peer_mac: + _espnow_add_peer(peer_mac) + PAIRMODE_ON_START = 0 + break + else: + PAIRMODE_ON_START = 0 + + if btn5.value() == 1 and CALIBRATE_ON_START == 1: + print("CALIBRATING") + cal_centers, cal_mins, cal_maxs = calibrate() + print(cal_centers, cal_mins, cal_maxs) + save_calibration(cal_centers, cal_mins, cal_maxs) + CALIBRATE_ON_START = 0 + else: + CALIBRATE_ON_START = 0 + + if peer_mac: + if cal_centers and cal_mins and cal_maxs: + # Normalize the four joystick axes + axes_raw = [adc_3.read(), adc_4.read(), adc_8.read(), adc_9.read()] + axes_norm = [ + normalize_axis(axes_raw[0], cal_centers[0], cal_mins[0], cal_maxs[0]), + normalize_axis(axes_raw[1], cal_centers[1], cal_mins[1], cal_maxs[1]), + normalize_axis(axes_raw[2], cal_centers[2], cal_mins[2], cal_maxs[2]), + normalize_axis(axes_raw[3], cal_centers[3], cal_mins[3], cal_maxs[3]), + ] + else: + # Fallback to raw values if not calibrated + axes_norm = [adc_3.read(), adc_4.read(), adc_8.read(), adc_9.read()] + + # Build the packet explicitly + payload = [ + axes_norm[0], axes_norm[1], axes_norm[2], axes_norm[3], + adc_1.read(), adc_10.read(), + btn2.value(), btn5.value(), btn6.value(), + btn7.value(), btn11.value(), btn12.value() + ] + _espnow_send_peer(peer_mac, payload) + led.value(not led.value()) + + time.sleep_ms(50) + +