Skip to content

Commit

Permalink
Refactor headless and pizzacmd to use StandaloneClient. Introduce Cal…
Browse files Browse the repository at this point in the history
…l Manager for more modular code. Fixes to pizzaui listview refresh.
  • Loading branch information
lilhoser committed Apr 11, 2024
1 parent 08da8ab commit f5d3ef0
Show file tree
Hide file tree
Showing 29 changed files with 958 additions and 867 deletions.
26 changes: 23 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ _Important_: Make sure your `trunk-recorder` system is configured to connect to
StreamServer Verbose: 1 : 3/22/2024 3:39 PM: Listening on port 9123
```

# Running

Whether you use `pizzaui`, `pizzacmd` or your own .NET application built on `pizzalib`, all calls will be stored in a `capture`, which is a folder in the root working directory (`<user profile>\pizzawave\`). This folder consists of:

* `calljournal.json`: Each line contains a JSON-serialized `TranscribedCall` structure. The audio data can be linked to this record via the `Location` field.
* `<timestamp>.mp3`: call audio files

The call journal can be deserialized into a list of `TranscribedCall` objects using `NewtonSoft.Json` as follows:

```
var lines = File.ReadAllLines("calljournal.json");
var calls = new List<TranscribedCall>();
foreach (var line in lines)
{
var call = (TranscribedCall)JsonConvert.DeserializeObject(line, typeof(TranscribedCall))!;
calls.Add(call);
}
```


# Other

## Diagnostics
Expand Down Expand Up @@ -98,9 +118,9 @@ Here is the parts list, all of which can be purchased on Amazon:
* [RTL-SDR Blog LNA](https://www.amazon.com/dp/B07G14Q6XX)
* [PL-259/UHF-F to F-type-M](https://www.amazon.com/dp/B0C36VGYKZ)
* [PL-259/UHF-F to SMA-M](https://www.amazon.com/dp/B00CVQOOAI)
* [DirecTV F-type 3-way splitter](https://www.amazon.com/dp/B00BW60R68)
* [SMA splitters](https://www.amazon.com/dp/B091DQ3HGZ) ([adapters needed](https://www.amazon.com/dp/B07FDHBS19))
* [F-type to SMA adapters](https://www.amazon.com/dp/B0814BQHJN) and [these too](https://www.amazon.com/dp/B09KB9RM6Q)
* [DirecTV F-type 8-way splitter](https://www.amazon.com/gp/product/B0045DVIP4)
* [F to SMA jumpers](https://www.amazon.com/gp/product/B09GVSHQJX)
* Other adapters you might need: [SMA adapters](https://www.amazon.com/dp/B07FDHBS19)), [F-type to SMA adapters](https://www.amazon.com/dp/B0814BQHJN) and [these too](https://www.amazon.com/dp/B09KB9RM6Q)
* [RTL-SDR v4 dongles](https://www.amazon.com/dp/B0CD745394)
* [10-port USB hub](https://www.amazon.com/dp/B098KZMR4J) with sufficient port spacing for side-by-side RTL-SDR dongles!
* [Monoprice USB extension cable](https://www.amazon.com/dp/B00AA0U08M)
Expand Down
Binary file modified docs/rtlsdr-setup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 1 addition & 3 deletions pizzacmd/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@ software distributed under the License is distributed on an
specific language governing permissions and limitations
under the License.
*/
using System.Reflection;

namespace pizzacmd;

class Program
{
static async Task<int> Main(string[] args)
{
var pizza = new pizzacmd();
var pizza = new PizzaCmd();
return await pizza.Run(args);
}
}
26 changes: 0 additions & 26 deletions pizzacmd/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,32 +68,6 @@ public override void Validate()
{
base.Validate();
}

public void SaveToFile(string? Target)
{
string target;

if (string.IsNullOrEmpty(Target))
{
target = DefaultSettingsFileLocation;
}
else
{
target = Target;
}

try
{
Validate();
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
File.WriteAllText(target, json);
}
catch (Exception ex)
{
throw new Exception($"Could not save the Settings object " +
$"to JSON: {ex.Message}");
}
}
}
}

257 changes: 19 additions & 238 deletions pizzacmd/pizzacmd.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,267 +16,48 @@ software distributed under the License is distributed on an
specific language governing permissions and limitations
under the License.
*/
using Newtonsoft.Json;
using pizzalib;
using System.Diagnostics;
using System.Reflection;
using System.Text;

namespace pizzacmd
{
using static TraceLogger;
using Whisper = pizzalib.Whisper;

internal class pizzacmd
internal class PizzaCmd : StandaloneClient
{
private StreamServer? m_StreamServer;
private Whisper? m_Whisper;
private Alerter? m_Alerter;
private Settings? m_Settings;

public pizzacmd()
public PizzaCmd()
{
m_CallManager = new CallManager(NewCallTranscribed);
}

public async Task<int> Run(string[] Args)
protected override void PrintUsage(string Error)
{
TraceLogger.Initialize(true);
pizzalib.TraceLogger.Initialize(true);
WriteBanner();
//
// Look. I've tried Microsoft's System.CommandLine for parsing and it's simply
// awful, awful awful. So this is all you'll get and YOU'LL LIKE IT.
//
string settingsPath = pizzalib.Settings.DefaultSettingsFileLocation;
string tgLocation = string.Empty;
foreach (var arg in Args)
{
if (arg.ToLower().StartsWith("--settings") ||
arg.ToLower().StartsWith("-settings"))
{
settingsPath = ParsePathArgument(arg);
if (string.IsNullOrEmpty(settingsPath))
{
Console.WriteLine($"Invalid settings file: {arg}");
Console.WriteLine("Usage: pizzacmd.exe --settings=<path>");
return 1;
}

break;
}
else if (arg.ToLower().StartsWith("--talkgroups") ||
arg.ToLower().StartsWith("-talkgroups"))
{
tgLocation = ParsePathArgument(arg);
if (string.IsNullOrEmpty(tgLocation))
{
Console.WriteLine($"Invalid talkgroup file: {arg}");
Console.WriteLine("Usage: pizzacmd.exe --talkgroups=<path_to_csv>");
return 1;
}
break;
}
else if (arg.ToLower().StartsWith("--help") || arg.ToLower().StartsWith("-help"))
{
Console.WriteLine("Usage: pizzacmd.exe --settings=<path>");
return 0;
}
else
{
Console.WriteLine($"Unknown argument {arg}");
Console.WriteLine("Usage: pizzacmd.exe --settings=<path> [--talkgroups=<talkgroup_csv_file>]");
return 1;
}
}
var result = await Initialize(settingsPath, tgLocation);
if (!result)
{
return 1;
}
result = await StartServer(); // blocks until CTRL+C
TraceLogger.Shutdown();
return result ? 0 : 1;
}

private string ParsePathArgument(string Argument)
{
if (!Argument.Contains('='))
{
return string.Empty;
}
var pieces = Argument.Split('=');
if (pieces.Length != 2)
{
return string.Empty;
}
var targetPath = pieces[1];
if (!File.Exists(targetPath))
{
return string.Empty;
}
return targetPath;
}

private async Task<bool> Initialize(string SettingsPath, string TalkgroupLocation)
{
Trace(TraceLoggerType.Main,
TraceEventType.Information,
$"Init: Using settings {SettingsPath}, talkgroups {TalkgroupLocation}");
if (!File.Exists(SettingsPath))
{
Trace(TraceLoggerType.Main,
TraceEventType.Warning,
$"Settings file {SettingsPath} does not exist, loading default...");
m_Settings = new Settings();
m_Settings.SaveToFile(SettingsPath); // persist it
}
else
{
try
{
var json = File.ReadAllText(SettingsPath);
m_Settings = (Settings)JsonConvert.DeserializeObject(json, typeof(Settings))!;
}
catch (Exception ex)
{
Trace(TraceLoggerType.Main, TraceEventType.Error, $"{ex.Message}");
return false;
}
}

if (!string.IsNullOrEmpty(TalkgroupLocation))
{
try
{
var tgs = TalkgroupHelper.GetTalkgroupsFromCsv(TalkgroupLocation);
if (tgs.Count == 0)
{
Trace(TraceLoggerType.Main,
TraceEventType.Warning,
"Invalid talkgroup file: no data in file.");
}
else
{
Trace(TraceLoggerType.Main,
TraceEventType.Information,
$"Loaded {tgs.Count} talkgroups");
m_Settings.talkgroups = tgs;
m_Settings.SaveToFile(SettingsPath); // persist
}
}
catch (Exception ex)
{
Trace(TraceLoggerType.Main,
TraceEventType.Warning,
$"Unable to parse talkgroup CSV '{TalkgroupLocation}': {ex.Message}");
}
}
else
{
Trace(TraceLoggerType.Main, TraceEventType.Warning, "No talkgroups provided!");
}

TraceLogger.SetLevel(m_Settings.TraceLevelApp);
pizzalib.TraceLogger.SetLevel(m_Settings.TraceLevelApp);

//
// Use Console directly here in case trace isn't verbose enough.
//
Console.WriteLine($"Init: trace level {m_Settings.TraceLevelApp}");

try
{
m_Whisper = new Whisper(m_Settings);
m_Alerter = new Alerter(m_Settings, m_Whisper, NewCallTranscribed);
m_StreamServer = new StreamServer(m_Alerter.NewCallDataAvailable, m_Settings);
_ = await m_Whisper.Initialize();
}
catch (Exception ex)
{
Trace(TraceLoggerType.Main, TraceEventType.Error, $"{ex.Message}");
return false;
}

Trace(TraceLoggerType.Main, TraceEventType.Verbose, "Init: Complete");

return true;
Console.WriteLine(Error);
Console.WriteLine("Usage: pizzacmd.exe --settings=<path> [--talkgroups=<path>]");
}

private async Task<bool> StartServer()
protected override void WriteBanner()
{
try
{
Console.CancelKeyPress += (sender, eventArgs) => {
eventArgs.Cancel = true;
Trace(TraceLoggerType.Main, TraceEventType.Information, "Server shutting down...");
m_StreamServer!.Shutdown();
};
_ = await m_StreamServer!.Listen(); // blocks until CTRL+C
}
catch (Exception ex)
{
Trace(TraceLoggerType.Main, TraceEventType.Error, $"{ex.Message}");
return false;
}
base.WriteBanner();

return true;
var version = Assembly.GetExecutingAssembly().GetName().Version;
Trace(TraceLoggerType.Main, TraceEventType.Warning, $"pizzacmd {version}");
}

private void NewCallTranscribed(TranscribedCall Call)
protected override void NewCallTranscribed(TranscribedCall Call)
{
//
// NB: do not use Trace() here, as this could hide the call data based on user's settings
//
Console.WriteLine($"{Call.ToString(m_Settings!)}");

var jsonContents = new StringBuilder();
try
{
var jsonObject = JsonConvert.SerializeObject(Call, Formatting.Indented);
jsonContents.AppendLine(jsonObject);
}
catch (Exception ex)
{
Trace(TraceLoggerType.Main,
TraceEventType.Error,
$"Failed to create JSON: {ex.Message}");
return;
}
try
{
var target = Path.Combine(pizzalib.Settings.DefaultWorkingDirectory,
pizzalib.Settings.DefaultCallLogFileName);
using (var writer = new StreamWriter(target, true, Encoding.UTF8))
{
writer.WriteLine(jsonContents.ToString());
}
}
catch (Exception ex)
{
Trace(TraceLoggerType.Main,
TraceEventType.Error,
$"Failed to save JSON: {ex.Message}");
return;
}
Trace(TraceLoggerType.Main, TraceEventType.Information, $"{Call.ToString(m_Settings!)}");
}

private static void WriteBanner()
public override async Task<int> Run(string[] Args)
{
var version = Assembly.GetExecutingAssembly().GetName().Version;
var assembly = Assembly.GetExecutingAssembly();
string resourceName = assembly.GetManifestResourceNames().Single(
str => str.EndsWith("banner.txt"));
string banner = string.Empty;
using (Stream stream = assembly.GetManifestResourceStream(resourceName)!)
using (StreamReader reader = new StreamReader(stream))
{
banner = reader.ReadToEnd();
}
var separator = "---------------------------------------------------";

Console.WriteLine(banner);
Trace(TraceLoggerType.Main, TraceEventType.Warning, $"pizzacmd {version}");
Trace(TraceLoggerType.Main, TraceEventType.Information, separator);
TraceLogger.Initialize(true);
pizzalib.TraceLogger.Initialize(true);
var result = await base.Run(Args.ToArray());
TraceLogger.Shutdown();
pizzalib.TraceLogger.Shutdown();
return result;
}
}
}
4 changes: 0 additions & 4 deletions pizzacmd/pizzacmd.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
<None Remove="banner.txt" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="banner.txt" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
Expand Down
Loading

0 comments on commit f5d3ef0

Please sign in to comment.