enhance the shit out of it

This commit is contained in:
Andrew Camilleri (Kukks)
2025-07-05 19:34:11 +02:00
parent 133b4bac03
commit 8c8a93e22b
3 changed files with 136 additions and 50 deletions

View File

@@ -9,7 +9,7 @@
<PropertyGroup> <PropertyGroup>
<Product>Bitcoin Switch</Product> <Product>Bitcoin Switch</Product>
<Description>Control harwdare using the POS as a switch</Description> <Description>Control harwdare using the POS as a switch</Description>
<Version>1.0.0</Version> <Version>1.0.1</Version>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup> </PropertyGroup>
<!-- Plugin development properties --> <!-- Plugin development properties -->
@@ -39,9 +39,4 @@
<Folder Include="Resources\js\" /> <Folder Include="Resources\js\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Views\Shared\BitcoinSwitch\FileSellerTemplateEditorItemDetail.cshtml" />
</ItemGroup>
</Project> </Project>

View File

@@ -17,25 +17,23 @@ using Newtonsoft.Json.Linq;
namespace BTCPayServer.Plugins.FileSeller namespace BTCPayServer.Plugins.FileSeller
{ {
public class BitcoinSwitchEvent public class BitcoinSwitchEvent
{ {
public string AppId { get; set; } public string AppId { get; set; }
public string Message { get; set; } public string SwitchSettings { get; set; }
} }
public class BitcoinSwitchService : EventHostedServiceBase public class BitcoinSwitchService : EventHostedServiceBase
{ {
private readonly AppService _appService; private readonly AppService _appService;
private readonly InvoiceRepository _invoiceRepository; private readonly InvoiceRepository _invoiceRepository;
public BitcoinSwitchService(EventAggregator eventAggregator,
public BitcoinSwitchService(
EventAggregator eventAggregator,
ILogger<BitcoinSwitchService> logger, ILogger<BitcoinSwitchService> logger,
AppService appService, AppService appService,
InvoiceRepository invoiceRepository) : base(eventAggregator, logger) InvoiceRepository invoiceRepository)
: base(eventAggregator, logger)
{ {
_appService = appService; _appService = appService;
_invoiceRepository = invoiceRepository; _invoiceRepository = invoiceRepository;
@@ -43,7 +41,6 @@ namespace BTCPayServer.Plugins.FileSeller
public ConcurrentMultiDictionary<string, WebSocket> AppToSockets { get; } = new(); public ConcurrentMultiDictionary<string, WebSocket> AppToSockets { get; } = new();
protected override void SubscribeToEvents() protected override void SubscribeToEvents()
{ {
Subscribe<InvoiceEvent>(); Subscribe<InvoiceEvent>();
@@ -55,26 +52,12 @@ namespace BTCPayServer.Plugins.FileSeller
{ {
if (evt is BitcoinSwitchEvent bitcoinSwitchEvent) if (evt is BitcoinSwitchEvent bitcoinSwitchEvent)
{ {
if (AppToSockets.TryGetValues(bitcoinSwitchEvent.AppId, out var sockets)) _ = HandleGPIOMessages(cancellationToken, bitcoinSwitchEvent);
{ return;
foreach (var socket in sockets)
{
try
{
await socket.SendAsync(
new ArraySegment<byte>(System.Text.Encoding.UTF8.GetBytes(bitcoinSwitchEvent.Message)),
WebSocketMessageType.Text, true, cancellationToken);
}
catch (Exception e)
{
}
}
}
} }
if (evt is not InvoiceEvent invoiceEvent) return; if (evt is not InvoiceEvent invoiceEvent) return;
List<AppCartItem> cartItems = null; List<AppCartItem> cartItems = null;
if (invoiceEvent.Name is not (InvoiceEvent.Completed or InvoiceEvent.MarkedCompleted if (invoiceEvent.Name is not (InvoiceEvent.Completed or InvoiceEvent.MarkedCompleted
or InvoiceEvent.Confirmed)) or InvoiceEvent.Confirmed))
{ {
@@ -124,27 +107,24 @@ namespace BTCPayServer.Plugins.FileSeller
return (null, null, null); return (null, null, null);
} }
}).Where(tuple => tuple.Data != null && tuple.Items.Any(item => }).Where(tuple => tuple.Data != null && tuple.Items.Any(item =>
item.AdditionalData?.ContainsKey("bitcoinswitch_gpio") is true && item.AdditionalData?.ContainsKey("bitcoinswitch") is true &&
items.Exists(cartItem => cartItem.Id == item.Id))); items.Exists(cartItem => cartItem.Id == item.Id)));
foreach (var valueTuple in apps) foreach (var valueTuple in apps)
{ {
foreach (var item1 in valueTuple.Items.Where(item => foreach (var item1 in valueTuple.Items.Where(item =>
item.AdditionalData?.ContainsKey("bitcoinswitch_gpio") is true && item.AdditionalData?.ContainsKey("bitcoinswitch") is true &&
items.Exists(cartItem => cartItem.Id == item.Id))) items.Exists(cartItem => cartItem.Id == item.Id)))
{ {
var appId = valueTuple.Data.Id; var appId = valueTuple.Data.Id;
var gpio = item1.AdditionalData["bitcoinswitch_gpio"].Value<string>(); var gpio = item1.AdditionalData["bitcoinswitch"].Value<string>();
var duration = item1.AdditionalData.TryGetValue("bitcoinswitch_duration", out var durationObj) &&
durationObj.Type == JTokenType.Integer
? durationObj.Value<string>()
: "5000";
PushEvent(new BitcoinSwitchEvent() PushEvent(new BitcoinSwitchEvent()
{ {
AppId = appId, AppId = appId,
Message = $"{gpio}-{duration}.0" SwitchSettings = gpio
}); });
} }
@@ -160,5 +140,105 @@ namespace BTCPayServer.Plugins.FileSeller
await base.ProcessEvent(evt, cancellationToken); await base.ProcessEvent(evt, cancellationToken);
} }
private async Task HandleGPIOMessages(CancellationToken cancellationToken, BitcoinSwitchEvent bitcoinSwitchEvent)
{
// Parse switch settings into actions
var actions = ParseActions(bitcoinSwitchEvent.SwitchSettings);
try
{
// Execute each action sequentially
foreach (var action in actions)
{
if (action.IsDelay)
{
// Wait for specified delay
await Task.Delay(action.DelayMs, cancellationToken);
}
else
{
// Send pin-duration command
var message = $"{action.Pin}-{action.Duration}";
var buffer = System.Text.Encoding.UTF8.GetBytes(message);
if (!AppToSockets.TryGetValues(bitcoinSwitchEvent.AppId, out var sockets))
return;
foreach (var socket in sockets)
{
await socket.SendAsync(
new ArraySegment<byte>(buffer),
WebSocketMessageType.Text,
true,
cancellationToken);
}
}
}
}
catch (Exception ex)
{
Logs.PayServer.LogError(ex, "Error sending BitcoinSwitchEvent to socket");
}
return;
}
/// <summary>
/// Parses a settings string like "25-5000.0,delay 1000,23-200.0" into a sequence of actions.
/// </summary>
private static List<SwitchAction> ParseActions(string settings)
{
var actions = new List<SwitchAction>();
var segments = settings.Split(',', StringSplitOptions.RemoveEmptyEntries);
foreach (var seg in segments.Select(s => s.Trim()))
{
if (seg.StartsWith("delay ", StringComparison.OrdinalIgnoreCase))
{
// Delay segment
var parts = seg.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2 && int.TryParse(parts[1], out var ms))
{
actions.Add(SwitchAction.Delay(ms));
}
}
else
{
// Pin-duration segment
var parts = seg.Split('-', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2
&& int.TryParse(parts[0], out var pin)
&& double.TryParse(parts[1], out var duration))
{
actions.Add(SwitchAction.Command(pin, duration));
}
}
}
return actions;
}
}
/// <summary>
/// Represents either a delay or a pin-duration command.
/// </summary>
public class SwitchAction
{
public bool IsDelay { get; }
public int DelayMs { get; }
public int Pin { get; }
public double Duration { get; }
private SwitchAction(bool isDelay, int delayMs, int pin, double duration)
{
IsDelay = isDelay;
DelayMs = delayMs;
Pin = pin;
Duration = duration;
}
public static SwitchAction Delay(int ms) => new(true, ms, 0, 0);
public static SwitchAction Command(int pin, double duration) => new(false, 0, pin, duration);
} }
} }

View File

@@ -1,12 +1,23 @@
 
<template v-if="editingItem"> <template v-if="editingItem">
<div class="form-group"> <div class="form-group">
<label class="form-label">Bitcoin Switch GPIO</label> <label class="form-label">Bitcoin Switch</label>
<input type="number" inputmode="numeric" min="0" step="1" class="form-control mb-2" :value="editingItem['bitcoinswitch_gpio'] || ''" v-on:change="if(event.target.value) Vue.set(editingItem, 'bitcoinswitch_gpio', event.target.value); else Vue.delete(editingItem, 'bitcoinswitch_gpio');"/> <input
type="text"
class="form-control mb-2"
:value="editingItem['bitcoinswitch'] || ''"
pattern="^[0-9]+-[0-9]+(?:\.[0-9]+)?(?:\s*,\s*(?:delay\s*[0-9]+|[0-9]+-[0-9]+(?:\.[0-9]+)?))*$"
title="e.g. 25-5000,delay 5000,26-5000.0,delay 2000,23-200"
v-on:change="
event.target.reportValidity();
const val = event.target.value;
if (val) Vue.set(editingItem, 'bitcoinswitch', val);
else Vue.delete(editingItem, 'bitcoinswitch');
"
/>
</div> </div>
<div class="form-group" v-if="editingItem['bitcoinswitch_gpio']"> <p>
<label class="form-label">Bitcoin Switch Duration</label> Each segment specifies a <strong>GPIO pin</strong> and its <strong>activation duration</strong> (in milliseconds) in the format <code>pin-duration</code> (e.g., <code>25-5000.0</code>). You can also insert <code>,delay&nbsp;milliseconds</code> (e.g., <code>,delay&nbsp;5000</code>) between segments to pause before the next activation. Separate all parts with commas.
<input type="number" inputmode="numeric" min="1" step="1000" class="form-control mb-2" :value="editingItem['bitcoinswitch_duration'] || ''" asp-items="files" class="form-select w-auto" v-on:change="if(event.target.value) Vue.set(editingItem, 'bitcoinswitch_duration', event.target.value); else Vue.delete(editingItem, 'bitcoinswitch_duration');"/> </p>
</div>
</template> </template>