Last active
April 15, 2024 17:22
-
-
Save ZanzyTHEbar/56fa89e36a0fa124a7efefcfe32d4209 to your computer and use it in GitHub Desktop.
tauri_local_http_plugin.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { removeFile, readTextFile, BaseDirectory, writeTextFile } from '@tauri-apps/api/fs' | |
import { getClient, ResponseType } from '@tauri-apps/api/http' | |
import { appConfigDir, join } from '@tauri-apps/api/path' | |
import { invoke, convertFileSrc } from '@tauri-apps/api/tauri' | |
import { createContext, useContext, createMemo, Accessor, type ParentComponent } from 'solid-js' | |
import { createStore, produce } from 'solid-js/store' | |
import { debug, error, trace, warn } from 'tauri-plugin-log-api' | |
import { download, upload } from 'tauri-plugin-upload-api' | |
import { useAppDeviceContext } from './device' | |
import { useAppNotificationsContext } from './notifications' | |
import { isEmpty } from '@src/lib/utils' | |
import { ENotificationType, RESTStatus, RESTType, ESPEndpoints } from '@static/enums' | |
import { | |
AppStoreAPI, | |
IEndpoint, | |
IEndpointKey, | |
IGHAsset, | |
IGHRelease, | |
IPOSTCommand, | |
} from '@static/types' | |
import { makeRequest } from 'tauri-plugin-request-client' | |
interface AppAPIContext { | |
//********************************* gh rest *************************************/ | |
getGHRestStatus: Accessor<RESTStatus> | |
getFirmware: Accessor<{ | |
assets: IGHAsset[] | |
type: string | |
version: string | |
}> | |
getGHEndpoint: Accessor<string> | |
setGHRestStatus: (status: RESTStatus) => void | |
setFirmware: (assets?: IGHAsset[], version?: string, type?: string) => void | |
//********************************* endpoints *************************************/ | |
getEndpoints: Accessor<Map<IEndpointKey, IEndpoint>> | |
getEndpoint: (key: IEndpointKey) => IEndpoint | undefined | |
//********************************* hooks *************************************/ | |
downloadAsset: (firmware: string) => Promise<void> | |
doGHRequest: () => Promise<void> | |
useRequestHook: ( | |
endpointName: IEndpointKey, | |
deviceID?: string, | |
body?: IPOSTCommand, | |
args?: string, | |
) => Promise<void> | |
useOTA: (firmwareName: string, device: string) => Promise<void> | |
} | |
const AppAPIContext = createContext<AppAPIContext>() | |
export const AppAPIProvider: ParentComponent = (props) => { | |
const { addNotification } = useAppNotificationsContext() | |
const { deviceState, setDeviceRestResponse, setDeviceRestStatus } = useAppDeviceContext() | |
// TODO: change to firmware release repo | |
const ghEndpoint = 'https://api.github.com/repos/Lumin/OpenIris/releases/latest' | |
// TODO: Use backend api schema to generate endpoints map and use that instead of hardcoding the endpoints | |
const endpointsMap: Map<IEndpointKey, IEndpoint> = new Map<IEndpointKey, IEndpoint>([ | |
//* ESP Specific Endpoints */ | |
['ota', { url: `:81${ESPEndpoints.OTA}`, type: RESTType.POST }], | |
['ping', { url: `:81${ESPEndpoints.PING}`, type: RESTType.GET }], | |
['save', { url: `:81${ESPEndpoints.SAVE}`, type: RESTType.GET }], | |
['wifi', { url: `:81${ESPEndpoints.WIFI}`, type: RESTType.POST }], | |
['setDevice', { url: `:81${ESPEndpoints.SET_DEVICE}`, type: RESTType.POST }], | |
['setTxPower', { url: `:81${ESPEndpoints.SET_TX_POWER}`, type: RESTType.POST }], | |
['resetConfig', { url: `:81${ESPEndpoints.RESET_CONFIG}`, type: RESTType.GET }], | |
['rebootDevice', { url: `:81${ESPEndpoints.REBOOT_DEVICE}`, type: RESTType.GET }], | |
['wifiStrength', { url: `:81${ESPEndpoints.WIFI_STRENGTH}`, type: RESTType.POST }], | |
['restartCamera', { url: `:81${ESPEndpoints.RESTART_CAMERA}`, type: RESTType.GET }], | |
['getStoredConfig', { url: `:81${ESPEndpoints.GET_STORED_CONFIG}`, type: RESTType.GET }], | |
['jsonHandler', { url: `:81${ESPEndpoints.JSON_HANDLER}`, type: RESTType.POST }], | |
]) | |
const defaultState: AppStoreAPI = { | |
ghAPI: { | |
status: RESTStatus.COMPLETE, | |
firmware: { | |
assets: [], | |
version: '', | |
type: '', | |
}, | |
}, | |
} | |
const [state, setState] = createStore<AppStoreAPI>(defaultState) | |
const apiState = createMemo(() => state) | |
/********************************* gh rest *************************************/ | |
//#region gh rest | |
const setGHRestStatus = (status: RESTStatus) => { | |
setState( | |
produce((s) => { | |
s.ghAPI.status = status | |
}), | |
) | |
} | |
const setFirmware = (assets?: IGHAsset[], version?: string, type?: string) => { | |
setState( | |
produce((s) => { | |
s.ghAPI.firmware = { | |
assets: assets ? [...assets] : s.ghAPI.firmware.assets, | |
version: version ? version : s.ghAPI.firmware.version, | |
type: type ? type : s.ghAPI.firmware.type, | |
} | |
}), | |
) | |
} | |
const getGHRestStatus = createMemo(() => apiState().ghAPI.status) | |
const getFirmware = createMemo(() => apiState().ghAPI.firmware) | |
const getGHEndpoint = createMemo(() => ghEndpoint) | |
//#endregion | |
/********************************* rest *************************************/ | |
//#region rest | |
const getEndpoints = createMemo(() => endpointsMap) | |
const getEndpoint = (key: IEndpointKey) => endpointsMap.get(key) | |
//#endregion | |
//#region hooks | |
const getRelease = async (firmware: string) => { | |
const appConfigDirPath = await appConfigDir() | |
if (isEmpty(firmware)) { | |
addNotification({ | |
title: 'Please Select a Firmware', | |
message: 'A firmware must be selected before downloading', | |
type: ENotificationType.WARNING, | |
}) | |
debug('[Github Release]: No firmware selected') | |
return | |
} | |
debug(`[Github Release]: App Config Dir: ${appConfigDirPath}`) | |
// check if the firmware chosen matches the one names in the firmwareAssets array of objects | |
const firmwareAsset = getFirmware().assets.find((asset) => asset.name === firmware) | |
debug(`[Github Release]: Firmware Asset: ${firmwareAsset}`) | |
if (!firmwareAsset) return | |
debug(`[Github Release]: Downloading firmware: ${firmware}`) | |
debug(`[Github Release]: Firmware URL: ${firmwareAsset}`) | |
// parse out the file name from the firmwareAsset.url and append it to the appConfigDirPath | |
const fileName = | |
firmwareAsset.browser_download_url.split('/')[ | |
firmwareAsset.browser_download_url.split('/').length - 1 | |
] | |
//debug('[Github Release]: File Name: ', fileName) | |
// ${appConfigDirPath}${fileName} | |
const path = await join(appConfigDirPath, fileName) | |
debug(`[Github Release]: Path: ${path}`) | |
// get the latest release | |
const response = await download( | |
firmwareAsset.browser_download_url, | |
path, | |
(progress, total) => { | |
debug(`[Github Release]: Downloaded ${progress} of ${total} bytes`) | |
}, | |
) | |
debug(`[Github Release]: Download Response: ${response}`) | |
addNotification({ | |
title: 'Lumin Firmware Downloaded', | |
message: `Downloaded Firmware ${firmware}`, | |
type: ENotificationType.INFO, | |
}) | |
const res = await invoke('unzip_archive', { | |
archivePath: path, | |
targetDir: appConfigDirPath, | |
}) | |
await removeFile(path) | |
debug(`[Github Release]: Unzip Response: ${res}`) | |
const manifest = await readTextFile('manifest.json', { dir: BaseDirectory.AppConfig }) | |
const config_json = JSON.parse(manifest) | |
if (isEmpty(manifest)) { | |
error('[Manifest Error]: Manifest does not exist') | |
addNotification({ | |
title: 'Error', | |
message: 'Firmware Manifest does not exist', | |
type: ENotificationType.ERROR, | |
}) | |
return | |
} | |
// modify the version property | |
config_json['version'] = getFirmware().version | |
// loop through the builds array and the parts array and update the path property | |
for (let i = 0; i < config_json['builds'].length; i++) { | |
for (let j = 0; j < config_json['builds'][i]['parts'].length; j++) { | |
const firmwarePath = await join( | |
appConfigDirPath, | |
config_json['builds'][i]['parts'][j]['path'], | |
) | |
debug(`[Github Release]: Firmware Path: ${firmwarePath}`) | |
const firmwareSrc = convertFileSrc(firmwarePath) | |
debug(`[Github Release]: Firmware Src: ${firmwareSrc}`) | |
config_json['builds'][i]['parts'][j]['path'] = firmwareSrc | |
} | |
} | |
// write the config file | |
writeTextFile('manifest.json', JSON.stringify(config_json), { | |
dir: BaseDirectory.AppConfig, | |
}) | |
.then(() => { | |
debug('[Manifest Updated]: Manifest Updated Successfully') | |
}) | |
.finally(() => { | |
debug('[Manifest Updated]: Finished') | |
}) | |
.catch((err) => { | |
error(`[Manifest Update Error]: ${err}`) | |
}) | |
debug('[Github Release]: Manifest: ', config_json) | |
} | |
/** | |
* @description A hook that will return the data from the github release endpoint and a function that will download the asset from the github release endpoint | |
* @returns {Promise<void>} data - The data returned from the github release endpoint | |
* @returns {function} downloadAsset - The function that will download the asset from the github release endpoint | |
*/ | |
const downloadAsset = async (firmware: string): Promise<void> => { | |
const response = await getRelease(firmware) | |
debug(`[Github Release]: Download Response: ${response}`) | |
} | |
// TODO: Implement a way to read if the merged-firmware.bin file and manifest.json file exists in the app config directory and if it does, then use that instead of downloading the firmware from github | |
// Note: If both files are present, we should early return and set a UI store value that will be used to display a message to the user that they can use the firmware that is already downloaded and show an optional button to erase the firmware | |
//TODO: Add notifications to all the debug statements | |
const setGHData = (data: IGHRelease, update: boolean) => { | |
if (data['name'] === undefined) { | |
setFirmware(undefined, data['version'], undefined) | |
} else { | |
setFirmware(undefined, data['name'], undefined) | |
} | |
debug(JSON.stringify(data)) | |
const assets: Array<{ | |
browser_download_url: string | |
name: string | |
}> = data['assets'] | |
const download_urls = assets.map( | |
(asset: { browser_download_url: string }) => asset.browser_download_url, | |
) | |
const firmware_assets = assets.map((asset: { name: string }) => asset.name) | |
// split the firmware_assets array of strings on the first dash and return the first element of the array | |
const boardName = firmware_assets.map((asset: string) => asset.split('-')[0]) | |
// set the board name in the store | |
const assetsBuffer: IGHAsset[] = [] | |
for (let i = 0; i < boardName.length; i++) { | |
debug(`[Github Release]: Board Name: ', ${boardName[i]}`) | |
debug(`[Github Release]: URLs:, ${download_urls[i]}`) | |
assetsBuffer.push({ name: boardName[i], browser_download_url: download_urls[i] }) | |
} | |
setFirmware(assetsBuffer) | |
if (update) { | |
writeTextFile( | |
'config.json', | |
JSON.stringify({ | |
version: getFirmware().version, | |
assets: getFirmware().assets, | |
}), | |
{ | |
dir: BaseDirectory.AppConfig, | |
}, | |
) | |
.then(() => { | |
debug( | |
update | |
? '[Config Updated]: Config Updated Successfully' | |
: '[Config Created]: Config Created Successfully', | |
) | |
}) | |
.catch((err) => { | |
error('[Config Creation Error]:', err) | |
}) | |
} | |
} | |
/** | |
* @description Invoke the do_gh_request command | |
* @function doGHRequest | |
* @async | |
* @export | |
* @note This function will call the github repo REST API release endpoint and update/create a config.json file with the latest release data | |
* @note This function will write the file to the app config directory C:\Users\<User>\AppData\Roaming\com.Lumin.dev\config.json | |
* @note Should be called on app start | |
* @example | |
* import { doGHRequest } from './github' | |
* doGHRequest() | |
* .then(() => debug('Request sent')) | |
* .catch((err) => error(err)) | |
*/ | |
const doGHRequest = async () => { | |
try { | |
const client = await getClient() | |
setGHRestStatus(RESTStatus.ACTIVE) | |
setGHRestStatus(RESTStatus.LOADING) | |
debug(`[Github Release]: Github Endpoint ${getGHEndpoint()}`) | |
try { | |
const response = await client.get<IGHRelease>(getGHEndpoint(), { | |
timeout: 30, | |
// the expected response type | |
headers: { | |
'User-Agent': 'Lumin', | |
}, | |
responseType: ResponseType.JSON, | |
}) | |
trace(`[Github Response]: ${JSON.stringify(response)}`) | |
if (!response.ok) { | |
debug('[Github Release Error]: Cannot Access Github API Endpoint') | |
return | |
} | |
debug(`[OpenIris Version]: ${response.data['name']}`) | |
try { | |
const config = await readTextFile('config.json', { | |
dir: BaseDirectory.AppConfig, | |
}) | |
const config_json = JSON.parse(config) | |
trace(`[Config]: ${JSON.stringify(config_json)}`) | |
if ((!response.ok || !(response instanceof Object)) && config === '') { | |
warn('[Config Exists]: Most likely rate limited') | |
setGHData(config_json, false) | |
setGHRestStatus(RESTStatus.COMPLETE) | |
return | |
} | |
if (response.data['name'] === config_json.version) { | |
debug('[Config Exists]: Config Exists and is up to date') | |
setGHData(response.data, false) | |
return | |
} | |
// update config | |
setGHData(response.data, true) | |
debug('[Config Exists]: Config Exists and is out of date - Updating') | |
setGHRestStatus(RESTStatus.COMPLETE) | |
return | |
} catch (err) { | |
setGHRestStatus(RESTStatus.NO_CONFIG) | |
if (response.ok) { | |
error(`[Config Read Error]: ${err} Creating config.json`) | |
setGHData(response.data, true) | |
setGHRestStatus(RESTStatus.COMPLETE) | |
} | |
} | |
} catch (err) { | |
setGHRestStatus(RESTStatus.FAILED) | |
error(`[Github Release Error]: ${err}`) | |
const config = await readTextFile('config.json', { | |
dir: BaseDirectory.AppConfig, | |
}) | |
if (!config) { | |
setGHRestStatus(RESTStatus.NO_CONFIG) | |
error(`[Config Read Error]: Config does not exist ${err}`) | |
} | |
const config_json = JSON.parse(config) | |
debug(`[OpenIris Version]: ${config_json.version}`) | |
trace(`[Config.JSON Contents]:${config_json}`) | |
if (config !== '') { | |
debug('[Config Exists]: Config Exists and is up to date') | |
setGHData(config_json, false) | |
return | |
} | |
setGHRestStatus(RESTStatus.NO_CONFIG) | |
// check if the error is a rate limit error | |
/* if (err instanceof Object) { | |
if (err.response instanceof Object) { | |
if (err.response.status === 403) { | |
// rate limit error | |
// check if the rate limit reset time is in the future | |
// if it is, set the rate limit reset time | |
// if it isn't, set the rate limit reset time to 0 | |
const rate_limit_reset = err.response.headers['x-ratelimit-reset'] | |
const rate_limit_reset_time = new Date(rate_limit_reset * 1000) | |
const now = new Date() | |
if (rate_limit_reset_time > now) { | |
setRateLimitReset(rate_limit_reset_time) | |
return | |
} | |
setRateLimitReset(new Date(0)) | |
} | |
} | |
} */ | |
} | |
} catch (err) { | |
setGHRestStatus(RESTStatus.FAILED) | |
error(`[Tauri Runtime Error - http client]: ${err}`) | |
return | |
} | |
} | |
type __Result__<T, E> = { status: 'ok'; data: T } | { status: 'error'; error: E } | |
const useRequestHook = async ( | |
endpointName: IEndpointKey, | |
deviceID?: string, | |
body?: IPOSTCommand, | |
args?: string, | |
): Promise<void> => { | |
const method: RESTType = getEndpoint(endpointName)!.type | |
const devices = deviceState.devices | |
const deviceExists = devices.find((d) => d.id === deviceID) | |
let endpoint: string = getEndpoint(endpointName)!.url | |
let deviceURL: string = '' | |
let jsonBody: string = '' | |
console.debug( | |
'[RequestHook]: Device: ', | |
deviceExists, | |
' Endpoint: ', | |
endpoint, | |
' Method: ', | |
method, | |
) | |
if (body) { | |
jsonBody = JSON.stringify(body) | |
console.debug('[RequestHook]: JSON Body: ', jsonBody) | |
} | |
if (deviceExists && typeof deviceExists?.network.address != 'undefined') { | |
deviceURL = 'http://' + deviceExists?.network.address | |
} else { | |
deviceURL = 'http://localhost' | |
} | |
console.debug('[RequestHook]: Device URL: ', deviceURL + endpoint) | |
if (!deviceExists || !deviceURL || deviceURL.length === 0) { | |
throw new Error(`No Device found at that address ${deviceURL}`) | |
} | |
if (args) { | |
endpoint += args | |
} | |
setDeviceRestStatus(deviceExists.id, RESTStatus.LOADING) | |
try { | |
console.log( | |
`Handling request for device ${deviceExists.name} at ${new Date().toISOString()}`, | |
) | |
setDeviceRestStatus(deviceExists.id, RESTStatus.ACTIVE) | |
const timeoutPromise = new Promise( | |
(_, reject) => setTimeout(() => reject(new Error('Request timed out')), 10000), // 10 seconds timeout | |
) | |
const response = (await Promise.race([ | |
makeRequest(endpoint, deviceURL, method, jsonBody), | |
timeoutPromise, | |
])) as __Result__<string, string> | |
console.debug('[REST Response]: ', response) | |
if (response.status === 'error') { | |
console.debug('[REST Request]: ', response.error) | |
throw new Error(`${deviceExists.name} is not reachable. ${response.error}`) | |
} | |
console.debug('[REST Request]: ', response) | |
setDeviceRestStatus(deviceExists.id, RESTStatus.COMPLETE) | |
const data = JSON.parse(response.data) | |
setDeviceRestResponse(deviceExists.id, data) | |
} catch (err) { | |
setDeviceRestStatus(deviceExists.id, RESTStatus.FAILED) | |
error(`[REST Request]: ${err}`) | |
addNotification({ | |
title: 'REST Request Error', | |
message: `${deviceExists.name} is not reachable.`, | |
type: ENotificationType.ERROR, | |
}) | |
} | |
} | |
/** | |
* @description Uploads a firmware to a device | |
* @param firmwareName The name of the firmware file | |
* @param device The device to upload the firmware to | |
* | |
*/ | |
const useOTA = async (firmwareName: string, deviceID: string) => { | |
try { | |
await useRequestHook('ping', deviceID) | |
const devices = deviceState.devices | |
const device = devices.find((d) => d.id === deviceID) | |
const endpoint: string = getEndpoint('ota')!.url | |
if (!device) { | |
throw new Error('No device found that matches the device ID') | |
} | |
const res = device.network.restAPI.response | |
if (!res) { | |
throw new Error('No response from device') | |
} | |
const firmwarePath = await join(await appConfigDir(), firmwareName + '.bin') | |
const deviceURL = 'http://' + device.network.address + endpoint | |
await upload(deviceURL, firmwarePath) | |
} catch (err) { | |
error(`[OTA Error]: ${err}`) | |
addNotification({ | |
title: 'OTA Upload Error', | |
message: `Failed to upload firmware ${err}`, | |
type: ENotificationType.ERROR, | |
}) | |
} | |
} | |
//#endregion | |
//#region API Provider | |
return ( | |
<AppAPIContext.Provider | |
value={{ | |
getGHRestStatus, | |
getFirmware, | |
getGHEndpoint, | |
getEndpoints, | |
getEndpoint, | |
setGHRestStatus, | |
setFirmware, | |
downloadAsset, | |
doGHRequest, | |
useRequestHook, | |
useOTA, | |
}}> | |
{props.children} | |
</AppAPIContext.Provider> | |
) | |
//#endregion | |
} | |
export const useAppAPIContext = () => { | |
const context = useContext(AppAPIContext) | |
if (context === undefined) { | |
throw new Error('useAppAPIContext must be used within an AppAPIProvider') | |
} | |
return context | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { debounce } from '@solid-primitives/scheduled' | |
import { | |
createContext, | |
useContext, | |
type ParentComponent, | |
onMount, | |
createEffect, | |
onCleanup, | |
} from 'solid-js' | |
import { useAppAPIContext } from './api' | |
import { useAppDeviceContext } from './device' | |
import { useAppNotificationsContext } from './notifications' | |
import type { Device } from '@static/types' | |
import { DEVICE_MODIFY_EVENT, DEVICE_STATUS, ENotificationType, RESTStatus } from '@static/enums' | |
interface IAppDeviceDiscoveryContext {} | |
const AppDeviceDiscoveryContext = createContext<IAppDeviceDiscoveryContext>() | |
export const AppDeviceDiscoveryProvider: ParentComponent = (props) => { | |
const { useRequestHook } = useAppAPIContext() | |
const { setDeviceStatus, setDevice, deviceState } = useAppDeviceContext() | |
const { addNotification } = useAppNotificationsContext() | |
const updateDeviceRSSI = debounce((device: Device, newRssi: number) => { | |
if (device.network.wifi.rssi === newRssi) return | |
const _device: Device = { | |
...device, | |
network: { | |
...device.network, | |
wifi: { | |
...device.network.wifi, | |
rssi: newRssi, | |
}, | |
}, | |
} | |
console.debug('Updating device RSSI:', device.name, newRssi) | |
setDevice(_device, DEVICE_MODIFY_EVENT.UPDATE) | |
}, 200) | |
const safeSetDeviceStatus = (device: Device, newStatus: DEVICE_STATUS) => { | |
const currentStatus = device.status | |
if (currentStatus === newStatus) return | |
setDeviceStatus(device.id, newStatus) | |
} | |
const handleDeviceCheck = async (device: Device) => { | |
try { | |
await useRequestHook('ping', device.id) | |
const deviceRestStatus = device.network.restAPI.status | |
console.debug('Checking device:', device.name) | |
if (deviceRestStatus === RESTStatus.FAILED) { | |
console.debug('Device not reachable:', device.name) | |
throw new Error(`${device.name} is not reachable.`) | |
} | |
safeSetDeviceStatus(device, DEVICE_STATUS.ACTIVE) | |
} catch (error) { | |
addNotification({ | |
title: 'Device Detection Error', | |
message: `Failed to detect ${device.name} - ${error}.`, | |
type: ENotificationType.ERROR, | |
}) | |
safeSetDeviceStatus(device, DEVICE_STATUS.FAILED) | |
updateDeviceRSSI(device, -95) | |
} | |
} | |
const handleDeviceRSSI = async (device: Device) => { | |
try { | |
console.debug('Checking wifi strength:', device.name) | |
if ( | |
[ | |
DEVICE_STATUS.FAILED, | |
DEVICE_STATUS.DISABLED, | |
DEVICE_STATUS.NONE, | |
DEVICE_STATUS.LOADING, | |
].includes(device.status) | |
) { | |
console.debug('Device status is not active:', device.name) | |
throw new Error(`${device.name} is not reachable.`) | |
} | |
await useRequestHook('wifiStrength', device.id, undefined, '?points=10') | |
const wifiStatus = device.network.restAPI.status | |
if (wifiStatus === RESTStatus.FAILED) { | |
console.error('Failed to get wifi strength:', device.name) | |
throw new Error(`${device.name} is not reachable.`) | |
} | |
const wifiRes = device.network.restAPI.response | |
if (!wifiRes) { | |
throw new Error('Invalid RSSI response format') | |
} | |
// find the object in the data array that contains an rssi key | |
const rssi = wifiRes.find((d: any) => d.rssi)?.rssi | |
console.debug('RSSI:', rssi) | |
if (!rssi) { | |
throw new Error('Invalid RSSI value') | |
} | |
updateDeviceRSSI(device, rssi) | |
} catch (error) { | |
addNotification({ | |
title: 'Device Detection Error', | |
message: `Failed to detect ${device.name} - ${error}.`, | |
type: ENotificationType.ERROR, | |
}) | |
safeSetDeviceStatus(device, DEVICE_STATUS.FAILED) | |
updateDeviceRSSI(device, -95) | |
} | |
} | |
const handleBatchedUpdate = (device: Device) => { | |
handleDeviceCheck(device).then(() => | |
handleDeviceRSSI(device) | |
.then(() => { | |
safeSetDeviceStatus(device, DEVICE_STATUS.ACTIVE) | |
}) | |
.catch(() => { | |
safeSetDeviceStatus(device, DEVICE_STATUS.FAILED) | |
updateDeviceRSSI(device, -95) | |
}), | |
) | |
} | |
onMount(async () => { | |
addNotification({ | |
title: 'Welcome to the Lumin LED Controller', | |
message: 'This is the dashboard where you can manage your devices.', | |
type: ENotificationType.INFO, | |
}) | |
const devices = deviceState.devices.allItems | |
await Promise.all(devices.map(handleBatchedUpdate)) | |
}) | |
createEffect(() => { | |
console.log('Checking devices...', deviceState.devices.allItems) | |
const interval = setInterval(async () => { | |
const devices = deviceState.devices.allItems | |
await Promise.all(devices.map(handleBatchedUpdate)) | |
}, 5000) | |
onCleanup(() => clearInterval(interval)) | |
}) | |
return ( | |
<AppDeviceDiscoveryContext.Provider value={{}}> | |
{props.children} | |
</AppDeviceDiscoveryContext.Provider> | |
) | |
} | |
export const useAppDeviceDiscoveryContext = () => { | |
const context = useContext(AppDeviceDiscoveryContext) | |
if (context === undefined) { | |
throw new Error( | |
'useAppDeviceDiscoveryContext must be used within an AppDeviceDiscoveryProvider', | |
) | |
} | |
return context | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use tauri::{ | |
plugin::{Builder, TauriPlugin}, | |
AppHandle, Manager, Runtime, | |
}; | |
use reqwest::Client; | |
use std::sync::{Arc, Mutex}; | |
use utils::{errors::AppResult, errors::Error, prelude::*}; | |
const PLUGIN_NAME: &str = "tauri-plugin-request-client"; | |
#[derive(Debug)] | |
pub struct RESTClient { | |
pub http_client: Arc<tauri::async_runtime::Mutex<Client>>, | |
pub base_url: Arc<Mutex<String>>, | |
pub method: Arc<Mutex<String>>, | |
pub body: Arc<Mutex<String>>, | |
} | |
/// A function to create a new RESTClient instance | |
/// ## Arguments | |
/// - `base_url` The base url of the api to query | |
impl RESTClient { | |
pub fn new(base_url: Option<String>, method: Option<String>) -> Self { | |
Self { | |
http_client: Arc::new(tauri::async_runtime::Mutex::new(Client::new())), | |
base_url: Arc::new(Mutex::new(base_url.unwrap_or_default())), | |
method: Arc::new(Mutex::new(method.unwrap_or_default())), | |
body: Arc::new(Mutex::new(String::new())), | |
} | |
} | |
} | |
#[derive(Debug)] | |
pub struct APIPlugin<R: Runtime> { | |
pub app_handle: AppHandle<R>, | |
pub rest_client: RESTClient, | |
} | |
impl<R: Runtime> APIPlugin<R> { | |
fn new(app_handle: AppHandle<R>) -> Self { | |
let rest_client = RESTClient::new(None, None); | |
Self { | |
app_handle, | |
rest_client, | |
} | |
} | |
fn set_base_url(&self, base_url: String) -> &Self { | |
*self.rest_client.base_url.lock().unwrap() = base_url; | |
self | |
} | |
fn set_method(&self, method: String) -> &Self { | |
*self.rest_client.method.lock().unwrap() = method; | |
self | |
} | |
fn get_base_url(&self) -> String { | |
self.rest_client.base_url.lock().unwrap().clone() | |
} | |
fn get_method(&self) -> String { | |
self.rest_client.method.lock().unwrap().clone() | |
} | |
fn set_body(&self, body: String) -> &Self { | |
*self.rest_client.body.lock().unwrap() = body; | |
self | |
} | |
fn get_body(&self) -> String { | |
self.rest_client.body.lock().unwrap().clone() | |
} | |
async fn request(&self) -> AppResult<String> { | |
info!("Making REST request"); | |
let base_url = self.get_base_url(); | |
let method = self.get_method(); | |
let method = method.as_str(); | |
let body = self.get_body(); | |
let body = body.trim(); // Trim the body to remove whitespace from both ends | |
debug!("JSON Body: {}", self.get_body()); | |
let body: serde_json::Value = if !body.is_empty() { | |
match serde_json::from_str(body) { | |
Ok(parsed) => parsed, | |
Err(e) => { | |
error!("Failed to parse body: {}", e); | |
serde_json::Value::Null // Use a null JSON value if parsing fails | |
} | |
} | |
} else { | |
serde_json::Value::Null // Default to null if body is empty | |
}; | |
let response = match method { | |
"GET" => { | |
self.rest_client | |
.http_client | |
.lock() | |
.await | |
.get(&base_url) | |
.header("User-Agent", "Lumin") | |
.send() | |
.await? | |
.text() | |
.await? | |
} | |
"POST" => { | |
self.rest_client | |
.http_client | |
.lock() | |
.await | |
.post(&base_url) | |
.json(&body) | |
.header("User-Agent", "Lumin") | |
.send() | |
.await? | |
.text() | |
.await? | |
} | |
_ => { | |
error!("Invalid method"); | |
return Err(Error::IO(std::io::Error::new( | |
std::io::ErrorKind::Other, | |
"Invalid method", | |
))); | |
} | |
}; | |
debug!("Response: {}", response); | |
Ok(response) | |
} | |
/// A function to run a REST Client and create a new RESTClient instance for each device found | |
/// ## Arguments | |
/// - `endpoint` The endpoint to query for | |
/// - `device_name` The name of the device to query | |
/// - `body` The body of the request to send - optional | |
async fn run_rest_client( | |
&self, | |
endpoint: String, | |
device_name: String, | |
method: String, | |
body: String, | |
) -> AppResult<String> { | |
info!("Starting REST client"); | |
let full_url = format!("{}{}", device_name, endpoint); | |
info!("[APIPlugin]: Full url: {}", full_url); | |
info!("[APIPlugin]: Body: {}", body); | |
self.set_base_url(full_url) | |
.set_method(method) | |
.set_body(body); | |
let request_result = self.request().await; | |
let response_msg = match request_result { | |
Ok(response) => response, | |
Err(e) => { | |
error!("[APIPlugin]: Request failed: {}", e); | |
e.to_string() | |
} | |
}; | |
Ok(response_msg) | |
} | |
} | |
#[tauri::command] | |
#[specta::specta] | |
async fn make_request<R: Runtime>( | |
endpoint: String, | |
device_name: String, | |
method: String, | |
body: String, | |
app_handle: AppHandle<R>, | |
) -> Result<String, String> { | |
info!("Starting REST request"); | |
let result = app_handle | |
.state::<APIPlugin<R>>() | |
.run_rest_client(endpoint, device_name, method, body) | |
.await; | |
match result { | |
Ok(response) => { | |
info!("[APIPlugin]: Request response: Ok"); | |
Ok(response) | |
} | |
Err(e) => { | |
error!("[APIPlugin]: Request failed: {}", e); | |
Err(e.to_string()) | |
} | |
} | |
} | |
#[allow(unused_macros)] | |
macro_rules! specta_builder { | |
($e:expr, Runtime) => { | |
ts::builder() | |
.commands(collect_commands![make_request::<$e>]) | |
.path(generate_plugin_path(PLUGIN_NAME)) | |
.config( | |
specta::ts::ExportConfig::default().formatter(specta::js_doc::formatter::prettier), | |
) | |
//.events(collect_events![RandomNumber]) | |
}; | |
} | |
pub fn init<R: Runtime>() -> TauriPlugin<R> { | |
//let plugin_utils = specta_builder!(R, Runtime).into_plugin_utils(PLUGIN_NAME); | |
Builder::new(PLUGIN_NAME) | |
//.invoke_handler(plugin_utils.invoke_handler) | |
.setup(move |app| { | |
let app = app.clone(); | |
//(plugin_utils.setup)(&app); | |
let plugin = APIPlugin::new(app.app_handle()); | |
app.manage(plugin); | |
Ok(()) | |
}) | |
.invoke_handler(tauri::generate_handler![make_request]) | |
.build() | |
} | |
#[cfg(test)] | |
mod test { | |
use super::*; | |
#[test] | |
fn export_types() { | |
println!("Exporting types for plugin: {}", PLUGIN_NAME); | |
println!("Export path: {}", generate_plugin_path(PLUGIN_NAME)); | |
assert_eq!( | |
specta_builder!(tauri::Wry, Runtime) | |
.export_for_plugin(PLUGIN_NAME) | |
.ok(), | |
Some(()) | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment