Support new wifi config, update firmware version modal, prevent connection of unsupported devices for real this time.

This commit is contained in:
Garth Vander Houwen 2022-08-06 08:07:16 -07:00
parent dccefee338
commit af4ef58e14
9 changed files with 403 additions and 111 deletions

View file

@ -150,6 +150,7 @@
DD8ED9C328978D9D00B3B0AB /* MeshtasticDataModel v 5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModel v 5.xcdatamodel"; sourceTree = "<group>"; };
DD8ED9C42898D51F00B3B0AB /* WiFiConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WiFiConfig.swift; sourceTree = "<group>"; };
DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingError.swift; sourceTree = "<group>"; };
DD8ED9C9289EA77E00B3B0AB /* MeshtasticDataModel v 6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModel v 6.xcdatamodel"; sourceTree = "<group>"; };
DD90860A26F645B700DC5189 /* Meshtastic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Meshtastic.entitlements; sourceTree = "<group>"; };
DD90860B26F684AF00DC5189 /* BatteryIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryIcon.swift; sourceTree = "<group>"; };
DD90860D26F69BAE00DC5189 /* NodeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMap.swift; sourceTree = "<group>"; };
@ -1097,13 +1098,14 @@
DD9D8F2D2764403B00080993 /* Meshtastic.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
DD8ED9C9289EA77E00B3B0AB /* MeshtasticDataModel v 6.xcdatamodel */,
DD8ED9C328978D9D00B3B0AB /* MeshtasticDataModel v 5.xcdatamodel */,
DD619373285CC7D600E59241 /* MeshtasticDataModel v 4.xcdatamodel */,
DDB2CC6F27F3F0AC009C5FCC /* MeshtasticDataModel v 3.xcdatamodel */,
DD45C77427BD4EF80011784F /* MeshtasticDataModel v2.xcdatamodel */,
DD9D8F2E2764403B00080993 /* CoreDataSample.xcdatamodel */,
);
currentVersion = DD8ED9C328978D9D00B3B0AB /* MeshtasticDataModel v 5.xcdatamodel */;
currentVersion = DD8ED9C9289EA77E00B3B0AB /* MeshtasticDataModel v 6.xcdatamodel */;
name = Meshtastic.xcdatamodeld;
path = Meshtastic/Meshtastic.xcdatamodeld;
sourceTree = "<group>";

View file

@ -29,7 +29,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
@Published var connectedPeripheral: Peripheral!
@Published var lastConnectionError: String
@Published var lastConnnectionVersion: String
@Published var connectedVersion: String
@Published var isSwitchedOn: Bool = false
@Published var isScanning: Bool = false
@ -77,7 +77,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
//self.meshLoggingEnabled = UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? false
self.lastConnectionError = ""
self.lastConnnectionVersion = "0.0.0"
self.connectedVersion = "0.0.0"
super.init()
// let bleQueue: DispatchQueue = DispatchQueue(label: "CentralManager")
centralManager = CBCentralManager(delegate: self, queue: nil)
@ -159,13 +159,15 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
if meshLoggingEnabled { MeshLogger.log("✅ BLE Connecting: \(peripheral.name ?? "Unknown")") }
stopScanning()
if self.connectedPeripheral != nil {
if meshLoggingEnabled { MeshLogger.log(" BLE Disconnecting from: \(self.connectedPeripheral.name) to connect to \(peripheral.name ?? "Unknown")") }
self.disconnectPeripheral()
}
self.connectedVersion = "0.0.0"
self.centralManager?.connect(peripheral)
// Use a timer to keep track of connecting peripherals, context to pass the radio name with the timer and the RunLoop to prevent
@ -460,15 +462,20 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph
// MyInfo
if decodedInfo.myInfo.isInitialized && decodedInfo.myInfo.myNodeNum > 0 {
let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".")
var version = decodedInfo.myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: decodedInfo.myInfo.firmwareVersion))]
nowKnown = true
connectedVersion = String(version)
let myInfo = myInfoPacket(myInfo: decodedInfo.myInfo, meshLogging: meshLoggingEnabled, context: context!)
if myInfo != nil {
self.connectedPeripheral.bitrate = myInfo!.bitrate
self.connectedPeripheral.num = myInfo!.myNodeNum
lastConnnectionVersion = myInfo?.firmwareVersion ?? myInfo!.firmwareVersion ?? "Unknown"
self.connectedPeripheral.firmwareVersion = myInfo!.firmwareVersion ?? "Unknown"
self.connectedPeripheral.name = myInfo!.bleName ?? "Unknown"
self.connectedPeripheral.longName = myInfo!.bleName ?? "Unknown"

View file

@ -377,15 +377,13 @@ func localConfig (config: Config, meshlogging: Bool, context:NSManagedObjectCont
newWiFiConfig.ssid = ""
newWiFiConfig.password = ""
newWiFiConfig.apMode = false
newWiFiConfig.apHidden = false
newWiFiConfig.mode = 0
} else {
newWiFiConfig.ssid = config.wifi.ssid
newWiFiConfig.password = config.wifi.psk
newWiFiConfig.apMode = config.wifi.apMode
newWiFiConfig.apHidden = config.wifi.apHidden
newWiFiConfig.mode = Int32(config.wifi.mode.rawValue)
}
newWiFiConfig.num = fetchedNode[0].num
fetchedNode[0].wiFiConfig = newWiFiConfig
@ -396,15 +394,13 @@ func localConfig (config: Config, meshlogging: Bool, context:NSManagedObjectCont
fetchedNode[0].wiFiConfig?.ssid = ""
fetchedNode[0].wiFiConfig?.password = ""
fetchedNode[0].wiFiConfig?.apMode = false
fetchedNode[0].wiFiConfig?.apHidden = false
fetchedNode[0].wiFiConfig?.mode = 0
} else {
fetchedNode[0].wiFiConfig?.ssid = config.wifi.ssid
fetchedNode[0].wiFiConfig?.password = config.wifi.psk
fetchedNode[0].wiFiConfig?.apMode = config.wifi.apMode
fetchedNode[0].wiFiConfig?.apHidden = config.wifi.apHidden
fetchedNode[0].wiFiConfig?.mode = Int32(config.wifi.mode.rawValue)
}
}

View file

@ -4,7 +4,7 @@
<dict>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:meshtastic.org/E/*</string>
<string>applinks:meshtastic.org/e/*</string>
</array>
<key>com.apple.security.app-sandbox</key>
<true/>

View file

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>MeshtasticDataModel v 5.xcdatamodel</string>
<string>MeshtasticDataModel v 6.xcdatamodel</string>
</dict>
</plist>

View file

@ -0,0 +1,221 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21G72" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<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="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<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="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" 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="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" 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"/>
<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="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
<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="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" 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="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="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="fromUser" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
<relationship name="toUser" 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="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
<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="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<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="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
<attribute name="bleName" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="lastHeard" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<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="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
<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="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"/>
<relationship name="wiFiConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="WiFiConfigEntity" inverseName="wiFiConfigNode" inverseEntity="WiFiConfigEntity"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="num"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
<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="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" 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="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="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="num" attributeType="Integer 32" defaultValueString="0" 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="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="num" attributeType="Integer 64" 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"/>
<attribute name="num" attributeType="Integer 64" 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="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="shortName" attributeType="String"/>
<attribute name="team" optional="YES" 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="(toUser.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 isEmoji == false AND admin = false"/>
</fetchedProperty>
</entity>
<entity name="WiFiConfigEntity" representedClassName="WiFiConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="password" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
<attribute name="ssid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
<relationship name="wiFiConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="wiFiConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<elements>
<element name="CannedMessageConfigEntity" positionX="45" positionY="144" width="128" height="209"/>
<element name="DeviceConfigEntity" positionX="45" positionY="144" width="128" height="104"/>
<element name="DisplayConfigEntity" positionX="54" positionY="153" width="128" height="104"/>
<element name="ExternalNotificationConfigEntity" positionX="63" positionY="162" width="128" height="149"/>
<element name="LoRaConfigEntity" positionX="45" positionY="144" width="128" height="119"/>
<element name="MessageEntity" positionX="-36" positionY="63" width="128" height="245"/>
<element name="MyInfoEntity" positionX="-18" positionY="81" width="128" height="209"/>
<element name="NodeInfoEntity" positionX="-63" positionY="-18" width="128" height="314"/>
<element name="PositionConfigEntity" positionX="63" positionY="162" width="128" height="149"/>
<element name="PositionEntity" positionX="-54" positionY="54" width="128" height="119"/>
<element name="RangeTestConfigEntity" positionX="72" positionY="171" width="128" height="104"/>
<element name="SerialConfigEntity" positionX="54" positionY="153" width="128" height="164"/>
<element name="TelemetryConfigEntity" positionX="72" positionY="171" width="128" height="134"/>
<element name="TelemetryEntity" positionX="160" positionY="192" width="128" height="209"/>
<element name="UserEntity" positionX="0" positionY="144" width="128" height="230"/>
<element name="WiFiConfigEntity" positionX="45" positionY="144" width="128" height="119"/>
</elements>
</model>

View file

@ -509,6 +509,15 @@ struct Config {
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
// methods supported on all messages.
///
/// Enable WiFi (disables Bluetooth)
var enabled: Bool = false
///
/// If set, this node will try to join the specified wifi network and
/// acquire an address via DHCP
var mode: Config.WiFiConfig.WiFiMode = .client
///
/// If set, this node will try to join the specified wifi network and
/// acquire an address via DHCP
@ -518,21 +527,48 @@ struct Config {
/// If set, will be use to authenticate to the named wifi
var psk: String = String()
///
/// If set, the node will operate as an AP (and DHCP server), otherwise it
/// will be a station
var apMode: Bool = false
///
/// If set, the node AP will broadcast as a hidden SSID
var apHidden: Bool = false
///
/// If set, wifi is enabled. Previously done through setting ssid and psk
var enabled: Bool = false
var unknownFields = SwiftProtobuf.UnknownStorage()
enum WiFiMode: SwiftProtobuf.Enum {
typealias RawValue = Int
///
/// This mode is used to connect to an external WiFi network
case client // = 0
///
/// In this mode the node will operate as an AP (and DHCP server)
case accessPoint // = 1
///
/// If set, the node AP will broadcast as a hidden SSID
case accessPointHidden // = 2
case UNRECOGNIZED(Int)
init() {
self = .client
}
init?(rawValue: Int) {
switch rawValue {
case 0: self = .client
case 1: self = .accessPoint
case 2: self = .accessPointHidden
default: self = .UNRECOGNIZED(rawValue)
}
}
var rawValue: Int {
switch self {
case .client: return 0
case .accessPoint: return 1
case .accessPointHidden: return 2
case .UNRECOGNIZED(let i): return i
}
}
}
init() {}
}
@ -557,6 +593,11 @@ struct Config {
/// Potentially useful for devices without user buttons.
var autoScreenCarouselSecs: UInt32 = 0
///
/// If this is set, the displayed compass will always point north. if unset, the old behaviour
/// (top of display is heading direction) is used.
var compassNorthTop: Bool = false
var unknownFields = SwiftProtobuf.UnknownStorage()
///
@ -920,6 +961,15 @@ extension Config.PowerConfig.ChargeCurrent: CaseIterable {
]
}
extension Config.WiFiConfig.WiFiMode: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.WiFiConfig.WiFiMode] = [
.client,
.accessPoint,
.accessPointHidden,
]
}
extension Config.DisplayConfig.GpsCoordinateFormat: CaseIterable {
// The compiler won't synthesize support with the UNRECOGNIZED case.
static var allCases: [Config.DisplayConfig.GpsCoordinateFormat] = [
@ -976,6 +1026,7 @@ extension Config.PositionConfig.PositionFlags: @unchecked Sendable {}
extension Config.PowerConfig: @unchecked Sendable {}
extension Config.PowerConfig.ChargeCurrent: @unchecked Sendable {}
extension Config.WiFiConfig: @unchecked Sendable {}
extension Config.WiFiConfig.WiFiMode: @unchecked Sendable {}
extension Config.DisplayConfig: @unchecked Sendable {}
extension Config.DisplayConfig.GpsCoordinateFormat: @unchecked Sendable {}
extension Config.LoRaConfig: @unchecked Sendable {}
@ -1381,11 +1432,10 @@ extension Config.PowerConfig.ChargeCurrent: SwiftProtobuf._ProtoNameProviding {
extension Config.WiFiConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = Config.protoMessageName + ".WiFiConfig"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .same(proto: "ssid"),
2: .same(proto: "psk"),
3: .standard(proto: "ap_mode"),
4: .standard(proto: "ap_hidden"),
5: .same(proto: "enabled"),
1: .same(proto: "enabled"),
2: .same(proto: "mode"),
3: .same(proto: "ssid"),
4: .same(proto: "psk"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -1394,52 +1444,56 @@ extension Config.WiFiConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem
// allocates stack space for every case branch when no optimizations are
// enabled. https://github.com/apple/swift-protobuf/issues/1034
switch fieldNumber {
case 1: try { try decoder.decodeSingularStringField(value: &self.ssid) }()
case 2: try { try decoder.decodeSingularStringField(value: &self.psk) }()
case 3: try { try decoder.decodeSingularBoolField(value: &self.apMode) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.apHidden) }()
case 5: try { try decoder.decodeSingularBoolField(value: &self.enabled) }()
case 1: try { try decoder.decodeSingularBoolField(value: &self.enabled) }()
case 2: try { try decoder.decodeSingularEnumField(value: &self.mode) }()
case 3: try { try decoder.decodeSingularStringField(value: &self.ssid) }()
case 4: try { try decoder.decodeSingularStringField(value: &self.psk) }()
default: break
}
}
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if self.enabled != false {
try visitor.visitSingularBoolField(value: self.enabled, fieldNumber: 1)
}
if self.mode != .client {
try visitor.visitSingularEnumField(value: self.mode, fieldNumber: 2)
}
if !self.ssid.isEmpty {
try visitor.visitSingularStringField(value: self.ssid, fieldNumber: 1)
try visitor.visitSingularStringField(value: self.ssid, fieldNumber: 3)
}
if !self.psk.isEmpty {
try visitor.visitSingularStringField(value: self.psk, fieldNumber: 2)
}
if self.apMode != false {
try visitor.visitSingularBoolField(value: self.apMode, fieldNumber: 3)
}
if self.apHidden != false {
try visitor.visitSingularBoolField(value: self.apHidden, fieldNumber: 4)
}
if self.enabled != false {
try visitor.visitSingularBoolField(value: self.enabled, fieldNumber: 5)
try visitor.visitSingularStringField(value: self.psk, fieldNumber: 4)
}
try unknownFields.traverse(visitor: &visitor)
}
static func ==(lhs: Config.WiFiConfig, rhs: Config.WiFiConfig) -> Bool {
if lhs.enabled != rhs.enabled {return false}
if lhs.mode != rhs.mode {return false}
if lhs.ssid != rhs.ssid {return false}
if lhs.psk != rhs.psk {return false}
if lhs.apMode != rhs.apMode {return false}
if lhs.apHidden != rhs.apHidden {return false}
if lhs.enabled != rhs.enabled {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}
}
extension Config.WiFiConfig.WiFiMode: SwiftProtobuf._ProtoNameProviding {
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
0: .same(proto: "Client"),
1: .same(proto: "AccessPoint"),
2: .same(proto: "AccessPointHidden"),
]
}
extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
static let protoMessageName: String = Config.protoMessageName + ".DisplayConfig"
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
1: .standard(proto: "screen_on_secs"),
2: .standard(proto: "gps_format"),
3: .standard(proto: "auto_screen_carousel_secs"),
4: .standard(proto: "compass_north_top"),
]
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
@ -1451,6 +1505,7 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.screenOnSecs) }()
case 2: try { try decoder.decodeSingularEnumField(value: &self.gpsFormat) }()
case 3: try { try decoder.decodeSingularUInt32Field(value: &self.autoScreenCarouselSecs) }()
case 4: try { try decoder.decodeSingularBoolField(value: &self.compassNorthTop) }()
default: break
}
}
@ -1466,6 +1521,9 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
if self.autoScreenCarouselSecs != 0 {
try visitor.visitSingularUInt32Field(value: self.autoScreenCarouselSecs, fieldNumber: 3)
}
if self.compassNorthTop != false {
try visitor.visitSingularBoolField(value: self.compassNorthTop, fieldNumber: 4)
}
try unknownFields.traverse(visitor: &visitor)
}
@ -1473,6 +1531,7 @@ extension Config.DisplayConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImp
if lhs.screenOnSecs != rhs.screenOnSecs {return false}
if lhs.gpsFormat != rhs.gpsFormat {return false}
if lhs.autoScreenCarouselSecs != rhs.autoScreenCarouselSecs {return false}
if lhs.compassNorthTop != rhs.compassNorthTop {return false}
if lhs.unknownFields != rhs.unknownFields {return false}
return true
}

View file

@ -16,15 +16,12 @@ struct Connect: View {
@EnvironmentObject var bleManager: BLEManager
@EnvironmentObject var userSettings: UserSettings
@State private var showingVersionSheet = false
@State var initialLoad: Bool = true
@State var isPreferredRadio: Bool = false
@State var firmwareVersion = "0.0.0"
@State var minimumVersion = "1.3.30"
@State var minimumVersion = "1.3.36"
@State var invalidVersion = false
var body: some View {
@ -267,23 +264,18 @@ struct Connect: View {
InvalidVersion(errorText: "1.3 ALPHA PREVIEW this version of the app supports only version \(minimumVersion) and above. Your device has been disconnected.")
}
.onChange(of: firmwareVersion) { iv in
.onChange(of: (self.bleManager.connectedVersion)) { ic in
bleManager.disconnectPeripheral()
}
.onChange(of: self.bleManager.isConnected) { ic in
firmwareVersion = bleManager.lastConnnectionVersion
firmwareVersion = self.bleManager.connectedVersion
let supportedVersion = firmwareVersion == "0.0.0" || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedSame
invalidVersion = !supportedVersion
if invalidVersion {
bleManager.disconnectPeripheral()
}
}
.onAppear(perform: {

View file

@ -7,6 +7,40 @@
import SwiftUI
enum WiFiModes: Int, CaseIterable, Identifiable {
case client = 0
case accessPoint = 1
case accessPointHidden = 2
var id: Int { self.rawValue }
var description: String {
get {
switch self {
case .client:
return "Client"
case .accessPoint:
return "Software Access Point"
case .accessPointHidden:
return "Software Access Point (Hidden)"
}
}
}
func protoEnumValue() -> Config.WiFiConfig.WiFiMode {
switch self {
case .client:
return Config.WiFiConfig.WiFiMode.client
case .accessPoint:
return Config.WiFiConfig.WiFiMode.accessPoint
case .accessPointHidden:
return Config.WiFiConfig.WiFiMode.accessPointHidden
}
}
}
struct WiFiConfig: View {
@Environment(\.managedObjectContext) var context
@ -19,12 +53,9 @@ struct WiFiConfig: View {
@State var hasChanges: Bool = false
@State var enabled = false
@State var ssid = ""
@State var password = ""
@State var apMode = false
@State var apHidden = false
@State var mode = 0
var body: some View {
@ -39,22 +70,35 @@ struct WiFiConfig: View {
Toggle(isOn: $enabled) {
Label("Enable", systemImage: "wifi")
Label("Enabled", systemImage: "wifi")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
Picker("Mode", selection: $mode ) {
ForEach(WiFiModes.allCases) { lu in
Text(lu.description)
}
}
.pickerStyle(DefaultPickerStyle())
}
Section(header: Text("SSID & Password")) {
HStack {
Label("SSID", systemImage: "network")
TextField("SSID", text: $ssid)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: ssid, perform: { value in
let totalBytes = ssid.utf8.count
// Only mess with the value if it is too big
if totalBytes > 30 {
if totalBytes > 32 {
let firstNBytes = Data(ssid.utf8.prefix(30))
let firstNBytes = Data(ssid.utf8.prefix(32))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
@ -66,20 +110,22 @@ struct WiFiConfig: View {
.foregroundColor(.gray)
}
.keyboardType(.default)
.disableAutocorrection(true)
HStack {
Label("Password", systemImage: "wallet.pass")
TextField("Password", text: $password)
.foregroundColor(.gray)
.autocapitalization(.none)
.disableAutocorrection(true)
.onChange(of: password, perform: { value in
let totalBytes = password.utf8.count
// Only mess with the value if it is too big
if totalBytes > 60 {
if totalBytes > 63 {
let firstNBytes = Data(ssid.utf8.prefix(60))
let firstNBytes = Data(ssid.utf8.prefix(63))
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
@ -91,28 +137,6 @@ struct WiFiConfig: View {
.foregroundColor(.gray)
}
.keyboardType(.default)
.disableAutocorrection(true)
}
Section(header: Text("Sofware Access Point")) {
Text("WiFi uses client mode by default, if Software Access Point(AP) is on the SSID and password will be used to access the AP at meshtastic.local.")
.font(.caption)
Toggle(isOn: $apMode) {
Label("Soft AP Mode", systemImage: "externaldrive.fill.badge.wifi")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if apMode {
Toggle(isOn: $apHidden) {
Label("Hidden SSID", systemImage: "eye.slash")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
}
.disabled(!(node != nil && node!.myInfo?.hasWifi ?? false))
@ -141,8 +165,7 @@ struct WiFiConfig: View {
wifi.enabled = self.enabled
wifi.ssid = self.ssid
wifi.psk = self.password
wifi.apMode = self.apMode
wifi.apHidden = self.apHidden
wifi.mode = WiFiModes(rawValue: self.mode)?.protoEnumValue() ?? WiFiModes.client.protoEnumValue()
let adminMessageId = bleManager.saveWiFiConfig(config: wifi, fromUser: node!.user!, toUser: node!.user!, wantResponse: true)
@ -174,9 +197,8 @@ struct WiFiConfig: View {
self.enabled = (node!.wiFiConfig?.enabled ?? false)
self.ssid = node!.wiFiConfig?.ssid ?? ""
self.password = node!.wiFiConfig?.password ?? ""
self.apMode = (node!.wiFiConfig?.apMode ?? false)
self.apHidden = (node!.wiFiConfig?.apHidden ?? false)
self.mode = Int(node!.wiFiConfig?.mode ?? 0)
self.hasChanges = false
self.initialLoad = false
}
@ -202,18 +224,11 @@ struct WiFiConfig: View {
if newPassword != node!.wiFiConfig!.password { hasChanges = true }
}
}
.onChange(of: apMode) { newApMode in
.onChange(of: mode) { newMode in
if node != nil && node!.wiFiConfig != nil {
if newApMode != node!.wiFiConfig!.apMode { hasChanges = true }
}
}
.onChange(of: apHidden) { newApHidden in
if node != nil && node!.wiFiConfig != nil {
if newApHidden != node!.wiFiConfig!.apHidden { hasChanges = true }
if newMode != node!.wiFiConfig!.mode { hasChanges = true }
}
}
.navigationViewStyle(StackNavigationViewStyle())