feat: initial commit - Carinha Control Panel v1.0 com UI glassmorphism, watch-bar, emojis, braco robotico e conversa IA
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""Testes individuais do PCA9685. Inicialização sem movimento; pulso com prazo."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import time
|
||||
|
||||
def integer(value, lo, hi):
|
||||
if type(value) is not int or not lo <= value <= hi:
|
||||
raise ValueError(f'Esperava inteiro entre {lo} e {hi}')
|
||||
return value
|
||||
|
||||
class PCA:
|
||||
def __init__(self, bus):
|
||||
import fcntl
|
||||
self.fd = os.open(bus, os.O_RDWR)
|
||||
fcntl.ioctl(self.fd, 0x0703, 0x40)
|
||||
self.ready = False
|
||||
def write(self, reg, values):
|
||||
os.write(self.fd, bytes([reg, *values]))
|
||||
def read(self, reg):
|
||||
os.write(self.fd, bytes([reg]))
|
||||
return os.read(self.fd, 1)[0]
|
||||
def init(self):
|
||||
# All outputs off before changing global frequency; no position at startup.
|
||||
self.write(0xFD, [0x10])
|
||||
for channel in range(16): self.off(channel)
|
||||
old = self.read(0)
|
||||
self.write(0, [(old & 0x7F) | 0x10])
|
||||
self.write(0xFE, [121]) # 25 MHz / (4096 * (121+1)) ≈ 50 Hz
|
||||
self.write(0, [(old & ~0x10) | 0x20])
|
||||
time.sleep(.005)
|
||||
self.write(0, [(old & ~0x10) | 0xA0])
|
||||
self.ready = True
|
||||
def pulse(self, channel, us):
|
||||
if not self.ready: self.init()
|
||||
ticks = round(us * 25 / 122)
|
||||
self.write(6 + 4 * channel, [0, 0, ticks & 255, ticks >> 8])
|
||||
def off(self, channel):
|
||||
self.write(9 + 4 * channel, [0x10])
|
||||
|
||||
class ControleServos:
|
||||
def __init__(self, arquivo, hardware=None):
|
||||
self.path = Path(arquivo)
|
||||
self.lock = threading.RLock()
|
||||
self.hw = hardware
|
||||
self.error = ''
|
||||
self.active = {}
|
||||
self.closed = threading.Event()
|
||||
self.config = [dict(nome=f'Servo {i+1}', canal=i, minimo=1400, maximo=1600, centro=1500) for i in range(6)]
|
||||
try:
|
||||
self.config = self.validate(json.loads(self.path.read_text(encoding='utf-8')))
|
||||
except (OSError, ValueError): pass
|
||||
if self.hw is None and os.name == 'posix':
|
||||
bus = os.environ.get('CARINHA_I2C')
|
||||
if bus:
|
||||
try: self.hw = PCA(bus)
|
||||
except OSError as e: self.error = str(e)
|
||||
self.worker = threading.Thread(target=self.watch, daemon=True)
|
||||
self.worker.start()
|
||||
def validate(self, rows):
|
||||
if not isinstance(rows, list) or not 1 <= len(rows) <= 16:
|
||||
raise ValueError('Use entre 1 e 16 servos')
|
||||
result, channels = [], set()
|
||||
for row in rows:
|
||||
if not isinstance(row, dict): raise ValueError('Servo inválido')
|
||||
channel = integer(row.get('canal'), 0, 15)
|
||||
if channel in channels: raise ValueError('Canais repetidos')
|
||||
channels.add(channel)
|
||||
name = row.get('nome')
|
||||
if not isinstance(name, str) or not 1 <= len(name.strip()) <= 40: raise ValueError('Nome inválido')
|
||||
lo = integer(row.get('minimo'), 1000, 2000)
|
||||
hi = integer(row.get('maximo'), 1000, 2000)
|
||||
center = integer(row.get('centro'), lo, hi)
|
||||
if lo >= hi: raise ValueError('Mínimo deve ser menor que máximo')
|
||||
result.append(dict(nome=name.strip(), canal=channel, minimo=lo, maximo=hi, centro=center))
|
||||
return result
|
||||
def status(self):
|
||||
with self.lock:
|
||||
return dict(disponivel=self.hw is not None, erro=self.error,
|
||||
servos=[dict(row) for row in self.config], ativos=list(self.active), prazo=2)
|
||||
def save(self, rows):
|
||||
rows = self.validate(rows)
|
||||
with self.lock:
|
||||
if self.active: raise ValueError('Desligue os sinais antes de salvar')
|
||||
tmp = self.path.with_suffix('.tmp')
|
||||
tmp.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
tmp.replace(self.path)
|
||||
self.config = rows
|
||||
return self.status()
|
||||
def move(self, index, pulse):
|
||||
with self.lock:
|
||||
index = integer(index, 0, len(self.config)-1)
|
||||
row = self.config[index]
|
||||
pulse = integer(pulse, row['minimo'], row['maximo'])
|
||||
if self.hw is None: raise OSError('PCA9685 indisponível neste servidor')
|
||||
self.hw.pulse(row['canal'], pulse)
|
||||
self.active[row['canal']] = time.monotonic() + 2
|
||||
return self.status()
|
||||
def expire(self, now):
|
||||
with self.lock:
|
||||
for channel, deadline in list(self.active.items()):
|
||||
if now >= deadline:
|
||||
self.hw.off(channel)
|
||||
del self.active[channel]
|
||||
def stop(self):
|
||||
with self.lock:
|
||||
if self.hw:
|
||||
for channel in range(16): self.hw.off(channel)
|
||||
self.active.clear()
|
||||
return self.status()
|
||||
def watch(self):
|
||||
while not self.closed.wait(.1):
|
||||
try: self.expire(time.monotonic())
|
||||
except OSError as e: self.error = str(e)
|
||||
def close(self):
|
||||
self.closed.set()
|
||||
self.worker.join(timeout=1)
|
||||
def rotas(self, h, method):
|
||||
route = h.path.split('?', 1)[0]
|
||||
if route not in ('/servos/status', '/servos/config', '/servos/mover', '/servos/parar'): return False
|
||||
try:
|
||||
if method == 'GET' and route == '/servos/status': result = self.status()
|
||||
elif method == 'POST':
|
||||
origin = h.headers.get('Origin')
|
||||
if origin and origin not in ('http://' + h.headers.get('Host', ''), 'https://' + h.headers.get('Host', '')):
|
||||
raise ValueError('Origem não permitida')
|
||||
if h.headers.get('Content-Type', '').split(';')[0] != 'application/json': raise ValueError('Use JSON')
|
||||
size = int(h.headers.get('Content-Length', '0'))
|
||||
if not 0 < size <= 8192: raise ValueError('Corpo inválido')
|
||||
data = json.loads(h.rfile.read(size))
|
||||
if not isinstance(data, dict): raise ValueError('Objeto JSON obrigatório')
|
||||
if route == '/servos/config': result = self.save(data.get('servos'))
|
||||
elif route == '/servos/mover': result = self.move(data.get('indice'), data.get('pulso'))
|
||||
elif route == '/servos/parar': result = self.stop()
|
||||
else: raise ValueError('Rota inválida')
|
||||
else:
|
||||
h.enviar(json.dumps({'erro':'Método não permitido'}), codigo=405)
|
||||
return True
|
||||
h.enviar(json.dumps(result, ensure_ascii=False))
|
||||
except (ValueError, TypeError) as e:
|
||||
h.enviar(json.dumps({'erro':str(e)}), codigo=400)
|
||||
except OSError as e:
|
||||
h.enviar(json.dumps({'erro':str(e)}), codigo=503)
|
||||
return True
|
||||
Reference in New Issue
Block a user