From 991200137b6faa929bee50357a60f5dca82d5374 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 14 Nov 2024 12:39:05 +0000 Subject: [PATCH 1/9] feat: check for and indicate sds firmware updates against hardcoded versions list --- companion/lib/Surface/Controller.ts | 22 ++++++- companion/lib/Surface/Types.ts | 9 ++- companion/lib/Surface/USB/ElgatoStreamDeck.ts | 57 +++++++++++++++++++ shared-lib/lib/Model/Surfaces.ts | 6 ++ webui/src/App.tsx | 3 +- webui/src/Stores/SurfacesStore.tsx | 14 +++++ webui/src/Surfaces/KnownSurfacesTable.tsx | 15 ++++- webui/src/Surfaces/TabNotifyIcon.tsx | 16 ++++++ webui/src/scss/_common.scss | 16 ++++++ 9 files changed, 153 insertions(+), 5 deletions(-) create mode 100644 webui/src/Surfaces/TabNotifyIcon.tsx diff --git a/companion/lib/Surface/Controller.ts b/companion/lib/Surface/Controller.ts index c9957ea33b..e16cb5adfe 100644 --- a/companion/lib/Surface/Controller.ts +++ b/companion/lib/Surface/Controller.ts @@ -19,7 +19,7 @@ import findProcess from 'find-process' import HID from 'node-hid' import jsonPatch from 'fast-json-patch' -import { cloneDeep } from 'lodash-es' +import { cloneDeep, isEqual } from 'lodash-es' import { nanoid } from 'nanoid' import pDebounce from 'p-debounce' import { getStreamDeckDeviceInfo } from '@elgato-stream-deck/node' @@ -341,6 +341,24 @@ export class SurfaceController extends EventEmitter { // Update the group to have the new surface this.#attachSurfaceToGroup(handler) + + // Perform an update check in the background + setTimeout(() => { + const firmwareUpdatesBefore = panel.info.hasFirmwareUpdates + panel + .checkForFirmwareUpdates?.() + .then(() => { + if (isEqual(firmwareUpdatesBefore, panel.info.hasFirmwareUpdates)) return + + this.#logger.info(`Firmware updates change for surface "${handler.surfaceId}"`) + + // Inform ui of the updates + this.updateDevicesList() + }) + .catch((e) => { + this.#logger.warn(`Failed to check for firmware updates for surface "${handler.surfaceId}": ${e}`) + }) + }, 0) } /** @@ -682,6 +700,7 @@ export class SurfaceController extends EventEmitter { isConnected: !!surfaceHandler, displayName: getSurfaceName(config, id), location: null, + hasFirmwareUpdates: null, size: config.gridSize || null, offset: { columns: config?.config?.xOffset ?? 0, rows: config?.config?.yOffset ?? 0 }, @@ -693,6 +712,7 @@ export class SurfaceController extends EventEmitter { surfaceInfo.location = location || null surfaceInfo.configFields = surfaceHandler.panel.info.configFields || [] + surfaceInfo.hasFirmwareUpdates = surfaceHandler.panel.info.hasFirmwareUpdates || null } return surfaceInfo diff --git a/companion/lib/Surface/Types.ts b/companion/lib/Surface/Types.ts index e90c974a12..7685c264ed 100644 --- a/companion/lib/Surface/Types.ts +++ b/companion/lib/Surface/Types.ts @@ -1,4 +1,8 @@ -import type { CompanionSurfaceConfigField, GridSize } from '@companion-app/shared/Model/Surfaces.js' +import type { + CompanionSurfaceConfigField, + GridSize, + SurfaceFirmwareUpdateInfo, +} from '@companion-app/shared/Model/Surfaces.js' import type { ImageResult } from '../Graphics/ImageResult.js' import type { EventEmitter } from 'events' import type { CompanionVariableValue, CompanionVariableValues } from '@companion-module/base' @@ -29,7 +33,9 @@ export interface SurfacePanelInfo { type: string configFields: CompanionSurfaceConfigField[] location?: string + hasFirmwareUpdates?: SurfaceFirmwareUpdateInfo } + export interface SurfacePanel extends EventEmitter { readonly info: SurfacePanelInfo readonly gridSize: GridSize @@ -41,6 +47,7 @@ export interface SurfacePanel extends EventEmitter { getDefaultConfig?: () => any onVariablesChanged?: (allChangedVariables: Set) => void quit(): void + checkForFirmwareUpdates?: (latestVersions?: Record) => Promise } export interface DrawButtonItem { x: number diff --git a/companion/lib/Surface/USB/ElgatoStreamDeck.ts b/companion/lib/Surface/USB/ElgatoStreamDeck.ts index 0e3a68d4b5..cb6f991aef 100644 --- a/companion/lib/Surface/USB/ElgatoStreamDeck.ts +++ b/companion/lib/Surface/USB/ElgatoStreamDeck.ts @@ -33,6 +33,7 @@ import type { CompanionSurfaceConfigField, GridSize } from '@companion-app/share import type { SurfacePanel, SurfacePanelEvents, SurfacePanelInfo } from '../Types.js' import type { LcdPosition, StreamDeckLcdSegmentControlDefinition, StreamDeckTcp } from '@elgato-stream-deck/tcp' import type { ImageResult } from '../../Graphics/ImageResult.js' +import { SemVer } from 'semver' const setTimeoutPromise = util.promisify(setTimeout) @@ -63,6 +64,18 @@ function getConfigFields(streamDeck: StreamDeck): CompanionSurfaceConfigField[] return fields } +/** + * The latest firmware versions for the SDS at the time this was last updated + */ +const LATEST_SDS_FIRMWARE_VERSIONS: Record = { + AP2: '1.05.009', + ENCODER_AP2_1: '1.01.012', + ENCODER_AP2_2: '1.01.012', + ENCODER_LD_1: '1.01.006', + ENCODER_LD_2: '1.01.006', +} +const SDS_UPDATE_TOOL_URL = 'https://bitfocus.io/?elgato-sds-firmware-updater' + export class SurfaceUSBElgatoStreamDeck extends EventEmitter implements SurfacePanel { readonly #logger: Logger @@ -336,6 +349,41 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter return self } + async checkForFirmwareUpdates(latestVersions?: Record): Promise { + // If no versions are provided, use the latest known versions for the SDS + if (!latestVersions && this.#streamDeck.MODEL === DeviceModelId.STUDIO) + latestVersions = LATEST_SDS_FIRMWARE_VERSIONS + + // If no versions are provided, we can't know that there are updates + if (!latestVersions) { + this.info.hasFirmwareUpdates = undefined + return + } + + let hasUpdate = false + + const currentVersions = await this.#streamDeck.getAllFirmwareVersions() + + for (const [key, targetVersion] of Object.entries(latestVersions)) { + const currentVersion = parseVersion(currentVersions[key]) + const latestVersion = parseVersion(targetVersion) + + if (currentVersion && latestVersion && latestVersion.compare(currentVersion) > 0) { + this.#logger.info(`Firmware update available for ${key}: ${currentVersion} -> ${latestVersion}`) + hasUpdate = true + break + } + } + + if (hasUpdate) { + this.info.hasFirmwareUpdates = { + updaterDownloadUrl: SDS_UPDATE_TOOL_URL, + } + } else { + this.info.hasFirmwareUpdates = undefined + } + } + /** * Process the information from the GUI and what is saved in database * @returns false when nothing happens @@ -381,3 +429,12 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter this.#writeQueue.queue(`${x}_${y}`, x, y, render) } } + +function parseVersion(rawVersion: string): SemVer | null { + // These versions are not semver, but can hopefully be safely cooerced into it + + const parts = rawVersion.split('.') + if (parts.length !== 3) return null + + return new SemVer(`${parseInt(parts[0])}.${parseInt(parts[1])}.${parseInt(parts[2])}`) +} diff --git a/shared-lib/lib/Model/Surfaces.ts b/shared-lib/lib/Model/Surfaces.ts index c9498db35f..30b458b6f8 100644 --- a/shared-lib/lib/Model/Surfaces.ts +++ b/shared-lib/lib/Model/Surfaces.ts @@ -16,6 +16,10 @@ export interface RowsAndColumns { columns: number } +export interface SurfaceFirmwareUpdateInfo { + updaterDownloadUrl: string +} + export interface ClientSurfaceItem { id: string type: string @@ -26,6 +30,8 @@ export interface ClientSurfaceItem { displayName: string location: string | null + hasFirmwareUpdates: SurfaceFirmwareUpdateInfo | null + size: RowsAndColumns | null offset: RowsAndColumns | null } diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 5fb1f6a7e6..6ae201a523 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -46,6 +46,7 @@ import { ImportExport } from './ImportExport/index.js' import { RootAppStoreContext } from './Stores/RootAppStore.js' import { observer } from 'mobx-react-lite' import { ConnectionVariables } from './Variables/index.js' +import { SurfacesTabNotifyIcon } from './Surfaces/TabNotifyIcon.js' const useTouchBackend = window.localStorage.getItem('test_touch_backend') === '1' const showCloudTab = window.localStorage.getItem('show_companion_cloud') === '1' @@ -456,7 +457,7 @@ const AppContent = observer(function AppContent({ buttonGridHotPress }: AppConte - Surfaces + Surfaces diff --git a/webui/src/Stores/SurfacesStore.tsx b/webui/src/Stores/SurfacesStore.tsx index c2da1bba5c..27c2015d9f 100644 --- a/webui/src/Stores/SurfacesStore.tsx +++ b/webui/src/Stores/SurfacesStore.tsx @@ -119,4 +119,18 @@ export class SurfacesStore { surfaces: overflowingSurfaces, } } + + public countFirmwareUpdates(): number { + let count = 0 + + for (const group of this.store.values()) { + for (const surface of group.surfaces) { + if (surface.hasFirmwareUpdates) { + count++ + } + } + } + + return count + } } diff --git a/webui/src/Surfaces/KnownSurfacesTable.tsx b/webui/src/Surfaces/KnownSurfacesTable.tsx index 1d70929ac6..f57b378132 100644 --- a/webui/src/Surfaces/KnownSurfacesTable.tsx +++ b/webui/src/Surfaces/KnownSurfacesTable.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useContext, useRef } from 'react' import { CButton, CButtonGroup } from '@coreui/react' import { socketEmitPromise } from '../util.js' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faCog, faFolderOpen, faSearch, faTrash } from '@fortawesome/free-solid-svg-icons' +import { faCircleUp, faCog, faFolderOpen, faSearch, faTrash } from '@fortawesome/free-solid-svg-icons' import { TextInputField } from '../Components/TextInputField.js' import { GenericConfirmModal, GenericConfirmModalRef } from '../Components/GenericConfirmModal.js' import { SurfaceEditModal, SurfaceEditModalRef } from './EditModal.js' @@ -11,6 +11,7 @@ import { ClientDevicesListItem, ClientSurfaceItem } from '@companion-app/shared/ import { RootAppStoreContext } from '../Stores/RootAppStore.js' import { observer } from 'mobx-react-lite' import { NonIdealState } from '../Components/NonIdealState.js' +import { WindowLinkOpen } from '../Helpers/Window.js' export const KnownSurfacesTable = observer(function SurfacesPage() { const { surfaces, socket } = useContext(RootAppStoreContext) @@ -236,7 +237,17 @@ function SurfaceRow({ - {surface.type} + + {surface.type} + {!!surface.hasFirmwareUpdates && ( + <> + {' '} + + + + + )} + {surface.isConnected ? surface.location || 'Local' : 'Offline'} {surface.isConnected ? ( diff --git a/webui/src/Surfaces/TabNotifyIcon.tsx b/webui/src/Surfaces/TabNotifyIcon.tsx new file mode 100644 index 0000000000..353c832a2b --- /dev/null +++ b/webui/src/Surfaces/TabNotifyIcon.tsx @@ -0,0 +1,16 @@ +import { observer } from 'mobx-react-lite' +import React, { useContext } from 'react' +import { RootAppStoreContext } from '../Stores/RootAppStore.js' + +export const SurfacesTabNotifyIcon = observer(function SurfacesTabNotifyIcon(): JSX.Element | null { + const { surfaces } = useContext(RootAppStoreContext) + + const updateCount = surfaces.countFirmwareUpdates() + if (updateCount === 0) return null + + return ( + + {updateCount} + + ) +}) diff --git a/webui/src/scss/_common.scss b/webui/src/scss/_common.scss index ff83f41b74..c7019bcfd3 100644 --- a/webui/src/scss/_common.scss +++ b/webui/src/scss/_common.scss @@ -310,3 +310,19 @@ table .td-reorder { display: grid; grid-template-columns: minmax(0, 1fr); } + +.notification-count { + background-color: $primary; + color: #fff; + font-weight: bold; + + border-radius: 50%; + // padding: 0.1rem; + font-size: 0.7rem; + vertical-align: top; + text-align: center; + + margin-right: -0.5rem; + min-width: calc(var(--cui-body-line-height) * 1em); + display: inline-block; +} From 153a536575089ed3d6f7643d085079db60badbf5 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 14 Nov 2024 13:58:23 +0000 Subject: [PATCH 2/9] chore: update streamdeck lib --- companion/package.json | 4 ++-- yarn.lock | 32 ++++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/companion/package.json b/companion/package.json index 27d0a61d77..8ba31a8599 100644 --- a/companion/package.json +++ b/companion/package.json @@ -48,8 +48,8 @@ "@blackmagic-controller/node": "^0.1.0", "@companion-app/shared": "*", "@companion-module/base": "^1.11.0", - "@elgato-stream-deck/node": "^7.0.2", - "@elgato-stream-deck/tcp": "^7.0.2", + "@elgato-stream-deck/node": "^7.1.0", + "@elgato-stream-deck/tcp": "^7.1.0", "@julusian/bonjour-service": "^1.3.0-2", "@julusian/image-rs": "^1.1.1", "@julusian/jpeg-turbo": "^2.2.0", diff --git a/yarn.lock b/yarn.lock index 0580b674b5..5ca18ba307 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1494,13 +1494,13 @@ __metadata: languageName: node linkType: hard -"@elgato-stream-deck/core@npm:7.0.2": - version: 7.0.2 - resolution: "@elgato-stream-deck/core@npm:7.0.2" +"@elgato-stream-deck/core@npm:7.1.0": + version: 7.1.0 + resolution: "@elgato-stream-deck/core@npm:7.1.0" dependencies: eventemitter3: "npm:^5.0.1" tslib: "npm:^2.7.0" - checksum: 10c0/a497fc9ce37487eeabc0c8ee59cca15253316a84c8b62acf3be093650ce849bbd291e87a1e64f5698f8eace1b921ba2276fdbb58bc08ea48a779e1c89325da28 + checksum: 10c0/1c0b78581ef2b6a8f529a14786350ae15d0ef7e9b71f0629f811365d391cee93d3dec4fa82b109c9e196b050878964b8839708fe7ab340d7a9d4097fecf3a9ea languageName: node linkType: hard @@ -1516,29 +1516,29 @@ __metadata: languageName: node linkType: hard -"@elgato-stream-deck/node@npm:^7.0.2": - version: 7.0.2 - resolution: "@elgato-stream-deck/node@npm:7.0.2" +"@elgato-stream-deck/node@npm:^7.1.0": + version: 7.1.0 + resolution: "@elgato-stream-deck/node@npm:7.1.0" dependencies: - "@elgato-stream-deck/core": "npm:7.0.2" + "@elgato-stream-deck/core": "npm:7.1.0" "@elgato-stream-deck/node-lib": "npm:7.0.2" eventemitter3: "npm:^5.0.1" node-hid: "npm:^3.1.0" tslib: "npm:^2.7.0" - checksum: 10c0/d5cee54f2659ec766a169b6023529eb41d2c387258f6bc9da281ad33d4364ddb144c36de690997a7c3ea92471e9061ef0a1e29c3e5c9642f2ddedc6df10196ef + checksum: 10c0/9863f4281087ada1c392d05db12700c7f3ce0ecf5718794b36864273be9b20ae4cf464363ac78664aab64382848b97d715ff53a752ead977e50beb44ff66818c languageName: node linkType: hard -"@elgato-stream-deck/tcp@npm:^7.0.2": - version: 7.0.2 - resolution: "@elgato-stream-deck/tcp@npm:7.0.2" +"@elgato-stream-deck/tcp@npm:^7.1.0": + version: 7.1.0 + resolution: "@elgato-stream-deck/tcp@npm:7.1.0" dependencies: - "@elgato-stream-deck/core": "npm:7.0.2" + "@elgato-stream-deck/core": "npm:7.1.0" "@elgato-stream-deck/node-lib": "npm:7.0.2" "@julusian/bonjour-service": "npm:^1.3.0-2" eventemitter3: "npm:^5.0.1" tslib: "npm:^2.6.3" - checksum: 10c0/59f06127cbe289a7a2c644b3987e0e9de3f243edec63fcac1036e61757e2007dd87dff868e1ca834d3e89cceeacc1a70211b14bc2dafd0a1e0d434a7c147e36e + checksum: 10c0/70488867525e00c409b6e5c0f80f3f3c1190122b87141b519827c55a08b389fbbaedeba1a4922accefd48fd2ffd9ed4245b2cd3562745d6bce71a381d1ad1085 languageName: node linkType: hard @@ -6450,8 +6450,8 @@ asn1@evs-broadcast/node-asn1: "@blackmagic-controller/node": "npm:^0.1.0" "@companion-app/shared": "npm:*" "@companion-module/base": "npm:^1.11.0" - "@elgato-stream-deck/node": "npm:^7.0.2" - "@elgato-stream-deck/tcp": "npm:^7.0.2" + "@elgato-stream-deck/node": "npm:^7.1.0" + "@elgato-stream-deck/tcp": "npm:^7.1.0" "@julusian/bonjour-service": "npm:^1.3.0-2" "@julusian/image-rs": "npm:^1.1.1" "@julusian/jpeg-turbo": "npm:^2.2.0" From a52ba44165e04f337895006e0748e37261534e19 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 14 Nov 2024 15:06:05 +0000 Subject: [PATCH 3/9] feat: online sds version update check --- companion/lib/Surface/Controller.ts | 21 +-- companion/lib/Surface/FirmwareUpdateCheck.ts | 170 ++++++++++++++++++ companion/lib/Surface/Types.ts | 1 + companion/lib/Surface/USB/ElgatoStreamDeck.ts | 5 + package.json | 1 + webui/src/Surfaces/KnownSurfacesTable.tsx | 4 +- yarn.lock | 19 +- 7 files changed, 194 insertions(+), 27 deletions(-) create mode 100644 companion/lib/Surface/FirmwareUpdateCheck.ts diff --git a/companion/lib/Surface/Controller.ts b/companion/lib/Surface/Controller.ts index e16cb5adfe..d44707cd2f 100644 --- a/companion/lib/Surface/Controller.ts +++ b/companion/lib/Surface/Controller.ts @@ -62,6 +62,7 @@ import { createOrSanitizeSurfaceHandlerConfig } from './Config.js' import { EventEmitter } from 'events' import LogController from '../Log/Controller.js' import type { DataDatabase } from '../Data/Database.js' +import { SurfaceFirmwareUpdateCheck } from './FirmwareUpdateCheck.js' // Force it to load the hidraw driver just in case HID.setDriverType('hidraw') @@ -133,6 +134,8 @@ export class SurfaceController extends EventEmitter { readonly #outboundController: SurfaceOutboundController + readonly #firmwareUpdates: SurfaceFirmwareUpdateCheck + constructor(db: DataDatabase, handlerDependencies: SurfaceHandlerDependencies, io: UIHandler) { super() @@ -141,6 +144,7 @@ export class SurfaceController extends EventEmitter { this.#io = io this.#outboundController = new SurfaceOutboundController(this, db, io) + this.#firmwareUpdates = new SurfaceFirmwareUpdateCheck(this.#surfaceHandlers, () => this.updateDevicesList()) this.#surfacesAllLocked = !!this.#handlerDependencies.userconfig.getKey('link_lockouts') @@ -343,22 +347,7 @@ export class SurfaceController extends EventEmitter { this.#attachSurfaceToGroup(handler) // Perform an update check in the background - setTimeout(() => { - const firmwareUpdatesBefore = panel.info.hasFirmwareUpdates - panel - .checkForFirmwareUpdates?.() - .then(() => { - if (isEqual(firmwareUpdatesBefore, panel.info.hasFirmwareUpdates)) return - - this.#logger.info(`Firmware updates change for surface "${handler.surfaceId}"`) - - // Inform ui of the updates - this.updateDevicesList() - }) - .catch((e) => { - this.#logger.warn(`Failed to check for firmware updates for surface "${handler.surfaceId}": ${e}`) - }) - }, 0) + this.#firmwareUpdates.triggerCheckSurfaceForUpdates(handler) } /** diff --git a/companion/lib/Surface/FirmwareUpdateCheck.ts b/companion/lib/Surface/FirmwareUpdateCheck.ts new file mode 100644 index 0000000000..bc56fd9e50 --- /dev/null +++ b/companion/lib/Surface/FirmwareUpdateCheck.ts @@ -0,0 +1,170 @@ +import { isEqual } from 'lodash-es' +import type { SurfaceHandler } from './Handler.js' +import LogController from '../Log/Controller.js' + +const FIRMWARE_UPDATE_POLL_INTERVAL = 1000 * 60 * 60 * 24 // 24 hours +const FIRMWARE_PAYLOAD_CACHE_TTL = 1000 * 60 * 60 * 4 // 4 hours +const FIRMWARE_PAYLOAD_CACHE_MAX_TTL = 1000 * 60 * 60 * 24 // 24 hours + +interface PayloadCacheEntry { + timestamp: number + payload: Record +} + +export class SurfaceFirmwareUpdateCheck { + readonly #logger = LogController.createLogger('Surface/FirmwareUpdateCheck') + + readonly #payloadCache = new Map() + + readonly #payloadUpdating = new Map | null>>() + + /** + * All the opened and active surfaces + */ + readonly #surfaceHandlers: Map + + readonly #updateDevicesList: () => void + + constructor(surfaceHandlers: Map, updateDevicesList: () => void) { + this.#surfaceHandlers = surfaceHandlers + this.#updateDevicesList = updateDevicesList + + setInterval(() => this.#checkAllSurfacesForUpdates(), FIRMWARE_UPDATE_POLL_INTERVAL) + setTimeout(() => this.#checkAllSurfacesForUpdates(), 5000) + } + + #checkAllSurfacesForUpdates() { + // Compile a list of all urls to check, and the surfaces that use them + const allUpdateUrls = new Map() + for (const [surfaceId, handler] of this.#surfaceHandlers) { + if (!handler) continue + const updateUrl = handler.panel.info.firmwareUpdateVersionsUrl + if (!updateUrl) continue + + const currentList = allUpdateUrls.get(updateUrl) + if (currentList) { + currentList.push(handler.surfaceId) + } else { + allUpdateUrls.set(updateUrl, [surfaceId]) + } + } + + // No updates to check + if (allUpdateUrls.size === 0) return + + this.#logger.debug(`Checking for firmware updates from ${allUpdateUrls.size} urls`) + + Promise.resolve() + .then(async () => { + await Promise.allSettled( + Array.from(allUpdateUrls).map(async ([url, surfaceIds]) => { + // Scrape the api for an updated payload + const versionsInfo = await this.#fetchPayloadForUrl(url, true) + + // Perform the update for each surface + await Promise.allSettled( + surfaceIds.map((surfaceId) => { + const handler = this.#surfaceHandlers.get(surfaceId) + if (!handler) return + + return this.#performForSurface(handler, versionsInfo) + }) + ) + }) + ) + + // Inform the ui, even though there may be no changes + this.#updateDevicesList() + }) + .catch((e) => { + this.#logger.warn(`Failed to check for firmware updates: ${e}`) + }) + } + + /** + * Fetch the pauload for a specific url, either from cache or from the server + * @param url The url to fetch the payload from + * @param skipCache Whether to skip the cache and always fetch a new payload + * @returns The payload, or null if it could not be fetched + */ + async #fetchPayloadForUrl(url: string, skipCache?: boolean): Promise | null> { + let cacheEntry = this.#payloadCache.get(url) + + // Check if the cache is too old to be usable + if (cacheEntry && cacheEntry.timestamp < Date.now() - FIRMWARE_PAYLOAD_CACHE_MAX_TTL) { + cacheEntry = undefined + this.#payloadCache.delete(url) + } + + // Check if cache is new enough to return directly + if (!skipCache && cacheEntry && cacheEntry.timestamp < Date.now() - FIRMWARE_PAYLOAD_CACHE_TTL) { + return cacheEntry.payload + } + + // If one is in flight, return that + const currentInFlight = this.#payloadUpdating.get(url) + if (currentInFlight) return currentInFlight + + // @ts-expect-error + const { promise: pendingPromise, resolve } = Promise.withResolvers | null>() + this.#payloadUpdating.set(url, pendingPromise) + + // Fetch new data + fetch(url) + .then((res) => res.json() as Promise>) + .catch((e) => { + this.#logger.warn(`Failed to fetch firmware update payload from "${url}": ${e}`) + return null + }) + .then((newPayload) => { + // Update cache with the new value + if (newPayload) { + this.#payloadCache.set(url, { timestamp: Date.now(), payload: newPayload }) + } + + // No longer in flight + this.#payloadUpdating.delete(url) + + // Return the new value + resolve(newPayload || cacheEntry?.payload || null) + }) + + return pendingPromise + } + + /** + * Trigger a check for updates for a specific surface + * @param surface Surface to check for updates + */ + triggerCheckSurfaceForUpdates(surface: SurfaceHandler): void { + setTimeout(() => { + Promise.resolve() + .then(async () => { + // fetch latest versions info + const versionsInfo = surface.panel.info.firmwareUpdateVersionsUrl + ? await this.#fetchPayloadForUrl(surface.panel.info.firmwareUpdateVersionsUrl) + : null + + const changed = await this.#performForSurface(surface, versionsInfo) + + // Inform ui of the updates + if (changed) this.#updateDevicesList() + }) + + .catch((e) => { + this.#logger.warn(`Failed to check for firmware updates for surface "${surface.surfaceId}": ${e}`) + }) + }, 0) + } + + async #performForSurface(surface: SurfaceHandler, versionsInfo: Record | null): Promise { + // Check if panel has updates + const firmwareUpdatesBefore = surface.panel.info.hasFirmwareUpdates + await surface.panel.checkForFirmwareUpdates?.(versionsInfo ?? undefined) + if (isEqual(firmwareUpdatesBefore, surface.panel.info.hasFirmwareUpdates)) return false + + this.#logger.info(`Firmware updates change for surface "${surface.surfaceId}"`) + + return true + } +} diff --git a/companion/lib/Surface/Types.ts b/companion/lib/Surface/Types.ts index 7685c264ed..abb7659229 100644 --- a/companion/lib/Surface/Types.ts +++ b/companion/lib/Surface/Types.ts @@ -33,6 +33,7 @@ export interface SurfacePanelInfo { type: string configFields: CompanionSurfaceConfigField[] location?: string + firmwareUpdateVersionsUrl?: string hasFirmwareUpdates?: SurfaceFirmwareUpdateInfo } diff --git a/companion/lib/Surface/USB/ElgatoStreamDeck.ts b/companion/lib/Surface/USB/ElgatoStreamDeck.ts index cb6f991aef..3cb288e005 100644 --- a/companion/lib/Surface/USB/ElgatoStreamDeck.ts +++ b/companion/lib/Surface/USB/ElgatoStreamDeck.ts @@ -75,6 +75,7 @@ const LATEST_SDS_FIRMWARE_VERSIONS: Record = { ENCODER_LD_2: '1.01.006', } const SDS_UPDATE_TOOL_URL = 'https://bitfocus.io/?elgato-sds-firmware-updater' +const SDS_UPDATE_VERSIONS_URL = 'https://builds.julusian.dev/builds/sds-test.json' export class SurfaceUSBElgatoStreamDeck extends EventEmitter implements SurfacePanel { readonly #logger: Logger @@ -119,6 +120,10 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter location: undefined, // set later } + if (this.#streamDeck.MODEL === DeviceModelId.STUDIO) { + this.info.firmwareUpdateVersionsUrl = SDS_UPDATE_VERSIONS_URL + } + const allRowValues = this.#streamDeck.CONTROLS.map((control) => control.row) const allColumnValues = this.#streamDeck.CONTROLS.map((button) => button.column) diff --git a/package.json b/package.json index 2898e8a966..b39c68be3c 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "devDependencies": { "@inquirer/prompts": "^7.0.1", + "@types/node": "^22.9.0", "@types/ps-tree": "^1.1.6", "chokidar": "^3.6.0", "concurrently": "^9.0.1", diff --git a/webui/src/Surfaces/KnownSurfacesTable.tsx b/webui/src/Surfaces/KnownSurfacesTable.tsx index f57b378132..10ebe083a7 100644 --- a/webui/src/Surfaces/KnownSurfacesTable.tsx +++ b/webui/src/Surfaces/KnownSurfacesTable.tsx @@ -212,7 +212,7 @@ interface SurfaceRowProps { noBorder: boolean } -function SurfaceRow({ +const SurfaceRow = observer(function SurfaceRow({ surface, index, updateName, @@ -275,4 +275,4 @@ function SurfaceRow({ ) -} +}) diff --git a/yarn.lock b/yarn.lock index 5ca18ba307..7ff8d7cbf2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1297,6 +1297,7 @@ __metadata: resolution: "@companion-app/workspace@workspace:." dependencies: "@inquirer/prompts": "npm:^7.0.1" + "@types/node": "npm:^22.9.0" "@types/ps-tree": "npm:^1.1.6" chokidar: "npm:^3.6.0" concurrently: "npm:^9.0.1" @@ -4465,12 +4466,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=20": - version: 22.5.5 - resolution: "@types/node@npm:22.5.5" +"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=20, @types/node@npm:^22.9.0": + version: 22.9.0 + resolution: "@types/node@npm:22.9.0" dependencies: - undici-types: "npm:~6.19.2" - checksum: 10c0/ead9495cfc6b1da5e7025856dcce2591e9bae635357410c0d2dd619fce797d2a1d402887580ca4b336cb78168b195224869967de370a23f61663cf1e4836121c + undici-types: "npm:~6.19.8" + checksum: 10c0/3f46cbe0a49bab4ba30494025e4c8a6e699b98ac922857aa1f0209ce11a1313ee46e6808b8f13fe5b8b960a9d7796b77c8d542ad4e9810e85ef897d5593b5d51 languageName: node linkType: hard @@ -13712,10 +13713,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"undici-types@npm:~6.19.2": - version: 6.19.6 - resolution: "undici-types@npm:6.19.6" - checksum: 10c0/9b2264c5700e7169c6c62c643aac56cd8984c5fd7e18ed31ff11780260e137f6340dee8317a2e6e0ae3c49f5e5ef6fa577ea07193cbaa535265cba76a267cae9 +"undici-types@npm:~6.19.8": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344 languageName: node linkType: hard From 0ac92acd3be4a7f6fcf606a7e4b42a3b135bd759 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 14 Nov 2024 15:14:50 +0000 Subject: [PATCH 4/9] chore: update lib --- companion/lib/Surface/Controller.ts | 2 +- companion/package.json | 4 +-- yarn.lock | 44 ++++++++++++++--------------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/companion/lib/Surface/Controller.ts b/companion/lib/Surface/Controller.ts index d44707cd2f..0e75765527 100644 --- a/companion/lib/Surface/Controller.ts +++ b/companion/lib/Surface/Controller.ts @@ -19,7 +19,7 @@ import findProcess from 'find-process' import HID from 'node-hid' import jsonPatch from 'fast-json-patch' -import { cloneDeep, isEqual } from 'lodash-es' +import { cloneDeep } from 'lodash-es' import { nanoid } from 'nanoid' import pDebounce from 'p-debounce' import { getStreamDeckDeviceInfo } from '@elgato-stream-deck/node' diff --git a/companion/package.json b/companion/package.json index 8ba31a8599..0555db733a 100644 --- a/companion/package.json +++ b/companion/package.json @@ -48,8 +48,8 @@ "@blackmagic-controller/node": "^0.1.0", "@companion-app/shared": "*", "@companion-module/base": "^1.11.0", - "@elgato-stream-deck/node": "^7.1.0", - "@elgato-stream-deck/tcp": "^7.1.0", + "@elgato-stream-deck/node": "^7.1.1", + "@elgato-stream-deck/tcp": "^7.1.1", "@julusian/bonjour-service": "^1.3.0-2", "@julusian/image-rs": "^1.1.1", "@julusian/jpeg-turbo": "^2.2.0", diff --git a/yarn.lock b/yarn.lock index 7ff8d7cbf2..54afa1bf54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1495,51 +1495,51 @@ __metadata: languageName: node linkType: hard -"@elgato-stream-deck/core@npm:7.1.0": - version: 7.1.0 - resolution: "@elgato-stream-deck/core@npm:7.1.0" +"@elgato-stream-deck/core@npm:7.1.1": + version: 7.1.1 + resolution: "@elgato-stream-deck/core@npm:7.1.1" dependencies: eventemitter3: "npm:^5.0.1" tslib: "npm:^2.7.0" - checksum: 10c0/1c0b78581ef2b6a8f529a14786350ae15d0ef7e9b71f0629f811365d391cee93d3dec4fa82b109c9e196b050878964b8839708fe7ab340d7a9d4097fecf3a9ea + checksum: 10c0/a9b72a336a8af57eb20697b6bbfed27a8f47fbedbfb0d7d5ce7956998707224c2f888c79756433b9522914758376919b61759330037f4fde084663c70f939ef9 languageName: node linkType: hard -"@elgato-stream-deck/node-lib@npm:7.0.2": - version: 7.0.2 - resolution: "@elgato-stream-deck/node-lib@npm:7.0.2" +"@elgato-stream-deck/node-lib@npm:7.1.1": + version: 7.1.1 + resolution: "@elgato-stream-deck/node-lib@npm:7.1.1" dependencies: jpeg-js: "npm:^0.4.4" tslib: "npm:^2.6.3" peerDependencies: "@julusian/jpeg-turbo": ^1.1.2 || ^2.0.0 - checksum: 10c0/ac1ba77a36e6cf9928dba971c4ac9c18c7f82bc9e25d5aad4f2fb55ce624679f2a48282088e8932bedee74aa8954e138d33c694a6dd315e45642faf1f2c93e99 + checksum: 10c0/63df7832d2b673882c506ec46a3e2a05f81b4f0def8e50005ced7869277af53b9b7782d922d8fa15e39fe8e977e066d2ea2a753811517e1cca8c6b6757272bb7 languageName: node linkType: hard -"@elgato-stream-deck/node@npm:^7.1.0": - version: 7.1.0 - resolution: "@elgato-stream-deck/node@npm:7.1.0" +"@elgato-stream-deck/node@npm:^7.1.1": + version: 7.1.1 + resolution: "@elgato-stream-deck/node@npm:7.1.1" dependencies: - "@elgato-stream-deck/core": "npm:7.1.0" - "@elgato-stream-deck/node-lib": "npm:7.0.2" + "@elgato-stream-deck/core": "npm:7.1.1" + "@elgato-stream-deck/node-lib": "npm:7.1.1" eventemitter3: "npm:^5.0.1" node-hid: "npm:^3.1.0" tslib: "npm:^2.7.0" - checksum: 10c0/9863f4281087ada1c392d05db12700c7f3ce0ecf5718794b36864273be9b20ae4cf464363ac78664aab64382848b97d715ff53a752ead977e50beb44ff66818c + checksum: 10c0/b6b67c890a91490c69be731ac093e39633266bce5053afe39fab9146d684c1b4f5377040a3527fd7e5687411862e8fb8eeb6b7aae6c0d3b13c856b6e0a52b9a8 languageName: node linkType: hard -"@elgato-stream-deck/tcp@npm:^7.1.0": - version: 7.1.0 - resolution: "@elgato-stream-deck/tcp@npm:7.1.0" +"@elgato-stream-deck/tcp@npm:^7.1.1": + version: 7.1.1 + resolution: "@elgato-stream-deck/tcp@npm:7.1.1" dependencies: - "@elgato-stream-deck/core": "npm:7.1.0" - "@elgato-stream-deck/node-lib": "npm:7.0.2" + "@elgato-stream-deck/core": "npm:7.1.1" + "@elgato-stream-deck/node-lib": "npm:7.1.1" "@julusian/bonjour-service": "npm:^1.3.0-2" eventemitter3: "npm:^5.0.1" tslib: "npm:^2.6.3" - checksum: 10c0/70488867525e00c409b6e5c0f80f3f3c1190122b87141b519827c55a08b389fbbaedeba1a4922accefd48fd2ffd9ed4245b2cd3562745d6bce71a381d1ad1085 + checksum: 10c0/f7f5e3ae9c5323cfe0147883ebda823844f9e631c0ef0a78f92c85c167f9b8979fbe0037686720d5384aafb3581b3d6e86269633922f644bb87cb840a5b53471 languageName: node linkType: hard @@ -6451,8 +6451,8 @@ asn1@evs-broadcast/node-asn1: "@blackmagic-controller/node": "npm:^0.1.0" "@companion-app/shared": "npm:*" "@companion-module/base": "npm:^1.11.0" - "@elgato-stream-deck/node": "npm:^7.1.0" - "@elgato-stream-deck/tcp": "npm:^7.1.0" + "@elgato-stream-deck/node": "npm:^7.1.1" + "@elgato-stream-deck/tcp": "npm:^7.1.1" "@julusian/bonjour-service": "npm:^1.3.0-2" "@julusian/image-rs": "npm:^1.1.1" "@julusian/jpeg-turbo": "npm:^2.2.0" From a914b8824238aac2f373072387e1f38309142b2b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 14 Nov 2024 15:15:43 +0000 Subject: [PATCH 5/9] fix --- companion/lib/Surface/USB/ElgatoStreamDeck.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/companion/lib/Surface/USB/ElgatoStreamDeck.ts b/companion/lib/Surface/USB/ElgatoStreamDeck.ts index 3cb288e005..5b23003145 100644 --- a/companion/lib/Surface/USB/ElgatoStreamDeck.ts +++ b/companion/lib/Surface/USB/ElgatoStreamDeck.ts @@ -69,10 +69,8 @@ function getConfigFields(streamDeck: StreamDeck): CompanionSurfaceConfigField[] */ const LATEST_SDS_FIRMWARE_VERSIONS: Record = { AP2: '1.05.009', - ENCODER_AP2_1: '1.01.012', - ENCODER_AP2_2: '1.01.012', - ENCODER_LD_1: '1.01.006', - ENCODER_LD_2: '1.01.006', + ENCODER_AP2: '1.01.012', + ENCODER_LD: '1.01.006', } const SDS_UPDATE_TOOL_URL = 'https://bitfocus.io/?elgato-sds-firmware-updater' const SDS_UPDATE_VERSIONS_URL = 'https://builds.julusian.dev/builds/sds-test.json' From abf81def2aae0d636ed85a678d98e24eda5b1960 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Sun, 17 Nov 2024 16:01:57 +0000 Subject: [PATCH 6/9] Update companion/lib/Surface/FirmwareUpdateCheck.ts Co-authored-by: Peter Newman --- companion/lib/Surface/FirmwareUpdateCheck.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/companion/lib/Surface/FirmwareUpdateCheck.ts b/companion/lib/Surface/FirmwareUpdateCheck.ts index bc56fd9e50..0eaf5c4a50 100644 --- a/companion/lib/Surface/FirmwareUpdateCheck.ts +++ b/companion/lib/Surface/FirmwareUpdateCheck.ts @@ -82,7 +82,7 @@ export class SurfaceFirmwareUpdateCheck { } /** - * Fetch the pauload for a specific url, either from cache or from the server + * Fetch the payload for a specific url, either from cache or from the server * @param url The url to fetch the payload from * @param skipCache Whether to skip the cache and always fetch a new payload * @returns The payload, or null if it could not be fetched From f94afea4f09f326fc49c655dfea3ee7f53d27c0f Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 9 Jan 2025 10:20:12 +0000 Subject: [PATCH 7/9] chore: latest types --- package.json | 2 +- yarn.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 4e5f0b1a66..dd2c34dce6 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "devDependencies": { "@inquirer/prompts": "^7.2.0", - "@types/node": "^22.9.0", + "@types/node": "^22.10.5", "@types/ps-tree": "^1.1.6", "chokidar": "^3.6.0", "concurrently": "^9.1.0", diff --git a/yarn.lock b/yarn.lock index 3473651671..fbf7ab1aa7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1303,7 +1303,7 @@ __metadata: resolution: "@companion-app/workspace@workspace:." dependencies: "@inquirer/prompts": "npm:^7.2.0" - "@types/node": "npm:^22.9.0" + "@types/node": "npm:^22.10.5" "@types/ps-tree": "npm:^1.1.6" chokidar: "npm:^3.6.0" concurrently: "npm:^9.1.0" @@ -4847,12 +4847,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=20, @types/node@npm:^22.9.0": - version: 22.9.0 - resolution: "@types/node@npm:22.9.0" +"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:>=20, @types/node@npm:^22.10.5": + version: 22.10.5 + resolution: "@types/node@npm:22.10.5" dependencies: - undici-types: "npm:~6.19.8" - checksum: 10c0/3f46cbe0a49bab4ba30494025e4c8a6e699b98ac922857aa1f0209ce11a1313ee46e6808b8f13fe5b8b960a9d7796b77c8d542ad4e9810e85ef897d5593b5d51 + undici-types: "npm:~6.20.0" + checksum: 10c0/6a0e7d1fe6a86ef6ee19c3c6af4c15542e61aea2f4cee655b6252efb356795f1f228bc8299921e82924e80ff8eca29b74d9dd0dd5cc1a90983f892f740b480df languageName: node linkType: hard @@ -14703,10 +14703,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"undici-types@npm:~6.19.8": - version: 6.19.8 - resolution: "undici-types@npm:6.19.8" - checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344 +"undici-types@npm:~6.20.0": + version: 6.20.0 + resolution: "undici-types@npm:6.20.0" + checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf languageName: node linkType: hard From 682482816659ca1f5eb3c14e91c361ffd7980d15 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 9 Jan 2025 10:42:14 +0000 Subject: [PATCH 8/9] feat: make logic a little more generic, in case we want to enable the check for more streamdeck models --- companion/lib/Surface/FirmwareUpdateCheck.ts | 12 ++--- companion/lib/Surface/Types.ts | 2 +- companion/lib/Surface/USB/ElgatoStreamDeck.ts | 47 +++++++++++++------ 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/companion/lib/Surface/FirmwareUpdateCheck.ts b/companion/lib/Surface/FirmwareUpdateCheck.ts index 0eaf5c4a50..5f185f8cd1 100644 --- a/companion/lib/Surface/FirmwareUpdateCheck.ts +++ b/companion/lib/Surface/FirmwareUpdateCheck.ts @@ -8,7 +8,7 @@ const FIRMWARE_PAYLOAD_CACHE_MAX_TTL = 1000 * 60 * 60 * 24 // 24 hours interface PayloadCacheEntry { timestamp: number - payload: Record + payload: unknown } export class SurfaceFirmwareUpdateCheck { @@ -16,7 +16,7 @@ export class SurfaceFirmwareUpdateCheck { readonly #payloadCache = new Map() - readonly #payloadUpdating = new Map | null>>() + readonly #payloadUpdating = new Map>() /** * All the opened and active surfaces @@ -87,7 +87,7 @@ export class SurfaceFirmwareUpdateCheck { * @param skipCache Whether to skip the cache and always fetch a new payload * @returns The payload, or null if it could not be fetched */ - async #fetchPayloadForUrl(url: string, skipCache?: boolean): Promise | null> { + async #fetchPayloadForUrl(url: string, skipCache?: boolean): Promise { let cacheEntry = this.#payloadCache.get(url) // Check if the cache is too old to be usable @@ -106,12 +106,12 @@ export class SurfaceFirmwareUpdateCheck { if (currentInFlight) return currentInFlight // @ts-expect-error - const { promise: pendingPromise, resolve } = Promise.withResolvers | null>() + const { promise: pendingPromise, resolve } = Promise.withResolvers() this.#payloadUpdating.set(url, pendingPromise) // Fetch new data fetch(url) - .then((res) => res.json() as Promise>) + .then((res) => res.json() as Promise) .catch((e) => { this.#logger.warn(`Failed to fetch firmware update payload from "${url}": ${e}`) return null @@ -157,7 +157,7 @@ export class SurfaceFirmwareUpdateCheck { }, 0) } - async #performForSurface(surface: SurfaceHandler, versionsInfo: Record | null): Promise { + async #performForSurface(surface: SurfaceHandler, versionsInfo: unknown | null): Promise { // Check if panel has updates const firmwareUpdatesBefore = surface.panel.info.hasFirmwareUpdates await surface.panel.checkForFirmwareUpdates?.(versionsInfo ?? undefined) diff --git a/companion/lib/Surface/Types.ts b/companion/lib/Surface/Types.ts index abb7659229..8b00634c13 100644 --- a/companion/lib/Surface/Types.ts +++ b/companion/lib/Surface/Types.ts @@ -48,7 +48,7 @@ export interface SurfacePanel extends EventEmitter { getDefaultConfig?: () => any onVariablesChanged?: (allChangedVariables: Set) => void quit(): void - checkForFirmwareUpdates?: (latestVersions?: Record) => Promise + checkForFirmwareUpdates?: (latestVersions?: unknown) => Promise } export interface DrawButtonItem { x: number diff --git a/companion/lib/Surface/USB/ElgatoStreamDeck.ts b/companion/lib/Surface/USB/ElgatoStreamDeck.ts index 5b23003145..56869303e4 100644 --- a/companion/lib/Surface/USB/ElgatoStreamDeck.ts +++ b/companion/lib/Surface/USB/ElgatoStreamDeck.ts @@ -64,16 +64,29 @@ function getConfigFields(streamDeck: StreamDeck): CompanionSurfaceConfigField[] return fields } +interface FirmwareVersionInfo { + productIds: number[] + versions: Record +} + /** * The latest firmware versions for the SDS at the time this was last updated */ -const LATEST_SDS_FIRMWARE_VERSIONS: Record = { - AP2: '1.05.009', - ENCODER_AP2: '1.01.012', - ENCODER_LD: '1.01.006', -} -const SDS_UPDATE_TOOL_URL = 'https://bitfocus.io/?elgato-sds-firmware-updater' -const SDS_UPDATE_VERSIONS_URL = 'https://builds.julusian.dev/builds/sds-test.json' +const LATEST_FIRMWARE_VERSIONS: FirmwareVersionInfo[] = [ + { + // Studio + productIds: [0x00aa], + versions: { + AP2: '1.05.009', + ENCODER_AP2: '1.01.012', + ENCODER_LD: '1.01.006', + }, + }, +] +const STREAMDECK_MODULES_SUPPORTING_UPDATES: ReadonlySet = new Set([DeviceModelId.STUDIO]) +const STREAMDECK_UPDATE_TOOL_URL = 'https://bitfocus.io/?elgato-sds-firmware-updater' +const STREAMDECK_UPDATE_VERSIONS_URL = + 'https://gist.githubusercontent.com/Julusian/130d6e7f8142be5196ce4b5da9021954/raw/761859f823a74e2f52e0395ceb172a97cb43db9a/sds-update-test.json' export class SurfaceUSBElgatoStreamDeck extends EventEmitter implements SurfacePanel { readonly #logger: Logger @@ -118,8 +131,8 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter location: undefined, // set later } - if (this.#streamDeck.MODEL === DeviceModelId.STUDIO) { - this.info.firmwareUpdateVersionsUrl = SDS_UPDATE_VERSIONS_URL + if (STREAMDECK_MODULES_SUPPORTING_UPDATES.has(this.#streamDeck.MODEL)) { + this.info.firmwareUpdateVersionsUrl = STREAMDECK_UPDATE_VERSIONS_URL } const allRowValues = this.#streamDeck.CONTROLS.map((control) => control.row) @@ -352,13 +365,17 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter return self } - async checkForFirmwareUpdates(latestVersions?: Record): Promise { + async checkForFirmwareUpdates(latestVersions0?: unknown): Promise { + let latestVersions: FirmwareVersionInfo[] | undefined = latestVersions0 as FirmwareVersionInfo[] // If no versions are provided, use the latest known versions for the SDS - if (!latestVersions && this.#streamDeck.MODEL === DeviceModelId.STUDIO) - latestVersions = LATEST_SDS_FIRMWARE_VERSIONS + if (!latestVersions) latestVersions = LATEST_FIRMWARE_VERSIONS + + // This should probably be cached, but it is cheap to check + const deviceInfo = await this.#streamDeck.getHidDeviceInfo() + const latestVersionsForDevice = latestVersions.find((info) => info.productIds.includes(deviceInfo.productId)) // If no versions are provided, we can't know that there are updates - if (!latestVersions) { + if (!latestVersionsForDevice) { this.info.hasFirmwareUpdates = undefined return } @@ -367,7 +384,7 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter const currentVersions = await this.#streamDeck.getAllFirmwareVersions() - for (const [key, targetVersion] of Object.entries(latestVersions)) { + for (const [key, targetVersion] of Object.entries(latestVersionsForDevice.versions)) { const currentVersion = parseVersion(currentVersions[key]) const latestVersion = parseVersion(targetVersion) @@ -380,7 +397,7 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter if (hasUpdate) { this.info.hasFirmwareUpdates = { - updaterDownloadUrl: SDS_UPDATE_TOOL_URL, + updaterDownloadUrl: STREAMDECK_UPDATE_TOOL_URL, } } else { this.info.hasFirmwareUpdates = undefined From 906029f79d463dfb6945b279176101dfbf26eb14 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Thu, 16 Jan 2025 17:56:44 +0000 Subject: [PATCH 9/9] fix --- companion/lib/Surface/FirmwareUpdateCheck.ts | 8 +++--- companion/lib/Surface/USB/ElgatoStreamDeck.ts | 26 +++++++++---------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/companion/lib/Surface/FirmwareUpdateCheck.ts b/companion/lib/Surface/FirmwareUpdateCheck.ts index 5f185f8cd1..0af353514e 100644 --- a/companion/lib/Surface/FirmwareUpdateCheck.ts +++ b/companion/lib/Surface/FirmwareUpdateCheck.ts @@ -43,7 +43,7 @@ export class SurfaceFirmwareUpdateCheck { const currentList = allUpdateUrls.get(updateUrl) if (currentList) { - currentList.push(handler.surfaceId) + currentList.push(surfaceId) } else { allUpdateUrls.set(updateUrl, [surfaceId]) } @@ -67,7 +67,9 @@ export class SurfaceFirmwareUpdateCheck { const handler = this.#surfaceHandlers.get(surfaceId) if (!handler) return - return this.#performForSurface(handler, versionsInfo) + return this.#performForSurface(handler, versionsInfo).catch((e) => { + this.#logger.error(`Failed to check for firmware updates for surface "${surfaceId}": ${e}`) + }) }) ) }) @@ -97,7 +99,7 @@ export class SurfaceFirmwareUpdateCheck { } // Check if cache is new enough to return directly - if (!skipCache && cacheEntry && cacheEntry.timestamp < Date.now() - FIRMWARE_PAYLOAD_CACHE_TTL) { + if (!skipCache && cacheEntry && cacheEntry.timestamp >= Date.now() - FIRMWARE_PAYLOAD_CACHE_TTL) { return cacheEntry.payload } diff --git a/companion/lib/Surface/USB/ElgatoStreamDeck.ts b/companion/lib/Surface/USB/ElgatoStreamDeck.ts index 56869303e4..0731573518 100644 --- a/companion/lib/Surface/USB/ElgatoStreamDeck.ts +++ b/companion/lib/Surface/USB/ElgatoStreamDeck.ts @@ -73,20 +73,20 @@ interface FirmwareVersionInfo { * The latest firmware versions for the SDS at the time this was last updated */ const LATEST_FIRMWARE_VERSIONS: FirmwareVersionInfo[] = [ - { - // Studio - productIds: [0x00aa], - versions: { - AP2: '1.05.009', - ENCODER_AP2: '1.01.012', - ENCODER_LD: '1.01.006', - }, - }, + // Tool is not ready, so there are no versions to compare + // { + // // Studio + // productIds: [0x00aa], + // versions: { + // AP2: '1.05.009', + // ENCODER_AP2: '1.01.012', + // ENCODER_LD: '1.01.006', + // }, + // }, ] const STREAMDECK_MODULES_SUPPORTING_UPDATES: ReadonlySet = new Set([DeviceModelId.STUDIO]) -const STREAMDECK_UPDATE_TOOL_URL = 'https://bitfocus.io/?elgato-sds-firmware-updater' -const STREAMDECK_UPDATE_VERSIONS_URL = - 'https://gist.githubusercontent.com/Julusian/130d6e7f8142be5196ce4b5da9021954/raw/761859f823a74e2f52e0395ceb172a97cb43db9a/sds-update-test.json' +const STREAMDECK_UPDATE_DOWNLOAD_URL = 'http://api.bitfocus.io/v1/product/elgato-updater/download' +const STREAMDECK_UPDATE_VERSIONS_URL = 'http://api.bitfocus.io/v1/product/elgato-updater/versions' export class SurfaceUSBElgatoStreamDeck extends EventEmitter implements SurfacePanel { readonly #logger: Logger @@ -397,7 +397,7 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter if (hasUpdate) { this.info.hasFirmwareUpdates = { - updaterDownloadUrl: STREAMDECK_UPDATE_TOOL_URL, + updaterDownloadUrl: STREAMDECK_UPDATE_DOWNLOAD_URL, } } else { this.info.hasFirmwareUpdates = undefined