Initial 1.3.x rework

This commit is contained in:
Sacha Weatherstone
2022-07-26 08:52:23 +10:00
parent ff6019098b
commit a2280ab96e
163 changed files with 6383 additions and 14397 deletions
-205
View File
@@ -1,205 +0,0 @@
import { connType } from '@core/slices/appSlice';
import {
addChannel,
addChat,
addLogEvent,
addMessage,
addNode,
addPosition,
addUser,
resetState,
setConfig,
setDeviceStatus,
setLastMeshInterraction,
setModuleConfig,
setMyNodeInfo,
setReady,
updateLastInteraction,
} from '@core/slices/meshtasticSlice';
import { store } from '@core/store';
import {
IBLEConnection,
IHTTPConnection,
ISerialConnection,
Protobuf,
SettingsManager,
Types,
} from '@meshtastic/meshtasticjs';
type connectionType = IBLEConnection | IHTTPConnection | ISerialConnection;
export let connection: connectionType = new IHTTPConnection();
const appState = store.getState().app;
export const connectionUrl = appState.connectionParams.HTTP.address;
export const setConnection = async (conn: connType): Promise<void> => {
await connection.disconnect();
cleanupListeners();
switch (conn) {
case connType.HTTP:
connection = new IHTTPConnection();
break;
case connType.BLE:
connection = new IBLEConnection();
break;
case connType.SERIAL:
connection = new ISerialConnection();
break;
}
registerListeners();
const connectionParams = store.getState().app.connectionParams;
console.log(connectionParams);
switch (conn) {
case connType.HTTP:
await connection.connect(connectionParams.HTTP);
break;
case connType.BLE:
await connection.connect(
// @ts-ignore tmp
connectionParams.BLE,
);
break;
case connType.SERIAL:
await connection.connect(
// @ts-ignore tmp
connectionParams.SERIAL,
);
break;
}
};
export const cleanupListeners = (): void => {
connection.onMeshPacket.cancelAll();
connection.onDeviceStatus.cancelAll();
connection.onMyNodeInfo.cancelAll();
connection.onUserPacket.cancelAll();
connection.onPositionPacket.cancelAll();
connection.onNodeInfoPacket.cancelAll();
connection.onAdminPacket.cancelAll();
connection.onMeshHeartbeat.cancelAll();
connection.onTextPacket.cancelAll();
};
const registerListeners = (): void => {
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
connection.onLogEvent.subscribe((log) => {
store.dispatch(addLogEvent(log));
});
connection.onDeviceStatus.subscribe((status) => {
store.dispatch(setDeviceStatus(status));
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) {
store.dispatch(setReady(true));
void connection.getConfig(Protobuf.AdminMessage_ConfigType.DEVICE_CONFIG);
void connection.getConfig(Protobuf.AdminMessage_ConfigType.WIFI_CONFIG);
void connection.getConfig(
Protobuf.AdminMessage_ConfigType.POSITION_CONFIG,
);
void connection.getConfig(
Protobuf.AdminMessage_ConfigType.DISPLAY_CONFIG,
);
void connection.getConfig(Protobuf.AdminMessage_ConfigType.LORA_CONFIG);
void connection.getConfig(Protobuf.AdminMessage_ConfigType.POWER_CONFIG);
}
if (status === Types.DeviceStatusEnum.DEVICE_DISCONNECTED) {
store.dispatch(setReady(false));
store.dispatch(resetState());
cleanupListeners();
}
});
connection.onMyNodeInfo.subscribe((nodeInfo) => {
store.dispatch(setMyNodeInfo(nodeInfo));
});
connection.onUserPacket.subscribe((user) => {
store.dispatch(addUser(user));
});
connection.onPositionPacket.subscribe((position) => {
store.dispatch(addPosition(position));
});
connection.onNodeInfoPacket.subscribe(
(nodeInfoPacket): void | { payload: Protobuf.NodeInfo; type: string } => {
store.dispatch(addNode(nodeInfoPacket.data));
store.dispatch(addChat(nodeInfoPacket.data.num));
},
);
connection.onAdminPacket.subscribe((adminPacket) => {
console.log(adminPacket.data.variant.oneofKind);
switch (adminPacket.data.variant.oneofKind) {
case 'getChannelResponse':
store.dispatch(addChannel(adminPacket.data.variant.getChannelResponse));
store.dispatch(
addChat(adminPacket.data.variant.getChannelResponse.index),
);
break;
case 'getOwnerResponse':
store.dispatch(
addUser({
data: adminPacket.data.variant.getOwnerResponse,
packet: adminPacket.packet,
}),
);
break;
case 'getConfigResponse':
store.dispatch(setConfig(adminPacket.data.variant.getConfigResponse));
break;
case 'getModuleConfigResponse':
store.dispatch(
setModuleConfig(adminPacket.data.variant.getModuleConfigResponse),
);
break;
}
});
connection.onMeshHeartbeat.subscribe(
(date): void | { payload: number; type: string } =>
store.dispatch(setLastMeshInterraction(date.getTime())),
);
connection.onRoutingPacket.subscribe((routingPacket) => {
console.log(routingPacket.data.variant.oneofKind);
switch (routingPacket.data.variant.oneofKind) {
case 'errorReason':
console.log(
Protobuf.Routing_Error[routingPacket.data.variant.errorReason],
);
break;
default:
break;
}
store.dispatch(
updateLastInteraction({
id: routingPacket.packet.from,
time: new Date(routingPacket.packet.rxTime * 1000),
}),
);
});
connection.onTextPacket.subscribe((message) => {
const myNodeNum = store.getState().meshtastic.radio.hardware.myNodeNum;
store.dispatch(
addMessage({
message: message,
ack: message.packet.from !== myNodeNum,
received: message.packet.rxTime
? new Date(message.packet.rxTime * 1000)
: new Date(),
}),
);
});
};
-60
View File
@@ -1,60 +0,0 @@
import type { Style } from 'mapbox-gl';
export type MapStyleName =
| 'Streets'
| 'Outdoors'
| 'Light'
| 'Dark'
| 'Satellite';
export interface MapStyle {
title: MapStyleName;
data: Style | string;
}
type MapStyleType = {
[mapStyleType in MapStyleName]: MapStyle;
};
export const MapStyles: MapStyleType = {
Streets: {
title: 'Streets',
data: 'mapbox://styles/mapbox/streets-v11?optimize=true',
} as MapStyle,
Outdoors: {
title: 'Outdoors',
data: 'mapbox://styles/mapbox/outdoors-v11?optimize=true',
} as MapStyle,
Light: {
title: 'Light',
data: 'mapbox://styles/sachaw/cl03ij03g001414l20w0w0ivj?optimize=true',
} as MapStyle,
Dark: {
title: 'Dark',
data: 'mapbox://styles/sachaw/ckwzwm92e1oep14pjunjqlqbo?optimize=true',
} as MapStyle,
Satellite: {
title: 'Satellite',
data: {
version: 8,
layers: [
{
id: 'esri',
type: 'raster',
source: 'esri',
},
],
sources: {
esri: {
type: 'raster',
tiles: [
'https://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
],
maxzoom: 17,
},
},
},
} as MapStyle,
};
-7
View File
@@ -1,7 +0,0 @@
import { createRouter, defineRoute } from 'type-route';
export const { RouteProvider, useRoute, routes } = createRouter({
messages: defineRoute('/'),
map: defineRoute('/map'),
extensions: defineRoute('/extensions'),
});
-103
View File
@@ -1,103 +0,0 @@
import type { Types } from '@meshtastic/meshtasticjs';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
export type currentPageName = 'messages' | 'settings';
export enum connType {
HTTP,
BLE,
SERIAL,
}
interface AppState {
mobileNavOpen: boolean;
navCollapsed: boolean;
connectionModalOpen: boolean;
darkMode: boolean;
currentPage: currentPageName;
connType: connType;
connectionParams: {
BLE: Types.BLEConnectionParameters;
HTTP: Types.HTTPConnectionParameters;
SERIAL: Types.SerialConnectionParameters;
};
updateAvaliable: boolean;
}
const initialState: AppState = {
mobileNavOpen: false,
navCollapsed: false,
connectionModalOpen: true,
darkMode: localStorage.getItem('darkModeDisabled') !== 'true' ?? false,
currentPage: 'messages',
connType: connType.HTTP,
connectionParams: {
BLE: {},
HTTP: {
address:
localStorage.getItem('connectionUrl') ?? import.meta.env.PROD
? window.location.hostname
: (import.meta.env.VITE_PUBLIC_DEVICE_IP as string) ??
'meshtastic.local',
tls: false,
receiveBatchRequests: false,
fetchInterval: 2000,
},
SERIAL: {},
},
updateAvaliable: false,
};
export const appSlice = createSlice({
name: 'app',
initialState,
reducers: {
toggleMobileNav(state) {
state.mobileNavOpen = !state.mobileNavOpen;
},
openConnectionModal(state) {
state.connectionModalOpen = true;
},
closeConnectionModal(state) {
state.connectionModalOpen = false;
},
setDarkModeEnabled(state, action: PayloadAction<boolean>) {
localStorage.setItem('darkModeDisabled', String(!action.payload));
state.darkMode = action.payload;
},
setCurrentPage(state, action: PayloadAction<currentPageName>) {
state.currentPage = action.payload;
},
setConnType(state, action: PayloadAction<connType>) {
state.connType = action.payload;
},
setConnectionParams(
state,
action: PayloadAction<{
type: connType;
params: Types.ConnectionParameters;
}>,
) {
// @ts-ignore tmp
state.connectionParams[connType[action.payload.type]] =
action.payload.params;
},
setUpdateAvaliable(state, action: PayloadAction<boolean>) {
state.updateAvaliable = action.payload;
},
},
});
export const {
toggleMobileNav,
openConnectionModal,
closeConnectionModal,
setDarkModeEnabled,
setCurrentPage,
setConnType,
setConnectionParams,
setUpdateAvaliable,
} = appSlice.actions;
export default appSlice.reducer;
-70
View File
@@ -1,70 +0,0 @@
import Point from '@arcgis/core/geometry/Point';
import type { MapStyleName } from '@core/mapStyles';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
interface MapState {
firstLoad: boolean;
latLng: Point;
zoom: number;
bearing: number;
pitch: number;
accessToken: string;
style: MapStyleName;
hillShade: boolean;
exaggeration: boolean;
}
const initialState: MapState = {
firstLoad: true,
latLng: new Point(),
zoom: 2,
bearing: 0,
pitch: 0,
accessToken:
'pk.eyJ1Ijoic2FjaGF3IiwiYSI6ImNrNW9meXozZjBsdW0zbHBjM2FnNnV6cmsifQ.3E4n8eFGD9ZOFo-XDVeZnQ',
style: localStorage.getItem('darkModeDisabled') !== 'true' ? 'Dark' : 'Light',
hillShade: false,
exaggeration: true,
};
export const mapSlice = createSlice({
name: 'map',
initialState,
reducers: {
setLatLng: (state, action: PayloadAction<Point>) => {
state.latLng = action.payload;
},
setZoom: (state, action: PayloadAction<number>) => {
state.zoom = action.payload;
},
setBearing: (state, action: PayloadAction<number>) => {
state.bearing = action.payload;
},
setPitch: (state, action: PayloadAction<number>) => {
state.pitch = action.payload;
},
setMapStyle(state, action: PayloadAction<MapStyleName>) {
state.style = action.payload;
},
setHillShade(state, action: PayloadAction<boolean>) {
state.hillShade = action.payload;
},
setExaggeration(state, action: PayloadAction<boolean>) {
state.exaggeration = action.payload;
},
},
});
export const {
setLatLng,
setZoom,
setBearing,
setPitch,
setMapStyle,
setHillShade,
setExaggeration,
} = mapSlice.actions;
export default mapSlice.reducer;
-343
View File
@@ -1,343 +0,0 @@
import { Protobuf, Types } from '@meshtastic/meshtasticjs';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
export interface MessageWithAck {
message: Types.TextPacket;
ack: boolean;
received: Date;
}
export interface Chat {
lastInterraction: Date;
messages: MessageWithAck[];
}
type ChatEntries = {
[key in number]: Chat;
};
interface Route {
from: number;
to: number;
hops: number;
//speed stats?
}
export interface Node {
metrics: Protobuf.DeviceMetrics;
data: Protobuf.NodeInfo;
}
export interface Radio {
channels: Protobuf.Channel[];
config: Config;
moduleConfig: ModuleConfig;
hardware: Protobuf.MyNodeInfo;
}
export interface Config {
device: Protobuf.Config_DeviceConfig;
position: Protobuf.Config_PositionConfig;
power: Protobuf.Config_PowerConfig;
wifi: Protobuf.Config_WiFiConfig;
display: Protobuf.Config_DisplayConfig;
lora: Protobuf.Config_LoRaConfig;
}
export interface ModuleConfig {
mqtt: Protobuf.ModuleConfig_MQTTConfig;
serial: Protobuf.ModuleConfig_SerialConfig;
extNotification: Protobuf.ModuleConfig_ExternalNotificationConfig;
storeForward: Protobuf.ModuleConfig_StoreForwardConfig;
rangeTest: Protobuf.ModuleConfig_RangeTestConfig;
telemetry: Protobuf.ModuleConfig_TelemetryConfig;
cannedMessage: Protobuf.ModuleConfig_CannedMessageConfig;
}
interface MeshtasticState {
deviceStatus: Types.DeviceStatusEnum;
lastMeshInterraction: number;
ready: boolean;
nodes: Node[];
radio: Radio;
chats: ChatEntries;
logs: Types.LogEventPacket[];
}
const initialState: MeshtasticState = {
deviceStatus: Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
lastMeshInterraction: 0,
ready: false,
nodes: [],
radio: {
channels: [],
config: {
device: Protobuf.Config_DeviceConfig.create(),
position: Protobuf.Config_PositionConfig.create(),
power: Protobuf.Config_PowerConfig.create(),
wifi: Protobuf.Config_WiFiConfig.create(),
display: Protobuf.Config_DisplayConfig.create(),
lora: Protobuf.Config_LoRaConfig.create(),
},
moduleConfig: {
mqtt: Protobuf.ModuleConfig_MQTTConfig.create(),
serial: Protobuf.ModuleConfig_SerialConfig.create(),
extNotification:
Protobuf.ModuleConfig_ExternalNotificationConfig.create(),
storeForward: Protobuf.ModuleConfig_StoreForwardConfig.create(),
rangeTest: Protobuf.ModuleConfig_RangeTestConfig.create(),
telemetry: Protobuf.ModuleConfig_TelemetryConfig.create(),
cannedMessage: Protobuf.ModuleConfig_CannedMessageConfig.create(),
},
hardware: Protobuf.MyNodeInfo.create(),
},
chats: {},
logs: [],
};
export const meshtasticSlice = createSlice({
name: 'meshtastic',
initialState,
reducers: {
addLogEvent: (state, action: PayloadAction<Types.LogEventPacket>) => {
state.logs.push(action.payload);
},
clearLogs: (state) => {
state.logs = [];
},
setDeviceStatus: (state, action: PayloadAction<Types.DeviceStatusEnum>) => {
state.deviceStatus = action.payload;
},
setLastMeshInterraction: (state, action: PayloadAction<number>) => {
state.lastMeshInterraction = action.payload;
},
setReady: (state, action: PayloadAction<boolean>) => {
state.ready = action.payload;
},
setMyNodeInfo: (state, action: PayloadAction<Protobuf.MyNodeInfo>) => {
state.radio.hardware = action.payload;
},
addUser: (state, action: PayloadAction<Types.UserPacket>) => {
const node = state.nodes.find(
(node) => node.data.num === action.payload.packet.from,
);
if (node) {
node.data.user = action.payload.data;
if (action.payload.packet.rxTime) {
node.data.lastHeard = new Date(
action.payload.packet.rxTime * 1000,
).getTime();
}
} else {
state.nodes.push({
data: {
num: action.payload.packet.from,
snr: action.payload.packet.rxSnr,
lastHeard: new Date().getTime(),
...action.payload.packet,
},
metrics: Protobuf.DeviceMetrics.create(),
});
}
},
addPosition: (state, action: PayloadAction<Types.PositionPacket>) => {
const node = state.nodes.find(
(node) => node.data.num === action.payload.packet.from,
);
if (node) {
node.data.position = action.payload.data;
if (action.payload.packet.rxTime) {
node.data.lastHeard = new Date(
action.payload.packet.rxTime * 1000,
).getTime();
}
}
},
addNode: (state, action: PayloadAction<Protobuf.NodeInfo>) => {
const node = state.nodes.find(
(node) => node.data.num === action.payload.num,
);
if (node) {
node.data.lastHeard = new Date(
action.payload.lastHeard * 1000,
).getTime();
node.data.snr = action.payload.snr;
} else {
state.nodes.push({
data: action.payload,
metrics: Protobuf.DeviceMetrics.create(),
});
}
},
addChannel: (state, action: PayloadAction<Protobuf.Channel>) => {
if (
state.radio.channels.findIndex(
(channel) => channel.index === action.payload.index,
) !== -1
) {
state.radio.channels = state.radio.channels.map((channel) => {
return channel.index === action.payload.index
? action.payload
: channel;
});
} else {
state.radio.channels.push(action.payload);
}
},
setConfig: (state, action: PayloadAction<Protobuf.Config>) => {
switch (action.payload.payloadVariant.oneofKind) {
case 'device': {
state.radio.config.device = action.payload.payloadVariant.device;
break;
}
case 'position': {
state.radio.config.position = action.payload.payloadVariant.position;
break;
}
case 'power': {
state.radio.config.power = action.payload.payloadVariant.power;
break;
}
case 'wifi': {
state.radio.config.wifi = action.payload.payloadVariant.wifi;
break;
}
case 'display': {
state.radio.config.display = action.payload.payloadVariant.display;
break;
}
case 'lora': {
state.radio.config.lora = action.payload.payloadVariant.lora;
break;
}
}
},
setModuleConfig: (state, action: PayloadAction<Protobuf.ModuleConfig>) => {
switch (action.payload.payloadVariant.oneofKind) {
case 'cannedMessage': {
state.radio.moduleConfig.cannedMessage =
action.payload.payloadVariant.cannedMessage;
break;
}
case 'externalNotification': {
state.radio.moduleConfig.extNotification =
action.payload.payloadVariant.externalNotification;
break;
}
case 'mqtt': {
state.radio.moduleConfig.mqtt = action.payload.payloadVariant.mqtt;
break;
}
case 'rangeTest': {
state.radio.moduleConfig.rangeTest =
action.payload.payloadVariant.rangeTest;
break;
}
case 'serial': {
state.radio.moduleConfig.serial =
action.payload.payloadVariant.serial;
break;
}
case 'storeForward': {
state.radio.moduleConfig.storeForward =
action.payload.payloadVariant.storeForward;
break;
}
case 'telemetry': {
state.radio.moduleConfig.telemetry =
action.payload.payloadVariant.telemetry;
break;
}
}
},
addMessage: (state, action: PayloadAction<MessageWithAck>) => {
// todo: is last interraction for just channel chats or dm's to?
state.chats[action.payload.message.packet.channel].lastInterraction =
new Date();
if (action.payload.message.packet.to === 0xffffffff) {
// TODO: use chatIndex
state.chats[action.payload.message.packet.channel].messages.push(
action.payload,
);
} else {
const dmIndex =
action.payload.message.packet.from === state.radio.hardware.myNodeNum
? action.payload.message.packet.to
: action.payload.message.packet.from;
state.chats[dmIndex].messages.push(action.payload);
}
},
ackMessage: (
state,
action: PayloadAction<{ chatIndex: number; messageId: number }>,
) => {
console.log(action.payload.chatIndex);
console.log(state.chats);
console.log(state.chats[action.payload.chatIndex]);
state.chats[action.payload.chatIndex].messages.map((message) => {
console.log('ack');
if (message.message.packet.id === action.payload.messageId) {
console.log('acked');
message.ack = true;
}
});
},
updateLastInteraction: (
state,
action: PayloadAction<{ id: number; time: Date }>,
) => {
const node = state.nodes.find(
(node) => node.data.num === action.payload.id,
);
if (node) {
node.data.lastHeard = action.payload.time.getTime();
}
},
addChat: (state, action: PayloadAction<number>) => {
state.chats[action.payload] = {
messages: [],
lastInterraction: new Date(),
};
},
resetState: (state) => {
state.deviceStatus = Types.DeviceStatusEnum.DEVICE_DISCONNECTED;
state.nodes = [];
state.radio = initialState.radio;
state.ready = false;
state.lastMeshInterraction = 0;
},
},
});
export const {
addLogEvent,
clearLogs,
setDeviceStatus,
setLastMeshInterraction,
setReady,
setMyNodeInfo,
addUser,
addPosition,
addNode,
addChannel,
setConfig,
setModuleConfig,
addMessage,
ackMessage,
updateLastInteraction,
addChat,
resetState,
} = meshtasticSlice.actions;
export default meshtasticSlice.reducer;
-19
View File
@@ -1,19 +0,0 @@
import appReducer from '@core/slices/appSlice';
import mapReducer from '@core/slices/mapSlice';
import meshtasticReducer from '@core/slices/meshtasticSlice';
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
app: appReducer,
meshtastic: meshtasticReducer,
map: mapReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
+32
View File
@@ -0,0 +1,32 @@
import create from "zustand";
interface AppState {
selectedDevice?: number;
devices: {
id: number;
num: number;
}[];
setSelectedDevice: (deviceId: number) => void;
addDevice: (device: { id: number; num: number }) => void;
removeDevice: (deviceId: number) => void;
}
export const useAppStore = create<AppState>()((set) => ({
selectedDevice: 0,
devices: [],
currentPage: "messages",
setSelectedDevice: (deviceId) =>
set(() => ({
selectedDevice: deviceId,
})),
addDevice: (device) =>
set((state) => ({
devices: [...state.devices, device],
})),
removeDevice: (deviceId) =>
set((state) => ({
devices: state.devices.filter((device) => device.id !== deviceId),
})),
}));
+418
View File
@@ -0,0 +1,418 @@
import { createContext, useContext } from "react";
import { produce } from "immer";
import create from "zustand";
import { IConnection, Protobuf, Types } from "@meshtastic/meshtasticjs";
export type Page =
| "messages"
| "map"
| "extensions"
| "config"
| "channels"
| "info";
export interface MessageWithAck {
message: Types.TextPacket;
ack: boolean;
received: Date;
}
export interface Channel {
config: Protobuf.Channel;
lastInterraction: Date;
messages: MessageWithAck[];
}
export interface Node {
deviceMetrics: Protobuf.DeviceMetrics;
environmentMetrics: Protobuf.EnvironmentMetrics;
data: Protobuf.NodeInfo;
}
export interface Device {
ready: boolean;
status: Types.DeviceStatusEnum;
channels: Channel[];
config: Protobuf.LocalConfig;
moduleConfig: Protobuf.LocalModuleConfig;
hardware: Protobuf.MyNodeInfo;
nodes: Node[];
connection?: IConnection;
activeChat: number;
activePage: Page;
setReady(ready: boolean): void;
setStatus: (status: Types.DeviceStatusEnum) => void;
addChannel: (channel: Channel) => void;
setConfig: (config: Protobuf.Config) => void;
setModuleConfig: (config: Protobuf.ModuleConfig) => void;
setHardware: (hardware: Protobuf.MyNodeInfo) => void;
setMetrics: (metrics: Types.TelemetryPacket) => void;
setActiveChat: (chatIndex: number) => void;
setActivePage: (page: Page) => void;
addNodeInfo: (nodeInfo: Types.NodeInfoPacket) => void;
addUser: (user: Types.UserPacket) => void;
addPosition: (position: Types.PositionPacket) => void;
addConnection: (connection: IConnection) => void;
addMessage: (message: MessageWithAck) => void;
ackMessage: (channelIndex: number, messageId: number) => void;
}
export interface DeviceState {
devices: Map<number, Device>;
remoteDevices: Map<number, undefined>;
addDevice: (id: number) => Device;
removeDevice: (id: number) => void;
getDevices: () => Device[];
}
export const useDeviceStore = create<DeviceState>((set, get) => ({
devices: new Map(),
remoteDevices: new Map(),
addDevice: (id: number) => {
set(
produce<DeviceState>((draft) => {
draft.devices.set(id, {
ready: false,
status: Types.DeviceStatusEnum.DEVICE_DISCONNECTED,
channels: [],
config: Protobuf.LocalConfig.create(),
moduleConfig: Protobuf.LocalModuleConfig.create(),
hardware: Protobuf.MyNodeInfo.create(),
nodes: [],
connection: undefined,
activeChat: 0,
activePage: "messages",
setReady: (ready: boolean) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.ready = ready;
}
})
);
},
setStatus: (status: Types.DeviceStatusEnum) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.status = status;
}
})
);
},
addChannel: (channel: Channel) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
const channelIndex = device.channels.findIndex(
(c) => c.config.index === channel.config.index
);
if (channelIndex !== -1) {
const messages = device.channels[channelIndex].messages;
device.channels[channelIndex] = channel;
device.channels[channelIndex].messages = messages;
} else {
device.channels.push(channel);
}
}
})
);
},
setConfig: (config: Protobuf.Config) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
switch (config.payloadVariant.oneofKind) {
case "device":
device.config.device = config.payloadVariant.device;
break;
case "position":
device.config.position = config.payloadVariant.position;
break;
case "power":
device.config.power = config.payloadVariant.power;
break;
case "wifi":
device.config.wifi = config.payloadVariant.wifi;
break;
case "display":
device.config.display = config.payloadVariant.display;
break;
case "lora":
device.config.lora = config.payloadVariant.lora;
break;
}
}
})
);
},
setModuleConfig: (config: Protobuf.ModuleConfig) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
switch (config.payloadVariant.oneofKind) {
case "mqtt":
device.moduleConfig.mqtt = config.payloadVariant.mqtt;
break;
case "serial":
device.moduleConfig.serial = config.payloadVariant.serial;
break;
case "externalNotification":
device.moduleConfig.externalNotification =
config.payloadVariant.externalNotification;
break;
case "storeForward":
device.moduleConfig.storeForward =
config.payloadVariant.storeForward;
break;
case "rangeTest":
device.moduleConfig.rangeTest =
config.payloadVariant.rangeTest;
break;
case "telemetry":
device.moduleConfig.telemetry =
config.payloadVariant.telemetry;
break;
case "cannedMessage":
device.moduleConfig.cannedMessage =
config.payloadVariant.cannedMessage;
break;
}
}
})
);
},
setHardware: (hardware: Protobuf.MyNodeInfo) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.hardware = hardware;
}
})
);
},
setMetrics: (metrics: Types.TelemetryPacket) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
let node = device?.nodes.find(
(n) => n.data.num === metrics.packet.from
);
if (device && !node) {
node = {
data: Protobuf.NodeInfo.create({
num: metrics.packet.from,
snr: metrics.packet.rxSnr,
lastHeard: new Date().getSeconds(),
}),
deviceMetrics: Protobuf.DeviceMetrics.create(),
environmentMetrics: Protobuf.EnvironmentMetrics.create(),
};
device.nodes.push(node);
}
if (node) {
switch (metrics.data.variant.oneofKind) {
case "deviceMetrics":
node.deviceMetrics = metrics.data.variant.deviceMetrics;
break;
case "environmentMetrics":
node.environmentMetrics =
metrics.data.variant.environmentMetrics;
break;
}
}
})
);
},
setActiveChat: (chatIndex) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.activeChat = chatIndex;
}
})
);
},
setActivePage: (page) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.activePage = page;
}
})
);
},
addNodeInfo: (nodeInfo) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
const node = device.nodes.find(
(node) => node.data.num === nodeInfo.data.num
);
if (node) {
node.data = nodeInfo.data;
// if (action.payload.packet.rxTime) {
// node.data.lastHeard = new Date(
// action.payload.packet.rxTime * 1000,
// ).getTime();
// }
} else {
device.nodes.push({
data: Protobuf.NodeInfo.create(nodeInfo.data),
deviceMetrics: Protobuf.DeviceMetrics.create(),
environmentMetrics: Protobuf.EnvironmentMetrics.create(),
});
}
}
})
);
},
addUser: (user) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
const node = device.nodes.find(
(node) => node.data.num === user.packet.from
);
if (node) {
node.data.user = user.data;
// if (action.payload.packet.rxTime) {
// node.data.lastHeard = new Date(
// action.payload.packet.rxTime * 1000,
// ).getTime();
// }
} else {
device.nodes.push({
data: Protobuf.NodeInfo.create({
num: user.packet.from,
snr: user.packet.rxSnr,
user: user.data,
}),
deviceMetrics: Protobuf.DeviceMetrics.create(),
environmentMetrics: Protobuf.EnvironmentMetrics.create(),
});
}
}
})
);
},
addPosition: (position) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
const node = device.nodes.find(
(node) => node.data.num === position.packet.from
);
if (node) {
node.data.position = position.data;
// if (action.payload.packet.rxTime) {
// node.data.lastHeard = new Date(
// action.payload.packet.rxTime * 1000,
// ).getTime();
// }
} else {
console.log("PUSH333333333333333333333");
device.nodes.push({
data: Protobuf.NodeInfo.create({
num: position.packet.from,
position: position.data,
}),
deviceMetrics: Protobuf.DeviceMetrics.create(),
environmentMetrics: Protobuf.EnvironmentMetrics.create(),
});
}
}
})
);
},
addConnection: (connection) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.connection = connection;
}
})
);
},
addMessage: (message) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
device.channels
.find(
(ch) => ch.config.index === message.message.packet.channel
)
?.messages.push(message);
}
})
);
},
ackMessage: (channelIndex: number, messageId: number) => {
set(
produce<DeviceState>((draft) => {
const device = draft.devices.get(id);
if (device) {
const channel = device.channels.find(
(ch) => ch.config.index === channelIndex
);
if (channel) {
const message = channel.messages.find(
(msg) => msg.message.packet.id === messageId
);
if (message) {
message.ack = true;
}
}
}
})
);
},
});
})
);
const device = get().devices.get(id);
if (!device) {
throw new Error("Device not found");
}
return device;
},
removeDevice: (id) =>
produce<DeviceState>((draft) => {
draft.devices.delete(id);
}),
getDevices: () => Array.from(get().devices.values()),
}));
export const DeviceContext = createContext<Device | undefined>(undefined);
export const useDevice = (): Device => {
const context = useContext(DeviceContext);
if (context === undefined) {
throw new Error("useDevice must be used within a ConnectionProvider");
}
return context;
};
+75
View File
@@ -0,0 +1,75 @@
import {
IConnection,
Protobuf,
SettingsManager,
Types,
} from "@meshtastic/meshtasticjs";
import type { Device } from "./stores/deviceStore.js";
export const subscribeAll = (device: Device, connection: IConnection) => {
SettingsManager.debugMode = Protobuf.LogRecord_Level.TRACE;
// onLogEvent
// onMeshHeartbeat
// onRoutingPacket
// onTelemetryPacket
connection.onRoutingPacket.subscribe((routingPacket) => {
console.log(routingPacket);
});
connection.onTelemetryPacket.subscribe((telemetryPacket) => {
device.setMetrics(telemetryPacket);
});
connection.onDeviceStatus.subscribe((status) => {
device.setStatus(status);
if (status === Types.DeviceStatusEnum.DEVICE_CONFIGURED) {
// device.setReady(true);
} else if (status === Types.DeviceStatusEnum.DEVICE_DISCONNECTED) {
device.setReady(false);
}
});
connection.onMyNodeInfo.subscribe((nodeInfo) => {
device.setHardware(nodeInfo);
});
connection.onUserPacket.subscribe((user) => {
device.addUser(user);
});
connection.onPositionPacket.subscribe((position) => {
device.addPosition(position);
});
connection.onNodeInfoPacket.subscribe((nodeInfo) => {
device.addNodeInfo(nodeInfo);
});
connection.onChannelPacket.subscribe((channel) => {
device.addChannel({
config: channel.data,
lastInterraction: new Date(),
messages: [],
});
});
connection.onConfigPacket.subscribe((config) => {
device.setConfig(config.data);
});
connection.onModuleConfigPacket.subscribe((moduleConfig) => {
device.setModuleConfig(moduleConfig.data);
});
connection.onTextPacket.subscribe((message) => {
device.addMessage({
message: message,
ack: message.packet.from !== device.hardware.myNodeNum,
received: message.packet.rxTime
? new Date(message.packet.rxTime * 1000)
: new Date(),
});
});
};
+1 -1
View File
@@ -1,6 +1,6 @@
export const fetcher = async <JSON>(
input: RequestInfo,
init?: RequestInit,
init?: RequestInit
): Promise<JSON> => {
const res = await fetch(input, init);
return res.json() as Promise<JSON>;
+3
View File
@@ -0,0 +1,3 @@
export const randId = () => {
return Math.floor(Math.random() * 1e9);
};
+15
View File
@@ -0,0 +1,15 @@
import type React from "react";
export const renderOptions = (enumValue: {
[s: string]: string | number;
}): JSX.Element[] => {
const optionsEnumValues = enumValue
? Object.entries(enumValue).filter((value) => typeof value[1] === "number")
: [];
return optionsEnumValues.map(([name, value], index) => (
<option key={index} value={value}>
{name}
</option>
));
};
+8
View File
@@ -0,0 +1,8 @@
import { LatLon } from "geodesy/mgrs.js";
export const toMGRS = (lat?: number, lng?: number) => {
if (lat && lng && lat !== 0 && lng !== 0) {
return new LatLon(lat / 1e7, lng / 1e7).toUtm().toMgrs().toString();
}
return "No Fix.";
};
+88
View File
@@ -0,0 +1,88 @@
export interface IAutor {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: false;
}
export interface IAsset {
url: string;
id: number;
node_id: string;
name: string;
label: string;
uploader: {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: false;
};
content_type: string;
state: string;
size: number;
download_count: 223;
created_at: string;
updated_at: string;
browser_download_url: string;
}
export interface IRelease {
url: string;
assets_url: string;
upload_url: string;
html_url: string;
id: number;
author: IAutor;
node_id: string;
tag_name: string;
target_commitish: string;
name: string;
draft: false;
prerelease: false;
created_at: string;
published_at: string;
assets: IAsset[];
tarball_url: string;
zipball_url: string;
body: string;
reactions: {
url: string;
total_count: number;
"+1": number;
"-1": number;
laugh: number;
hooray: 1;
confused: number;
heart: number;
rocket: number;
eyes: number;
};
}