File size: 39,120 Bytes
61ab9d3
 
 
 
 
 
 
 
 
 
 
ea5221d
61ab9d3
 
 
ea5221d
61ab9d3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
---
license: mit
language:
  - fa
  - en
tags:
  - embedded
  - esp32
  - iot
  - constrained-decoding
  - intent-parsing
  - text-generation
  - persian
  - edge-ai
  - tinyml
pipeline_tag: text-generation
inference: false
---

![llmOS — natural language, real world action. Offline AI firmware for the ESP32-S3.](banner-en.png)

# llmOS — intent compiler

**A 2.85M-parameter language model that runs on an ESP32-S3 microcontroller and
turns a spoken sentence into a hardware automation rule.**

Say *"if the sensor on pin 1 goes high, turn off pin 2"* and the board does that
from then on. No code, no recompiling, no internet, no cloud account. The model
runs on the chip itself, from a microSD card.

| | |
|---|---|
| **Firmware + installer** | [github.com/AliAkrami1375/llmos-firmware](https://github.com/AliAkrami1375/llmos-firmware) |
| **Try it in the browser** | [the Space](https://huggingface.co/spaces/DibaAi/llmOS) |
| **Languages** | Persian (primary) and English |
| **Hardware** | ESP32-S3 with 8 MB PSRAM |

[**فارسی — پایین صفحه**](#فارسی)

---

## What it does

| You say | What it produces |
|---|---|
| if pin 1 goes high, turn off pin 2 | `ON GPIO1=1 DO GPIO2=0` |
| toggle pin 5 every 30 seconds | `ON EVERY 30s DO GPIO5=T` |
| when the light sensor on pin 4 drops below 800, turn on pin 7 | `ON ADC4<800 DO GPIO7=1` |
| at 07:00 turn on pin 9 for 30 seconds | `ON AT 07:00 DO GPIO9=1; WAIT 30s; GPIO9=0` |
| turn on pin 16 now | `DO GPIO16=1` |
| connect to wifi MyHome with password 1a2b3c | `CALL wifi.connect "MyHome" "1a2b3c"` |
| read pin 5 | `CALL pin.read "5"` |
| hello, how are you? | `ERR` — correctly refused |

The output is not free text. It is a small formal language a deterministic rule
engine executes; the model's only job is the translation.

### Persian is the primary language

The training distribution is mostly Persian, and the gap is not cosmetic. Two
capabilities are reliable in Persian and are not reached in English:

| Persian | Result |
|---|---|
| پایه ۷ رو بذار اتاق علی | `NAME GPIO7 "اتاق علی"` |
| اتاق علی رو روشن کن | `DO GPIO7=1` |
| ساعت ۳ بامداد بازر پایه ۹ را ۳۰ ثانیه بزن | `ON AT 03:00 DO GPIO9=1; WAIT 30s; GPIO9=0` |

Pin **naming** is Persian-only in practice, and so are clock times written as
words. In English, give the time in figures — `at 07:00`, not *"at 7am"*, which
is read unreliably. Everything in the first table works in both languages.

---

## Why invalid output is impossible, not merely unlikely

This is the part worth reading even if you never touch an ESP32.

A 2.85M-parameter model left to itself produces nonsense some of the time. This
one cannot, because **the grammar is enforced during decoding rather than
learned**.

The rule language is compiled into a prefix automaton. At every generation step
each candidate token is tested: *would accepting this token leave the output a
valid prefix of some complete rule?* Tokens that fail are removed before the
sample. The end-of-output token only becomes reachable once the text is a
complete, valid rule.

The same test enforces **numeric range**, not just syntax. `GPIO99` is
unreachable because 99 is not an allowed pin; `ADC4<9999` is unreachable
because the ADC is 12-bit. Pins that carry flash, PSRAM or the SD card are
excluded from the automaton entirely, so no sentence — however phrased — can
produce a rule that unmounts the card the model is running from.

### Quoted values are copied, not spelled

A model this small cannot spell a random WiFi password, and it never has to.

While the decoder is inside a quoted argument it stops asking *"is this valid
syntax?"* and starts asking **"does this keep the argument a verbatim substring
of what the user typed?"** Only tokens preserving that property survive. The
model never spells anything — it decides **where to start copying and where to
stop**, which is what attention is naturally good at.

A password full of strange characters therefore comes out byte-exact by
construction rather than by luck. Range checking deliberately does *not* look
inside quotes: a network named `GPIO99 ADC9999` is a legal SSID and is never
read as a pin.

---

## Accuracy

Measured on 5,000 held-out requests, through the same C engine that runs on the
board, against these exact quantized weights:

```
exact match      :  99.92%     4 wrong out of 5,000
syntactically ok : 100.00%
parses + in range: 100.00%
mean confidence  :   1.000
```

Broken down by intent class, because one number hides which half of the
behaviour is weak:

| Intent | n | Accuracy |
|---|---|---|
| digital rule | 1,496 | 99.9% |
| immediate action | 530 | 100% |
| analog threshold | 828 | 100% |
| periodic | 611 | 100% |
| scheduled (`at 3am`) | 580 | 99.8% |
| pin naming | 527 | 99.8% |
| tool call | 317 | 99.7% |
| out of domain | 111 | 100% |

The last row matters as much as the first: the model recognises when a request
is *not* an automation rule and says so, instead of forcing it into one.

The two 100% rows above are **by construction, not by training** — see the
previous section.

---

## Architecture

| | |
|---|---|
| Type | decoder-only transformer, Llama-style |
| Parameters | 2,853,312 |
| Dimension / layers / heads | 192 / 6 / 4 (4 KV heads) |
| Feed-forward | SwiGLU, hidden 512 |
| Positional | RoPE |
| Normalisation | RMSNorm |
| Context | 128 tokens |
| Vocabulary | 1,024 |
| Quantization | int8 symmetric, group size 32 |
| On disk | 3,142 KB |
| Working memory on device | 1,166 KB (KV cache + buffers) |

### Tokenizer

Byte-level BPE, 1,024 tokens, with **atomic digits** — no digit-digit merge
exists in the vocabulary.

That last detail was not cosmetic. An earlier tokenizer split `500` as
`['500']`, `800` as `['8','00']` and `4095` as `['40','9','5']`. The model had
to learn a different spelling strategy per number, and scheduled rules sat at
89.2% accuracy. Making digits atomic moved that class to 99.8% with no
architecture change.

Tokenization is greedy longest-match, implemented identically in C and Python
and verified byte-for-byte over 740,000 strings — because a tokenizer that
disagrees with itself across the training/inference boundary produces a model
that looks trained and behaves broken.

---

## Files

| File | What it is |
|---|---|
| `model.llm` | the quantized weights, custom binary format |
| `tokenizer.bin` | the vocabulary — **must match the model** |
| `tools.json` | the shipped HTTP tool definitions, as reference |
| `LICENSE` | MIT |

> `model.llm` and `tokenizer.bin` are a **matched pair**. Replacing one without
> the other gives confident nonsense rather than an error. Always take both
> from the same revision.

### This is not a `transformers` checkpoint

`model.llm` is a flat int8 format read by a C99 inference engine with no
dependencies, designed to be `mmap`-friendly on a microcontroller. There is no
`config.json`, no `safetensors`, and `AutoModel.from_pretrained` will not work.

To run it you either flash the firmware, or use the Space, which runs that same
C engine on a CPU.

---

## How to run it

### On hardware

```bash
git clone https://github.com/AliAkrami1375/llmos-firmware.git
cd llmos-firmware
./install.sh                 # Linux, macOS
```

```powershell
powershell -ExecutionPolicy Bypass -File install.ps1     # Windows
```

The installer finds the board, flashes it and copies the model to a FAT32
microSD card. You need an ESP32-S3 with **8 MB PSRAM** (N8R8 or N16R8) and a
USB **data** cable.

### In a browser

[**huggingface.co/spaces/DibaAi/llmOS**](https://huggingface.co/spaces/DibaAi/llmOS)
— the same C engine, the same weights, on a CPU.

---

## The rule language

```
rule    := "ON " trigger " DO " actions
         | "DO " actions
         | "CALL " tool arg*
         | "NAME GPIO" pin " " label
         | "ERR"

trigger := "GPIO" pin "=" ("0"|"1")
         | "ADC" pin ("<"|">") value
         | "EVERY " n unit
         | "AT " hh ":" mm

action  := "GPIO" pin "=" ("0"|"1"|"T")     T = toggle
         | "WAIT " n unit
         | "CALL " tool arg*

unit    := "ms" | "s" | "m" | "h"
```

Allowed pins: **1–9, 14–18, 21, 38–44, 47, 48**. ADC: **1–9**, threshold
0–4095. Durations up to 24 hours. Everything else is unreachable in the
automaton, not merely rejected afterwards.

---

## Tool calls and notifications

Rules control pins. A **tool** is something the device does that is not a pin
rule — joining a WiFi network, reading an API, and most usefully **telling you
when something happened**.

`CALL` is an action like any other, so a rule may contain one. These are valid,
and the parser and engine both have tests for them:

```
ON GPIO7=1 DO CALL notify.send "door opened"
ON ADC3>2400 DO GPIO5=1; CALL notify.send "too hot"
ON AT 07:00 DO CALL notify.send "good morning"
```

> **The model will not write those lines for you.** It only ever emits the five
> tool names it was trained on — `wifi.connect`, `wifi.scan`, `wifi.status`,
> `sys.info`, `pin.read`. `notify.send` is a *user* tool, defined in a file on
> the SD card long after training, so no sentence produces it. Ask *"if pin 7
> goes high, notify me"* and you get a plain pin rule or `ERR`.
>
> To get a rule like the ones above onto the device, write the line into
> `rules.txt` on the card; it is parsed and stored at boot.

### `notify.send`

Every other tool answers a question you asked. `notify.send` is the one that
speaks **without being asked** — the one that tells you the door opened at 3 a.m.
while you were asleep.

It posts to a webhook on **your own network**, so it needs no internet, no TLS,
no API key and no third party that can be blocked or shut down:

```json
{
  "name": "notify.send",
  "kind": "http",
  "parameters": [{ "name": "text", "required": true }],
  "phrases": ["پیام بده", "خبرم کن", "اطلاع بده", "notify me"],
  "execute": {
    "method": "POST",
    "url": "http://192.168.1.50:8123/api/webhook/llmos",
    "body": "{\"message\":\"{text}\"}",
    "timeout_ms": 5000
  },
  "reply": { "fa": "پیام فرستاده شد.", "on_error": "پیام فرستاده نشد." }
}
```

Point the URL at Home Assistant, Node-RED, or a five-line Python receiver.
Until you do, it correctly reports that the message was not sent — nothing is
listening at the placeholder address.

There are exactly two ways to reach it:

| What you want | How |
|---|---|
| A message **now** | say *"خبرم کن …"* / *"notify me …"* — the phrase router matches it, and whatever follows the phrase becomes the message |
| A message **when something happens** | write `ON GPIO7=1 DO CALL notify.send "door opened"` into `rules.txt` on the card |

**A rule containing a `CALL` fires on the edge.** One message on the low→high
transition, not one every 20 ms tick while the pin stays high; a 30 ms debounce
means a bouncing contact still counts as one event. That is the difference
between a useful notification and three thousand messages.

**A failed call does not stop the rule.** The remaining actions still run — a
dead webhook must not stop the light from switching on.

### The two routes into a tool

```
your sentence

     ├─► phrase router ──── matched a tool's trigger phrase? ──► run it now
     │   (typo-tolerant)              │ no
     │                                ▼
     └───────────────────────────► the model ──► ON … DO … │ CALL … │ ERR

rules.txt on the card ────────────► parsed at boot ──► stored as a rule
```

The router normalises Persian text (ی/ي, ک/ك, zero-width non-joiners, Persian,
Arabic and Latin digits, diacritics) and tolerates typos. It runs the tool
immediately rather than creating a rule.

> **Adding a tool to `tools.json` teaches the model nothing.** The model was
> trained on a fixed set of tool names; a tool added afterwards is reached by
> its own trigger phrases, not by the model writing `CALL yourtool`. This is
> not a bug — it is what a closed vocabulary means. It is also exactly why
> every tool carries its own phrases, and why the third route above exists.

Seven HTTP tools ship in `tools.json` — notifications, weather, air quality,
sunrise/sunset, time, earthquakes, connectivity. Every URL was actually called
and every extraction path checked against the real response.

Five more are **compiled into the firmware and cannot be deleted**`wifi.scan`, `wifi.connect`, `wifi.status`, `sys.info`, `pin.read`. Not because
of a flag in the file, but because they are not in the file at all; they are
rebuilt from a table at every boot. Pull the SD card out and `wifi.connect` is
still there.

**→ [Full tool and notification documentation](https://github.com/AliAkrami1375/llmos-firmware/blob/main/docs/TOOLS.md)**
([Persian](https://github.com/AliAkrami1375/llmos-firmware/blob/main/docs/TOOLS.fa.md))

---

## Pin names

You can call pin 7 *"Ali's room"* and then say *"turn on Ali's room"*.

**The model never learns your names.** They are created long after training.
A name is substituted with its pin number *before* the sentence reaches the
model and substituted back *afterwards*, entirely outside it. That is why a
name you invented one minute ago works immediately, and why 32 names cost the
model nothing.

---

## Training

Trained from scratch on a synthetic Persian/English corpus of automation
requests paired with their target rules, on a single T4. No pretrained
checkpoint; the vocabulary and the task are both narrow enough that
pretraining on general text would have spent capacity on nothing useful.

The loss is applied only to the rule span, not to the input sentence, so all
capacity goes to the translation rather than to modelling how people write.

### What the numbers actually taught us

Overall accuracy said almost nothing useful. **Breaking it down by intent class
exposed every real bug.** Five separate apparent "model weaknesses" turned out
to be data or tooling defects:

- 10% of quoted values were corrupted by the typo-noise augmentation, teaching
  the model to copy text that was no longer in the input;
- 4.1% of the test set asked for durations above the 24-hour cap, which the
  grammar makes unreachable — the model was being scored on impossible targets;
- the decoder's own range check quietly made ADC thresholds 410–999 unreachable,
  so it emitted `55` when asked for `550`. This one accounted for 123 of the
  last 127 failures and had misled three training runs before it was found;
- the output token limit was below what 15% of targets needed, showing up as a
  12-point accuracy drop while the training loss reported 0.0001;
- the tokenizer split numbers inconsistently, as described above.

The dataset generator now refuses to emit an example it can prove is
impossible, and the test suite sweeps **every** legal value — all 4,096 ADC
thresholds and every duration — to assert each one is reachable.

---

## Limits, stated plainly

- **Narrow by design.** Pins, sensors, timers and the listed tools. *"Make the
  house warm"* is refused, and that is intended behaviour, not a gap.
- **No compound conditions.** `if pin 1 AND pin 2` is not supported yet.
- **128-token context.** Long, rambling sentences are truncated.
- **Scheduling needs the network once.** There is no RTC, so nothing
  time-based fires until the clock has synchronised over SNTP.
- **24 rules and 32 pin names** at a time on the device.
- **Persian first.** English works, but the training distribution is mostly
  Persian and English coverage is correspondingly thinner.
- **No authentication** on the device's HTTP API. Anyone on the network can
  control it.

## What is not in this repository

The training code, the dataset generator, the model architecture and the
firmware sources are not published here. This repository is the **release**:
weights, tokenizer and documentation. Ready-to-flash binaries and the installer
are at
[github.com/AliAkrami1375/llmos-firmware](https://github.com/AliAkrami1375/llmos-firmware).

## License

MIT.

<br>

---

<br>

<a name="فارسی"></a>

![llmOS — فرم‌ور هوشمند آفلاین برای ESP32-S3](banner-fa.png)

<div dir="rtl">

# ‏llmOS — کامپایلر نیت

**یک مدل زبانی ۲٫۸۵ میلیون پارامتری که روی میکروکنترلر ESP32-S3 اجرا می‌شود و یک جمله‌ی معمولی را به قانون اتوماسیون سخت‌افزاری تبدیل می‌کند.**

می‌گویید «اگر سنسور پایه ۱ روشن شد پایه ۲ را خاموش کن» و برد از همان لحظه این کار را می‌کند. بدون کد، بدون کامپایل مجدد، بدون اینترنت و بدون هیچ سرویس ابری. مدل روی خود چیپ اجرا می‌شود، از روی کارت microSD.

| | |
|---|---|
| **فرم‌ور و نصب‌کننده** | [github.com/AliAkrami1375/llmos-firmware](https://github.com/AliAkrami1375/llmos-firmware) |
| **تست در مرورگر** | [اسپیس](https://huggingface.co/spaces/DibaAi/llmOS) |
| **زبان** | فارسی (اصلی) و انگلیسی |
| **سخت‌افزار** | ESP32-S3 با ۸ مگابایت PSRAM |

---

## چه کار می‌کند

| می‌گویید | تولید می‌کند |
|---|---|
| اگر سنسور پایه ۱ روشن شد پایه ۲ را خاموش کن | `ON GPIO1=1 DO GPIO2=0` |
| هر ۳۰ ثانیه پایه ۵ را چشمک بزن | `ON EVERY 30s DO GPIO5=T` |
| وقتی سنسور نور پایه ۴ کمتر از ۸۰۰ شد چراغ پایه ۷ را روشن کن | `ON ADC4<800 DO GPIO7=1` |
| ساعت ۳ صبح بازر پایه ۹ را ۳۰ ثانیه بزن | `ON AT 03:00 DO GPIO9=1; WAIT 30s; GPIO9=0` |
| پایه ۷ را بگذار «اتاق علی» | `NAME GPIO7 "اتاق علی"` |
| اتاق علی را روشن کن | `DO GPIO7=1` |
| به وای‌فای MyHome با رمز 1a2b3c وصل شو | `CALL wifi.connect "MyHome" "1a2b3c"` |
| شبکه‌های وای‌فای رو اسکن کن | `CALL wifi.scan` |
| سلام خوبی؟ | `ERR` — درست رد شد |

خروجی متن آزاد نیست. یک زبان صوری کوچک است که یک موتور قوانین قطعی اجرایش می‌کند؛ تنها کار مدل ترجمه است.

---

## چرا خروجی نامعتبر ناممکن است، نه فقط بعید

این بخش حتی اگر هیچ‌وقت به ESP32 دست نزنید ارزش خواندن دارد.

مدل ۲٫۸۵ میلیون پارامتری اگر رها شود گاهی چرت می‌گوید. این یکی نمی‌تواند، چون **گرامر هنگام دیکود کردن اعمال می‌شود نه اینکه یاد گرفته شده باشد**.

زبان قانون به یک اتوماتای پیشوندی کامپایل می‌شود. در هر گام تولید، هر توکن کاندید تست می‌شود: *اگر این توکن را بپذیریم، خروجی هنوز پیشوند معتبری از یک قانون کامل می‌ماند؟* توکن‌هایی که رد شوند قبل از نمونه‌برداری حذف می‌شوند. توکن پایان تنها وقتی قابل دسترس می‌شود که متن یک قانون کامل و معتبر باشد.

همین تست **بازه‌ی عددی** را هم اعمال می‌کند، نه فقط نحو را. `GPIO99` غیرقابل‌دسترس است چون ۹۹ پایه‌ی مجاز نیست؛ `ADC4<9999` غیرقابل‌دسترس است چون ADC دوازده‌بیتی است. پایه‌هایی که فلش، PSRAM یا کارت حافظه رویشان است کلاً از اتوماتا بیرون‌اند، پس هیچ جمله‌ای — با هر عبارتی — نمی‌تواند قانونی بسازد که کارتی را که مدل از رویش اجرا می‌شود unmount کند.

### مقدار نقل‌قولی کپی می‌شود، هجی نمی‌شود

مدلی به این کوچکی نمی‌تواند رمز تصادفی وای‌فای را هجی کند، و هیچ‌وقت لازم نیست بکند.

وقتی دیکودر داخل یک آرگومان نقل‌قولی است، از پرسیدن «آیا این نحو معتبری است؟» به پرسیدن **«آیا این آرگومان را زیررشته‌ای از متن کاربر نگه می‌دارد؟»** سوئیچ می‌کند. فقط توکن‌هایی که این شرط را حفظ کنند زنده می‌مانند. مدل هیچ‌وقت چیزی را هجی نمی‌کند — فقط تصمیم می‌گیرد **از کجا شروع به کپی کند و کجا بایستد**، و این دقیقاً کاری است که مکانیزم توجه به‌طور طبیعی خوب انجام می‌دهد.

رمزی پر از کاراکتر عجیب بنا به ساختار بایت‌به‌بایت درست درمی‌آید، نه به شانس. بررسی بازه‌ها عمداً داخل نقل‌قول نگاه نمی‌کند: شبکه‌ای به نام `GPIO99 ADC9999` یک SSID مجاز است و هیچ‌وقت به‌عنوان پایه خوانده نمی‌شود.

---

## دقت

روی ۵۰۰۰ درخواست کنارگذاشته‌شده، از مسیر همان موتور C که روی برد اجرا می‌شود و با همین وزن‌های کوانتیزه:

</div>

```
exact match      :  99.92%     4 wrong out of 5,000
syntactically ok : 100.00%
parses + in range: 100.00%
mean confidence  :   1.000
```

<div dir="rtl">

و به تفکیک نوع نیت، چون یک عدد کلی پنهان می‌کند کدام نیمه‌ی رفتار ضعیف است:

| نیت | تعداد | دقت |
|---|---|---|
| قانون دیجیتال | ۱۴۹۶ | ۹۹٫۹٪ |
| اقدام فوری | ۵۳۰ | ۱۰۰٪ |
| آستانه‌ی آنالوگ | ۸۲۸ | ۱۰۰٪ |
| تناوبی | ۶۱۱ | ۱۰۰٪ |
| زمان‌بندی‌شده (`ساعت ۳`) | ۵۸۰ | ۹۹٫۸٪ |
| نام‌گذاری پایه | ۵۲۷ | ۹۹٫۸٪ |
| فراخوانی ابزار | ۳۱۷ | ۹۹٫۷٪ |
| خارج از دامنه | ۱۱۱ | ۱۰۰٪ |

سطر آخر به اندازه‌ی سطر اول مهم است: مدل تشخیص می‌دهد که یک درخواست **قانون اتوماسیون نیست** و همین را می‌گوید، به‌جای اینکه به زور در قالبی بچپاندش.

دو سطر ۱۰۰٪ بالا **بنا به ساختار است، نه بنا به آموزش** — بخش قبل را ببینید.

---

## معماری

| | |
|---|---|
| نوع | ترنسفورمر فقط-دیکودر، سبک Llama |
| پارامتر | ۲٬۸۵۳٬۳۱۲ |
| بعد / لایه / هد | ۱۹۲ / ۶ / ۴ (۴ هد KV) |
| فیدفوروارد | SwiGLU، پنهان ۵۱۲ |
| موقعیتی | RoPE |
| نرمال‌سازی | RMSNorm |
| کانتکست | ۱۲۸ توکن |
| واژگان | ۱۰۲۴ |
| کوانتیزاسیون | int8 متقارن، گروه ۳۲ |
| روی دیسک | ۳۱۴۲ کیلوبایت |
| حافظه‌ی کاری روی دستگاه | ۱۱۶۶ کیلوبایت (KV cache و بافرها) |

### توکنایزر

‏BPE سطح-بایت با ۱۰۲۴ توکن و **ارقام اتمی** — هیچ ادغام رقم-رقمی در واژگان وجود ندارد.

این جزئیات تزئینی نبود. توکنایزر قبلی `500` را `['500']`، `800` را `['8','00']` و `4095` را `['40','9','5']` می‌شکست. مدل مجبور بود برای هر عدد راهبرد هجی متفاوتی یاد بگیرد و قوانین زمان‌بندی‌شده روی ۸۹٫۲٪ گیر کرده بودند. اتمی کردن ارقام همان کلاس را بدون هیچ تغییری در معماری به ۹۹٫۸٪ رساند.

توکنایزیشن حریصانه‌ی طولانی‌ترین-تطبیق است، در C و پایتون یکسان پیاده شده و روی ۷۴۰٬۰۰۰ رشته بایت‌به‌بایت بررسی شده — چون توکنایزری که در مرز آموزش/استنتاج با خودش اختلاف داشته باشد مدلی می‌سازد که آموزش‌دیده به نظر می‌رسد و خراب رفتار می‌کند.

---

## فایل‌ها

| فایل | چیست |
|---|---|
| `model.llm` | وزن‌های کوانتیزه، فرمت باینری اختصاصی |
| `tokenizer.bin` | واژگان — **باید با مدل جفت باشد** |
| `tools.json` | تعریف ابزارهای HTTP همراه، به‌عنوان مرجع |
| `LICENSE` | MIT |

> `model.llm` و `tokenizer.bin` یک **جفت هماهنگ‌اند**. عوض کردن یکی بدون دیگری به‌جای خطا، جواب اشتباه با اطمینان کامل می‌دهد. همیشه هر دو را از یک نسخه بردارید.

### این یک چک‌پوینت `transformers` نیست

`model.llm` یک فرمت تخت int8 است که یک موتور استنتاج C99 بدون هیچ وابستگی می‌خواندش، و طوری طراحی شده که روی میکروکنترلر `mmap`-پذیر باشد. نه `config.json` دارد، نه `safetensors`، و `AutoModel.from_pretrained` کار نمی‌کند.

برای اجرا یا فرم‌ور را فلش کنید، یا از اسپیس استفاده کنید که همان موتور C را روی CPU اجرا می‌کند.

---

## چطور اجرایش کنیم

### روی سخت‌افزار

</div>

```bash
git clone https://github.com/AliAkrami1375/llmos-firmware.git
cd llmos-firmware
./install.sh                 # لینوکس، مک
```

```powershell
powershell -ExecutionPolicy Bypass -File install.ps1     # ویندوز
```

<div dir="rtl">

نصب‌کننده برد را پیدا می‌کند، فلش می‌کند و مدل را روی کارت microSD با فرمت FAT32 کپی می‌کند. به یک ESP32-S3 با **۸ مگابایت PSRAM** (مدل N8R8 یا N16R8) و یک کابل USB **دیتا** نیاز دارید.

### در مرورگر

[**huggingface.co/spaces/DibaAi/llmOS**](https://huggingface.co/spaces/DibaAi/llmOS) — همان موتور C، همان وزن‌ها، روی CPU.

---

## زبان قانون‌ها

</div>

```
rule    := "ON " trigger " DO " actions
         | "DO " actions
         | "CALL " tool arg*
         | "NAME GPIO" pin " " label
         | "ERR"

trigger := "GPIO" pin "=" ("0"|"1")
         | "ADC" pin ("<"|">") value
         | "EVERY " n unit
         | "AT " hh ":" mm

action  := "GPIO" pin "=" ("0"|"1"|"T")     T = toggle
         | "WAIT " n unit
         | "CALL " tool arg*

unit    := "ms" | "s" | "m" | "h"
```

<div dir="rtl">

پایه‌های مجاز: **۱ تا ۹، ۱۴ تا ۱۸، ۲۱، ۳۸ تا ۴۴، ۴۷، ۴۸**. ‏ADC: **۱ تا ۹**، آستانه ۰ تا ۴۰۹۵. مدت‌ها تا ۲۴ ساعت. هر چیز دیگری در اتوماتا **غیرقابل‌دسترس** است، نه اینکه بعداً رد شود.

---

## فراخوانی ابزار و اطلاع‌رسانی

قانون‌ها پایه‌ها را کنترل می‌کنند. **ابزار** کاری است که دستگاه انجام می‌دهد ولی قانون GPIO نیست — وصل شدن به وای‌فای، خواندن یک API، و از همه کاربردی‌تر **خبر دادن وقتی اتفاقی افتاد**.

`CALL` هم یک اکشن است مثل بقیه، پس قانون می‌تواند شاملش شود. این‌ها معتبرند و پارسر و موتور هر دو برایشان تست دارند:

</div>

```
ON GPIO7=1 DO CALL notify.send "درب باز شد"
ON ADC3>2400 DO GPIO5=1; CALL notify.send "خیلی گرم شد"
ON AT 07:00 DO CALL notify.send "صبح بخیر"
```

<div dir="rtl">

> **ولی مدل این خط‌ها را برایتان نمی‌نویسد.** مدل فقط پنج نام ابزاری را تولید می‌کند که رویشان آموزش دیده — `wifi.connect`، `wifi.scan`، `wifi.status`، `sys.info`، `pin.read`. ‏`notify.send` یک ابزار *کاربر* است که خیلی بعد از آموزش در فایلی روی کارت حافظه تعریف می‌شود، پس هیچ جمله‌ای تولیدش نمی‌کند. اگر بگویید «اگر پایه ۷ روشن شد بهم خبر بده»، یک قانون پایه‌ی ساده یا `ERR` می‌گیرید.
>
> برای رساندن چنین قانونی به دستگاه، خط را در `rules.txt` روی کارت بنویسید؛ هنگام بوت پارس و ذخیره می‌شود.

</div>

<div dir="rtl">

### ‏`notify.send`

بقیه‌ی ابزارها به سؤالی که پرسیده‌اید جواب می‌دهند. `notify.send` تنها ابزاری است که **بدون پرسیده شدن حرف می‌زند** — همان که می‌گوید ساعت ۳ بامداد که خواب بودید درب باز شد.

روی یک وب‌هوک در **شبکه‌ی خودتان** POST می‌کند، پس نه اینترنت می‌خواهد، نه TLS، نه کلید API و نه هیچ طرف سومی که بشود فیلترش کرد یا تعطیل شود:

</div>

```json
{
  "name": "notify.send",
  "kind": "http",
  "parameters": [{ "name": "text", "required": true }],
  "phrases": ["پیام بده", "خبرم کن", "اطلاع بده", "notify me"],
  "execute": {
    "method": "POST",
    "url": "http://192.168.1.50:8123/api/webhook/llmos",
    "body": "{\"message\":\"{text}\"}",
    "timeout_ms": 5000
  },
  "reply": { "fa": "پیام فرستاده شد.", "on_error": "پیام فرستاده نشد." }
}
```

<div dir="rtl">

آدرس را به Home Assistant، Node-RED یا یک گیرنده‌ی پنج‌خطی پایتون وصل کنید. تا وقتی این کار را نکرده‌اید درست می‌گوید که پیام فرستاده نشد — چیزی روی آدرس نمونه گوش نمی‌دهد.

دقیقاً دو راه برای رسیدن به آن هست:

| چه می‌خواهید | چطور |
|---|---|
| **همین حالا** پیام | بگویید «خبرم کن …» — مسیریاب عبارت تطبیقش می‌دهد و هر چه بعد از عبارت بیاید متن پیام می‌شود |
| **وقتی اتفاقی افتاد** پیام | خط `ON GPIO7=1 DO CALL notify.send "درب باز شد"` را در `rules.txt` روی کارت بنویسید |

**قانونی که `CALL` داشته باشد روی لبه شلیک می‌کند.** یک پیام روی گذار صفر→یک، نه یک پیام در هر تیک ۲۰ میلی‌ثانیه‌ای تا وقتی پایه روشن مانده؛ و ۳۰ میلی‌ثانیه debounce باعث می‌شود کنتاکت لرزان هم یک رویداد شمرده شود. تفاوت یک اطلاع‌رسانی مفید با سه هزار پیام همین است.

**شکست یک فراخوانی، قانون را متوقف نمی‌کند.** اکشن‌های بعدی همچنان اجرا می‌شوند — یک وب‌هوک مرده نباید جلوی روشن شدن چراغ را بگیرد.

### دو مسیری که به ابزار می‌رسند

</div>

```
جمله‌ی شما

     ├─► مسیریاب عبارت ──── با عبارت تریگر ابزاری خواند؟ ──► همین حالا اجرا
     │   (با تحمل غلط املایی)          │ نه
     │                                 ▼
     └───────────────────────────► مدل ──► ON … DO … │ CALL … │ ERR

rules.txt روی کارت ───────────────► پارس در بوت ──► ذخیره به‌عنوان قانون
```

<div dir="rtl">

مسیریاب متن فارسی را نرمال می‌کند (ی/ي، ک/ك، نیم‌فاصله، ارقام فارسی و عربی و لاتین، اعراب) و غلط تایپی را تحمل می‌کند. ابزار را بلافاصله اجرا می‌کند، نه اینکه قانون بسازد.

> **اضافه کردن ابزار به `tools.json` به مدل چیزی یاد نمی‌دهد.** مدل روی مجموعه‌ی ثابتی از نام ابزارها آموزش دیده؛ ابزاری که بعداً اضافه شود از راه عبارت‌های تریگر خودش صدا زده می‌شود، نه با اینکه مدل `CALL yourtool` بنویسد. این باگ نیست — معنای «واژگان بسته» همین است. و دقیقاً به همین دلیل هر ابزار عبارت‌های خودش را حمل می‌کند.

هفت ابزار HTTP در `tools.json` آماده‌اند — اطلاع‌رسانی، آب و هوا، کیفیت هوا، طلوع و غروب، ساعت، زلزله، وضعیت اینترنت. هر آدرس واقعاً صدا زده شده و هر مسیر استخراج با پاسخ واقعی بررسی شده.

پنج ابزار دیگر **داخل فرم‌ور کامپایل شده‌اند و حذف نمی‌شوند**`wifi.scan`، `wifi.connect`، `wifi.status`، `sys.info`، `pin.read`. نه به خاطر فلگی در فایل، بلکه چون اصلاً داخل فایل نیستند؛ در هر بوت از یک جدول ساخته می‌شوند. کارت را در بیاورید، `wifi.connect` سر جایش است.

**← [مستندات کامل ابزار و اطلاع‌رسانی](https://github.com/AliAkrami1375/llmos-firmware/blob/main/docs/TOOLS.fa.md)** ([انگلیسی](https://github.com/AliAkrami1375/llmos-firmware/blob/main/docs/TOOLS.md))

---

## نام‌گذاری پایه‌ها

می‌توانید پایه ۷ را «اتاق علی» بنامید و بعد بگویید «اتاق علی را روشن کن».

**مدل هیچ‌وقت اسم‌های شما را یاد نمی‌گیرد.** این اسم‌ها خیلی بعد از آموزش ساخته می‌شوند. اسم **قبل** از رسیدن جمله به مدل با شماره‌ی پایه جایگزین می‌شود و **بعد** از آن برمی‌گردد، کاملاً بیرون از مدل. به همین دلیل اسمی که یک دقیقه پیش ساخته‌اید بلافاصله کار می‌کند، و ۳۲ اسم هیچ هزینه‌ای به مدل تحمیل نمی‌کند.

---

## آموزش

از صفر روی یک پیکره‌ی مصنوعی فارسی/انگلیسی از درخواست‌های اتوماسیون همراه با قانون هدفشان، روی یک T4 آموزش دیده. بدون چک‌پوینت از پیش آموزش‌دیده؛ واژگان و خود مسئله آن‌قدر باریک‌اند که پیش‌آموزش روی متن عمومی ظرفیت را صرف چیز بی‌فایده می‌کرد.

خطا فقط روی بازه‌ی قانون اعمال می‌شود نه روی جمله‌ی ورودی، پس تمام ظرفیت صرف ترجمه می‌شود نه مدل کردن اینکه آدم‌ها چطور می‌نویسند.

### چیزی که اعداد واقعاً یادمان دادند

دقت کلی تقریباً هیچ چیز مفیدی نمی‌گفت. **تفکیک آن به کلاس‌های نیت، هر باگ واقعی را بیرون کشید.** پنج «ضعف مدل» ظاهری در واقع نقص داده یا ابزار بودند:

- ۱۰٪ از مقادیر نقل‌قولی توسط نویز غلط تایپی خراب می‌شدند و به مدل یاد می‌دادند متنی را کپی کند که دیگر در ورودی نبود؛
- ۴٫۱٪ از مجموعه‌ی تست مدت‌هایی بالاتر از سقف ۲۴ ساعت می‌خواستند که گرامر غیرقابل‌دسترسشان می‌کند — مدل داشت روی هدف‌های ناممکن نمره می‌گرفت؛
- بررسی بازه‌ی خود دیکودر بی‌سروصدا آستانه‌های ADC از ۴۱۰ تا ۹۹۹ را غیرقابل‌دسترس کرده بود، پس وقتی ۵۵۰ خواسته می‌شد `55` تولید می‌کرد. همین یکی مسئول ۱۲۳ تا از ۱۲۷ شکست باقی‌مانده بود و سه اجرای آموزش را گمراه کرده بود؛
- سقف توکن خروجی کمتر از چیزی بود که ۱۵٪ از هدف‌ها لازم داشتند، و به شکل ۱۲ واحد افت دقت ظاهر می‌شد در حالی که خطای آموزش ۰٫۰۰۰۱ گزارش می‌کرد؛
- توکنایزر اعداد را ناهماهنگ می‌شکست، همان‌طور که بالا توضیح داده شد.

حالا مولد دیتاست از تولید نمونه‌ای که بتواند ناممکن بودنش را ثابت کند خودداری می‌کند، و مجموعه‌ی تست **همه‌ی** مقادیر مجاز را جارو می‌کند — هر ۴۰۹۶ آستانه‌ی ADC و هر مدت — تا مطمئن شود همه قابل دسترس‌اند.

---

## محدودیت‌ها، صادقانه

- **دامنه عمداً باریک است.** پایه‌ها، سنسورها، تایمرها و ابزارهای فهرست‌شده. «خونه رو گرم کن» رد می‌شود و این رفتار درست است نه کمبود.
- **بدون شرط ترکیبی.** `اگر پایه ۱ و پایه ۲` هنوز پشتیبانی نمی‌شود.
- **کانتکست ۱۲۸ توکن.** جمله‌های خیلی طولانی بریده می‌شوند.
- **زمان‌بندی یک بار به شبکه نیاز دارد.** ‏RTC روی برد نیست، پس تا وقتی ساعت با SNTP همگام نشده هیچ قانون زمانی شلیک نمی‌کند.
- **۲۴ قانون و ۳۲ نام پایه** در هر لحظه روی دستگاه.
- **اول فارسی.** انگلیسی کار می‌کند، ولی توزیع آموزش عمدتاً فارسی است و پوشش انگلیسی به همان نسبت نازک‌تر.
- **بدون احراز هویت** روی API دستگاه. هر کسی روی شبکه می‌تواند کنترلش کند.

## چه چیزی در این مخزن نیست

کد آموزش، مولد دیتاست، معماری مدل و سورس فرم‌ور اینجا منتشر نشده‌اند. این مخزن **ریلیز** است: وزن‌ها، توکنایزر و مستندات. باینری‌های آماده‌ی فلش و نصب‌کننده در [github.com/AliAkrami1375/llmos-firmware](https://github.com/AliAkrami1375/llmos-firmware) هستند.

## مجوز

‏MIT

</div>