Luau Bindings — floppy

The require("floppy") script API — real floppy disks at the flux level through a Greaseweazle: read and write flux, detect and decode formats, copy disks, format, verify, and inspect bootblocks. Everything here is [antos]. Design & native core: antos_floppy. For the everyday view — a disk as a drive letter — see filesystem.

local floppy = require("floppy")
local dev = floppy.open()
local flux = dev:read_flux{ cyl = 0, head = 0, revs = 3 }
print(floppy.detect(flux).format)      --> "amiga.dd"

All functions are AntOS additions ([antos]); failures return nil, errmsg. Cylinders and heads are zero-based throughout.


Devices & media

Function Behaviour
floppy.devices() Array of connected units: { id, port, model, firmware }.
floppy.open([id]) Open a unit → device handle. Omit id for the first.
dev:close() Release the device.
dev:info() { model, firmware, max_revs, can_write }.
dev:drive(n) Select physical drive 0/1 on the cable.
dev:media() { present, write_protected, rpm, cyls, heads }; present=false for an empty drive.
dev:on_change(fn) Callback on disk insert/remove — the hook the auto-mounter uses.
dev:motor(on) Force the motor; normally managed automatically.
dev:recalibrate() Seek to track 0 and re-establish position.
local m = dev:media()
if not m.present then print("no disk") end
if m.write_protected then print("read-only") end

Flux

The raw level: magnetic transition timings, no interpretation.

Function Behaviour
dev:read_flux(opts) Capture flux → flux object. opts: cyl, head, cyls (range), heads, revs (default 3).
dev:write_flux(flux, opts) Write flux back to disk. Refuses on write-protected media.
floppy.load_flux(path) Load a .scp / .hfe container from any drive letter.
flux:save(path) Save as .scp / .hfe — chosen by extension.
flux:tracks() Iterator over captured tracks: { cyl, head, revs, transitions }.
flux:stats() { rpm, jitter, weak_regions, unformatted } — the numbers that say why a disk is hard.

More revolutions cost time and buy confidence on marginal media. Three is the sane default; one is for a quick look; five or more is for a disk you only get one shot at.


Detect, decode, encode

Function Behaviour
floppy.formats() Array of known format ids with geometry and whether writing is supported.
floppy.detect(flux) { format, confidence, geometry }, or nil, err if nothing matches.
floppy.decode(flux [, format]) Decode to sectors. Omit format to auto-detect; pass one to override.
floppy.encode(image, format) Build flux from a decoded image, ready for write_flux.

decode always returns a status map, because a partly-readable disk is the normal case:

local img, err = floppy.decode(flux)
for _, s in ipairs(img.sectors) do
  -- s.status: "ok" | "crc" | "missing" | "weak" | "unformatted"
  if s.status ~= "ok" then
    print(("c%d h%d s%d: %s"):format(s.cyl, s.head, s.sector, s.status))
  end
end
print(img.ok_count .. "/" .. img.total .. " sectors good")

That map is what the tool's error grid is drawn from — do not throw it away by testing only whether decode succeeded.


Images

Function Behaviour
dev:read_image(path [, opts]) Read the disk and write an image — .adf, .img, .d64, .dsk, … by extension.
dev:write_image(path [, opts]) Write an image onto the disk.
floppy.image_info(path) { format, cyls, heads, sectors, bytes } without loading it all.

opts accepts format, cyls, retries, and on_progress — a callback of { cyl, head, status, done, total } per track, for a progress bar or a live grid.


Copy

The X-Copy modes, kept by name because they are the names people know. See floppy for what each meant and what it means now.

Mode Behaviour
"dos" Decode every track, re-encode to the destination.
"dos+" As "dos" with aggressive retries and read-error reporting rather than silent tolerance.
"bam" Filesystem-aware — copy only allocated blocks. Needs a filesystem reader for the format.
"nibble" Copy without interpreting. Now a flux copy, which preserves strictly more than the original nibble mode could.
floppy.copy(src_dev, dst_dev, {
  mode  = "nibble",
  cyls  = { 0, 79 },
  verify = true,
  on_progress = function(p) grid:set(p.cyl, p.head, p.status) end,
})

floppy.copy also accepts a flux object or image path as src, so "write this .adf to a disk" and "clone this disk" are the same call.


Format, erase, verify

Function Behaviour
dev:format(opts) Full format. opts: format, cyls, label, verify.
dev:qformat(opts) Quick format — filesystem structures only, existing data left in place.
dev:erase(opts) Erase tracks to unformatted.
dev:verify(opts) Read back and check without writing; returns the same status map as decode.
dev:speed([secs]) Measure drive RPM and jitter — the SPEEDCHK tool. A drive off-speed is the cause of a surprising share of "bad" disks.

Inspecting bootblocks

Function Behaviour
floppy.scan_boot(src) Scan a decoded image or flux for known bootblock viruses → nil if clean, else { name, offset, provenance }.
floppy.signatures() The loaded signature table, for display or auditing.
floppy.boot_dump(src) The bootblock as bytes, for the ASCII dump the tool shows.
dev:repair_boot() Write a clean bootblock. Checks write-protect first, and replaces rather than zeroing.

Report, then let the user decide. A signature match is not proof — a custom bootblock can collide — so the tool shows the dump and offers a choice. Nothing here modifies a disk on its own.

local hit = floppy.scan_boot(img)
if hit then
  print("possible: " .. hit.name)
  print(floppy.boot_dump(img))       -- show it, then ask
end

Mounting

Function Behaviour
dev:mount([letter]) Decode the disk and publish it as a drive letter (default: the device's own port letter, normally E:).
dev:unmount() Remove it.
dev:mounted() The letter, or nil.

After mount, the disk is an ordinary volume — use fs and io on it, and it is catalogued like anything else. This is the same device seen from above; the flux calls remain available on dev throughout.


Related

Important: The Ant64 family of home computers are at early design/prototype stage, everything you see here is subject to change.