Skip to content

Commit

Permalink
feat: check for and indicate sds firmware updates against hardcoded v…
Browse files Browse the repository at this point in the history
…ersions list
  • Loading branch information
Julusian committed Nov 14, 2024
1 parent 4dc4d51 commit 9912001
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 5 deletions.
22 changes: 21 additions & 1 deletion companion/lib/Surface/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -341,6 +341,24 @@ export class SurfaceController extends EventEmitter<SurfaceControllerEvents> {

// 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)
}

/**
Expand Down Expand Up @@ -682,6 +700,7 @@ export class SurfaceController extends EventEmitter<SurfaceControllerEvents> {
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 },
Expand All @@ -693,6 +712,7 @@ export class SurfaceController extends EventEmitter<SurfaceControllerEvents> {

surfaceInfo.location = location || null
surfaceInfo.configFields = surfaceHandler.panel.info.configFields || []
surfaceInfo.hasFirmwareUpdates = surfaceHandler.panel.info.hasFirmwareUpdates || null
}

return surfaceInfo
Expand Down
9 changes: 8 additions & 1 deletion companion/lib/Surface/Types.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -29,7 +33,9 @@ export interface SurfacePanelInfo {
type: string
configFields: CompanionSurfaceConfigField[]
location?: string
hasFirmwareUpdates?: SurfaceFirmwareUpdateInfo
}

export interface SurfacePanel extends EventEmitter<SurfacePanelEvents> {
readonly info: SurfacePanelInfo
readonly gridSize: GridSize
Expand All @@ -41,6 +47,7 @@ export interface SurfacePanel extends EventEmitter<SurfacePanelEvents> {
getDefaultConfig?: () => any
onVariablesChanged?: (allChangedVariables: Set<string>) => void
quit(): void
checkForFirmwareUpdates?: (latestVersions?: Record<string, string>) => Promise<void>
}
export interface DrawButtonItem {
x: number
Expand Down
57 changes: 57 additions & 0 deletions companion/lib/Surface/USB/ElgatoStreamDeck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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<string, string> = {
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<SurfacePanelEvents> implements SurfacePanel {
readonly #logger: Logger

Expand Down Expand Up @@ -336,6 +349,41 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter<SurfacePanelEvents>
return self
}

async checkForFirmwareUpdates(latestVersions?: Record<string, string>): Promise<void> {
// 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
Expand Down Expand Up @@ -381,3 +429,12 @@ export class SurfaceUSBElgatoStreamDeck extends EventEmitter<SurfacePanelEvents>
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])}`)
}
6 changes: 6 additions & 0 deletions shared-lib/lib/Model/Surfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface RowsAndColumns {
columns: number
}

export interface SurfaceFirmwareUpdateInfo {
updaterDownloadUrl: string
}

export interface ClientSurfaceItem {
id: string
type: string
Expand All @@ -26,6 +30,8 @@ export interface ClientSurfaceItem {
displayName: string
location: string | null

hasFirmwareUpdates: SurfaceFirmwareUpdateInfo | null

size: RowsAndColumns | null
offset: RowsAndColumns | null
}
Expand Down
3 changes: 2 additions & 1 deletion webui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -456,7 +457,7 @@ const AppContent = observer(function AppContent({ buttonGridHotPress }: AppConte
</CNavItem>
<CNavItem>
<CNavLink to={SURFACES_PAGE_PREFIX} as={NavLink}>
<FontAwesomeIcon icon={faGamepad} /> Surfaces
<FontAwesomeIcon icon={faGamepad} /> Surfaces <SurfacesTabNotifyIcon />
</CNavLink>
</CNavItem>
<CNavItem>
Expand Down
14 changes: 14 additions & 0 deletions webui/src/Stores/SurfacesStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
15 changes: 13 additions & 2 deletions webui/src/Surfaces/KnownSurfacesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)
Expand Down Expand Up @@ -236,7 +237,17 @@ function SurfaceRow({
<td>
<TextInputField value={surface.name} setValue={updateName2} />
</td>
<td>{surface.type}</td>
<td>
{surface.type}
{!!surface.hasFirmwareUpdates && (
<>
{' '}
<WindowLinkOpen href={surface.hasFirmwareUpdates.updaterDownloadUrl}>
<FontAwesomeIcon icon={faCircleUp} title="Firmware update is available" />
</WindowLinkOpen>
</>
)}
</td>
<td>{surface.isConnected ? surface.location || 'Local' : 'Offline'}</td>
<td className="text-right">
{surface.isConnected ? (
Expand Down
16 changes: 16 additions & 0 deletions webui/src/Surfaces/TabNotifyIcon.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<span className="notification-count" title={`${updateCount} surfaces have firmware updates available`}>
{updateCount}
</span>
)
})
16 changes: 16 additions & 0 deletions webui/src/scss/_common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit 9912001

Please sign in to comment.