mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #346 from meshtastic/2.1.6_Working_Changes
2.1.6 working changes
This commit is contained in:
commit
b25bc19957
31 changed files with 991 additions and 473 deletions
|
|
@ -127,6 +127,7 @@
|
|||
DDDE5A1129AFE69700490C6C /* MeshActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */; };
|
||||
DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; };
|
||||
DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; };
|
||||
DDDEE5E129DA3E1100A8E078 /* NodeInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */; };
|
||||
DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; };
|
||||
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; };
|
||||
DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; };
|
||||
|
|
@ -305,6 +306,8 @@
|
|||
DDDE5A0429AF163E00490C6C /* WidgetsExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetsExtension.entitlements; sourceTree = "<group>"; };
|
||||
DDDE5A0F29AFE69700490C6C /* MeshActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshActivityAttributes.swift; sourceTree = "<group>"; };
|
||||
DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoView.swift; sourceTree = "<group>"; };
|
||||
DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = "<group>"; };
|
||||
DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -632,6 +635,7 @@
|
|||
DDC2E18D26CE25CB0042C5E4 /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DDDEE5DF29DA3DA000A8E078 /* Node */,
|
||||
DD5E523D298F5A7D00D21B61 /* Weather */,
|
||||
DD47E3D526F17ED900029299 /* CircleText.swift */,
|
||||
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */,
|
||||
|
|
@ -689,6 +693,14 @@
|
|||
path = Widgets;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DDDEE5DF29DA3DA000A8E078 /* Node */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DDDEE5E029DA3E1100A8E078 /* NodeInfoView.swift */,
|
||||
);
|
||||
path = Node;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
|
@ -938,6 +950,7 @@
|
|||
DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */,
|
||||
DD47E3CE26F103C600029299 /* NodeList.swift in Sources */,
|
||||
DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */,
|
||||
DDDEE5E129DA3E1100A8E078 /* NodeInfoView.swift in Sources */,
|
||||
DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */,
|
||||
DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */,
|
||||
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */,
|
||||
|
|
@ -1199,7 +1212,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.1.4;
|
||||
MARKETING_VERSION = 2.1.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1233,7 +1246,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.1.4;
|
||||
MARKETING_VERSION = 2.1.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1480,6 +1493,7 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */,
|
||||
DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */,
|
||||
DDDD527729B5B83F0045BC3C /* MeshtasticDataModelV9.xcdatamodel */,
|
||||
DDBA45EC299ED78100DEEDDC /* MeshtasticDataModelV8.xcdatamodel */,
|
||||
|
|
@ -1491,7 +1505,7 @@
|
|||
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = DDC94FC329CED7280082EA6E /* MeshtasticDataModelV10.xcdatamodel */;
|
||||
currentVersion = DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */;
|
||||
name = Meshtastic.xcdatamodeld;
|
||||
path = Meshtastic/Meshtastic.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ extension Character {
|
|||
extension CLLocationCoordinate2D {
|
||||
/// Returns distance from coordianate in meters.
|
||||
/// - Parameter from: coordinate which will be used as end point.
|
||||
/// - Returns: Returns distance in meters.
|
||||
/// - Returns: distance in meters.
|
||||
func distance(from: CLLocationCoordinate2D) -> CLLocationDistance {
|
||||
let from = CLLocation(latitude: from.latitude, longitude: from.longitude)
|
||||
let to = CLLocation(latitude: self.latitude, longitude: self.longitude)
|
||||
|
|
@ -20,6 +20,36 @@ extension CLLocationCoordinate2D {
|
|||
}
|
||||
}
|
||||
|
||||
extension Color {
|
||||
/// Returns a boolean for a SwiftUI Color to determine what color of text to use
|
||||
/// - Returns: true if the color is light
|
||||
func isLight() -> Bool {
|
||||
guard let components = cgColor?.components, components.count > 2 else {return false}
|
||||
let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000
|
||||
return (brightness > 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
/// Returns a boolean for a UIColor to determine what color of text to use
|
||||
/// - Returns: true if the color is light
|
||||
func isLight() -> Bool {
|
||||
guard let components = cgColor.components, components.count > 2 else {return false}
|
||||
let brightness = ((components[0] * 299) + (components[1] * 587) + (components[2] * 114)) / 1000
|
||||
return (brightness > 0.5)
|
||||
}
|
||||
/// Returns a UIColor from a UInt32 value
|
||||
/// - Parameter hex: UInt32 value to convert to a color
|
||||
/// - Returns: UIColor
|
||||
convenience init(hex: UInt32) {
|
||||
let red = CGFloat((hex & 0xFF0000) >> 16)
|
||||
let green = CGFloat((hex & 0x00FF00) >> 8)
|
||||
let blue = CGFloat((hex & 0x0000FF))
|
||||
//print("\(red) - \(green) - \(blue)")
|
||||
self.init(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
var macAddressString: String {
|
||||
let mac: String = reduce("") {$0 + String(format: "%02x:", $1)}
|
||||
|
|
@ -135,3 +165,25 @@ extension String {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UserDefaults {
|
||||
|
||||
enum Keys: String, CaseIterable {
|
||||
case meshtasticUsername
|
||||
case preferredPeripheralId
|
||||
case provideLocation
|
||||
case provideLocationInterval
|
||||
case keyboardType
|
||||
case meshMapType
|
||||
case meshMapCenteringMode
|
||||
case meshMapRecentering
|
||||
case meshMapCustomTileServer
|
||||
case meshMapUserTrackingMode
|
||||
case meshMapShowNodeHistory
|
||||
case meshMapShowRouteLines
|
||||
}
|
||||
|
||||
func reset() {
|
||||
Keys.allCases.forEach { removeObject(forKey: $0.rawValue) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
let newNode = NodeInfoEntity(context: context)
|
||||
newNode.id = Int64(nodeInfo.num)
|
||||
newNode.num = Int64(nodeInfo.num)
|
||||
newNode.channel = Int32(channel)
|
||||
newNode.channel = Int32(nodeInfo.channel)
|
||||
|
||||
if nodeInfo.hasDeviceMetrics {
|
||||
let telemetry = TelemetryEntity(context: context)
|
||||
|
|
@ -321,7 +321,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
fetchedNode[0].num = Int64(nodeInfo.num)
|
||||
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard)))
|
||||
fetchedNode[0].snr = nodeInfo.snr
|
||||
fetchedNode[0].channel = Int32(channel)
|
||||
fetchedNode[0].channel = Int32(nodeInfo.channel)
|
||||
|
||||
if nodeInfo.hasUser {
|
||||
|
||||
|
|
@ -766,23 +766,25 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM
|
|||
guard let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as? [MyInfoEntity] else {
|
||||
return
|
||||
}
|
||||
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
|
||||
if channel.index == newMessage.channel {
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
}
|
||||
|
||||
if channel.index == newMessage.channel && !channel.mute {
|
||||
// Create an iOS Notification for the received private channel message and schedule it immediately
|
||||
let manager = LocalNotificationManager()
|
||||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")",
|
||||
content: messageText)
|
||||
]
|
||||
manager.schedule()
|
||||
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))")
|
||||
if !fetchedMyInfo.isEmpty {
|
||||
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
|
||||
if channel.index == newMessage.channel {
|
||||
context.refresh(channel, mergeChanges: true)
|
||||
}
|
||||
|
||||
if channel.index == newMessage.channel && !channel.mute {
|
||||
// Create an iOS Notification for the received private channel message and schedule it immediately
|
||||
let manager = LocalNotificationManager()
|
||||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")",
|
||||
content: messageText)
|
||||
]
|
||||
manager.schedule()
|
||||
print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? NSLocalizedString("unknown", comment: "Unknown"))")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV10.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV11.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,332 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E252" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="bluetoothConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="bluetoothConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="CannedMessageConfigEntity" representedClassName="CannedMessageConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCcw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventPress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinA" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinB" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinPress" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="messages" optional="YES" attributeType="String" minValueString="0" maxValueString="198"/>
|
||||
<attribute name="rotary1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="updown1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="cannedMessagesConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="cannedMessageConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ChannelEntity" representedClassName="ChannelEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="downlinkEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="index" attributeType="Integer 32" minValueString="0" maxValueString="13" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="psk" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uplinkEnabled" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
|
||||
<fetchedProperty name="allPrivateMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="channel == $FETCH_SOURCE.index && toUser == nil AND isEmoji == false"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="index"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="nodeInfoBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hwModel" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="latitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
|
||||
</entity>
|
||||
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideDutyCycle" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideFrequency" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sx126xRxBoostedGain" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePreset" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="loRaConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="loRaConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ackError" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackSNR" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="admin" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="adminDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="messagePayload" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="messagePayloadMarkdown" optional="YES" attributeType="String"/>
|
||||
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
|
||||
<fetchedProperty name="tapbacks" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isEmoji == true"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="messageId"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="bitrate" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="errorCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="firmwareVersion" attributeType="String"/>
|
||||
<attribute name="hasGps" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="maxChannels" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="messageTimeoutMsec" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
|
||||
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="myNodeNum"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ntpServer" optional="YES" attributeType="String"/>
|
||||
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
|
||||
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
|
||||
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
|
||||
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
|
||||
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
|
||||
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
|
||||
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
|
||||
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
|
||||
<relationship name="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
|
||||
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
|
||||
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
|
||||
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
|
||||
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
|
||||
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
|
||||
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
|
||||
<relationship name="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
|
||||
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
|
||||
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
|
||||
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
|
||||
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="num"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RouteEntity" representedClassName="RouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="color" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="notes" optional="YES" attributeType="String"/>
|
||||
<relationship name="locations" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
|
||||
</entity>
|
||||
<entity name="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
|
||||
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hwModel" attributeType="String"/>
|
||||
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="longName" attributeType="String"/>
|
||||
<attribute name="macaddr" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="shortName" attributeType="String"/>
|
||||
<attribute name="userId" attributeType="String"/>
|
||||
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
|
||||
<fetchedProperty name="adminMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND isEmoji == false AND admin = true"/>
|
||||
</fetchedProperty>
|
||||
<fetchedProperty name="allMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false"/>
|
||||
</fetchedProperty>
|
||||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -117,7 +117,11 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
newNode.num = Int64(packet.from)
|
||||
newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
|
||||
newNode.snr = packet.rxSnr
|
||||
newNode.channel = Int32(packet.channel)
|
||||
|
||||
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
|
||||
newNode.channel = Int32(nodeInfoMessage.channel)
|
||||
}
|
||||
|
||||
if let newUserMessage = try? User(serializedData: packet.decoded.payload) {
|
||||
let newUser = UserEntity(context: context)
|
||||
newUser.userId = newUserMessage.id
|
||||
|
|
@ -134,9 +138,10 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
fetchedNode[0].num = Int64(packet.from)
|
||||
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
|
||||
fetchedNode[0].snr = packet.rxSnr
|
||||
fetchedNode[0].channel = Int32(packet.channel)
|
||||
|
||||
if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) {
|
||||
|
||||
fetchedNode[0].channel = Int32(nodeInfoMessage.channel)
|
||||
if nodeInfoMessage.hasDeviceMetrics {
|
||||
let telemetry = TelemetryEntity(context: context)
|
||||
telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel)
|
||||
|
|
|
|||
|
|
@ -21,11 +21,9 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP
|
|||
}
|
||||
|
||||
///
|
||||
/// Full settings (center freq, spread factor, pre-shared secret key etc...)
|
||||
/// needed to configure a radio for speaking on a particular channel This
|
||||
/// information can be encoded as a QRcode/url so that other users can configure
|
||||
/// This information can be encoded as a QRcode/url so that other users can configure
|
||||
/// their radio to join the same channel.
|
||||
/// A note about how channel names are shown to users: channelname-Xy
|
||||
/// A note about how channel names are shown to users: channelname-X
|
||||
/// poundsymbol is a prefix used to indicate this is a channel name (idea from @professr).
|
||||
/// Where X is a letter from A-Z (base 26) representing a hash of the PSK for this
|
||||
/// channel - so that if the user changes anything about the channel (which does
|
||||
|
|
@ -35,8 +33,6 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP
|
|||
/// The PSK is hashed into this letter by "0x41 + [xor all bytes of the psk ] modulo 26"
|
||||
/// This also allows the option of someday if people have the PSK off (zero), the
|
||||
/// users COULD type in a channel name and be able to talk.
|
||||
/// Y is a lower case letter from a-z that represents the channel 'speed' settings
|
||||
/// (for some future definition of speed)
|
||||
/// FIXME: Add description of multi-channel support and how primary vs secondary channels are used.
|
||||
/// FIXME: explain how apps use channels for security.
|
||||
/// explain how remote settings and remote gpio are managed as an example
|
||||
|
|
@ -57,7 +53,7 @@ struct ChannelSettings {
|
|||
/// because they are listed in this source code.
|
||||
/// Those bytes are mapped using the following scheme:
|
||||
/// `0` = No crypto
|
||||
/// `1` = The special "default" channel key: {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf}
|
||||
/// `1` = The special "default" channel key: {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01}
|
||||
/// `2` through 10 = The default channel key, except with 1 through 9 added to the last byte.
|
||||
/// Shown to user as simple1 through 10
|
||||
var psk: Data = Data()
|
||||
|
|
|
|||
|
|
@ -177,6 +177,10 @@ struct Config {
|
|||
/// Defaults to 900 Seconds (15 minutes)
|
||||
var nodeInfoBroadcastSecs: UInt32 = 0
|
||||
|
||||
///
|
||||
/// Treat double tap interrupt on supported accelerometers as a button press if set to true
|
||||
var doubleTapAsButtonPress: Bool = false
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
///
|
||||
|
|
@ -1587,6 +1591,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
|
|||
5: .standard(proto: "buzzer_gpio"),
|
||||
6: .standard(proto: "rebroadcast_mode"),
|
||||
7: .standard(proto: "node_info_broadcast_secs"),
|
||||
8: .standard(proto: "double_tap_as_button_press"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -1602,6 +1607,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
|
|||
case 5: try { try decoder.decodeSingularUInt32Field(value: &self.buzzerGpio) }()
|
||||
case 6: try { try decoder.decodeSingularEnumField(value: &self.rebroadcastMode) }()
|
||||
case 7: try { try decoder.decodeSingularUInt32Field(value: &self.nodeInfoBroadcastSecs) }()
|
||||
case 8: try { try decoder.decodeSingularBoolField(value: &self.doubleTapAsButtonPress) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -1629,6 +1635,9 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
|
|||
if self.nodeInfoBroadcastSecs != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.nodeInfoBroadcastSecs, fieldNumber: 7)
|
||||
}
|
||||
if self.doubleTapAsButtonPress != false {
|
||||
try visitor.visitSingularBoolField(value: self.doubleTapAsButtonPress, fieldNumber: 8)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -1640,6 +1649,7 @@ extension Config.DeviceConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImpl
|
|||
if lhs.buzzerGpio != rhs.buzzerGpio {return false}
|
||||
if lhs.rebroadcastMode != rhs.rebroadcastMode {return false}
|
||||
if lhs.nodeInfoBroadcastSecs != rhs.nodeInfoBroadcastSecs {return false}
|
||||
if lhs.doubleTapAsButtonPress != rhs.doubleTapAsButtonPress {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -242,6 +242,10 @@ struct ModuleConfig {
|
|||
/// Whether to send / consume json packets on MQTT
|
||||
var jsonEnabled: Bool = false
|
||||
|
||||
///
|
||||
/// If true, we attempt to establish a secure connection using TLS
|
||||
var tlsEnabled: Bool = false
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
|
@ -1109,6 +1113,7 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message
|
|||
4: .same(proto: "password"),
|
||||
5: .standard(proto: "encryption_enabled"),
|
||||
6: .standard(proto: "json_enabled"),
|
||||
7: .standard(proto: "tls_enabled"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -1123,6 +1128,7 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message
|
|||
case 4: try { try decoder.decodeSingularStringField(value: &self.password) }()
|
||||
case 5: try { try decoder.decodeSingularBoolField(value: &self.encryptionEnabled) }()
|
||||
case 6: try { try decoder.decodeSingularBoolField(value: &self.jsonEnabled) }()
|
||||
case 7: try { try decoder.decodeSingularBoolField(value: &self.tlsEnabled) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -1147,6 +1153,9 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message
|
|||
if self.jsonEnabled != false {
|
||||
try visitor.visitSingularBoolField(value: self.jsonEnabled, fieldNumber: 6)
|
||||
}
|
||||
if self.tlsEnabled != false {
|
||||
try visitor.visitSingularBoolField(value: self.tlsEnabled, fieldNumber: 7)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -1157,6 +1166,7 @@ extension ModuleConfig.MQTTConfig: SwiftProtobuf.Message, SwiftProtobuf._Message
|
|||
if lhs.password != rhs.password {return false}
|
||||
if lhs.encryptionEnabled != rhs.encryptionEnabled {return false}
|
||||
if lhs.jsonEnabled != rhs.jsonEnabled {return false}
|
||||
if lhs.tlsEnabled != rhs.tlsEnabled {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ struct CircleText: View {
|
|||
var circleSize: CGFloat? = 60
|
||||
var fontSize: CGFloat? = 20
|
||||
var brightness: Double? = 0
|
||||
var textColor: Color? = .white
|
||||
|
||||
var body: some View {
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ struct CircleText: View {
|
|||
.fill(color)
|
||||
.brightness(brightness ?? 0)
|
||||
.frame(width: circleSize, height: circleSize)
|
||||
Text(text).textCase(.uppercase).font(font).foregroundColor(.white).fixedSize()
|
||||
Text(text).textCase(.uppercase).font(font).foregroundColor(textColor).fixedSize()
|
||||
.frame(width: circleSize, height: circleSize, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/).offset(x: 0, y: 0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ struct LastHeardText: View {
|
|||
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
|
||||
var body: some View {
|
||||
if lastHeard != nil && lastHeard! >= sixMonthsAgo! {
|
||||
Text("heard")+Text(": \(lastHeard!, style: .relative) ")+Text("ago")
|
||||
Text("heard")+Text(" \(lastHeard!, style: .relative) ")+Text("ago")
|
||||
} else {
|
||||
Text("unknown.age")
|
||||
}
|
||||
|
|
|
|||
285
Meshtastic/Views/Helpers/Node/NodeInfoView.swift
Normal file
285
Meshtastic/Views/Helpers/Node/NodeInfoView.swift
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
//
|
||||
// NodeInfoView.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 4/2/23.
|
||||
//
|
||||
|
||||
//
|
||||
// DistanceText.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 8/19/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
|
||||
struct NodeInfoView: View {
|
||||
|
||||
var node: NodeInfoEntity
|
||||
|
||||
var body: some View {
|
||||
|
||||
let hwModelString = node.user?.hwModel ?? "UNSET"
|
||||
|
||||
Divider()
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
HStack {
|
||||
VStack(alignment: .center) {
|
||||
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 75, fontSize: 24, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white )
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
if node.user != nil {
|
||||
Image(hwModelString)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(5)
|
||||
|
||||
Text(String(hwModelString))
|
||||
.foregroundColor(.gray)
|
||||
.font(.largeTitle).fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
if node.snr > 0 {
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
|
||||
Image(systemName: "waveform.path")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.padding(.bottom, 10)
|
||||
Text("SNR").font(.largeTitle).fixedSize()
|
||||
Text("\(String(format: "%.2f", node.snr)) dB")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
|
||||
if deviceMetrics?.count ?? 0 >= 1 {
|
||||
|
||||
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0))
|
||||
if mostRecent?.voltage ?? 0 > 0.0 {
|
||||
|
||||
Text(String(format: "%.2f", mostRecent?.voltage ?? 0.0) + " V")
|
||||
.font(.title)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
Divider()
|
||||
HStack(alignment: .center) {
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "person")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("user").font(.title)+Text(":").font(.title)
|
||||
}
|
||||
Text("!\(String(format: "%02x", node.num))")
|
||||
.font(.title).foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "number")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("Node Number:").font(.title)
|
||||
}
|
||||
Text(String(node.num)).font(.title).foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "globe")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("MAC Address: ").font(.title)
|
||||
|
||||
}
|
||||
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address"))
|
||||
.font(.title)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "clock.badge.checkmark.fill")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("heard.last").font(.title)+Text(":").font(.title)
|
||||
|
||||
}
|
||||
DateTimeText(dateTime: node.lastHeard)
|
||||
.font(.title3)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
|
||||
} else {
|
||||
|
||||
HStack {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: 20, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white )
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
if node.user != nil {
|
||||
Image(node.user!.hwModel ?? NSLocalizedString("unset", comment: "Unset"))
|
||||
.resizable()
|
||||
.frame(width: 75, height: 75)
|
||||
.cornerRadius(5)
|
||||
Text(String(node.user!.hwModel ?? NSLocalizedString("unset", comment: "Unset")))
|
||||
.font(.callout).fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
if node.snr > 0 {
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
|
||||
Image(systemName: "waveform.path")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("SNR").font(.title2).fixedSize()
|
||||
Text("\(String(format: "%.2f", node.snr)) dB")
|
||||
.font(.title2)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
|
||||
if deviceMetrics?.count ?? 0 >= 1 {
|
||||
|
||||
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0))
|
||||
if mostRecent?.voltage ?? 0 > 0 {
|
||||
|
||||
Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
HStack(alignment: .center) {
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "person")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("User Id:").font(.title2)
|
||||
}
|
||||
Text(node.user?.userId ?? "??????").font(.title3).foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "number")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("Node Number:").font(.title2)
|
||||
}
|
||||
Text(String(node.num)).font(.title3).foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
HStack {
|
||||
Image(systemName: "globe")
|
||||
.font(.headline)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("MAC Address: ")
|
||||
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray)
|
||||
}
|
||||
.padding([.bottom], 10)
|
||||
Divider()
|
||||
}
|
||||
|
||||
VStack {
|
||||
|
||||
if (node.positions?.count ?? 0) > 0 {
|
||||
|
||||
NavigationLink {
|
||||
PositionLog(node: node)
|
||||
} label: {
|
||||
|
||||
Image(systemName: "building.columns")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
Text("Position Log")
|
||||
.font(.title3)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Divider()
|
||||
}
|
||||
|
||||
if (node.telemetries?.count ?? 0) > 0 {
|
||||
|
||||
NavigationLink {
|
||||
DeviceMetricsLog(node: node)
|
||||
} label: {
|
||||
|
||||
Image(systemName: "flipphone")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
Text("Device Metrics Log")
|
||||
.font(.title3)
|
||||
}
|
||||
Divider()
|
||||
NavigationLink {
|
||||
EnvironmentMetricsLog(node: node)
|
||||
} label: {
|
||||
|
||||
Image(systemName: "chart.xyaxis.line")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
Text("Environment Metrics Log")
|
||||
.font(.title3)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
struct NodeInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
|
||||
VStack {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,8 +16,6 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
var onLongPress: (_ waypointCoordinate: CLLocationCoordinate2D) -> Void
|
||||
var onWaypointEdit: (_ waypointId: Int ) -> Void
|
||||
let mapView = MKMapView()
|
||||
let lineColors: [UIColor] = [UIColor.systemIndigo, UIColor.yellow, UIColor.white, UIColor.red, UIColor.purple, UIColor.orange, UIColor.magenta, UIColor.lightGray, UIColor.green, UIColor.gray, UIColor.systemMint, UIColor.darkGray, UIColor.cyan, UIColor.brown, UIColor.blue, UIColor.black, UIColor.systemPink,
|
||||
UIColor.systemTeal]
|
||||
// Parameters
|
||||
let positions: [PositionEntity]
|
||||
let waypoints: [WaypointEntity]
|
||||
|
|
@ -142,7 +140,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
return position.nodeCoordinate!
|
||||
})
|
||||
let polyline = MKPolyline(coordinates: lineCoords, count: nodePositions.count)
|
||||
polyline.title = "\(String(position.nodePosition?.num ?? 0))-\(String(lineIndex))"
|
||||
polyline.title = "\(String(position.nodePosition?.num ?? 0))"
|
||||
mapView.addOverlay(polyline)
|
||||
lineIndex += 1
|
||||
// There are 18 colors for lines, start over if we are at index 17
|
||||
|
|
@ -199,7 +197,7 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
annotationView.displayPriority = .required
|
||||
annotationView.titleVisibility = .visible
|
||||
} else {
|
||||
annotationView.markerTintColor = UIColor(.indigo)
|
||||
annotationView.markerTintColor = UIColor(hex: UInt32(positionAnnotation.nodePosition?.num ?? 0))
|
||||
annotationView.displayPriority = .defaultHigh
|
||||
annotationView.titleVisibility = .adaptive
|
||||
}
|
||||
|
|
@ -351,10 +349,10 @@ struct MapViewSwiftUI: UIViewRepresentable {
|
|||
} else {
|
||||
if let routePolyline = overlay as? MKPolyline {
|
||||
|
||||
let titleString = routePolyline.title ?? "None-0"
|
||||
let titleString = routePolyline.title ?? "0"
|
||||
let index = Int(titleString.components(separatedBy: "-").last ?? "0")
|
||||
let renderer = MKPolylineRenderer(polyline: routePolyline)
|
||||
renderer.strokeColor = parent.lineColors[index ?? 0]
|
||||
renderer.strokeColor = UIColor(hex: UInt32(titleString) ?? 0)
|
||||
renderer.lineWidth = 8
|
||||
return renderer
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ struct ChannelMessageList: View {
|
|||
HStack(alignment: .top) {
|
||||
if currentUser { Spacer(minLength: 50) }
|
||||
if !currentUser {
|
||||
CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.gray), circleSize: 44, fontSize: 14)
|
||||
CircleText(text: message.fromUser?.shortName ?? "????", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44, fontSize: 14, textColor: UIColor(hex: UInt32(message.fromUser?.num ?? 0)).isLight() ? .black : .white)
|
||||
.padding(.all, 5)
|
||||
.offset(y: -5)
|
||||
}
|
||||
|
|
@ -233,16 +233,29 @@ struct ChannelMessageList: View {
|
|||
#if targetEnvironment(macCatalyst)
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
let bell = "🔔 Alert Bell Character! \u{7}"
|
||||
print(bell)
|
||||
typingMessage += bell
|
||||
|
||||
} label: {
|
||||
Text("Alert Bell")
|
||||
Image(systemName: "bell.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
if userSettings.meshtasticUsername.count > 0 {
|
||||
|
||||
typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName
|
||||
typingMessage += "📍 " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName
|
||||
|
||||
} else {
|
||||
|
||||
typingMessage = "📍 " + userLongName + " has shared their position with you."
|
||||
typingMessage += "📍 " + userLongName + " has shared their position with you."
|
||||
}
|
||||
|
||||
} label: {
|
||||
|
|
@ -285,6 +298,18 @@ struct ChannelMessageList: View {
|
|||
}
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
Button {
|
||||
let bell = "🔔 Alert Bell Character! \u{7}"
|
||||
print(bell)
|
||||
typingMessage += bell
|
||||
|
||||
} label: {
|
||||
Text("Alert")
|
||||
Image(systemName: "bell.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ struct Contacts: View {
|
|||
let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
CircleText(text: String(channel.index), color: .accentColor, circleSize: 52, fontSize: 40, brightness: 0.1)
|
||||
CircleText(text: String(channel.index), color: .accentColor, circleSize: 60, fontSize: 42, brightness: 0.1)
|
||||
.padding(.trailing, 5)
|
||||
VStack {
|
||||
HStack {
|
||||
|
|
@ -150,7 +150,7 @@ struct Contacts: View {
|
|||
HStack {
|
||||
VStack {
|
||||
HStack {
|
||||
CircleText(text: user.shortName ?? "???", color: .accentColor, circleSize: 52, fontSize: 16, brightness: 0.1)
|
||||
CircleText(text: user.shortName ?? "???", color: Color(UIColor(hex: UInt32(user.num))), circleSize: 60, fontSize: 18, textColor: UIColor(hex: UInt32(user.num)).isLight() ? .black : .white)
|
||||
.padding(.trailing, 5)
|
||||
VStack {
|
||||
HStack {
|
||||
|
|
|
|||
|
|
@ -57,11 +57,6 @@ struct UserMessageList: View {
|
|||
}
|
||||
HStack(alignment: .top) {
|
||||
if currentUser { Spacer(minLength: 50) }
|
||||
if !currentUser {
|
||||
CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.gray), circleSize: 44, fontSize: 14)
|
||||
.padding(.all, 5)
|
||||
.offset(y: -5)
|
||||
}
|
||||
VStack(alignment: currentUser ? .trailing : .leading) {
|
||||
let markdownText: LocalizedStringKey = LocalizedStringKey.init(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE"))
|
||||
|
||||
|
|
@ -360,7 +355,7 @@ struct UserMessageList: View {
|
|||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
CircleText(text: user.shortName ?? "???", color: .accentColor, circleSize: 44, fontSize: 14).fixedSize()
|
||||
CircleText(text: user.shortName ?? "???", color: Color(UIColor(hex: UInt32(user.num))), circleSize: 44, fontSize: 14, textColor: UIColor(hex: UInt32(user.num)).isLight() ? .black : .white ).fixedSize()
|
||||
Text(user.longName ?? NSLocalizedString("unknown", comment: "Unknown")).font(.headline)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,75 +8,114 @@ import SwiftUI
|
|||
import Charts
|
||||
|
||||
struct DeviceMetricsLog: View {
|
||||
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
|
||||
@State private var isPresentingClearLogConfirm: Bool = false
|
||||
@State var isExporting = false
|
||||
@State var exportString = ""
|
||||
|
||||
@State private var batteryChartColor: Color = .blue
|
||||
@State private var airtimeChartColor: Color = .orange
|
||||
@State private var channelUtilizationChartColor: Color = .green
|
||||
var node: NodeInfoEntity
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date())
|
||||
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).reversed() as? [TelemetryEntity] ?? []
|
||||
let chartData = deviceMetrics
|
||||
.filter { $0.time != nil && $0.time! >= oneDayAgo! }
|
||||
.sorted { $0.time! < $1.time! }
|
||||
|
||||
NavigationStack {
|
||||
let oneDayAgo = Calendar.current.date(byAdding: .day, value: -3, to: Date())
|
||||
let data = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0 && time !=nil && time >= %@", oneDayAgo! as CVarArg)) ?? []
|
||||
if data.count > 0 {
|
||||
GroupBox(label: Label("battery.level.trend", systemImage: "battery.100")) {
|
||||
Chart(data.array as? [TelemetryEntity] ?? [], id: \.self) {
|
||||
LineMark(
|
||||
x: .value("Hour", $0.time!.formattedDate(format: "ha")),
|
||||
y: .value("Value", $0.batteryLevel)
|
||||
)
|
||||
PointMark(
|
||||
x: .value("Hour", $0.time!.formattedDate(format: "ha")),
|
||||
y: .value("Value", $0.batteryLevel)
|
||||
)
|
||||
|
||||
if chartData.count > 0 {
|
||||
GroupBox(label: Label("\(deviceMetrics.count) Readings Total", systemImage: "chart.xyaxis.line")) {
|
||||
|
||||
Chart {
|
||||
|
||||
ForEach(chartData, id: \.self) { point in
|
||||
|
||||
Plot {
|
||||
LineMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.batteryLevel)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.batteryLevel)")
|
||||
.foregroundStyle(batteryChartColor)
|
||||
.interpolationMethod(.cardinal)
|
||||
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.channelUtilization)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.channelUtilization)")
|
||||
.foregroundStyle(channelUtilizationChartColor)
|
||||
|
||||
RuleMark(y: .value("Limit", 10))
|
||||
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 10]))
|
||||
.foregroundStyle(airtimeChartColor)
|
||||
|
||||
Plot {
|
||||
PointMark(
|
||||
x: .value("x", point.time!),
|
||||
y: .value("y", point.airUtilTx)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("Line Series")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.airUtilTx)")
|
||||
.foregroundStyle(airtimeChartColor)
|
||||
}
|
||||
}
|
||||
.frame(height: 150)
|
||||
.chartXAxis(content: {
|
||||
AxisMarks(position: .top)
|
||||
})
|
||||
.chartXAxis(.automatic)
|
||||
.chartForegroundStyleScale([
|
||||
"Battery Level" : .blue,
|
||||
"Channel Utilization": .green,
|
||||
"Airtime": .orange
|
||||
])
|
||||
.chartLegend(position: .automatic, alignment: .bottom)
|
||||
}
|
||||
.frame(minHeight: 250)
|
||||
}
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
|
||||
// Add a table for mac and ipad
|
||||
Table(node.telemetries?.reversed() as? [TelemetryEntity] ?? []) {
|
||||
|
||||
//Table(Array(deviceMetrics),id: \.self) {
|
||||
Table(deviceMetrics) {
|
||||
TableColumn("battery.level") { dm in
|
||||
if dm.metricsType == 0 {
|
||||
if dm.batteryLevel == 0 {
|
||||
Text("Powered")
|
||||
} else {
|
||||
|
||||
Text("\(String(dm.batteryLevel))%")
|
||||
}
|
||||
if dm.batteryLevel > 100 {
|
||||
Text("Powered")
|
||||
} else {
|
||||
Text("\(String(dm.batteryLevel))%")
|
||||
}
|
||||
}
|
||||
TableColumn("voltage") { dm in
|
||||
if dm.metricsType == 0 {
|
||||
Text("\(String(format: "%.2f", dm.voltage))")
|
||||
}
|
||||
Text("\(String(format: "%.2f", dm.voltage))")
|
||||
}
|
||||
TableColumn("channel.utilization") { dm in
|
||||
if dm.metricsType == 0 {
|
||||
Text(String(format: "%.2f", dm.channelUtilization))
|
||||
}
|
||||
Text(String(format: "%.2f", dm.channelUtilization))
|
||||
}
|
||||
TableColumn("airtime") { dm in
|
||||
if dm.metricsType == 0 {
|
||||
Text("\(String(format: "%.2f", dm.airUtilTx))%")
|
||||
}
|
||||
Text("\(String(format: "%.2f", dm.airUtilTx))%")
|
||||
}
|
||||
TableColumn("timestamp") { dm in
|
||||
if dm.metricsType == 0 {
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
|
||||
}
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
ScrollView {
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1),
|
||||
GridItem(.flexible(minimum: 30, maximum: 60), spacing: 0.1),
|
||||
|
|
@ -102,26 +141,23 @@ struct DeviceMetricsLog: View {
|
|||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
ForEach(node.telemetries?.reversed() as? [TelemetryEntity] ?? [], id: \.self) { (dm: TelemetryEntity) in
|
||||
if dm.metricsType == 0 {
|
||||
GridRow {
|
||||
if dm.batteryLevel == 111 {
|
||||
Text("USB")
|
||||
.font(.caption)
|
||||
} else {
|
||||
Text("\(String(dm.batteryLevel))%")
|
||||
.font(.caption)
|
||||
}
|
||||
Text(String(dm.voltage))
|
||||
ForEach(deviceMetrics) { dm in
|
||||
GridRow {
|
||||
if dm.batteryLevel > 100 {
|
||||
Text("PWD")
|
||||
.font(.caption)
|
||||
Text("\(String(format: "%.2f", dm.channelUtilization))%")
|
||||
} else {
|
||||
Text("\(String(dm.batteryLevel))%")
|
||||
.font(.caption)
|
||||
Text("\(String(format: "%.2f", dm.airUtilTx))%")
|
||||
.font(.caption)
|
||||
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown time")
|
||||
.font(.caption2)
|
||||
}
|
||||
Text(String(dm.voltage))
|
||||
.font(.caption)
|
||||
Text("\(String(format: "%.2f", dm.channelUtilization))%")
|
||||
.font(.caption)
|
||||
Text("\(String(format: "%.2f", dm.airUtilTx))%")
|
||||
.font(.caption)
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown time")
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -182,7 +218,6 @@ struct DeviceMetricsLog: View {
|
|||
if case .success = result {
|
||||
print("Device metrics log download succeeded.")
|
||||
self.isExporting = false
|
||||
|
||||
} else {
|
||||
print("Device metrics log download failed: \(result).")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,47 +19,36 @@ struct EnvironmentMetricsLog: View {
|
|||
var node: NodeInfoEntity
|
||||
|
||||
var body: some View {
|
||||
|
||||
let environmentMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).reversed() as? [TelemetryEntity] ?? []
|
||||
|
||||
NavigationStack {
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current)
|
||||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
|
||||
Text("\(environmentMetrics.count) Readings")
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
// Add a table for mac and ipad
|
||||
Table(node.telemetries!.reversed() as! [TelemetryEntity]) {
|
||||
Table(environmentMetrics) {
|
||||
TableColumn("Temperature") { em in
|
||||
if em.metricsType == 1 {
|
||||
Text(em.temperature.formattedTemperature())
|
||||
}
|
||||
Text(em.temperature.formattedTemperature())
|
||||
}
|
||||
TableColumn("Humidity") { em in
|
||||
if em.metricsType == 1 {
|
||||
Text("\(String(format: "%.2f", em.relativeHumidity))%")
|
||||
}
|
||||
Text("\(String(format: "%.2f", em.relativeHumidity))%")
|
||||
}
|
||||
TableColumn("Barometric Pressure") { em in
|
||||
if em.metricsType == 1 {
|
||||
Text("\(String(format: "%.2f", em.barometricPressure)) hPa")
|
||||
}
|
||||
Text("\(String(format: "%.2f", em.barometricPressure)) hPa")
|
||||
}
|
||||
TableColumn("gas.resistance") { em in
|
||||
if em.metricsType == 1 {
|
||||
Text("\(String(format: "%.2f", em.gasResistance)) ohms")
|
||||
}
|
||||
Text("\(String(format: "%.2f", em.gasResistance)) ohms")
|
||||
}
|
||||
TableColumn("current") { em in
|
||||
if em.metricsType == 1 {
|
||||
Text("\(String(format: "%.2f", em.current))")
|
||||
}
|
||||
Text("\(String(format: "%.2f", em.current))")
|
||||
}
|
||||
TableColumn("voltage") { em in
|
||||
if em.metricsType == 1 {
|
||||
Text("\(String(format: "%.2f", em.voltage))")
|
||||
}
|
||||
Text("\(String(format: "%.2f", em.voltage))")
|
||||
}
|
||||
TableColumn("timestamp") { em in
|
||||
if em.metricsType == 1 {
|
||||
Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
|
||||
}
|
||||
Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -92,21 +81,18 @@ struct EnvironmentMetricsLog: View {
|
|||
}
|
||||
ForEach(node.telemetries?.reversed() as? [TelemetryEntity] ?? [], id: \.self) { (em: TelemetryEntity) in
|
||||
|
||||
if em.metricsType == 1 {
|
||||
GridRow {
|
||||
|
||||
GridRow {
|
||||
|
||||
Text(em.temperature.formattedTemperature())
|
||||
.font(.caption)
|
||||
Text("\(String(format: "%.2f", em.relativeHumidity))%")
|
||||
.font(.caption)
|
||||
Text("\(String(format: "%.2f", em.barometricPressure))")
|
||||
.font(.caption)
|
||||
Text("\(String(format: "%.2f", em.gasResistance))")
|
||||
.font(.caption)
|
||||
Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
|
||||
.font(.caption2)
|
||||
}
|
||||
Text(em.temperature.formattedTemperature())
|
||||
.font(.caption)
|
||||
Text("\(String(format: "%.2f", em.relativeHumidity))%")
|
||||
.font(.caption)
|
||||
Text("\(String(format: "%.2f", em.barometricPressure))")
|
||||
.font(.caption)
|
||||
Text("\(String(format: "%.2f", em.gasResistance))")
|
||||
.font(.caption)
|
||||
Text(em.time?.formattedDate(format: dateFormatString) ?? NSLocalizedString("unknown.age", comment: ""))
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,257 +140,8 @@ struct NodeDetail: View {
|
|||
.padding([.top], 20)
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Divider()
|
||||
if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac {
|
||||
HStack {
|
||||
VStack(alignment: .center) {
|
||||
CircleText(text: node.user?.shortName ?? "???", color: .accentColor, circleSize: 75, fontSize: 26)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
if node.user != nil {
|
||||
Image(hwModelString)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(5)
|
||||
|
||||
Text(String(hwModelString))
|
||||
.foregroundColor(.gray)
|
||||
.font(.largeTitle).fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
if node.snr > 0 {
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
|
||||
Image(systemName: "waveform.path")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.padding(.bottom, 10)
|
||||
Text("SNR").font(.largeTitle).fixedSize()
|
||||
Text("\(String(format: "%.2f", node.snr)) dB")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
|
||||
if deviceMetrics?.count ?? 0 >= 1 {
|
||||
|
||||
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0))
|
||||
if mostRecent?.voltage ?? 0 > 0.0 {
|
||||
|
||||
Text(String(format: "%.2f", mostRecent?.voltage ?? 0.0) + " V")
|
||||
.font(.title)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
Divider()
|
||||
HStack(alignment: .center) {
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "person")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("user").font(.title)+Text(":").font(.title)
|
||||
}
|
||||
Text("!\(String(format: "%02x", node.num))")
|
||||
.font(.title).foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "number")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("Node Number:").font(.title)
|
||||
}
|
||||
Text(String(node.num)).font(.title).foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "globe")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("MAC Address: ").font(.title)
|
||||
|
||||
}
|
||||
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address"))
|
||||
.font(.title)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "clock.badge.checkmark.fill")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("heard.last").font(.title)+Text(":").font(.title)
|
||||
|
||||
}
|
||||
DateTimeText(dateTime: node.lastHeard)
|
||||
.font(.title3)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
|
||||
} else {
|
||||
|
||||
HStack {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
CircleText(text: node.user?.shortName ?? "???", color: .accentColor)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
if node.user != nil {
|
||||
Image(node.user!.hwModel ?? NSLocalizedString("unset", comment: "Unset"))
|
||||
.resizable()
|
||||
.frame(width: 75, height: 75)
|
||||
.cornerRadius(5)
|
||||
Text(String(node.user!.hwModel ?? NSLocalizedString("unset", comment: "Unset")))
|
||||
.font(.callout).fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
if node.snr > 0 {
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
|
||||
Image(systemName: "waveform.path")
|
||||
.font(.title)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("SNR").font(.title2).fixedSize()
|
||||
Text("\(String(format: "%.2f", node.snr)) dB")
|
||||
.font(.title2)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
|
||||
let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
|
||||
if deviceMetrics?.count ?? 0 >= 1 {
|
||||
|
||||
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
|
||||
Divider()
|
||||
VStack(alignment: .center) {
|
||||
BatteryGauge(batteryLevel: Double(mostRecent?.batteryLevel ?? 0))
|
||||
if mostRecent?.voltage ?? 0 > 0 {
|
||||
|
||||
Text(String(format: "%.2f", mostRecent?.voltage ?? 0) + " V")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
HStack(alignment: .center) {
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "person")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("User Id:").font(.title2)
|
||||
}
|
||||
Text(node.user?.userId ?? "??????").font(.title3).foregroundColor(.gray)
|
||||
}
|
||||
Divider()
|
||||
VStack {
|
||||
HStack {
|
||||
Image(systemName: "number")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("Node Number:").font(.title2)
|
||||
}
|
||||
Text(String(node.num)).font(.title3).foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
HStack {
|
||||
Image(systemName: "globe")
|
||||
.font(.headline)
|
||||
.foregroundColor(.accentColor)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("MAC Address: ")
|
||||
Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray)
|
||||
}
|
||||
.padding([.bottom], 10)
|
||||
Divider()
|
||||
}
|
||||
|
||||
VStack {
|
||||
|
||||
if (node.positions?.count ?? 0) > 0 {
|
||||
|
||||
NavigationLink {
|
||||
PositionLog(node: node)
|
||||
} label: {
|
||||
|
||||
Image(systemName: "building.columns")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
Text("Position Log")
|
||||
.font(.title3)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
Divider()
|
||||
}
|
||||
|
||||
if (node.telemetries?.count ?? 0) > 0 {
|
||||
|
||||
NavigationLink {
|
||||
DeviceMetricsLog(node: node)
|
||||
} label: {
|
||||
|
||||
Image(systemName: "flipphone")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
Text("Device Metrics Log")
|
||||
.font(.title3)
|
||||
}
|
||||
Divider()
|
||||
NavigationLink {
|
||||
EnvironmentMetricsLog(node: node)
|
||||
} label: {
|
||||
|
||||
Image(systemName: "chart.xyaxis.line")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
Text("Environment Metrics Log")
|
||||
.font(.title3)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView() {
|
||||
NodeInfoView(node: node)
|
||||
if self.bleManager.connectedPeripheral != nil && node.metadata != nil {
|
||||
|
||||
HStack {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ struct NodeList: View {
|
|||
var body: some View {
|
||||
|
||||
NavigationSplitView {
|
||||
List(nodes, id: \.self, selection: $selection) { node in
|
||||
List(nodes, id: \.self, selection: $selection) { node in
|
||||
if nodes.count == 0 {
|
||||
Text("no.nodes").font(.title)
|
||||
} else {
|
||||
|
|
@ -36,7 +36,7 @@ struct NodeList: View {
|
|||
let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num)
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
CircleText(text: node.user?.shortName ?? "???", color: .accentColor, circleSize: 52, fontSize: 16, brightness: 0.1)
|
||||
CircleText(text: node.user?.shortName ?? "???", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 65, fontSize: 20, brightness: 0.0, textColor: UIColor(hex: UInt32(node.num)).isLight() ? .black : .white)
|
||||
.padding(.trailing, 5)
|
||||
VStack(alignment: .leading) {
|
||||
Text(node.user?.longName ?? NSLocalizedString("unknown", comment: "Unknown")).font(.headline)
|
||||
|
|
@ -64,6 +64,15 @@ struct NodeList: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
if node.channel > 0 {
|
||||
HStack(alignment: .bottom) {
|
||||
Image(systemName: "fibrechannel")
|
||||
.font(.title3)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("Channel: \(node.channel)")
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
HStack(alignment: .bottom) {
|
||||
Image(systemName: "clock.badge.checkmark.fill")
|
||||
.font(.title3)
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@ struct AppSettings: View {
|
|||
Button("Erase all app data?", role: .destructive) {
|
||||
bleManager.disconnectPeripheral()
|
||||
clearCoreDataDatabase(context: context)
|
||||
UserDefaults.standard.reset()
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ struct DeviceConfig: View {
|
|||
Section(header: Text("GPIO")) {
|
||||
|
||||
Picker("Button GPIO", selection: $buttonGPIO) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
@ -110,7 +110,7 @@ struct DeviceConfig: View {
|
|||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("Buzzer GPIO", selection: $buzzerGPIO) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
@ -207,6 +207,7 @@ struct DeviceConfig: View {
|
|||
dc.debugLogEnabled = debugLogEnabled
|
||||
dc.buttonGpio = UInt32(buttonGPIO)
|
||||
dc.buzzerGpio = UInt32(buzzerGPIO)
|
||||
dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue()
|
||||
|
||||
let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
if adminMessageId > 0 {
|
||||
|
|
@ -277,6 +278,13 @@ struct DeviceConfig: View {
|
|||
if newBuzzerGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: rebroadcastMode) { newRebroadcastMode in
|
||||
|
||||
if node != nil && node!.deviceConfig != nil {
|
||||
|
||||
if newRebroadcastMode != node!.deviceConfig!.rebroadcastMode { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
func setDeviceValues() {
|
||||
self.deviceRole = Int(node?.deviceConfig?.role ?? 0)
|
||||
|
|
@ -284,6 +292,7 @@ struct DeviceConfig: View {
|
|||
self.debugLogEnabled = node?.deviceConfig?.debugLogEnabled ?? false
|
||||
self.buttonGPIO = Int(node?.deviceConfig?.buttonGpio ?? 0)
|
||||
self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0)
|
||||
self.rebroadcastMode = Int(node?.deviceConfig?.rebroadcastMode ?? 0)
|
||||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ struct CannedMessagesConfig: View {
|
|||
.disabled(configPreset > 0)
|
||||
Section(header: Text("Inputs")) {
|
||||
Picker("Pin A", selection: $inputbrokerPinA) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
@ -137,7 +137,7 @@ struct CannedMessagesConfig: View {
|
|||
Text("GPIO pin for rotary encoder A port.")
|
||||
.font(.caption)
|
||||
Picker("Pin B", selection: $inputbrokerPinB) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
@ -149,7 +149,7 @@ struct CannedMessagesConfig: View {
|
|||
Text("GPIO pin for rotary encoder B port.")
|
||||
.font(.caption)
|
||||
Picker("Press Pin", selection: $inputbrokerPinPress) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ struct ExternalNotificationConfig: View {
|
|||
Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.")
|
||||
.font(.caption)
|
||||
Picker("Output pin GPIO", selection: $output) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
@ -140,7 +140,7 @@ struct ExternalNotificationConfig: View {
|
|||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
@ -150,7 +150,7 @@ struct ExternalNotificationConfig: View {
|
|||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("Output pin vibra GPIO", selection: $outputVibra) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -67,18 +67,18 @@ struct RangeTestConfig: View {
|
|||
Label("save", systemImage: "square.and.arrow.down.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.disabled(!(node != nil && node!.myInfo?.hasWifi ?? false))
|
||||
.disabled(!(node != nil && node?.metadata?.hasWifi ?? false))
|
||||
Text("Saves a CSV with the range test message details, currently only available on ESP32 devices with a web server.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.positionConfig == nil || !(node != nil && node!.myInfo?.hasWifi ?? false))
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.rangeTestConfig == nil || !(node != nil && node?.metadata?.hasWifi ?? false))
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.disabled(bleManager.connectedPeripheral == nil || !hasChanges || !(node?.myInfo?.hasWifi ?? false))
|
||||
.disabled(bleManager.connectedPeripheral == nil || !hasChanges || !(node?.metadata?.hasWifi ?? false))
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ struct SerialConfig: View {
|
|||
Section(header: Text("GPIO")) {
|
||||
|
||||
Picker("Receive data (rxd) GPIO pin", selection: $rxd) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
@ -109,7 +109,7 @@ struct SerialConfig: View {
|
|||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
Picker("Transmit data (txd) GPIO pin", selection: $txd) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// WiFiConfig.swift
|
||||
// NetworkConfig.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright (c) Garth Vander Houwen 8/1/2022
|
||||
|
|
@ -55,64 +55,68 @@ struct NetworkConfig: View {
|
|||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("WiFi Options (ESP32 Only)")) {
|
||||
Toggle(isOn: $wifiEnabled) {
|
||||
Label("enabled", systemImage: "wifi")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
HStack {
|
||||
Label("ssid", systemImage: "network")
|
||||
TextField("ssid", text: $wifiSsid)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: wifiSsid, perform: { _ in
|
||||
let totalBytes = wifiSsid.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 32 {
|
||||
let firstNBytes = Data(wifiSsid.utf8.prefix(32))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
wifiSsid = maxBytesString
|
||||
if (node != nil && node?.metadata?.hasWifi ?? false) {
|
||||
Section(header: Text("WiFi Options")) {
|
||||
Toggle(isOn: $wifiEnabled) {
|
||||
Label("enabled", systemImage: "wifi")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
HStack {
|
||||
Label("ssid", systemImage: "network")
|
||||
TextField("ssid", text: $wifiSsid)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: wifiSsid, perform: { _ in
|
||||
let totalBytes = wifiSsid.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 32 {
|
||||
let firstNBytes = Data(wifiSsid.utf8.prefix(32))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
wifiSsid = maxBytesString
|
||||
}
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
HStack {
|
||||
Label("password", systemImage: "wallet.pass")
|
||||
TextField("password", text: $wifiPsk)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: wifiPsk, perform: { _ in
|
||||
let totalBytes = wifiPsk.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 63 {
|
||||
let firstNBytes = Data(wifiPsk.utf8.prefix(63))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
wifiPsk = maxBytesString
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
HStack {
|
||||
Label("password", systemImage: "wallet.pass")
|
||||
TextField("password", text: $wifiPsk)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: wifiPsk, perform: { _ in
|
||||
let totalBytes = wifiPsk.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 63 {
|
||||
let firstNBytes = Data(wifiPsk.utf8.prefix(63))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
wifiPsk = maxBytesString
|
||||
}
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
Text("Enabling WiFi will disable the bluetooth connection to the app.")
|
||||
.font(.callout)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
Text("Enabling WiFi will disable the bluetooth connection to the app.")
|
||||
.font(.callout)
|
||||
|
||||
}
|
||||
.disabled(!(node != nil && node!.myInfo?.hasWifi ?? false))
|
||||
Section(header: Text("Ethernet Options")) {
|
||||
Toggle(isOn: $ethEnabled) {
|
||||
Label("enabled", systemImage: "network")
|
||||
if (node != nil && node?.metadata?.hasEthernet ?? false) {
|
||||
Section(header: Text("Ethernet Options")) {
|
||||
Toggle(isOn: $ethEnabled) {
|
||||
Label("enabled", systemImage: "network")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Enabling Ethernet will disable the bluetooth connection to the app.")
|
||||
.font(.callout)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Enabling Ethernet will disable the bluetooth connection to the app.")
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ struct PositionConfig: View {
|
|||
.font(.caption)
|
||||
|
||||
Picker("GPS Receive GPIO", selection: $rxGpio) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
@ -247,7 +247,7 @@ struct PositionConfig: View {
|
|||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
Picker("GPS Transmit GPIO", selection: $txGpio) {
|
||||
ForEach(0..<40) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
"available.radios"="Geräte in der Nähe";
|
||||
"automatic.detection"="Automatische erkennung";
|
||||
"battery.level"="Batterie Ladung";
|
||||
"battery.level.trend"="Batterie Ladungstrend";
|
||||
"ble.name"="BLE Name";"ble.connection.timeout %d %@"="Verbindung nach %d Versuchen zu %@ fehlgeschlagen. Evtl. hilft es, die Verbindung unter Einstellungen > Bluetooth manuell zu löschen.";
|
||||
"ble.connection.timeout %d %@"="Verbindung nach %d Versuchen zu %@ fehlgeschlagen. Evtl. hilft es, die Verbindung unter Einstellungen > Bluetooth manuell zu löschen.";
|
||||
"ble.errorcode.6 %@"="%@ Die App wird automatisch zum präferierten Gerät wiederverbinden, sobald es in Reichweite kommt.";
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
"available.radios"="Available Radios";
|
||||
"automatic.detection"="Automatic Detection";
|
||||
"battery.level"="Battery Level";
|
||||
"battery.level.trend"="Battery Level Trend";
|
||||
"ble.name"="BLE Name";
|
||||
"ble.connection.timeout %d %@"="Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth.";
|
||||
"ble.errorcode.6 %@"="%@ The app will automatically reconnect to the preferred radio if it comes back in range.";
|
||||
|
|
@ -51,7 +50,7 @@
|
|||
"config.save.confirm"="After config values save the node will reboot.";
|
||||
"communicating"="Communicating with device. .";
|
||||
"connected.radio"="Connected Radio";
|
||||
"connected"="Currently Connected";
|
||||
"connected"="Connected";
|
||||
"connecting"="Connecting . .";
|
||||
"contacts"="Contacts";
|
||||
"copy"="Copy";
|
||||
|
|
@ -69,7 +68,7 @@
|
|||
"device.role.repeater"="Repeater - Mesh packets will prefer to be routed over this node. This role eliminates unnecessary overhead such as NodeInfo, DeviceTelemetry, and any other mesh packet, resulting in the device not appearing as part of the network. Please see Rebroadcast Mode for additional settings specific to this role.";
|
||||
"device.role.tracker"="Tracker - For use with devices intended as a GPS tracker. Position packets sent from this device will be higher priority, with position broadcasting every two minutes. Smart Position Broadcast will default to off.";
|
||||
"direct.messages"="Direct Messages";
|
||||
"dismiss.keyboard"="Dismiss Keyboard";
|
||||
"dismiss.keyboard"="Dismiss";
|
||||
"display"="Display (Device Screen)";
|
||||
"display.config"="Display Config";
|
||||
"distance"="Distance";
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
"available.radios"="可用的电台";
|
||||
"automatic.detection"="自动识别";
|
||||
"battery.level"="电池电量";
|
||||
"battery.level.trend"="电池电量趋势";
|
||||
"ble.name"="蓝牙名称";
|
||||
"ble.connection.timeout %d %@"="尝试连接%@失败,你可能需要在系统设置的蓝牙选项中忽略该电台。";
|
||||
"ble.errorcode.6 %@"="%@ 如果在首选电台的旁边,App 将会自动重连。";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue