primocode 10.14.1 → 10.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,275 @@
1
+ """hud.py — a tela do Primo (macOS, PyObjC + WKWebView).
2
+
3
+ Uma janela GRANDE e normal (ocupa a área visível da tela, com os botões de
4
+ fechar/minimizar/redimensionar no canto, título transparente) que carrega hud.html:
5
+ calendário e compromissos de hoje, o robô 3D (é o próprio Primo; cai no SVG se o WebGL falhar),
6
+ clima, música com disco girando, anéis da máquina, rede e o visualizador de voz.
7
+ Abre no monitor onde está o mouse. Este arquivo só hospeda: os dados vêm de tela.py.
8
+
9
+ entra (stdin): mensagens do painel.py ({"estado":..,"nivel":..,"transcricao":..})
10
+ sai (stdout): {"acao":"ptt"} quando a pessoa clica
11
+ Esconder: ESC, "esconde o painel" ou ctrl+opt+P (este precisa de Acessibilidade).
12
+ Teste sem voz: python -m primo_voz.hud --demo
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import math
18
+ import os
19
+ import queue
20
+ import sys
21
+ import threading
22
+ import time
23
+ from pathlib import Path
24
+
25
+ from primo_voz import identidade, tela as td
26
+ from primo_voz import painel as pn
27
+ from primo_voz.sons import Sons
28
+
29
+ AQUI = Path(__file__).resolve().parent
30
+
31
+
32
+ def main(demo: bool = False, entrada=None, saida=None, ao_iniciar=None, ao_sair=None) -> int:
33
+ """entrada/saida/ao_iniciar/ao_sair: o modo app (app.py) liga o painel ao motor de voz sem passar por stdin/stdout."""
34
+ try:
35
+ from AppKit import (NSApplication, NSApplicationActivationPolicyRegular, NSImage, NSMenu, NSMenuItem, NSBackingStoreBuffered, NSColor,
36
+ NSEvent, NSScreen, NSWindow)
37
+ from Foundation import NSMakeRect, NSObject, NSTimer, NSURL
38
+ import objc
39
+ from WebKit import WKWebView, WKWebViewConfiguration, WKUserContentController
40
+ except ImportError as e:
41
+ sys.stderr.write(f"o painel precisa do PyObjC ({e.name})\n")
42
+ return 3
43
+
44
+ p = pn.Painel()
45
+ fila: "queue.Queue[str]" = queue.Queue()
46
+ sons = Sons(os.environ.get("PRIMOCODE_SEM_SONS") != "1")
47
+
48
+ def emitir(obj: dict) -> None:
49
+ if saida:
50
+ saida(obj)
51
+ else:
52
+ sys.stdout.write(json.dumps(obj) + "\n")
53
+ sys.stdout.flush()
54
+
55
+ def js(msg: dict) -> None:
56
+ fila.put("window.primo(" + json.dumps(msg, ensure_ascii=False) + ")")
57
+
58
+ class Janela(NSWindow):
59
+ def canBecomeKeyWindow(self):
60
+ return True
61
+
62
+ def canBecomeMainWindow(self):
63
+ return True
64
+
65
+ class Alvo(NSObject):
66
+ def tick_(self, _):
67
+ n = 0
68
+ while n < 40:
69
+ try:
70
+ cod = fila.get_nowait()
71
+ except queue.Empty:
72
+ break
73
+ self.web.evaluateJavaScript_completionHandler_(cod, None)
74
+ n += 1
75
+ if self.jan.isMiniaturized():
76
+ return
77
+ if p.visivel != bool(self.jan.isVisible()):
78
+ if p.visivel:
79
+ self.jan.deminiaturize_(None)
80
+ self.jan.makeKeyAndOrderFront_(None)
81
+ js({"boot": True})
82
+ sons.tocar("boot")
83
+ else:
84
+ self.jan.orderOut_(None)
85
+
86
+ class Dono(NSObject):
87
+ def windowShouldClose_(self, _): # fechar o painel só esconde; o PrimoCode segue
88
+ p.visivel = False
89
+ return True
90
+
91
+ def applicationWillTerminate_(self, _n):
92
+ if ao_sair:
93
+ try:
94
+ ao_sair()
95
+ except Exception:
96
+ pass
97
+
98
+ def applicationShouldHandleReopen_hasVisibleWindows_(self, _a, _v): # clique no Dock traz de volta
99
+ p.visivel = True
100
+ return True
101
+
102
+ class Msg(NSObject, protocols=[objc.protocolNamed("WKScriptMessageHandler")]):
103
+ def userContentController_didReceiveScriptMessage_(self, ucc, msg):
104
+ try:
105
+ m = json.loads(str(msg.body()))
106
+ except ValueError:
107
+ return
108
+ a = m.get("acao")
109
+ if a == "erro": # erro de JavaScript da página: vai para o terminal
110
+ sys.stderr.write("hud.html: " + str(m.get("arg")) + "\n")
111
+ sys.stderr.flush()
112
+ elif a == "sair":
113
+ p.visivel = False
114
+ elif a in ("ptt", "entrar"):
115
+ emitir({"acao": a})
116
+ elif a == "print": # esconde o HUD para o print pegar a tela de verdade
117
+ p.visivel = False
118
+ threading.Timer(0.6, lambda: pn.executar_acao(m)).start()
119
+ threading.Timer(2.2, lambda: setattr(p, "visivel", True)).start()
120
+ elif td.acao(m):
121
+ pass
122
+ else:
123
+ def _fazer():
124
+ pn.executar_acao(m)
125
+ if a == "midia":
126
+ time.sleep(0.4)
127
+ js({"musica": td.musica()})
128
+ threading.Thread(target=_fazer, daemon=True).start()
129
+
130
+ app = NSApplication.sharedApplication()
131
+ app.setActivationPolicy_(NSApplicationActivationPolicyRegular) # app de verdade: Dock e Cmd+Tab
132
+ try: # nome e ícone do Primo, mesmo fora do bundle
133
+ from AppKit import NSBundle
134
+ info = NSBundle.mainBundle().infoDictionary()
135
+ info["CFBundleName"] = info["CFBundleDisplayName"] = "Primo"
136
+ icone = AQUI / "vendor" / "primo-icone.png"
137
+ if icone.exists():
138
+ app.setApplicationIconImage_(NSImage.alloc().initWithContentsOfFile_(str(icone)))
139
+ except Exception as e:
140
+ sys.stderr.write(f"hud: ícone: {e}\n")
141
+ barra = NSMenu.alloc().init()
142
+ raiz = NSMenuItem.alloc().init()
143
+ barra.addItem_(raiz)
144
+ menu = NSMenu.alloc().init()
145
+ for rot, sel, tec in (("Sobre o Primo", "orderFrontStandardAboutPanel:", ""), (None, None, None),
146
+ ("Esconder o Primo", "hide:", "h"), (None, None, None), ("Sair do Primo", "terminate:", "q")):
147
+ menu.addItem_(NSMenuItem.separatorItem() if rot is None else
148
+ NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(rot, sel, tec))
149
+ raiz.setSubmenu_(menu)
150
+ app.setMainMenu_(barra)
151
+ mouse = NSEvent.mouseLocation()
152
+ alvo_tela = next((s for s in NSScreen.screens() if s.frame().origin.x <= mouse.x < s.frame().origin.x + s.frame().size.width
153
+ and s.frame().origin.y <= mouse.y < s.frame().origin.y + s.frame().size.height), NSScreen.mainScreen())
154
+ tela = alvo_tela.visibleFrame() # maximizada, sem cobrir Dock nem barra de menu
155
+ # título | fechar | minimizar | redimensionar | conteúdo sob a barra de título
156
+ jan = Janela.alloc().initWithContentRect_styleMask_backing_defer_(tela, 1 | 2 | 4 | 8 | (1 << 15), NSBackingStoreBuffered, False)
157
+ jan.setTitlebarAppearsTransparent_(True)
158
+ jan.setTitleVisibility_(1) # sem texto de título
159
+ jan.setTitle_("Primo")
160
+ jan.setReleasedWhenClosed_(False)
161
+ dono = Dono.alloc().init()
162
+ jan.setDelegate_(dono)
163
+ app.setDelegate_(dono)
164
+ jan.setMinSize_((900, 600))
165
+ jan.setOpaque_(True)
166
+ jan.setBackgroundColor_(NSColor.colorWithSRGBRed_green_blue_alpha_(0.043, 0.043, 0.039, 1.0))
167
+ jan.setHasShadow_(False)
168
+ cfg = WKWebViewConfiguration.alloc().init()
169
+ cfg.preferences().setValue_forKey_(True, "allowFileAccessFromFileURLs") # o hud.html lê o .glb local (vendor/)
170
+ msg_h = Msg.alloc().init()
171
+ cfg.userContentController().addScriptMessageHandler_name_(msg_h, "primo")
172
+ web = WKWebView.alloc().initWithFrame_configuration_(NSMakeRect(0, 0, tela.size.width, tela.size.height), cfg)
173
+ web.setAutoresizingMask_(2 | 16)
174
+ web.setValue_forKey_(False, "drawsBackground")
175
+ jan.setContentView_(web)
176
+ url = NSURL.fileURLWithPath_(str(AQUI / "hud.html"))
177
+ web.loadFileURL_allowingReadAccessToURL_(url, NSURL.fileURLWithPath_(str(AQUI)))
178
+ jan.makeKeyAndOrderFront_(None)
179
+ app.activateIgnoringOtherApps_(True)
180
+ alvo = Alvo.alloc().init()
181
+ alvo.jan, alvo.web = jan, web
182
+ NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(1 / 30, alvo, b"tick:", None, True)
183
+ threading.Timer(1.2, lambda: (js({"nome": identidade.ler()["nome"]}), js({"robo": identidade.ler()["robo"]}), js({"boot": True}), sons.tocar("boot"))).start()
184
+
185
+ def esconder():
186
+ p.visivel = False
187
+
188
+ def tecla(ev):
189
+ if ev.keyCode() == 53: # ESC
190
+ esconder()
191
+ return None
192
+ return ev
193
+ NSEvent.addLocalMonitorForEventsMatchingMask_handler_(1 << 10, tecla)
194
+
195
+ def atalho(ev):
196
+ f = ev.modifierFlags()
197
+ if (f & 0x40000) and (f & 0x80000) and ev.keyCode() == 35:
198
+ p.visivel = not p.visivel
199
+ try:
200
+ NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(1 << 10, atalho)
201
+ except Exception:
202
+ pass
203
+
204
+ def medir():
205
+ am = pn.Amostrador()
206
+ cache: dict = {}
207
+ k = 0
208
+ while True:
209
+ try:
210
+ js({"maquina": td.maquina(am)})
211
+ js({"musica": td.musica()})
212
+ if k % 20 == 0:
213
+ js({"clima": td.clima_widget(cache)})
214
+ except Exception as e:
215
+ sys.stderr.write(f"hud: {e}\n")
216
+ k += 1
217
+ time.sleep(1)
218
+ threading.Thread(target=medir, daemon=True).start()
219
+
220
+ def agenda(): # em outra thread: o pedido de permissão do Calendário pode demorar
221
+ evs: dict = {"permissao": "pendente", "eventos": []}
222
+ k = 0
223
+ while True:
224
+ try:
225
+ if k % 12 == 0 or evs["permissao"] != "ok": # sem acesso, olha a cada 5 s (a pessoa pode ter liberado)
226
+ evs = td.eventos_do_mes()
227
+ js({"calendario": {"permissao": evs["permissao"], "dias": sorted({e["dia"] for e in evs["eventos"]})}})
228
+ if evs["permissao"] == "ok":
229
+ js({"hoje": td.hoje_lista(evs["eventos"], time.time())})
230
+ except Exception as e:
231
+ sys.stderr.write(f"hud agenda: {e}\n")
232
+ k += 1
233
+ time.sleep(5)
234
+ threading.Thread(target=agenda, daemon=True).start()
235
+
236
+ def ler():
237
+ for l in (entrada if entrada is not None else sys.stdin):
238
+ try:
239
+ m = json.loads(l)
240
+ except ValueError:
241
+ continue
242
+ p.aplicar(m)
243
+ js(m)
244
+ if "estado" in m or "nivel" in m:
245
+ js({"barras": td.barras(p.estado, p.nivel)})
246
+ if entrada is None:
247
+ os._exit(0)
248
+
249
+ def encenar():
250
+ roteiro = [("dormindo", "", "", 3), ("ouvindo", "roda os testes do projeto", "", 4),
251
+ ("pensando", "roda os testes do projeto", "", 3),
252
+ ("falando", "roda os testes do projeto", "Rodei. Passou tudo, 412 testes. Um aviso no painel.", 5)]
253
+ while True:
254
+ for est, tr, rs, dur in roteiro:
255
+ p.aplicar({"estado": est})
256
+ js({"estado": est, "transcricao": tr, "parcial": est == "ouvindo", "resposta": rs})
257
+ fim = time.time() + dur
258
+ while time.time() < fim:
259
+ v = abs(math.sin(time.time() * 9) * math.sin(time.time() * 2.3)) if est in ("ouvindo", "falando") else 0
260
+ js({"barras": td.barras(est, v)})
261
+ time.sleep(0.06)
262
+ if est == "falando":
263
+ js({"evento": "sucesso"})
264
+ if entrada is not None:
265
+ threading.Thread(target=ler, daemon=True).start()
266
+ elif "--livre" not in sys.argv:
267
+ threading.Thread(target=encenar if demo else ler, daemon=True).start()
268
+ if ao_iniciar:
269
+ ao_iniciar(js, p)
270
+ app.run()
271
+ return 0
272
+
273
+
274
+ if __name__ == "__main__":
275
+ sys.exit(main(demo="--demo" in sys.argv))
@@ -0,0 +1,88 @@
1
+ """identidade.py — quem é o assistente e quem é a pessoa.
2
+
3
+ O assistente se chama "Primo" até a pessoa dar outro nome ("seu nome agora é
4
+ Luna", ou `primocode voz nome Luna`). A palavra de ativação é esse nome. Fica em
5
+ ~/.primocode/voz.json, junto com o nome da pessoa e as chaves de luz e som.
6
+
7
+ Personalidade: curta, natural, humor leve. Cumprimenta pela hora, lembra o nome
8
+ da pessoa, e varia a frase (nunca a mesma resposta robótica duas vezes seguidas).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import random
15
+ import re
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ PADRAO = {"nome": "Primo", "pessoa": "", "luz": True, "sons": True, "robo": "spline", "voz": "francisca", "tts": "edge"}
20
+
21
+
22
+ def arquivo() -> Path:
23
+ return Path(os.environ.get("PRIMOCODE_VOZ_CONFIG") or (Path.home() / ".primocode" / "voz.json"))
24
+
25
+
26
+ def ler() -> dict:
27
+ try:
28
+ d = json.loads(arquivo().read_text("utf-8"))
29
+ cfg = {**PADRAO, **{k: d[k] for k in PADRAO if k in d}}
30
+ if cfg["robo"] not in ("spline", "expressive"): # valor antigo ou torto: o padrão
31
+ cfg["robo"] = PADRAO["robo"]
32
+ return cfg
33
+ except Exception:
34
+ return dict(PADRAO)
35
+
36
+
37
+ def salvar(**campos) -> dict:
38
+ cfg = {**ler(), **campos}
39
+ a = arquivo()
40
+ a.parent.mkdir(parents=True, exist_ok=True)
41
+ a.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), "utf-8")
42
+ return cfg
43
+
44
+
45
+ def _nome_valido(x: str) -> Optional[str]:
46
+ x = re.sub(r"[^\wÀ-ÿ \-]", "", x or "").strip()
47
+ palavras = x.split()
48
+ if not 1 <= len(palavras) <= 2 or len(x) > 20 or len(x) < 2:
49
+ return None
50
+ return " ".join(w[:1].upper() + w[1:] for w in palavras)
51
+
52
+
53
+ def aprender(texto: str) -> Optional[dict]:
54
+ """"seu nome agora é Luna" / "pode me chamar de Joel" → o que gravar (ou None)."""
55
+ t = " ".join(str(texto or "").split()).strip(" .!?,")
56
+ m = re.search(r"(?:seu nome (?:agora )?é|vou te chamar de|(?:a partir de agora )?você se chama|te chamo de)\s+(.+)$", t, re.I)
57
+ if m and _nome_valido(m.group(1)):
58
+ return {"nome": _nome_valido(m.group(1))}
59
+ m = re.search(r"(?:pode me chamar de|meu nome é|me chama de|eu sou o|eu sou a)\s+(.+)$", t, re.I)
60
+ if m and _nome_valido(m.group(1)):
61
+ return {"pessoa": _nome_valido(m.group(1))}
62
+ return None
63
+
64
+
65
+ def periodo(hora: int) -> str:
66
+ return "Bom dia" if 5 <= hora < 12 else "Boa tarde" if 12 <= hora < 18 else "Boa noite"
67
+
68
+
69
+ def saudacao(hora: int, pessoa: str = "", nome: str = "Primo", rnd: Optional[random.Random] = None) -> str:
70
+ r = rnd or random
71
+ p = f", {pessoa}" if pessoa else ""
72
+ if hora < 5:
73
+ opcoes = [f"Ainda de pé{p}? Tô aqui.", f"Madrugada, hein{p}. Bora, mas sem exagero."]
74
+ else:
75
+ opcoes = [f"{periodo(hora)}{p}! {nome} na área.", f"{periodo(hora)}{p}. Pode falar.",
76
+ f"{periodo(hora)}{p}, tô ouvindo."]
77
+ return r.choice(opcoes)
78
+
79
+
80
+ FEITO = ["Feito.", "Pronto.", "Tá na mão.", "Resolvido."]
81
+ ERRO = ["Opa, deu ruim. Olha o painel.", "Travei aqui. O detalhe tá no painel."]
82
+ NAO_ENTENDI = ["Não peguei essa. Repete?", "Hã? Fala de novo."]
83
+
84
+
85
+ def variar(opcoes, ultimo: str = "", rnd: Optional[random.Random] = None) -> str:
86
+ r = rnd or random
87
+ cand = [o for o in opcoes if o != ultimo] or list(opcoes)
88
+ return r.choice(cand)
@@ -0,0 +1,137 @@
1
+ """luz.py — a luz do teclado reage ao estado do Primo.
2
+
3
+ MacBook: o teclado embutido só tem brilho (branco). O método: o framework
4
+ privado CoreBrightness (KeyboardBrightnessClient) via PyObjC, carregado em
5
+ tempo de execução; guarda o brilho e o modo automático da pessoa e restaura no
6
+ fim. Teclado RGB externo: OpenRGB SDK (servidor local, porta 6742), se houver
7
+ `openrgb` em PATH e o módulo `openrgb` instalado. Sem suporte, ou com
8
+ PRIMOCODE_SEM_LUZ=1 / "luz": false em voz.json, tudo vira silêncio.
9
+
10
+ ouvindo = respira · pensando = pulso duplo · falando = segue a voz · erro = pisca
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import math
15
+ import os
16
+ import sys
17
+ import threading
18
+ import time
19
+ from typing import Optional
20
+
21
+
22
+ def respira(t: float, periodo: float = 4.0, base: float = 0.10, topo: float = 0.45) -> float:
23
+ return base + (topo - base) * (0.5 - 0.5 * math.cos(t / periodo * math.tau))
24
+
25
+
26
+ def pulso(t: float, periodo: float = 1.1, base: float = 0.08, topo: float = 0.8) -> float:
27
+ """dois toques curtos e uma pausa (tipo coração apressado: o Primo está pensando)."""
28
+ x = (t % periodo) / periodo
29
+ forca = max(math.exp(-((x - 0.1) ** 2) / 0.002), 0.7 * math.exp(-((x - 0.3) ** 2) / 0.002))
30
+ return base + (topo - base) * forca
31
+
32
+
33
+ def alvo(estado: str, t: float, nivel: float = 0.0, erro_ate: float = 0.0, agora: float = 0.0) -> Optional[float]:
34
+ """Brilho 0..1 que o estado pede agora (None = não mexer)."""
35
+ if erro_ate > agora:
36
+ return 0.9 if int((erro_ate - agora) * 8) % 2 == 0 else 0.0
37
+ if estado == "ouvindo":
38
+ return respira(t)
39
+ if estado in ("pensando", "executando"):
40
+ return pulso(t)
41
+ if estado == "falando":
42
+ return 0.15 + 0.85 * min(1.0, nivel)
43
+ if estado == "interrompido":
44
+ return 0.7
45
+ return 0.0 if estado == "dormindo" else 0.12
46
+
47
+
48
+ class _Mac:
49
+ def __init__(self) -> None:
50
+ import objc
51
+ from Foundation import NSBundle
52
+ NSBundle.bundleWithPath_("/System/Library/PrivateFrameworks/CoreBrightness.framework").load()
53
+ self._c = objc.lookUpClass("KeyboardBrightnessClient").alloc().init()
54
+ ids = self._c.copyKeyboardBacklightIDs() or []
55
+ if not ids:
56
+ raise RuntimeError("sem luz de teclado")
57
+ self._k = ids[0]
58
+ self.antes = float(self._c.brightnessForKeyboard_(self._k))
59
+ self.auto = bool(self._c.isAutoBrightnessEnabledForKeyboard_(self._k))
60
+ self._c.enableAutoBrightness_forKeyboard_(False, self._k)
61
+
62
+ def definir(self, v: float) -> None:
63
+ self._c.setBrightness_fadeSpeed_commit_forKeyboard_(min(1.0, max(0.0, v)), 60, False, self._k)
64
+
65
+ def restaurar(self) -> None:
66
+ self._c.setBrightness_fadeSpeed_commit_forKeyboard_(self.antes, 200, False, self._k)
67
+ self._c.enableAutoBrightness_forKeyboard_(self.auto, self._k)
68
+
69
+
70
+ class _OpenRGB:
71
+ """Teclado RGB externo pelo servidor do OpenRGB. Cor ciano, intensidade = brilho."""
72
+
73
+ def __init__(self) -> None:
74
+ from openrgb import OpenRGBClient
75
+ from openrgb.utils import RGBColor
76
+ self._cor = RGBColor
77
+ self._cli = OpenRGBClient(name="Primo")
78
+ self._tec = [d for d in self._cli.devices if "keyboard" in str(d.type).lower()]
79
+ if not self._tec:
80
+ raise RuntimeError("sem teclado RGB")
81
+
82
+ def definir(self, v: float) -> None:
83
+ for d in self._tec:
84
+ d.set_color(self._cor(int(56 * v), int(225 * v), int(255 * v)))
85
+
86
+ def restaurar(self) -> None:
87
+ for d in self._tec:
88
+ d.set_mode("direct")
89
+
90
+
91
+ class Luz:
92
+ def __init__(self, ligada: bool = True) -> None:
93
+ self.dev = None
94
+ self.estado = "esperando"
95
+ self.nivel = 0.0
96
+ self._erro_ate = 0.0
97
+ self._vivo = True
98
+ if not ligada or os.environ.get("PRIMOCODE_SEM_LUZ") == "1":
99
+ return
100
+ for fabrica in ((_Mac,) if sys.platform == "darwin" else ()) + (_OpenRGB,):
101
+ try:
102
+ self.dev = fabrica()
103
+ break
104
+ except Exception:
105
+ continue
106
+ if self.dev:
107
+ threading.Thread(target=self._animar, daemon=True).start()
108
+
109
+ def mudar(self, estado: str, nivel: Optional[float] = None) -> None:
110
+ self.estado = estado
111
+ if nivel is not None:
112
+ self.nivel = nivel
113
+
114
+ def erro(self) -> None:
115
+ self._erro_ate = time.time() + 0.9
116
+
117
+ def _animar(self) -> None:
118
+ t0 = time.time()
119
+ while self._vivo:
120
+ agora = time.time()
121
+ v = alvo(self.estado, agora - t0, self.nivel, self._erro_ate, agora)
122
+ if v is not None:
123
+ try:
124
+ self.dev.definir(v)
125
+ except Exception:
126
+ self.dev = None
127
+ return
128
+ time.sleep(1 / 20)
129
+
130
+ def fechar(self) -> None:
131
+ self._vivo = False
132
+ if self.dev:
133
+ time.sleep(0.1)
134
+ try:
135
+ self.dev.restaurar()
136
+ except Exception:
137
+ pass