using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using Logic.ProductionToProductMapper.Cordonel; using Newtonsoft.Json; using Xylem.Common.Hardware.WaterMeter.Genesis.Applications.Const; using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore; using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore.Consts; using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile; using Xylem.Common.Hardware.WaterMeter.WaterMeterCore; using Xylem.Common.Logic.ProductionOrderCore.OrderData; using Xylem.Common.Logic.SoftwareAccessHelper; using Xylem.Common.Utils.ProcessExec.EventArguments; namespace Xylem.Common.Ui.GenesisToolBox { /// /// FW update form /// public partial class FrmLutUpdate : Form { #region Variables private readonly MeterBatch _meterBatch = new MeterBatch(); private GenesisMeter _currentGenesis; private const String StrConnecting = "Connecting to PCB"; private const String StrNotConnected = "NOT CONNECTED"; private const String StrPartPcbConnected = "PCB ID: "; private const String StrOverallProcess = "Overall Process"; private MeterLutUpdate _meterLutUpdate; private String _lastFwUpdateState; private Int32 _lastSingleProgress; private DateTimeOffset _startTime; private Boolean _resetTimeMeasurement; private readonly Boolean _updateGui = true; private Boolean _lutFileValid; private Boolean _tryConnectPcb; private Boolean _lutFileSuccessfulWritten; private const String StrUpdateErrorMessage = "METROLOGY LOOKUP TABLE UPDATE FAILED!"; private const String StrUpdateSuccessMessage = "Metrology lookup table successfulley written!"; private const String StrLutFileInvalid = "LUT FILE INVALID"; private const String StrLutFileValid = "LUT file loaded"; // external update remarks form private readonly FrmHistory _frmHistory = new FrmHistory(); private readonly Version _version; // status information private static readonly Color ColorDefault = Color.Black; private static readonly Color ColorSuccess = Color.Green; private static readonly Color ColorProcessFailed = Color.Red; //private static readonly Color ColorOngoingProcess = Color.Blue; //private static readonly Color ColorUnknownStatus = Color.Gray; //private const String SuccessSign = @"✔"; //private const String FailedSign = @"✘"; private const String StrSeparator = "-----------------------------------------------------" + "-----------------------------------------------------"; #endregion #region FormControls /// /// Ctor FW update /// public FrmLutUpdate() { InitializeComponent(); var cultureInfo = new CultureInfo("en-GB"); Thread.CurrentThread.CurrentUICulture = cultureInfo; Thread.CurrentThread.CurrentCulture = cultureInfo; _version = Assembly.GetExecutingAssembly().GetName().Version; } /// /// Clear data table and set genesis to not connected /// private void Init() { ProcessViewControl(false); lblConnectPcb.Text = StrNotConnected; lblConnectPcb.ForeColor = Color.Red; lblWaitingForMeterResponse.Visible = false; btnConnect.Enabled = true; btnDownloadLutFile.ForeColor = Color.DarkGreen; btnStopFwUpdate.ForeColor = Color.Red; SetControlDownloadsLocked(); btnStopFwUpdate.Enabled = false; btnOpenLutFile.Enabled = true; btnClearDisplay.Enabled = false; tbxPcbId.Visible = false; } private void FrmLutUpdate_Load(Object sender, EventArgs e) { var version = Assembly.GetExecutingAssembly().GetName().Version; lblFwUpdateInfo.Text = $@"GTB Version: {version.Major}.{version.Minor}.{version.Build}"; _resetTimeMeasurement = true; Init(); var xPosition = Location.X + Size.Width; var yPosition = Location.Y; if (_frmHistory != null) { _frmHistory.SetDesktopLocation(xPosition, yPosition); _frmHistory.Show(); } LogText($"GenesisToolBox: {_version.Major}.{_version.Minor}.{_version.Build}"); LogText(StrSeparator); } private void FrmLutUpdate_FormClosing(Object sender, FormClosingEventArgs e) { _frmHistory?.Close(); _currentGenesis?.Logout(); _meterBatch?.RemoveAllMeters(); _meterBatch?.Dispose(); if (_meterLutUpdate != null) { _meterLutUpdate.OnProcessUpdate -= ProcessUpdate_Event; _meterLutUpdate.Dispose(); _meterLutUpdate = null; } Dispose(); } private void cbComSlot_SelectedIndexChanged(Object sender, EventArgs e) { _resetTimeMeasurement = true; Init(); } #endregion #region ActivationControls private void SetControlDownloadsLocked() { btnDownloadLutFile.Enabled = false; btnStopFwUpdate.Enabled = false; btnConnect.Enabled = true; } private void SetControlsDownloadIsOngoing() { btnConnect.Enabled = false; btnOpenLutFile.Enabled = false; btnDownloadLutFile.Enabled = false; btnClearDisplay.Enabled = false; btnStopFwUpdate.Enabled = true; } private void SetControlsDownloadIsFinished() { _currentGenesis.RequestProtocol.AdditionalRetryTimeoutMs = 0; btnConnect.Enabled = true; btnOpenLutFile.Enabled = true; btnDownloadLutFile.Enabled = true; btnStopFwUpdate.Enabled = false; btnClearDisplay.Enabled = true; } private void SetControlsDownloadEnabled() { btnConnect.Enabled = true; btnOpenLutFile.Enabled = true; btnDownloadLutFile.Enabled = true; btnStopFwUpdate.Enabled = false; btnClearDisplay.Enabled = true; } #endregion #region BoardControls /// /// Establish connection /// /// /// - Compare the max supported FW versions of the configuration.json. /// /// /// - Catch error message on unknown data type and kill meter. /// private void Connect() { try { Init(); if (string.IsNullOrEmpty(cbComSlot.SelectedItem.ToString()) || !int.TryParse(cbComSlot.SelectedItem.ToString(), out var slotNr)) { return; } lblOverall.Text = @"Login to Cordonel..."; ViewProgressPcb(true); _currentGenesis?.DisposeMeter(); //dispose old meter _meterBatch.RemoveAllMeters(); _currentGenesis = null; //assign new meter and assign meter to FW update file if this exists _currentGenesis = new GenesisMeter(); _currentGenesis.UseOfflinePasswords = cbxUseOfflinePwds.Checked; _currentGenesis.SetupFromConfigFile(slotNr); _meterBatch.AddMeter(_currentGenesis); _currentGenesis.Configuration.UseRegisterWatchService = false; _currentGenesis.Configuration.UseMinMaxCheck = false; lblActualProcess.Text = @"Connecting to PCB..."; Task.Factory.StartNew(() => { _meterBatch.MetersLogin(); if (!_currentGenesis.IsLoggedOn) { //this is needed from meter as an unsuccessful login requires a delay for the next trial Thread.Sleep(2000); _meterBatch.MetersLogin(); } if (!_currentGenesis.IsLoggedOn) { return; } _meterLutUpdate?.AssignGenesis(_currentGenesis); Invoke(new Action(() => { lblConnectPcb.Text = StrPartPcbConnected; tbxPcbId.Text = _currentGenesis.PcbId; tbxPcbId.Visible = true; tbxMeterFw.Text = _currentGenesis.FwVersion; tbxMeterMeterSize.Text = _currentGenesis.MeterSize; tbxMeterLutCrc.Text = _currentGenesis.LutCrc; tbxRegion.Text = _currentGenesis.Region; LogText("Meter FW Version: " + _currentGenesis.FwVersion); LogText("Meter LUT CRC: " + _currentGenesis.LutCrc); LogText("Meter Size: " + _currentGenesis.MeterSize); LogText("Meter Region: " + _currentGenesis.Region); if (_currentGenesis.RadioFrequencyMhz != null) { tbxRadio.Text = $@"{_currentGenesis.RadioFrequencyMhz}"; LogText($"Meter Radio Frequency [MHz]: {_currentGenesis.RadioFrequencyMhz}"); } LogText("Configuration.json version: " + _currentGenesis.InterfaceInfo.InterfaceVersion); lblConfigVersion.Text = @"Configuration Version: " + _currentGenesis.InterfaceInfo.InterfaceVersion; lblConfigVersion.ForeColor = _currentGenesis.InterfaceSupportsFwVersion ? Color.Green: Color.Red; if (!_currentGenesis.InterfaceSupportsFwVersion) { var text = "CONFIGURATION OUTDATED!\n" + "The loaded \"configuration.json\" " + $"version: {_currentGenesis.InterfaceInfo.InterfaceVersion}\n" + $"does NOT support the Cordonel FW version: {_currentGenesis.FwVersion}!"; MessageBox.Show(text, @"FAILED", MessageBoxButtons.OK, MessageBoxIcon.Error); LogText(text); } else { LogText($"Configuration.json supports installed FW version: {_currentGenesis.FwVersion}"); } lblConnectPcb.ForeColor = Color.Green; CheckUpdateEnabled(); _currentGenesis.Logout(); LogInstalledMeterFw(); })); }).ContinueWith(delegate { ViewProgressPcb(false); }); } catch (Exception ex) { ViewProgressPcb(false); MessageBox.Show(ex.Message, @"FAILED", MessageBoxButtons.OK, MessageBoxIcon.Error); LogErrorText(ex.Message); // Dispose meter _meterBatch.RemoveAllMeters(); // If meter is not already assigned to batch as the config reader may fail _currentGenesis?.DisposeMeter(); } } #endregion #region ProcessControls private void ViewProgressPcb(Boolean isActive) { if (_updateGui) { if (isActive) { Invoke(new Action(() => { _tryConnectPcb = true; ProcessViewControl(true); lblActualProcess.Text = StrConnecting; lblOverall.Text = StrOverallProcess; })); } else { Invoke(new Action(() => { _tryConnectPcb = false; ProcessViewControl(false); btnClearDisplay.Enabled = true; })); } } } /// /// View all process bars and labels /// private void ProcessViewControl(Boolean view) { if (view) { lblActualProcess.Visible = true; lblOverall.Visible = true; barOverallProgressUpdate.Visible = true; barSingleProgressUpdate.Visible = true; _lastFwUpdateState = ""; tmrProgressUpdate.Enabled = true; } else { lblActualProcess.Visible = false; lblOverall.Visible = false; barOverallProgressUpdate.Visible = false; barSingleProgressUpdate.Visible = false; tmrProgressUpdate.Enabled = false; _lastFwUpdateState = ""; lblWaitingForMeterResponse.Visible = false; lblActualProcess.Text = ""; lblOverall.Text = ""; } //common actions and settings lblActualProcess.Update(); lblOverall.Update(); barOverallProgressUpdate.Value = 0; barOverallProgressUpdate.Update(); barSingleProgressUpdate.Value = 0; barSingleProgressUpdate.Update(); Update(); } private void StartStopDownload(Boolean isActive) { if (isActive) { Invoke(new Action(() => { ProcessViewControl(true); tmrProgressUpdate.Enabled = true; })); } else { Invoke(new Action(() => { ProcessViewControl(false); SetControlsDownloadIsFinished(); // update the display of the meter size, LUT CRC and region tbxMeterFw.Text = _currentGenesis.FwVersion; tbxMeterMeterSize.Text = _currentGenesis.MeterSize; tbxMeterLutCrc.Text = _currentGenesis.LutCrc; tbxRegion.Text = _currentGenesis.Region; if (!_lutFileSuccessfulWritten) { lblLutFileLoadedInfo.ForeColor = Color.Red; lblLutFileLoadedInfo.Text = StrUpdateErrorMessage; var txt = StrUpdateErrorMessage; if (!string.IsNullOrEmpty(_meterLutUpdate?.ErrorMessage)) { txt += "\n"; txt += _meterLutUpdate?.ErrorMessage; } LogErrorText(txt); MessageBoxShow(txt, @"FAILED", MessageBoxButtons.OK, MessageBoxIcon.Error); } else { LogSuccessText(StrUpdateSuccessMessage); MessageBoxShow(StrUpdateSuccessMessage, @"SUCCESS", MessageBoxButtons.OK, MessageBoxIcon.Information); } })); } } /// /// Display installed meter FW. /// /// /// - Init. /// private void LogInstalledMeterFw() { if (_currentGenesis == null) return; LogText(StrSeparator); LogText($"PCB ID: {_currentGenesis.PcbId}"); LogText($"Core Version: {_currentGenesis.StrCoreRevision}"); LogText(""); LogText("Installed Applications:"); foreach (var app in _currentGenesis.MeterAppListVersion) { var versionString = app.IsInstalled ? $"V: {app.StrVersion} - CRC: 0x{app.Crc:X4}" : "App NOT Installed"; if (app.Status == MeterAppState.Unknown) versionString = "COMMUNICATION ERROR"; LogText($"AppId: 0x{app.AppId:X2} - {versionString} - " + $"AppName: {app.AppName}"); if (app.IsInstalled) { var appVersion = new CordonelAppVersion(app.AppId, app.StrVersion); // Copy metrology update permission if (app.AppName == MetrologyDefinition.MetrologyName && _currentGenesis.MetrologyUpgradePermission != MetrologyDefinition.MetrologyUpgradePermitted) { appVersion.IsUpdateable = false; } } } LogText(StrSeparator); } /// /// Clear history window. /// /// /// - Initial /// private void ClearHistoryWindow() { if (_frmHistory != null && _frmHistory.rtbHistory.InvokeRequired) { _frmHistory.rtbHistory.Invoke(new Action(() => { _frmHistory.rtbHistory.Clear(); })); } else _frmHistory?.rtbHistory.Clear(); } /// /// Output exclusively to user update remarks text window. /// /// /// - Color added. /// private void LogText(String txtHistory) { InfoWindowColoredText(txtHistory, ColorDefault); } /// /// Output exclusively to user update remarks text window. /// private void LogErrorText(String txtHistory) { InfoWindowColoredText(txtHistory, ColorProcessFailed); } /// /// Output exclusively to user update remarks text window. /// private void LogSuccessText(String txtHistory) { InfoWindowColoredText(txtHistory, ColorSuccess); } /// /// Output exclusively to user update remarks text window. /// private void InfoWindowColoredText(String txtHistory, Color color) { if (_frmHistory?.rtbHistory == null) return; Invoke(new Action(() => { _frmHistory.rtbHistory.SuspendLayout(); _frmHistory.rtbHistory.SelectionStart = _frmHistory.rtbHistory.Text.Length; _frmHistory.rtbHistory.SelectionLength = 0; _frmHistory.rtbHistory.SelectionColor = color; _frmHistory.rtbHistory.AppendText($"{txtHistory}{Environment.NewLine}"); _frmHistory.rtbHistory.SelectionColor = _frmHistory.rtbHistory.ForeColor; _frmHistory.rtbHistory.ScrollToCaret(); _frmHistory.rtbHistory.ResumeLayout(); })); } #endregion #region Checks private void CheckUpdateEnabled() { //this check has been placed here to force display update if (_meterLutUpdate != null && _currentGenesis != null && _lutFileValid) { SetControlsDownloadEnabled(); return; } SetControlDownloadsLocked(); } #endregion #region Events /// /// Process update event for displaying messages in history window /// /// /// /// /// - Initial /// public virtual void ProcessUpdate_Event(Object sender, ProcessExecEventArgs e) { LogText(e.OverallProcessMessage); } #endregion #region TimerControls private void tmrProgressUpdate_Tick(Object sender, EventArgs e) { //cbxManualControl.Visible = true; if (_tryConnectPcb) { barOverallProgressUpdate.Value = barOverallProgressUpdate.Value + 1 > 100 ? 0 : barOverallProgressUpdate.Value + 1; barSingleProgressUpdate.Value = barSingleProgressUpdate.Value + 4 > 100 ? 0 : barSingleProgressUpdate.Value + 4; lblOverall.Text = StrOverallProcess; } else { lblOverall.Text = _meterLutUpdate?.GetLutUpdateStateOperation(); lblActualProcess.Text = _meterLutUpdate?.GetActualOperation(); var overallProgress = (Int32)(_meterLutUpdate?.OverallProcessCtrPercent ?? 0); if (overallProgress < 0) overallProgress = 0; barOverallProgressUpdate.Value = overallProgress > 100 ? 100 : overallProgress; barOverallProgressUpdate.Update(); var singleProgress = (Int32)(_meterLutUpdate?.SingleFileProcessCtrPercent ?? 0); if (_meterLutUpdate?.GetActualOperation() != _lastFwUpdateState) { _lastFwUpdateState = _meterLutUpdate?.GetActualOperation(); lblWaitingForMeterResponse.Visible = false; } else { //if the communication gets frozen lblWaitingForMeterResponse.Visible = _lastSingleProgress == singleProgress; } barSingleProgressUpdate.Value = singleProgress > 100 ? 100 : singleProgress; _lastSingleProgress = singleProgress; barSingleProgressUpdate.Update(); } SetTimeDisplay(); Update(); } private void SetTimeDisplay() { var time = DateTimeOffset.UtcNow; var timeSpan = time - _startTime; lblUpdateTime.Text = $@"{(UInt32)timeSpan.TotalMinutes}:{timeSpan.Seconds:00}"; } #endregion #region Buttons private void btnConnect_Click(Object sender, EventArgs e) { if (_resetTimeMeasurement) { _startTime = DateTimeOffset.UtcNow; _resetTimeMeasurement = false; } Connect(); } private void btnOpenLutFile_Click(Object sender, EventArgs e) { if (dlgOpenLuFile.ShowDialog() != DialogResult.OK) { return; } var lutFilePathName = dlgOpenLuFile.FileName; lblLutFileLoadedInfo.Text = ""; tbxLutFilePathName.Text = lutFilePathName; if (_meterLutUpdate == null) { _meterLutUpdate = new MeterLutUpdate(_currentGenesis); _meterLutUpdate.OnProcessUpdate += ProcessUpdate_Event; } if (_meterLutUpdate == null) { return; } if (_meterLutUpdate.LoadLutFile(lutFilePathName)) { lblLutFileLoadedInfo.ForeColor = Color.Green; lblLutFileLoadedInfo.Text = StrLutFileValid; tbxFileCrc.Text = $@"0x{_meterLutUpdate.LutFile.FileCrc:X4}"; tbxFileMeterSize.Text = MeterSizeConverter.ConvertMeterSizeEnumToSizeName(_meterLutUpdate.LutFile.MeterSize); _lutFileValid = true; LogText("File Meter Size: " + tbxFileMeterSize.Text); LogText("File LUT CRC: " + tbxFileCrc.Text); LogText(StrSeparator); } else { var txt = ""; if (!string.IsNullOrEmpty(_meterLutUpdate?.ErrorMessage)) { txt += _meterLutUpdate?.ErrorMessage; } else { txt = StrLutFileInvalid; } LogErrorText(txt); lblLutFileLoadedInfo.ForeColor = Color.Red; lblLutFileLoadedInfo.Text = txt; tbxFileCrc.Text = @" --- "; tbxFileMeterSize.Text = @" --- "; _lutFileValid = false; } CheckUpdateEnabled(); _resetTimeMeasurement = true; } /// /// Download LUT file /// /// /// private void btnDownloadLutFile_Click(Object sender, EventArgs e) { if (_currentGenesis == null || _meterLutUpdate?.LutFile == null) { return; } lblLutFileLoadedInfo.Text = ""; SetControlsDownloadIsOngoing(); StartStopDownload(true); _startTime = DateTimeOffset.UtcNow; _resetTimeMeasurement = false; Task.Factory.StartNew(() => { _lutFileSuccessfulWritten = _meterLutUpdate.UpdateMeterLut(); Thread.Sleep(1000); }).ContinueWith(delegate { StartStopDownload(false); }); } private void btnStopFwUpdate_Click(Object sender, EventArgs e) { if (_meterLutUpdate != null) { _meterLutUpdate.StopUpdateProcess = true; } } #endregion #region ExternalCalls /// /// /// public MsgEventArgs LastMsgEvent; /// public class MsgEventArgs : EventArgs { /// /// Text to display /// public String Text; /// /// Headline /// public String Caption; /// /// Buttons /// public MessageBoxButtons Buttons; /// /// Icon /// public MessageBoxIcon Icon; } /// /// Message pop up event handler /// public event EventHandler OnMsgPopUp; private void MessageBoxShow(String text, String caption, MessageBoxButtons buttons, MessageBoxIcon icon) { if (_updateGui) { MessageBox.Show(text, caption, buttons, icon); } else { LastMsgEvent = new MsgEventArgs() { Text = text, Caption = caption, Buttons = buttons, Icon = icon }; OnMsgPopUp?.Invoke(this, LastMsgEvent); } } #endregion private void SwitchView(Object sender, EventArgs e) { btnLoadfromDb.Enabled = false; var file = rbtFile.Checked; cbxDbLuts.Visible = !file; lblSelectDb.Visible = !file; btnLoadfromDb.Visible = !file; lvlSelechtFile.Visible = file; tbxLutFilePathName.Visible = file; btnOpenLutFile.Visible = file; if (!file) { LoadDbLuts(rbtProd.Checked); } } private void LoadDbLuts(Boolean prod) { var sbUrl = new StringBuilder(); sbUrl.Append("http://10.49.40.25/MeterProcessState/api/FileUpToDate/GetLutFiles"); //sbUrl.Append("http://localhost:56011/api/FileUpToDate/GetLutFiles"); //sbUrl.Append($"?DataLength={_DataLength}"); //sbUrl.Append($"&version={_Version}"); //sbUrl.Append($"&crc={_FileCrc}"); //sbUrl.Append($"&Metersize={_MeterSize.GetHashCode()}"); //sbUrl.Append($"&Region={_StrLutRegion}"); try { var ret = LocalWebRequest.GetRequest(sbUrl.ToString(), 80000); var a = JsonConvert.DeserializeObject>(ret); cbxDbLuts.SelectedIndex = -1; cbxDbLuts.Items.Clear(); if (a == null) return; foreach (var item in a) { if (!prod || item.CurrentVersionForProd) { cbxDbLuts.Items.Add(item); } } } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void cbxDbLuts_SelectedIndexChanged(Object sender, EventArgs e) { if (cbxDbLuts.SelectedItem is CordonelMetrologyLut) { btnLoadfromDb.Enabled = true; } else { btnLoadfromDb.Enabled = false; } } private void btnLoadfromDb_Click(Object sender, EventArgs e) { if (cbxDbLuts.SelectedItem is CordonelMetrologyLut lut) { lblLutFileLoadedInfo.Text = ""; tbxLutFilePathName.Text = $@"fromDB{lut.FileName}"; if (_meterLutUpdate == null) { _meterLutUpdate = new MeterLutUpdate(_currentGenesis); _meterLutUpdate.OnProcessUpdate += ProcessUpdate_Event; } if (_meterLutUpdate == null) { return; } if (_meterLutUpdate.LoadLutFromStream(lut.FileName, lut.FileContent.ToList())) { lblLutFileLoadedInfo.ForeColor = Color.Green; lblLutFileLoadedInfo.Text = StrLutFileValid; tbxFileCrc.Text = $@"0x{_meterLutUpdate.LutFile.FileCrc:X4}"; tbxFileMeterSize.Text = MeterSizeConverter.ConvertMeterSizeEnumToSizeName(_meterLutUpdate.LutFile.MeterSize); _lutFileValid = true; LogText("File Meter Size: " + tbxFileMeterSize.Text); LogText("File LUT CRC: " + tbxFileCrc.Text); LogText(StrSeparator); } else { var txt = ""; if (!string.IsNullOrEmpty(_meterLutUpdate?.ErrorMessage)) { txt += _meterLutUpdate?.ErrorMessage; } else { txt = StrLutFileInvalid; } LogErrorText(txt); lblLutFileLoadedInfo.ForeColor = Color.Red; lblLutFileLoadedInfo.Text = txt; tbxFileCrc.Text = @" --- "; tbxFileMeterSize.Text = @" --- "; _lutFileValid = false; } CheckUpdateEnabled(); _resetTimeMeasurement = true; } } private void rbtProd_CheckedChanged(Object sender, EventArgs e) { } private void btnClearDisplay_Click(Object sender, EventArgs e) { if (_currentGenesis == null) return; lblOverall.Text = @"Set display to normal operation"; lblOverall.Visible = true; Update(); if (!_currentGenesis.IsLoggedOn) { _currentGenesis.ReLogin(); } _currentGenesis.SetLcdText(true, new Byte[] { 0x00, 0x00, 0x00, 0x00 }); _currentGenesis.Logout(); lblOverall.Visible = false; } private void cbxUseOfflinePwds_CheckedChanged(Object sender, EventArgs e) { cbxUseOfflinePwds.ForeColor = cbxUseOfflinePwds.Checked ? Color.Red : Color.Black; } } }