diff --git a/AquaCubeIT.Service.NetFloppy/Fat16Disk.cs b/AquaCubeIT.Service.NetFloppy/Fat16Disk.cs new file mode 100644 index 0000000..223a87b --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/Fat16Disk.cs @@ -0,0 +1,335 @@ +// Fat16ImageLib.cs +// Minimal C# library for creating a FAT16 disk image and adding files (root directory only) +// Target: .NET 6+ +// Limitations: 8.3 filenames, root directory only (no subdirectories), no long file names, no deletions/compaction. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Fat16Lib +{ + public class Fat16ImageOptions + { + // Total size of the image in bytes. Common sizes: 16 MiB, 32 MiB, 64 MiB, 128 MiB, 256 MiB, etc. + public long TotalSizeBytes { get; set; } = 32L * 1024 * 1024; // 32 MiB default + public ushort BytesPerSector { get; set; } = 512; + public byte SectorsPerCluster { get; set; } = 0; // 0 = auto-select based on size + public byte NumFats { get; set; } = 2; + public ushort ReservedSectors { get; set; } = 1; // boot sector + public ushort MaxRootDirEntries { get; set; } = 512; // typical for FAT16 + public byte MediaDescriptor { get; set; } = 0xF8; // fixed disk + public ushort SectorsPerTrack { get; set; } = 32; // geometry hints (not critical) + public ushort NumberOfHeads { get; set; } = 64; // geometry hints (not critical) + public uint HiddenSectors { get; set; } = 0; + public string OemId { get; set; } = "MSDOS5.0"; + public string VolumeLabel { get; set; } = "NO NAME "; // exactly 11 chars; will be padded/trimmed + public string FsTypeLabel { get; set; } = "FAT16 "; // 8 chars in boot sector label + public ushort? VolumeId { get; set; } = null; // null => random + } + + public class Fat16Image + { + private readonly Fat16ImageOptions _opt; + + // Computed layout + private uint _totalSectors; // 16-bit or 32-bit depending on size + private ushort _rootDirSectors; + private ushort _fatSectors; // sectors per FAT + private uint _firstFatSectorLba; + private uint _firstRootDirSectorLba; + private uint _firstDataSectorLba; + private uint _clusterCount; // data clusters (not counting 0 and 1) + private byte _sectorsPerCluster; + + // Runtime buffers + private byte[] _bootSector = Array.Empty(); + private byte[] _fat; // a single FAT table; will be duplicated NumFats times + private byte[] _rootDir; // root directory area (fixed size) + private byte[] _data; // data region sized to contain all clusters (clusterCount * spc * bps) + + // Allocation state + private uint _nextFreeCluster = 2; // FAT clusters start at 2 + private int _nextFreeRootDirEntry = 0; + + public Fat16Image(Fat16ImageOptions options) + { + _opt = options; + if (_opt.BytesPerSector != 512) + throw new NotSupportedException("Only 512-byte sectors supported in this minimal implementation."); + if (_opt.TotalSizeBytes % _opt.BytesPerSector != 0) + throw new ArgumentException("TotalSizeBytes must be a multiple of BytesPerSector."); + + InitializeLayout(); + BuildBootSector(); + InitializeFatAndRoot(); + } + + // Adds a file into the root directory. Uses 8.3 name rules. + public void AddFile(string fileName, byte[] content, DateTime? timestamp = null) + { + if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("fileName"); + if (content == null) throw new ArgumentNullException(nameof(content)); + + var (name83, ext83) = ToShortName(fileName); + if (_nextFreeRootDirEntry >= _opt.MaxRootDirEntries) + throw new IOException("Root directory is full."); + + // Compute how many clusters needed + uint bytesPerCluster = (uint)(_opt.BytesPerSector * _sectorsPerCluster); + uint clustersNeeded = (uint)((content.Length + bytesPerCluster - 1) / bytesPerCluster); + if (clustersNeeded == 0) clustersNeeded = 1; // zero-length file occupies 0 clusters in FAT16 spec? Practically some tools allow 0. We'll allocate 1 for simplicity. + + if ((_nextFreeCluster - 2) + clustersNeeded > _clusterCount) + throw new IOException("Not enough free space for file."); + + // Allocate cluster chain + List chain = new(); + for (int i = 0; i < clustersNeeded; i++) + { + uint c = _nextFreeCluster++; + chain.Add(c); + } + + // Write data to clusters + int srcOffset = 0; + foreach (var c in chain) + { + uint lba = ClusterToLba(c); + uint offset = (lba - _firstDataSectorLba) * _opt.BytesPerSector; + int toCopy = (int)Math.Min(bytesPerCluster, (uint)(content.Length - srcOffset)); + if (toCopy > 0) + Buffer.BlockCopy(content, srcOffset, _data, (int)offset, toCopy); + // Zero the remainder of the cluster (already zeroed by default construction) + srcOffset += toCopy; + } + + // Link FAT entries + for (int i = 0; i < chain.Count - 1; i++) + SetFatEntry(chain[i], (ushort)chain[i + 1]); + SetFatEntry(chain[^1], 0xFFFF); // EOF + + // Build directory entry + var dt = timestamp ?? DateTime.Now; + ushort time = ToFatTime(dt); + ushort date = ToFatDate(dt); + + Span entry = stackalloc byte[32]; + entry.Clear(); + Encoding.ASCII.GetBytes(name83, entry); + Encoding.ASCII.GetBytes(ext83, entry.Slice(8)); + entry[11] = 0x20; // Archive + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(14), time); // Create time + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(16), date); // Create date + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(18), date); // Last access date (approx) + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(22), time); // Write time + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(24), date); // Write date + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(26), (ushort)chain[0]); // First cluster + BinaryPrimitives.WriteUInt32LittleEndian(entry.Slice(28), (uint)content.Length); // File size + + // Write entry into root dir + int rootOffset = _nextFreeRootDirEntry * 32; + _rootDir.AsSpan(rootOffset, 32).Clear(); + entry.CopyTo(_rootDir.AsSpan(rootOffset)); + _nextFreeRootDirEntry++; + } + + public byte[] GetImage() + { + using var ms = new MemoryStream((int)(_opt.TotalSizeBytes)); + + // Boot sector + ms.Write(_bootSector, 0, _bootSector.Length); + + // FATs + for (int f = 0; f < _opt.NumFats; f++) + ms.Write(_fat, 0, _fat.Length); + + // Root directory + ms.Write(_rootDir, 0, _rootDir.Length); + + // Data region + ms.Write(_data, 0, _data.Length); + + // Pad to exact size if necessary (should be exact) + if (ms.Length < _opt.TotalSizeBytes) + ms.Write(new byte[_opt.TotalSizeBytes - ms.Length], 0, (int)(_opt.TotalSizeBytes - ms.Length)); + + return ms.ToArray(); + } + + public void SaveToFile(string path) + { + File.WriteAllBytes(path, GetImage()); + } + + private void InitializeLayout() + { + _totalSectors = (uint)(_opt.TotalSizeBytes / _opt.BytesPerSector); + + // Decide sectors per cluster if not specified, per rough guidance for FAT16 + _sectorsPerCluster = _opt.SectorsPerCluster != 0 ? _opt.SectorsPerCluster : SelectSectorsPerCluster(_totalSectors, _opt.BytesPerSector); + + _rootDirSectors = (ushort)(((uint)_opt.MaxRootDirEntries * 32 + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector); + + // We need to iteratively solve for FAT size and cluster count + ushort fatSectors = 1; + while (true) + { + uint dataSectors = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * fatSectors) - _rootDirSectors; + uint clusterCount = dataSectors / _sectorsPerCluster; + if (clusterCount < 4085 || clusterCount >= 65525) + throw new NotSupportedException($"Cluster count {clusterCount} out of FAT16 bounds (needs 4,085..65,524). Adjust size or sectors/cluster."); + + // FAT16 entry size 2 bytes, need (clusterCount + 2) entries + ushort requiredFatSectors = (ushort)(((clusterCount + 2) * 2 + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector); + if (requiredFatSectors == fatSectors) + { + _fatSectors = fatSectors; + _clusterCount = clusterCount; + break; + } + fatSectors = requiredFatSectors; + } + + _firstFatSectorLba = _opt.ReservedSectors; + _firstRootDirSectorLba = _firstFatSectorLba + (uint)(_opt.NumFats * _fatSectors); + _firstDataSectorLba = _firstRootDirSectorLba + _rootDirSectors; + + // Buffers + _bootSector = new byte[_opt.BytesPerSector]; + _fat = new byte[_fatSectors * _opt.BytesPerSector]; + _rootDir = new byte[_rootDirSectors * _opt.BytesPerSector]; + uint dataSectorsFinal = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * _fatSectors) - _rootDirSectors; + _data = new byte[dataSectorsFinal * _opt.BytesPerSector]; + } + + private void BuildBootSector() + { + Span bs = _bootSector; + bs.Clear(); + + // Jump + OEM + bs[0] = 0xEB; bs[1] = 0x3C; bs[2] = 0x90; // JMP short + Encoding.ASCII.GetBytes((_opt.OemId ?? "MSDOS5.0").PadRight(8).Substring(0,8), bs.Slice(3, 8)); + + // BIOS Parameter Block (BPB) + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(11), _opt.BytesPerSector); + bs[13] = _sectorsPerCluster; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(14), _opt.ReservedSectors); + bs[16] = _opt.NumFats; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(17), _opt.MaxRootDirEntries); + ushort totalSectors16 = _totalSectors <= 0xFFFF ? (ushort)_totalSectors : (ushort)0; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(19), totalSectors16); + bs[21] = _opt.MediaDescriptor; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(22), _fatSectors); + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(24), _opt.SectorsPerTrack); + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(26), _opt.NumberOfHeads); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(28), _opt.HiddenSectors); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(32), _totalSectors); + + // Extended BPB for FAT16 + ushort volId = _opt.VolumeId ?? (ushort)Random.Shared.Next(1, 0xFFFF); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(39), volId); + string label = (_opt.VolumeLabel ?? "NO NAME").ToUpperInvariant(); + label = new string(label.Where(ch => ch >= 0x20 && ch <= 0x7E).ToArray()); + label = label.PadRight(11).Substring(0, 11); + Encoding.ASCII.GetBytes(label, bs.Slice(43, 11)); + string fstype = (_opt.FsTypeLabel ?? "FAT16").PadRight(8).Substring(0, 8); + Encoding.ASCII.GetBytes(fstype, bs.Slice(54, 8)); + + // Minimal bootstrap code area (ignored for data disks) + bs[_opt.BytesPerSector - 2] = 0x55; // signature + bs[_opt.BytesPerSector - 1] = 0xAA; + } + + private void InitializeFatAndRoot() + { + // FAT initial entries + // Entry 0: media descriptor in low byte, 0xFF in high byte + SetFatEntryRaw(0, (ushort)(_opt.MediaDescriptor | 0xFF00)); + // Entry 1: EOF + SetFatEntryRaw(1, 0xFFFF); + // Zero rest (already zero) + } + + private uint ClusterToLba(uint cluster) + { + if (cluster < 2) throw new ArgumentOutOfRangeException(nameof(cluster)); + return _firstDataSectorLba + (cluster - 2) * _sectorsPerCluster; + } + + private void SetFatEntry(uint cluster, ushort value) + { + if (cluster >= _clusterCount + 2) throw new ArgumentOutOfRangeException(nameof(cluster)); + SetFatEntryRaw(cluster, value); + } + + private void SetFatEntryRaw(uint cluster, ushort value) + { + int index = (int)(cluster * 2); + BinaryPrimitives.WriteUInt16LittleEndian(_fat.AsSpan(index, 2), value); + } + + private static (string name, string ext) ToShortName(string fileName) + { + // Extract base and extension, sanitize to 8.3 uppercase ASCII + string name = Path.GetFileNameWithoutExtension(fileName) ?? "FILE"; + string ext = Path.GetExtension(fileName); + if (ext.StartsWith('.')) ext = ext.Substring(1); + + string Sanitize(string s) => new string(s.ToUpperInvariant() + .Select(c => (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "!#$%&'()-@^_`{}~".Contains(c) ? c : '_') + .ToArray()); + + string n = Sanitize(name); + string e = Sanitize(ext); + if (n.Length > 8) n = n.Substring(0, 8); + if (e.Length > 3) e = e.Substring(0, 3); + n = n.PadRight(8); + e = e.PadRight(3); + return (n, e); + } + + private static ushort ToFatDate(DateTime dt) + { + int year = Math.Clamp(dt.Year - 1980, 0, 127); + int month = Math.Clamp(dt.Month, 1, 12); + int day = Math.Clamp(dt.Day, 1, 31); + return (ushort)((year << 9) | (month << 5) | day); + } + + private static ushort ToFatTime(DateTime dt) + { + int hour = Math.Clamp(dt.Hour, 0, 23); + int minute = Math.Clamp(dt.Minute, 0, 59); + int second = Math.Clamp(dt.Second / 2, 0, 29); // 2-second resolution + return (ushort)((hour << 11) | (minute << 5) | second); + } + + private static byte SelectSectorsPerCluster(uint totalSectors, ushort bytesPerSector) + { + // Very rough mapping consistent with FAT16 recommendations for 512-byte sectors + // Values chosen to keep cluster count in FAT16 range and cluster size reasonable + long sizeMiB = (long)totalSectors * bytesPerSector / (1024 * 1024); + return sizeMiB switch + { + <= 16 => 2, // 1 KiB clusters + <= 32 => 4, // 2 KiB + <= 64 => 8, // 4 KiB + <= 128 => 16, // 8 KiB + <= 256 => 32, // 16 KiB + <= 512 => 64, // 32 KiB + _ => 64, // 32 KiB clusters for larger images in this minimal impl + }; + } + } +} + +// --------- Example usage (not part of the library) --------- +// var img = new Fat16Lib.Fat16Image(new Fat16Lib.Fat16ImageOptions { TotalSizeBytes = 32L * 1024 * 1024, VolumeLabel = "MYDISK" }); +// img.AddFile("HELLO.TXT", Encoding.ASCII.GetBytes("Hello FAT16!\r\n")); +// img.SaveToFile("fat16.img"); diff --git a/AquaCubeIT.Service.NetFloppy/fat_16_image_lib.cs b/AquaCubeIT.Service.NetFloppy/fat_16_image_lib.cs new file mode 100644 index 0000000..afeda14 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/fat_16_image_lib.cs @@ -0,0 +1,342 @@ +// Fat12ImageLib.cs +// Minimal C# library for creating a FAT12 disk image and adding files (root directory only) +// Target: .NET 6+ +// Limitations: 8.3 filenames, root directory only (no subdirectories), no long file names, no deletions/compaction. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Fat12Lib +{ + public class Fat12ImageOptions + { + // Total size of the image in bytes. Common sizes: 360 KiB, 720 KiB, 1.2 MiB, 1.44 MiB, 2.88 MiB, etc. + public long TotalSizeBytes { get; set; } = 1440L * 1024; // 1.44 MiB default + public ushort BytesPerSector { get; set; } = 512; + public byte SectorsPerCluster { get; set; } = 0; // 0 = auto-select based on size + public byte NumFats { get; set; } = 2; + public ushort ReservedSectors { get; set; } = 1; // boot sector + public ushort MaxRootDirEntries { get; set; } = 224; // typical for 1.44MB floppy + public byte MediaDescriptor { get; set; } = 0xF0; // floppy + public ushort SectorsPerTrack { get; set; } = 18; // 1.44MB geometry (not critical) + public ushort NumberOfHeads { get; set; } = 2; // geometry (not critical) + public uint HiddenSectors { get; set; } = 0; + public string OemId { get; set; } = "MSDOS5.0"; + public string VolumeLabel { get; set; } = "NO NAME "; // exactly 11 chars; will be padded/trimmed + public string FsTypeLabel { get; set; } = "FAT12 "; // 8 chars in boot sector label + public ushort? VolumeId { get; set; } = null; // null => random + } + + public class Fat12Image + { + private readonly Fat12ImageOptions _opt; + + // Computed layout + private uint _totalSectors; // 16-bit or 32-bit depending on size + private ushort _rootDirSectors; + private ushort _fatSectors; // sectors per FAT + private uint _firstFatSectorLba; + private uint _firstRootDirSectorLba; + private uint _firstDataSectorLba; + private uint _clusterCount; // data clusters (not counting 0 and 1) + private byte _sectorsPerCluster; + + // Runtime buffers + private byte[] _bootSector = Array.Empty(); + private byte[] _fat; // a single FAT table; will be duplicated NumFats times + private byte[] _rootDir; // root directory area (fixed size) + private byte[] _data; // data region sized to contain all clusters (clusterCount * spc * bps) + + // Allocation state + private uint _nextFreeCluster = 2; // FAT clusters start at 2 + private int _nextFreeRootDirEntry = 0; + + public Fat12Image(Fat12ImageOptions options) + { + _opt = options; + if (_opt.BytesPerSector != 512) + throw new NotSupportedException("Only 512-byte sectors supported in this minimal implementation."); + if (_opt.TotalSizeBytes % _opt.BytesPerSector != 0) + throw new ArgumentException("TotalSizeBytes must be a multiple of BytesPerSector."); + + InitializeLayout(); + BuildBootSector(); + InitializeFatAndRoot(); + } + + // Adds a file into the root directory. Uses 8.3 name rules. + public void AddFile(string fileName, byte[] content, DateTime? timestamp = null) + { + if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("fileName"); + if (content == null) throw new ArgumentNullException(nameof(content)); + + var (name83, ext83) = ToShortName(fileName); + if (_nextFreeRootDirEntry >= _opt.MaxRootDirEntries) + throw new IOException("Root directory is full."); + + // Compute how many clusters needed + uint bytesPerCluster = (uint)(_opt.BytesPerSector * _sectorsPerCluster); + uint clustersNeeded = (uint)((content.Length + bytesPerCluster - 1) / bytesPerCluster); + if (clustersNeeded == 0) clustersNeeded = 1; // allocate 1 cluster to store 0-byte file metadata/simple handling + + if ((_nextFreeCluster - 2) + clustersNeeded > _clusterCount) + throw new IOException("Not enough free space for file."); + + // Allocate cluster chain + List chain = new(); + for (int i = 0; i < clustersNeeded; i++) + { + uint c = _nextFreeCluster++; + chain.Add(c); + } + + // Write data to clusters + int srcOffset = 0; + foreach (var c in chain) + { + uint lba = ClusterToLba(c); + uint offset = (lba - _firstDataSectorLba) * _opt.BytesPerSector; + int toCopy = (int)Math.Min(bytesPerCluster, (uint)(content.Length - srcOffset)); + if (toCopy > 0) + Buffer.BlockCopy(content, srcOffset, _data, (int)offset, toCopy); + srcOffset += toCopy; + } + + // Link FAT entries (12-bit values) + for (int i = 0; i < chain.Count - 1; i++) + SetFat12Entry(chain[i], (ushort)chain[i + 1]); + SetFat12Entry(chain[^1], 0xFFF); // EOF for FAT12 + + // Build directory entry + var dt = timestamp ?? DateTime.Now; + ushort time = ToFatTime(dt); + ushort date = ToFatDate(dt); + + Span entry = stackalloc byte[32]; + entry.Clear(); + Encoding.ASCII.GetBytes(name83, entry); + Encoding.ASCII.GetBytes(ext83, entry.Slice(8)); + entry[11] = 0x20; // Archive + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(14), time); // Create time + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(16), date); // Create date + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(18), date); // Last access date (approx) + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(22), time); // Write time + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(24), date); // Write date + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(26), (ushort)chain[0]); // First cluster + BinaryPrimitives.WriteUInt32LittleEndian(entry.Slice(28), (uint)content.Length); // File size + + // Write entry into root dir + int rootOffset = _nextFreeRootDirEntry * 32; + _rootDir.AsSpan(rootOffset, 32).Clear(); + entry.CopyTo(_rootDir.AsSpan(rootOffset)); + _nextFreeRootDirEntry++; + } + + public byte[] GetImage() + { + using var ms = new MemoryStream((int)(_opt.TotalSizeBytes)); + + // Boot sector + ms.Write(_bootSector, 0, _bootSector.Length); + + // FATs + for (int f = 0; f < _opt.NumFats; f++) + ms.Write(_fat, 0, _fat.Length); + + // Root directory + ms.Write(_rootDir, 0, _rootDir.Length); + + // Data region + ms.Write(_data, 0, _data.Length); + + // Pad to exact size if necessary (should be exact) + if (ms.Length < _opt.TotalSizeBytes) + ms.Write(new byte[_opt.TotalSizeBytes - ms.Length], 0, (int)(_opt.TotalSizeBytes - ms.Length)); + + return ms.ToArray(); + } + + public void SaveToFile(string path) + { + File.WriteAllBytes(path, GetImage()); + } + + private void InitializeLayout() + { + _totalSectors = (uint)(_opt.TotalSizeBytes / _opt.BytesPerSector); + + // Decide sectors per cluster if not specified + _sectorsPerCluster = _opt.SectorsPerCluster != 0 ? _opt.SectorsPerCluster : SelectSectorsPerCluster(_totalSectors, _opt.BytesPerSector); + + _rootDirSectors = (ushort)(((uint)_opt.MaxRootDirEntries * 32 + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector); + + // Iteratively solve for FAT size and cluster count (FAT12 uses 12-bit entries => 1.5 bytes per entry) + ushort fatSectors = 1; + while (true) + { + uint dataSectors = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * fatSectors) - _rootDirSectors; + uint clusterCount = dataSectors / _sectorsPerCluster; + if (clusterCount < 1 || clusterCount > 4084) + throw new NotSupportedException($"Cluster count {clusterCount} out of FAT12 bounds (needs 1..4,084). Adjust size or sectors/cluster."); + + // FAT12 entries: (clusterCount + 2) entries, each 12 bits => total bytes = ceil(3 * entries / 2) + uint entries = clusterCount + 2; + uint fatBytes = (entries * 3 + 1) / 2; // ceil(1.5 * entries) + ushort requiredFatSectors = (ushort)((fatBytes + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector); + if (requiredFatSectors == fatSectors) + { + _fatSectors = fatSectors; + _clusterCount = clusterCount; + break; + } + fatSectors = requiredFatSectors; + } + + _firstFatSectorLba = _opt.ReservedSectors; + _firstRootDirSectorLba = _firstFatSectorLba + (uint)(_opt.NumFats * _fatSectors); + _firstDataSectorLba = _firstRootDirSectorLba + _rootDirSectors; + + // Buffers + _bootSector = new byte[_opt.BytesPerSector]; + _fat = new byte[_fatSectors * _opt.BytesPerSector]; + _rootDir = new byte[_rootDirSectors * _opt.BytesPerSector]; + uint dataSectorsFinal = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * _fatSectors) - _rootDirSectors; + _data = new byte[dataSectorsFinal * _opt.BytesPerSector]; + } + + private void BuildBootSector() + { + Span bs = _bootSector; + bs.Clear(); + + // Jump + OEM + bs[0] = 0xEB; bs[1] = 0x3C; bs[2] = 0x90; // JMP short + Encoding.ASCII.GetBytes((_opt.OemId ?? "MSDOS5.0").PadRight(8).Substring(0,8), bs.Slice(3, 8)); + + // BIOS Parameter Block (BPB) + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(11), _opt.BytesPerSector); + bs[13] = _sectorsPerCluster; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(14), _opt.ReservedSectors); + bs[16] = _opt.NumFats; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(17), _opt.MaxRootDirEntries); + ushort totalSectors16 = _totalSectors <= 0xFFFF ? (ushort)_totalSectors : (ushort)0; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(19), totalSectors16); + bs[21] = _opt.MediaDescriptor; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(22), _fatSectors); + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(24), _opt.SectorsPerTrack); + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(26), _opt.NumberOfHeads); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(28), _opt.HiddenSectors); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(32), _totalSectors); + + // Extended BPB for FAT12/FAT16 + ushort volId = _opt.VolumeId ?? (ushort)Random.Shared.Next(1, 0xFFFF); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(39), volId); + string label = (_opt.VolumeLabel ?? "NO NAME").ToUpperInvariant(); + label = new string(label.Where(ch => ch >= 0x20 && ch <= 0x7E).ToArray()); + label = label.PadRight(11).Substring(0, 11); + Encoding.ASCII.GetBytes(label, bs.Slice(43, 11)); + string fstype = (_opt.FsTypeLabel ?? "FAT12").PadRight(8).Substring(0, 8); + Encoding.ASCII.GetBytes(fstype, bs.Slice(54, 8)); + + // Minimal bootstrap code area (ignored for data disks) + bs[_opt.BytesPerSector - 2] = 0x55; // signature + bs[_opt.BytesPerSector - 1] = 0xAA; + } + + private void InitializeFatAndRoot() + { + // FAT initial entries for FAT12 + // First three bytes: media descriptor and end markers -> usually F0 FF FF for 0xF0 media + Array.Clear(_fat, 0, _fat.Length); + _fat[0] = _opt.MediaDescriptor; // e.g., 0xF0 + _fat[1] = 0xFF; + _fat[2] = 0xFF; // cluster 1 reserved/end + } + + private uint ClusterToLba(uint cluster) + { + if (cluster < 2) throw new ArgumentOutOfRangeException(nameof(cluster)); + return _firstDataSectorLba + (cluster - 2) * _sectorsPerCluster; + } + + private void SetFat12Entry(uint cluster, ushort value) + { + if (cluster >= _clusterCount + 2) throw new ArgumentOutOfRangeException(nameof(cluster)); + // value is 12-bit + value &= 0x0FFF; + int index = (int)(cluster + (cluster / 2)); // floor(1.5 * cluster) + if ((cluster & 1) == 0) + { + // even cluster: low 12 bits starting at index + _fat[index] = (byte)((_fat[index] & 0x00) | (value & 0xFF)); + _fat[index + 1] = (byte)((_fat[index + 1] & 0xF0) | ((value >> 8) & 0x0F)); + } + else + { + // odd cluster: high 12 bits overlapping starting at index + _fat[index] = (byte)((_fat[index] & 0x0F) | ((value << 4) & 0xF0)); + _fat[index + 1] = (byte)((_fat[index + 1] & 0x00) | ((value >> 4) & 0xFF)); + } + } + + private static (string name, string ext) ToShortName(string fileName) + { + // Extract base and extension, sanitize to 8.3 uppercase ASCII + string name = Path.GetFileNameWithoutExtension(fileName) ?? "FILE"; + string ext = Path.GetExtension(fileName); + if (ext.StartsWith('.')) ext = ext.Substring(1); + + string Sanitize(string s) => new string(s.ToUpperInvariant() + .Select(c => (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "!#$%&'()-@^_`{}~".Contains(c) ? c : '_') + .ToArray()); + + string n = Sanitize(name); + string e = Sanitize(ext); + if (n.Length > 8) n = n.Substring(0, 8); + if (e.Length > 3) e = e.Substring(0, 3); + n = n.PadRight(8); + e = e.PadRight(3); + return (n, e); + } + + private static ushort ToFatDate(DateTime dt) + { + int year = Math.Clamp(dt.Year - 1980, 0, 127); + int month = Math.Clamp(dt.Month, 1, 12); + int day = Math.Clamp(dt.Day, 1, 31); + return (ushort)((year << 9) | (month << 5) | day); + } + + private static ushort ToFatTime(DateTime dt) + { + int hour = Math.Clamp(dt.Hour, 0, 23); + int minute = Math.Clamp(dt.Minute, 0, 59); + int second = Math.Clamp(dt.Second / 2, 0, 29); // 2-second resolution + return (ushort)((hour << 11) | (minute << 5) | second); + } + + private static byte SelectSectorsPerCluster(uint totalSectors, ushort bytesPerSector) + { + // Simple mapping aimed at floppy-like sizes with 512B sectors + long sizeKiB = (long)totalSectors * bytesPerSector / 1024; + if (sizeKiB <= 720) return 1; // 360K/720K + if (sizeKiB <= 1440) return 1; // 1.44MB + if (sizeKiB <= 2400) return 1; // 2.4MB + if (sizeKiB <= 2880) return 1; // 2.88MB + if (sizeKiB <= 4096) return 2; // up to 4MB + if (sizeKiB <= 8192) return 4; // up to 8MB + return 8; // larger volumes still FAT12 but uncommon + } + } +} + +// --------- Example usage (not part of the library) --------- +// using System.Text; +// var img = new Fat12Lib.Fat12Image(new Fat12Lib.Fat12ImageOptions { TotalSizeBytes = 1440L * 1024, VolumeLabel = "MY12DISK" }); +// img.AddFile("HELLO.TXT", Encoding.ASCII.GetBytes("Hello FAT12! +")); +// img.SaveToFile("fat12.img"); diff --git a/Content/TestDisk.adf b/Content/TestDisk.adf new file mode 100644 index 0000000..344d7c1 Binary files /dev/null and b/Content/TestDisk.adf differ diff --git a/Frontend/AuthApiClient_Example.ino b/Frontend/AuthApiClient_Example.ino new file mode 100644 index 0000000..5ced6f4 --- /dev/null +++ b/Frontend/AuthApiClient_Example.ino @@ -0,0 +1,65 @@ +#include +#include +#include +#include "AuthApiClient.h" + +const char* WIFI_SSID = "Barriball - Automation"; +const char* WIFI_PASS = "password123abc"; +const char* API_BASE = "http://172.21.10.191:5250/"; +const char* USERNAME = "test"; +const char* PASSWORD = "password"; + +AuthApiClient api(API_BASE); + +void connectWiFi() +{ + Serial.printf("Connecting to %s", WIFI_SSID); + WiFi.begin(WIFI_SSID, WIFI_PASS); + + while (WiFi.status() != WL_CONNECTED) + { + delay(500); Serial.print("."); + } + + Serial.println("\nWiFi connected."); +} + +void setupTime() +{ + configTime(0, 0, "pool.ntp.org", "time.nist.gov"); + Serial.print(" Syncing time"); + + for (int i = 0; i < 20; i++) + { + if (time(nullptr) > 1700000000) break; + + delay(500); + Serial.print("."); + } + + Serial.println(); +} + +void setup() +{ + Serial.begin(115200); + delay(200); + connectWiFi(); + setupTime(); + //api.useCertBundle(true); // ESP32-S3 recommended + + if (!api.login(USERNAME, PASSWORD)) + { + Serial.println("Login failed."); return; + } + + Serial.println("Login OK."); +} + +unsigned long lastTick = 0; + +void loop() +{ + + delay(20); +} diff --git a/Frontend/ESP32_Firmware/test1.ino/test1.ino.ino b/Frontend/ESP32_Firmware/test1.ino/test1.ino.ino index 6cfa721..029e591 100644 --- a/Frontend/ESP32_Firmware/test1.ino/test1.ino.ino +++ b/Frontend/ESP32_Firmware/test1.ino/test1.ino.ino @@ -10,105 +10,6 @@ void loop() {} USBMSC MSC; -#define FAT_U8(v) ((v) & 0xFF) -#define FAT_U16(v) FAT_U8(v), FAT_U8((v) >> 8) -#define FAT_U32(v) FAT_U8(v), FAT_U8((v) >> 8), FAT_U8((v) >> 16), FAT_U8((v) >> 24) -#define FAT_MS2B(s, ms) FAT_U8(((((s) & 0x1) * 1000) + (ms)) / 10) -#define FAT_HMS2B(h, m, s) FAT_U8(((s) >> 1) | (((m) & 0x7) << 5)), FAT_U8((((m) >> 3) & 0x7) | ((h) << 3)) -#define FAT_YMD2B(y, m, d) FAT_U8(((d) & 0x1F) | (((m) & 0x7) << 5)), FAT_U8((((m) >> 3) & 0x1) | ((((y) - 1980) & 0x7F) << 1)) -#define FAT_TBL2B(l, h) FAT_U8(l), FAT_U8(((l >> 8) & 0xF) | ((h << 4) & 0xF0)), FAT_U8(h >> 4) - -const char README_CONTENTS[] = "This is tinyusb's MassStorage Class demo.\r\n\r\nIf you find any bugs or get any questions, feel free to file an\r\nissue at github.com/hathach/tinyusb"; - -static const uint32_t DISK_SECTOR_COUNT = 500 * 8; // 8KB is the smallest size that windows allow to mount -static const uint16_t DISK_SECTOR_SIZE = 512; // Should be 512 -static const uint16_t DISC_SECTORS_PER_TABLE = 1; //each table sector can fit 170KB (340 sectors) - -static uint8_t* msc_disk[DISK_SECTOR_COUNT]; - -//static uint8_t msc_disk[DISK_SECTOR_COUNT][DISK_SECTOR_SIZE] = { - const uint8_t sector0[512] = - //------------- Block0: Boot Sector -------------// - { // Header (62 bytes) - 0xEB, 0x3C, 0x90, //jump_instruction - 'M', 'S', 'D', 'O', 'S', '5', '.', '0', //oem_name - FAT_U16(DISK_SECTOR_SIZE), //bytes_per_sector - FAT_U8(1), //sectors_per_cluster - FAT_U16(1), //reserved_sectors_count - FAT_U8(1), //file_alloc_tables_num - FAT_U16(16), //max_root_dir_entries - FAT_U16(DISK_SECTOR_COUNT), //fat12_sector_num - 0xF8, //media_descriptor - FAT_U16(DISC_SECTORS_PER_TABLE), //sectors_per_alloc_table;//FAT12 and FAT16 - FAT_U16(1), //sectors_per_track;//A value of 0 may indicate LBA-only access - FAT_U16(1), //num_heads - FAT_U32(0), //hidden_sectors_count - FAT_U32(0), //total_sectors_32 - 0x00, //physical_drive_number;0x00 for (first) removable media, 0x80 for (first) fixed disk - 0x00, //reserved - 0x29, //extended_boot_signature;//should be 0x29 - FAT_U32(0x1234), //serial_number: 0x1234 => 1234 - 'N', 'e', 't', 'F', 'l', 'o', 'p', 'p', 'y', ' ', ' ', //volume_label padded with spaces (0x20) - 'F', 'A', 'T', '1', '2', ' ', ' ', ' ', //file_system_type padded with spaces (0x20) - - // Zero up to 2 last bytes of FAT magic code (448 bytes) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - - //boot signature (2 bytes) - 0x55, 0xAA - }; - - const uint8_t sector1[] = //------------- Block1: FAT12 Table -------------// - { - FAT_TBL2B(0xFF8, 0xFFF), FAT_TBL2B(0xFFF, 0x000) // first 2 entries must be 0xFF8 0xFFF, third entry is cluster end of readme file - }; - - const uint8_t sector2[512] = //------------- Block2: Root Directory -------------// - { - // first entry is volume label - 'N', 'e', 't', 'F', 'l', 'o', 'p', 'p', 'y', ' ', ' ', - 0x08, //FILE_ATTR_VOLUME_LABEL - 0x00, FAT_MS2B(0, 0), FAT_HMS2B(0, 0, 0), FAT_YMD2B(0, 0, 0), FAT_YMD2B(0, 0, 0), FAT_U16(0), FAT_HMS2B(13, 42, 30), //last_modified_hms - FAT_YMD2B(2025, 8, 26), //last_modified_ymd - FAT_U16(0), FAT_U32(0), - - // second entry is readme file - 'F', 'i', 'l', 'e', ' ', '1', ' ', ' ', //file_name[8]; padded with spaces (0x20) - 'T', 'X', 'T', //file_extension[3]; padded with spaces (0x20) - 0x20, //file attributes: FILE_ATTR_ARCHIVE - 0x00, //ignore - FAT_MS2B(1, 980), //creation_time_10_ms (max 199x10 = 1s 990ms) - FAT_HMS2B(13, 42, 36), //create_time_hms [5:6:5] => h:m:(s/2) - FAT_YMD2B(2018, 11, 5), //create_time_ymd [7:4:5] => (y+1980):m:d - FAT_YMD2B(2020, 11, 5), //last_access_ymd - FAT_U16(0), //extended_attributes - FAT_HMS2B(13, 44, 16), //last_modified_hms - FAT_YMD2B(2019, 11, 5), //last_modified_ymd - FAT_U16(2), //start of file in cluster - FAT_U32(sizeof(README_CONTENTS) - 1) //file size - }; -//}; - static int32_t onWrite(uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize) { Serial.printf("MSC WRITE: lba: %lu, offset: %lu, bufsize: %lu\n", lba, offset, bufsize); diff --git a/Frontend/ESP32_Firmware/test2_ino/AuthApiClient.cpp b/Frontend/ESP32_Firmware/test2_ino/AuthApiClient.cpp new file mode 100755 index 0000000..9afb776 --- /dev/null +++ b/Frontend/ESP32_Firmware/test2_ino/AuthApiClient.cpp @@ -0,0 +1,502 @@ +#include "AuthApiClient.h" +#include +#include "mbedtls/base64.h" +#include "SpiRamAllocator.cpp" + +AuthApiClient::AuthApiClient(const String& apiBase, int requestId) : _apiBase(apiBase), _requestId(requestId) +{ + +} + +SpiRamAllocator alloc; + +void AuthApiClient::setInsecure(bool enable) +{ + _useInsecure = enable; +} + +void AuthApiClient::setRootCA(const char* pemRootCA) +{ + _rootCA = pemRootCA; _useInsecure = false; _useBundle = false; +} +void AuthApiClient::useCertBundle(bool enable) +{ + _useBundle = enable; + + if (enable) + { _useInsecure = false; + _rootCA = nullptr; + } +} + +bool AuthApiClient::beginHttp(HTTPClient& http, WiFiClientSecure& client, const String& fullUrl) +{ + client.setTimeout(15000); + +#if defined(ARDUINO_ARCH_ESP32) + if (_useBundle) + { + Serial.printf("Using certificate bundle\n"); + //client.setCACertBundle(nullptr); + return false; + } + else if (_rootCA) + { + Serial.printf("Using root certificate authority\n"); + client.setCACert(_rootCA); + } + else if (_useInsecure) + { + Serial.printf("Using HTTP Insecure\n"); + client.setInsecure(); + } + else + { + return false; + } +#else + if (_rootCA == nullptr && !_useInsecure) + { + return false; + } +#endif + return http.begin(client, fullUrl); +} + +bool AuthApiClient::httpsPostJsonFullUrl(const String& fullUrl, const String& jsonBody, String& respBody) +{ + WiFiClientSecure client; + HTTPClient http; + + Serial.printf("CREATE HTTP: %s\n", fullUrl.c_str()); + + if (!beginHttp(http, client, fullUrl)) + { + return false; + } + + Serial.printf("Adding Headers\n"); + + http.addHeader("Content-Type", "application/json"); + + const String& tok = accessToken(); + + // Build Authorization header safely (avoid large temporaries) + if (!tok.isEmpty()) + { + Serial.printf("Adding Token\n"); + + String auth; + auth.reserve(8 + tok.length()); // "Bearer " + token + auth = "Bearer "; + auth += tok; + + // sanitize in case token includes stray CR/LF + auth.replace("\r", ""); + auth.replace("\n", ""); + + http.addHeader("Authorization", auth); + + //Serial.printf("TOKEN: %s\n", auth.c_str()); + } + + Serial.printf("JSON: %u %s \n", (unsigned)jsonBody.length(), jsonBody.c_str()); + + Serial.printf("POST: Start\n"); + int code = http.POST(jsonBody); + Serial.printf("POST: End\n"); + + if (code <= 0) + { + Serial.printf("POST failed: %s (%d)\n", http.errorToString(code).c_str(), code); + + http.end(); + return false; + } + + respBody = http.getString(); + http.end(); + + //Serial.printf("RESPONSE BODY: %s\n", respBody.c_str()); + return (code >= 200 && code < 300); +} + + +bool AuthApiClient::httpsPostJsonReturnBytesFullUrl(const String& fullUrl, const String& jsonBody, uint8_t*& outBufPS, size_t& outLen) +{ + WiFiClientSecure client; + HTTPClient http; + + Serial.printf("CREATE HTTP: %s\n", fullUrl.c_str()); + + if (!beginHttp(http, client, fullUrl)) + { + return false; + } + +Serial.printf("Free heap: %u\n", (unsigned)ESP.getFreeHeap()); + Serial.printf("Adding Headers"); + + http.addHeader("Content-Type", "application/json"); + + // Build Authorization header safely (avoid large temporaries) + if (!_accessToken.isEmpty()) + { + static String auth; // reuse to reduce heap churn + auth.remove(0); + + Serial.printf("Adding Token\n"); + + auth.reserve(8 + _accessToken.length()); // "Bearer " + token + auth = "Bearer "; + auth += _accessToken; + + // sanitize in case token includes stray CR/LF + auth.replace("\r", ""); + auth.replace("\n", ""); + + http.addHeader("Authorization", auth); + + //Serial.printf("TOKEN: %s\n", auth.c_str()); + } + +Serial.printf("Free heap: %u\n", (unsigned)ESP.getFreeHeap()); + //Serial.printf("JSON: %u %s \n", (unsigned)jsonBody.length(), jsonBody.c_str()); + + Serial.printf("POST: Start\n"); + int code = http.POST(jsonBody); + Serial.printf("Free heap: %u\n", (unsigned)ESP.getFreeHeap()); + Serial.printf("POST: End\n"); + + // Content-Length if provided, else -1 for chunked + int contentLen = http.getSize(); + NetworkClient* s = http.getStreamPtr(); + + // Allocate initial PSRAM buffer + size_t cap = 0; + if (contentLen > 0) + { + cap = (size_t)contentLen; + } else + { + // Unknown length: start with 64 KB and grow as needed + cap = 64 * 1024; + } + + uint8_t* buf = (uint8_t*) ps_malloc(cap); + + if (!buf) + { + Serial.println("PSRAM alloc failed"); + http.end(); + return false; + } + + size_t total = 0; + const uint32_t deadline = millis() + 15000; + + while (http.connected() && (contentLen != 0 || contentLen == -1)) + { + if (millis() > deadline) + { + Serial.println("Receive timeout"); + free(buf); + http.end(); + return false; + } + + size_t avail = s->available(); + if (!avail) { delay(1); continue; } + + // Determine how many bytes we can/should read this iteration + size_t want = (contentLen > 0) ? (size_t)min((int)avail, contentLen) : avail; + + // Ensure capacity (grow if needed for chunked or short Content-Length) + if (total + want > cap) { + size_t newCap = max(cap * 2, total + want); // grow exponentially + uint8_t* grown = (uint8_t*) ps_realloc(buf, newCap); + + if (!grown) + { + Serial.println("PSRAM realloc failed"); + free(buf); + http.end(); + return false; + } + + buf = grown; + cap = newCap; + } + + int r = s->read(buf + total, want); + + if (r > 0) + { + total += (size_t)r; + + if (contentLen > 0) + { + contentLen -= r; + } + } + } + + http.end(); + + outBufPS = buf; + outLen = total; + + //Serial.printf("RESPONSE BODY: %s\n", respBody.c_str()); + return (code >= 200 && code < 300); +} + +bool AuthApiClient::httpsGetFullUrl(const String& fullUrl, String& respBody) { + WiFiClientSecure client; HTTPClient http; + if (!beginHttp(http, client, fullUrl)) return false; + if (_accessToken.length() > 0) http.addHeader("Authorization", "Bearer " + _accessToken); + int code = http.GET(); + if (code <= 0) { http.end(); return false; } + respBody = http.getString(); + http.end(); + return (code >= 200 && code < 300); +} + +bool AuthApiClient::base64UrlDecode(const String& in, String& out) +{ + String s = in; s.replace('-', '+'); s.replace('_', '/'); while (s.length() % 4 != 0) s += '='; + auto idx=[](char c)->int{if(c>='A'&&c<='Z')return c-'A';if(c>='a'&&c<='z')return c-'a'+26;if(c>='0'&&c<='9')return c-'0'+52;if(c=='+')return 62;if(c=='/')return 63;return-1;}; + out="";int val=0,valb=-8;for(size_t i=0;i=0){out+=char((val>>valb)&0xFF);valb-=8;}}return true; +} + +time_t AuthApiClient::jwtExp(const String& jwt) { + int dot1=jwt.indexOf('.'); int dot2=jwt.indexOf('.',dot1+1); if(dot1<0||dot2<0)return 0; String payloadB64=jwt.substring(dot1+1,dot2); + String payloadJson; if(!base64UrlDecode(payloadB64,payloadJson)) return 0; + StaticJsonDocument<1024> doc; if(deserializeJson(doc,payloadJson)) return 0; + if(!doc.containsKey("exp")) return 0; return (time_t)doc["exp"].as(); +} + +bool AuthApiClient::login(const String& userName, const String& password) +{ + String url=_apiBase+"/logon"; + //SpiRamAllocator alloc; + JsonDocument req(&alloc); + + req["id"]=_requestId; + JsonObject p=req.createNestedObject("payload"); + p["userName"]=userName; + p["password"]=password; + + String body,resp; + + serializeJson(req,body); + + Serial.printf("JSON: %s\n", body); + + if(!httpsPostJsonFullUrl(url,body,resp)) + { + return false; + } + + JsonDocument doc(&alloc); + + if(deserializeJson(doc,resp)) + { + return false; + } + + if(!(doc["success"]|false)) + { + return false; + } + + + setAccessToken(doc["payload"]["token"].as()); + _refreshToken=doc["payload"]["refreshToken"].as(); + _tokenExpEpoch=jwtExp(_accessToken); + _authed=(_accessToken.length()>0&&_tokenExpEpoch>0); + + return _authed; +} + +bool AuthApiClient::loadDisk(const String& unitId, const String& diskId) +{ + String url=_apiBase+"/operations/loadimage"; + //SpiRamAllocator alloc; + JsonDocument req(&alloc); + + req["id"]=_requestId; + JsonObject p=req.createNestedObject("payload"); + p["unitId"]=unitId; + p["diskId"]=diskId; + + String body,resp; + + serializeJson(req,body); + + req.clear(); + + if(!httpsPostJsonFullUrl(url,body,resp)) + { + return false; + } + + JsonDocument doc; + + if(deserializeJson(doc,resp)) + { + return false; + } + + if(!(doc["success"]|false)) + { + return false; + } + + return _authed; +} + +bool AuthApiClient::loadDiskSector(const String& unitId, const String& diskId, uint32_t lba, uint32_t offset, void* buffer, size_t bufsize) +{ + if (!buffer || bufsize == 0) + { + Serial.println("API: invalid buffer"); + return false; + } + + String url=_apiBase+"/operations/loadsector"; + //SpiRamAllocator alloc; + JsonDocument req(&alloc); + + req["id"]=_requestId; + JsonObject p=req.createNestedObject("payload"); + p["unitId"]=unitId; + p["diskId"]=diskId; + //p["lba"]=lba; + //p["offset"]=offset; + //p["length"]=bufsize; + + String body,resp; + + Serial.printf("API: Serialise\n"); + serializeJson(req,body); + + Serial.printf("API: Start\n"); + + Serial.printf("Free heap: %u\n", (unsigned)ESP.getFreeHeap()); + +uint8_t* psBuf = nullptr; +size_t psLen = 0; + + if(!httpsPostJsonReturnBytesFullUrl(url,body, psBuf, psLen)) + { + Serial.printf("API: Failed\n"); + return false; + } + + memcpy(buffer, psBuf, min(psLen, bufsize)); + + free(psBuf); + psBuf = nullptr; + + Serial.printf("API: End\n"); + +/* + JsonDocument doc; + + if(deserializeJson(doc,resp)) + { + return false; + } + + if(!(doc["success"]|false)) + { + return false; + } + + // --- Get payload.data --- + const char* encoded = doc["payload"]["data"]; + + if (!encoded) + { + return false; + } + + size_t out_len = 0; + int res = mbedtls_base64_decode( + (unsigned char*)buffer, bufsize, &out_len, + (const unsigned char*)encoded, strlen(encoded) + ); + + if (res != 0) + { + // decoding failed + return false; + }*/ + + return true; +} + +bool AuthApiClient::refresh() +{ + if(_refreshToken.length()==0)return false; String url=_apiBase+"/logon/refresh"; + StaticJsonDocument<320> req; req["id"]=_requestId; JsonObject p=req.createNestedObject("payload"); p["refreshToken"]=_refreshToken; + String body,resp; serializeJson(req,body); if(!httpsPostJsonFullUrl(url,body,resp)) return false; + StaticJsonDocument<2048> doc; if(deserializeJson(doc,resp)) return false; if(!(doc["success"]|false)) return false; + _accessToken=doc["payload"]["token"].as(); _refreshToken=doc["payload"]["refreshToken"].as(); _tokenExpEpoch=jwtExp(_accessToken); + _authed=(_accessToken.length()>0&&_tokenExpEpoch>0); return _authed; +} + +bool AuthApiClient::ensureTokenFresh(long refreshEarlySeconds) +{ + if(!_authed) + { + Serial.printf("Token Refresh: Not Authorised"); + return false; + } + + time_t nowT=time(nullptr); + if(nowT==0) + { + Serial.printf("Token Refresh: Refresh Overdue"); + return refresh(); + } + + if(nowT>=(_tokenExpEpoch-refreshEarlySeconds)) + { + Serial.printf("Token Refresh: Not Early"); + return refresh(); + } + + return true; +} + +bool AuthApiClient::postJson(const String& path, const String& jsonBody, String& respBody, bool autoRefresh) +{ + if(autoRefresh&&_authed) + { + ensureTokenFresh(); + } + + String url=_apiBase+path; + return httpsPostJsonFullUrl(url,jsonBody,respBody); +} + +void AuthApiClient::setAccessToken(const String& token) +{ + _accessToken.reserve(token.length()); + _accessToken = token; + _accessToken.replace("\r", ""); + _accessToken.replace("\n", ""); +} + +bool AuthApiClient::get(const String& path, String& respBody, bool autoRefresh) +{ + if(autoRefresh&&_authed) + { + ensureTokenFresh(); + } + + String url=_apiBase+path; + return httpsGetFullUrl(url,respBody); +} + \ No newline at end of file diff --git a/Frontend/ESP32_Firmware/test2_ino/AuthApiClient.h b/Frontend/ESP32_Firmware/test2_ino/AuthApiClient.h new file mode 100644 index 0000000..0eb327c --- /dev/null +++ b/Frontend/ESP32_Firmware/test2_ino/AuthApiClient.h @@ -0,0 +1,47 @@ +#ifndef AUTH_API_CLIENT_H +#define AUTH_API_CLIENT_H + +#include +#include +#include +#include + +class AuthApiClient { +public: + AuthApiClient(const String& apiBase, int requestId = 12345); + void setInsecure(bool enable = true); + void setRootCA(const char* pemRootCA); + void useCertBundle(bool enable = true); + bool login(const String& userName, const String& password); + bool loadDisk(const String& unitId, const String& diskId); + bool loadDiskSector(const String& unitId, const String& diskId, uint32_t lba, uint32_t offset, void* buffer, size_t bufsize); + bool refresh(); + bool ensureTokenFresh(long refreshEarlySeconds = 60); + bool postJson(const String& path, const String& jsonBody, String& respBody, bool autoRefresh = true); + bool get(const String& path, String& respBody, bool autoRefresh = true); + bool authed() const { return _authed; } + const String& accessToken() const { return _accessToken; } + void setAccessToken(const String& token); + const String& refreshToken() const { return _refreshToken; } + time_t tokenExp() const { return _tokenExpEpoch; } + +private: + bool httpsPostJsonFullUrl(const String& fullUrl, const String& jsonBody, String& respBody); + bool httpsPostJsonReturnBytesFullUrl(const String& fullUrl, const String& jsonBody, uint8_t*& outBufPS, size_t& outLen); + bool httpsGetFullUrl(const String& fullUrl, String& respBody); + bool beginHttp(HTTPClient& http, WiFiClientSecure& client, const String& fullUrl); + static bool base64UrlDecode(const String& in, String& out); + static time_t jwtExp(const String& jwt); + + String _apiBase; + int _requestId; + String _accessToken; + String _refreshToken; + time_t _tokenExpEpoch = 0; + bool _authed = false; + bool _useInsecure = true; + const char* _rootCA = nullptr; + bool _useBundle = false; +}; + +#endif // AUTH_API_CLIENT_H diff --git a/Frontend/ESP32_Firmware/test2_ino/SpiRamAllocator.cpp b/Frontend/ESP32_Firmware/test2_ino/SpiRamAllocator.cpp new file mode 100644 index 0000000..9973782 --- /dev/null +++ b/Frontend/ESP32_Firmware/test2_ino/SpiRamAllocator.cpp @@ -0,0 +1,23 @@ +#include +#include + +struct SpiRamAllocator : ArduinoJson::Allocator +{ + void* allocate(size_t size) override + { + Serial.printf("Json - PSRAM - Allocating %u Memory", size); + return ps_malloc(size); + } + + void deallocate(void* pointer) override + { + Serial.printf("Json - PRAM - Freeing Memory"); + heap_caps_free(pointer); + } + + void* reallocate(void* ptr, size_t new_size) override + { + Serial.printf("Json - PSRAM - Rellocating %u Memory", new_size); + return ps_realloc(ptr, new_size); + } +}; \ No newline at end of file diff --git a/Frontend/ESP32_Firmware/test2_ino/test2_ino.ino b/Frontend/ESP32_Firmware/test2_ino/test2_ino.ino index 2f48440..604905f 100644 --- a/Frontend/ESP32_Firmware/test2_ino/test2_ino.ino +++ b/Frontend/ESP32_Firmware/test2_ino/test2_ino.ino @@ -7,6 +7,8 @@ #include "USBMSC.h" #include #include +#include +#include "esp32-hal-psram.h" #define LED_ON 2 #define LED_TX 43 @@ -14,13 +16,23 @@ #define BUTTON 0 #define WS2812 48 -// ---- Disk parameters ---- -static const uint32_t DISK_SECTOR_SIZE = 512; // 512 bytes/sector -static const uint32_t DISK_SECTOR_COUNT = 1000 * 8; // ~2 MB (4000 * 512) -static const uint32_t DISK_BYTE_SIZE = DISK_SECTOR_COUNT * DISK_SECTOR_SIZE; +const char* WIFI_SSID = "Aqua Cube IT"; +const char* WIFI_PASS = "Iamazombie123"; +const char* API_BASE = "https://netfloppy.com.puter.club"; +const char* USERNAME = "test"; +const char* PASSWORD = "password"; +uint32_t CHIP_ID = 0; -static uint8_t* msc_disk = nullptr; +// ---- Disk parameters ---- +uint32_t DISK_SIZE_MB = 5; +uint32_t DISK_SECTOR_SIZE = 512; // 512 bytes/sector +uint32_t DISK_SECTOR_COUNT = DISK_SIZE_MB * 1024 * 1024 / DISK_SECTOR_SIZE; +uint32_t DISK_BYTE_SIZE = DISK_SECTOR_COUNT * DISK_SECTOR_SIZE; + +//static uint8_t* msc_disk = nullptr; static volatile bool media_ready = false; + +AuthApiClient _api(API_BASE); USBMSC msc; Adafruit_NeoPixel pixels(1, WS2812, NEO_GRB + NEO_KHZ800); @@ -46,13 +58,12 @@ bool autoConnect = false; void loadPrefs() { prefs.begin("wifi", true); // read-only - ssid = prefs.getString("ssid", "Barriball - Automation"); - pass = prefs.getString("pass", "password123abc"); + ssid = prefs.getString("ssid", WIFI_SSID); + pass = prefs.getString("pass", WIFI_PASS); autoConnect = prefs.getBool("auto", true); prefs.end(); } - void savePrefs() { prefs.begin("wifi", false); @@ -62,7 +73,6 @@ void savePrefs() prefs.end(); } - void forgetPrefs() { prefs.begin("wifi", false); @@ -75,7 +85,6 @@ void forgetPrefs() autoConnect = false; } - void showStatus() { wl_status_t st = WiFi.status(); @@ -97,8 +106,52 @@ void showStatus() } +void setupWifi() +{ + forgetPrefs(); + loadPrefs(); + if (ssid == "") + { + ssid = WIFI_SSID; + pass = WIFI_PASS; + autoConnect = true; + + Serial.printf(" SSID: '%s', Pass: '%s', Auto: %u\n", ssid.c_str(), pass.c_str(), autoConnect); + + savePrefs(); + } + + Serial.printf(" Connecting to WIFI '%s'...\n", ssid.c_str()); + WiFi.begin(ssid.c_str(), pass.c_str()); + + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.println("\n WiFi connected."); + + showStatus(); +} + +void setupTime() +{ + configTime(0, 0, "pool.ntp.org", "time.nist.gov"); + Serial.print("Syncing time"); + + for (int i = 0; i < 20; i++) + { + if (time(nullptr) > 1700000000) break; + + delay(500); + Serial.print("."); + } + + Serial.println(); +} void changeMediaStatus(bool mediaPresent) { @@ -165,7 +218,6 @@ bool onStartStop(uint8_t power_condition, bool start, bool load_eject) Serial.printf("StartStop: pwr=%u start=%u load_eject=%u\n", power_condition, start, load_eject); - if (load_eject && !start) { Serial.printf("Eject"); @@ -209,7 +261,7 @@ bool onStartStop(uint8_t power_condition, bool start, bool load_eject) void initialiseDisk() { // Zero out storage area - memset(msc_disk, 0x00, DISK_BYTE_SIZE); + //memset(msc_disk, 0x00, DISK_BYTE_SIZE); // Bring up native USB (CDC + MSC share the device) //USB.onEvent(usbEvent) @@ -231,8 +283,13 @@ void initialiseDisk() return -1; } - memcpy(buffer, msc_disk + (lba * DISK_SECTOR_SIZE) + offset, bufsize); - //Serial.printf("READ lba=%lu off=%lu len=%lu\n", lba, offset, bufsize); + //memcpy(buffer, msc_disk + (lba * DISK_SECTOR_SIZE) + offset, bufsize); + + String unitId = "0"; + String driveId = "0"; + + _api.loadDiskSector(unitId, driveId, lba, offset, buffer, bufsize); + Serial.printf("READ lba=%lu off=%lu len=%lu\n", lba, offset, bufsize); return (int32_t)bufsize; }); @@ -251,8 +308,8 @@ void initialiseDisk() return -1; } - memcpy(msc_disk + (lba * DISK_SECTOR_SIZE) + offset, buffer, bufsize); - //Serial.printf("WRITE lba=%lu off=%lu len=%lu\n", lba, offset, bufsize); + //memcpy(msc_disk + (lba * DISK_SECTOR_SIZE) + offset, buffer, bufsize); + Serial.printf("WRITE lba=%lu off=%lu len=%lu\n", lba, offset, bufsize); return (int32_t)bufsize; }); @@ -272,12 +329,10 @@ void initialiseDisk() } changeMediaStatus(true); - // No setUnitReady() / onReady() in this API; readiness handled inside callbacks. } void setup() -{ - +{ Serial.begin(SERIAL_BAUD); while (!Serial) @@ -289,6 +344,14 @@ void setup() Serial.flush(); + if (psramInit() && psramFound()) + { + if (psramAddToHeap()) + { + Serial.println("PSRAM added to heap"); + } + } + pinMode(LED_ON, OUTPUT); digitalWrite(LED_ON, LOW); @@ -301,55 +364,46 @@ void setup() Serial.printf("| Version 1.0 |\n"); Serial.printf("| Copyright Aqua Cube IT Limited 2025 |\n"); Serial.printf("+=====================================+\n\n"); - - uint32_t chipId = 0; + for (int i = 0; i < 17; i = i + 8) { - chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; + CHIP_ID |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; } Serial.printf("ESP32 Chip model = %s Rev %d\n", ESP.getChipModel(), ESP.getChipRevision()); Serial.printf("This chip has %d cores\n", ESP.getChipCores()); Serial.print("Chip ID: "); - Serial.println(chipId); - + Serial.println(CHIP_ID); Serial.printf("Initialising...\n"); + + setupWifi(); + setupTime(); + + //_api.useCertBundle(false); + //_api.setInsecure(true); + + if (_api.login(USERNAME, PASSWORD)) + { + Serial.printf(" API Login Successfull\n"); + } + else + { + Serial.printf(" API Login Failed\n"); + } + Serial.printf("Allocating memory...\n"); // Allocate ramdisk in PSRAM - msc_disk = (uint8_t*) ps_malloc(DISK_BYTE_SIZE); + //msc_disk = (uint8_t*) ps_malloc(DISK_BYTE_SIZE); - if (!msc_disk) - { - Serial.println("PSRAM alloc FAILED\n"); - } + //if (!msc_disk) + //{ + // Serial.println("PSRAM alloc FAILED\n"); + //} - Serial.printf("Allocated %u bytes of %u - %u Free\n", (unsigned)DISK_BYTE_SIZE, ESP.getPsramSize(), ESP.getFreePsram()); - - - loadPrefs(); - - if (ssid == "") - { - ssid = "Barriball - Automation"; - pass = "password123abc"; - autoConnect = true; - -Serial.printf("SSID: '%s', Pass: '%s', Auto: %u\n", ssid.c_str(), pass.c_str(), autoConnect); - - savePrefs(); - } - - if (autoConnect && ssid.length()) - { - Serial.printf("Connecting to WIFI '%s'...\n", ssid.c_str()); - //connectWiFi(ssid, pass); - WiFi.begin(ssid.c_str(), pass.c_str()); - - showStatus(); - } + //Serial.printf("Allocated %u bytes of %u - %u Free\n", (unsigned)DISK_BYTE_SIZE, ESP.getPsramSize(), ESP.getFreePsram()); initialiseDisk(); } @@ -357,6 +411,6 @@ Serial.printf("SSID: '%s', Pass: '%s', Auto: %u\n", ssid.c_str(), pass.c_str(), void loop() { // Simple heartbeat - //digitalWrite(LED_ON, !digitalRead(LED_ON)); - delay(10); + digitalWrite(LED_ON, !digitalRead(LED_ON)); + delay(100); } diff --git a/Frontend/test.ino b/Frontend/test/test.ino similarity index 100% rename from Frontend/test.ino rename to Frontend/test/test.ino