mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-12 04:16:05 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| abe8390237 |
@@ -199,15 +199,6 @@ $env:MESHCORE_SERIAL_PORT="COM8" # or your COM port
|
||||
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> **Windows + MQTT fanout:** Python's default Windows event loop (ProactorEventLoop) is not compatible with the MQTT libraries used by RemoteTerm. If you configure any MQTT integration, add `--loop none` to your uvicorn command:
|
||||
>
|
||||
> ```powershell
|
||||
> uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --loop none
|
||||
> ```
|
||||
>
|
||||
> If you forget, the app will start normally but MQTT connections will fail and you'll see a toast in the UI with this same guidance.
|
||||
|
||||
If you enable Basic Auth, protect the app with HTTPS. HTTP Basic credentials are not safe on plain HTTP. Also note that the app's permissive CORS policy is a deliberate trusted-network tradeoff, so cross-origin browser JavaScript is not a reliable way to use that Basic Auth gate.
|
||||
|
||||
## Where To Go Next
|
||||
|
||||
+5
-13
@@ -254,22 +254,14 @@ class BaseMqttPublisher(ABC):
|
||||
self._last_error = _format_error_detail(e)
|
||||
|
||||
# Windows ProactorEventLoop does not implement add_reader /
|
||||
# add_writer, which paho-mqtt requires. The failure can
|
||||
# surface as a direct NotImplementedError (add_writer in
|
||||
# __aenter__) or as a generic timeout (add_reader fails
|
||||
# inside an event-loop callback, so paho never hears back).
|
||||
# Either way, if we're on Windows with Proactor the root
|
||||
# cause is the same and retrying won't help.
|
||||
_on_proactor = (
|
||||
sys.platform == "win32"
|
||||
and type(asyncio.get_event_loop()).__name__ == "ProactorEventLoop"
|
||||
)
|
||||
if _on_proactor:
|
||||
# add_writer, which paho-mqtt requires. Give a specific,
|
||||
# actionable toast instead of the generic connection error.
|
||||
if isinstance(e, NotImplementedError) and sys.platform == "win32":
|
||||
broadcast_error(
|
||||
"MQTT unavailable — Windows event loop incompatible",
|
||||
"The default Windows event loop (ProactorEventLoop) does "
|
||||
"not support MQTT. Add --loop none to your uvicorn "
|
||||
"command and restart. See README.md for details.",
|
||||
"not support MQTT. Restart with: uv run uvicorn "
|
||||
"app.main:app --loop none",
|
||||
)
|
||||
_broadcast_health()
|
||||
logger.error(
|
||||
|
||||
+100
-19
@@ -1,41 +1,122 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Windows event-loop advisory for MQTT fanout
|
||||
# Windows event-loop fix for MQTT fanout (aiomqtt / paho-mqtt) compatibility
|
||||
# ---------------------------------------------------------------------------
|
||||
# On Windows, uvicorn's default event loop (ProactorEventLoop) does not
|
||||
# implement add_reader()/add_writer(), which paho-mqtt (via aiomqtt) requires.
|
||||
# We cannot fix this from inside the app — the loop is already created by the
|
||||
# time this module is imported. Log a prominent warning so Windows operators
|
||||
# who want MQTT know to add ``--loop none`` to their uvicorn command.
|
||||
# On Windows, uvicorn's default "auto" loop explicitly creates ProactorEventLoop,
|
||||
# which does NOT implement add_reader()/add_writer() — calls that paho-mqtt
|
||||
# requires internally. Setting the event loop *policy* alone is not enough
|
||||
# because uvicorn's "auto" factory bypasses it.
|
||||
#
|
||||
# The fix: re-exec the current process with "--loop none", which tells uvicorn
|
||||
# to let asyncio.run() create the loop through the standard policy (where we
|
||||
# have just installed WindowsSelectorEventLoopPolicy).
|
||||
#
|
||||
# Guards:
|
||||
# - "--loop" already in argv → we (or the operator) already handled it
|
||||
# - MESHCORE_NO_AUTO_LOOP_ON_WIN32=true → operator opt-out for custom
|
||||
# runners, test harnesses, or other non-uvicorn invocations
|
||||
# ---------------------------------------------------------------------------
|
||||
if sys.platform == "win32":
|
||||
_win32_needs_reexec = (
|
||||
sys.platform == "win32"
|
||||
and os.environ.get("MESHCORE_NO_AUTO_LOOP_ON_WIN32", "").lower() not in ("true", "1")
|
||||
and "--loop" not in sys.argv
|
||||
)
|
||||
# Skip re-exec when --reload is active: on Windows os.execv spawns a new
|
||||
# process and exits, so the reloader's child dies and a fresh uvicorn
|
||||
# (with its own reloader) starts — creating doubled watchers or a loop.
|
||||
# Also skip if sys.executable is missing (embedded / frozen Python).
|
||||
if _win32_needs_reexec and "--reload" in sys.argv:
|
||||
print(
|
||||
"\n" + "!" * 78 + "\n"
|
||||
" WINDOWS + --reload DETECTED\n" + "!" * 78 + "\n"
|
||||
"\n"
|
||||
" We can't auto-fix the event loop when --reload is active because\n"
|
||||
" the re-exec would fight with uvicorn's reloader process.\n"
|
||||
"\n"
|
||||
" If you need MQTT fanout, add --loop none to your command:\n"
|
||||
"\n"
|
||||
" uv run uvicorn app.main:app --reload \033[1m--loop none\033[0m [... other options ...]\n"
|
||||
"\n"
|
||||
" Everything else works fine as-is.\n"
|
||||
"\n" + "!" * 78 + "\n",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
_win32_needs_reexec = False
|
||||
|
||||
if _win32_needs_reexec and not sys.executable:
|
||||
# Embedded or frozen Python — can't re-exec, just warn.
|
||||
_win32_needs_reexec = False
|
||||
|
||||
if _win32_needs_reexec:
|
||||
import asyncio as _asyncio
|
||||
|
||||
_loop = _asyncio.get_event_loop()
|
||||
_is_proactor = type(_loop).__name__ == "ProactorEventLoop"
|
||||
if _is_proactor:
|
||||
_asyncio.set_event_loop_policy(
|
||||
_asyncio.WindowsSelectorEventLoopPolicy() # type: ignore[attr-defined]
|
||||
)
|
||||
|
||||
print(
|
||||
"\n" + "=" * 78 + "\n"
|
||||
" HALLO FRIEND WINDOWS USER <3 WE GOTTA ADJUST THINGS BEFORE YOU STARTUP\n"
|
||||
+ "="
|
||||
* 78
|
||||
+ "\n"
|
||||
"\n"
|
||||
" uvicorn's default event loop on Windows (ProactorEventLoop) is not\n"
|
||||
" compatible with aiomqtt/paho-mqtt, which require add_reader() /\n"
|
||||
" add_writer(). Re-executing with '--loop none' so uvicorn honours\n"
|
||||
" WindowsSelectorEventLoopPolicy and MQTT fanout can function.\n"
|
||||
""
|
||||
" In English: The code we use for MQTT is fussy. We're restarting\n"
|
||||
" the server with the right settings for MQTT to work.\n"
|
||||
"\n"
|
||||
" This may or may not work :) If the app starts up after this without a warning, you're good to go.\n"
|
||||
"\n" + "=" * 78 + "\n",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# sys.argv[0] on Windows is typically a .exe console-script launcher
|
||||
# (e.g. .venv\Scripts\uvicorn.exe) which Python can't open as a script.
|
||||
# use "python -m uvicorn" instead, forwarding the original arguments.
|
||||
# yes, this is brittle as all hell.
|
||||
try:
|
||||
os.execv(
|
||||
sys.executable,
|
||||
[sys.executable, "-m", "uvicorn"] + sys.argv[1:] + ["--loop", "none"],
|
||||
)
|
||||
except Exception:
|
||||
# execv failed — fall through and let the app start normally.
|
||||
# MQTT fanout will not work, but everything else will.
|
||||
print(
|
||||
"\n" + "!" * 78 + "\n"
|
||||
" NOTE FOR WINDOWS USERS\n" + "!" * 78 + "\n"
|
||||
" AUTO-RESTART FAILED :<\n" + "!" * 78 + "\n"
|
||||
"\n"
|
||||
" The running event loop is ProactorEventLoop, which is not\n"
|
||||
" compatible with MQTT fanout (aiomqtt / paho-mqtt).\n"
|
||||
" We tried to restart uvicorn with the necessary settings\n"
|
||||
" automatically, but there was a problem with the invocation\n"
|
||||
" (not shocking; this is a fragile system).\n"
|
||||
"\n"
|
||||
" If you use MQTT integrations, restart with --loop none:\n"
|
||||
" Please rerun RemoteTerm with a command like:\n"
|
||||
"\n"
|
||||
" uv run uvicorn app.main:app \033[1m--loop none\033[0m"
|
||||
" [... other options ...]\n"
|
||||
" uv run uvicorn app.main:app \033[1m--loop none\033[0m [... other options ...]\n"
|
||||
"\n"
|
||||
" Everything else works fine as-is.\n"
|
||||
" Setting '--loop none' on uvicorn startup will put you in a good\n"
|
||||
" state for MQTT and bypass this self-repair.\n"
|
||||
"\n"
|
||||
" The server is starting anyway -- everything except MQTT fanout\n"
|
||||
" will work normally. If you want to suppress this attempt, \n"
|
||||
" set the env var MESHCORE_NO_AUTO_LOOP_ON_WIN32=true\n"
|
||||
"\n" + "!" * 78 + "\n",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
del _loop, _is_proactor
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
|
||||
# TCP
|
||||
# MESHCORE_TCP_HOST: 192.168.1.100
|
||||
# MESHCORE_TCP_PORT: 5000
|
||||
# MESHCORE_TCP_PORT: 4000
|
||||
|
||||
# BLE
|
||||
# BLE in Docker usually needs additional manual compose changes such as
|
||||
|
||||
@@ -11,14 +11,11 @@ import {
|
||||
Cell,
|
||||
} from 'recharts';
|
||||
|
||||
import { MeshCoreDecoder, Utils } from '@michaelhart/meshcore-decoder';
|
||||
|
||||
import { RawPacketList } from './RawPacketList';
|
||||
import { RawPacketInspectorDialog } from './RawPacketDetailModal';
|
||||
import { Button } from './ui/button';
|
||||
import type { Channel, Contact, RawPacket } from '../types';
|
||||
import {
|
||||
KNOWN_PAYLOAD_TYPES,
|
||||
RAW_PACKET_STATS_WINDOWS,
|
||||
buildRawPacketStatsSnapshot,
|
||||
type NeighborStat,
|
||||
@@ -27,26 +24,9 @@ import {
|
||||
type RawPacketStatsSessionState,
|
||||
type RawPacketStatsWindow,
|
||||
} from '../utils/rawPacketStats';
|
||||
import { createDecoderOptions } from '../utils/rawPacketInspector';
|
||||
import { getContactDisplayName } from '../utils/pubkey';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const KNOWN_PAYLOAD_TYPE_SET = new Set<string>(KNOWN_PAYLOAD_TYPES);
|
||||
|
||||
function getPacketTypeName(
|
||||
packet: RawPacket,
|
||||
decoderOptions?: ReturnType<typeof createDecoderOptions>
|
||||
): string {
|
||||
try {
|
||||
const decoded = MeshCoreDecoder.decode(packet.data, decoderOptions);
|
||||
if (!decoded.isValid) return 'Unknown';
|
||||
const name = Utils.getPayloadTypeName(decoded.payloadType);
|
||||
return KNOWN_PAYLOAD_TYPE_SET.has(name) ? name : 'Unknown';
|
||||
} catch {
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
interface RawPacketFeedViewProps {
|
||||
packets: RawPacket[];
|
||||
rawPacketStatsSession: RawPacketStatsSessionState;
|
||||
@@ -448,48 +428,6 @@ export function RawPacketFeedView({
|
||||
const [nowSec, setNowSec] = useState(() => Math.floor(Date.now() / 1000));
|
||||
const [selectedPacket, setSelectedPacket] = useState<RawPacket | null>(null);
|
||||
const [analyzeModalOpen, setAnalyzeModalOpen] = useState(false);
|
||||
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
|
||||
const [enabledTypes, setEnabledTypes] = useState<Set<string>>(() => new Set(KNOWN_PAYLOAD_TYPES));
|
||||
|
||||
const decoderOptions = useMemo(() => createDecoderOptions(channels), [channels]);
|
||||
|
||||
const packetsWithTypes = useMemo(
|
||||
() =>
|
||||
packets.map((packet) => ({
|
||||
packet,
|
||||
payloadType: getPacketTypeName(packet, decoderOptions),
|
||||
})),
|
||||
[packets, decoderOptions]
|
||||
);
|
||||
|
||||
const allTypesEnabled = enabledTypes.size === KNOWN_PAYLOAD_TYPES.length;
|
||||
|
||||
const filteredPackets = useMemo(() => {
|
||||
if (allTypesEnabled) return packets;
|
||||
return packetsWithTypes
|
||||
.filter(({ payloadType }) => enabledTypes.has(payloadType))
|
||||
.map(({ packet }) => packet);
|
||||
}, [packetsWithTypes, enabledTypes, packets, allTypesEnabled]);
|
||||
|
||||
const handleToggleAll = () => {
|
||||
setEnabledTypes(allTypesEnabled ? new Set() : new Set(KNOWN_PAYLOAD_TYPES));
|
||||
};
|
||||
|
||||
const handleToggleType = (type: string) => {
|
||||
setEnabledTypes((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(type)) {
|
||||
next.delete(type);
|
||||
} else {
|
||||
next.add(type);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnly = (type: string) => {
|
||||
setEnabledTypes(new Set([type]));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const interval = window.setInterval(() => {
|
||||
@@ -530,129 +468,38 @@ export function RawPacketFeedView({
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<div className="border-b border-border px-4 py-2.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="font-semibold text-base text-foreground">Raw Packet Feed</h2>
|
||||
<p className="hidden md:block text-xs text-muted-foreground">
|
||||
Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setAnalyzeModalOpen(true)}
|
||||
>
|
||||
Analyze Packet
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setStatsOpen((current) => !current)}
|
||||
aria-expanded={statsOpen}
|
||||
>
|
||||
{statsOpen ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
)}
|
||||
{statsOpen ? 'Hide Stats' : 'Show Stats'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3 border-b border-border px-4 py-2.5">
|
||||
<div>
|
||||
<h2 className="font-semibold text-base text-foreground">Raw Packet Feed</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="md:hidden text-xs text-muted-foreground">
|
||||
Collecting stats since {formatTimestamp(rawPacketStatsSession.sessionStartedAt)}
|
||||
{!mobileFiltersOpen && (
|
||||
<>
|
||||
{' · '}
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary hover:text-primary/80 transition-colors"
|
||||
onClick={() => setMobileFiltersOpen(true)}
|
||||
>
|
||||
Show Filters
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{mobileFiltersOpen && (
|
||||
<div className="mt-1.5 md:hidden flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<label className="flex items-center gap-1 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allTypesEnabled}
|
||||
onChange={handleToggleAll}
|
||||
className="rounded"
|
||||
/>
|
||||
All
|
||||
</label>
|
||||
{KNOWN_PAYLOAD_TYPES.map((type) => (
|
||||
<span key={type} className="inline-flex items-center gap-1 text-xs">
|
||||
<label className="flex items-center gap-1 text-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabledTypes.has(type)}
|
||||
onChange={() => handleToggleType(type)}
|
||||
className="rounded"
|
||||
/>
|
||||
{type}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="text-[0.625rem] text-muted-foreground hover:text-primary transition-colors"
|
||||
onClick={() => handleOnly(type)}
|
||||
>
|
||||
(only)
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-1.5 hidden md:flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<label className="flex items-center gap-1 text-xs text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allTypesEnabled}
|
||||
onChange={handleToggleAll}
|
||||
className="rounded"
|
||||
/>
|
||||
All
|
||||
</label>
|
||||
{KNOWN_PAYLOAD_TYPES.map((type) => (
|
||||
<span key={type} className="inline-flex items-center gap-1 text-xs">
|
||||
<label className="flex items-center gap-1 text-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabledTypes.has(type)}
|
||||
onChange={() => handleToggleType(type)}
|
||||
className="rounded"
|
||||
/>
|
||||
{type}
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="text-[0.625rem] text-muted-foreground hover:text-primary transition-colors"
|
||||
onClick={() => handleOnly(type)}
|
||||
>
|
||||
(only)
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setAnalyzeModalOpen(true)}
|
||||
>
|
||||
Analyze Packet
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setStatsOpen((current) => !current)}
|
||||
aria-expanded={statsOpen}
|
||||
>
|
||||
{statsOpen ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
|
||||
{statsOpen ? 'Hide Stats' : 'Show Stats'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
|
||||
<div className={cn('min-h-0 min-w-0 flex-1', statsOpen && 'md:border-r md:border-border')}>
|
||||
<RawPacketList
|
||||
packets={filteredPackets}
|
||||
channels={channels}
|
||||
onPacketClick={setSelectedPacket}
|
||||
/>
|
||||
<RawPacketList packets={packets} channels={channels} onPacketClick={setSelectedPacket} />
|
||||
</div>
|
||||
|
||||
<aside
|
||||
|
||||
@@ -1666,8 +1666,7 @@ function AppriseConfigEditor({
|
||||
rows={4}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
One URL per line. All URLs receive every matched notification. For Matrix room version 12
|
||||
(servername-less room IDs), append <code>?hsreq=no</code> to the URL.
|
||||
One URL per line. All URLs receive every matched notification.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const RAW_PACKET_STATS_WINDOW_SECONDS: Record<Exclude<RawPacketStatsWindow, 'ses
|
||||
|
||||
export const MAX_RAW_PACKET_STATS_OBSERVATIONS = 20000;
|
||||
|
||||
export const KNOWN_PAYLOAD_TYPES = [
|
||||
const KNOWN_PAYLOAD_TYPES = [
|
||||
'Advert',
|
||||
'GroupText',
|
||||
'TextMessage',
|
||||
|
||||
+4
-1
@@ -14,7 +14,7 @@ dependencies = [
|
||||
"pynacl>=1.5.0",
|
||||
"meshcore==2.3.2",
|
||||
"aiomqtt>=2.0",
|
||||
"apprise>=1.9.8",
|
||||
"apprise>=1.9.7",
|
||||
"boto3>=1.38.0",
|
||||
]
|
||||
|
||||
@@ -53,6 +53,9 @@ ignore = [
|
||||
"SIM117", # nested with statements - can be clearer in tests
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"app/main.py" = ["E402"] # imports after Windows event-loop re-exec block
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["app"]
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ SERIAL_HOST_PATH="/dev/ttyACM0"
|
||||
SERIAL_COMPOSE_HOST_PATH="/dev/ttyACM0"
|
||||
SERIAL_CONTAINER_PATH="/dev/meshcore-radio"
|
||||
TCP_HOST=""
|
||||
TCP_PORT="5000"
|
||||
TCP_PORT="4000"
|
||||
BLE_ADDRESS=""
|
||||
BLE_PIN=""
|
||||
ENABLE_BOTS="N"
|
||||
@@ -311,8 +311,8 @@ case "$TRANSPORT_CHOICE" in
|
||||
echo -e "${RED}TCP host is required.${NC}"
|
||||
read -r -p "TCP host: " TCP_HOST
|
||||
done
|
||||
read -r -p "TCP port (default: 5000): " TCP_PORT
|
||||
TCP_PORT="${TCP_PORT:-5000}"
|
||||
read -r -p "TCP port (default: 4000): " TCP_PORT
|
||||
TCP_PORT="${TCP_PORT:-4000}"
|
||||
echo -e "${GREEN}TCP: ${TCP_HOST}:${TCP_PORT}${NC}"
|
||||
;;
|
||||
3)
|
||||
|
||||
@@ -114,8 +114,8 @@ case "$TRANSPORT_CHOICE" in
|
||||
echo -e "${RED}TCP host is required.${NC}"
|
||||
read -rp "TCP host: " TCP_HOST
|
||||
done
|
||||
read -rp "TCP port (default: 5000): " TCP_PORT
|
||||
TCP_PORT="${TCP_PORT:-5000}"
|
||||
read -rp "TCP port (default: 4000): " TCP_PORT
|
||||
TCP_PORT="${TCP_PORT:-4000}"
|
||||
echo -e "${GREEN}TCP: ${TCP_HOST}:${TCP_PORT}${NC}"
|
||||
;;
|
||||
4)
|
||||
|
||||
@@ -56,7 +56,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "apprise"
|
||||
version = "1.9.9"
|
||||
version = "1.9.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
@@ -67,9 +67,9 @@ dependencies = [
|
||||
{ name = "requests-oauthlib" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/f4/be5c7e39b83a2285ab62ae7c19bb10704836f59c0a5b4c471730f54c9f98/apprise-1.9.9.tar.gz", hash = "sha256:fd622c0df16bdc79ed385539735573488cafe2405d25747e87eebd6b09b26012", size = 2032822 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/f5/97dc06b3401bb67abcef6e8bef7155f192b75795c2a2aa4d59eb5aa7fa66/apprise-1.9.7.tar.gz", hash = "sha256:2f73cc1e0264fb119fdb9b7cde82e8fde40a0f531ac885d8c6f0cf0f6e13aec2", size = 1937173 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/2f/54d068d7e011a8b4e0aae3e93b09a30b33bcf780829fe70c6e8876aeb0e0/apprise-1.9.9-py3-none-any.whl", hash = "sha256:55ceb8827a1c783d683881c9f77fa42eb43b3fc91b854419c452d557101c7068", size = 1519940 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/6b/cfa80a13437896eb8f4504ddac6dfa4ef7f1d2b2261057aa4a30003b8de6/apprise-1.9.7-py3-none-any.whl", hash = "sha256:c7640a81a1097685de66e0508e3da89f49235d566cb44bbead1dd98419bf5ee3", size = 1459879 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1022,7 +1022,7 @@ dev = [
|
||||
requires-dist = [
|
||||
{ name = "aiomqtt", specifier = ">=2.0" },
|
||||
{ name = "aiosqlite", specifier = ">=0.19.0" },
|
||||
{ name = "apprise", specifier = ">=1.9.8" },
|
||||
{ name = "apprise", specifier = ">=1.9.7" },
|
||||
{ name = "boto3", specifier = ">=1.38.0" },
|
||||
{ name = "fastapi", specifier = ">=0.115.0" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
|
||||
Reference in New Issue
Block a user