diff --git a/.gitignore b/.gitignore index 524f096..aed3588 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,52 @@ -# Compiled class file +# Gradle cache + build outputs +.gradle/ +**/.gradle/ +**/build/ +local.properties + +# IDE +.idea/ +*.iml +*.ipr +*.iws +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Java *.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # *.jar -*.war -*.nar -*.ear +!modules/**/*.jar +!gradle/wrapper/gradle-wrapper.jar +!android/gradle/wrapper/gradle-wrapper.jar + +# Logs +*.log +hs_err_pid* + +# Package files *.zip *.tar.gz *.rar +*.deb -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* +# BlueJ +*.ctxt +.mtj.tmp/ + +# Virtual machine crash logs replay_pid* + +# Windows portable runtime (prebuilt binaries) +dist/windows-portable/runtime/ +dist/windows-portable/app/cbe-emu.jar + +# Compiled binaries +*.exe +*.dll +*.dex diff --git a/MANUAL.md b/MANUAL.md new file mode 100644 index 0000000..1b8d7c1 --- /dev/null +++ b/MANUAL.md @@ -0,0 +1,1042 @@ +# CBE Platform — Полное руководство по созданию плагинов и систем + +## Содержание + +1. [Введение](#1-введение) +2. [Архитектура CBE](#2-архитектура-cbe) +3. [Формат плагина (.cbeplugin)](#3-формат-плагина-cbeplugin) +4. [Создание модуля CPU](#4-создание-модуля-cpu) +5. [Создание модуля RAM](#5-создание-модуля-ram) +6. [Создание модуля GPU](#6-создание-модуля-gpu) +7. [Создание модуля KBD (клавиатура)](#7-создание-модуля-kbd) +8. [Создание модуля SND (звук)](#8-создание-модуля-snd) +9. [Создание модуля BIOS](#9-создание-модуля-bios) +10. [Создание модуля DISK](#10-создание-модуля-disk) +11. [Семантические операции инструкций](#11-семантические-операции) +12. [Написание программ на ассемблере](#12-написание-программ-на-ассемблере) +13. [Написание программ на C/C++](#13-написание-программ-на-cc) +14. [Написание программ на Python](#14-написание-программ-на-python) +15. [Hex-машинный код](#15-hex-машинный-код) +16. [Сборка плагинов (cbecc)](#16-сборка-плагинов-cbecc) +17. [Тулчейн: компиляция из разных языков](#17-тулчейн-компиляция-из-разных-языков) +18. [Система POST-диагностики](#18-система-post-диагностики) +19. [Memory-mapped I/O](#19-memory-mapped-io) +20. [Бесконечные шаги и детектор циклов](#20-бесконечные-шаги-и-детектор-циклов) +21. [Тёмная тема GUI](#21-тёмная-тема-gui) +22. [Примеры](#22-примеры) +23. [Устранение неполадок](#23-устранение-неполадок) +24. [Справочник команд](#24-справочник-команд) + +--- + +## 1. Введение + +**CBE (Create. Build. Execute.)** — платформа для эмуляции и создания собственных компьютерных компонентов: процессоров, оперативной памяти, видеокарт, дисков, BIOS, клавиатур и звуковых карт. Всё это собирается в единую архитектуру и запускается через эмулятор с графическим интерфейсом. + +Основная идея: вы описываете поведение компонента через JSON-конфигурацию и/или бинарные данные, а платформа предоставляет среду выполнения с: + +- Эмулятором на Java с GUI (Swing) +- Компилятором .cbeplugin +- Тулчейном для ассемблера, C/C++, Python, hex +- Системой POST-диагностики +- Детектором бесконечных циклов +- Тёмной темой + +### Ключевые концепции + +- **Plugin** — модуль в формате `.cbeplugin`, содержащий метаданные, инструкции, микрокод и данные +- **ModuleInstance** — экземпляр загруженного модуля в эмуляторе +- **Engine** — оркестратор, управляющий всеми модулями и циклом выполнения CPU +- **Bus** — системная шина, связывающая модули +- **Registers** — регистры CPU +- **POST** — Power-On Self-Test, система самодиагностики при старте +- **Memory-mapped I/O** — регистры ввода-вывода, отображённые на адресное пространство + +--- + +## 2. Архитектура CBE + +``` +┌─────────────────────────────────────────────────────────────┐ +│ CBE Emulator │ +├─────────────────────────────────────────────────────────────┤ +│ ┌───────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ +│ │ CPU │ │ RAM │ │ GPU │ │ KBD │ │ +│ │ (плагин) │ │ (плагин) │ │ (плагин) │ │ (плагин) │ │ +│ └─────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │ +│ │ │ │ │ │ +│ ┌─────┴──────────────┴─────────────┴───────────────┴─────┐ │ +│ │ System Bus (шина) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ │ │ │ │ +│ ┌─────┴─────┐ ┌────┴────┐ ┌─────┴─────┐ ┌──────┴─────┐ │ +│ │ SND/Sound│ │ BIOS │ │ DISK │ │ POST │ │ +│ │ (плагин) │ │ (плагин)│ │ (плагин) │ │ (встроено) │ │ +│ └───────────┘ └─────────┘ └───────────┘ └────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Модули (ModuleType) + +| Тип | ID | Описание | +|---------|----|--------------------------------------------------------------| +| CPU | 0 | Процессор. Определяет регистры, набор инструкций, микрокод | +| RAM | 1 | Оперативная память с банками данных | +| DISK | 2 | Диск с секторной адресацией (512 байт/сектор) | +| GPU | 3 | Видеокарта. 80x25 текстовый режим, VRAM, курсор | +| BIOS | 4 | BIOS с информацией о системе | +| KBD | 5 | Клавиатура с буфером клавиш, handshake-протокол | +| SND | 6 | Звуковая карта (PC Speaker beep) | +| DATA_ONLY | 7 | Только данные, без логики | + +### CompileMode + +| Режим | ID | Описание | +|------------|----|------------------------------------| +| FULL | 0 | Только логика (инструкции + микрокод) | +| HYBRID | 1 | Логика + данные | +| PACK_ONLY | 2 | Только данные | + +--- + +## 3. Формат плагина (.cbeplugin) + +### Структура файла + +Бинарный формат с little-endian числами: + +``` +Смещение Размер Поле +-------- ------ ------------------------------------ +0 8 Магическая строка: "CBE_PLUG\0" +8 4 Версия (int32 LE) +12 4 Размер заголовка (int32 LE, всегда 62) +16 1 ModuleType (byte) +17 1 CompileMode (byte) +18 4 Смещение секции метаданных (int32 LE) +22 4 Длина секции метаданных +26 4 Смещение таблицы опкодов +30 4 Длина таблицы опкодов +34 4 Смещение таблицы микрокода +38 4 Длина таблицы микрокода +42 4 Смещение байткода обработчика +46 4 Длина байткода обработчика +50 4 Смещение секции данных +54 4 Длина секции данных +58 4 CRC32 (покрывает байты 0..57) +``` + +### Секции после заголовка + +- **Метаданные:** JSON-строка с name, arch, module_type, version, tdp, frequency +- **Таблица опкодов:** JSON-массив объектов инструкций +- **Микрокод:** JSON-объект { command: [byte, ...] } +- **Байткод обработчика:** зарезервировано (raw bytecode) +- **Секция данных:** [bank_count:4][bank0_size:4][bank0_data...][bank1_size:4]... + +--- + +## 4. Создание модуля CPU + +### Структура директории + +``` +my-cpu.cpu/ +├── module.json # Метаданные модуля +├── registers.json # Определения регистров +├── instructions/ # Определения инструкций +│ ├── 0x00.json # NOP +│ ├── 0x01.json # MOV +│ └── ... +├── microcode/ +│ └── bus.json # Сигналы шины +├── roms/ +│ └── boot.bin # Boot ROM (исполняемый код) +├── banks/ # Банки данных (опционально) +│ └── bank_0.bin +├── program.asm # Исходник программы (альтернатива boot.bin) +├── program.c # Исходник на C +├── program.py # Исходник на Python +└── program.hex # Hex-машинный код +``` + +### module.json + +```json +{ + "name": "MyCPU", + "arch": "my-8bit", + "module_type": "cpu", + "version": 1, + "tdp": 5.0, + "memory_size": 65536, + "frequency": 1000000 +} +``` + +Поля: +- `name` — название процессора +- `arch` — архитектура (строка) +- `module_type` — "cpu" +- `tdp` — тепловыделение (W) +- `memory_size` — размер адресного пространства (байт) +- `frequency` — частота в Hz + +### registers.json + +```json +{ + "a": 0, + "b": 0, + "c": 0, + "d": 0, + "pc": 0, + "sp": 0, + "carry": 0, + "zero": 0 +} +``` + +Каждый регистр — ключ с начальным значением. `pc` (program counter) и `sp` (stack pointer) обязательны. + +### Определение инструкции + +Файл `instructions/0x00.json`: + +```json +{ + "opcode": 0, + "mnemonic": "NOP", + "args": [], + "cycles": 1, + "semantics": [ + {"op": "nop"} + ] +} +``` + +Инструкция с аргументами: + +```json +{ + "opcode": 2, + "mnemonic": "MOV_IMM_A", + "args": [ + {"name": "value", "type": "imm"} + ], + "cycles": 2, + "semantics": [ + {"op": "load_imm", "to": "a", "value": "$next"} + ] +} +``` + +- `opcode` — числовой код операции (0-255) +- `mnemonic` — мнемоника для ассемблера +- `args` — список аргументов (name + type: "reg", "imm", "address") +- `cycles` — тактов на выполнение +- `semantics` — список семантических операций (см. раздел 11) + +### Микрокод (bus.json) + +```json +{ + "memory_read": [0x01, 0x00], + "memory_write": [0x02, 0x00], + "io_read": [0x03], + "io_write": [0x04] +} +``` + +Каждый сигнал — имя и массив байт. + +### Boot ROM + +Файл `roms/boot.bin` содержит исполняемый код, который загружается в начало адресного пространства CPU при старте. Максимальный размер — 64KB. Можно создать: +- через ассемблер (program.asm) +- через компилятор C (program.c) +- через Python-транслятор (program.py) +- как hex-дамп (program.hex) + +--- + +## 5. Создание модуля RAM + +``` +my-ram.ram/ +├── module.json +├── banks/ +│ └── bank_0.bin # Начальные данные RAM +└── controller/ # Опциональный контроллер + └── microcode/ + └── bus.json +``` + +### module.json + +```json +{ + "name": "MyRAM", + "arch": "generic-ram", + "module_type": "ram", + "version": 1, + "tdp": 2.0, + "bank_size": 256, + "bank_count": 1 +} +``` + +Поля: +- `bank_size` — размер банка в байтах +- `bank_count` — количество банков + +--- + +## 6. Создание модуля GPU + +``` +my-gpu.gpu/ +├── module.json +├── banks/ +│ └── bank_0.bin # VRAM (2000 байт для 80x25) +└── microcode/ + └── bus.json +``` + +### module.json + +```json +{ + "name": "MyGPU", + "arch": "vga-text-80x25", + "module_type": "gpu", + "version": 1, + "tdp": 10.0, + "vram_size": 2000, + "rows": 25, + "cols": 80 +} +``` + +Поля: +- `vram_size` — размер видеопамяти (80x25 = 2000) +- `rows` — количество строк +- `cols` — количество колонок + +GPU отображается как 80x25 текстовый терминал (а-ля QEMU). Вывод символов осуществляется записью в порт 0xC0. + +--- + +## 7. Создание модуля KBD + +``` +my-kbd.kbd/ +└── module.json +``` + +### module.json + +```json +{ + "name": "MyKeyboard", + "arch": "ps2-like", + "module_type": "kbd", + "version": 1, + "tdp": 0.5, + "buffer_size": 32 +} +``` + +Поля: +- `buffer_size` — размер буфера клавиш + +Клавиатура работает через memory-mapped I/O: +- Чтение `0xFB` — данные клавиши (peek, не удаляет) +- Чтение `0xFA` — статус (0 = нет клавиши, 1 = есть) +- Запись `0xFA = 0` — acknowledge (удаляет клавишу из буфера) + +--- + +## 8. Создание модуля SND + +``` +my-snd.snd/ +└── module.json +``` + +### module.json + +```json +{ + "name": "MySound", + "arch": "pc-speaker", + "module_type": "snd", + "version": 1, + "tdp": 1.0 +} +``` + +Любая запись в порт `0xF9` вызывает системный звуковой сигнал (beep). Частота ограничена 10 beep/сек. + +--- + +## 9. Создание модуля BIOS + +``` +my-bios.bios/ +└── module.json +``` + +### module.json + +```json +{ + "name": "MyBIOS", + "arch": "generic-bios", + "module_type": "bios", + "version": 1, + "tdp": 1.0 +} +``` + +BIOS хранит строку информации о системе, доступную через `engine.getSystemInfo()`. Триггер перезапуска диагностики — запись в порт `0xF7`. + +--- + +## 10. Создание модуля DISK + +``` +my-disk.disk/ +├── module.json +└── banks/ + └── bank_0.bin # Данные диска +``` + +### module.json + +```json +{ + "name": "MyDisk", + "arch": "ata-like", + "module_type": "disk", + "version": 1, + "tdp": 5.0, + "sector_size": 512 +} +``` + +Диск работает через порты: +- `0xB0` — запись LBA сектора (инициирует чтение) +- `0xB1` — статус готовности (1 = готов) +- `0xB2+` — данные сектора (512 байт) + +Если последние 2 байта сектора 0 равны `0x55 0xAA`, диск считается загрузочным. + +### Загрузка с диска (Boot flow) + +При старте эмулятор выполняет POST-диагностику, которая: +1. Пишет BIOS-программу в память CPU (адреса `0x00..0x15`) +2. Строит POST-текст и размещает его в буфере display (`0x16+`) +3. Если диск загрузочный (сектор 0 содержит `0x55 0xAA`): + - Копирует сектор 0 в память CPU по адресу `0x80` + - Заменяет idle-цикл BIOS (`0x14..0x15`) на `JMP_IMM 0x80` +4. CPU начинает выполнение с `PC=0`: + - Исполняет BIOS-программу, которая выводит POST-текст на GPU + - После вывода POST-текста переходит к `0x14` → прыжок на `0x80` + - CPU выполняет загрузочный код из сектора 0 + +Пример создания загрузочного диска (Python): +```python +# TinyCPU bytecode for boot sector at address 0x80 +# Prints "BOOT OK!" to GPU via port 0xC0, then halts +program = bytes([ + 0x02, 0x42, # MOV_IMM_A 'B' + 0x0A, 0xC0, # STORE_A 0xC0 + 0x02, 0x4F, # MOV_IMM_A 'O' + 0x0A, 0xC0, # STORE_A 0xC0 + 0x02, 0x4F, # MOV_IMM_A 'O' + 0x0A, 0xC0, # STORE_A 0xC0 + 0x02, 0x54, # MOV_IMM_A 'T' + 0x0A, 0xC0, # STORE_A 0xC0 + 0x02, 0x20, # MOV_IMM_A ' ' + 0x0A, 0xC0, # STORE_A 0xC0 + 0x02, 0x4F, # MOV_IMM_A 'O' + 0x0A, 0xC0, # STORE_A 0xC0 + 0x02, 0x4B, # MOV_IMM_A 'K' + 0x0A, 0xC0, # STORE_A 0xC0 + 0x02, 0x21, # MOV_IMM_A '!' + 0x0A, 0xC0, # STORE_A 0xC0 + 0xFF, # HLT +]) +sector = program + b'\x00' * (510 - len(program)) + b'\x55\xAA' +``` + +Готовый пример: `examples/system-disk/disk.img/`. + +--- + +## 11. Семантические операции + +| Операция | Описание | +|-------------|--------------------------------------------------------------| +| `copy` | Копирует значение из `from` в `to` | +| `load_imm` | Загружает непосредственное значение (literal или `$next`) | +| `add` | Складывает `from` + `to`, сохраняет в `result`, обновляет флаги carry/zero | +| `sub` | Вычитает `to` из `from`, обновляет флаги borrow/zero | +| `cmp` | Сравнивает `from` - `to`, обновляет флаги | +| `store` | Записывает значение в память `mem[address]` | +| `load` | Читает из памяти `mem[address]` в регистр | +| `jmp` | Безусловный переход на адрес в `to` | +| `jcc` | Условный переход, если флаг равен ожидаемому | +| `jmp_imm` | Переход на байт, следующий за опкодом | +| `jcc_imm` | Условный переход на байт за опкодом | +| `call` | Push адреса возврата в стек, прыжок | +| `ret` | Pop адреса из стека, прыжок | +| `push` | Push значения в стек | +| `pop` | Pop значения из стека | +| `inc` | Инкремент значения | +| `nop` | Нет операции | + +### Специальные значения + +- `$next` — следующий байт в памяти (используется для immediate значений) +- `arg.X` — ссылка на аргумент инструкции по имени +- `mem[N]` — прямая ссылка на память по адресу N +- `mem[arg.X]` — косвенная адресация через аргумент + +### Условные переходы + +```json +{ + "op": "jcc_imm", + "condition": "zero,1", + "to": "$next" +} +``` + +Формат условия: `"имя_флага,ожидаемое_значение"`. Например `"zero,1"`, `"carry,0"`. + +--- + +## 12. Написание программ на ассемблере + +Ассемблер CBE поддерживает мнемоники, определённые в инструкциях CPU. + +### Синтаксис + +```asm +; комментарий + +label: ; определение метки + MNEMONIC ; инструкция без аргументов + MNEMONIC arg1, arg2 ; инструкция с аргументами + +.org 0x100 ; установка адреса +.byte 0xAB ; эмит байта +.db "hello", 0 ; эмит строки с нулевым терминатором +``` + +### Адресация + +- Числа: `0x10` (hex), `10` (dec), `0b1010` (bin), `$10` (hex) +- Метки: `loop_start`, `loop_start+5` +- Символы: `'A'` (ASCII код) + +### Пример + +```asm +; Программа Hello World +.org 0x00 + +start: + MOV_IMM_A 0x48 ; 'H' + STORE_A 0xC0 ; вывод на GPU + MOV_IMM_A 0x65 ; 'e' + STORE_A 0xC0 + MOV_IMM_A 0x6C ; 'l' + STORE_A 0xC0 + MOV_IMM_A 0x6C ; 'l' + STORE_A 0xC0 + MOV_IMM_A 0x6F ; 'o' + STORE_A 0xC0 + HLT +``` + +### Сборка + +```bash +# Собрать .asm в .bin с архитектурой TinyCPU +cbecc asm program.asm -o roms/boot.bin --arch examples/tiny-cpu.cpu + +# Или собрать весь плагин (авто-детекция program.asm) +cbecc build my-cpu.cpu -o build/my-cpu.cbeplugin +``` + +--- + +## 13. Написание программ на C/C++ + +### Поддержка + +Транслятор C → TinyCPU поддерживает: + +- Объявление переменных: `int x = 5;`, `char c;` +- Присваивание: `x = expr;` +- Вывод на GPU: `write_gpu('A');` +- Вывод в порт: `outb(addr, value);` +- Циклы: `while`, `for` (ограниченно) +- Условные операторы: `if` (ограниченно) +- `return 0` → HLT + +### Пример + +```c +void write_gpu(char c) { + // Транслируется в MOV_IMM_A c; STORE_A 0xC0 +} + +int main() { + write_gpu('H'); + write_gpu('e'); + write_gpu('l'); + write_gpu('l'); + write_gpu('o'); + write_gpu('\n'); + return 0; +} +``` + +### Компиляция + +```bash +# Для TinyCPU (встроенный транслятор) +cbecc ccompile program.c -o roms/boot.bin --arch tinycpu + +# Для нативной архитектуры (x86_64 через gcc) +cbecc ccompile program.c -o program.bin --arch x86_64 + +# C++ +cbecc ccompile program.cpp -o roms/boot.bin --arch tinycpu +``` + +### Для реальных архитектур + +Для не-tinycpu архитектур `cbecc ccompile` использует: +1. `gcc` для компиляции в объектный файл +2. `ld` или `objcopy` для извлечения flat binary + +Убедитесь, что установлены `gcc`, `binutils`. + +--- + +## 14. Написание программ на Python + +### Поддержка + +Транслятор Python → TinyCPU поддерживает упрощённый синтаксис: + +- Присваивание: `x = 5` +- Вывод: `print(x)` → GPU (0xC0) +- Циклы: `for i in range(n):`, `while condition:` +- Условные операторы: `if x == 5:` +- Комментарии: `# comment` + +### Пример + +```python +# Hello World для TinyCPU +print("Hello from Python!") + +# Счётчик +count = 0 +while count < 5: + print(count) + count = count + 1 +``` + +### Трансляция + +```bash +cbecc py program.py -o roms/boot.bin +# Или в составе сборки плагина (auto-detect) +cbecc build my-cpu.cpu -o build/my-cpu.cbeplugin +``` + +--- + +## 15. Hex-машинный код + +Hex-загрузчик поддерживает форматы: +- Простые hex-байты: `02 48 0A C0 FF` +- C-стиль: `{0x02, 0x48, 0x0A, 0xC0, 0xFF}` +- Intel HEX (.hex): `:1000000002480AC0FF...` +- Один байт на строку +- Разделители: пробелы, запятые, новые строки + +### Пример + +``` +; boot.bin в hex-формате +; MOV_IMM_A 'H' ; STORE_A 0xC0 ; HLT +02 48 0A C0 FF +``` + +### Конвертация + +```bash +cbecc hex program.hex -o roms/boot.bin +``` + +--- + +## 16. Сборка плагинов (cbecc) + +### Основные команды + +```bash +# Сборка из source-директории (авто-детекция program.*) +cbecc build my-plugin.cpu -o build/my-plugin.cbeplugin + +# Сборка с указанием бинарника программы +cbecc plugin my-plugin.cpu --program build/program.bin -o build/my-plugin.cbeplugin + +# Сборка стандартная (без авто-детекции) +java -cp modules/cbecc/build/libs/cbecc-0.1.0.jar:modules/loader/build/libs/loader-0.1.0.jar:modules/core/build/libs/core-0.1.0.jar com.cbe.cbecc.Main build my-plugin.cpu -o build/my-plugin.cbeplugin +``` + +### Процесс сборки + +1. Чтение `module.json` +2. Определение ModuleType +3. Парсинг метаданных +4. Если есть `program.asm/py/c/cpp/hex` — компиляция в `roms/boot.bin` +5. Построение секции метаданных (JSON) +6. Построение таблицы опкодов (JSON-массив из инструкций) +7. Построение таблицы микрокода (JSON) +8. Построение секции данных (банки + ROMs) +9. Вычисление CRC32 +10. Запись .cbeplugin файла + +--- + +## 17. Тулчейн: компиляция из разных языков + +### Общая схема + +``` +program.asm ──→ Assembler ──→ boot.bin +program.c ──→ CCompiler ──→ boot.bin (gcc + objcopy или встроенный транслятор) +program.py ──→ PythonTranslator ──→ boot.bin +program.hex ──→ HexLoader ──→ boot.bin + │ + ▼ + module.json + registers.json + instructions/ + microcode/ + │ + ▼ + Compiler (.cbeplugin) + │ + ▼ + my-plugin.cbeplugin ←── run.sh/gradle +``` + +### Авто-детекция + +При запуске `cbecc build `, тулчейн проверяет файлы в порядке приоритета: + +1. `program.asm` → Assembler +2. `program.py` → PythonTranslator +3. `program.c` → CCompiler +4. `program.cpp` → CCompiler +5. `program.hex` → HexLoader + +Если ни один не найден, ищет `roms/boot.bin`. + +--- + +## 18. Система POST-диагностики + +POST (Power-On Self-Test) автоматически выполняется при первом шаге CPU. + +### Коды POST + +| Код | Описание | +|-----|---------------------------------------| +| 0x12| CPU не обнаружен | +| 0x61| Диск обнаружен | +| 0x73| CPU обнаружен | +| 0x74| RAM обнаружена | +| 0x75| GPU обнаружен | +| 0x76| KBD обнаружена | +| 0x77| SND обнаружена | +| 0x78| Все устройства проверены | +| 0x00| BOOT_OK — система готова | +| 0xFF| WARNING — не все компоненты найдены | + +### LED индикаторы + +- PWR — питание +- CPU — процессор +- MEM — память +- VID — видео +- KBD — клавиатура +- SND — звук +- DSK — диск +- CLK — тактовый генератор + +Адреса memory-mapped I/O для POST: +- `0xFE` — POST code +- `0xFD` — Error code +- `0xFC` — LED status + +--- + +## 19. Memory-mapped I/O + +| Адрес | Размер | Назначение | +|-------|--------|-------------------------------------| +| 0xB0 | 1 | DISK_SECTOR_ADDR (LBA сектора) | +| 0xB1 | 1 | DISK_READY_ADDR (1 = готов) | +| 0xB2 | 512 | DISK_DATA_BASE (данные сектора) | +| 0xC0 | 1 | GPU_OUT_CHAR (вывод символа) | +| 0xD0 | 16 | DEVICE_TABLE_BASE (таблица устройств)| +| 0xE0 | 16 | BIOS_INFO_BASE (информация BIOS) | +| 0xF7 | 1 | BIOS_TRIGGER (перезапуск диагностики)| +| 0xF9 | 1 | SND_BEEP_ADDR (звуковой сигнал) | +| 0xFA | 1 | KBD_STATUS_ADDR (0 = нет, 1 = есть) | +| 0xFB | 1 | KBD_DATA_ADDR (код клавиши) | +| 0xFC | 1 | LED_STATUS_ADDR (LED индикаторы) | +| 0xFD | 1 | ERROR_CODE_ADDR (код ошибки) | +| 0xFE | 1 | POST_CODE_ADDR (POST код) | + +--- + +## 20. Бесконечные шаги и детектор циклов + +### Бесконечное выполнение + +В CBE нет максимального лимита шагов. CPU выполняется: + +- **В GUI:** пока не нажата кнопка Stop или CPU не выполнит HLT (0xFF) +- **В консоли:** пока CPU не выполнит HLT или процесс не будет прерван + +### Детектор бесконечных циклов + +В Engine встроен `LoopDetector`, который: + +1. Записывает последние 64 значения PC +2. Анализирует повторяющиеся паттерны (длиной 1-8) +3. При обнаружении цикла выводит сообщение в консоль +4. **НЕ останавливает выполнение** — циклы не прерываются + +Пример сообщения: +``` +[LoopDetector] Infinite loop detected: jump pattern [0x14, 0x06] repeats indefinitely. Execution continues. +``` + +--- + +## 21. Тёмная тема GUI + +Эмулятор CBE использует тёмную тему по умолчанию. + +### Цветовая схема + +| Элемент | Цвет | +|------------------|------------------------| +| Фон | #1E1E24 | +| Панели | #26262E | +| Границы | #32323C | +| Текст основной | #C8C8D2 | +| Текст яркий | #DCDCF0 | +| Акцент (синий) | #50A0FF | +| Зелёный (терминал)| #50DC78 | +| Оранжевый (7-seg) | #FF6414 | + +### Компоненты + +Все панели, текстовые поля, списки, скроллы, кнопки и рамки используют тёмную тему, настроенную через UIManager и явные цвета компонентов. + +### Изменение темы + +Для возврата к системной теме, закомментируйте `applyDarkUIManager()` в `EmulatorWindow.java` и раскомментируйте: +```java +UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); +``` + +--- + +## 22. Примеры + +### Полный пример плагина + +Структура: +``` +examples/tiny-cpu.cpu/ +├── module.json +├── registers.json +├── instructions/ (18 инструкций) +│ ├── 0x00.json NOP +│ ├── 0x01.json MOV_A_B +│ ├── 0x02.json MOV_IMM_A +│ ├── 0x03.json ADD +│ ├── 0x04.json SUB +│ ├── 0x05.json CMP_A_B +│ ├── 0x06.json JMP +│ ├── 0x07.json JC +│ ├── 0x08.json CMP_A_B (reuse) +│ ├── 0x09.json MOV_IMM_B +│ ├── 0x0A.json STORE_A +│ ├── 0x0B.json LOAD_A +│ ├── 0x0C.json JMP_IMM +│ ├── 0x0D.json JZ_IMM +│ ├── 0x0E.json RET +│ ├── 0x0F.json INC_A +│ ├── 0x10.json CALL +│ └── 0x11.json (stack ops) +├── microcode/ +│ └── bus.json +└── roms/ + └── boot.bin +``` + +### Запуск эмулятора + +```bash +# Сборка + запуск с GUI +./run.sh + +# Только сборка +./run.sh build + +# Без GUI (консоль) +./run.sh nogui + +# Прямой запуск с кастомными плагинами +java -cp modules/gui/build/libs/gui-0.1.0.jar:modules/loader/build/libs/loader-0.1.0.jar:modules/core/build/libs/core-0.1.0.jar com.cbe.gui.Main \ + --cpu build/tiny-cpu.cbeplugin \ + --ram build/basic-ram.cbeplugin \ + --gpu build/vga-display.cbeplugin + +# Собрать fat-jar +./run.sh jar +``` + +--- + +## 23. Устранение неполадок + +### "Invalid magic: expected CBEPLUGIN" + +Файл .cbeplugin повреждён или это не валидный плагин. Пересоберите: +```bash +./run.sh build +``` + +### "Missing module.json" + +Убедитесь, что source-директория содержит `module.json`. + +### "No CPU loaded" + +Используйте флаг `--cpu`: +```bash +# По директории +--cpu examples/tiny-cpu.cpu +# По .cbeplugin +--cpu build/tiny-cpu.cbeplugin +``` + +### "Checksum mismatch" + +Файл .cbeplugin повреждён. Пересоберите. + +### "Unknown opcode: 0xNN" + +Инструкция с данным опкодом не определена в вашем CPU. Проверьте `instructions/`. + +### GPU не отображает ничего + +1. Убедитесь, что GPU загружен (`--gpu examples/vga-display.gpu`) +2. CPU должен писать в порт `0xC0` +3. Проверьте, что boot ROM содержит валидный код +4. После HLT (0xFF) CPU останавливается — новые символы не выводятся + +### Клавиатура не реагирует + +1. Убедитесь, что KBD модуль загружен +2. CPU должен читать статус (`0xFA`) и данные (`0xFB`) +3. После чтения данных CPU должен записать `0` в статус (`0xFA`) для acknowledge + +--- + +## 24. Справочник команд + +### cbecc + +```bash +# Сборка плагина из директории +cbecc build -o + +# Ассемблирование .asm → .bin +cbecc asm -o --arch + +# Конвертация hex → .bin +cbecc hex -o + +# Компиляция C/C++ → .bin +cbecc ccompile -o --arch + +# Трансляция Python → .bin +cbecc py -o + +# Сборка плагина с готовой программой +cbecc plugin --program -o +``` + +### cbe-emu + +```bash +# Запуск с GUI +./run.sh + +# Запуск без GUI +./run.sh nogui + +# Прямой запуск Java +java -cp com.cbe.gui.Main \ + --cpu --ram --gpu \ + [--kbd ] [--snd ] [--bios ] [--disk ] \ + [--program ] [--nogui] + +# Параметры RAM +--ram --ram-base --ram-size +``` + +### Gradle + +```bash +# Полная сборка всех модулей +./gradlew build + +# Сборка fat-jar +./gradlew :modules:gui:stage + +# Сборка native-образа (jpackage) +./gradlew :modules:gui:packageImage + +# Сборка Windows portable +./gradlew :modules:gui:portableWindowsZip +``` + +### run.sh + +```bash +./run.sh build # пересобрать все jar +./run.sh compile # скомпилировать example-плагины +./run.sh nogui # запуск без GUI +./run.sh disk # загрузка с диска (nogui) +./run.sh # запуск с GUI +./run.sh jar # fat-jar +./run.sh image # native-образ +./run.sh windows # Windows portable +``` diff --git a/README.md b/README.md index 70ec394..077b601 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # CBE C.B.E. - CREATE. BUILD. EXECUTE. -Что такое C.B.E.? - -C.B.E. - Платфорам для создания своего: +Платформа для создания своего: - процессора - контроллера оперативки - видюхи @@ -11,4 +9,40 @@ C.B.E. - Платфорам для создания своего: - чипсета - архитектуры - почти всего в компе -- та даже gpu дрйвер что бы выводить изображение в COM1-to-VGA +- та даже gpu драйвер что бы выводить изображение в COM1-to-VGA + +## Новые возможности + +- **Тёмная тема** — стильный тёмный GUI для эмулятора +- **Бесконечные шаги** — нет лимита на количество инструкций, детектор бесконечных циклов +- **Мультиязычный тулчейн** — пишите программы для своего CPU на ASM, C/C++, Python или hex-машинном коде +- **Полное руководство** — см. [MANUAL.md](MANUAL.md) для детального описания + +## Быстрый старт + +```bash +./run.sh # Сборка + запуск с GUI +./run.sh build # Только сборка +./run.sh nogui # Запуск без GUI +``` + +## Инструкция + +Полное руководство по созданию плагинов, систем, написанию программ на разных языках и т.п.: +→ **[MANUAL.md](MANUAL.md)** + +## Примеры + +```bash +# Собрать плагин из директории с авто-детекцией program.asm/.c/.py/.hex +cbecc build examples/asm-demo.cpu -o build/asm-demo.cbeplugin + +# Ассемблировать .asm в .bin +cbecc asm examples/asm-demo.cpu/program.asm -o build/demo.bin --arch examples/tiny-cpu.cpu + +# Транслировать Python в байткод +cbecc py examples/asm-demo.cpu/program.py -o build/demo.bin + +# Сконвертировать hex в бинарник +cbecc hex examples/asm-demo.cpu/program.hex -o build/demo.bin +``` diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..e582e85 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "com.cbe.android" + compileSdk = 35 + + defaultConfig { + applicationId = "com.cbe.android" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "1.0.0" + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.5.11" + } +} + +dependencies { + implementation(project(":cbe-core")) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.material.icons.core) + debugImplementation(libs.androidx.compose.ui.tooling) +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0fb32a0 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/assets/plugins/basic-kbd.cbeplugin b/android/app/src/main/assets/plugins/basic-kbd.cbeplugin new file mode 100644 index 0000000..44a9d33 Binary files /dev/null and b/android/app/src/main/assets/plugins/basic-kbd.cbeplugin differ diff --git a/android/app/src/main/assets/plugins/basic-ram.cbeplugin b/android/app/src/main/assets/plugins/basic-ram.cbeplugin new file mode 100644 index 0000000..d16365f Binary files /dev/null and b/android/app/src/main/assets/plugins/basic-ram.cbeplugin differ diff --git a/android/app/src/main/assets/plugins/basic-snd.cbeplugin b/android/app/src/main/assets/plugins/basic-snd.cbeplugin new file mode 100644 index 0000000..d30d7a1 Binary files /dev/null and b/android/app/src/main/assets/plugins/basic-snd.cbeplugin differ diff --git a/android/app/src/main/assets/plugins/big-disk.cbeplugin b/android/app/src/main/assets/plugins/big-disk.cbeplugin new file mode 100644 index 0000000..7f74108 Binary files /dev/null and b/android/app/src/main/assets/plugins/big-disk.cbeplugin differ diff --git a/android/app/src/main/assets/plugins/big-ram.cbeplugin b/android/app/src/main/assets/plugins/big-ram.cbeplugin new file mode 100644 index 0000000..fdde200 Binary files /dev/null and b/android/app/src/main/assets/plugins/big-ram.cbeplugin differ diff --git a/android/app/src/main/assets/plugins/system-disk.cbeplugin b/android/app/src/main/assets/plugins/system-disk.cbeplugin new file mode 100644 index 0000000..aff50eb Binary files /dev/null and b/android/app/src/main/assets/plugins/system-disk.cbeplugin differ diff --git a/android/app/src/main/assets/plugins/tiny-bios.cbeplugin b/android/app/src/main/assets/plugins/tiny-bios.cbeplugin new file mode 100644 index 0000000..c0cadf2 Binary files /dev/null and b/android/app/src/main/assets/plugins/tiny-bios.cbeplugin differ diff --git a/android/app/src/main/assets/plugins/tiny-cpu.cbeplugin b/android/app/src/main/assets/plugins/tiny-cpu.cbeplugin new file mode 100644 index 0000000..f78e4a1 Binary files /dev/null and b/android/app/src/main/assets/plugins/tiny-cpu.cbeplugin differ diff --git a/android/app/src/main/assets/plugins/vga-display.cbeplugin b/android/app/src/main/assets/plugins/vga-display.cbeplugin new file mode 100644 index 0000000..352dd6b Binary files /dev/null and b/android/app/src/main/assets/plugins/vga-display.cbeplugin differ diff --git a/android/app/src/main/assets/plugins/vga-plus.cbeplugin b/android/app/src/main/assets/plugins/vga-plus.cbeplugin new file mode 100644 index 0000000..337171d Binary files /dev/null and b/android/app/src/main/assets/plugins/vga-plus.cbeplugin differ diff --git a/android/app/src/main/java/com/cbe/android/CbeApp.kt b/android/app/src/main/java/com/cbe/android/CbeApp.kt new file mode 100644 index 0000000..49fb5c7 --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/CbeApp.kt @@ -0,0 +1,15 @@ +package com.cbe.android + +import android.app.Application +import com.cbe.android.engine.ModuleProvider + +class CbeApp : Application() { + lateinit var moduleProvider: ModuleProvider + private set + + override fun onCreate() { + super.onCreate() + moduleProvider = ModuleProvider(this) + moduleProvider.setupBeep() + } +} diff --git a/android/app/src/main/java/com/cbe/android/EmulatorViewModel.kt b/android/app/src/main/java/com/cbe/android/EmulatorViewModel.kt new file mode 100644 index 0000000..d3ca206 --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/EmulatorViewModel.kt @@ -0,0 +1,170 @@ +package com.cbe.android + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.cbe.android.engine.AndroidEngine +import com.cbe.android.engine.ModuleProvider +import com.cbe.android.engine.PluginConfig +import com.cbe.android.engine.PluginEntry +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +data class EmulatorUiState( + val gpuText: String = "", + val postCode: Int = 0, + val ledStatus: Int = 0, + val isRunning: Boolean = false, + val isHalted: Boolean = false, + val instructionCount: Long = 0, + val systemInfo: String = "", + val statusMessage: String = "", + val pluginConfig: PluginConfig = PluginConfig(), + val pluginEntries: List = emptyList(), + val pluginPaths: Map = emptyMap(), + val speedIndex: Int = 3, + val speedLabel: String = "1x", + val speedIps: Long = 100_000 +) + +class EmulatorViewModel(application: Application) : AndroidViewModel(application) { + + private val _uiState = MutableStateFlow(EmulatorUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val engine = AndroidEngine() + private var runJob: Job? = null + private var refreshJob: Job? = null + + private val moduleProvider get() = getApplication().moduleProvider + + init { + engine.init() + } + + fun initialize() { + viewModelScope.launch { + refreshPluginList() + reloadPlugins() + } + } + + fun refreshPluginList() { + viewModelScope.launch { + val entries = moduleProvider.listAllPlugins() + val paths = moduleProvider.getPluginPaths() + _uiState.value = _uiState.value.copy( + pluginEntries = entries, + pluginPaths = paths + ) + } + } + + fun setSpeed(index: Int) { + engine.setSpeed(index) + val speed = engine.speed + _uiState.value = _uiState.value.copy( + speedIndex = index, + speedLabel = speed.label, + speedIps = speed.ips + ) + } + + fun updatePluginConfig(slot: String, pluginName: String) { + viewModelScope.launch { + val cfg = _uiState.value.pluginConfig + val newCfg = when (slot) { + "cpu" -> cfg.copy(cpu = pluginName) + "ram" -> cfg.copy(ram = pluginName) + "gpu" -> cfg.copy(gpu = pluginName) + "kbd" -> cfg.copy(kbd = pluginName) + "snd" -> cfg.copy(snd = pluginName) + "bios" -> cfg.copy(bios = pluginName) + "disk" -> cfg.copy(disk = if (pluginName == "none") null else pluginName) + else -> cfg + } + reloadPlugins(newCfg) + } + } + + fun reloadPlugins(config: PluginConfig? = null) { + viewModelScope.launch { + val cfg = config ?: _uiState.value.pluginConfig + _uiState.value = _uiState.value.copy(pluginConfig = cfg) + + try { + val plugins = moduleProvider.extractPlugins(cfg) + engine.loadModules(plugins) + _uiState.value = _uiState.value.copy( + systemInfo = engine.systemInfo, + statusMessage = "" + ) + startPolling() + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + statusMessage = "Failed to load: ${e.message}" + ) + } + } + } + + private fun startPolling() { + refreshJob?.cancel() + refreshJob = viewModelScope.launch { + while (isActive) { + updateUiState() + delay(50) + } + } + } + + private fun updateUiState() { + _uiState.value = _uiState.value.copy( + gpuText = engine.getGpuText() ?: "", + postCode = engine.postCode, + ledStatus = engine.ledStatus, + isRunning = engine.isRunning, + isHalted = engine.isHalted, + instructionCount = engine.instructionsExecuted + ) + engine.markGpuClean() + } + + fun runFull() { + if (engine.isRunning || engine.isHalted) return + runJob = viewModelScope.launch { + engine.runFull() + } + } + + fun pause() { + runJob?.cancel() + } + + fun pushKey(keyCode: Int) { + engine.pushKey(keyCode) + } + + fun reset() { + runJob?.cancel() + engine.reset() + updateUiState() + viewModelScope.launch { + withContext(Dispatchers.IO) { engine.engine.runDiagnostics() } + updateUiState() + } + } + + override fun onCleared() { + super.onCleared() + runJob?.cancel() + refreshJob?.cancel() + } +} diff --git a/android/app/src/main/java/com/cbe/android/MainActivity.kt b/android/app/src/main/java/com/cbe/android/MainActivity.kt new file mode 100644 index 0000000..9bff8f5 --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/MainActivity.kt @@ -0,0 +1,29 @@ +package com.cbe.android + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import com.cbe.android.ui.MainScreen +import com.cbe.android.ui.theme.CbeTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + CbeTheme { + Surface(modifier = Modifier.fillMaxSize()) { + val viewModel: EmulatorViewModel = viewModel() + LaunchedEffect(Unit) { viewModel.initialize() } + MainScreen(viewModel) + } + } + } + } +} diff --git a/android/app/src/main/java/com/cbe/android/engine/AndroidEngine.kt b/android/app/src/main/java/com/cbe/android/engine/AndroidEngine.kt new file mode 100644 index 0000000..792c4ff --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/engine/AndroidEngine.kt @@ -0,0 +1,183 @@ +package com.cbe.android.engine + +import com.cbe.loader.Engine +import com.cbe.loader.SimpleRegisters +import kotlin.coroutines.coroutineContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import java.io.File + +data class SpeedConfig( + val label: String = "1x", + val batchSize: Int = 1000, + val batchDelayMs: Long = 10 +) { + /** Estimated instructions per second */ + val ips: Long get() = (batchSize * 1000L) / maxOf(batchDelayMs, 1) +} + +class AndroidEngine { + + val engine = Engine() + val registers = SimpleRegisters() + + var postCode: Int = 0 + private set + var ledStatus: Int = 0 + private set + var postDescription: String = "" + private set + + var isRunning: Boolean = false + private set + var isHalted: Boolean = false + private set + var instructionsExecuted: Long = 0 + private set + var systemInfo: String = "" + private set + + var speed: SpeedConfig = SPEEDS[3] // default = 1x + private set + + private var postListener: Engine.PostListener? = null + + companion object { + val SPEEDS = listOf( + SpeedConfig("0.25x", 100, 40), + SpeedConfig("0.5x", 250, 20), + SpeedConfig("1x", 1000, 10), + SpeedConfig("2x", 2000, 5), + SpeedConfig("4x", 5000, 2), + SpeedConfig("MAX", 50000, 0) + ) + } + + fun init() { + postListener = Engine.PostListener { code, leds, desc -> + postCode = code + ledStatus = leds + postDescription = desc ?: "" + } + engine.addPostListener(postListener!!) + } + + fun setSpeed(index: Int) { + if (index in SPEEDS.indices) speed = SPEEDS[index] + } + + suspend fun loadModules(plugins: Map) = withContext(Dispatchers.IO) { + val cpu = plugins["tiny-cpu"] + val ram = plugins["basic-ram"] + val gpu = plugins["vga-display"] + val kbd = plugins["basic-kbd"] + val snd = plugins["basic-snd"] + val bios = plugins["tiny-bios"] + val disk = plugins["big-disk"] ?: plugins["system-disk"] + + if (cpu != null) engine.loadCompiledCpu(cpu.toPath()) + if (ram != null) engine.loadCompiledRam(ram.toPath(), 0x00) + if (gpu != null) engine.loadCompiledGpu(gpu.toPath()) + if (kbd != null) engine.loadCompiledKbd(kbd.toPath()) + if (snd != null) engine.loadCompiledSnd(snd.toPath()) + if (bios != null) engine.loadCompiledBios(bios.toPath()) + if (disk != null) engine.loadCompiledDisk(disk.toPath()) + + systemInfo = engine.runDiagnostics() + } + + suspend fun runFull() { + if (isRunning || isHalted) return + isRunning = true + try { + while (coroutineContext[Job]?.isActive != false && !isHalted) { + val batchStart = instructionsExecuted + val limit = speed.batchSize + while (coroutineContext[Job]?.isActive != false && !isHalted + && instructionsExecuted - batchStart < limit) { + val alive = engine.step(registers) + instructionsExecuted++ + if (!alive) { + isHalted = true + break + } + } + if (speed.batchDelayMs > 0 && !isHalted && coroutineContext[Job]?.isActive != false) { + delay(speed.batchDelayMs) + } + } + } finally { + isRunning = false + } + } + + fun step(): Boolean { + if (isHalted) return false + val alive = engine.step(registers) + instructionsExecuted++ + if (!alive) isHalted = true + return alive + } + + suspend fun runSteps(count: Int): Int { + if (isRunning || isHalted) return 0 + isRunning = true + try { + var executed = 0 + for (i in 0 until count) { + if (coroutineContext[Job]?.isActive == false) break + if (!engine.step(registers)) { + isHalted = true + break + } + executed++ + } + instructionsExecuted += executed + return executed + } finally { + isRunning = false + } + } + + fun pushKey(keyCode: Int) { + engine.pushKey(keyCode) + } + + fun reset() { + engine.reset() + registers.write("pc", 0) + registers.write("sp", 0x80) + isHalted = false + isRunning = false + instructionsExecuted = 0 + postCode = 0 + ledStatus = 0 + } + + fun getGpuText(): String? { + return engine.sourceGpu?.readString() + ?: engine.compiledGpu?.readString() + } + + fun getGpuRows(): Int { + return engine.sourceGpu?.getRows() + ?: engine.compiledGpu?.getRows() ?: 0 + } + + fun getGpuCols(): Int { + return engine.sourceGpu?.getCols() + ?: engine.compiledGpu?.getCols() ?: 0 + } + + fun isGpuDirty(): Boolean { + return engine.sourceGpu?.isDirty() + ?: engine.compiledGpu?.isDirty() ?: false + } + + fun markGpuClean() { + engine.sourceGpu?.markClean() + engine.compiledGpu?.markClean() + } +} diff --git a/android/app/src/main/java/com/cbe/android/engine/ModuleProvider.kt b/android/app/src/main/java/com/cbe/android/engine/ModuleProvider.kt new file mode 100644 index 0000000..74e734e --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/engine/ModuleProvider.kt @@ -0,0 +1,172 @@ +package com.cbe.android.engine + +import android.content.Context +import android.os.Environment +import com.cbe.loader.AudioBridge +import com.cbe.loader.BeepHandler +import java.io.File +import java.io.FileOutputStream + +data class PluginEntry( + val name: String, + val file: File, + val source: String // "assets", "external", "media" +) + +data class PluginConfig( + val cpu: String = "tiny-cpu.cbeplugin", + val ram: String = "basic-ram.cbeplugin", + val gpu: String = "vga-display.cbeplugin", + val kbd: String = "basic-kbd.cbeplugin", + val snd: String = "basic-snd.cbeplugin", + val bios: String = "tiny-bios.cbeplugin", + val disk: String? = "big-disk.cbeplugin" +) { + fun allPlugins(): List { + val list = mutableListOf(cpu, ram, gpu, kbd, snd, bios) + if (disk != null) list.add(disk) + return list + } + + companion object { + val SLOTS = listOf("cpu", "ram", "gpu", "kbd", "snd", "bios", "disk") + } +} + +class ModuleProvider(private val context: Context) { + + /** Internal directory where assets are extracted */ + private val pluginDir: File by lazy { + File(context.filesDir, "plugins").also { it.mkdirs() } + } + + /** App-specific external storage: /sdcard/Android/data//files/plugins/ */ + private val externalPluginDir: File by lazy { + context.getExternalFilesDir("plugins")?.also { it.mkdirs() } + ?: File(context.filesDir, "external-plugins").also { it.mkdirs() } + } + + /** Shared media path: /sdcard/Android/media/com.cbe/ */ + private val mediaPluginDir: File by lazy { + try { + File(Environment.getExternalStorageDirectory(), "Android/media/com.cbe") + } catch (e: Exception) { + File(context.filesDir, "media-plugins").also { it.mkdirs() } + } + } + + /** Scan all sources and return available plugins with their source info. + * External files override internal ones with the same name. */ + fun listAllPlugins(): List { + val seen = mutableSetOf() + val result = mutableListOf() + + // External overrides (highest priority) + for (dir in listOf(mediaPluginDir, externalPluginDir)) { + if (dir.exists() && dir.isDirectory) { + val source = if (dir == mediaPluginDir) "media" else "external" + dir.listFiles { f -> f.name.endsWith(".cbeplugin") }?.forEach { f -> + if (seen.add(f.name)) { + result.add(PluginEntry(f.name, f, source)) + } + } + } + } + + // Internal assets (lower priority) + try { + context.assets.list("plugins")?.forEach { name -> + if (name.endsWith(".cbeplugin") && seen.add(name)) { + val target = File(pluginDir, name) + result.add(PluginEntry(name, target, "assets")) + } + } + } catch (e: Exception) { + android.util.Log.w("ModuleProvider", "Failed to list assets: ${e.message}") + } + + return result + } + + /** Extract all needed plugins from assets to internal storage if not already there. + * External files are used in-place (no copy needed). */ + fun extractPlugins(config: PluginConfig = PluginConfig()): Map { + val all = listAllPlugins() + val byName = all.associateBy { it.name } + val result = mutableMapOf() + + for (name in config.allPlugins()) { + val entry = byName[name] + if (entry != null && entry.file.exists()) { + // External file found — use directly + val key = name.removeSuffix(".cbeplugin") + result[key] = entry.file + } else { + // Not found externally — extract from assets + val target = File(pluginDir, name) + if (!target.exists()) { + try { + context.assets.open("plugins/$name").use { input -> + FileOutputStream(target).use { output -> + input.copyTo(output) + } + } + android.util.Log.i("ModuleProvider", "Extracted $name to $target") + } catch (e: Exception) { + android.util.Log.w("ModuleProvider", "Failed to extract $name: ${e.message}") + } + } + if (target.exists()) { + val key = name.removeSuffix(".cbeplugin") + result[key] = target + } + } + } + return result + } + + companion object { + /** List plugins grouped by their likely slot type based on filename patterns. */ + fun classifyPlugins(entries: List): Map> { + val result = linkedMapOf>() + for (slot in PluginConfig.SLOTS) result[slot] = mutableListOf() + result["other"] = mutableListOf() + + for (e in entries) { + val name = e.name.lowercase() + val slot = when { + name.contains("cpu") || name.contains("tiny") -> "cpu" + name.contains("ram") || name.contains("memory") -> "ram" + name.contains("gpu") || name.contains("vga") || name.contains("video") || name.contains("display") -> "gpu" + name.contains("kbd") || name.contains("keyboard") || name.contains("key") -> "kbd" + name.contains("snd") || name.contains("sound") || name.contains("audio") || name.contains("speaker") -> "snd" + name.contains("bios") || name.contains("system") -> "bios" + name.contains("disk") || name.contains("disc") || name.contains("storage") -> "disk" + else -> "other" + } + result.getOrPut(slot) { mutableListOf() }.add(e) + } + return result + } + } + + fun getPluginPaths(): Map { + return mapOf( + "Internal (assets)" to pluginDir.absolutePath, + "External (app)" to externalPluginDir.absolutePath, + "Media (shared)" to mediaPluginDir.absolutePath + ) + } + + fun setupBeep() { + AudioBridge.setBeepHandler(BeepHandler { + try { + val toneGen = android.media.ToneGenerator( + android.media.ToneGenerator.TONE_DTMF_0, 60 + ) + toneGen.startTone(android.media.ToneGenerator.TONE_DTMF_0, 80) + toneGen.release() + } catch (_: Exception) {} + }) + } +} diff --git a/android/app/src/main/java/com/cbe/android/ui/KeyboardSheet.kt b/android/app/src/main/java/com/cbe/android/ui/KeyboardSheet.kt new file mode 100644 index 0000000..5d65478 --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/ui/KeyboardSheet.kt @@ -0,0 +1,132 @@ +package com.cbe.android.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun KeyboardSheet( + onKey: (Int) -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surface, + modifier = modifier + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Row 1: 0-9 + KeyboardRow(keys = listOf( + "0" to 0x30, "1" to 0x31, "2" to 0x32, "3" to 0x33, + "4" to 0x34, "5" to 0x35, "6" to 0x36, "7" to 0x37, + "8" to 0x38, "9" to 0x39 + ), onKey) + + // Row 2: A-Z top half + KeyboardRow(keys = listOf( + "Q" to 0x51, "W" to 0x57, "E" to 0x45, "R" to 0x52, + "T" to 0x54, "Y" to 0x59, "U" to 0x55, "I" to 0x49, + "O" to 0x4F, "P" to 0x50 + ), onKey) + + // Row 3: A-Z bottom half + KeyboardRow(keys = listOf( + "A" to 0x41, "S" to 0x53, "D" to 0x44, "F" to 0x46, + "G" to 0x47, "H" to 0x48, "J" to 0x4A, "K" to 0x4B, + "L" to 0x4C + ), onKey) + + // Row 4: Shift, Z-M, Enter, Backspace + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + KeyButton("Shift", modifier = Modifier.weight(1.5f)) { + onKey(0x10) // SHIFT + } + for ((label, code) in listOf( + "Z" to 0x5A, "X" to 0x58, "C" to 0x43, + "V" to 0x56, "B" to 0x42, "N" to 0x4E, "M" to 0x4D + )) { + KeyButton(label, modifier = Modifier.weight(1f)) { onKey(code) } + } + KeyButton("", modifier = Modifier.weight(1f)) {} + } + + // Row 5: Space, function keys + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + KeyButton("Enter", modifier = Modifier.weight(2f)) { + onKey(0x0D) // CR + } + KeyButton("Space", modifier = Modifier.weight(4f)) { + onKey(0x20) // Space + } + KeyButton("BS", modifier = Modifier.weight(1.5f)) { + onKey(0x08) // Backspace + } + } + + Spacer(Modifier.height(16.dp)) + } + } +} + +@Composable +private fun KeyboardRow( + keys: List>, + onKey: (Int) -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + for ((label, code) in keys) { + KeyButton(label, modifier = Modifier.weight(1f)) { onKey(code) } + } + } +} + +@Composable +private fun KeyButton( + label: String, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = modifier.height(44.dp), + shape = MaterialTheme.shapes.small, + contentPadding = PaddingValues(0.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Text( + text = label, + style = MaterialTheme.typography.labelLarge.copy( + fontFamily = FontFamily.Monospace, + fontSize = 14.sp + ), + maxLines = 1 + ) + } +} diff --git a/android/app/src/main/java/com/cbe/android/ui/MainScreen.kt b/android/app/src/main/java/com/cbe/android/ui/MainScreen.kt new file mode 100644 index 0000000..d5e3fbd --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/ui/MainScreen.kt @@ -0,0 +1,230 @@ +package com.cbe.android.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cbe.android.EmulatorViewModel +import com.cbe.android.engine.AndroidEngine + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen(viewModel: EmulatorViewModel) { + val uiState by viewModel.uiState.collectAsState() + var showKeyboard by remember { mutableStateOf(false) } + var showPlugins by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = "CBE Emulator", + style = MaterialTheme.typography.headlineMedium + ) + }, + actions = { + if (uiState.instructionCount > 0) { + Text( + text = formatCount(uiState.instructionCount), + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(end = 8.dp) + ) + } + IconButton(onClick = { + if (uiState.isRunning) viewModel.pause() + else viewModel.runFull() + }) { + Icon( + imageVector = if (uiState.isRunning) Icons.Default.Close + else Icons.Default.PlayArrow, + contentDescription = if (uiState.isRunning) "Pause" else "Run" + ) + } + IconButton(onClick = { viewModel.reset() }) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Reset" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.background + ) + ) + }, + bottomBar = { + BottomAppBar( + containerColor = MaterialTheme.colorScheme.background + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + FilledTonalButton( + onClick = { showPlugins = !showPlugins }, + modifier = Modifier.height(48.dp) + ) { + Icon(Icons.Default.Settings, contentDescription = null) + Spacer(Modifier.width(4.dp)) + Text("Plugins") + } + FilledTonalButton( + onClick = { showKeyboard = !showKeyboard }, + modifier = Modifier.height(48.dp) + ) { + Text(if (showKeyboard) "Hide KBD" else "Show KBD") + } + } + } + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Speed controls + SpeedControl( + speedIndex = uiState.speedIndex, + speedLabel = uiState.speedLabel, + speedIps = uiState.speedIps, + onSelectSpeed = { viewModel.setSpeed(it) } + ) + + // GPU Screen + ScreenPanel( + text = uiState.gpuText, + modifier = Modifier.weight(1f) + ) + + // POST Panel + PostPanel( + postCode = uiState.postCode, + ledStatus = uiState.ledStatus + ) + + // Status info + if (uiState.isHalted) { + Text( + text = "HALTED after ${formatCount(uiState.instructionCount)} instructions", + style = MaterialTheme.typography.labelMedium.copy( + color = MaterialTheme.colorScheme.error + ) + ) + } + if (uiState.statusMessage.isNotEmpty()) { + Text( + text = uiState.statusMessage, + style = MaterialTheme.typography.labelSmall.copy( + color = MaterialTheme.colorScheme.error + ) + ) + } + } + } + + if (showKeyboard) { + KeyboardSheet( + onKey = { viewModel.pushKey(it) }, + onDismiss = { showKeyboard = false } + ) + } + + if (showPlugins) { + PluginSheet( + entries = uiState.pluginEntries, + pluginConfig = uiState.pluginConfig, + pluginPaths = uiState.pluginPaths, + onSelectPlugin = { slot, name -> viewModel.updatePluginConfig(slot, name) }, + onRefresh = { viewModel.refreshPluginList() }, + onDismiss = { showPlugins = false } + ) + } +} + +@Composable +private fun SpeedControl( + speedIndex: Int, + speedLabel: String, + speedIps: Long, + onSelectSpeed: (Int) -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + ) { + Column(modifier = Modifier.padding(8.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Speed: $speedLabel", + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + text = formatIps(speedIps), + style = MaterialTheme.typography.labelSmall + ) + } + Spacer(Modifier.height(4.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .selectableGroup(), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + AndroidEngine.SPEEDS.forEachIndexed { index, speedCfg -> + FilterChip( + selected = index == speedIndex, + onClick = { onSelectSpeed(index) }, + label = { + Text( + text = speedCfg.label, + fontSize = 11.sp, + maxLines = 1 + ) + }, + modifier = Modifier.weight(1f) + ) + } + } + } + } +} + +private fun formatCount(count: Long): String { + return when { + count < 1000 -> "$count steps" + count < 1_000_000 -> "${count / 1000}K steps" + else -> "${count / 1_000_000}M steps" + } +} + +private fun formatIps(ips: Long): String { + return when { + ips < 1000 -> "$ips IPS" + ips < 1_000_000 -> "${ips / 1000}K IPS" + else -> "${ips / 1_000_000}M IPS" + } +} diff --git a/android/app/src/main/java/com/cbe/android/ui/PluginSheet.kt b/android/app/src/main/java/com/cbe/android/ui/PluginSheet.kt new file mode 100644 index 0000000..a7961e3 --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/ui/PluginSheet.kt @@ -0,0 +1,230 @@ +package com.cbe.android.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cbe.android.engine.ModuleProvider +import com.cbe.android.engine.PluginConfig +import com.cbe.android.engine.PluginEntry + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PluginSheet( + entries: List, + pluginConfig: PluginConfig, + pluginPaths: Map, + onSelectPlugin: (slot: String, pluginName: String) -> Unit, + onRefresh: () -> Unit, + onDismiss: () -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + val classified = remember(entries) { + ModuleProvider.classifyPlugins(entries) + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxHeight(0.85f) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text("Plugin Configuration", style = MaterialTheme.typography.titleLarge) + + // Current config summary + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp)) { + Text("Current Config", style = MaterialTheme.typography.labelLarge) + Spacer(Modifier.height(4.dp)) + PluginConfigSummary(pluginConfig) + } + } + + // Paths + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp)) { + Text("Plugin Paths", style = MaterialTheme.typography.labelLarge) + Spacer(Modifier.height(4.dp)) + for ((label, path) in pluginPaths) { + Text( + text = "$label: $path", + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + fontSize = 10.sp + ) + ) + } + } + } + + // Plugin selectors by slot + Text("Select Plugins", style = MaterialTheme.typography.titleMedium) + for (slot in PluginConfig.SLOTS) { + val slotEntries = classified[slot].orEmpty() + if (slotEntries.isEmpty()) continue + + val currentName = when (slot) { + "cpu" -> pluginConfig.cpu + "ram" -> pluginConfig.ram + "gpu" -> pluginConfig.gpu + "kbd" -> pluginConfig.kbd + "snd" -> pluginConfig.snd + "bios" -> pluginConfig.bios + "disk" -> pluginConfig.disk ?: "none" + else -> "?" + } + + SlotSelector( + slot = slot, + current = currentName, + entries = slotEntries, + onSelect = { name -> onSelectPlugin(slot, name) } + ) + } + + // Other (unclassified) plugins + val other = classified["other"].orEmpty() + if (other.isNotEmpty()) { + Text("Other Plugins", style = MaterialTheme.typography.titleSmall) + for (entry in other) { + Text( + text = " ${entry.name} (${entry.source})", + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + fontSize = 10.sp + ) + ) + } + } + + // Refresh button + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + OutlinedButton(onClick = onRefresh) { + Text("Rescan Files") + } + FilledTonalButton(onClick = onDismiss) { + Text("Done") + } + } + + Spacer(Modifier.height(24.dp)) + } + } +} + +@Composable +private fun PluginConfigSummary(config: PluginConfig) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = "CPU: ${config.cpu}", + style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp) + ) + Text( + text = "RAM: ${config.ram}", + style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp) + ) + Text( + text = "GPU: ${config.gpu}", + style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp) + ) + Text( + text = "KBD: ${config.kbd}", + style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp) + ) + Text( + text = "SND: ${config.snd}", + style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp) + ) + Text( + text = "BIOS: ${config.bios}", + style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp) + ) + Text( + text = "DISK: ${config.disk ?: "(none)"}", + style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 11.sp) + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SlotSelector( + slot: String, + current: String, + entries: List, + onSelect: (String) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = slot.uppercase(), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.SemiBold + ) + Spacer(Modifier.height(4.dp)) + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded } + ) { + OutlinedTextField( + value = current, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) }, + modifier = Modifier + .menuAnchor() + .fillMaxWidth(), + textStyle = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + fontSize = 12.sp + ) + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + if (slot == "disk") { + DropdownMenuItem( + text = { Text("(none)") }, + onClick = { + onSelect("none") + expanded = false + } + ) + } + for (entry in entries) { + val label = "${entry.name} [${entry.source}]" + DropdownMenuItem( + text = { Text(label, fontSize = 12.sp) }, + onClick = { + onSelect(entry.name) + expanded = false + } + ) + } + } + } + Spacer(Modifier.height(8.dp)) + } +} diff --git a/android/app/src/main/java/com/cbe/android/ui/PostPanel.kt b/android/app/src/main/java/com/cbe/android/ui/PostPanel.kt new file mode 100644 index 0000000..a9a656c --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/ui/PostPanel.kt @@ -0,0 +1,99 @@ +package com.cbe.android.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cbe.android.ui.theme.* + +data class LedSpec(val name: String, val bit: Int, val color: Color) + +val LEDS = listOf( + LedSpec("PWR", 0, LedGreen), + LedSpec("CPU", 1, LedRed), + LedSpec("MEM", 2, LedAmber), + LedSpec("VID", 3, LedBlue), + LedSpec("KBD", 4, LedGreen), + LedSpec("SND", 5, LedAmber), + LedSpec("DSK", 6, LedBlue), + LedSpec("CLK", 7, LedRed) +) + +@Composable +fun PostPanel( + postCode: Int, + ledStatus: Int, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + tonalElevation = 2.dp + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + // 7-segment display + Box( + modifier = Modifier + .fillMaxWidth() + .background(SegBackground, RoundedCornerShape(8.dp)) + .padding(horizontal = 16.dp, vertical = 12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = String.format("%02X", postCode and 0xFF), + style = MaterialTheme.typography.displayLarge.copy( + fontFamily = FontFamily.Monospace, + fontSize = 36.sp, + color = SegActive + ) + ) + } + + // LED row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + for (led in LEDS) { + val isOn = (ledStatus shr led.bit) and 1 == 1 + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .size(10.dp) + .clip(CircleShape) + .background(if (isOn) led.color else LedOff) + ) + Spacer(Modifier.height(2.dp)) + Text( + text = led.name, + style = MaterialTheme.typography.labelSmall.copy( + fontSize = 8.sp, + color = if (isOn) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + } + } + } + } +} diff --git a/android/app/src/main/java/com/cbe/android/ui/ScreenPanel.kt b/android/app/src/main/java/com/cbe/android/ui/ScreenPanel.kt new file mode 100644 index 0000000..d042fbb --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/ui/ScreenPanel.kt @@ -0,0 +1,72 @@ +package com.cbe.android.ui + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.cbe.android.ui.theme.ScreenBezel +import com.cbe.android.ui.theme.ScreenBg +import com.cbe.android.ui.theme.ScreenText + +@Composable +fun ScreenPanel( + text: String, + modifier: Modifier = Modifier +) { + Surface( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + shape = RoundedCornerShape(12.dp), + color = ScreenBezel, + tonalElevation = 4.dp + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + .clip(RoundedCornerShape(4.dp)) + .background(ScreenBg) + ) { + BasicText( + text = text, + style = TextStyle( + fontFamily = FontFamily.Monospace, + fontSize = 9.sp, + color = ScreenText, + lineHeight = 11.sp + ), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) + + Canvas(modifier = Modifier.matchParentSize()) { + val scanAlpha = 0.04f + var y = 0f + val step = 5f + while (y < size.height) { + drawLine( + color = Color.Black.copy(alpha = scanAlpha), + start = Offset(0f, y), + end = Offset(size.width, y), + strokeWidth = 1f + ) + y += step + } + } + } + } +} diff --git a/android/app/src/main/java/com/cbe/android/ui/theme/Color.kt b/android/app/src/main/java/com/cbe/android/ui/theme/Color.kt new file mode 100644 index 0000000..0013d37 --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/ui/theme/Color.kt @@ -0,0 +1,41 @@ +package com.cbe.android.ui.theme + +import androidx.compose.ui.graphics.Color + +// Dark theme - retro terminal vibe with amber/teal accents +val DarkBackground = Color(0xFF0D1117) +val DarkSurface = Color(0xFF161B22) +val DarkSurfaceVariant = Color(0xFF21262D) +val DarkOutline = Color(0xFF30363D) + +val Amber = Color(0xFFFFB000) +val AmberDim = Color(0xFFB8860B) +val AmberGlow = Color(0xFFFFD700) +val Teal = Color(0xFF00BFA5) +val TealDim = Color(0xFF00897B) +val Green = Color(0xFF00FF41) +val Red = Color(0xFFFF3333) +val Blue = Color(0xFF58A6FF) +val White = Color(0xFFE6EDF3) +val WhiteDim = Color(0xFF8B949E) + +// LED colors +val LedRed = Color(0xFFFF3333) +val LedGreen = Color(0xFF00FF41) +val LedAmber = Color(0xFFFFB000) +val LedBlue = Color(0xFF58A6FF) +val LedOff = Color(0xFF1A1A2E) + +// 7-segment display colors +val SegBackground = Color(0xFF0A0A0F) +val SegActive = Color(0xFFFF3333) +val SegInactive = Color(0xFF331111) + +// GPU screen colors +val ScreenBg = Color(0xFF000000) +val ScreenText = Color(0xFFC0C0C0) +val ScreenScanline = Color(0x08000000) +val ScreenBezel = Color(0xFF2A2A2A) + +// Material3 light (used only for system chrome) +val LightBackground = Color(0xFFF6F8FA) diff --git a/android/app/src/main/java/com/cbe/android/ui/theme/Theme.kt b/android/app/src/main/java/com/cbe/android/ui/theme/Theme.kt new file mode 100644 index 0000000..bb4cf82 --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/ui/theme/Theme.kt @@ -0,0 +1,52 @@ +package com.cbe.android.ui.theme + +import android.app.Activity +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Amber, + onPrimary = DarkBackground, + primaryContainer = AmberDim, + secondary = Teal, + onSecondary = DarkBackground, + secondaryContainer = TealDim, + tertiary = Green, + background = DarkBackground, + onBackground = White, + surface = DarkSurface, + onSurface = White, + surfaceVariant = DarkSurfaceVariant, + onSurfaceVariant = WhiteDim, + outline = DarkOutline, + error = Red, + onError = DarkBackground +) + +@Composable +fun CbeTheme(content: @Composable () -> Unit) { + val colorScheme = DarkColorScheme + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = DarkBackground.toArgb() + window.navigationBarColor = DarkBackground.toArgb() + WindowCompat.getInsetsController(window, view).apply { + isAppearanceLightStatusBars = false + isAppearanceLightNavigationBars = false + } + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/android/app/src/main/java/com/cbe/android/ui/theme/Type.kt b/android/app/src/main/java/com/cbe/android/ui/theme/Type.kt new file mode 100644 index 0000000..50ac586 --- /dev/null +++ b/android/app/src/main/java/com/cbe/android/ui/theme/Type.kt @@ -0,0 +1,79 @@ +package com.cbe.android.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val MonospaceFamily = FontFamily.Monospace + +val Typography = Typography( + displayLarge = TextStyle( + fontFamily = MonospaceFamily, + fontWeight = FontWeight.Bold, + fontSize = 32.sp, + lineHeight = 40.sp + ), + headlineLarge = TextStyle( + fontFamily = MonospaceFamily, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + lineHeight = 32.sp + ), + headlineMedium = TextStyle( + fontFamily = MonospaceFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + lineHeight = 28.sp + ), + titleLarge = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + lineHeight = 24.sp + ), + titleMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp + ), + bodyLarge = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp + ), + bodyMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp + ), + bodySmall = TextStyle( + fontFamily = MonospaceFamily, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp + ), + labelLarge = TextStyle( + fontFamily = MonospaceFamily, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp + ), + labelMedium = TextStyle( + fontFamily = MonospaceFamily, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp + ), + labelSmall = TextStyle( + fontFamily = MonospaceFamily, + fontWeight = FontWeight.Medium, + fontSize = 10.sp, + lineHeight = 14.sp + ) +) diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..d16a3eb --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..25bb108 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..25bb108 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..cc09442 --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #0D1117 + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..a1055d7 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,4 @@ + + +