Skip to content

Use a muscle, blink or jaw-clench switch

The biosignal switch reads raw electrode signals and turns a small set of physical events into the same inputs a key press gives YazSes: a squeeze of a muscle, a blink or double blink, or a jaw clench. A squeeze can hold the microphone open like the hold-to-talk key; the other events can trigger a spoken-command label.

It is for people who cannot press or hold a key reliably, and for anyone whose hands are busy. It reads muscle and eye artifacts, not thoughts. It does not decode what you are thinking, imagining or looking at (the reasoning is on the research page).

Experimental, off by default, and every threshold is yours to calibrate. Nobody has yet measured how these switches behave over a full workday across people and electrode placements. A default install is unchanged until you set [biosignal] enabled = true.

What hardware you need

Pick one. Both are read locally; nothing is sent anywhere.

Hardware How YazSes reads it Best for
A muscle sensor on a microcontroller: MyoWare 2.0 or a BioAmp EXG Pill The existing serial path ([emg] device_port, the YESP protocol). The microcontroller does the detection and sends HOLD_START / HOLD_END. See the YESP spec. A squeeze as push-to-talk, cheapest, most reliable.
A board BrainFlow supports, such as an OpenBCI board or a Muse headband BrainFlow streams raw samples to YazSes, which does the detection itself (the emg-band extra). Raw-signal control, blink and jaw-clench switches, and measuring your own false-positive rate.
Other devices: a Lab Streaming Layer app (OpenBCI GUI, muse-lsl), a NeuroSky headset, a BITalino kit, a Myo armband YazSes reads the raw samples directly (lsl:, thinkgear:, bitalino:, myo: sources) and does the detection itself. The same as a BrainFlow board, with the device you already own. See the hardware guide.
No hardware The built-in synthetic source and replay of a recording file (CSV, .npz, EDF/BDF or XDF). Trying the commands and learning the output.

Put the electrode on the muscle you want to use, if you can. The research page explains why: a blink or jaw clench seen through an EEG headband is the same activity measured through the skull, noisily, and a dedicated electrode on the muscle is cleaner. The hardware guide compares the two paths, says where the repository documentation places electrodes, and covers the safety rules. This is not a medical device.

Install what it needs

The serial path needs the emg extra (pyserial); the BrainFlow path needs the emg-band extra (brainflow). The other devices need: lsl for an lsl: stream, emg for a NeuroSky or BITalino (thinkgear:, bitalino:), ble for a Myo armband (myo:), and xdf to replay an .xdf recording. EDF and BDF files need nothing. With nothing installed the feature is simply off and the rest of YazSes is unchanged. For the BrainFlow boards, install the extra into the environment YazSes runs from, using the command that matches how you installed it:

pipx install 'yazses[emg-band]'            # a pipx install
uv tool install 'yazses[emg-band]@latest'  # a uv tool install (list every extra you already use)

If a library is missing, YazSes logs a message with the exact command for your install method, and yazses biosignal doctor repeats it. Use that command if it differs from the two above. The pure signal-processing code needs only NumPy, which a normal install already has.

Turn it on

Add a section to your config.toml (~/.config/yazses/config.toml on Linux and macOS, %LOCALAPPDATA%\yazses\config.toml on Windows). The example shows the shape of the section; the generated configuration reference is the authority for exact key names, values and defaults.

[biosignal]
enabled = true
source = "synthetic"          # start here; later "file:<path>" or "brainflow:<board>[:<port>]"
mains_hz = 50                 # 50 or 60 -- the mains frequency where you live
emg = true                    # squeeze detector
double_blink = true           # two quick blinks
jaw_clench = false            # off by default: chewing, talking and yawning all clench the jaw
max_cancelled_per_hour = 6.0  # the false-positive budget; 0 means never suspend
calibration_file = ""         # empty = the default file in the data directory
speech_guard = true           # suppress a jaw clench while you dictate
ledger_autosave_seconds = 60  # seconds between saves of the activation counters (5 to 3600)
calibration_max_age_days = 30 # a calibration older than this is not used (1 to 365)
watchdog_seconds = 1.5        # release a held squeeze if no samples arrive for this long (0.5 to 30)
min_event_confidence = 0.0    # drop detections below this confidence (0 to 1); 0 keeps them all

# What a squeeze does, by strength (light, hard, or on for a detector that reports none).
# A light squeeze types what you say; a hard one speaks a command.
mode_map = { light = "full_text", hard = "command" }

# Discrete events (double_blink, jaw_clench) become one command phrase from the command
# grammar, run through the activation confirmation gate. Undo is the default for a double blink.
intent_map = { double_blink = "undo" }

Then restart so the daemon picks it up:

yazses restart

source is empty by default, and with no source nothing starts even when enabled is true. A BrainFlow source is brainflow:<board_id> with an optional serial port, where the board id is BrainFlow's integer id for your board, for example brainflow:0:/dev/ttyUSB0 for an OpenBCI Cyton. yazses biosignal doctor shows what it can see, and yazses biosignal boards lists the boards. Only the listed boards open: any other id is refused, and that includes boards that stream over a network, such as OpenBCI's Wi-Fi shield. The other source kinds (lsl:, thinkgear:, bitalino:, myo:) are listed in the hardware guide; run yazses biosignal check-source <source> once to see that a new device streams cleanly. An event left out of mode_map or intent_map does nothing. The values in the example are the shipped defaults except source and enabled; they are starting points, not tuned recommendations. Nobody has measured any of them on a real person yet, which is why the next sections show you how to measure your own.

The commands

Run yazses biosignal --help for exact options. The ones you will use first, in order:

  1. yazses biosignal doctor checks the chain end to end before you trust it: the extra is installed, the source opens, samples arrive at the expected rate, mains_hz matches the interference it sees, the signal is not flat or clipped, and a calibration profile exists and matches your channels. Each problem comes with the reason and the fix, and the output ends with one Verdict: line: either that you are ready, or the single most important next step (pick a source, install a library, calibrate). Read that line first. When it says you are ready, the next thing it points you to is yazses biosignal field-test. Start here, and again after you move the electrode. The system-wide yazses doctor also has a "Biosignal switch" row that stays quiet while the feature is off.
  2. yazses biosignal calibrate runs a short guided session: you relax, squeeze lightly, squeeze hard, blink three times, and clench your jaw three times. It skips a step your source has no channel for, and the blink and clench steps only run when those switches are enabled (or with --blink / --clench). It writes a small local profile, not your raw signal. It stops and writes nothing if the signal is bad, and it tells you which step failed.
  3. yazses biosignal monitor shows the live signal quality, the events as they are detected, and the state of the false-positive budget. Use it to see, before you rely on a switch, what makes it fire.
  4. yazses biosignal replay <recording> runs a saved recording through the same detectors with no hardware, so you can try a threshold change on yesterday's data or check a bug report.
  5. yazses biosignal report summarises how often each event fired and how often you cancelled it, with a confidence interval, so you can judge the switch on evidence.
  6. yazses biosignal field-test is the in-situ measurement of how often the switch fires when you did not mean it. See Measure your own rate.

Further commands, for working with recordings, your profile and the budget guard:

  • yazses biosignal boards lists the boards the switches are known to work with and their BrainFlow ids, for the source setting. It needs no hardware.
  • yazses biosignal record --out session.csv --seconds 60 saves raw signal from your source to a file (.csv or .npz) for replay or evaluate. It writes only the file you name, readable only by you, and refuses to overwrite one without --force (see Privacy).
  • yazses biosignal evaluate scores the detectors against known events and reports precision, recall, latency and false alarms per hour. With no options it runs a scripted synthetic session, which shows the pipeline is wired correctly, not how it will do on your skin. For your own signal, record a session, label the events in a JSON file, and run yazses biosignal evaluate --file session.csv --truth events.json.
  • yazses biosignal profile says which board and channels your calibration profile was made for and how old it is; it never prints a threshold. yazses biosignal profile-clear deletes it so you can start again. Without a profile the muscle-squeeze switch stays off.

A first-time sequence:

yazses biosignal doctor
yazses biosignal calibrate
yazses biosignal monitor      # squeeze, blink and talk; watch what it reports
yazses biosignal field-test   # how often does it fire by mistake, by activity?
yazses restart

Measure your own rate

Whether a switch is livable depends on one number: how often it fires when you did not mean it. Nobody has published that number for the muscle, blink and jaw switches in daily use, and the detectors here have been tested only on generated signals. Your own measurement is currently the best evidence there is, and it is only evidence about you, your board and your electrode placement.

yazses biosignal field-test                      # the standard guided session, about ten minutes
yazses biosignal field-test --minutes 20         # stretch it: the bounds tighten with more signal
yazses biosignal field-test --json --out ft.json # machine-readable, and keep the counts

The session prompts you on screen to sit still, talk, chew, type and use the mouse, and look around, one activity at a time, with your real detectors running on your real signal. Then it asks you for each gesture on cue (squeeze, double blink and jaw clench, whether or not you have that switch turned on; a switch that is off cannot fire, so ignore the rows for the ones you are not using). Nothing is typed or acted on, and no signal is saved. Whatever a switch fires on while you were not trying to trigger it is a phantom activation; the report gives those per hour of usable signal for each activity, so you can see whether the trouble is the chewing, the talking or the desk. A single natural blink is not counted as a phantom (it is not mapped to an action). Time the quality gate rejected is left out of the denominator, so a bad contact cannot make the switch look quieter than it is, and a phase with too little usable signal is marked inconclusive.

Read the upper 95% column, not the estimate. A few minutes of data bound a rate only loosely: zero phantoms in N minutes still allows a rate of about 180 divided by N per hour (the "rule of three"), so a single short session cannot show that a switch is quiet, only that it is not obviously loud. The report ends with a verdict (usable, promising, needs_tuning, do_not_enable or inconclusive) and the reasons for it. If you left a switch off, its gesture row will show no hits and can pull the verdict down: read the rows of the switches you actually use. Those rungs and every number behind them are design choices that have not been validated on real recordings, and the weighted "workday estimate" uses an assumed mix of activities that is illustrative, not data. To tighten a result, repeat the test on different days and with the electrode re-seated, and compare the sessions.

If you choose to share your result (for example in an issue), share the JSON from --json or --out and add, in words, your board, where the electrode was, and the YazSes version; the file carries none of those. The file holds aggregate counts, whole-second durations, activity labels and cue-to-response times; it holds no signal, no amplitudes, no thresholds and no timestamps of events. It is written readable only by you and never over an existing file, and nothing sends it anywhere. Please do not attach a recording or a screenshot of the live signal.

The field test cannot see the one failure that matters most in daily use, because it drives nothing: it cannot tell you whether a stuck microphone is possible. The release watchdog and the stress cases in the validation protocol address that, and so does watching yazses biosignal report after some days of real use.

Calibration is per person

Calibration sets the thresholds for blink, jaw clench and squeeze strength from your own signal, not from a population average: your relaxed baseline, how big your blink is on your electrode, how hard you clench, and how a light squeeze differs from a hard one. Without a profile the muscle-squeeze switch stays off and doctor says so. Re-run calibrate whenever you move or replace an electrode or change board or channel; yazses biosignal profile tells you which board and channels the current profile was made for and how old it is (in words, such as "3 days ago").

YazSes also checks the profile for you while it runs. A profile made for a different board, a different set of channels or a different sampling rate is not used, and neither is one older than calibration_max_age_days (30 by default). The switches that depend on it are held off, a held squeeze is released, and doctor and the status say why in plain words. The muscle-squeeze switch has no built-in defaults, so it stays off until you calibrate again; the blink and jaw-clench switches fall back to conservative built-in values. Failing closed is deliberate: a calibration for a different electrode is a guess, and a guess here opens the microphone.

The speech guard

Speaking moves the muscles you clench with, so a jaw clench read while you talk is not a deliberate one. With speech_guard = true (the default) a jaw clench is suppressed while you are speaking: while a dictation turn is being recorded (including one a squeeze started) or a meeting is being captured, plus a short moment after it. The guard follows what YazSes is doing, not an analysis of the signal. The trade is deliberate: you may miss a clench made mid-sentence, but the guard is there so that talking is not mistaken for a clench. It applies to the jaw clench only, and it has no effect while jaw_clench is off. Turn it off only to measure the unguarded rate with yazses biosignal report.

Pausing it

The biosignal switch takes part in the global hands-free pause when the global stop is on ([handsfree_safety] enabled, which the hands-free preset sets; see Set up hands-free input). With it on:

  • yazses handsfree pause stops the switch along with the other hands-free inputs. A squeeze that is being held when you pause is released, so the microphone closes with the pause, and a squeeze is ignored while paused.
  • It starts paused after each start of YazSes, once it has received its first samples, and yazses handsfree resume arms it. A pause is never undone by itself.
  • If the samples stop arriving (a headset out of range, a cable out), the switch stops acting. A switch that only holds the microphone (no event labels in intent_map) re-arms itself shortly after samples return; after a pause or a fault, only yazses handsfree resume does.

Without [handsfree_safety], the global pause is not in play and the switch is limited only by its own budget guard.

Tune the false-positive budget

A switch that fires when you did not mean it is worse than no switch, so YazSes counts its own mistakes.

  • What is counted. Every activation is recorded locally. One you cancel, or that is followed by nothing, counts as a cancelled activation. That is a proxy for a false positive: it also counts an activation you cancelled for some other reason, and it misses a false positive you did not notice. report shows the rate with a confidence interval, because with only a few activations the number alone means little. The counters are saved to disk every ledger_autosave_seconds while YazSes runs (60 by default) and again when it stops cleanly, so a crash or power loss costs you at most that interval of the record.
  • max_cancelled_per_hour is your budget (6.0 by default, a starting point and not a measured recommendation; 0 turns the guard off, so the rate is still counted but nothing is ever suspended).
  • What happens when you go over it. The guard acts in two steps. First it desensitizes the detectors, so a stray movement has to be stronger to count. If the cancelled rate stays above the budget, it then suspends them and tells you, rather than keep misfiring. A suspension is not permanent: you can release it with one action, yazses handsfree resume. If you do nothing it lifts on its own after a pause (5 minutes to start, doubling on each repeat up to an hour), and the switch comes back at reduced sensitivity. It never re-enables itself at full sensitivity while the rate is still too high.
  • Choosing the budget. Raise it only if the cancelled activations really are acceptable to you; lower it if a stray activation is costly (for example when you dictate into a terminal).
  • If it fires too often: re-run calibrate (a re-seated electrode changes everything), check monitor for what is triggering it, make sure the electrode contact is good, and prefer a squeeze on a muscle you do not otherwise use while you are working.
  • If it misses you: the signal may be below the threshold, the budget guard may have desensitized the detectors, or the quality gate may be suppressing events because the contact is poor. monitor shows which, and doctor names the cause.

There is no recommended setting yet. The community has not measured these rates over real workdays, and your own field-test and report are currently the best evidence available. The guard is not a substitute for either: it can only react to activations you notice and cancel.

Three further settings shape what reaches the microphone. watchdog_seconds releases a held squeeze when no samples arrive for that long (a cable pulled, a Bluetooth stall), so a vanished device cannot leave the microphone open. min_event_confidence drops weak detections before they act (raising it trades missed presses for fewer false ones; releasing a held squeeze is never blocked by it). While the budget guard has desensitized the switch, it raises its own effective floor on top of this.

Honest limitations

  • Blink and jaw clench are artifacts, not brain signals. On an EEG headband you are reading eye and muscle activity that leaks into the headband. The research page records a best measured blink switch of 99.5% accuracy at 0.10 false positives per minute, which is about 50 stray activations in a workday without extra safeguards. That is why single blinks do nothing by default and a double blink is the gesture.
  • Talking confounds the jaw. Speaking moves the same muscles as clenching. The speech guard suppresses a jaw clench while you dictate, which means it may miss one, but it is there so that talking is not mistaken for one. It cannot tell a clench from chewing or yawning, so check your own rate with report before you rely on a jaw clench.
  • It is personal. Your skin, electrode placement and muscle differ from everyone else's. A profile made for one placement is wrong for another. Re-calibrate when you move or replace an electrode.
  • A bad signal means no events, on purpose. A flat line, a clipped signal, strong mains hum, a drifting baseline or dropped samples all make YazSes ignore the channel and say why. That is the quality gate working, not a bug.
  • Unmeasured. The detectors have been tested on synthetic and replayed signals. They have not been shown to work for any particular person, electrode or board, and their defaults are provisional. No real-hardware result has been collected yet; field-test is how that changes.
  • A squeeze is the recommended trigger. A muscle electrode on a muscle you choose gives the cleanest signal. Blink and jaw clench are artifacts, and the jaw clench is off by default for that reason.
  • Not thought control. Decoding motor imagery, SSVEP or P300 is not in this feature, and EEG headbands here are only read for the eye and muscle activity on them.

Privacy

Everything stays on your machine. The electrodes are read locally, and no network connection is made. Your calibration profile and the activation log are local files; the profile is readable only by you. A recording of your signal is biometric-adjacent data: it is written only when you ask for one, and you should not paste one into a public issue. For a bug report, share the output of yazses biosignal doctor instead, which contains no signal values.

Troubleshooting

Symptom What to check
doctor says the library is missing Install the emg-band (BrainFlow) or emg (serial) extra with the command it prints for your install method, then yazses restart.
doctor cannot open the source The device is unplugged, in use by another program, or the serial port or board id is wrong. On Linux your user may need to be in the dialout group for a serial port. A missing device is a logged no-op, not a crash.
Nothing ever fires, monitor says the channel is untrusted Flat line (no contact or a dead lead), clipping (gain too high), or strong mains noise. Re-seat the electrode, keep leads away from power cables, and check mains_hz.
Mains interference reported although the electrode is fine mains_hz is the wrong frequency for where you are (50 or 60).
It fires when you chew or yawn Expected to a degree on a jaw clench. Re-run calibrate, or use a squeeze on another muscle. The speech guard covers dictation, not chewing.
It fires constantly, then gets harder to trigger, then stops The budget guard first desensitized the detectors and then suspended them. Check report, fix the cause (usually contact or placement), re-run calibrate, then run yazses handsfree resume.
It stopped responding after yazses handsfree pause, or does not act after a restart The global pause covers it when [handsfree_safety] is on, and it starts paused. Run yazses handsfree resume.
doctor or the status says the calibration "is not used" It was made for a different board, channels or rate, or it is older than calibration_max_age_days. Run yazses biosignal calibrate. The squeeze stays off until then.
It worked yesterday, not today The electrode moved. Re-run calibrate.
Squeeze opens the mic but it will not close The signal never fell below the release threshold; a watchdog ends such a hold on its own. Re-run calibrate and check contact.
Nothing is typed after a squeeze That is the normal dictation path now; see silent audio and run yazses doctor.

See also