feat: scan-modal polish, Pine 1.2.0, bitcoind RPC creds off argv #102
@ -35,6 +35,9 @@ app:
|
|||||||
fi;
|
fi;
|
||||||
RPC_USER="$(printenv BITCOIN_RPC_USER)";
|
RPC_USER="$(printenv BITCOIN_RPC_USER)";
|
||||||
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
|
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
|
||||||
|
RPC_CONF="/tmp/rpc.conf";
|
||||||
|
umask 077;
|
||||||
|
printf "rpcuser=%s\nrpcpassword=%s\n" "$RPC_USER" "$RPC_PASS" > "$RPC_CONF";
|
||||||
RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)";
|
RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)";
|
||||||
DISK_GB_VALUE="$(printenv DISK_GB || true)";
|
DISK_GB_VALUE="$(printenv DISK_GB || true)";
|
||||||
RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256";
|
RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256";
|
||||||
@ -43,9 +46,9 @@ app:
|
|||||||
RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips";
|
RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips";
|
||||||
fi;
|
fi;
|
||||||
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
|
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
|
||||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=1024 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
|
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=1024 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||||
else
|
else
|
||||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
|
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||||
fi
|
fi
|
||||||
derived_env:
|
derived_env:
|
||||||
- key: DISK_GB
|
- key: DISK_GB
|
||||||
|
|||||||
@ -35,6 +35,9 @@ app:
|
|||||||
fi;
|
fi;
|
||||||
RPC_USER="$(printenv BITCOIN_RPC_USER)";
|
RPC_USER="$(printenv BITCOIN_RPC_USER)";
|
||||||
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
|
RPC_PASS="$(printenv BITCOIN_RPC_PASS)";
|
||||||
|
RPC_CONF="/tmp/rpc.conf";
|
||||||
|
umask 077;
|
||||||
|
printf "rpcuser=%s\nrpcpassword=%s\n" "$RPC_USER" "$RPC_PASS" > "$RPC_CONF";
|
||||||
RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)";
|
RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)";
|
||||||
DISK_GB_VALUE="$(printenv DISK_GB || true)";
|
DISK_GB_VALUE="$(printenv DISK_GB || true)";
|
||||||
RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256";
|
RPC_HEADROOM="-rpcthreads=16 -rpcworkqueue=256";
|
||||||
@ -43,9 +46,9 @@ app:
|
|||||||
RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips";
|
RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips";
|
||||||
fi;
|
fi;
|
||||||
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
|
if [ "${DISK_GB_VALUE:-0}" -lt 1000 ]; then
|
||||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
|
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -printtoconsole=0 -server=1 -prune=550 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||||
else
|
else
|
||||||
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -noconf -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS -rpcuser="$RPC_USER" -rpcpassword="$RPC_PASS";
|
exec "$BITCOIND" -datadir=/home/bitcoin/.bitcoin -conf="$RPC_CONF" -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;
|
||||||
fi
|
fi
|
||||||
derived_env:
|
derived_env:
|
||||||
- key: DISK_GB
|
- key: DISK_GB
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
app:
|
app:
|
||||||
id: pine
|
id: pine
|
||||||
name: Pine
|
name: Pine
|
||||||
version: "1.1.1"
|
version: "1.2.0"
|
||||||
description: A private voice assistant for your home. Pine runs speech-to-text (Whisper) and text-to-speech (Piper) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud.
|
description: A private voice assistant for your home. Pine runs speech-to-text (Whisper) and text-to-speech (Piper) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud.
|
||||||
category: home
|
category: home
|
||||||
|
|
||||||
@ -174,10 +174,15 @@ app:
|
|||||||
<div id="log">Ready. Click the button, then pick “PineVoice” in the Bluetooth popup.</div>
|
<div id="log">Ready. Click the button, then pick “PineVoice” in the Bluetooth popup.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="ha">After WiFi joins, add the speaker to Home Assistant:
|
<p class="ha">After WiFi joins, one manual step remains — pair the
|
||||||
<b>Settings → Devices & services → Add Wyoming Protocol</b>, host =
|
speaker in Home Assistant: <b>Settings → Devices & services →
|
||||||
the speaker’s IP, port <b>10700</b>, then pick an Assist pipeline using
|
Add Wyoming Protocol</b>, host = the speaker’s IP, port
|
||||||
Whisper + Piper. Wake word: <b>“Hey Jarvis.”</b></p>
|
<b>10700</b>. Whisper, Piper and the Assist pipeline are wired up
|
||||||
|
automatically when Pine installs. Wake word: <b>“Hey Jarvis.”</b>
|
||||||
|
You can also ask node things like <i>“what’s the block height?”</i></p>
|
||||||
|
<p class="ha">Troubleshooting: if it hears you (LED reacts) but answers
|
||||||
|
are silent, unplug and replug the speaker — an interrupted answer can
|
||||||
|
wedge its audio output until it reboots.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -44,12 +44,22 @@
|
|||||||
<!-- ============ SCAN PANE ============ -->
|
<!-- ============ SCAN PANE ============ -->
|
||||||
<div v-if="pane === 'scan'" key="scan">
|
<div v-if="pane === 'scan'" key="scan">
|
||||||
<div class="relative w-full aspect-square rounded-xl overflow-hidden bg-black/40 border border-white/10 mb-4">
|
<div class="relative w-full aspect-square rounded-xl overflow-hidden bg-black/40 border border-white/10 mb-4">
|
||||||
<video ref="videoElement" class="w-full h-full object-cover" autoplay muted playsinline></video>
|
<!-- opacity (not v-if/v-show): the scanner needs the element, and a
|
||||||
|
source-less <video> flashes a native play glyph in Android WebViews -->
|
||||||
|
<video ref="videoElement" class="w-full h-full object-cover transition-opacity duration-200" :class="isScanning ? 'opacity-100' : 'opacity-0'" autoplay muted playsinline></video>
|
||||||
<!-- Viewfinder -->
|
<!-- Viewfinder -->
|
||||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||||
<div class="scan-viewfinder"></div>
|
<div class="scan-viewfinder"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isScanning" class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/50">
|
<!-- While auto-start is settling, show only a quiet spinner — the
|
||||||
|
Start/Take-photo buttons are the FALLBACK, not a splash screen -->
|
||||||
|
<div v-if="!isScanning && autoStarting" class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/50">
|
||||||
|
<svg class="w-8 h-8 text-white/40 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" />
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v3a5 5 0 00-5 5H4z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!isScanning" class="absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/50">
|
||||||
<svg class="w-10 h-10 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-10 h-10 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
@ -272,7 +282,7 @@ function goBack() {
|
|||||||
if (pane.value === 'amount') {
|
if (pane.value === 'amount') {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
goTo('scan', 'back')
|
goTo('scan', 'back')
|
||||||
nextTick(() => startScanning())
|
nextTick(() => { if (!liveCameraUnavailable.value) startScanning() })
|
||||||
} else if (pane.value === 'success') {
|
} else if (pane.value === 'success') {
|
||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
@ -281,6 +291,10 @@ function goBack() {
|
|||||||
// --- Scanner ---
|
// --- Scanner ---
|
||||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||||
const isScanning = ref(false)
|
const isScanning = ref(false)
|
||||||
|
// True while a camera-start attempt is in flight (incl. the automatic one on
|
||||||
|
// open) — the fallback buttons stay hidden until it resolves, so they don't
|
||||||
|
// flash for a second on every open.
|
||||||
|
const autoStarting = ref(false)
|
||||||
const scanStatus = ref('')
|
const scanStatus = ref('')
|
||||||
const scanStatusIsError = ref(false)
|
const scanStatusIsError = ref(false)
|
||||||
const cameraError = ref(false)
|
const cameraError = ref(false)
|
||||||
@ -291,6 +305,7 @@ const animatedDecoder = useAnimatedQRDecoder()
|
|||||||
async function startScanning() {
|
async function startScanning() {
|
||||||
cameraError.value = false
|
cameraError.value = false
|
||||||
scanStatusIsError.value = false
|
scanStatusIsError.value = false
|
||||||
|
autoStarting.value = true
|
||||||
try {
|
try {
|
||||||
if (!videoElement.value) return
|
if (!videoElement.value) return
|
||||||
if (!navigator.mediaDevices?.getUserMedia) {
|
if (!navigator.mediaDevices?.getUserMedia) {
|
||||||
@ -307,7 +322,9 @@ async function startScanning() {
|
|||||||
returnDetailedScanResult: true,
|
returnDetailedScanResult: true,
|
||||||
highlightScanRegion: false,
|
highlightScanRegion: false,
|
||||||
preferredCamera: 'environment',
|
preferredCamera: 'environment',
|
||||||
maxScansPerSecond: 10,
|
// 4/s decodes plenty fast for a hand-held QR while keeping the
|
||||||
|
// preview smooth on phone WebViews (10/s visibly lagged the video).
|
||||||
|
maxScansPerSecond: 4,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
await qrScanner.value.start()
|
await qrScanner.value.start()
|
||||||
@ -317,6 +334,8 @@ async function startScanning() {
|
|||||||
cameraError.value = true
|
cameraError.value = true
|
||||||
scanStatusIsError.value = true
|
scanStatusIsError.value = true
|
||||||
scanStatus.value = err instanceof Error ? err.message : 'Unable to access camera'
|
scanStatus.value = err instanceof Error ? err.message : 'Unable to access camera'
|
||||||
|
} finally {
|
||||||
|
autoStarting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -465,7 +484,8 @@ function acceptDetected(raw: string) {
|
|||||||
action.value = 'pay-invoice'
|
action.value = 'pay-invoice'
|
||||||
dest.value = text
|
dest.value = text
|
||||||
const invoiceAmount = parseBolt11AmountSats(text)
|
const invoiceAmount = parseBolt11AmountSats(text)
|
||||||
amount.value = invoiceAmount ?? 0
|
// Zero-amount invoice inside a unified BIP21 URI: prefill from amount=
|
||||||
|
amount.value = invoiceAmount ?? bip21AmountSats ?? 0
|
||||||
amountLocked.value = invoiceAmount !== null
|
amountLocked.value = invoiceAmount !== null
|
||||||
} else if (isCashuToken(text)) {
|
} else if (isCashuToken(text)) {
|
||||||
rail.value = 'cashu'
|
rail.value = 'cashu'
|
||||||
@ -491,6 +511,16 @@ function acceptDetected(raw: string) {
|
|||||||
dest.value = text
|
dest.value = text
|
||||||
amount.value = bip21AmountSats ?? 0
|
amount.value = bip21AmountSats ?? 0
|
||||||
amountLocked.value = false
|
amountLocked.value = false
|
||||||
|
} else if (text.toLowerCase().startsWith('lno1')) {
|
||||||
|
// BOLT12 offer — LND (this node's Lightning backend) can't pay offers yet
|
||||||
|
scanStatusIsError.value = true
|
||||||
|
scanStatus.value = 'BOLT12 offers aren\'t supported yet — ask the recipient for a BOLT11 invoice'
|
||||||
|
return
|
||||||
|
} else if (text.toLowerCase().startsWith('lnurl1') || /^[\w.+-]+@[\w-]+(\.[\w-]+)+$/.test(text)) {
|
||||||
|
// LNURL-pay / lightning address — needs an HTTP callback flow we don't do yet
|
||||||
|
scanStatusIsError.value = true
|
||||||
|
scanStatus.value = 'LNURL and lightning addresses aren\'t supported yet — ask for a BOLT11 invoice'
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
scanStatusIsError.value = true
|
scanStatusIsError.value = true
|
||||||
scanStatus.value = 'Not a recognised invoice, address or token'
|
scanStatus.value = 'Not a recognised invoice, address or token'
|
||||||
@ -656,7 +686,7 @@ function close() {
|
|||||||
watch(() => props.show, (open) => {
|
watch(() => props.show, (open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
resetAll()
|
resetAll()
|
||||||
nextTick(() => startScanning())
|
nextTick(() => { if (!liveCameraUnavailable.value) startScanning() })
|
||||||
} else {
|
} else {
|
||||||
stopScanning()
|
stopScanning()
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user