Compare commits
2 Commits
0d0887bfb8
...
2777a4034f
| Author | SHA1 | Date | |
|---|---|---|---|
| 2777a4034f | |||
| 776c1253a4 |
335
AquaCubeIT.Service.NetFloppy/Fat16Disk.cs
Normal file
335
AquaCubeIT.Service.NetFloppy/Fat16Disk.cs
Normal file
@ -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<byte>();
|
||||
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<uint> 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<byte> 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<byte> 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");
|
||||
342
AquaCubeIT.Service.NetFloppy/fat_16_image_lib.cs
Normal file
342
AquaCubeIT.Service.NetFloppy/fat_16_image_lib.cs
Normal file
@ -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<byte>();
|
||||
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<uint> 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<byte> 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<byte> 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");
|
||||
65
Frontend/AuthApiClient_Example.ino
Normal file
65
Frontend/AuthApiClient_Example.ino
Normal file
@ -0,0 +1,65 @@
|
||||
#include <WiFi.h>
|
||||
#include <time.h>
|
||||
#include <ArduinoJson.h>
|
||||
#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);
|
||||
}
|
||||
@ -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);
|
||||
|
||||
502
Frontend/ESP32_Firmware/test2_ino/AuthApiClient.cpp
Executable file
502
Frontend/ESP32_Firmware/test2_ino/AuthApiClient.cpp
Executable file
@ -0,0 +1,502 @@
|
||||
#include "AuthApiClient.h"
|
||||
#include <ArduinoJson.h>
|
||||
#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<s.length();i++){char c=s[i];if(c=='=')break;int d=idx(c);if(d<0)continue;val=(val<<6)+d;valb+=6;if(valb>=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<long>();
|
||||
}
|
||||
|
||||
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<String>());
|
||||
_refreshToken=doc["payload"]["refreshToken"].as<String>();
|
||||
_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<String>(); _refreshToken=doc["payload"]["refreshToken"].as<String>(); _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);
|
||||
}
|
||||
| ||||