diff --git a/CasterQuickReference.pdf b/CasterQuickReference.pdf index 376da8c85..c5b94a8fa 100644 Binary files a/CasterQuickReference.pdf and b/CasterQuickReference.pdf differ diff --git a/castervoice/lib/const.py b/castervoice/lib/const.py index 6fabb7b70..52950f62d 100644 --- a/castervoice/lib/const.py +++ b/castervoice/lib/const.py @@ -13,7 +13,7 @@ class CCRType(object): # Original Caster CCR "core" set: "Alphabet", "Navigation", "NavigationNon", "Numbers", "Punctuation", # Rules which were split out of _caster.py: - "CasterRule", "HardwareRule", "MouseAlternativesRule", "WindowManagementRule", + "CasterRule", "CasterMicRule", "HardwareRule", "MouseAlternativesRule", "WindowManagementRule", # Alternate mouse grid controls: "DouglasGridRule", "RainbowGridRule", "SudokuGridRule", # HMC GUI control rules: diff --git a/castervoice/lib/ctrl/configure_engine.py b/castervoice/lib/ctrl/configure_engine.py index daff0453e..03ad439e1 100644 --- a/castervoice/lib/ctrl/configure_engine.py +++ b/castervoice/lib/ctrl/configure_engine.py @@ -1,7 +1,7 @@ # TODO: Create and utilize base class. These classes should be initialized only once. # TODO: Add a function for end-user user to overload in EngineConfigEarly and EngineConfigLate -class EngineConfigEarly(): +class EngineConfigEarly: """ Initializes engine specific customizations before Nexus initializes. Grammars are not loaded @@ -23,43 +23,40 @@ def set_cancel_word(self): SymbolSpecs.set_cancel_word("escape") -class EngineConfigLate(): +class EngineConfigLate: """ Initializes engine specific customizations after Nexus has initialized. Grammars are loaded into engine. """ from castervoice.lib import settings - from castervoice.lib import printer from dragonfly import get_current_engine engine = get_current_engine().name def __init__(self): - from castervoice.lib import control # Access to Nexus instance - self.instannce = control.nexus()._engine_modes_manager + from castervoice.lib.ctrl.mgr.engine_manager import EngineModesManager + self.EngineModesManager = EngineModesManager.initialize() if self.engine != "text": self.set_default_mic_mode() self.set_engine_default_mode() - def set_default_mic_mode(self): """ Sets the microphone state on Caster startup. """ # Only DNS supports mic_state 'off'. Substituts `sleep` mode on other engines" - if self.settings.SETTINGS["engine"]["default_mic"]: # Default is `False` - default_mic_state = self.settings.SETTINGS["engine"]["mic_mode"] # Default is `on` - if self.engine != "natlink" and default_mic_state == "off": - default_mic_state == "sleep" - self.instannce.set_mic_mode(default_mic_state) - + if self.settings.SETTINGS["engine"]["default_mic"]: # Default is `False` + default_mic_state = self.settings.SETTINGS["engine"]["mic_mode"] # Default is `on` + if self.engine != "natlink" and default_mic_state == "off": + default_mic_state = "sleep" + self.EngineModesManager.set_mic_mode(default_mic_state) def set_engine_default_mode(self): """ Sets the engine mode on Caster startup. """ # Only DNS supports 'normal'. Substituts `command` mode on other engines" - if self.settings.SETTINGS["engine"]["default_engine_mode"]: # Default is `False` + if self.settings.SETTINGS["engine"]["default_engine_mode"]: # Default is `False` default_mode = self.settings.SETTINGS["engine"]["engine_mode"] # Default is `normal` if self.engine != "natlink" and default_mode == "normal": - default_mode == "command" - self.instannce.set_engine_mode(mode=default_mode, state=True) \ No newline at end of file + default_mode = "command" + self.EngineModesManager.set_engine_mode(mode=default_mode, state=True) diff --git a/castervoice/lib/ctrl/mgr/engine_manager.py b/castervoice/lib/ctrl/mgr/engine_manager.py index 85b680a47..d31ad617b 100644 --- a/castervoice/lib/ctrl/mgr/engine_manager.py +++ b/castervoice/lib/ctrl/mgr/engine_manager.py @@ -1,104 +1,178 @@ -from dragonfly import get_engine, get_current_engine -from castervoice.lib import settings, printer +from dragonfly import get_engine, get_current_engine, FuncContext, Function, MappingRule, Grammar, Choice, Dictation +from castervoice.lib import printer + +engine = get_current_engine().name +if engine == 'natlink': + import natlink -# TODO: Implement a grammar exclusivity for non-DNS engines in a separate class class EngineModesManager(object): """ Manages engine modes and microphone states using backend engine API and through dragonfly grammar exclusivity. """ - engine = get_current_engine().name - if engine == 'natlink': - import natlink - - engine_modes = {"normal":0, "command":2, "dictation":1,"numbers":3, "spell":4} - mic_modes = ["on", "sleeping", "off"] + engine_modes = {"normal": 0, "command": 2, + "dictation": 1, "numbers": 3, "spell": 4} + mic_modes = {"on": 5, "sleeping": 6, "off": 7} engine_state = None previous_engine_state = None mic_state = None + timer_callback = None - def initialize(self): + @classmethod + def initialize(cls): # Remove "normal" and "off" from 'states' for non-DNS based engines. - if self.engine != 'natlink': - self.engine_modes.pop("normal", 0) - self.mic_modes.remove("off") + if engine != 'natlink': + cls.engine_modes.pop("normal", 0) + cls.mic_modes.pop("off", 7) + #if self.engine == 'natlink': # Sets 1st index key ("normal" or "command") depending on engine type as default mode - self.engine_state = self.previous_engine_state = next(iter(self.engine_modes.keys())) - + cls.engine_state = cls.previous_engine_state = next( + iter(cls.engine_modes.keys())) + # Timer to synchronize natlink.getMicState/SetRecognitionMode with mode_state in case of changed by end-user via DNS GUI. + if engine == 'natlink' and cls.timer_callback is None: + cls.timer_callback = get_current_engine().create_timer(callback=cls._sync_mode, interval=1) + cls.timer_callback.start() - def set_mic_mode(self, mode): + @classmethod + def set_mic_mode(cls, mode): """ Changes the engine microphone mode - 'on': mic is on - 'sleeping': mic from the sleeping and can be woken up by command - 'off': mic off and cannot be turned back on by voice. (DNS Only) + :param mode: str + 'on': mic is on + 'sleeping': mic from the sleeping and can be woken up by command + 'off': mic off and cannot be turned back on by voice. (DPI Only) """ - if mode in self.mic_modes: - self.mic_state = mode - if self.engine == 'natlink': - self.natlink.setMicState(mode) - # From here other engines use grammar exclusivity to re-create the sleep mode - #if mode != "off": # off does not need grammar exclusivity - #pass - # TODO: Implement mic mode sleep mode using grammar exclusivity. This should override DNS is built in sleep grammar but kept in sync automatically with natlink.setmic_state - else: - printer.out("Caster: 'set_mic_mode' is not implemented for '{}'".format(self.engine)) + if mode in cls.mic_modes: + cls.mic_state = mode + if engine == 'natlink': + natlink.setMicState(mode) + # Overrides DNS/DPI is built in sleep grammar + ExclusiveManager(mode, modetype="mic_mode") else: - printer.out("Caster: '{}' is not a valid. set_mic_state modes are: 'off' - DNS Only, 'on', 'sleeping'".format(mode)) + printer.out( + "Caster: '{}' is not valid. set_mic_state modes are: 'off' - DPI Only, 'on', 'sleeping'".format(mode)) - - def get_mic_mode(self): + @classmethod + def get_mic_mode(cls): """ Returns mic state. - mode: string + mode: str """ - return self.mic_state - + return cls.mic_state - def set_engine_mode(self, mode=None, state=True): + @classmethod + def set_engine_mode(cls, mode=None, state=True): """ Sets the engine mode so that only certain types of commands/dictation are recognized. - 'state': Bool - enable/disable mode. + :param state: Bool - enable/disable mode. 'True': replaces current mode (Default) 'False': restores previous mode - 'normal': dictation and command (Default: DNS only) - 'dictation': Dictation only - 'command': Commands only (Default: Other engines) - 'numbers': Numbers only - 'spell': Spelling only + :param mode: str + 'normal': dictation and command (Default: DPI only) + 'dictation': Dictation only + 'command': Commands only (Default: Other engines) + 'numbers': Numbers only + 'spell': Spelling only """ if state and mode is not None: # Track previous engine state - # TODO: Timer to synchronize natlink.getMicState() with mengine_state in case of changed by end-user via DNS GUI. - self.previous_engine_state = self.engine_state + cls.previous_engine_state = cls.engine_state else: if not state: # Restore previous mode - mode = self.previous_engine_state + mode = cls.previous_engine_state else: - printer.out("Caster: set_engine_mode: 'State' cannot be 'True' with a undefined a 'mode'") - - if mode in self.engine_modes: - if self.engine == 'natlink': + printer.out( + "Caster: set_engine_mode: 'State' cannot be 'True' with a undefined a 'mode'") + + if mode in cls.engine_modes: + if engine == 'natlink': try: - self.natlink.execScript("SetRecognitionMode {}".format(self.engine_modes[mode])) # engine_modes[mode] is an integer - self.engine_state = mode + natlink.execScript("SetRecognitionMode {}".format( + cls.engine_modes[mode])) # engine_modes[mode] is an integer + cls.engine_state = mode + ExclusiveManager(mode, modetype="engine_mode") except Exception as e: printer.out("natlink.execScript failed \n {}".format(e)) else: - # TODO: Implement mode exclusivity. This should override DNS is built in sleep grammar but kept in sync automatically with natlinks SetRecognitionMode - # Once DNS enters its native mode exclusivity will override the any native DNS mode except for normal/command mode. - if self.engine == 'text': - self.engine_state = mode + # TODO: Implement set_engine_mode exclusivity. This should override DPI is built mode but kept in sync automatically. + if engine == 'text': + cls.engine_state = mode else: - printer.out("Caster: 'set_engine_mode' is not implemented for '{}'".format(self.engine)) + printer.out( + "Caster: 'set_engine_mode' is not implemented for '{}'".format(engine)) else: - printer.out("Caster: '{}' mode is not a valid. set_engine_mode: Modes: 'normal'- DNS Only, 'dictation', 'command', 'numbers', 'spell'".format(mode)) - + printer.out( + "Caster: '{}' mode is not valid. set_engine_mode: Modes: 'normal'- DPI Only, 'dictation', 'command', 'numbers', 'spell'".format(mode)) - def get_engine_mode(self): + @classmethod + def get_engine_mode(cls): """ Returns engine mode. - mode: str + mode: str + """ + return cls.engine_state + + @classmethod + def _sync_mode(cls): + """ + Synchronizes Caster exclusivity modes an with DNS/DPI GUI built-in modes state. """ - return self.engine_state + # TODO: Implement set_engine_mode logic with modes not just mic_state. + mic_state = cls.get_mic_mode() + if mic_state is None: + cls.mic_state = natlink.getMicState() + else: + if natlink.getMicState() != mic_state: + cls.set_mic_mode(natlink.getMicState()) + + +class ExclusiveManager: + """ + Loads and switches exclusivity for caster modes + :param mode: str + :param modetype: 'mic_mode' or 'engine_mode' str + """ + # TODO: Implement set_engine_mode exclusivity with mode rules. + # TODO: Implement timer for sleep mode. + # TODO: Implement hotkey for microphone on-off + sleep_grammar = None + sleeping = False + + sleep_rule = MappingRule( + name="sleep_rule", + mapping={ + "caster ": Function(lambda mic_mode: EngineModesManager.set_mic_mode(mode=mic_mode)), + "": Function(lambda text: False) + }, + extras=[Choice("mic_mode", { + "off": "off", + "on": "on", + "sleep": "sleeping", + }), + Dictation("text")], + context=FuncContext(lambda: ExclusiveManager.sleeping), + ) + + def __init__(self, mode, modetype): + if modetype == "mic_mode": + if not isinstance(ExclusiveManager.sleep_grammar, Grammar): + grammar = ExclusiveManager.sleep_grammar = Grammar("sleeping") + grammar.add_rule(self.sleep_rule) + grammar.load() + if mode == "sleeping": + self.set_exclusive(state=True) + printer.out("Caster: Microphone is sleeping") + if mode == "on": + self.set_exclusive(state=False) + printer.out("Caster: Microphone is on") + if mode == "off": + printer.out("Caster: Microphone is off") + else: + printer.out("{}, {} not implemented".format(mode, modetype)) + + def set_exclusive(self, state): + grammar = ExclusiveManager.sleep_grammar + ExclusiveManager.sleeping = state + grammar.set_exclusive(state) + get_engine().process_grammars_context() \ No newline at end of file diff --git a/castervoice/lib/ctrl/nexus.py b/castervoice/lib/ctrl/nexus.py index 1c1c67002..231571c84 100644 --- a/castervoice/lib/ctrl/nexus.py +++ b/castervoice/lib/ctrl/nexus.py @@ -32,7 +32,6 @@ from castervoice.lib.ctrl.mgr.validation.rules.rule_validation_delegator import CCRRuleValidationDelegator from castervoice.lib.merge.ccrmerging2.ccrmerger2 import CCRMerger2 from castervoice.lib.merge.ccrmerging2.merging.classic_merging_strategy import ClassicMergingStrategy -from castervoice.lib.ctrl.mgr.engine_manager import EngineModesManager class Nexus: def __init__(self, content_loader): @@ -79,13 +78,9 @@ def __init__(self, content_loader): self._content_loader, hooks_runner, rules_config, smrc, mapping_rule_maker, transformers_runner) - '''tracks engine grammar exclusivity and mic states -- TODO Grammar exclusivity should be managed through grammar manager''' - self._engine_modes_manager = EngineModesManager() - '''ACTION TIME:''' self._load_and_register_all_content(rules_config, hooks_runner, transformers_runner) self._grammar_manager.initialize() - self._engine_modes_manager.initialize() def _load_and_register_all_content(self, rules_config, hooks_runner, transformers_runner): """ diff --git a/castervoice/rules/apps/speech_engine/dragon_rules/dragon.py b/castervoice/rules/apps/speech_engine/dragon_rules/dragon.py index 07b2a7c83..3c8a7fd36 100644 --- a/castervoice/rules/apps/speech_engine/dragon_rules/dragon.py +++ b/castervoice/rules/apps/speech_engine/dragon_rules/dragon.py @@ -9,8 +9,6 @@ class DragonRule(MappingRule): mapping = { - '(lock Dragon | deactivate)': - R(Playback([(["go", "to", "sleep"], 0.0)])), '(number|numbers) mode': R(Playback([(["numbers", "mode", "on"], 0.0)])), 'spell mode': diff --git a/castervoice/rules/core/engine_manager_rules/mic_rules.py b/castervoice/rules/core/engine_manager_rules/mic_rules.py new file mode 100644 index 000000000..bbf348ba2 --- /dev/null +++ b/castervoice/rules/core/engine_manager_rules/mic_rules.py @@ -0,0 +1,24 @@ +from dragonfly import Dictation, MappingRule, Choice, Function +from castervoice.lib.actions import Text + +from castervoice.lib.ctrl.mgr.rule_details import RuleDetails +from castervoice.lib.merge.state.short import R + +from castervoice.lib.ctrl.mgr.engine_manager import EngineModesManager + +class CasterMicRule(MappingRule): + mapping = { + "caster ": Function(lambda mic_mode: EngineModesManager.set_mic_mode(mode=mic_mode)), + } + extras = [ + Choice( + "mic_mode", { + "off": "off", + "on": "on", + "sleep": "sleeping", + }), + ] + defaults = {} + +def get_rule(): + return CasterMicRule, RuleDetails(name="caster mic mode", transformer_exclusion=True) \ No newline at end of file diff --git a/docs/CasterQuickReference.pdf b/docs/CasterQuickReference.pdf index 376da8c85..c5b94a8fa 100644 Binary files a/docs/CasterQuickReference.pdf and b/docs/CasterQuickReference.pdf differ diff --git a/docs/CasterQuickReference.tex b/docs/CasterQuickReference.tex index 39bbf25e5..9946ce914 100644 --- a/docs/CasterQuickReference.tex +++ b/docs/CasterQuickReference.tex @@ -437,11 +437,13 @@ \section*{Caster Quick Reference} % Title \sectiontitle{Update and Caster Management} +\command{caster \footnotemark[2]}{microphone on, off, sleep} \command{clear caster log}{Clears Log Window} \command{update dragonfly}{Updates Dragonfly} \command{reboot caster}{Restarts Caster} \command{caster settings editor}{Caster settings editor} +\footnotetext[2]{'Off' Implemented for DNS/DPI only} %---------------------------------------------------------------------------------------- \end{minipage} }