Strange behaviour when communicating over serial from VS WPF to Arduino - c#

A basic overview. I am sending serial data from an Arduino Due to WPF application. Up until now this has all been working perfectly. Today I implemented a loop into the Arduino code that looks for a "Y" (ascii 89) in the serial port, if received it leaves the loop and returns back into what I am calling offline mode and stops sending over data, via online = false.
Now what is strange about this is that...
It was working fine before this loop so it must be something to do with trying to resend new data once it has left the 'online loop'.
It works perfectly from the Arduino serial monitor, which suggests it's a WPF problem, although the code hasn't changed on the upload section.
The code for both of these programmes is pretty big so I will try and keep it concise whilst providing all the information necessary.
void loop() {
// Check to see if the testbench is in offline mode and run the respective code.
if (Online == false) {
OfflineMode();
}
// Check to see if the testbench is in online mode and run the respective code.
if (Online == true) {
OnlineMode();
}
}
void OfflineMode() {
while (Serial.available())
processlncomingByte(Serial.read());
}
I then have switch cases to handle incoming settings - I know this works fine as it will also upload after the Arduino is reset.
void processlncomingByte (const byte c) {
if (isdigit (c)) {
currentValue *= 10;
currentValue += c - '0';
} else {
// end of digit
// The end of the number signals a state change
handlePreviousState ();
// set the new state, if we recognize it
switch (c) {
case 'A':
state = GOT_A;
break;
etc...
Online Mode
void OnlineMode() {
CheckForStop();
SendSerialData();
}
void CheckForStop() {
//Serial.println("...");
if (Serial.available() > 0) {
//Serial.println("getting something");
ch = (char)Serial.read();
inputString = ch;
if (ch == 89) {
//Serial.end();
Online = false;
//Serial.begin(9600);
exit;
//return;
}
} else
delay(5);
}
SendSerialData() consists of just a range of serial.print, outputting into one large string for WPF to handle.
Here is a screenshot of the serial monitor working
As you will see from the link above the monitor spits out a load of data, stops when I send a Y and finally I send a Q to 'question' whether the Arduino is ready to receive settings and S signifies a Yes. Great stuff it works!
However as you can see from the link below this isn't the case in WPF. Sorry, I can only upload 2 images at the moment so had to combine them.
Combo of screenshots
Here is the loop it is currently getting stuck in
private bool checkArduinoisReady() {
Stopwatch Uploadtimer = new Stopwatch();
if (!myPort.IsOpen)
return false;
// Debug.Print("port is ready to be opened");
string tempdata;
Uploadtimer.Start();
myPort.DiscardInBuffer();
Start:
myPort.WriteLine("Q" + Environment.NewLine);
Debug.Print("sent Q");
tempdata = myPort.ReadExisting();
Debug.Print("tempdata_" + tempdata.ToString());
if (Uploadtimer.ElapsedMilliseconds > 5000)
return false;
if (tempdata.Contains("S"))
return true;
else
goto Start;
}
And on a separate page this is how I am stopping the incoming data.
private void StopTest(object sender, RoutedEventArgs e) {
MessageBoxResult StopConfirm = MessageBox.Show("Are you sure you want to stop the test?", "Stop the test", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (StopConfirm == MessageBoxResult.Yes) {
Timer.Stop();
Debug.Print("Timer Stopped");
myPort.DiscardInBuffer();
Start:
for (int i = 0; i < 100; i++) {
myPort.WriteLine("Y");
}
string tempData = myPort.ReadExisting();
Debug.Print("Checking...");
Debug.Print("tempData_" + tempData);
if (string.IsNullOrWhiteSpace(tempData)) {
Debug.Print("Its null!!");
comments_textbox.Text = comments_textbox.Text + "Test Aborted";
MessageBoxResult SaveCurrentData = MessageBox.Show("Would you like to save the data collected up until this point?", "Save", MessageBoxButton.YesNo, MessageBoxImage.Question);
if (SaveCurrentData == MessageBoxResult.Yes) {
SaveFile();
}
if (SaveCurrentData == MessageBoxResult.No) {
myPort.Close();
NavigationService.Navigate(new Uri("testSettings.xaml", UriKind.RelativeOrAbsolute));
}
} else {
Debug.Print("Still going...");
goto Start;
}
}
}
The biggest stumbling block for me is why this works over the serial monitor but not within the application. And it also works as soon as I reset the Arduino. I have also tried the resetFunc() in Arduino but this didn't help either.
Thanks in advance.

It turns out i still had a resetFunc() in my switch case which was preventing the serial monitor from continuing to send data!

Related

Android: Sending Metadata from media player app to car stereo via bluetooth blocks broadcast receiver to play next song or pause music

I am working on a little media player app. When I go into my car, I want the app to play music and show the current song's metadata on my car stereo's screen. But I also want the media player to play the NEXT song, when I press NEXT on my car stereo.
I got both function to work already, but not together.
Sounds a bit wierd but let me explain:
First, I set up a broadcast receiver in my app to catch the next, prev, paus, and stop clicks from a remote bluetooth device, such as a car stereo ( or a headset, etc). This looks like this:
[BroadcastReceiver]
[IntentFilter(new[] { Intent.ActionMediaButton })]
public class MyMediaButtonBroadcastReceiver : BroadcastReceiver
{
public string ComponentName { get { return Class.Name; } }
static long lastClick = 0;
public override void OnReceive(Context context, Intent intent)
{
if (intent.Action != Intent.ActionMediaButton)
return;
var keyEvent = (KeyEvent)intent.GetParcelableExtra(Intent.ExtraKeyEvent);
switch (keyEvent.KeyCode)
{
case Keycode.Headsethook:
if(canClick())
Activity_Player.Instance.PlayOrPauseLogic();
break;
case Keycode.MediaPlay:
if (canClick())
Activity_Player.Instance.PlayOrPauseLogic();
break;
case Keycode.MediaPlayPause:
if (canClick())
Activity_Player.Instance.PlayOrPauseLogic();
break;
case Keycode.MediaNext:
if(Activity_Player.CurrentSongObject != null && canClick())
Activity_Player.Instance.ChooseRandomNewSongAndPlay(false);
break;
case Keycode.MediaPrevious:
if (Activity_Player.CurrentSongObject != null && canClick())
Activity_Player.mediaPlayer.SeekTo(0);
break;
}
if (intent.GetStringExtra(BluetoothAdapter.ExtraState) == BluetoothAdapter.ActionConnectionStateChanged)
{
Toast.MakeText(Activity_Player.ctx, "connection off1", ToastLength.Short).Show();
if (canClick())
Activity_Player.Instance.PlayOrPauseLogic();
}
}
private bool canClick()
{
if(lastClick < Java.Lang.JavaSystem.CurrentTimeMillis() - 500) // needs to be atleast one second bigger
{
lastClick = Java.Lang.JavaSystem.CurrentTimeMillis();
return true;
}
else
return false;
}
}
I Init this broadcast receiver in my app like so:
private void RegisterBroadCastReceiver()
{
var am = (AudioManager)this.GetSystemService(AudioService);
var componentName = new ComponentName(PackageName, new MyMediaButtonBroadcastReceiver().ComponentName);
am.RegisterMediaButtonEventReceiver(componentName);
}
Again: this totally worked perfectly.
But then I noticed that my car stereo does not display the current song information (meta data from the mp3) on its screen. So with a bit of googeling I coded this:
private void InitBluetoohSending()
{
if (mAudioManager == null)
{
mAudioManager = (AudioManager)GetSystemService(Context.AudioService);
}
if (Build.VERSION.SdkInt < Build.VERSION_CODES.Lollipop)
{
if (mRemoteControlClient == null)
{
mRemoteControlClient = new RemoteControlClient(PendingIntent.GetBroadcast(this, 0, new Intent(Intent.ActionMediaButton), 0));
mAudioManager.RegisterRemoteControlClient(mRemoteControlClient);
}
}
else
{
if (mMediaSession == null)
{
mMediaSession = new MediaSession(this, "PlayerServiceMediaSession");
mMediaSession.SetFlags(MediaSession.FlagHandlesTransportControls);
mMediaSession.Active = true;
}
}
}
AND:
public static void SendInfoToBluetoothDevice()
{
if (Build.VERSION.SdkInt < Build.VERSION_CODES.Lollipop)
{
RemoteControlClient.MetadataEditor ed = mRemoteControlClient.EditMetadata(true);
ed.PutString(MediaMetadataRetriever.MetadataKeyTitle, CurrentSongObject.SongName);
ed.PutString(MediaMetadataRetriever.MetadataKeyArtist, CurrentSongObject.ArtistName);
ed.PutString(MediaMetadataRetriever.MetadataKeyAlbum, CurrentSongObject.AlbumName);
ed.PutLong(MediaMetadataRetriever.MetadataKeyDuration, CurrentSongObject.DurationInSec);
ed.Apply();
mRemoteControlClient.SetPlaybackState(RemoteControlClient.PlaystatePlaying, mediaPlayer.CurrentPosition, 1.0f);
}
else
{
MediaMetadata metadata = new MediaMetadata.Builder()
.PutString(MediaMetadata.MetadataKeyTitle, CurrentSongObject.SongName)
.PutString(MediaMetadata.MetadataKeyArtist, CurrentSongObject.ArtistName)
.PutString(MediaMetadata.MetadataKeyAlbum, CurrentSongObject.AlbumName)
.PutLong(MediaMetadata.MetadataKeyDuration, CurrentSongObject.DurationInSec)
.Build();
mMediaSession.SetMetadata(metadata);
PlaybackState state = new PlaybackState.Builder()
.SetActions(PlaybackState.ActionPlay)
.SetState(PlaybackState.StatePlaying, mediaPlayer.CurrentPosition, 1.0f, SystemClock.ElapsedRealtime())
.Build();
mMediaSession.SetPlaybackState(state);
}
}
Again, this works fine. I am calling the last function everytime I change the track on my phone and the car stereo displays everything as it should BUT now the broadcast receiver to catch the next, pause, prev, and play clicks from the car stopped working immediately.
They do however still work, when the phone is NOT sending out the info. Before sending info out, I am checking wether a device is connected or not like so:
if (mAudioManager.BluetoothA2dpOn)
{
SendInfoToBluetoothDevice();
}
With my headset on but no device connected, I can stop my music with the headset just fine. As soon as I am in my car and the car stereo now displays the songs (a device is now connected) I cannot use my headset hook anymore, neither my next or prev buttons on my car ...
This is especially frustrating since I have to try some code and then go back to my car with a release version on my phone to see if anything worked, but with no debugger attached. Also, I dont know what to change since both things work just fine individually but stop working once used together. Or at least the receiver stops working entirely when the sender is active.
Please, can anyone help me out here?
Thanks a lot!
Thank god, I have found it!
The problem was the mediaSession. This itself registered a broadcast receiver which interefered with my custom one. The issue was very simple:
First, remove the "old" broadcast receiver entirely, then go:
mMediaSession = new MediaSession(this, "PlayerServiceMediaSession");
mMediaSession.SetFlags(MediaSession.FlagHandlesTransportControls);
mMediaSession.SetFlags(MediaSession.FlagHandlesMediaButtons);
mMediaSession.Active = true;
mMediaSession.SetCallback(new MediaButtonReceiver(this));
And for the custom class:
public class MediaButtonReceiver : MediaSession.Callback
{
static long lastClick = 0;
Context ctx;
public MediaButtonReceiver(Context ctx)
{
this.ctx = ctx;
}
public override bool OnMediaButtonEvent(Intent mediaButtonIntent)
{
if (mediaButtonIntent.Action != Intent.ActionMediaButton)
return false;
var keyEvent = (KeyEvent)mediaButtonIntent.GetParcelableExtra(Intent.ExtraKeyEvent);
switch (keyEvent.KeyCode)
{
case Keycode.Headsethook:
if (canClick())
Activity_Player.Instance.PlayOrPauseLogic();
break;
case Keycode.MediaPlay:
if (canClick())
Activity_Player.Instance.PlayOrPauseLogic();
break;
case Keycode.MediaPlayPause:
if (canClick())
Activity_Player.Instance.PlayOrPauseLogic();
break;
case Keycode.MediaNext:
if (Activity_Player.CurrentSongObject != null && canClick())
Activity_Player.Instance.ChooseRandomNewSongAndPlay(false);
break;
case Keycode.MediaPrevious:
if (Activity_Player.CurrentSongObject != null && canClick())
Activity_Player.mediaPlayer.SeekTo(0);
break;
}
return base.OnMediaButtonEvent(mediaButtonIntent);
}
private bool canClick()
{
if (lastClick < Java.Lang.JavaSystem.CurrentTimeMillis() - 500) // needs to be atleast one second bigger
{
lastClick = Java.Lang.JavaSystem.CurrentTimeMillis();
return true;
}
else
return false;
}
}

How to check output from the serial port match a message to continue write a command in C#?

I use this code to initialize a serial port in C# :
serialPort.PortName = cboCOMPort.Text;
serialPort.BaudRate = Convert.ToInt32(cboBaudRate.Text);
serialPort.Parity = Parity.None;
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;
serialPort.Handshake = Handshake.None;
serialPort.WriteTimeout = 500;
serialPort.ReadTimeout = 500;
serialPort.DataReceived += new SerialDataReceivedEventHandler(SerialDataReceived);
serialPort.Open();
then use this code to read data from serial port :
private void SerialDataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort serialPort = (SerialPort)sender;
strDataReceived = serialPort.ReadExisting();
ShowSerialOutput(strDataReceived);
}
and write :
private void SerialDataSend(string strCommand)
{
serialPort.WriteLine(strCommand);
}
My problem : when a device (example a switch) connects to the serial port and executes a command I have written, my program has to wait for the switch finishes executing this command before writing a new command to the serial port. How to check if the switch is finished? (when finished, the switch will send some message contain the keywords like 'completed', 'finished',...). I have tried to use this code but not work :
while(true)
{
if(serialPort.ReadLine().Contains("completed"))
serialDataSend(nextCommand);
}
Sorry for not clearly explaining my problem.
**Example my problem : **
I connect to the switch using serial port and use my program to read/write data. I want to copy a large file from server to the switch using this command : cp tftp://10.0.0.1/file.tgz /var/tmp/file.tgz. I use my program to write this command to the serial port and the switch executes this command. The file is very large so the program need to wait the file copied completely before sending the next command. When finished, the switch show message "Completed". That is my problem : how to check the copy process completed to write the new command.
I will try to approach your problem. Probably will need to rephrase as more information might come from you :)
As I understand it you have more than 1 command that you are sending to the device. Something like a command-list.
As you already have the event for data reception you could also use it to verify whether the completed keyword has been send and set a flag.
EDIT: The flag you can use for the while-wait-loop as you already do.
Your program will wait there until your device confirms the completion and jump out of the loop. Then you need to reset the flag for the next waiting loop.
public class DeviceCommunication
{
bool FlagToProceed = false;
private void MainJob()
{
// send command 1...
SerialDataSend("cp tftp://10.0.0.1/file.tgz /var/tmp/file.tgz.");
// wait
while(!FlagToProceed)
{}
// reset the flag
FlagToProceed = false;
// send command 2 ...
SerialDataSend("WhatEverComesNext");
// wait
while(!FlagToProceed)
{}
// reset the flag
FlagToProceed = false;
}
private void SerialDataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort serialPort = (SerialPort)sender;
strDataReceived = serialPort.ReadExisting();
ShowSerialOutput(strDataReceived);
// check whether affirmation has been received and open the gates
if(strDataReceived.Contains("completed"))
{
FlagToProceed = true;
}
}
private void SerialDataSend(string strCommand)
{
serialPort.WriteLine(strCommand);
}
}
This waiting technique is not a very good one! It is just the closest to your posted code. If you want to use it, I would suggest to have a timer running and break out of the while - loop if timeout is reached. Otherwise you might keep stuck in the while loop if the device decides not to confirm with "complete" or data just gets lost during transmission.
Another possibility to solve your problem could be to apply an asynch / await approach.
EDIT: be ware of case sensitivity! "Complete" & "complete" is not the same for the Contains method!
You need to use a state machine and delegates to achieve what you are trying to do. See the code below, I recommend doing all this in a separate thread other then Main. You keep track of the state you're in, and when you get a response you parse it with the correct callback function and if it is what you are expecting you move onto the next send command state.
private delegate void CallbackFunction(String Response); //our generic Delegate
private CallbackFunction CallbackResponse; //instantiate our delegate
private StateMachine currentState = ATRHBPCalStateMachine.Waiting;
SerialPort sp; //our serial port
private enum StateMachine
{
Waiting,
SendCmd1,
Cmd1Response,
SendCmd2,
Cmd2Response,
Error
}
private void do_State_Machine()
{
switch (StateMachine)
{
case StateMachine.Waiting:
//do nothing
break;
case StateMachine.SendCmd1:
CallbackResponse = Cmd1Response; //set our delegate to the first response
sp.Write("Send first command1"); //send our command through the serial port
currentState = StateMachine.Cmd1Response; //change to cmd1 response state
break;
case StateMachine.Cmd1Response:
//waiting for a response....you can put a timeout here
break;
case StateMachine.SendCmd2:
CallbackResponse = Cmd2Response; //set our delegate to the second response
sp.Write("Send command2"); //send our command through the serial port
currentState = StateMachine.Cmd2Response; //change to cmd1 response state
break;
case StateMachine.Cmd2Response:
//waiting for a response....you can put a timeout here
break;
case StateMachine.Error:
//error occurred do something
break;
}
}
private void Cmd1Response(string s)
{
//Parse the string, make sure its what you expect
//if it is, then set the next state to run the next command
if(s.contains("expected"))
{
currentState = StateMachine.SendCmd2;
}
else
{
currentState = StateMachine.Error;
}
}
private void Cmd2Response(string s)
{
//Parse the string, make sure its what you expect
//if it is, then set the next state to run the next command
if(s.contains("expected"))
{
currentState = StateMachine.Waiting;
backgroundWorker1.CancelAsync();
}
else
{
currentState = StateMachine.Error;
}
}
//In my case, I build a string builder until I get a carriage return or a colon character. This tells me
//I got all the characters I want for the response. Now we call my delegate which calls the correct response
//function. The datareceived event can fire mid response, so you need someway to know when you have the whole
//message.
private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
string CurrentLine = "";
string Data = serialPortSensor.ReadExisting();
Data.Replace("\n", "");
foreach (char c in Data)
{
if (c == '\r' || c == ':')
{
sb.Append(c);
CurrentLine = sb.ToString();
sb.Clear();
CallbackResponse(CurrentLine); //calls our correct response function depending on the current delegate assigned
}
else
{
sb.Append(c);
}
}
}
I would put this in a background worker, and when you press a button or something you can set the current state to SendCmd1.
Button press
private void buttonStart_Click(object sender, EventArgs e)
{
if(!backgroundWorker1.IsBusy)
{
currentState = StateMachine.SendCmd1;
backgroundWorker1.RunWorkerAsync();
}
}
Background worker do work event
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
while (true)
{
if (backgroundWorker1.CancellationPending)
break;
do_State_Machine();
Thread.Sleep(100);
}
}

Boolean Resets When App Loads Wp8

Hi I am still fairly new to C# & windows phone.
When the app Loads I wanted popup asking the user if they would like to do something
MessageBoxResult m = MessageBox.Show("Info.", "Question?", MessageBoxButton.OKCancel);
if (m == MessageBoxResult.Cancel)
{ }
else if (m == MessageBoxResult.OK)
{ //Do Something }
Now that works fine, if the user says no I wanted a popup that asked the user if they would like reminding next time so U used
MessageBoxResult m = MessageBox.Show("Info.", "Question?", MessageBoxButton.OKCancel);
if (m == MessageBoxResult.Cancel)
{
MessageBoxResult r = MessageBox.Show("", "Would You Like Reminding Next Time ?",MessageBoxButton.OKCancel);
if (r == MessageBoxResult.Cancel)
{ }
else if (r == MessageBoxResult.OK)
{ }
}
else if (m == MessageBoxResult.OK)
{ //Do Something }
I need some kind of a switch, so when the app starts for the first time
app checks switch which is on,
they get asked a question
if they answer cancel,
they get asked if they want reminding
if they answer no,
set switch to off
I've tried to use a boolean but it just resets to true when the app closes, if i use a string it says a string cant be used as a bool
Any Advice ?
Use IsolatedStorageSettings.ApplicationSettings to quickly save small values for example
// this will save my "your_key" to false;
IsolatedStorageSettings.ApplicationSettings.Add("your_key", false);
IsolatedStorageSettings.ApplicationSettings.Save(); // make sure you call save
// so the next time the app runs I can get it back doing this
bool your_key = (bool) IsolatedStorageSettings.ApplicationSettings["your_key"];
But, should always enclose it in a try catch because the key might not exist
bool your_key = false; // or default value
try
{
your_key = (bool) IsolatedStorageSettings.ApplicationSettings["your_key"];
}
catch(Exception ex)
{
}
More Information can be found here:
How to: Store and Retrieve Application Settings Using Isolated Storage
if(!IsolatedStorageSettings.ApplicationSettings.Contains("first"))
{
// Do your stuff
IsolatedStorageSettings.ApplicationSettings["first"] = true;
IsolatedStorageSettings.ApplicationSettings.Save();
}
This is all the code you need.
Place everything you want to do only on first launch into this if statement. Then execute this code either in main page Loaded event or OnNavigatedTo.

Serial Communication in C# using arduino, android, winform

I have a program that is similar to an ATM application. I use my android device a the Keypad. No problem with transmitting data from phone to my C# program. I use threading. I have a problem on how to really "start" the ATM application.
The only input device is the android device. I will not use computer keyboard or anything. So once the program is shown, the first thing is that the program will ask what kind of transaction is it.
private void FrontPage_Shown(object sender, EventArgs e)
{
Ask_Transaction();
}
and inside that... (Edited part)
public void Ask_Transaction()
{
string decoded_input = "";
decoded_input = KT.CheckPress(input); //"input" is the data from the arduino. the checkpress will decode it.
do
{
try
{
//input = serialPort.ReadLine();
switch (KT.CheckPress(input))
{
case "S5": //Card;
break;
case "S6": //Cardless
{
PF.Format_Cardless();
AskLanguage(); //if cardless, it will now ask the language. Once again, another input from serial is needed
}
break;
case "Cancel": PF.Format_TransactionCancelled();
break;
default:
break;
}
}
catch
{
//To catch ReadTimeout after 6 seconds
//"You do not respond to the alloted time.."
}
}
while (decoded_input != "S5"
|| decoded_input != "S6"
|| decoded_input != "Cancel");
}
my problem is that when I try to loop Ask_Transaction until I get the correct inputs (S5 or S6 or cancel) using a do-while loop, my program is lagging and eventually crashing. No errors displaying.
EDIT:
Of course we cannot assure that the user will input the correct key. And usually when you click just numbers on the keypad, ATM program will not notify you at first. It will just wait for possible correct inputs. Also, if the user enters S6, the program will now ask another input using those keypad numbers s5 and s6. the problem is that the program is not "continuous". Any kind of help will be appreciated.

Writing to a log, inside a timer, that's inside a thread, from another thread

I'm working on writing an application that scans an imageboard for images, then downloads them. Ofcourse this has to be threaded, because multiple boards could be scraped at once. I've got the basic funcionality done already, but now I'm hitting a wall.
Currently I start threads by giving an url, then press a button, this button starts a thread which points to a class.
My problem resides inside this class, as I'm using a timer there.
Currently the data gets pushed to the log at one go, but should be pushing the data as it is set.
Currently this is my function that is bound to the tick event of my timer:
public void scanForImages(object s, ElapsedEventArgs e)
{
if (status != 1 && status != 4)
{
status = 1;
int i = 0;
while (status == 1)
{
main.updateLog(th.Name + ": Blaat\n");
i++;
if (i > 50)
{
status = 4;
t.Stop();
main.updateThreads("Aborting: " + th.Name, th);
th.Abort();
}
}
}
else
{
t.Stop();
}
}
It is returning output in my textbox, but it's pushing everything at once( All the th.Name + ": Blaat\n"
updateLog:
public void updateLog(string txt)
{
if (InvokeRequired)
{
Action action = () => textBox2.AppendText(txt);
textBox2.Invoke(action);
}
else
{
textBox2.AppendText(txt);
}
}
What am I doing wrong? (More code can be supplied, if neccessary)
you do not give the .updatelog code so we can answer correctly
so...
either you must flush the file inside that function or you must initiate a dispatcher
Dispatcher.Invoke(DispatcherPriority.Normal,
new Action<string>(main.updateLog),
th.Name + ": Blaat\n");

Categories