first
commit
1b4d60e19a
|
|
@ -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)
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue