Initial build

This commit is contained in:
Stephen Barriball 2025-10-13 18:41:03 +01:00
parent 10ad74f50c
commit 776c1253a4
10 changed files with 1423 additions and 154 deletions

View 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");

View 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");

BIN
Content/TestDisk.adf Normal file

Binary file not shown.

View 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);
}

View File

@ -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);

View 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);
}

View File

@ -0,0 +1,47 @@
#ifndef AUTH_API_CLIENT_H
#define AUTH_API_CLIENT_H
#include <Arduino.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <time.h>
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

View File

@ -0,0 +1,23 @@
#include <ArduinoJson.h>
#include <esp_heap_caps.h>
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);
}
};

View File

@ -7,6 +7,8 @@
#include "USBMSC.h"
#include <WiFi.h>
#include <Preferences.h>
#include <AuthApiClient.h>
#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);
}