Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/usb troubleshooter #1872

Merged
merged 6 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docker-compose.override.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ services:
# source: ../brewblox-devcon-spark/brewblox_devcon_spark
# target: /app/brewblox_devcon_spark
# ports:
# - "5678:5678"
# - '5678:5678'
# environment:
# - ENABLE_DEBUGGER=1
# labels:
Expand Down Expand Up @@ -75,3 +75,12 @@ services:
# - type: bind
# source: ../brewblox-auth/entrypoint.sh
# target: /app/entrypoint.sh

# You need to change "enp8s0" to the name of your physical network interface
# reflector:
# image: yuxzhu/mdns-reflector:latest
# restart: on-failure:3
# network_mode: host
# labels:
# - traefik.enable=false
# command: mdns-reflector -fn4 enp8s0 br-${COMPOSE_PROJECT_NAME}
13 changes: 13 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
name: brewblox-ui

networks:
default:
driver_opts:
com.docker.network.bridge.name: br-${COMPOSE_PROJECT_NAME}

services:
eventbus:
image: ghcr.io/brewblox/mosquitto:develop
Expand Down Expand Up @@ -64,6 +70,13 @@ services:
source: ${BREWBLOX_CACHE_DIR}/simulator__spock
target: /app/simulator

usb-proxy:
image: ghcr.io/brewblox/brewblox-usb-proxy:develop
restart: unless-stopped
privileged: true
labels:
- traefik.http.services.usb-proxy.loadbalancer.server.port=5000

tilt:
image: ghcr.io/brewblox/brewblox-tilt:develop
restart: unless-stopped
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"parse-duration": "^1.1.0",
"pinia": "^2.1.7",
"plotly.js": "^2.32.0",
"pluralize": "^8.0.0",
"portal-vue": "^3.0.0",
"process": "^0.11.10",
"quasar": "^2.16.2",
Expand Down
8 changes: 1 addition & 7 deletions src/plugins/spark/service/SparkActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
discoverBlocks,
startResetBlocks,
} from '@/plugins/spark/utils/actions';
import { cleanUnusedNames } from '@/plugins/spark/utils/formatting';
import { useServiceStore } from '@/store/services';
import { useSystemStore } from '@/store/system';
import { userUISettings } from '@/user-settings';
Expand Down Expand Up @@ -68,7 +67,7 @@ function startDialog(component: GlobalDialogName): void {
/>
<ActionItem
icon="mdi-magnify-plus-outline"
label="Discover new OneWire blocks"
label="Discover blocks"
@click="discoverBlocks(serviceId)"
/>
<ActionItem
Expand Down Expand Up @@ -96,11 +95,6 @@ function startDialog(component: GlobalDialogName): void {
label="Controller backups"
@click="startDialog('SparkBackupDialog')"
/>
<ActionItem
icon="mdi-tag-remove"
label="Remove unused block names"
@click="cleanUnusedNames(serviceId)"
/>
<ActionItem
icon="delete"
label="Remove all blocks"
Expand Down
114 changes: 104 additions & 10 deletions src/plugins/spark/service/SparkTroubleshooter.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
<script setup lang="ts">
import { SparkStatusDescription } from 'brewblox-proto/ts';
import { computed, provide, reactive } from 'vue';
import pluralize from 'pluralize';
import { computed, inject, provide, reactive, ref, watch } from 'vue';
import { useSparkStore } from '@/plugins/spark/store';
import {
fetchKnownMdnsDevices,
fetchKnownUsbDevices,
} from '@/plugins/spark/store/spark-api';
import { SparkUsbDevices } from '@/plugins/spark/types';
import { WidgetContext } from '@/store/features';
import { ContextKey } from '@/symbols';
import { ContextKey, NowKey } from '@/symbols';
import { createDialog } from '@/utils/dialog';
import { shortDateString } from '@/utils/quantity';

Expand Down Expand Up @@ -36,6 +42,10 @@ provide(
);

const sparkStore = useSparkStore();
const now = inject(NowKey)!;

const mDNSDevices = ref<string[] | null>(null);
const usbDevices = ref<SparkUsbDevices | null>(null);

const status = computed<SparkStatusDescription | null>(() =>
sparkStore.statusByService(props.serviceId),
Expand All @@ -52,6 +62,34 @@ const connectionStep = computed<ConnectionStep>(() => {
return status.value.connection_status;
});

const desiredDeviceId = computed<string | null>(
() => status.value?.service.device.device_id ?? null,
);

const discoveryKindDesc = computed<string>(() => {
const kind = status.value?.['discovery_kind'];
switch (kind) {
case undefined:
return 'Unknown';
case 'ADDRESS':
return 'Fixed IP address';
case 'SIM':
return 'Simulator';
case 'MDNS':
return 'mDNS';
case 'ALL':
return 'USB, mDNS, and MQTT';
default:
return kind;
}
});

// The Spark 4 does not support USB, and has a device ID of 12 (hex MAC address)
// If there is no desired device ID, we assume USB may be supported.
const usbSupported = computed<boolean>(
() => desiredDeviceId.value?.length != 12,
);

function isStepDone(step: ConnectionStep): boolean {
return stepOrder.indexOf(step) < stepOrder.indexOf(connectionStep.value);
}
Expand All @@ -77,6 +115,21 @@ function startFirmwareUpdate(): void {
function serviceReboot(): void {
sparkStore.serviceReboot(props.serviceId);
}

watch(
() => now.value,
async () => {
if (
connectionStep.value != 'UNREACHABLE' &&
usbSupported.value &&
status.value?.enabled !== false
) {
usbDevices.value = await fetchKnownUsbDevices(props.serviceId);
mDNSDevices.value = await fetchKnownMdnsDevices(props.serviceId);
}
},
{ immediate: true },
);
</script>

<template>
Expand Down Expand Up @@ -140,17 +193,58 @@ function serviceReboot(): void {
<template v-if="status">
<template v-if="status.enabled">
Your Spark service is online, but not connected to your
controller.
controller. The service discovers controllers using
<b>{{ discoveryKindDesc }} </b>, and the desired device ID is
<b>{{ desiredDeviceId || 'any' }}</b>
.

<ul>
<li>Is your controller turned on?</li>
<li>Does your controller have the correct firmware?</li>
<li>WiFi: Does your controller display its IP address?</li>
<li>Are there any error messages in your service logs?</li>
<li>
USB: Your service must have been (re)started after plugging in
the USB cable.
</li>
<li>USB: Can your service access USB devices? (Mac hosts)</li>
<li>Does your controller display its IP address?</li>
<template v-if="mDNSDevices == null">
<li>Checking mDNS devices...</li>
</template>
<template v-else>
<li>
Detected
{{ pluralize('controller', mDNSDevices.length, true) }}
using mDNS
<ul>
<li
v-for="dev in mDNSDevices"
:key="dev"
>
{{ dev }}
</li>
</ul>
</li>
</template>
<template v-if="usbSupported">
<template v-if="usbDevices == null">
<li>Checking USB devices...</li>
</template>
<template v-else-if="usbDevices.enabled">
<li>
Detected
{{
pluralize('controller', usbDevices.devices.length, true)
}}
using USB
<ul>
<li
v-for="dev in usbDevices.devices"
:key="dev"
>
{{ dev }}
</li>
</ul>
</li>
</template>
<template v-else>
<li>USB: The USB proxy is not enabled in brewblox.yml.</li>
</template>
</template>
</ul>
</template>
<template v-else>
Expand Down
27 changes: 20 additions & 7 deletions src/plugins/spark/store/spark-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { AxiosResponse } from 'axios';
import { Block, SparkStatusDescription } from 'brewblox-proto/ts';
import { BlockIds, BlockPatchArgs, SparkBackup } from '@/plugins/spark/types';
import {
BlockIds,
BlockPatchArgs,
SparkBackup,
SparkUsbDevices,
} from '@/plugins/spark/types';
import { http, intercept } from '@/utils/http';
import { notify } from '@/utils/notify';

Expand Down Expand Up @@ -139,12 +144,6 @@ export const clearBlocks = (serviceId: string): Promise<any> =>
.post(`/${encodeURIComponent(serviceId)}/blocks/all/delete`)
.catch(intercept(`Failed to clear blocks on ${serviceId}`));

export const cleanUnusedNames = (serviceId: string): Promise<string[]> =>
http
.post<BlockIds[]>(`/${encodeURIComponent(serviceId)}/blocks/cleanup`)
.then((resp) => resp.data.map((v) => v.id!))
.catch(intercept(`Failed to clean unused block names on ${serviceId}`));

export const fetchDiscoveredBlocks = (serviceId: string): Promise<Block[]> =>
http
.post<Block[]>(`/${encodeURIComponent(serviceId)}/blocks/discover`)
Expand Down Expand Up @@ -280,3 +279,17 @@ export const controllerReboot = (serviceId: string): Promise<any> =>
.post(`/${encodeURIComponent(serviceId)}/system/reboot/controller`, {})
.then((resp) => resp.data)
.catch(intercept(`Failed to reboot ${serviceId}`));

export const fetchKnownUsbDevices = (
serviceId: string,
): Promise<SparkUsbDevices> =>
http
.post(`/${encodeURIComponent(serviceId)}/system/usb`, {})
.then((resp) => resp.data)
.catch(intercept(`Failed to query USB devices for ${serviceId}`));

export const fetchKnownMdnsDevices = (serviceId: string): Promise<string[]> =>
http
.post(`/${encodeURIComponent(serviceId)}/system/mdns`, {})
.then((resp) => resp.data)
.catch(intercept(`Failed to query mDNS devices for ${serviceId}`));
5 changes: 0 additions & 5 deletions src/plugins/spark/store/spark-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,6 @@ export const useSparkStore = defineStore('sparkStore', () => {
discoveredBlockIds.value[serviceId] = [];
}

async function cleanUnusedNames(serviceId: string): Promise<string[]> {
return await sparkApi.cleanUnusedNames(serviceId);
}

async function fetchBlock(block: Maybe<Block>): Promise<void> {
if (block) {
const fetched = await sparkApi.fetchBlock(block);
Expand Down Expand Up @@ -525,7 +521,6 @@ export const useSparkStore = defineStore('sparkStore', () => {
invalidateBlocks,
saveEnabled,
clearDiscoveredBlocks,
cleanUnusedNames,
fetchBlock,
fetchStoredBlock,
fetchBlocks,
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/spark/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export interface SparkBackup {
store: SparkStoreEntry[];
}

export interface SparkUsbDevices {
enabled: boolean;
devices: string[];
}

export interface BlockLimitation {
target: string;
constraint: AnyConstraintKey;
Expand Down
19 changes: 0 additions & 19 deletions src/plugins/spark/utils/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,13 @@ import {
OneWireGpioModuleBlock,
} from 'brewblox-proto/ts';
import { Enum } from 'typescript-string-enums';
import { useSparkStore } from '@/plugins/spark/store';
import { BlockAddress } from '@/plugins/spark/types';
import { notify } from '@/utils/notify';
import { matchesType } from '@/utils/objects';
import { durationString, prettyLink } from '@/utils/quantity';

export const prettyBlock = (v: BlockAddress | null | undefined): string =>
v?.id || '<not set>';

export async function cleanUnusedNames(
serviceId: string | null,
): Promise<void> {
const sparkStore = useSparkStore();
if (!sparkStore.has(serviceId)) {
return;
}
const names = await sparkStore.cleanUnusedNames(serviceId);

const message =
names.length > 0
? `Cleaned block names: <i>${names.join(', ')}</i>.`
: 'No unused names found.';

notify.info({ message, icon: 'mdi-tag-remove' });
}

export const enumHint = (e: Enum<any>): string =>
'One of: ' +
Enum.values(e)
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3124,6 +3124,7 @@ __metadata:
parse-duration: "npm:^1.1.0"
pinia: "npm:^2.1.7"
plotly.js: "npm:^2.32.0"
pluralize: "npm:^8.0.0"
portal-vue: "npm:^3.0.0"
prettier: "npm:^3.2.5"
process: "npm:^0.11.10"
Expand Down Expand Up @@ -7987,6 +7988,13 @@ __metadata:
languageName: node
linkType: hard

"pluralize@npm:^8.0.0":
version: 8.0.0
resolution: "pluralize@npm:8.0.0"
checksum: 10c0/2044cfc34b2e8c88b73379ea4a36fc577db04f651c2909041b054c981cd863dd5373ebd030123ab058d194ae615d3a97cfdac653991e499d10caf592e8b3dc33
languageName: node
linkType: hard

"point-in-polygon@npm:^1.1.0":
version: 1.1.0
resolution: "point-in-polygon@npm:1.1.0"
Expand Down
Loading