diff --git a/atlas_os/CMC.lua b/atlas_os/CMC.lua new file mode 100644 index 0000000..2ce4433 --- /dev/null +++ b/atlas_os/CMC.lua @@ -0,0 +1,236 @@ +-- CMC.lua — Command Controller (AtlasOS Shell/CPU v2.0) +-- in[1] = cmd_rx ← IOC.out[4] (IN|command) +-- in[2] = mem_rx ← MMC.out[1] (FS response) +-- out[1] = text_tx → IOC.in[2] (OUT|text) +-- out[2] = clear_tx → IOC.in[3] (1 = clear) +-- out[3] = color_tx → IOC.in[4] (R,G,B) +-- out[4] = mem_tx → MMC.in[1] (FS request) + +local state = 0 +local ST_IDLE = 0 +local ST_WAIT_FS = 1 + +local uid, gid, cwd, curUser = 0, 0, "/", "root" + +local pendingCmd = nil +local lastRespSeq = 0 + +local function parseMsg(msg) + local p = msg:find("|") + return p and msg:sub(1, p - 1) or msg, p and msg:sub(p + 1) or nil +end + +local function printOut(t) + out[1] = "OUT|" .. tostring(t) +end + +local function printErr(t) + out[1] = "OUT|" .. tostring(t) +end + +local function doClear() + out[2] = 1 +end + +local function doColor(r, g, b) + out[3] = r .. "," .. g .. "," .. b +end + +local function fsReq(op, arg) + local msg = op + if arg then msg = msg .. "|" .. arg end + out[4] = msg +end + +local cmds = {} + +local function reg(name, fn, desc) + cmds[name] = { fn = fn, desc = desc } +end + +reg("help", function(a) + local t = "AtlasOS v2.0 commands:\n" + for n, e in pairs(cmds) do + t = t .. " " .. n .. " " .. e.desc .. "\n" + end + return t +end, "Show help") + +reg("echo", function(a) + if #a == 0 then error("echo: missing text") end + return table.concat(a, " ") +end, "Echo text") + +reg("clear", function(a) + doClear() +end, "Clear screen") + +reg("color", function(a) + if #a < 3 then error("color: need R G B") end + local r, g, b = tonumber(a[1]), tonumber(a[2]), tonumber(a[3]) + if not (r and g and b) then error("color: invalid values") end + r = math.floor(math.max(0, math.min(255, r))) + g = math.floor(math.max(0, math.min(255, g))) + b = math.floor(math.max(0, math.min(255, b))) + doColor(r, g, b) + printOut("Color set to " .. r .. "," .. g .. "," .. b) +end, "Set color R G B") + +reg("status", function(a) + return "AtlasOS v2.0 | User: " .. curUser .. "(" .. uid .. ") | CWD: " .. cwd +end, "System status") + +reg("ls", function(a) + local path = cwd + local opts = {} + for i = 1, #a do + if a[i]:sub(1, 1) == "-" then + opts[a[i]] = true + else + path = a[i] + end + end + pendingCmd = { name = "ls", path = path, opts = opts } + state = ST_WAIT_FS + fsReq("LS", path) +end, "List directory [path]") + +reg("cat", function(a) + if #a == 0 then error("cat: missing path") end + pendingCmd = { name = "cat", path = a[1] } + state = ST_WAIT_FS + fsReq("READ", a[1] .. "|0|8192") +end, "Show file content") + +reg("cd", function(a) + local path = a[1] + if not path then path = "/" end + pendingCmd = { name = "cd", path = path } + state = ST_WAIT_FS + fsReq("CHDIR", path) +end, "Change directory [path]") + +reg("stat", function(a) + if #a == 0 then error("stat: missing path") end + pendingCmd = { name = "stat", path = a[1] } + state = ST_WAIT_FS + fsReq("STAT", a[1]) +end, "Show file metadata") + +local function execCmd(cmdStr) + local parts = {} + for t in cmdStr:gmatch("%S+") do + parts[#parts + 1] = t + end + if #parts == 0 then return end + + local name = parts[1] + local args = {} + for i = 2, #parts do + args[#args + 1] = parts[i] + end + + local entry = cmds[name] + if not entry then + printErr("Unknown: " .. name .. ". Try 'help'.") + return + end + + local ok, result = pcall(entry.fn, args) + if not ok then + printErr(tostring(result)) + elseif result ~= nil then + printOut(result) + end +end + +local function handleFSResponse(op, data) + if not pendingCmd then return end + local cmd = pendingCmd + + if op == "ERR" then + local code = data or "EIO" + if code == "ENOENT" then + printErr(cmd.path .. ": No such file or directory") + elseif code == "EACCES" then + printErr(cmd.path .. ": Permission denied") + elseif code == "ENOTDIR" then + printErr(cmd.path .. ": Not a directory") + elseif code == "EISDIR" then + printErr(cmd.path .. ": Is a directory") + else + printErr(cmd.path .. ": " .. code) + end + pendingCmd = nil + state = ST_IDLE + return + end + + if cmd.name == "ls" then + local showAll = (cmd.opts["-la"] or cmd.opts["-a"]) + local names = {} + for n in data:gmatch("([^|]+)") do + names[#names + 1] = n + end + local outLines = {} + for _, n in ipairs(names) do + if n ~= "." and n ~= ".." or showAll then + outLines[#outLines + 1] = n + end + end + if #outLines == 0 then outLines[1] = "(empty)" end + printOut(table.concat(outLines, "\n")) + elseif cmd.name == "cat" then + printOut(data) + elseif cmd.name == "cd" then + cwd = data + printOut(cwd) + elseif cmd.name == "stat" then + local parts = {} + for s in data:gmatch("([^|]+)") do + parts[#parts + 1] = s + end + if #parts >= 6 then + local typ = parts[2] == "d" and "directory" or "file" + local out = "Inode: " .. parts[1] .. " Type: " .. typ .. "\n" + out = out .. "Mode: " .. parts[3] .. " UID: " .. parts[4] .. " GID: " .. parts[5] .. "\n" + out = out .. "Size: " .. parts[6] .. " Modified: " .. (parts[7] or "?") + printOut(out) + else + printOut(data) + end + end + + pendingCmd = nil + state = ST_IDLE +end + +inp = {} + +function upd() + if state == ST_IDLE then + if inp[1] ~= nil then + local op, cmd = parseMsg(tostring(inp[1])) + if op == "IN" and cmd and #cmd > 0 then + execCmd(cmd) + end + end + end + + if state == ST_WAIT_FS and inp[2] ~= nil then + local resp = tostring(inp[2]) + if #resp > 0 then + local op, rest = parseMsg(resp) + if op and rest then + local seqStr, data = parseMsg(rest) + local seq = tonumber(seqStr) or 0 + if seq > lastRespSeq then + lastRespSeq = seq + handleFSResponse(op, data) + end + end + end + end + + table.clear(inp) +end diff --git a/atlas_os/IOC.lua b/atlas_os/IOC.lua new file mode 100644 index 0000000..8fbe347 --- /dev/null +++ b/atlas_os/IOC.lua @@ -0,0 +1,39 @@ +-- IOC.lua — I/O Controller (AtlasOS Terminal v2.0) +-- out[1] = display_text → Terminal +-- out[2] = display_clear → Terminal +-- out[3] = display_color → Terminal +-- out[4] = cmd_tx → CMC.in[1] (IN|command) +-- in[1] = user_input ← Terminal +-- in[2] = text_rx ← CMC.out[1] (OUT|text) +-- in[3] = clear_rx ← CMC.out[2] (1 = clear) +-- in[4] = color_rx ← CMC.out[3] (R,G,B) + +local txActive = false + +inp = {} + +function upd() + if inp[1] ~= nil then + out[4] = "IN|" .. tostring(inp[1]) + txActive = true + elseif txActive then + out[4] = "" + txActive = false + end + + if inp[2] ~= nil then + local msg = tostring(inp[2]) + local _, arg = msg:match("^(%w+)%|(.+)$") + if arg then out[1] = arg end + end + + if inp[3] ~= nil and tonumber(inp[3]) == 1 then + out[2] = 1 + end + + if inp[4] ~= nil then + out[3] = tostring(inp[4]) + end + + table.clear(inp) +end diff --git a/atlas_os/MMC.lua b/atlas_os/MMC.lua new file mode 100644 index 0000000..23b0a1f --- /dev/null +++ b/atlas_os/MMC.lua @@ -0,0 +1,273 @@ +-- MMC.lua — Memory/FS Controller (AtlasOS v2.0) +-- in[1] = mem_rx ← CMC.out[4] (FS request) +-- in[3] = mem_read ← Memory.out[1] +-- out[1] = mem_tx → CMC.in[2] (FS response) +-- out[5] = mem_data → Memory.in[1] +-- out[6] = mem_write → Memory.in[2] + +local SEG_SIZE = 8192 +local MAX_INODES = 64 +local ROOT_INO = 1 + +local function parseMsg(msg) + local p = msg:find("|") + return p and msg:sub(1, p - 1) or msg, p and msg:sub(p + 1) or nil +end + +local function split(str, sep) + local t = {} + if not str then return t end + for s in str:gmatch("[^" .. sep .. "]+") do + t[#t + 1] = s + end + return t +end + +local inodes = {} +local dirs = {} +local files = {} +local users = {} +local sb = { seg_size = SEG_SIZE, max_inodes = MAX_INODES, root_ino = ROOT_INO } + +local function fsSerialize() + local lines = {} + lines[#lines + 1] = "V|1" + lines[#lines + 1] = "S|" .. sb.seg_size .. "|" .. sb.max_inodes .. "|" .. sb.root_ino + for _, u in pairs(users) do + lines[#lines + 1] = "U|" .. u.uid .. "|" .. u.name .. "|" .. u.gid .. "|" .. u.home .. "|" .. (u.hash or "-") + end + for i = 1, MAX_INODES do + local ino = inodes[i] + if ino then + lines[#lines + 1] = "I|" .. ino.ino .. "|" .. ino.typ .. "|" .. ino.mode .. "|" .. ino.uid .. "|" .. ino.gid .. "|" .. ino.size .. "|" .. ino.mtime .. "|" + if ino.typ == "d" and dirs[ino.ino] then + local entries = {} + for _, e in ipairs(dirs[ino.ino]) do + entries[#entries + 1] = e.name .. ":" .. e.ino + end + lines[#lines + 1] = "D|" .. ino.ino .. "|" .. table.concat(entries, "|") + elseif ino.typ == "f" and files[ino.ino] then + lines[#lines + 1] = "F|" .. ino.ino .. "|" .. files[ino.ino] + end + end + end + return table.concat(lines, "\n") +end + +local function fsDeserialize(str) + inodes = {} + dirs = {} + files = {} + users = {} + sb = { seg_size = SEG_SIZE, max_inodes = MAX_INODES, root_ino = ROOT_INO } + if not str or #str == 0 then return false end + for line in str:gmatch("[^\n]+") do + local typ = line:sub(1, 1) + local rest = line:sub(3) + if typ == "V" then + elseif typ == "S" then + local parts = split(rest, "|") + if #parts >= 3 then + sb.seg_size = tonumber(parts[1]) or SEG_SIZE + sb.max_inodes = tonumber(parts[2]) or MAX_INODES + sb.root_ino = tonumber(parts[3]) or ROOT_INO + end + elseif typ == "U" then + local parts = split(rest, "|") + if #parts >= 4 then + users[tonumber(parts[1])] = { uid = tonumber(parts[1]), name = parts[2], gid = tonumber(parts[3]), home = parts[4], hash = parts[5] or "-" } + end + elseif typ == "I" then + local parts = split(rest, "|") + if #parts >= 7 then + local ino = tonumber(parts[1]) + inodes[ino] = { ino = ino, typ = parts[2], mode = parts[3], uid = tonumber(parts[4]), gid = tonumber(parts[5]), size = tonumber(parts[6]), mtime = tonumber(parts[7]) or 0 } + end + elseif typ == "D" then + local parts = split(rest, "|") + if #parts >= 2 then + local ino = tonumber(parts[1]) + local entries = {} + for i = 2, #parts do + local eparts = split(parts[i], ":") + if #eparts >= 2 then + entries[#entries + 1] = { name = eparts[1], ino = tonumber(eparts[2]) } + end + end + dirs[ino] = entries + end + elseif typ == "F" then + local parts = split(rest, "|") + if #parts >= 2 then + local ino = tonumber(parts[1]) + local content = parts[2] or "" + for i = 3, #parts do + content = content .. "|" .. parts[i] + end + files[ino] = content + end + end + end + return next(inodes) ~= nil +end + +local function findInodeByPath(path) + if not path or #path == 0 then path = "/" end + local parts = split(path, "/") + local cur = inodes[sb.root_ino] + if not cur then return nil end + if path == "/" then return cur end + local curIno = sb.root_ino + for i = 1, #parts do + local name = parts[i] + if #name > 0 then + local found = false + local dentries = dirs[curIno] + if dentries then + for _, e in ipairs(dentries) do + if e.name == name then + curIno = e.ino + cur = inodes[curIno] + found = true + break + end + end + end + if not found then return nil end + end + end + return cur and inodes[curIno] or nil +end + +local respSeq = 0 + +local function processReq(req) + local op, rest = parseMsg(req) + if not op then return "ERR|EINVAL" end + respSeq = respSeq + 1 + local seqStr = tostring(respSeq) .. "|" + + if op == "STAT" then + local path = rest or "/" + local ino = findInodeByPath(path) + if not ino then return "ERR|" .. seqStr .. "ENOENT" end + return "OK|" .. seqStr .. ino.ino .. "|" .. ino.typ .. "|" .. ino.mode .. "|" .. ino.uid .. "|" .. ino.gid .. "|" .. ino.size .. "|" .. ino.mtime + + elseif op == "READ" then + local parts = split(rest or "", "|") + local path = parts[1] or "" + local offset = tonumber(parts[2]) or 0 + local len = tonumber(parts[3]) or 8192 + local ino = findInodeByPath(path) + if not ino then return "ERR|" .. seqStr .. "ENOENT" end + if ino.typ ~= "f" then return "ERR|" .. seqStr .. "EISDIR" end + local content = files[ino.ino] or "" + local data = content:sub(offset + 1, offset + len) + return "OK|" .. seqStr .. data + + elseif op == "LS" then + local path = rest or "/" + local ino = findInodeByPath(path) + if not ino then return "ERR|" .. seqStr .. "ENOENT" end + if ino.typ ~= "d" then return "ERR|" .. seqStr .. "ENOTDIR" end + local dentries = dirs[ino.ino] + if not dentries then return "OK|" .. seqStr end + local names = {} + for _, e in ipairs(dentries) do + names[#names + 1] = e.name + end + return "OK|" .. seqStr .. table.concat(names, "|") + + elseif op == "CHDIR" then + local path = rest or "/" + local ino = findInodeByPath(path) + if not ino then return "ERR|" .. seqStr .. "ENOENT" end + if ino.typ ~= "d" then return "ERR|" .. seqStr .. "ENOTDIR" end + return "OK|" .. seqStr .. path + + elseif op == "WRITE" then + return "ERR|" .. seqStr .. "EROFS" + end + + return "ERR|" .. seqStr .. "EINVAL" +end + +local function defaultFS() + inodes = {} + dirs = {} + files = {} + users = {} + + users[0] = { uid = 0, name = "root", gid = 0, home = "/", hash = "-" } + users[100] = { uid = 100, name = "captain", gid = 10, home = "/home/captain", hash = "-" } + + local function mkIno(num, typ, mode, uid, gid, size) + inodes[num] = { ino = num, typ = typ, mode = mode, uid = uid, gid = gid, size = size or 0, mtime = 0 } + end + + local function mkDir(num, entries) + dirs[num] = entries + end + + local function mkFile(num, content) + files[num] = content or "" + local sz = #(content or "") + if inodes[num] then inodes[num].size = sz end + end + + mkIno(1, "d", "755", 0, 0, 0) + mkDir(1, { { name = ".", ino = 1 }, { name = "..", ino = 1 }, { name = "home", ino = 2 }, { name = "etc", ino = 4 } }) + + mkIno(2, "d", "755", 0, 0, 0) + mkDir(2, { { name = ".", ino = 2 }, { name = "..", ino = 1 }, { name = "captain", ino = 5 } }) + + mkIno(5, "d", "700", 100, 10, 0) + mkDir(5, { { name = ".", ino = 5 }, { name = "..", ino = 2 }, { name = "readme.txt", ino = 3 } }) + + mkIno(3, "f", "644", 100, 10, 0) + mkFile(3, "Welcome to AtlasOS v2.0!\n") + + mkIno(4, "d", "755", 0, 0, 0) + mkDir(4, { { name = ".", ino = 4 }, { name = "..", ino = 1 } }) + + local str = fsSerialize() + out[5] = str + out[6] = 1 +end + +local fsLoaded = false +local prevReq = "" +local writePending = false + +inp = {} + +function upd() + if not fsLoaded then + local memStr = inp[3] or "" + if #memStr > 0 and fsDeserialize(memStr) then + fsLoaded = true + else + fsLoaded = true + defaultFS() + writePending = true + end + end + + if writePending then + local str = fsSerialize() + out[5] = str + out[6] = 1 + writePending = false + end + + if inp[1] ~= nil then + local req = tostring(inp[1]) + if #req > 0 and req ~= prevReq then + prevReq = req + local resp = processReq(req) + out[1] = resp + end + end + + table.clear(inp) +end diff --git a/atlas_os/docs/wiring.md b/atlas_os/docs/wiring.md new file mode 100644 index 0000000..aaa0ed8 --- /dev/null +++ b/atlas_os/docs/wiring.md @@ -0,0 +1,152 @@ +# Схема соединений AtlasOS v2.0 + +## Компоненты + +| Компонент | Кол-во | Назначение | +|---|---|---| +| MicroLua (IOC) | 1 | I/O Controller — терминал | +| MicroLua (CMC) | 1 | Command Controller — shell | +| MicroLua (MMC) | 1 | Memory/FS Controller — ФС | +| Terminal | 1 | текстовый дисплей | +| Memory | 1 | хранилище 8192 символа | + +## Полная схема + +``` + Терминал (Keyboard/Button) Терминал (Display) + │ text ▲ + ▼ │ text + ┌──────────┐ ┌──────────┐ │ clear + │ │─────────▶ │──────┤ color + │ IOC │ IN|cmd │ CMC │ │ + │ │◀────────│ │◀─────┤ + └──────────┘ OUT|text│ │ 1 │ + ▲ clear=1 │ │◀─────┤ + │ R,G,B └────▲────┘ R,G,B│ + │ │ │ + │ LS|path │ + │ READ|... │ + │ │ │ + │ ┌────▼────┐ │ + │ │ │ │ + │ │ MMC │ │ + │ │ │ │ + │ └──┬──┬───┘ │ + │ │ │ │ + │ data │ │ write(1) │ + │ ▼ ▼ │ + │ ┌────────┐ │ + │ │ Memory │ │ + │ │ 8192 │ │ + │ └───┬────┘ │ + │ │ │ + └──────────────────┴────────────┘ +``` + +## Соединения проводом + +### IOC ↔ Терминал + +| Пин IOC | → / ← | Пин Terminal | Назначение | +|---|---|---|---| +| IOC.out[1] | → | Display.in (text) | текст на экран | +| IOC.out[2] | → | Display.in (clear) | очистка (1) | +| IOC.out[3] | → | Display.in (color) | цвет R,G,B | +| IOC.in[1] | ← | Keyboard.out (text) | ввод пользователя | + +### IOC ↔ CMC + +| Пин IOC | → / ← | Пин CMC | Назначение | +|---|---|---|---| +| IOC.out[4] | → | CMC.in[1] | команда `IN|cmd` | +| CMC.out[1] | → | IOC.in[2] | текст `OUT|text` | +| CMC.out[2] | → | IOC.in[3] | очистка (1) | +| CMC.out[3] | → | IOC.in[4] | цвет `R,G,B` | + +### CMC ↔ MMC + +| Пин CMC | → / ← | Пин MMC | Назначение | +|---|---|---|---| +| CMC.out[4] | → | MMC.in[1] | запрос `OP|args` | +| MMC.out[1] | → | CMC.in[2] | ответ `OK|seq|...` | + +### MMC ↔ Memory + +| Пин MMC | → / ← | Пин Memory | Назначение | +|---|---|---|---| +| MMC.out[5] | → | Memory.in[1] | данные для записи | +| MMC.out[6] | → | Memory.in[2] | сигнал записи (1) | +| Memory.out[1] | → | MMC.in[3] | чтение данных | + +## Протокол шины + +### IOC → CMC (CMD) + +``` +IN|команда +``` + +### CMC → IOC (CMD) + +``` +OUT|текст — вывести текст +1 на out[2] — очистить экран +R,G,B на out[3] — установить цвет +``` + +### CMC → MMC (MEM) + +``` +STAT|путь — метаданные +READ|путь|смещение|длина — чтение файла +LS|путь — список каталога +CHDIR|путь — смена директории (проверка) +WRITE|путь|смещение|данные — запись (пока EROFS) +``` + +### MMC → CMC (MEM) + +``` +OK|seq|данные +ERR|seq|код_ошибки +``` + +Коды ошибок: `ENOENT`, `EACCES`, `ENOTDIR`, `EISDIR`, `EROFS`, `EINVAL` + +## Последовательность запуска + +1. Загрузить код MMC.lua в контроллер MMC +2. Загрузить код CMC.lua в контроллер CMC +3. Загрузить код IOC.lua в контроллер IOC +4. Соединить провода по схеме выше +5. При первом запуске MMC инициализирует пустую ФС и запишет в Memory +6. Подать сигнал на Terminal — появится приглашение + +## Пример сессии + +``` +> ls +home +etc +> ls /home/captain +readme.txt +> cat /home/captain/readme.txt +Welcome to AtlasOS v2.0! +> cd /home/captain +/home/captain +> status +AtlasOS v2.0 | User: root(0) | CWD: /home/captain +> color 0 255 128 +Color set to 0,255,128 +> help +AtlasOS v2.0 commands: + help Show help + echo Echo text + clear Clear screen + color Set color R G B + status System status + ls List directory [path] + cat Show file content + cd Change directory [path] + stat Show file metadata +```