639 lines
26 KiB
C#
639 lines
26 KiB
C#
using System.IO.Compression;
|
|
using InnovEnergy.Lib.Devices.WITGrowatt4_15K;
|
|
using System.IO.Ports;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Xml;
|
|
using Flurl.Http;
|
|
using InnovEnergy.App.GrowattCommunication.DataLogging;
|
|
using InnovEnergy.App.GrowattCommunication.ESS;
|
|
using InnovEnergy.App.GrowattCommunication.SystemConfig;
|
|
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
|
using InnovEnergy.Lib.Units;
|
|
using InnovEnergy.Lib.Utils;
|
|
using Newtonsoft.Json;
|
|
using Formatting = Newtonsoft.Json.Formatting;
|
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using InnovEnergy.App.GrowattCommunication.DataTypes;
|
|
using InnovEnergy.Lib.Devices.WITGrowatt4_15K.DataType;
|
|
|
|
namespace InnovEnergy.App.GrowattCommunication;
|
|
|
|
public static class Program
|
|
{
|
|
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(2);
|
|
private const UInt16 NbrOfFileToConcatenate = 30; // add this to config file
|
|
private static UInt16 _fileCounter = 0;
|
|
//
|
|
private static Channel _growattChannel;
|
|
|
|
private const String SwVersionNumber =" V1.00.310725 beta";
|
|
private const String VpnServerIp = "10.2.0.11";
|
|
private static Boolean _subscribedToQueue = false;
|
|
private static Boolean _subscribeToQueueForTheFirstTime = false;
|
|
private static Int32 _failsCounter = 0; // move to a config file
|
|
private static SodistoreAlarmState _prevSodistoreAlarmState = SodistoreAlarmState.Green;
|
|
private static SodistoreAlarmState _sodistoreAlarmState = SodistoreAlarmState.Green;
|
|
|
|
|
|
// move all this to config file
|
|
private const String Port = "/dev/ttyUSB0";
|
|
private const Byte SlaveId = 1;
|
|
private const Parity Parity = 0; //none
|
|
private const Int32 StopBits = 1;
|
|
private const Int32 BaudRate = 9600;
|
|
private const Int32 DataBits = 8;
|
|
|
|
[UnconditionalSuppressMessage("Trimming",
|
|
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
|
|
Justification = "<Pending>")]
|
|
public static async Task Main(String[] args)
|
|
{
|
|
_growattChannel = new SerialPortChannel(Port, BaudRate, Parity, DataBits, StopBits);
|
|
|
|
while (true)
|
|
{
|
|
try
|
|
{
|
|
await Run();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
e.LogError();
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private static async Task Run()
|
|
{
|
|
Watchdog.NotifyReady();
|
|
|
|
Console.WriteLine("Starting Growatt Communication");
|
|
|
|
var growatrrDevicet415K = new WITGrowatDevice(_growattChannel, SlaveId);
|
|
|
|
StatusRecord ReadStatus()
|
|
{
|
|
var config = Config.Load();
|
|
var growattRecord = growatrrDevicet415K.Read();
|
|
|
|
return new StatusRecord
|
|
{
|
|
AcDcGrowatt = growattRecord,
|
|
Config = config // load from disk every iteration, so config can be changed while running
|
|
};
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
try
|
|
{
|
|
Watchdog.NotifyAlive();
|
|
|
|
var timestamp = DateTime.Now.Round(UpdateInterval).ToUnixTime();
|
|
|
|
$"{timestamp} : {DateTime.Now.Round(UpdateInterval):dd/MM/yyyy HH:mm:ss}".WriteLine();
|
|
|
|
var startTime = DateTime.Now;
|
|
Console.WriteLine("***************************** Reading Battery Data *********************************************");
|
|
Console.WriteLine(startTime.ToString("HH:mm:ss.fff"));
|
|
|
|
// the order matter of the next three lines
|
|
var statusrecord = ReadStatus();
|
|
await DataLogging(statusrecord, timestamp); // save a csv file locally
|
|
await SaveModbusTcpFile(statusrecord); // save the json file for modbuscTCP
|
|
|
|
|
|
statusrecord.AcDcGrowatt.EnableCommand.WriteLine(" = EnableCommand");
|
|
statusrecord.AcDcGrowatt.ControlPermession.WriteLine(" ControlPermession");
|
|
statusrecord.AcDcGrowatt.GridMeterPower.WriteLine(" GridMeterPower");
|
|
statusrecord.AcDcGrowatt.InverterActivePower.WriteLine(" InverterActivePower");
|
|
statusrecord.AcDcGrowatt.BatteryPower1.WriteLine(" BatteryPower1"); // 30408 this the duration
|
|
statusrecord.AcDcGrowatt.PhaseACurrent.WriteLine(" PhaseACurrent "); //30409 we set power here
|
|
statusrecord.AcDcGrowatt.GridAbLineVoltage.WriteLine(" GridAbLineVoltage "); //30409 we set power here
|
|
statusrecord.AcDcGrowatt.RemotePowerControlChargeDuration.WriteLine(" = RemotePowerControlChargeDuration");
|
|
statusrecord.AcDcGrowatt.Batteries[0].Soc.WriteLine(" SOC");
|
|
statusrecord.AcDcGrowatt.Batteries[0].Power.WriteLine(" Battery Power");
|
|
statusrecord.AcDcGrowatt.Batteries[0].Current.WriteLine(" Battery Current");
|
|
statusrecord.AcDcGrowatt.Batteries[0].Voltage.WriteLine(" Battery Voltage");
|
|
statusrecord.AcDcGrowatt.BatteryMaxChargePower.WriteLine(" BatteryMaxChargePower "); //30409 we set power here
|
|
statusrecord.AcDcGrowatt.BatteryMaxDischargePower.WriteLine(" BatteryMaxDischargePower "); //30409 we set power here
|
|
|
|
statusrecord.AcDcGrowatt.SystemOperatingMode.WriteLine(" = SystemOperatingMode");
|
|
statusrecord.AcDcGrowatt.BatteryOperatingMode.WriteLine(" BatteryOperatingMode");
|
|
statusrecord.AcDcGrowatt.OperatingPriority.WriteLine(" OperatingPriority"); // 30408 this the duration
|
|
|
|
var stopTime = DateTime.Now;
|
|
Console.WriteLine(stopTime.ToString("HH:mm:ss.fff"));
|
|
Console.WriteLine("***************************** Finish Battery Data *********************************************");
|
|
statusrecord.AcDcGrowatt.EnableCommand = true;
|
|
statusrecord.AcDcGrowatt.ControlPermession = true;
|
|
statusrecord.AcDcGrowatt.RemotePowerControl = true;
|
|
statusrecord.AcDcGrowatt.RemotePowerControlChargeDuration = 0; // 30408 this the duration
|
|
statusrecord.AcDcGrowatt.ActivePowerPercent = 50; // 30408 this the duration
|
|
statusrecord.AcDcGrowatt.ActivePowerPercentDerating = 50; // 30408 this the duration
|
|
|
|
statusrecord.AcDcGrowatt.RemoteChargDischargePower = 50; //30409 we set power here
|
|
statusrecord.AcDcGrowatt.ActualChargeDischargePowerControlValue.WriteLine(" register 30474");
|
|
|
|
|
|
statusrecord.ApplyDefaultSettings();
|
|
|
|
statusrecord.Config.Save(); // save the config file
|
|
|
|
Console.WriteLine( " ************************************ We are writing ************************************");
|
|
growatrrDevicet415K.Write(statusrecord.AcDcGrowatt);
|
|
|
|
// Wait for 2 seconds before the next reading
|
|
// await Task.Delay(1000); // Delay in milliseconds (1000ms = 1 seconds)
|
|
await Task.Delay(2000); // Delay in milliseconds (1000ms = 1 seconds)
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// Handle exception and print the error
|
|
Console.WriteLine(e );
|
|
// await Task.Delay(2000); // Delay in milliseconds (2000ms = 2 seconds)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
private static StatusMessage GetSalimaxStateAlarm(StatusRecord record)
|
|
{
|
|
var s3Bucket = Config.Load().S3?.Bucket;
|
|
|
|
var alarmList = new List<AlarmOrWarning>();
|
|
var warningList = new List<AlarmOrWarning>();
|
|
|
|
if (record.AcDcGrowatt.SystemOperatingMode == GrowattSystemStatus.Fault)
|
|
{
|
|
if (record.AcDcGrowatt.FaultMainCode != 0)
|
|
{
|
|
alarmList.Add(new AlarmOrWarning
|
|
{
|
|
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
|
Time = DateTime.Now.ToString("HH:mm:ss"),
|
|
CreatedBy = "Growatt Inverter",
|
|
Description = record.AcDcGrowatt.WarningMainCode.ToString(), // to add the sub code
|
|
});
|
|
}
|
|
|
|
if (record.AcDcGrowatt.FaultSubCode != 0)
|
|
{
|
|
warningList.Add(new AlarmOrWarning
|
|
{
|
|
Date = DateTime.Now.ToString("yyyy-MM-dd"),
|
|
Time = DateTime.Now.ToString("HH:mm:ss"),
|
|
CreatedBy = "Growatt inverter",
|
|
Description = record.AcDcGrowatt.FaultMainCode.ToString(), //to add the sub code
|
|
});
|
|
}
|
|
}
|
|
|
|
_sodistoreAlarmState = warningList.Any()
|
|
? SodistoreAlarmState.Orange
|
|
: SodistoreAlarmState.Green; // this will be replaced by LedState
|
|
|
|
_sodistoreAlarmState = alarmList.Any()
|
|
? SodistoreAlarmState.Red
|
|
: _sodistoreAlarmState; // this will be replaced by LedState
|
|
|
|
var installationId = GetInstallationId(s3Bucket ?? string.Empty);
|
|
|
|
var returnedStatus = new StatusMessage
|
|
{
|
|
InstallationId = installationId,
|
|
Product = 3,
|
|
Status = _sodistoreAlarmState,
|
|
Type = MessageType.AlarmOrWarning,
|
|
Alarms = alarmList,
|
|
Warnings = warningList
|
|
};
|
|
|
|
return returnedStatus;
|
|
}
|
|
|
|
private static Int32 GetInstallationId(String s3Bucket)
|
|
{
|
|
var part = s3Bucket.Split('-').FirstOrDefault();
|
|
return int.TryParse(part, out var id) ? id : 0; // is 0 a default safe value? check with Marios
|
|
}
|
|
|
|
private static void SendSalimaxStateAlarm(StatusMessage currentSalimaxState, StatusRecord record)
|
|
{
|
|
var s3Bucket = Config.Load().S3?.Bucket;
|
|
var subscribedNow = false;
|
|
|
|
//When the controller boots, it tries to subscribe to the queue
|
|
if (_subscribeToQueueForTheFirstTime == false)
|
|
{
|
|
subscribedNow = true;
|
|
_subscribeToQueueForTheFirstTime = true;
|
|
_prevSodistoreAlarmState = currentSalimaxState.Status;
|
|
_subscribedToQueue = RabbitMqManager.SubscribeToQueue(currentSalimaxState, s3Bucket, VpnServerIp);
|
|
}
|
|
|
|
//If already subscribed to the queue and the status has been changed, update the queue
|
|
if (!subscribedNow && _subscribedToQueue && currentSalimaxState.Status != _prevSodistoreAlarmState)
|
|
{
|
|
_prevSodistoreAlarmState = currentSalimaxState.Status;
|
|
if (s3Bucket != null)
|
|
RabbitMqManager.InformMiddleware(currentSalimaxState);
|
|
}
|
|
|
|
//If there is an available message from the RabbitMQ Broker, apply the configuration file
|
|
Configuration? config = SetConfigurationFile();
|
|
if (config != null)
|
|
{
|
|
record.ApplyConfigFile(config);
|
|
}
|
|
}
|
|
|
|
private static void ApplyDefaultSettings(this StatusRecord? st)
|
|
{
|
|
if (st is null)
|
|
return;
|
|
|
|
st.AcDcGrowatt.EmsCommunicationFailureTime = 20; // 20 sec
|
|
st.AcDcGrowatt.EnableEmsCommunicationFailureTime = true;
|
|
st.AcDcGrowatt.EnableCommand = true;
|
|
st.AcDcGrowatt.ControlPermession = true;
|
|
st.AcDcGrowatt.BatteryChargeCutoffVoltage = 100; //st.Config.BatteryChargeCutoffVoltage;
|
|
st.AcDcGrowatt.BatteryDischargeCutoffVoltage = 20; //st.Config.BatteryDischargeCutoffVoltage;
|
|
st.AcDcGrowatt.BatteryMaxChargeCurrent = 150; //st.Config.BatteryChargeCutoffVoltage;
|
|
st.AcDcGrowatt.BatteryMaxdischargeCurrent = 150; //st.Config.BatteryChargeCutoffVoltage;
|
|
|
|
}
|
|
|
|
private static Dictionary<String, UInt16> ConvertToModbusRegisters(Object value, String outputType, Int32 startingAddress)
|
|
{
|
|
var registers = new Dictionary<String, UInt16>();
|
|
|
|
switch (outputType)
|
|
{
|
|
case "UInt16":
|
|
registers[startingAddress.ToString()] = Convert.ToUInt16(value);
|
|
break;
|
|
|
|
case "Int16":
|
|
var int16Val = Convert.ToInt16(value);
|
|
registers[startingAddress.ToString()] = (UInt16)int16Val; // reinterpret signed as ushort
|
|
break;
|
|
|
|
case "UInt32":
|
|
var uint32Val = Convert.ToUInt32(value);
|
|
registers[startingAddress.ToString()] = (UInt16)(uint32Val & 0xFFFF); // Low word
|
|
registers[(startingAddress + 1).ToString()] = (UInt16)(uint32Val >> 16); // High word
|
|
break;
|
|
|
|
case "Int32":
|
|
var int32Val = Convert.ToInt32(value);
|
|
var raw = unchecked((UInt32)int32Val); // reinterprets signed int as unsigned
|
|
registers[startingAddress.ToString()] = (UInt16)(raw & 0xFFFF);
|
|
registers[(startingAddress + 1).ToString()] = (UInt16)(raw >> 16);
|
|
break;
|
|
|
|
default:
|
|
throw new ArgumentException("Unsupported output type: " + outputType);
|
|
}
|
|
return registers;
|
|
}
|
|
|
|
[RequiresUnreferencedCode("Calls System.Text.Json.JsonSerializer.Serialize<System.Collections.Generic.Dictionary<string, ushort>>(System.Collections.Generic.Dictionary<string, ushort>, System.Text.Json.JsonSerializerOptions?)")]
|
|
private static async Task SaveModbusTcpFile(StatusRecord status)
|
|
{
|
|
var modbusData = new Dictionary<String, UInt16>();
|
|
|
|
Console.WriteLine(new DateTimeOffset(status.AcDcGrowatt.SystemDateTime).ToUnixTimeSeconds() + " This Growatt time");
|
|
// SYSTEM DATA
|
|
var result1 = ConvertToModbusRegisters((status.AcDcGrowatt.VppProtocolVerNumber * 10), "UInt16", 30001);
|
|
var result2 = ConvertToModbusRegisters(status.AcDcGrowatt.SystemDateTime.ToUnixTime(), "UInt32", 30002);
|
|
var result3 = ConvertToModbusRegisters(status.AcDcGrowatt.SystemOperatingMode, "Int16", 30004);
|
|
|
|
// BATTERY SUMMARY (assuming single battery [0])
|
|
var battery = status.AcDcGrowatt.BatteriesRecords!.Batteries[0];
|
|
|
|
var result4 = ConvertToModbusRegisters((status.AcDcGrowatt.BatteriesRecords!.Batteries.Count ), "UInt16", 31000);
|
|
var result5 = ConvertToModbusRegisters((battery.Power.Value * 10), "Int32", 31001);
|
|
var result6 = ConvertToModbusRegisters((battery.DailyChargeEnergy.Value * 10), "UInt32", 31003);
|
|
var result7 = ConvertToModbusRegisters((battery.AccumulatedChargeEnergy.Value * 10), "UInt32", 31005);
|
|
var result8 = ConvertToModbusRegisters((battery.DailyDischargeEnergy.Value * 10), "UInt32", 31007);
|
|
var result9 = ConvertToModbusRegisters((battery.AccumulatedDischargeEnergy.Value * 10), "UInt32", 31009);
|
|
var result10 = ConvertToModbusRegisters((battery.MaxAllowableDischargePower.Value * 10), "UInt32", 31011);
|
|
var result11 = ConvertToModbusRegisters((battery.MaxAllowableDischargePower.Value * 10), "UInt32", 31013);
|
|
|
|
var result12 = ConvertToModbusRegisters((battery.Voltage.Value * 10), "Int16", 31015);
|
|
var result13 = ConvertToModbusRegisters((battery.Current.Value * 10), "Int32", 31016);
|
|
var result14 = ConvertToModbusRegisters((battery.Soc.Value * 100), "UInt16", 31018);
|
|
var result15 = ConvertToModbusRegisters((status.AcDcGrowatt.BatteriesRecords!.AverageSoh * 100), "UInt16", 31019);
|
|
var result16 = ConvertToModbusRegisters((battery.BatteryAmbientTemperature.Value * 100), "UInt16", 31021);
|
|
|
|
|
|
// Merge all results into one dictionary
|
|
var allResults = new[]
|
|
{
|
|
result1, result2, result3, result4, result5, result6, result7, result8,
|
|
result9, result10, result11, result12, result13, result14, result15, result16
|
|
};
|
|
|
|
foreach (var result in allResults)
|
|
{
|
|
foreach (var entry in result)
|
|
{
|
|
modbusData[entry.Key] = entry.Value;
|
|
}
|
|
}
|
|
// Write to JSON
|
|
var json = JsonSerializer.Serialize(modbusData, new JsonSerializerOptions { WriteIndented = true });
|
|
await File.WriteAllTextAsync("/home/inesco/ModbusTCP/modbus_tcp_data.json", json);
|
|
|
|
//Console.WriteLine("JSON file written successfully.");
|
|
//Console.WriteLine(json);
|
|
}
|
|
|
|
private static async Task<Boolean> DataLogging(StatusRecord status, Int64 timestamp)
|
|
{
|
|
var csv = status.ToCsv();
|
|
|
|
// for debug, only to be deleted.
|
|
//foreach (var item in csv.SplitLines())
|
|
//{
|
|
// Console.WriteLine(item + "");
|
|
//}
|
|
|
|
await SavingLocalCsvFile(timestamp, csv);
|
|
|
|
var jsonData = new Dictionary<String, Object>();
|
|
|
|
ConvertToJson(csv, jsonData).LogInfo();
|
|
|
|
var s3Config = status.Config.S3;
|
|
|
|
if (s3Config is null)
|
|
return false;
|
|
|
|
//Concatenating 15 files in one file
|
|
return await ConcatinatingAndCompressingFiles(timestamp, s3Config);
|
|
}
|
|
|
|
private static String ConvertToJson(String csv, Dictionary<String, Object> jsonData)
|
|
{
|
|
foreach (var line in csv.Split('\n'))
|
|
{
|
|
if (string.IsNullOrWhiteSpace(line)) continue;
|
|
|
|
var parts = line.Split(';');
|
|
var keyPath = parts[0];
|
|
var value = parts[1];
|
|
var unit = parts.Length > 2 ? parts[2].Trim() : "";
|
|
InsertIntoJson(jsonData, keyPath.Split('/'), value);
|
|
}
|
|
|
|
var jsonOutput = JsonConvert.SerializeObject(jsonData, Formatting.None);
|
|
return jsonOutput;
|
|
}
|
|
|
|
private static void InsertIntoJson(Dictionary<String, Object> jsonDict, String[] keys, String value)
|
|
{
|
|
var currentDict = jsonDict;
|
|
for (Int16 i = 1; i < keys.Length; i++) // Start at 1 to skip empty root
|
|
{
|
|
var key = keys[i];
|
|
if (!currentDict.ContainsKey(key))
|
|
{
|
|
currentDict[key] = new Dictionary<String, Object>();
|
|
}
|
|
|
|
if (i == keys.Length - 1) // Last key, store the value
|
|
{
|
|
|
|
if (!value.Contains(",") && double.TryParse(value, out Double doubleValue)) // Try to parse value as a number
|
|
{
|
|
currentDict[key] = Math.Round(doubleValue, 2); // Round to 2 decimal places
|
|
|
|
}
|
|
else
|
|
{
|
|
currentDict[key] = value; // Store as string if not a number
|
|
}
|
|
}
|
|
else
|
|
{
|
|
currentDict = (Dictionary<String, Object>)currentDict[key];
|
|
}
|
|
}
|
|
}
|
|
private static async Task SavingLocalCsvFile(Int64 timestamp, String csv)
|
|
{
|
|
const String directoryPath = "/home/inesco/SodiStoreHome/csvFile";
|
|
|
|
// Ensure directory exists
|
|
if (!Directory.Exists(directoryPath))
|
|
{
|
|
Directory.CreateDirectory(directoryPath);
|
|
}
|
|
|
|
// Get all .csv files ordered by creation time (oldest first)
|
|
var csvFiles = new DirectoryInfo(directoryPath)
|
|
.GetFiles("*.csv")
|
|
.OrderBy(f => f.CreationTimeUtc)
|
|
.ToList();
|
|
|
|
// If more than 5000 files, delete the oldest
|
|
if (csvFiles.Count >= 5000)
|
|
{
|
|
var oldestFile = csvFiles.First();
|
|
try
|
|
{
|
|
oldestFile.Delete();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Failed to delete file: {oldestFile.FullName}, Error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
// Prepare the filtered CSV content
|
|
var filteredCsv = csv
|
|
.SplitLines()
|
|
.Where(l => !l.Contains("Secret"))
|
|
.JoinLines();
|
|
|
|
// Save the new CSV file
|
|
var filePath = Path.Combine(directoryPath, timestamp + ".csv");
|
|
await File.WriteAllTextAsync(filePath, filteredCsv);
|
|
}
|
|
|
|
private static async Task<Boolean> ConcatinatingAndCompressingFiles(Int64 timeStamp, S3Config s3Config)
|
|
{
|
|
if (_fileCounter >= NbrOfFileToConcatenate)
|
|
{
|
|
_fileCounter = 0;
|
|
|
|
var logFileConcatenator = new LogFileConcatenator();
|
|
var jsontoSend = logFileConcatenator.ConcatenateFiles(NbrOfFileToConcatenate);
|
|
|
|
var fileNameWithoutExtension = timeStamp.ToString(); // used for both S3 and local
|
|
var s3Path = fileNameWithoutExtension + ".json";
|
|
|
|
var request = s3Config.CreatePutRequest(s3Path);
|
|
|
|
var compressedBytes = CompresseBytes(jsontoSend);
|
|
var base64String = Convert.ToBase64String(compressedBytes);
|
|
var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
|
|
|
|
var uploadSucceeded = false;
|
|
|
|
try
|
|
{
|
|
var response = await request.PutAsync(stringContent);
|
|
|
|
if (response.StatusCode != 200)
|
|
{
|
|
Console.WriteLine("ERROR: PUT");
|
|
var error = await response.GetStringAsync();
|
|
Console.WriteLine(error);
|
|
|
|
await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension);
|
|
//Heartbit();
|
|
return false;
|
|
}
|
|
|
|
uploadSucceeded = true;
|
|
Console.WriteLine("✅ File uploaded to S3 successfully.");
|
|
|
|
Console.WriteLine("---------------------------------------- Resending FailedUploadedFiles----------------------------------------");
|
|
// Heartbit();
|
|
|
|
await ResendLocalFailedFilesAsync(s3Config); // retry any pending failed files
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("Upload exception: " + ex.Message);
|
|
|
|
if (!uploadSucceeded)
|
|
{
|
|
await SaveToLocalCompressedFallback(compressedBytes, fileNameWithoutExtension);
|
|
}
|
|
|
|
//Heartbit();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_fileCounter++;
|
|
return true;
|
|
}
|
|
|
|
private static async Task SaveToLocalCompressedFallback(Byte[] compressedData, String fileNameWithoutExtension)
|
|
{
|
|
try
|
|
{
|
|
var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads");
|
|
Directory.CreateDirectory(fallbackDir);
|
|
|
|
var fileName = fileNameWithoutExtension + ".json"; // Save as .json, but still compressed
|
|
var fullPath = Path.Combine(fallbackDir, fileName);
|
|
|
|
await File.WriteAllBytesAsync(fullPath, compressedData); // Compressed data
|
|
Console.WriteLine($"Saved compressed failed upload to: {fullPath}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine("Failed to save compressed file locally: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private static async Task ResendLocalFailedFilesAsync(S3Config s3Config)
|
|
{
|
|
var fallbackDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FailedUploads");
|
|
|
|
if (!Directory.Exists(fallbackDir))
|
|
return;
|
|
|
|
var files = Directory.GetFiles(fallbackDir, "*.json");
|
|
files.Length.WriteLine(" Number of failed files, to upload");
|
|
|
|
foreach (var filePath in files)
|
|
{
|
|
var fileName = Path.GetFileName(filePath); // e.g., "1720023600.json"
|
|
|
|
try
|
|
{
|
|
byte[] compressedBytes = await File.ReadAllBytesAsync(filePath);
|
|
var base64String = Convert.ToBase64String(compressedBytes);
|
|
var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
|
|
|
|
var request = s3Config.CreatePutRequest(fileName);
|
|
var response = await request.PutAsync(stringContent);
|
|
|
|
if (response.StatusCode == 200)
|
|
{
|
|
File.Delete(filePath);
|
|
Console.WriteLine($"✅ Successfully resent and deleted: {fileName}");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"❌ Failed to resend {fileName}, status: {response.StatusCode}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"⚠️ Exception while resending {fileName}: {ex.Message}");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Byte[] CompresseBytes(String jsonToSend)
|
|
{
|
|
//Compress JSON data to a byte array
|
|
using var memoryStream = new MemoryStream();
|
|
//Create a zip directory and put the compressed file inside
|
|
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
|
|
{
|
|
var entry = archive.CreateEntry("data.json", CompressionLevel.SmallestSize); // Add JSON data to the ZIP archive
|
|
using (var entryStream = entry.Open())
|
|
using (var writer = new StreamWriter(entryStream))
|
|
{
|
|
writer.Write(jsonToSend);
|
|
}
|
|
}
|
|
|
|
var compressedBytes = memoryStream.ToArray();
|
|
|
|
return compressedBytes;
|
|
}
|
|
|
|
private static byte[] ComputeCrc16Appended(byte[] data)
|
|
{
|
|
ushort crc = 0xFFFF;
|
|
|
|
foreach (byte b in data)
|
|
{
|
|
crc ^= b;
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
bool lsb = (crc & 0x0001) != 0;
|
|
crc >>= 1;
|
|
if (lsb)
|
|
{
|
|
crc ^= 0xA001;
|
|
}
|
|
}
|
|
}
|
|
|
|
byte crcLow = (byte)(crc & 0xFF);
|
|
byte crcHigh = (byte)((crc >> 8) & 0xFF);
|
|
|
|
// Create a new array with space for CRC
|
|
byte[] result = new byte[data.Length + 2];
|
|
Array.Copy(data, result, data.Length);
|
|
result[result.Length - 2] = crcLow;
|
|
result[result.Length - 1] = crcHigh;
|
|
|
|
return result;
|
|
}
|
|
} |